edoxen 0.7.2 → 0.8.2
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +26 -1
- data/.rubocop_todo.yml +1 -1
- data/CHANGELOG.md +133 -0
- data/README.adoc +414 -182
- data/edoxen.gemspec +10 -10
- data/lib/edoxen/action.rb +5 -4
- data/lib/edoxen/agenda.rb +4 -7
- data/lib/edoxen/agenda_item.rb +18 -5
- data/lib/edoxen/approval.rb +2 -2
- data/lib/edoxen/attendance.rb +16 -10
- data/lib/edoxen/body.rb +26 -0
- data/lib/edoxen/body_register.rb +17 -0
- data/lib/edoxen/body_vocabulary_entry.rb +24 -0
- data/lib/edoxen/body_vocabulary_host.rb +40 -0
- data/lib/edoxen/cli.rb +175 -115
- data/lib/edoxen/consideration.rb +6 -4
- data/lib/edoxen/contact.rb +37 -0
- data/lib/edoxen/contact_identifier.rb +13 -0
- data/lib/edoxen/contact_method.rb +16 -0
- data/lib/edoxen/contact_register.rb +26 -0
- data/lib/edoxen/date_time_range.rb +16 -0
- data/lib/edoxen/deadline.rb +1 -1
- data/lib/edoxen/decision.rb +57 -0
- data/lib/edoxen/decision_collection.rb +10 -0
- data/lib/edoxen/decision_date.rb +9 -0
- data/lib/edoxen/decision_metadata.rb +27 -0
- data/lib/edoxen/decision_relation.rb +11 -0
- data/lib/edoxen/declaration.rb +25 -0
- data/lib/edoxen/entity_ref.rb +74 -0
- data/lib/edoxen/entity_resolver.rb +68 -0
- data/lib/edoxen/enums.rb +123 -9
- data/lib/edoxen/error.rb +1 -1
- data/lib/edoxen/extension_attribute.rb +78 -0
- data/lib/edoxen/host_ref.rb +1 -1
- data/lib/edoxen/link_checker.rb +143 -34
- data/lib/edoxen/localized_name.rb +16 -0
- data/lib/edoxen/localized_string.rb +22 -0
- data/lib/edoxen/meeting.rb +36 -42
- data/lib/edoxen/meeting_collection.rb +1 -2
- data/lib/edoxen/meeting_collection_metadata.rb +4 -2
- data/lib/edoxen/meeting_component.rb +37 -0
- data/lib/edoxen/meeting_extension.rb +25 -0
- data/lib/edoxen/meeting_identifier.rb +1 -1
- data/lib/edoxen/meeting_series.rb +26 -0
- data/lib/edoxen/minutes.rb +3 -4
- data/lib/edoxen/minutes_section.rb +12 -13
- data/lib/edoxen/motion.rb +51 -0
- data/lib/edoxen/name.rb +29 -0
- data/lib/edoxen/officer.rb +19 -0
- data/lib/edoxen/officers_host.rb +19 -0
- data/lib/edoxen/person.rb +5 -9
- data/lib/edoxen/physical_venue.rb +15 -0
- data/lib/edoxen/recurrence.rb +27 -0
- data/lib/edoxen/recurrence_by_day.rb +10 -0
- data/lib/edoxen/reference.rb +1 -1
- data/lib/edoxen/reference_data.rb +34 -44
- data/lib/edoxen/schema_validator.rb +3 -4
- data/lib/edoxen/source_url.rb +4 -4
- data/lib/edoxen/statement.rb +21 -0
- data/lib/edoxen/structured_identifier.rb +2 -2
- data/lib/edoxen/topic.rb +39 -0
- data/lib/edoxen/topic_asset.rb +14 -0
- data/lib/edoxen/topic_document.rb +16 -0
- data/lib/edoxen/urn_for.rb +68 -0
- data/lib/edoxen/venue.rb +81 -0
- data/lib/edoxen/venue_register.rb +18 -0
- data/lib/edoxen/venue_validator.rb +74 -0
- data/lib/edoxen/version.rb +1 -1
- data/lib/edoxen/virtual_venue.rb +15 -0
- data/lib/edoxen/vote_record.rb +17 -13
- data/lib/edoxen/voting.rb +45 -0
- data/lib/edoxen/voting_counts.rb +23 -0
- data/lib/edoxen.rb +51 -15
- data/schema/edoxen.yaml +1427 -246
- data/schema/meeting.yaml +1311 -308
- data/sig/edoxen.rbs +581 -1
- metadata +66 -22
- data/lib/edoxen/_metadata.rb.deprecated +0 -57
- data/lib/edoxen/localization.rb +0 -18
- data/lib/edoxen/location.rb +0 -15
- data/lib/edoxen/meeting_localization.rb +0 -15
- data/lib/edoxen/resolution.rb +0 -43
- data/lib/edoxen/resolution_collection.rb +0 -10
- data/lib/edoxen/resolution_date.rb +0 -11
- data/lib/edoxen/resolution_metadata.rb +0 -30
- data/lib/edoxen/resolution_relation.rb +0 -11
- data/lib/edoxen/resolution_set.rb +0 -17
- data/lib/edoxen/schedule_item.rb +0 -27
- data/lib/edoxen/schedule_item_localization.rb +0 -21
|
@@ -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, LocalizedString, collection: true
|
|
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 (1.0, 1.0 design review). Parallel to
|
|
25
|
+
# `resulting_decision` (String). Prefer the typed form in new code;
|
|
26
|
+
# the bare String form is removed in 1.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
|
+
# --- 1.0 derivation accessor (1.0 design review) --------------------
|
|
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; 1.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
|
data/lib/edoxen/name.rb
ADDED
|
@@ -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 legacy 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
|
-
#
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
data/lib/edoxen/reference.rb
CHANGED
|
@@ -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,
|
|
6
|
+
# 15924 script codes, UN/LOCODEs, and IATA airport codes.
|
|
7
7
|
#
|
|
8
|
-
# UN/LOCODE lookups delegate to the `
|
|
9
|
-
# Ruby registry of the UNECE UN/LOCODE dataset).
|
|
10
|
-
#
|
|
11
|
-
#
|
|
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
|
|
@@ -62,34 +43,43 @@ module Edoxen
|
|
|
62
43
|
UAIEV USMIA USNYC USORL VNSGN ZACPT
|
|
63
44
|
].freeze
|
|
64
45
|
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
|
|
46
|
+
# Memoise the registries so we don't re-parse the bundled datasets on
|
|
47
|
+
# every lookup. Each gem's Registry is immutable after construction.
|
|
48
|
+
@unlocodes_registry = nil
|
|
49
|
+
@iata_registry = nil
|
|
68
50
|
|
|
69
51
|
class << self
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
52
|
+
# @return [Unlocodes::Registry]
|
|
53
|
+
def unlocodes_registry
|
|
54
|
+
@unlocodes_registry ||= Unlocodes::Registry.load_default
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Iata::Registry]
|
|
58
|
+
def iata_registry
|
|
59
|
+
@iata_registry ||= Iata::Registry.load_default
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Look up a UN/LOCODE entry via the canonical `unlocodes` gem.
|
|
80
63
|
# @param code [String, #to_s] 5-character UN/LOCODE
|
|
81
64
|
# @return [Unlocodes::Entry, nil]
|
|
82
65
|
def find_unlocode(code)
|
|
83
|
-
|
|
66
|
+
unlocodes_registry.find(code.to_s.upcase)
|
|
84
67
|
end
|
|
85
68
|
|
|
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
69
|
def unlocode_exists?(code)
|
|
91
70
|
!find_unlocode(code).nil?
|
|
92
71
|
end
|
|
72
|
+
|
|
73
|
+
# Look up an IATA airport/city code via the canonical `iata` gem.
|
|
74
|
+
# @param code [String, #to_s] 3-character IATA code
|
|
75
|
+
# @return [Iata::Entry, nil]
|
|
76
|
+
def find_iata(code)
|
|
77
|
+
iata_registry.find(code.to_s.upcase)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def iata_exists?(code)
|
|
81
|
+
!find_iata(code).nil?
|
|
82
|
+
end
|
|
93
83
|
end
|
|
94
84
|
end
|
|
95
85
|
end
|
|
@@ -9,7 +9,7 @@ module Edoxen
|
|
|
9
9
|
# The SchemaValidator is intentionally small and only owns one concern:
|
|
10
10
|
# JSON-Schema validation with line-accurate error reporting. It does not
|
|
11
11
|
# touch the Ruby model — CLI layers SchemaValidator and
|
|
12
|
-
# `
|
|
12
|
+
# `DecisionCollection.from_yaml` to catch both schema-level violations
|
|
13
13
|
# (additionalProperties, required, enum) and structural issues
|
|
14
14
|
# (numeric/date coercion, missing nested classes) the schema can't express.
|
|
15
15
|
#
|
|
@@ -72,7 +72,7 @@ module Edoxen
|
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def load_schemer(path)
|
|
75
|
-
JSONSchemer.schema(YAML.
|
|
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
|
data/lib/edoxen/source_url.rb
CHANGED
|
@@ -4,13 +4,13 @@ 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,
|
|
8
|
-
# model. Existing
|
|
9
|
-
#
|
|
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
|
|
13
|
-
attribute :
|
|
13
|
+
attribute :spelling, :string
|
|
14
14
|
attribute :kind, :string, values: Enums::SOURCE_URL_KIND
|
|
15
15
|
end
|
|
16
16
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# One remark made by one or more meeting members on a topic or
|
|
5
|
+
# a minutes section. The `kind` discriminator separates the
|
|
6
|
+
# three BS 0:2006 statement types (statement, comment,
|
|
7
|
+
# standpoint); adding a new kind is a one-line StatementKind
|
|
8
|
+
# enum extension (OCP).
|
|
9
|
+
#
|
|
10
|
+
# Attachment points: `MinutesSection#statements[]` (per-meeting)
|
|
11
|
+
# and `Topic#statements[]` (standing position that travels with
|
|
12
|
+
# the topic across meetings). Same class, two attachment points
|
|
13
|
+
# — the semantic distinction comes from where the statement is
|
|
14
|
+
# attached, not from a subclass.
|
|
15
|
+
class Statement < Lutaml::Model::Serializable
|
|
16
|
+
attribute :kind, :string, values: Enums::STATEMENT_KIND
|
|
17
|
+
attribute :description, LocalizedString, collection: true
|
|
18
|
+
attribute :party, Person, collection: true
|
|
19
|
+
attribute :extensions, MeetingExtension, collection: true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Edoxen
|
|
4
|
-
# A structured
|
|
5
|
-
# A
|
|
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
|
data/lib/edoxen/topic.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
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, LocalizedString, collection: true
|
|
14
|
+
attribute :description, LocalizedString, collection: true
|
|
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 :statements, Statement, collection: true
|
|
23
|
+
attribute :declarations, Declaration, collection: true
|
|
24
|
+
attribute :extensions, MeetingExtension, collection: true
|
|
25
|
+
|
|
26
|
+
# --- 1.0 derivation accessor (1.0 design review) --------------------
|
|
27
|
+
# Returns the Decisions in `collection` whose `about_topics` includes
|
|
28
|
+
# this Topic's URN.
|
|
29
|
+
#
|
|
30
|
+
# Storage side: Decision.aboutTopics (SSOT for the relationship).
|
|
31
|
+
# The stored `decisions[]` field remains on the wire for back-compat
|
|
32
|
+
# through v2.x; 1.0 removes it and this becomes the only path.
|
|
33
|
+
def decisions_in(collection:)
|
|
34
|
+
return [] unless collection && urn
|
|
35
|
+
|
|
36
|
+
collection.decisions.select { |decision| decision.about_topics&.include?(urn) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
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, LocalizedString, collection: true
|
|
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, LocalizedString, collection: true
|
|
9
|
+
attribute :version, :string
|
|
10
|
+
attribute :status, :string
|
|
11
|
+
attribute :url, :string
|
|
12
|
+
attribute :format, :string
|
|
13
|
+
attribute :spelling, :string
|
|
14
|
+
attribute :extensions, MeetingExtension, collection: true
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# URN helpers for entities that don't carry their URN inline (e.g.
|
|
5
|
+
# AgendaItem which derives its URN from the parent Meeting). The
|
|
6
|
+
# scheme is hierarchical:
|
|
7
|
+
#
|
|
8
|
+
# urn:oiml:{body}:{kind}:{slug}(:{sub-kind}:{sub-id})*
|
|
9
|
+
#
|
|
10
|
+
# Examples:
|
|
11
|
+
# urn:oiml:ciml:meeting:ciml-60
|
|
12
|
+
# urn:oiml:ciml:meeting:ciml-60:agenda:6.2
|
|
13
|
+
# urn:oiml:doc:ciml:resolution:2025-01
|
|
14
|
+
#
|
|
15
|
+
# Use {UrnFor.agenda_item} to compute an agenda item URN from its
|
|
16
|
+
# parent meeting URN + label, and {UrnFor.parse} to decompose one.
|
|
17
|
+
module UrnFor
|
|
18
|
+
AGENDA_SEGMENT = "agenda"
|
|
19
|
+
SEGMENT_RE = /[^:]+/
|
|
20
|
+
|
|
21
|
+
# Build an AgendaItem URN from its parent Meeting URN and the
|
|
22
|
+
# agenda item label (e.g. "6.2"). Returns nil if either input is
|
|
23
|
+
# blank so callers can use this as a default-value generator
|
|
24
|
+
# without raising.
|
|
25
|
+
def self.agenda_item(meeting_urn:, label:)
|
|
26
|
+
m = meeting_urn.to_s
|
|
27
|
+
l = label.to_s
|
|
28
|
+
return nil if m.empty? || l.empty?
|
|
29
|
+
|
|
30
|
+
"#{m}:#{AGENDA_SEGMENT}:#{l}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Parse an agenda item URN into its component parts. Returns a Hash
|
|
34
|
+
# with :meeting_urn and :label keys, or nil if the URN doesn't
|
|
35
|
+
# match the expected shape.
|
|
36
|
+
#
|
|
37
|
+
# UrnFor.parse("urn:oiml:ciml:meeting:ciml-60:agenda:6.2")
|
|
38
|
+
# # => { meeting_urn: "urn:oiml:ciml:meeting:ciml-60",
|
|
39
|
+
# # label: "6.2" }
|
|
40
|
+
def self.parse(urn)
|
|
41
|
+
return nil unless urn
|
|
42
|
+
|
|
43
|
+
s = urn.to_s
|
|
44
|
+
idx = s.rindex(":#{AGENDA_SEGMENT}:")
|
|
45
|
+
return nil unless idx
|
|
46
|
+
|
|
47
|
+
meeting_urn = s[0...idx]
|
|
48
|
+
label = s[(idx + AGENDA_SEGMENT.length + 2)..]
|
|
49
|
+
return nil if meeting_urn.empty? || label.to_s.empty?
|
|
50
|
+
|
|
51
|
+
{ meeting_urn: meeting_urn, label: label }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Walk an Agenda tree and set each item's `urn` based on the
|
|
55
|
+
# parent meeting URN. Skips items that already have a URN. Useful
|
|
56
|
+
# as a backfill step in data-fix scripts.
|
|
57
|
+
def self.assign_to_agenda!(agenda, meeting_urn)
|
|
58
|
+
return nil unless agenda && meeting_urn
|
|
59
|
+
|
|
60
|
+
(agenda.items || []).each do |item|
|
|
61
|
+
next if item.urn && !item.urn.to_s.empty?
|
|
62
|
+
|
|
63
|
+
item.urn = agenda_item(meeting_urn: meeting_urn, label: item.label)
|
|
64
|
+
end
|
|
65
|
+
agenda
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/edoxen/venue.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Venue — polymorphic by discriminator, flat on the wire.
|
|
5
|
+
#
|
|
6
|
+
# The `kind` field discriminates between physical and virtual venues.
|
|
7
|
+
# All possible fields (physical-specific and virtual-specific) live on
|
|
8
|
+
# this one class as optional attributes — only the ones matching the
|
|
9
|
+
# `kind` are populated.
|
|
10
|
+
#
|
|
11
|
+
# 1.0 (per-field localization, ISO 24229):
|
|
12
|
+
# - All translatable fields are Localized<String>[0..*].
|
|
13
|
+
# - Added: `urn` for registry storage; `ref` for reference-by-URN.
|
|
14
|
+
class Venue < Lutaml::Model::Serializable
|
|
15
|
+
attribute :ref, :string
|
|
16
|
+
attribute :local_ref, :string
|
|
17
|
+
attribute :urn, :string
|
|
18
|
+
attribute :kind, :string, values: Enums::VENUE_KIND
|
|
19
|
+
attribute :name, LocalizedString, collection: true
|
|
20
|
+
attribute :label, LocalizedString, collection: true
|
|
21
|
+
attribute :description, LocalizedString, collection: true
|
|
22
|
+
attribute :capacity, :integer
|
|
23
|
+
attribute :url, :string
|
|
24
|
+
attribute :contact_methods, ContactMethod, collection: true
|
|
25
|
+
|
|
26
|
+
# Physical-venue fields (populated when kind == "physical").
|
|
27
|
+
attribute :unlocode, :string
|
|
28
|
+
attribute :iata_code, :string
|
|
29
|
+
attribute :address, LocalizedString, collection: true
|
|
30
|
+
attribute :city, :string
|
|
31
|
+
attribute :country_code, :string
|
|
32
|
+
attribute :lat, :float
|
|
33
|
+
attribute :lon, :float
|
|
34
|
+
attribute :building, LocalizedString, collection: true
|
|
35
|
+
attribute :floor, LocalizedString, collection: true
|
|
36
|
+
attribute :room, LocalizedString, collection: true
|
|
37
|
+
attribute :access_notes, LocalizedString, collection: true
|
|
38
|
+
|
|
39
|
+
# Virtual-venue fields (populated when kind == "virtual").
|
|
40
|
+
attribute :uri, :string
|
|
41
|
+
attribute :features, :string, collection: true, values: Enums::VIRTUAL_FEATURE
|
|
42
|
+
attribute :passcode, :string
|
|
43
|
+
attribute :meeting_id, :string
|
|
44
|
+
attribute :dial_in_numbers, :string, collection: true
|
|
45
|
+
attribute :waiting_room, :boolean
|
|
46
|
+
attribute :registration_required, :boolean
|
|
47
|
+
|
|
48
|
+
attribute :extensions, MeetingExtension, collection: true
|
|
49
|
+
|
|
50
|
+
def reference?
|
|
51
|
+
(!ref.nil? && !ref.to_s.empty?) ||
|
|
52
|
+
(!local_ref.nil? && !local_ref.to_s.empty?)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def physical?
|
|
56
|
+
kind == "physical"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def virtual?
|
|
60
|
+
kind == "virtual"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def unlocode_entry
|
|
64
|
+
return nil if unlocode.nil? || unlocode.to_s.empty?
|
|
65
|
+
|
|
66
|
+
Edoxen::ReferenceData.find_unlocode(unlocode)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def iata_entry
|
|
70
|
+
return nil if iata_code.nil? || iata_code.to_s.empty?
|
|
71
|
+
|
|
72
|
+
Edoxen::ReferenceData.find_iata(iata_code)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def features_list
|
|
76
|
+
return "" if features.nil? || features.empty?
|
|
77
|
+
|
|
78
|
+
features.join(", ")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Registry of Venues indexed by scoped URN. Mirrors ContactRegister:
|
|
5
|
+
# members carry `urn: urn:edoxen:venue:{scope}:{local-id}`; the
|
|
6
|
+
# collection's `scope` MUST match the scope segment in member URNs.
|
|
7
|
+
# Stream loading belongs to the service layer.
|
|
8
|
+
class VenueRegister < Lutaml::Model::Serializable
|
|
9
|
+
attribute :scope, :string
|
|
10
|
+
attribute :title, LocalizedString, collection: true
|
|
11
|
+
attribute :venues, Venue, collection: true
|
|
12
|
+
attribute :extensions, MeetingExtension, collection: true
|
|
13
|
+
|
|
14
|
+
def find_by_urn(urn)
|
|
15
|
+
venues&.find { |v| v.urn == urn }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|