edoxen 0.7.2 → 0.8.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.
- 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 +12 -5
- data/lib/edoxen/approval.rb +2 -2
- data/lib/edoxen/attendance.rb +16 -10
- 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 +35 -0
- data/lib/edoxen/contact_collection.rb +26 -0
- data/lib/edoxen/contact_identifier.rb +13 -0
- data/lib/edoxen/contact_method.rb +16 -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/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 +34 -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/venue.rb +79 -0
- data/lib/edoxen/venue_collection.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 +47 -15
- data/schema/edoxen.yaml +1376 -246
- data/schema/meeting.yaml +1207 -272
- data/sig/edoxen.rbs +581 -1
- metadata +62 -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
data/edoxen.gemspec
CHANGED
|
@@ -8,25 +8,24 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ["Ribose Inc."]
|
|
9
9
|
spec.email = ["open.source@ribose.com"]
|
|
10
10
|
|
|
11
|
-
spec.summary = "Edoxen is a
|
|
11
|
+
spec.summary = "Edoxen is a generic information model for meetings, agendas, and decisions."
|
|
12
12
|
spec.description = <<~HEREDOC
|
|
13
|
-
Edoxen provides a Ruby library for working with
|
|
14
|
-
users to create, manipulate, and serialize
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
Edoxen provides a Ruby library for working with a generic meeting/decision
|
|
14
|
+
model, allowing users to create, manipulate, and serialize meeting, agenda,
|
|
15
|
+
motion, voting, and decision data in a structured format. Built on top of
|
|
16
|
+
the lutaml-model serialization framework, with profile-based customization
|
|
17
|
+
(ISO 8601-2 §15) for domain-specific extensions.
|
|
18
18
|
HEREDOC
|
|
19
19
|
|
|
20
20
|
spec.homepage = "https://github.com/metanorma/edoxen"
|
|
21
21
|
spec.license = "BSD-2-Clause"
|
|
22
|
-
spec.required_ruby_version = ">= 3.
|
|
22
|
+
spec.required_ruby_version = ">= 3.1.0"
|
|
23
23
|
|
|
24
24
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
25
25
|
spec.metadata["source_code_uri"] = "https://github.com/metanorma/edoxen"
|
|
26
26
|
spec.metadata["changelog_uri"] = "https://github.com/metanorma/edoxen"
|
|
27
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
27
28
|
|
|
28
|
-
# Specify which files should be added to the gem when it is released.
|
|
29
|
-
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
30
29
|
spec.files = Dir.chdir(__dir__) do
|
|
31
30
|
`git ls-files -z`.split("\x0").reject do |f|
|
|
32
31
|
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
|
@@ -39,5 +38,6 @@ Gem::Specification.new do |spec|
|
|
|
39
38
|
spec.add_dependency "json_schemer", "~> 2.0"
|
|
40
39
|
spec.add_dependency "lutaml-model", "~> 0.7"
|
|
41
40
|
spec.add_dependency "thor", "~> 1.0"
|
|
42
|
-
spec.add_dependency "unlocodes", "~> 0.
|
|
41
|
+
spec.add_dependency "unlocodes", "~> 0.3"
|
|
42
|
+
spec.add_dependency "iata", "~> 0.1"
|
|
43
43
|
end
|
data/lib/edoxen/action.rb
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Edoxen
|
|
4
|
-
# Verb + one effective date +
|
|
5
|
-
#
|
|
4
|
+
# Verb + one effective date + per-field Localized message. Each
|
|
5
|
+
# `Action` belongs to a `Decision`; the message field carries one
|
|
6
|
+
# `LocalizedString` per ISO 24229 spelling.
|
|
6
7
|
class Action < Lutaml::Model::Serializable
|
|
7
8
|
attribute :type, :string, values: Enums::ACTION_TYPE
|
|
8
|
-
attribute :date_effective,
|
|
9
|
-
attribute :message, :
|
|
9
|
+
attribute :date_effective, DecisionDate
|
|
10
|
+
attribute :message, LocalizedString, collection: true
|
|
10
11
|
end
|
|
11
12
|
end
|
data/lib/edoxen/agenda.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Edoxen
|
|
4
|
-
# The business-order document of a Meeting. Distinct from the
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# The business-order document of a Meeting. Distinct from the timetable
|
|
5
|
+
# (MeetingComponent) — the Agenda orders topics; components order time
|
|
6
|
+
# slots. Opening/closing sessions are components, not agenda entries.
|
|
7
7
|
#
|
|
8
8
|
# An Agenda may be versioned independently of the Meeting: a draft
|
|
9
9
|
# agenda circulates weeks before; a final agenda at meeting time;
|
|
@@ -14,11 +14,8 @@ module Edoxen
|
|
|
14
14
|
attribute :status, :string, values: Enums::AGENDA_STATUS
|
|
15
15
|
attribute :source_doc, :string
|
|
16
16
|
attribute :items, AgendaItem, collection: true
|
|
17
|
-
attribute :
|
|
18
|
-
attribute :closing_session, ScheduleItem
|
|
17
|
+
attribute :extensions, MeetingExtension, collection: true
|
|
19
18
|
|
|
20
|
-
# Find an agenda item by its label (e.g., "5.2"). Returns nil
|
|
21
|
-
# when no item matches.
|
|
22
19
|
def find_item(label)
|
|
23
20
|
items&.find { |item| item.label == label.to_s }
|
|
24
21
|
end
|
data/lib/edoxen/agenda_item.rb
CHANGED
|
@@ -3,15 +3,22 @@
|
|
|
3
3
|
module Edoxen
|
|
4
4
|
# One entry on an Agenda. `label` is the visible identifier (e.g.,
|
|
5
5
|
# "5.2"); `kind` discriminates numbered/unnumbered/header/opening/
|
|
6
|
-
# closing; `outcome` records what happened; `
|
|
7
|
-
# optionally links to the URN of the
|
|
6
|
+
# closing/aob; `outcome` records what happened; `decision_ref`
|
|
7
|
+
# optionally links to the URN of the decision this item produced.
|
|
8
|
+
#
|
|
9
|
+
# Topics (0..*) are the subject(s) of discussion at this agenda item.
|
|
10
|
+
# AOB (Any Other Business) items have 0 topics until raised during
|
|
11
|
+
# the meeting.
|
|
8
12
|
class AgendaItem < Lutaml::Model::Serializable
|
|
9
13
|
attribute :label, :string
|
|
10
14
|
attribute :kind, :string, values: Enums::AGENDA_ITEM_KIND
|
|
11
|
-
attribute :title, :
|
|
12
|
-
attribute :description, :
|
|
15
|
+
attribute :title, LocalizedString, collection: true
|
|
16
|
+
attribute :description, LocalizedString, collection: true
|
|
13
17
|
attribute :references, Reference, collection: true
|
|
14
18
|
attribute :outcome, :string, values: Enums::AGENDA_ITEM_OUTCOME
|
|
15
|
-
attribute :
|
|
19
|
+
attribute :decision_ref, :string
|
|
20
|
+
attribute :topics, Topic, collection: true
|
|
21
|
+
attribute :components, :string, collection: true
|
|
22
|
+
attribute :extensions, MeetingExtension, collection: true
|
|
16
23
|
end
|
|
17
24
|
end
|
data/lib/edoxen/approval.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Edoxen
|
|
|
6
6
|
class Approval < Lutaml::Model::Serializable
|
|
7
7
|
attribute :type, :string, values: Enums::APPROVAL_TYPE
|
|
8
8
|
attribute :degree, :string, values: Enums::APPROVAL_DEGREE
|
|
9
|
-
attribute :date,
|
|
10
|
-
attribute :message, :
|
|
9
|
+
attribute :date, DecisionDate
|
|
10
|
+
attribute :message, LocalizedString, collection: true
|
|
11
11
|
end
|
|
12
12
|
end
|
data/lib/edoxen/attendance.rb
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Edoxen
|
|
4
|
-
# One attendance record per person.
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
4
|
+
# One attendance record per person. Combines:
|
|
5
|
+
# - status: ParticipationStatus (in-meeting observation: present/absent/
|
|
6
|
+
# apologies/observer/excused)
|
|
7
|
+
# - role: AttendanceRole (structural importance: chair/required/optional/
|
|
8
|
+
# non_participant — from iCalendar ROLE, plain English)
|
|
9
|
+
# - response: AttendanceResponse (RSVP state: pending/confirmed/declined/
|
|
10
|
+
# tentative/delegated — from iCalendar PARTSTAT, plain English)
|
|
8
11
|
class Attendance < Lutaml::Model::Serializable
|
|
9
12
|
attribute :person, Person
|
|
10
13
|
attribute :status, :string, values: Enums::PARTICIPATION_STATUS
|
|
14
|
+
attribute :role, :string, values: Enums::ATTENDANCE_ROLE
|
|
15
|
+
attribute :response, :string, values: Enums::ATTENDANCE_RESPONSE
|
|
11
16
|
attribute :affiliation, :string
|
|
12
17
|
attribute :proxy_for, Person
|
|
13
18
|
attribute :notes, :string
|
|
19
|
+
attribute :extensions, MeetingExtension, collection: true
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
def present?
|
|
22
|
+
status == "present"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def declined?
|
|
26
|
+
response == "declined"
|
|
21
27
|
end
|
|
22
28
|
end
|
|
23
29
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# BodyVocabularyEntry — one entry in a per-dataset body_vocabulary
|
|
5
|
+
# list (1.0, 1.0 design review).
|
|
6
|
+
#
|
|
7
|
+
# Maps a free-form `body_type` (e.g. "CIML Meeting", "Plenary") to a
|
|
8
|
+
# short canonical value (e.g. "plenary"). Bodies declare their
|
|
9
|
+
# vocabulary on the collection metadata; consumers look up the
|
|
10
|
+
# canonical_type via the parent collection's vocabulary.
|
|
11
|
+
#
|
|
12
|
+
# SSOT: the body_vocabulary list on collection metadata is the single
|
|
13
|
+
# source of truth for body_type → canonical_type resolution within
|
|
14
|
+
# that dataset.
|
|
15
|
+
#
|
|
16
|
+
# Permissive: when no vocabulary entry matches a body_type, the gem
|
|
17
|
+
# returns the body_type string itself (with a warning at debug
|
|
18
|
+
# level). Strict mode is a v3.x concern.
|
|
19
|
+
class BodyVocabularyEntry < Lutaml::Model::Serializable
|
|
20
|
+
attribute :body_type, :string
|
|
21
|
+
attribute :canonical_type, :string
|
|
22
|
+
attribute :definition, :string
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Mixed into metadata classes that carry a per-dataset
|
|
5
|
+
# `body_vocabulary[]` collection (1.0, 1.0 design review). Provides
|
|
6
|
+
# the attribute declaration and the `canonical_type_for` lookup in
|
|
7
|
+
# one place so DecisionMetadata and MeetingCollectionMetadata share
|
|
8
|
+
# a single implementation.
|
|
9
|
+
#
|
|
10
|
+
# Permissive by design: when no vocabulary entry matches a
|
|
11
|
+
# body_type, the body_type string itself is returned (with no
|
|
12
|
+
# warning). Strict mode is a v3.x concern.
|
|
13
|
+
#
|
|
14
|
+
# == Why the `included` hook declares the attribute
|
|
15
|
+
#
|
|
16
|
+
# The block-form `included` callback calls
|
|
17
|
+
# `base.attribute :body_vocabulary, ...` on the includer so both
|
|
18
|
+
# consumers (DecisionMetadata, MeetingCollectionMetadata) pick up
|
|
19
|
+
# the attribute and the lookup method from one source. This is a
|
|
20
|
+
# deliberate DRY trade-off: the alternative — declaring the
|
|
21
|
+
# attribute in each metadata class — leaves the lookup method
|
|
22
|
+
# without its required backing field on any class that forgets the
|
|
23
|
+
# declaration. The hook makes the field and the method inseparable.
|
|
24
|
+
module BodyVocabularyHost
|
|
25
|
+
def self.included(base)
|
|
26
|
+
base.attribute :body_vocabulary, BodyVocabularyEntry, collection: true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Resolve a body_type to its canonical_type via this collection's
|
|
30
|
+
# vocabulary. Returns the body_type unchanged when nil/empty or
|
|
31
|
+
# when no matching entry exists.
|
|
32
|
+
def canonical_type_for(body_type)
|
|
33
|
+
return body_type if body_type.nil? || body_type.to_s.empty?
|
|
34
|
+
return body_type unless body_vocabulary
|
|
35
|
+
|
|
36
|
+
entry = body_vocabulary.find { |e| e.body_type == body_type }
|
|
37
|
+
entry ? entry.canonical_type : body_type
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/edoxen/cli.rb
CHANGED
|
@@ -5,17 +5,60 @@ require "fileutils"
|
|
|
5
5
|
|
|
6
6
|
module Edoxen
|
|
7
7
|
class Cli < Thor
|
|
8
|
+
# Each top-level YAML document is either a Decision collection or a
|
|
9
|
+
# Meeting (single or collection). The two share the same batch
|
|
10
|
+
# scaffold (expand → header → loop → tally → exit) but differ in
|
|
11
|
+
# which schema validates them and which model class loads them.
|
|
12
|
+
#
|
|
13
|
+
# A Profile bundles those two facts so the Thor command methods are
|
|
14
|
+
# one-line delegations. Adding a third document kind (e.g. an
|
|
15
|
+
# attendance-only file) is one Profile constant + one desc block —
|
|
16
|
+
# no new scaffolding.
|
|
17
|
+
Profile = Struct.new(:name, :schema_file, :loader) do
|
|
18
|
+
def schema_path
|
|
19
|
+
File.expand_path("../../schema/#{schema_file}", __dir__)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns the loaded model object, raising StandardError on failure.
|
|
23
|
+
# Single-vs-collection detection is up to the loader (Meeting side
|
|
24
|
+
# sniffs the top-level key; Decision side always returns a
|
|
25
|
+
# DecisionCollection).
|
|
26
|
+
def load(content)
|
|
27
|
+
loader.call(content)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
PROFILES = {
|
|
32
|
+
decisions: Profile.new(
|
|
33
|
+
"decisions", "edoxen.yaml",
|
|
34
|
+
lambda do |content|
|
|
35
|
+
data = YAML.safe_load(content, permitted_classes: [Date, Time])
|
|
36
|
+
case Batch.decision_kind(data)
|
|
37
|
+
when :contacts then ContactCollection.from_yaml(content)
|
|
38
|
+
when :venues then VenueCollection.from_yaml(content)
|
|
39
|
+
else DecisionCollection.from_yaml(content)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
),
|
|
43
|
+
meetings: Profile.new(
|
|
44
|
+
"meetings", "meeting.yaml",
|
|
45
|
+
lambda do |content|
|
|
46
|
+
data = YAML.safe_load(content, permitted_classes: [Date, Time])
|
|
47
|
+
case Batch.meetings_kind(data)
|
|
48
|
+
when :collection then MeetingCollection.from_yaml(content)
|
|
49
|
+
when :series then MeetingSeries.from_yaml(content)
|
|
50
|
+
else Meeting.from_yaml(content)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
)
|
|
54
|
+
}.freeze
|
|
55
|
+
|
|
8
56
|
# Deep module behind the per-command interface. Owns the
|
|
9
57
|
# expand/sort/empty/header/loop/tally/summary/exit scaffold so
|
|
10
|
-
# `validate` and `normalize` collapse to
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# batch runner prints progress, tallies, prints the summary, and
|
|
15
|
-
# exits with the right code.
|
|
16
|
-
#
|
|
17
|
-
# In-process; no adapter. The seam is the call site in each
|
|
18
|
-
# command method — internal to the CLI.
|
|
58
|
+
# `validate` and `normalize` collapse to one-line delegations.
|
|
59
|
+
# Also owns the per-profile shape sniffers that pick the correct
|
|
60
|
+
# model class for each YAML file (one of N shapes accepted by the
|
|
61
|
+
# canonical `oneOf` roots).
|
|
19
62
|
module Batch
|
|
20
63
|
# Per-file outcome. `ok` carries an optional message appended to
|
|
21
64
|
# the success indicator (e.g. "NORMALIZED → /out/path"). `bad`
|
|
@@ -32,11 +75,38 @@ module Edoxen
|
|
|
32
75
|
|
|
33
76
|
module_function
|
|
34
77
|
|
|
35
|
-
#
|
|
78
|
+
# Discriminate the three top-level shapes the canonical
|
|
79
|
+
# `schema/edoxen.yaml` accepts via `oneOf`:
|
|
36
80
|
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
81
|
+
# * :contacts — `{ contacts: [...] }` (ContactCollection registry).
|
|
82
|
+
# * :venues — `{ venues: [...] }` (VenueCollection registry).
|
|
83
|
+
# * :decisions — anything else (DecisionCollection, the default).
|
|
84
|
+
def decision_kind(data)
|
|
85
|
+
return :decisions unless data.is_a?(Hash)
|
|
86
|
+
|
|
87
|
+
return :contacts if data["contacts"].is_a?(Array)
|
|
88
|
+
return :venues if data["venues"].is_a?(Array)
|
|
89
|
+
|
|
90
|
+
:decisions
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Discriminate the three top-level shapes the canonical
|
|
94
|
+
# `schema/meeting.yaml` accepts via `oneOf`:
|
|
95
|
+
#
|
|
96
|
+
# * :collection — `{ meetings: [...] }`
|
|
97
|
+
# * :series — `{ identifier: [...], meeting_refs: [...] }`
|
|
98
|
+
# (or any hash with `identifier` + `recurrence`)
|
|
99
|
+
# * :meeting — anything else (single Meeting).
|
|
100
|
+
def meetings_kind(data)
|
|
101
|
+
return :meeting unless data.is_a?(Hash)
|
|
102
|
+
|
|
103
|
+
return :collection if data["meetings"].is_a?(Array)
|
|
104
|
+
return :series if data["identifier"].is_a?(Array) &&
|
|
105
|
+
(data.key?("meeting_refs") || data.key?("recurrence"))
|
|
106
|
+
|
|
107
|
+
:meeting
|
|
108
|
+
end
|
|
109
|
+
|
|
40
110
|
def run(cli, pattern, header:, summary_extra: [])
|
|
41
111
|
files = expand(pattern)
|
|
42
112
|
if files.empty?
|
|
@@ -84,62 +154,32 @@ module Edoxen
|
|
|
84
154
|
|
|
85
155
|
package_name "edoxen"
|
|
86
156
|
|
|
157
|
+
# --- Decision-side commands -----------------------------------------
|
|
158
|
+
|
|
87
159
|
desc "validate YAML_FILE_PATTERN",
|
|
88
|
-
"Validate
|
|
160
|
+
"Validate Decision YAML file(s) against schema/edoxen.yaml and the model."
|
|
89
161
|
|
|
90
162
|
def validate(pattern)
|
|
91
|
-
|
|
92
|
-
Batch.run(self, pattern, header: "🔍 Validating") do |file|
|
|
93
|
-
schema_errors = validator.validate_file(file)
|
|
94
|
-
model_errors = collect_model_errors(file)
|
|
95
|
-
if schema_errors.empty? && model_errors.empty?
|
|
96
|
-
Batch::Result.ok("VALID")
|
|
97
|
-
else
|
|
98
|
-
errors = (schema_errors + model_errors).map(&:to_clickable_format)
|
|
99
|
-
Batch::Result.bad(errors)
|
|
100
|
-
end
|
|
101
|
-
end
|
|
163
|
+
run_validate(PROFILES.fetch(:decisions), pattern)
|
|
102
164
|
end
|
|
103
165
|
|
|
104
166
|
desc "normalize YAML_FILE_PATTERN",
|
|
105
|
-
"Round-trip YAML file(s) through the
|
|
167
|
+
"Round-trip Decision YAML file(s) through the model (--output DIR or --inplace)."
|
|
106
168
|
|
|
107
169
|
option :output, type: :string, desc: "Output directory for normalized files"
|
|
108
170
|
option :inplace, type: :boolean, desc: "Modify files in place (no backup)"
|
|
109
171
|
|
|
110
172
|
def normalize(pattern)
|
|
111
|
-
|
|
112
|
-
say normalize_options_error, :red
|
|
113
|
-
exit 1
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
summary_extra = [
|
|
117
|
-
[" Output directory", options[:output]],
|
|
118
|
-
[" Mode", options[:inplace] ? "in place" : "--output"]
|
|
119
|
-
].compact
|
|
120
|
-
|
|
121
|
-
Batch.run(self, pattern, header: "🔄 Normalizing", summary_extra: summary_extra) do |file|
|
|
122
|
-
Batch::Result.ok(normalize_file(file))
|
|
123
|
-
rescue StandardError => e
|
|
124
|
-
Batch::Result.bad(["#{file}:1:1: #{e.message}"])
|
|
125
|
-
end
|
|
173
|
+
run_normalize(PROFILES.fetch(:decisions), pattern)
|
|
126
174
|
end
|
|
127
175
|
|
|
176
|
+
# --- Meeting-side commands ------------------------------------------
|
|
177
|
+
|
|
128
178
|
desc "validate-meetings YAML_FILE_PATTERN",
|
|
129
179
|
"Validate Meeting/Agenda YAML file(s) against schema/meeting.yaml."
|
|
130
180
|
|
|
131
181
|
def validate_meetings(pattern)
|
|
132
|
-
|
|
133
|
-
Batch.run(self, pattern, header: "🔍 Validating meetings") do |file|
|
|
134
|
-
schema_errors = validator.validate_file(file)
|
|
135
|
-
model_errors = collect_meeting_model_errors(file)
|
|
136
|
-
if schema_errors.empty? && model_errors.empty?
|
|
137
|
-
Batch::Result.ok("VALID")
|
|
138
|
-
else
|
|
139
|
-
errors = (schema_errors + model_errors).map(&:to_clickable_format)
|
|
140
|
-
Batch::Result.bad(errors)
|
|
141
|
-
end
|
|
142
|
-
end
|
|
182
|
+
run_validate(PROFILES.fetch(:meetings), pattern)
|
|
143
183
|
end
|
|
144
184
|
|
|
145
185
|
desc "normalize-meetings YAML_FILE_PATTERN",
|
|
@@ -149,24 +189,34 @@ module Edoxen
|
|
|
149
189
|
option :inplace, type: :boolean, desc: "Modify files in place (no backup)"
|
|
150
190
|
|
|
151
191
|
def normalize_meetings(pattern)
|
|
152
|
-
|
|
153
|
-
|
|
192
|
+
run_normalize(PROFILES.fetch(:meetings), pattern)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# --- Cross-document link checking ----------------------------------
|
|
196
|
+
|
|
197
|
+
desc "check-links DIRECTORY",
|
|
198
|
+
"Scan a directory of Meeting/DecisionCollection YAML and report " \
|
|
199
|
+
"any cross-document URN that does not resolve."
|
|
200
|
+
|
|
201
|
+
def check_links(directory)
|
|
202
|
+
unless File.directory?(directory)
|
|
203
|
+
say "Error: #{directory} is not a directory", :red
|
|
154
204
|
exit 1
|
|
155
205
|
end
|
|
156
206
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
].compact
|
|
207
|
+
say "🔗 Checking cross-document URN links in #{directory}...", :blue
|
|
208
|
+
errors = Edoxen::LinkChecker.check(directory)
|
|
209
|
+
return say("✅ No broken or duplicate links found", :green) if errors.empty?
|
|
161
210
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
Batch::Result.bad(["#{file}:1:1: #{e.message}"])
|
|
166
|
-
end
|
|
211
|
+
say "❌ Found #{errors.size} link error(s):", :red
|
|
212
|
+
errors.each { |e| say " #{e.message}", :red }
|
|
213
|
+
exit 1
|
|
167
214
|
end
|
|
168
215
|
|
|
216
|
+
# --- Reference-data lookups -----------------------------------------
|
|
217
|
+
|
|
169
218
|
desc "unlocode CODE", "Resolve a UN/LOCODE via the canonical registry."
|
|
219
|
+
|
|
170
220
|
def unlocode(code)
|
|
171
221
|
entry = Edoxen::ReferenceData.find_unlocode(code)
|
|
172
222
|
if entry.nil?
|
|
@@ -180,18 +230,59 @@ module Edoxen
|
|
|
180
230
|
say " Subdiv: #{entry.subdivision}" if entry.subdivision
|
|
181
231
|
coords = entry.coordinates
|
|
182
232
|
say " Coords: #{coords}" if coords
|
|
183
|
-
funcs = entry.
|
|
233
|
+
funcs = entry.function_codes.compact
|
|
184
234
|
say " Functions: #{funcs.join(", ")}" unless funcs.empty?
|
|
185
235
|
end
|
|
186
236
|
|
|
237
|
+
desc "iata CODE", "Resolve an IATA airport/city code via the canonical registry."
|
|
238
|
+
|
|
239
|
+
def iata(code)
|
|
240
|
+
entry = Edoxen::ReferenceData.find_iata(code)
|
|
241
|
+
if entry.nil?
|
|
242
|
+
say "No entry for #{code.upcase} in the IATA registry.", :red
|
|
243
|
+
exit 1
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
say "IATA: #{entry.code}", :blue
|
|
247
|
+
say " Name: #{entry.name}"
|
|
248
|
+
say " Country: #{entry.country_iso2}"
|
|
249
|
+
end
|
|
250
|
+
|
|
187
251
|
private
|
|
188
252
|
|
|
189
|
-
def
|
|
190
|
-
|
|
253
|
+
def run_validate(profile, pattern)
|
|
254
|
+
validator = SchemaValidator.new(profile.schema_path)
|
|
255
|
+
Batch.run(self, pattern, header: "🔍 Validating #{profile.name}") do |file|
|
|
256
|
+
schema_errors = validator.validate_file(file)
|
|
257
|
+
model_errors = collect_model_errors(profile, file)
|
|
258
|
+
if schema_errors.empty? && model_errors.empty?
|
|
259
|
+
Batch::Result.ok("VALID")
|
|
260
|
+
else
|
|
261
|
+
Batch::Result.bad((schema_errors + model_errors).map(&:to_clickable_format))
|
|
262
|
+
end
|
|
263
|
+
end
|
|
191
264
|
end
|
|
192
265
|
|
|
193
|
-
def
|
|
194
|
-
|
|
266
|
+
def run_normalize(profile, pattern)
|
|
267
|
+
unless valid_normalize_options?
|
|
268
|
+
say normalize_options_error, :red
|
|
269
|
+
exit 1
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
summary_extra = [
|
|
273
|
+
[" Output directory", options[:output]],
|
|
274
|
+
[" Mode", options[:inplace] ? "in place" : "--output"]
|
|
275
|
+
].compact
|
|
276
|
+
|
|
277
|
+
Batch.run(self, pattern, header: "🔄 Normalizing #{profile.name}", summary_extra: summary_extra) do |file|
|
|
278
|
+
Batch::Result.ok(normalize_file(profile, file))
|
|
279
|
+
rescue StandardError => e
|
|
280
|
+
Batch::Result.bad(["#{file}:1:1: #{e.message}"])
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def collect_model_errors(profile, file)
|
|
285
|
+
profile.load(File.read(file))
|
|
195
286
|
[]
|
|
196
287
|
rescue StandardError => e
|
|
197
288
|
[Edoxen::ValidationError.new(
|
|
@@ -201,27 +292,26 @@ module Edoxen
|
|
|
201
292
|
)]
|
|
202
293
|
end
|
|
203
294
|
|
|
204
|
-
def
|
|
205
|
-
|
|
206
|
-
|
|
295
|
+
def normalize_file(profile, file)
|
|
296
|
+
original = File.read(file)
|
|
297
|
+
schema_comment = extract_yaml_language_server_comment(original)
|
|
298
|
+
normalized = profile.load(original).to_yaml
|
|
299
|
+
write_normalized(file, normalized, schema_comment)
|
|
207
300
|
rescue StandardError => e
|
|
208
|
-
|
|
209
|
-
file: file, line: 1, column: 1,
|
|
210
|
-
message_text: "Meeting model parsing failed: #{e.message}",
|
|
211
|
-
source: Edoxen::ValidationError::SOURCE_MODEL
|
|
212
|
-
)]
|
|
301
|
+
raise "#{file}: #{e.message}"
|
|
213
302
|
end
|
|
214
303
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if data.is_a?(Hash) && data.key?("meetings")
|
|
222
|
-
MeetingCollection.from_yaml(File.read(file))
|
|
304
|
+
def write_normalized(file, normalized, schema_comment)
|
|
305
|
+
normalized = "#{schema_comment}\n#{normalized}" if schema_comment
|
|
306
|
+
|
|
307
|
+
if options[:inplace]
|
|
308
|
+
File.write(file, normalized)
|
|
309
|
+
"NORMALIZED"
|
|
223
310
|
else
|
|
224
|
-
|
|
311
|
+
out = File.join(options[:output], File.basename(file))
|
|
312
|
+
FileUtils.mkdir_p(File.dirname(out))
|
|
313
|
+
File.write(out, normalized)
|
|
314
|
+
"NORMALIZED → #{out}"
|
|
225
315
|
end
|
|
226
316
|
end
|
|
227
317
|
|
|
@@ -244,35 +334,5 @@ module Edoxen
|
|
|
244
334
|
"Error: Must specify either --output or --inplace option"
|
|
245
335
|
end
|
|
246
336
|
end
|
|
247
|
-
|
|
248
|
-
# Writes the normalized YAML either to the original file (--inplace)
|
|
249
|
-
# or under the --output directory. Returns a one-line status message
|
|
250
|
-
# for the batch runner to print after the ✅.
|
|
251
|
-
def normalize_file(file)
|
|
252
|
-
original = File.read(file)
|
|
253
|
-
yaml_language_server_comment = extract_yaml_language_server_comment(original)
|
|
254
|
-
normalized = ResolutionCollection.from_yaml(original).to_yaml
|
|
255
|
-
write_normalized(file, normalized, yaml_language_server_comment)
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
def normalize_meeting_file(file)
|
|
259
|
-
yaml_language_server_comment = extract_yaml_language_server_comment(File.read(file))
|
|
260
|
-
normalized = load_meeting_document(file).to_yaml
|
|
261
|
-
write_normalized(file, normalized, yaml_language_server_comment)
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def write_normalized(file, normalized, yaml_language_server_comment)
|
|
265
|
-
normalized = "#{yaml_language_server_comment}\n#{normalized}" if yaml_language_server_comment
|
|
266
|
-
|
|
267
|
-
if options[:inplace]
|
|
268
|
-
File.write(file, normalized)
|
|
269
|
-
"NORMALIZED"
|
|
270
|
-
else
|
|
271
|
-
out = File.join(options[:output], File.basename(file))
|
|
272
|
-
FileUtils.mkdir_p(File.dirname(out))
|
|
273
|
-
File.write(out, normalized)
|
|
274
|
-
"NORMALIZED → #{out}"
|
|
275
|
-
end
|
|
276
|
-
end
|
|
277
337
|
end
|
|
278
338
|
end
|
data/lib/edoxen/consideration.rb
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Edoxen
|
|
4
|
-
# Basis for a
|
|
5
|
-
# one effective date and the
|
|
4
|
+
# Basis for a Decision: a verb (having, noting, considering, ...) plus
|
|
5
|
+
# one effective date and the per-field Localized reasoning. Each
|
|
6
|
+
# `Consideration` belongs to a `Decision`; the message field carries
|
|
7
|
+
# one `LocalizedString` per ISO 24229 spelling.
|
|
6
8
|
class Consideration < Lutaml::Model::Serializable
|
|
7
9
|
attribute :type, :string, values: Enums::CONSIDERATION_TYPE
|
|
8
|
-
attribute :date_effective,
|
|
9
|
-
attribute :message, :
|
|
10
|
+
attribute :date_effective, DecisionDate
|
|
11
|
+
attribute :message, LocalizedString, collection: true
|
|
10
12
|
end
|
|
11
13
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# VCARD-like abstract contact. Generalises Person for cases where the
|
|
5
|
+
# contact may be a person, an organisation, a department, a role
|
|
6
|
+
# ("Secretariat"), or any other entity with a name and one or more
|
|
7
|
+
# communication channels.
|
|
8
|
+
#
|
|
9
|
+
# 1.0 (per-field localization, ISO 24229):
|
|
10
|
+
# - All translatable fields are Localized<String/Name>[0..*]
|
|
11
|
+
# (one entry per spelling/conversion system code).
|
|
12
|
+
# - Added: `urn` for registry storage; `ref` for reference-by-URN.
|
|
13
|
+
#
|
|
14
|
+
# Reference vs inline: when `ref` is set, treat this Contact as a
|
|
15
|
+
# reference (other fields ignored). When `ref` is unset, the Contact
|
|
16
|
+
# is inline (full data). Both patterns are valid on every field that
|
|
17
|
+
# accepts a Contact.
|
|
18
|
+
class Contact < Lutaml::Model::Serializable
|
|
19
|
+
attribute :ref, :string
|
|
20
|
+
attribute :urn, :string
|
|
21
|
+
attribute :name, LocalizedName, collection: true
|
|
22
|
+
attribute :kind, :string
|
|
23
|
+
attribute :role, :string
|
|
24
|
+
attribute :title, LocalizedString, collection: true
|
|
25
|
+
attribute :affiliation, LocalizedString, collection: true
|
|
26
|
+
attribute :contact_methods, ContactMethod, collection: true
|
|
27
|
+
attribute :identifiers, ContactIdentifier, collection: true
|
|
28
|
+
attribute :address, LocalizedString, collection: true
|
|
29
|
+
attribute :extensions, MeetingExtension, collection: true
|
|
30
|
+
|
|
31
|
+
def reference?
|
|
32
|
+
!ref.nil? && !ref.to_s.empty?
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|