edoxen 0.7.2 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +17 -1
  3. data/.rubocop_todo.yml +1 -1
  4. data/CHANGELOG.md +246 -0
  5. data/README.adoc +308 -153
  6. data/edoxen.gemspec +10 -10
  7. data/lib/edoxen/action.rb +1 -1
  8. data/lib/edoxen/agenda.rb +4 -7
  9. data/lib/edoxen/agenda_item.rb +10 -3
  10. data/lib/edoxen/approval.rb +1 -1
  11. data/lib/edoxen/attendance.rb +16 -10
  12. data/lib/edoxen/body_vocabulary_entry.rb +24 -0
  13. data/lib/edoxen/body_vocabulary_host.rb +29 -0
  14. data/lib/edoxen/cli.rb +117 -120
  15. data/lib/edoxen/component_localization.rb +11 -0
  16. data/lib/edoxen/consideration.rb +2 -2
  17. data/lib/edoxen/contact.rb +24 -0
  18. data/lib/edoxen/contact_identifier.rb +13 -0
  19. data/lib/edoxen/contact_method.rb +16 -0
  20. data/lib/edoxen/decision.rb +69 -0
  21. data/lib/edoxen/decision_collection.rb +10 -0
  22. data/lib/edoxen/decision_date.rb +9 -0
  23. data/lib/edoxen/{resolution_metadata.rb → decision_metadata.rb} +5 -6
  24. data/lib/edoxen/decision_relation.rb +11 -0
  25. data/lib/edoxen/entity_ref.rb +74 -0
  26. data/lib/edoxen/enums.rb +117 -9
  27. data/lib/edoxen/error.rb +1 -1
  28. data/lib/edoxen/extension_attribute.rb +78 -0
  29. data/lib/edoxen/host_ref.rb +1 -1
  30. data/lib/edoxen/link_checker.rb +8 -8
  31. data/lib/edoxen/localization.rb +2 -2
  32. data/lib/edoxen/localization_host.rb +23 -0
  33. data/lib/edoxen/meeting.rb +63 -39
  34. data/lib/edoxen/meeting_collection.rb +1 -2
  35. data/lib/edoxen/meeting_collection_metadata.rb +3 -1
  36. data/lib/edoxen/meeting_component.rb +41 -0
  37. data/lib/edoxen/meeting_extension.rb +25 -0
  38. data/lib/edoxen/meeting_identifier.rb +1 -1
  39. data/lib/edoxen/meeting_localization.rb +6 -1
  40. data/lib/edoxen/meeting_series.rb +21 -0
  41. data/lib/edoxen/minutes.rb +2 -2
  42. data/lib/edoxen/minutes_section.rb +8 -11
  43. data/lib/edoxen/motion.rb +51 -0
  44. data/lib/edoxen/name.rb +29 -0
  45. data/lib/edoxen/officer.rb +19 -0
  46. data/lib/edoxen/officers_host.rb +19 -0
  47. data/lib/edoxen/person.rb +5 -9
  48. data/lib/edoxen/physical_venue.rb +15 -0
  49. data/lib/edoxen/recurrence.rb +27 -0
  50. data/lib/edoxen/recurrence_by_day.rb +10 -0
  51. data/lib/edoxen/reference_data.rb +35 -41
  52. data/lib/edoxen/schema_validator.rb +2 -3
  53. data/lib/edoxen/source_url.rb +3 -3
  54. data/lib/edoxen/structured_identifier.rb +2 -2
  55. data/lib/edoxen/topic.rb +37 -0
  56. data/lib/edoxen/topic_asset.rb +14 -0
  57. data/lib/edoxen/topic_document.rb +16 -0
  58. data/lib/edoxen/venue.rb +80 -0
  59. data/lib/edoxen/venue_validator.rb +56 -0
  60. data/lib/edoxen/version.rb +1 -1
  61. data/lib/edoxen/virtual_venue.rb +15 -0
  62. data/lib/edoxen/vote_record.rb +17 -13
  63. data/lib/edoxen/voting.rb +45 -0
  64. data/lib/edoxen/voting_counts.rb +23 -0
  65. data/lib/edoxen.rb +40 -13
  66. data/schema/edoxen.yaml +792 -145
  67. data/schema/meeting.yaml +600 -142
  68. data/sig/edoxen.rbs +581 -1
  69. metadata +57 -20
  70. data/lib/edoxen/_metadata.rb.deprecated +0 -57
  71. data/lib/edoxen/location.rb +0 -15
  72. data/lib/edoxen/resolution.rb +0 -43
  73. data/lib/edoxen/resolution_collection.rb +0 -10
  74. data/lib/edoxen/resolution_date.rb +0 -11
  75. data/lib/edoxen/resolution_relation.rb +0 -11
  76. data/lib/edoxen/resolution_set.rb +0 -17
  77. data/lib/edoxen/schedule_item.rb +0 -27
  78. data/lib/edoxen/schedule_item_localization.rb +0 -21
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # MeetingComponent — flat sub-event of a Meeting.
5
+ # Replaces v0.x ScheduleItem. Component kinds include both
6
+ # substantive (track, session, debate, breakout, bof, keynote) and
7
+ # procedural (opening, closing, break, reception, registration).
8
+ #
9
+ # Flat by design (no nesting) per 2026-07 architectural decision.
10
+ class MeetingComponent < Lutaml::Model::Serializable
11
+ include OfficersHost
12
+
13
+ attribute :identifier, :string
14
+ attribute :urn, :string
15
+ attribute :kind, :string, values: Enums::COMPONENT_KIND
16
+ # v2.1 (TODO.refactor/46): free-form body-specific label (e.g.
17
+ # "Working Group Session", "Public Bill Committee Stage"). Resolves
18
+ # to a short canonical value via the parent collection's vocabulary.
19
+ attribute :body_type, :string
20
+ attribute :title, :string
21
+ attribute :description, :string
22
+ attribute :starts_at, :date_time
23
+ attribute :ends_at, :date_time
24
+ # Free-form time display (e.g. "9:00–10:30") for schedules where
25
+ # exact ISO timestamps aren't available or timezone is unknown.
26
+ attribute :time_label, :string
27
+ attribute :venue_refs, :string, collection: true
28
+ attribute :officers, Officer, collection: true
29
+ attribute :agenda_ref, :string
30
+ attribute :minutes_ref, :string
31
+ attribute :attendance_refs, :string, collection: true
32
+ attribute :localizations, ComponentLocalization, collection: true
33
+ attribute :extensions, MeetingExtension, collection: true
34
+
35
+ def duration_seconds
36
+ return nil unless starts_at && ends_at
37
+
38
+ ends_at.to_time - starts_at.to_time
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # MeetingExtension — the profile mechanism (ISO 8601-2 §15).
5
+ # Every core entity has an `extensions: MeetingExtension[0..*]` slot.
6
+ # Adopters register their own profile namespace (e.g. "legco",
7
+ # "us-congress", "ietf") and define `kind` values within it.
8
+ #
9
+ # Field semantics (tightened v2.1, per TODO.refactor/47):
10
+ #
11
+ # profile — the profile namespace (lowercase, hyphen-separated).
12
+ # kind — discriminator within the profile.
13
+ # ref — URN of the profile document this extension references.
14
+ # attributes — typed key/value pairs (ExtensionAttribute).
15
+ #
16
+ # Recursion (`extensions: MeetingExtension[0..*]`) was removed in
17
+ # v2.1 — no documented use case. Profiles needing nesting can encode
18
+ # it via dotted keys ("vote.count", "vote.method") in `attributes[]`.
19
+ class MeetingExtension < Lutaml::Model::Serializable
20
+ attribute :profile, :string
21
+ attribute :kind, :string
22
+ attribute :ref, :string
23
+ attribute :attributes, ExtensionAttribute, collection: true
24
+ end
25
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Edoxen
4
4
  # Identifier of a meeting (venue + date). Singular — the meeting a
5
- # particular Resolution belongs to.
5
+ # particular Decision belongs to.
6
6
  class MeetingIdentifier < Lutaml::Model::Serializable
7
7
  attribute :venue, :string
8
8
  attribute :date, :date
@@ -2,9 +2,14 @@
2
2
 
3
3
  module Edoxen
4
4
  # Per-language content for a Meeting. Mirrors the glossarist
5
- # pattern used by `Localization` for Resolutions: language-agnostic
5
+ # pattern used by `Localization` for Decisions: language-agnostic
6
6
  # admin fields live on the parent Meeting; per-language content
7
7
  # lives here.
8
+ #
9
+ # Structured practical info (accommodation options, transport details,
10
+ # documents notes) goes via MeetingExtension on the parent Meeting —
11
+ # the profile mechanism handles any body-specific structured data
12
+ # without core model changes.
8
13
  class MeetingLocalization < Lutaml::Model::Serializable
9
14
  attribute :language_code, :string
10
15
  attribute :script, :string
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # MeetingSeries — parent of recurring Meeting instances.
5
+ # Crossref `proceedings-series`, IETF meeting series, Apache Board
6
+ # meetings, etc. Carries a `Recurrence` (ISO 8601-2 §13) rule and
7
+ # a list of member Meeting URNs.
8
+ class MeetingSeries < Lutaml::Model::Serializable
9
+ attribute :identifier, StructuredIdentifier, collection: true
10
+ attribute :urn, :string
11
+ attribute :name, :string
12
+ attribute :description, :string
13
+ attribute :recurrence, Recurrence
14
+ attribute :term, :string
15
+ attribute :contact, Contact
16
+ attribute :hosts, HostRef, collection: true
17
+ attribute :kind, :string
18
+ attribute :meeting_refs, :string, collection: true
19
+ attribute :extensions, MeetingExtension, collection: true
20
+ end
21
+ end
@@ -7,11 +7,11 @@ module Edoxen
7
7
  # TODO.meeting-agenda/10-future-enhancements.md).
8
8
  #
9
9
  # Distinct from Agenda (the business order drafted before the
10
- # meeting) and from ResolutionCollection (the formal decisions
10
+ # meeting) and from DecisionCollection (the formal decisions
11
11
  # adopted). The Minutes are the running record.
12
12
  #
13
13
  # Sections are typically keyed by agenda-item number so consumers
14
- # can join Minutes ↔ AgendaItem ↔ Resolution by `number`/`label`.
14
+ # can join Minutes ↔ AgendaItem ↔ Decision by `number`/`label`.
15
15
  class Minutes < Lutaml::Model::Serializable
16
16
  attribute :identifier, StructuredIdentifier, collection: true
17
17
  attribute :urn, :string
@@ -5,6 +5,14 @@ module Edoxen
5
5
  # item by its `number` field. Carries the narrative as Markdown
6
6
  # (the format the GLM-OCR pipeline emits) plus optional page range
7
7
  # for provenance back to the source PDF.
8
+ #
9
+ # Localization lives on the parent Minutes document, not on each
10
+ # section — Minutes has a per-document language_code; the section
11
+ # lookup is `Minutes#find_section(number)`. Earlier copies of this
12
+ # class inherited `in_language`/`primary_localization` from
13
+ # ScheduleItem/Meeting, but those methods referenced an undeclared
14
+ # `localizations` attribute and raised NameError on every call.
15
+ # Removed 2026-07-05.
8
16
  class MinutesSection < Lutaml::Model::Serializable
9
17
  attribute :number, :string
10
18
  attribute :title, :string
@@ -12,16 +20,5 @@ module Edoxen
12
20
  attribute :page_start, :integer
13
21
  attribute :page_end, :integer
14
22
  attribute :references, Reference, collection: true
15
-
16
- def in_language(code, fallback: false)
17
- match = localizations&.find { |loc| loc.language_code == code.to_s }
18
- return match if match
19
-
20
- fallback ? localizations&.first : nil
21
- end
22
-
23
- def primary_localization
24
- in_language("eng", fallback: true)
25
- end
26
23
  end
27
24
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Motion — a procedural act that brings a Decision. "I move that..."
5
+ # Distinct from a Decision (formal outcome) and from a TopicDocument
6
+ # (written text). A Motion does NOT require any document.
7
+ #
8
+ # State machine (MotionStatus):
9
+ # introduced → seconded → debating → question_put → voting
10
+ # ├── carried → resulting_decision set
11
+ # ├── negatived
12
+ # ├── withdrawn
13
+ # └── lapsed
14
+ class Motion < Lutaml::Model::Serializable
15
+ attribute :identifier, :string
16
+ attribute :urn, :string
17
+ attribute :text, :string
18
+ attribute :mover, Person
19
+ attribute :seconders, Person, collection: true
20
+ attribute :status, :string, values: Enums::MOTION_STATUS
21
+ attribute :introduced_at, :date_time
22
+ attribute :proposed_decision, :string
23
+ attribute :resulting_decision, :string
24
+ # Pilot EntityRef field (v2.1, TODO.refactor/44). Parallel to
25
+ # `resulting_decision` (String). Prefer the typed form in new code;
26
+ # the bare String form is removed in v3.0.
27
+ attribute :resulting_decision_ref, EntityRef
28
+ attribute :votings, :string, collection: true
29
+ attribute :extensions, MeetingExtension, collection: true
30
+
31
+ def carried?
32
+ status == "carried"
33
+ end
34
+
35
+ def pending?
36
+ status && !Enums::MOTION_TERMINAL.include?(status)
37
+ end
38
+
39
+ # --- v2.1 derivation accessor (TODO.refactor/45) --------------------
40
+ # Storage side: Voting.on_motion (SSOT for the Motion→Voting
41
+ # relationship). This computed method returns the Voting instances
42
+ # in `meeting` whose `on_motion` points at this Motion's URN.
43
+ # The stored `votings[]` field remains on the wire for back-compat
44
+ # through v2.x; v3.0 removes it and this becomes the only path.
45
+ def votings_in(meeting:)
46
+ return [] unless meeting && urn
47
+
48
+ meeting.votings.select { |voting| voting.on_motion == urn }
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Structured personal/organisational name. VCARD conventions
5
+ # (RFC 6350): separate structured components (N) from a pre-formatted
6
+ # display string (FN). Either or both may be populated.
7
+ #
8
+ # `formatted` is the display string when callers have it pre-built;
9
+ # the structured fields (`family`, `given`, `additional`, `prefix`,
10
+ # `suffix`) let adopters store parsed components for sorting, indexing,
11
+ # or locale-aware rendering.
12
+ class Name < Lutaml::Model::Serializable
13
+ attribute :formatted, :string
14
+ attribute :family, :string
15
+ attribute :given, :string
16
+ attribute :additional, :string
17
+ attribute :prefix, :string
18
+ attribute :suffix, :string
19
+ attribute :extensions, MeetingExtension, collection: true
20
+
21
+ # Convenience: returns `formatted` if present, else builds one from
22
+ # the structured components. Useful for callers that always want a
23
+ # display string regardless of how the name was authored.
24
+ def display
25
+ formatted || [prefix, given, additional, family, suffix]
26
+ .reject { |s| s.to_s.empty? }.join(" ")
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Officer — a person holding a structural role in a Meeting.
5
+ # Replaces v0.x Meeting#chair and Meeting#secretary direct shortcuts.
6
+ # One list, role discriminates. Open for adopter extension via `other`.
7
+ class Officer < Lutaml::Model::Serializable
8
+ attribute :role, :string, values: Enums::OFFICER_ROLE
9
+ attribute :person, Person
10
+ attribute :term_start, :date
11
+ attribute :term_end, :date
12
+ attribute :extensions, MeetingExtension, collection: true
13
+
14
+ def current?(date = Date.today)
15
+ (term_start.nil? || date >= term_start) &&
16
+ (term_end.nil? || date <= term_end)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Mixed into entities that carry an `officers` collection. Provides
5
+ # `officers_with_role(role)` and the `chair` accessor so Meeting and
6
+ # MeetingComponent share a single implementation.
7
+ #
8
+ # The including class must declare an `officers` attribute whose
9
+ # entries expose `role` and `person`.
10
+ module OfficersHost
11
+ def officers_with_role(role)
12
+ (officers || []).select { |o| o.role == role.to_s }
13
+ end
14
+
15
+ def chair
16
+ officers_with_role("chair").first&.person
17
+ end
18
+ end
19
+ end
data/lib/edoxen/person.rb CHANGED
@@ -1,14 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Edoxen
4
- # Identity + role + affiliation + contact. Used for meeting officers
5
- # (chair, secretary, local contact) and host-ref contacts.
6
- class Person < Lutaml::Model::Serializable
7
- attribute :name, :string
8
- attribute :role, :string
9
- attribute :affiliation, :string
10
- attribute :email, :string
11
- attribute :phone, :string
12
- attribute :orcid, :string
4
+ # A Contact that is specifically an individual human. Inherits all
5
+ # Contact fields. The old `email`, `phone`, and `orcid` fields are
6
+ # replaced by entries in `contact_methods` (kind=email / kind=phone)
7
+ # and `identifiers` (kind=orcid).
8
+ class Person < Contact
13
9
  end
14
10
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # PhysicalVenue — type marker for a Venue with kind == "physical".
5
+ #
6
+ # The wire format is flat on Venue (all fields live there). This
7
+ # subclass exists for type-checking and to host physical-only helpers
8
+ # so callers can ask `venue.is_a?(PhysicalVenue)` and validators
9
+ # can dispatch by type.
10
+ class PhysicalVenue < Venue
11
+ def initialize(attributes = {})
12
+ super(attributes.merge(kind: "physical"))
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Recurrence — structured representation of ISO 8601-2 §13 recurring
5
+ # time intervals. QUERYABLE (each BYxxx part is its own attribute).
6
+ #
7
+ # Round-trip to/from ISO 8601-2 wire string is handled by adapters
8
+ # in ReferenceData (NOT hand-rolled to_h — declared via lutaml-model
9
+ # mapping).
10
+ class Recurrence < Lutaml::Model::Serializable
11
+ attribute :freq, :string, values: Enums::RECURRENCE_FREQ
12
+ attribute :interval, :integer, default: 1
13
+ attribute :count, :integer
14
+ attribute :until, :date_time
15
+ attribute :by_day, RecurrenceByDay, collection: true
16
+ attribute :by_month_day, :integer, collection: true
17
+ attribute :by_month, :integer, collection: true
18
+ attribute :by_week_no, :integer, collection: true
19
+ attribute :by_year_day, :integer, collection: true
20
+ attribute :by_hour, :integer, collection: true
21
+ attribute :by_minute, :integer, collection: true
22
+ attribute :by_second, :integer, collection: true
23
+ attribute :by_set_pos, :integer, collection: true
24
+ attribute :week_start, :string, default: "MO"
25
+ attribute :extensions, MeetingExtension, collection: true
26
+ end
27
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # RecurrenceByDay — a BYDAY part of an ISO 8601-2 §13 recurrence.
5
+ # `ordinal` is null for "every", +1 for "first", -1 for "last", etc.
6
+ class RecurrenceByDay < Lutaml::Model::Serializable
7
+ attribute :ordinal, :integer
8
+ attribute :weekday, :string
9
+ end
10
+ end
@@ -3,56 +3,37 @@
3
3
  module Edoxen
4
4
  # Built-in reference data for ISO codes used across the Edoxen model:
5
5
  # ISO 3166-1 alpha-2 country codes, ISO 639-3 language codes, ISO
6
- # 15924 script codes, and UN/LOCODEs.
6
+ # 15924 script codes, UN/LOCODEs, and IATA airport codes.
7
7
  #
8
- # UN/LOCODE lookups delegate to the `unlocode` gem (the canonical
9
- # Ruby registry of the UNECE UN/LOCODE dataset). The frozen
10
- # ReferenceData::UNLOCODES list stays as a curated offline subset
11
- # used by specs and consumers that don't want to load the full
12
- # ~100k-entry registry.
8
+ # UN/LOCODE lookups delegate to the `unlocodes` gem (the canonical
9
+ # Ruby registry of the UNECE UN/LOCODE dataset).
10
+ # IATA lookups delegate to the `iata` gem (the canonical Ruby registry
11
+ # of the IATA airport code list, sourced from Wikidata).
13
12
  #
14
13
  # Reference data sources:
15
14
  # * https://www.iso.org/iso-3166-country-codes.html
16
15
  # * https://iso639-3.sil.org/
17
16
  # * https://unicode.org/iso15924/
18
17
  # * https://unece.org/trade/cefact/unlocode-code-list-country-and-territory
19
- #
20
- # UN/LOCODE format: 2-letter ISO 3166-1 country code + 3-character
21
- # location alphanumeric (e.g., FRPAR = Paris, France; HKHKG = Hong
22
- # Kong; THCNM = Chiang Mai). UN-maintained, supersedes the
23
- # airport-centric IATA city codes edoxen previously used.
18
+ # * https://www.wikidata.org/wiki/Property:P238 (IATA airport code)
24
19
  module ReferenceData
25
- # Hard require the unlocode gem. It's a declared runtime dep, so a
26
- # clean install of edoxen brings it in. The bundled dataset ships
27
- # 4 sample entries; full coverage requires `rake unlocodes:fetch`
28
- # from the unlocode gem.
29
20
  require "unlocodes"
21
+ require "iata"
30
22
 
31
- # ISO 3166-1 alpha-2 — countries ISO/TC 154 / OIML / BIPM operate in.
32
- # Two-letter uppercase.
33
23
  COUNTRY_CODES = %w[
34
24
  AE AF AR AT AU BE BG BR BY CA CH CL CN CO CY CZ DE DK EE ES FI FR
35
25
  GB GR HK HR HU ID IE IL IN IS IT JP KE KR LK LT LU LV MT MX MY NL NO PH
36
26
  NZ PL PT RO RS RU SA SE SG SI SK TH TN TR TW UA US VN ZA
37
27
  ].freeze
38
28
 
39
- # ISO 639-3 — languages actually used in the edoxen fixtures
40
- # (eng + fra in OIML, plus the ISO official languages).
41
- # Three-letter lowercase.
42
29
  LANGUAGE_CODES = %w[
43
30
  ara chi deu eng fra jpn rus spa zho
44
31
  ].freeze
45
32
 
46
- # ISO 15924 — scripts. Latn + Cyrl + Hant + Hans cover every script
47
- # ISO and OIML resolutions are written in.
48
33
  SCRIPT_CODES = %w[
49
34
  Arab Cyrl Hans Hant Hang Hebr Jpan Kore Latn
50
35
  ].freeze
51
36
 
52
- # Curated offline subset of UN/LOCODEs — the cities ISO/TC 154 and
53
- # OIML actually meet in plus the major transit hubs. Used by specs
54
- # and consumers that don't want to load the full registry. For
55
- # arbitrary lookups, use {find_unlocode}.
56
37
  UNLOCODES = %w[
57
38
  AUMEL ATVIE BEBRU BGSOF BRBSB CHGVA CNCAN CNHKG CNSHA CNXSZ COCTG
58
39
  CYLCA CZPRG DEBER DEHAM DEFRA DKCPH ESMAD FIHEL FRARC FRLYS FRMRS
@@ -66,30 +47,43 @@ module Edoxen
66
47
  # migration; will be removed in the next minor.
67
48
  CITY_CODES = UNLOCODES
68
49
 
50
+ # Memoise the registries so we don't re-parse the bundled datasets on
51
+ # every lookup. Each gem's Registry is immutable after construction.
52
+ @unlocodes_registry = nil
53
+ @iata_registry = nil
54
+
69
55
  class << self
70
- # Look up a UN/LOCODE entry via the canonical `unlocode` gem.
71
- # Returns an `Unlocodes::Entry` (with `#name`, `#country`,
72
- # `#coordinates`, `#iata`, `#functions`, etc.) or nil when the
73
- # code is not in the registry.
74
- #
75
- # Requires the full UN/LOCODE dataset to be loaded. The gem
76
- # ships a 4-entry sample by default; run `rake unlocodes:fetch`
77
- # from the unlocode gem to populate the full ~100k-entry
78
- # registry.
79
- #
56
+ # @return [Unlocodes::Registry]
57
+ def unlocodes_registry
58
+ @unlocodes_registry ||= Unlocodes::Registry.load_default
59
+ end
60
+
61
+ # @return [Iata::Registry]
62
+ def iata_registry
63
+ @iata_registry ||= Iata::Registry.load_default
64
+ end
65
+
66
+ # Look up a UN/LOCODE entry via the canonical `unlocodes` gem.
80
67
  # @param code [String, #to_s] 5-character UN/LOCODE
81
68
  # @return [Unlocodes::Entry, nil]
82
69
  def find_unlocode(code)
83
- Unlocodes.find(code.to_s.upcase)
70
+ unlocodes_registry.find(code.to_s.upcase)
84
71
  end
85
72
 
86
- # True when the code resolves to a real UN/LOCODE entry via the
87
- # canonical registry. Use this when validating user-supplied
88
- # city codes; use the schema's pattern check for cheap shape
89
- # validation.
90
73
  def unlocode_exists?(code)
91
74
  !find_unlocode(code).nil?
92
75
  end
76
+
77
+ # Look up an IATA airport/city code via the canonical `iata` gem.
78
+ # @param code [String, #to_s] 3-character IATA code
79
+ # @return [Iata::Entry, nil]
80
+ def find_iata(code)
81
+ iata_registry.find(code.to_s.upcase)
82
+ end
83
+
84
+ def iata_exists?(code)
85
+ !find_iata(code).nil?
86
+ end
93
87
  end
94
88
  end
95
89
  end
@@ -72,7 +72,7 @@ module Edoxen
72
72
  end
73
73
 
74
74
  def load_schemer(path)
75
- JSONSchemer.schema(YAML.safe_load(File.read(path), permitted_classes: [Date, Time]))
75
+ JSONSchemer.schema(YAML.safe_load_file(path, permitted_classes: [Date, Time]))
76
76
  end
77
77
 
78
78
  # Coerce Date/Time instances back to ISO strings before handing the
@@ -81,8 +81,7 @@ module Edoxen
81
81
  # here keeps the gem OCP-compliant (no json_schemer plugin/tweak).
82
82
  def normalize_dates(value)
83
83
  case value
84
- when Date then value.iso8601
85
- when Time then value.iso8601
84
+ when Date, Time then value.iso8601
86
85
  when Hash then value.transform_values { |v| normalize_dates(v) }
87
86
  when Array then value.map { |v| normalize_dates(v) }
88
87
  else value
@@ -4,9 +4,9 @@ module Edoxen
4
4
  # Per-language canonical source URL (e.g. one PDF per language).
5
5
  # Carries the URL ref, its format, the ISO 639-3 language_code the URL
6
6
  # is the canonical source for, and an optional `kind` discriminator
7
- # (agenda_pdf, minutes_pdf, resolutions_pdf, ...) used by the Meeting
8
- # model. Existing Resolution fixtures without `kind` still parse —
9
- # the field is optional.
7
+ # (agenda_pdf, minutes_pdf, decisions_pdf, ...) used by the Meeting
8
+ # model. Existing fixtures without `kind` still parse — the field is
9
+ # optional.
10
10
  class SourceUrl < Lutaml::Model::Serializable
11
11
  attribute :ref, :string
12
12
  attribute :format, :string
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Edoxen
4
- # A structured resolution identifier, e.g. prefix "ISO" + number "2019-01".
5
- # A Resolution carries 1..* StructuredIdentifier so a single resolution can
4
+ # A structured decision identifier, e.g. prefix "ISO" + number "2019-01".
5
+ # A Decision carries 1..* StructuredIdentifier so a single decision can
6
6
  # hold its TC number, its SC number, and any cross-cutting reference number
7
7
  # without forcing callers to flatten them into one opaque string.
8
8
  class StructuredIdentifier < Lutaml::Model::Serializable
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Topic — the subject of discussion at a Meeting.
5
+ # A Topic can have documents (text), assets (non-text), references
6
+ # (external pointers), and is the anchor for Motions and Decisions.
7
+ #
8
+ # Cross-meeting threading via `resumption_of` (URN to a prior Topic
9
+ # in a prior Meeting) — pattern from HK LegCo OData schema.
10
+ class Topic < Lutaml::Model::Serializable
11
+ attribute :identifier, :string
12
+ attribute :urn, :string
13
+ attribute :title, :string
14
+ attribute :description, :string
15
+ attribute :status, :string, values: Enums::TOPIC_STATUS
16
+ attribute :resumption_of, :string
17
+ attribute :documents, TopicDocument, collection: true
18
+ attribute :assets, TopicAsset, collection: true
19
+ attribute :references, Reference, collection: true
20
+ attribute :motions, :string, collection: true
21
+ attribute :decisions, :string, collection: true
22
+ attribute :extensions, MeetingExtension, collection: true
23
+
24
+ # --- v2.1 derivation accessor (TODO.refactor/45) --------------------
25
+ # Returns the Decisions in `collection` whose `about_topics` includes
26
+ # this Topic's URN.
27
+ #
28
+ # Storage side: Decision.aboutTopics (SSOT for the relationship).
29
+ # The stored `decisions[]` field remains on the wire for back-compat
30
+ # through v2.x; v3.0 removes it and this becomes the only path.
31
+ def decisions_in(collection:)
32
+ return [] unless collection && urn
33
+
34
+ collection.decisions.select { |decision| decision.about_topics&.include?(urn) }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # TopicAsset — non-text resource about a Topic.
5
+ # Examples: image, dataset, model, video.
6
+ class TopicAsset < Lutaml::Model::Serializable
7
+ attribute :identifier, :string
8
+ attribute :title, :string
9
+ attribute :kind, :string
10
+ attribute :url, :string
11
+ attribute :format, :string
12
+ attribute :extensions, MeetingExtension, collection: true
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # TopicDocument — text-bearing document about a Topic.
5
+ # Examples: bill text, ISO draft, report, agenda attachment.
6
+ class TopicDocument < Lutaml::Model::Serializable
7
+ attribute :identifier, :string
8
+ attribute :title, :string
9
+ attribute :version, :string
10
+ attribute :status, :string
11
+ attribute :url, :string
12
+ attribute :format, :string
13
+ attribute :language_code, :string
14
+ attribute :extensions, MeetingExtension, collection: true
15
+ end
16
+ end