edoxen 0.3.0 → 0.5.0
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/CLAUDE.md +24 -10
- data/README.adoc +170 -31
- data/TODO.meeting-agenda/01-design-decisions.md +132 -0
- data/TODO.meeting-agenda/02-lutaml-canonical.md +61 -0
- data/TODO.meeting-agenda/03-ruby-models.md +73 -0
- data/TODO.meeting-agenda/04-schema.md +34 -0
- data/TODO.meeting-agenda/05-fixtures.md +43 -0
- data/TODO.meeting-agenda/06-specs.md +52 -0
- data/TODO.meeting-agenda/07-cli.md +55 -0
- data/TODO.meeting-agenda/08-docs.md +35 -0
- data/TODO.meeting-agenda/09-verify-and-release.md +41 -0
- data/TODO.meeting-agenda/10-future-enhancements.md +72 -0
- data/TODO.meeting-agenda/README.md +30 -0
- data/lib/edoxen/action.rb +0 -6
- data/lib/edoxen/agenda.rb +26 -0
- data/lib/edoxen/agenda_item.rb +17 -0
- data/lib/edoxen/approval.rb +0 -7
- data/lib/edoxen/cli.rb +199 -95
- data/lib/edoxen/consideration.rb +0 -6
- data/lib/edoxen/date_range.rb +11 -0
- data/lib/edoxen/deadline.rb +10 -0
- data/lib/edoxen/enums.rb +24 -0
- data/lib/edoxen/error.rb +32 -2
- data/lib/edoxen/host_ref.rb +14 -0
- data/lib/edoxen/localization.rb +0 -12
- data/lib/edoxen/location.rb +15 -0
- data/lib/edoxen/meeting.rb +68 -0
- data/lib/edoxen/meeting_collection.rb +26 -0
- data/lib/edoxen/meeting_collection_metadata.rb +11 -0
- data/lib/edoxen/meeting_identifier.rb +0 -5
- data/lib/edoxen/meeting_localization.rb +15 -0
- data/lib/edoxen/meeting_relation.rb +12 -0
- data/lib/edoxen/person.rb +14 -0
- data/lib/edoxen/reference.rb +13 -0
- data/lib/edoxen/resolution.rb +21 -12
- data/lib/edoxen/resolution_collection.rb +0 -5
- data/lib/edoxen/resolution_date.rb +0 -5
- data/lib/edoxen/resolution_metadata.rb +5 -9
- data/lib/edoxen/resolution_relation.rb +0 -6
- data/lib/edoxen/schedule_item.rb +16 -0
- data/lib/edoxen/schema_validator.rb +12 -28
- data/lib/edoxen/source_url.rb +6 -8
- data/lib/edoxen/structured_identifier.rb +0 -5
- data/lib/edoxen/url.rb +0 -6
- data/lib/edoxen/version.rb +1 -1
- data/lib/edoxen.rb +17 -0
- data/schema/edoxen.yaml +7 -0
- data/schema/meeting.yaml +358 -0
- metadata +27 -2
data/lib/edoxen/cli.rb
CHANGED
|
@@ -4,54 +4,101 @@ require "thor"
|
|
|
4
4
|
require "fileutils"
|
|
5
5
|
|
|
6
6
|
module Edoxen
|
|
7
|
-
# Thor command-line surface for the gem. Two responsibilities:
|
|
8
|
-
# * `validate PATTERN` — runs both JSON-Schema validation and the
|
|
9
|
-
# Ruby model parser against each matching YAML file.
|
|
10
|
-
# * `normalize PATTERN (--output DIR | --inplace)` — round-trips each
|
|
11
|
-
# matching YAML file through the Ruby model, preserving any
|
|
12
|
-
# `# yaml-language-server: $schema=...` directive on the first line.
|
|
13
|
-
#
|
|
14
|
-
# The CLI deliberately does NOT own schema-or-model decisions — those
|
|
15
|
-
# live in `SchemaValidator` and `Lutaml::Model` respectively. It only
|
|
16
|
-
# glues them together and formats output.
|
|
17
7
|
class Cli < Thor
|
|
18
|
-
|
|
8
|
+
# Deep module behind the per-command interface. Owns the
|
|
9
|
+
# expand/sort/empty/header/loop/tally/summary/exit scaffold so
|
|
10
|
+
# `validate` and `normalize` collapse to per-file blocks.
|
|
11
|
+
#
|
|
12
|
+
# Commands call `Batch.run(self, pattern, header:)` and yield a block
|
|
13
|
+
# that returns `Result.ok(message)` or `Result.bad(errors)`. The
|
|
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.
|
|
19
|
+
module Batch
|
|
20
|
+
# Per-file outcome. `ok` carries an optional message appended to
|
|
21
|
+
# the success indicator (e.g. "NORMALIZED → /out/path"). `bad`
|
|
22
|
+
# carries a list of pre-formatted error strings.
|
|
23
|
+
Result = Struct.new(:status, :message, :errors) do
|
|
24
|
+
def self.ok(message = nil)
|
|
25
|
+
new(:ok, message, nil)
|
|
26
|
+
end
|
|
19
27
|
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
def self.bad(errors)
|
|
29
|
+
new(:bad, nil, Array(errors))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
# Run a batch over every YAML file matching `pattern`.
|
|
36
|
+
#
|
|
37
|
+
# Yields each file path to the caller's block. Block must return
|
|
38
|
+
# a Batch::Result. The batch runner handles progress output,
|
|
39
|
+
# tallies, summary, and the exit code.
|
|
40
|
+
def run(cli, pattern, header:, summary_extra: [])
|
|
41
|
+
files = expand(pattern)
|
|
42
|
+
if files.empty?
|
|
43
|
+
cli.say "No files found matching pattern: #{pattern}", :red
|
|
44
|
+
exit 1
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
cli.say "#{header} #{files.size} file(s)...", :blue
|
|
48
|
+
|
|
49
|
+
ok_count = 0
|
|
50
|
+
bad_count = 0
|
|
51
|
+
|
|
52
|
+
files.each do |file|
|
|
53
|
+
$stdout.print " #{File.basename(file)}... "
|
|
54
|
+
result = yield file
|
|
55
|
+
if result.status == :ok
|
|
56
|
+
label = result.message ? "✅ #{result.message}" : "✅"
|
|
57
|
+
cli.say label, :green
|
|
58
|
+
ok_count += 1
|
|
59
|
+
else
|
|
60
|
+
cli.say "❌ INVALID", :red
|
|
61
|
+
bad_count += 1
|
|
62
|
+
Array(result.errors).each { |e| cli.say " #{e}", :red }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
print_summary(cli, files.size, ok_count, bad_count, summary_extra)
|
|
67
|
+
exit(bad_count.positive? ? 1 : 0)
|
|
28
68
|
end
|
|
29
69
|
|
|
30
|
-
|
|
70
|
+
def expand(pattern)
|
|
71
|
+
Dir.glob(pattern).select { |f| File.file?(f) && f.match?(/\.ya?ml\z/i) }.sort
|
|
72
|
+
end
|
|
31
73
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
74
|
+
def print_summary(cli, total, ok_count, bad_count, summary_extra)
|
|
75
|
+
cli.say "\n📊 Summary:", :blue
|
|
76
|
+
cli.say " Total: #{total}", :blue
|
|
77
|
+
cli.say " Success: #{ok_count}, Failed: #{bad_count}",
|
|
78
|
+
bad_count.positive? ? :red : :green
|
|
79
|
+
rate = total.zero? ? 0 : ((ok_count.to_f / total) * 100).round(1)
|
|
80
|
+
cli.say " Success rate: #{rate}%", :blue
|
|
81
|
+
summary_extra.each { |label, value| cli.say " #{label}: #{value}", :blue }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
35
84
|
|
|
36
|
-
|
|
37
|
-
print " #{File.basename(file)}... "
|
|
85
|
+
package_name "edoxen"
|
|
38
86
|
|
|
87
|
+
desc "validate YAML_FILE_PATTERN",
|
|
88
|
+
"Validate one or more Edoxen YAML files against the schema and the model."
|
|
89
|
+
|
|
90
|
+
def validate(pattern)
|
|
91
|
+
validator = SchemaValidator.new
|
|
92
|
+
Batch.run(self, pattern, header: "🔍 Validating") do |file|
|
|
39
93
|
schema_errors = validator.validate_file(file)
|
|
40
94
|
model_errors = collect_model_errors(file)
|
|
41
|
-
|
|
42
95
|
if schema_errors.empty? && model_errors.empty?
|
|
43
|
-
|
|
44
|
-
valid_count += 1
|
|
96
|
+
Batch::Result.ok("VALID")
|
|
45
97
|
else
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
schema_errors.each { |e| say " #{e.to_clickable_format}", :red }
|
|
49
|
-
model_errors.each { |m| say " #{file}:1:1: #{m}", :red }
|
|
98
|
+
errors = (schema_errors + model_errors).map(&:to_clickable_format)
|
|
99
|
+
Batch::Result.bad(errors)
|
|
50
100
|
end
|
|
51
101
|
end
|
|
52
|
-
|
|
53
|
-
print_summary(files.size, valid_count, invalid_count, validator_type: :binary)
|
|
54
|
-
exit(invalid_count.positive? ? 1 : 0)
|
|
55
102
|
end
|
|
56
103
|
|
|
57
104
|
desc "normalize YAML_FILE_PATTERN",
|
|
@@ -61,77 +108,103 @@ module Edoxen
|
|
|
61
108
|
option :inplace, type: :boolean, desc: "Modify files in place (no backup)"
|
|
62
109
|
|
|
63
110
|
def normalize(pattern)
|
|
64
|
-
|
|
65
|
-
say
|
|
111
|
+
unless valid_normalize_options?
|
|
112
|
+
say normalize_options_error, :red
|
|
66
113
|
exit 1
|
|
67
114
|
end
|
|
68
115
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
116
|
+
summary_extra = [
|
|
117
|
+
[" Output directory", options[:output]],
|
|
118
|
+
[" Mode", options[:inplace] ? "in place" : "--output"]
|
|
119
|
+
].compact
|
|
73
120
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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}"])
|
|
78
125
|
end
|
|
126
|
+
end
|
|
79
127
|
|
|
80
|
-
|
|
128
|
+
desc "validate-meetings YAML_FILE_PATTERN",
|
|
129
|
+
"Validate Meeting/Agenda YAML file(s) against schema/meeting.yaml."
|
|
81
130
|
|
|
82
|
-
|
|
83
|
-
|
|
131
|
+
def validate_meetings(pattern)
|
|
132
|
+
validator = SchemaValidator.new(meeting_schema_path)
|
|
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
|
|
143
|
+
end
|
|
84
144
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
begin
|
|
88
|
-
yaml_language_server_comment = extract_yaml_language_server_comment(File.read(file))
|
|
89
|
-
normalized = ResolutionCollection.from_yaml(File.read(file)).to_yaml
|
|
90
|
-
normalized = "#{yaml_language_server_comment}\n#{normalized}" if yaml_language_server_comment
|
|
145
|
+
desc "normalize-meetings YAML_FILE_PATTERN",
|
|
146
|
+
"Round-trip Meeting YAML file(s) through the model (--output DIR or --inplace)."
|
|
91
147
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
say "✅ NORMALIZED → #{out}", :green
|
|
100
|
-
end
|
|
101
|
-
success_count += 1
|
|
102
|
-
rescue StandardError => e
|
|
103
|
-
say "❌ FAILED — #{e.message}", :red
|
|
104
|
-
error_count += 1
|
|
105
|
-
end
|
|
148
|
+
option :output, type: :string, desc: "Output directory for normalized files"
|
|
149
|
+
option :inplace, type: :boolean, desc: "Modify files in place (no backup)"
|
|
150
|
+
|
|
151
|
+
def normalize_meetings(pattern)
|
|
152
|
+
unless valid_normalize_options?
|
|
153
|
+
say normalize_options_error, :red
|
|
154
|
+
exit 1
|
|
106
155
|
end
|
|
107
156
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
].compact)
|
|
113
|
-
exit(error_count.positive? ? 1 : 0)
|
|
114
|
-
end
|
|
157
|
+
summary_extra = [
|
|
158
|
+
[" Output directory", options[:output]],
|
|
159
|
+
[" Mode", options[:inplace] ? "in place" : "--output"]
|
|
160
|
+
].compact
|
|
115
161
|
|
|
116
|
-
|
|
117
|
-
|
|
162
|
+
Batch.run(self, pattern, header: "🔄 Normalizing meetings", summary_extra: summary_extra) do |file|
|
|
163
|
+
Batch::Result.ok(normalize_meeting_file(file))
|
|
164
|
+
rescue StandardError => e
|
|
165
|
+
Batch::Result.bad(["#{file}:1:1: #{e.message}"])
|
|
166
|
+
end
|
|
118
167
|
end
|
|
119
168
|
|
|
120
169
|
private
|
|
121
170
|
|
|
122
|
-
def
|
|
123
|
-
|
|
171
|
+
def meeting_schema_path
|
|
172
|
+
File.expand_path("../../schema/meeting.yaml", __dir__)
|
|
124
173
|
end
|
|
125
174
|
|
|
126
|
-
# Round-trip the file through the model to catch structural issues
|
|
127
|
-
# (missing nested classes, type mismatches) that the JSON-Schema can't
|
|
128
|
-
# express. The model is permissive about field names — schema is the
|
|
129
|
-
# strict source.
|
|
130
175
|
def collect_model_errors(file)
|
|
131
176
|
ResolutionCollection.from_yaml(File.read(file))
|
|
132
177
|
[]
|
|
133
178
|
rescue StandardError => e
|
|
134
|
-
[
|
|
179
|
+
[Edoxen::ValidationError.new(
|
|
180
|
+
file: file, line: 1, column: 1,
|
|
181
|
+
message_text: "Model parsing failed: #{e.message}",
|
|
182
|
+
source: Edoxen::ValidationError::SOURCE_MODEL
|
|
183
|
+
)]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def collect_meeting_model_errors(file)
|
|
187
|
+
load_meeting_document(file)
|
|
188
|
+
[]
|
|
189
|
+
rescue StandardError => e
|
|
190
|
+
[Edoxen::ValidationError.new(
|
|
191
|
+
file: file, line: 1, column: 1,
|
|
192
|
+
message_text: "Meeting model parsing failed: #{e.message}",
|
|
193
|
+
source: Edoxen::ValidationError::SOURCE_MODEL
|
|
194
|
+
)]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# A meeting YAML may be either a single Meeting or a
|
|
198
|
+
# MeetingCollection. Detect by the presence of a top-level
|
|
199
|
+
# `meetings:` key and parse accordingly. Returns whichever
|
|
200
|
+
# model object was successfully constructed.
|
|
201
|
+
def load_meeting_document(file)
|
|
202
|
+
data = YAML.safe_load(File.read(file), permitted_classes: [Date, Time])
|
|
203
|
+
if data.is_a?(Hash) && data.key?("meetings")
|
|
204
|
+
MeetingCollection.from_yaml(File.read(file))
|
|
205
|
+
else
|
|
206
|
+
Meeting.from_yaml(File.read(file))
|
|
207
|
+
end
|
|
135
208
|
end
|
|
136
209
|
|
|
137
210
|
def extract_yaml_language_server_comment(content)
|
|
@@ -139,18 +212,49 @@ module Edoxen
|
|
|
139
212
|
lines.find { |l| l.strip.match?(/\A#\s*yaml-language-server:\s*\$schema=/) }&.rstrip
|
|
140
213
|
end
|
|
141
214
|
|
|
142
|
-
def
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
215
|
+
def valid_normalize_options?
|
|
216
|
+
return false if options[:output] && options[:inplace]
|
|
217
|
+
return false unless options[:output] || options[:inplace]
|
|
218
|
+
|
|
219
|
+
true
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def normalize_options_error
|
|
223
|
+
if options[:output] && options[:inplace]
|
|
224
|
+
"Error: Cannot use both --output and --inplace options"
|
|
225
|
+
else
|
|
226
|
+
"Error: Must specify either --output or --inplace option"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Writes the normalized YAML either to the original file (--inplace)
|
|
231
|
+
# or under the --output directory. Returns a one-line status message
|
|
232
|
+
# for the batch runner to print after the ✅.
|
|
233
|
+
def normalize_file(file)
|
|
234
|
+
original = File.read(file)
|
|
235
|
+
yaml_language_server_comment = extract_yaml_language_server_comment(original)
|
|
236
|
+
normalized = ResolutionCollection.from_yaml(original).to_yaml
|
|
237
|
+
write_normalized(file, normalized, yaml_language_server_comment)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def normalize_meeting_file(file)
|
|
241
|
+
yaml_language_server_comment = extract_yaml_language_server_comment(File.read(file))
|
|
242
|
+
normalized = load_meeting_document(file).to_yaml
|
|
243
|
+
write_normalized(file, normalized, yaml_language_server_comment)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def write_normalized(file, normalized, yaml_language_server_comment)
|
|
247
|
+
normalized = "#{yaml_language_server_comment}\n#{normalized}" if yaml_language_server_comment
|
|
248
|
+
|
|
249
|
+
if options[:inplace]
|
|
250
|
+
File.write(file, normalized)
|
|
251
|
+
"NORMALIZED"
|
|
252
|
+
else
|
|
253
|
+
out = File.join(options[:output], File.basename(file))
|
|
254
|
+
FileUtils.mkdir_p(File.dirname(out))
|
|
255
|
+
File.write(out, normalized)
|
|
256
|
+
"NORMALIZED → #{out}"
|
|
257
|
+
end
|
|
154
258
|
end
|
|
155
259
|
end
|
|
156
260
|
end
|
data/lib/edoxen/consideration.rb
CHANGED
|
@@ -7,11 +7,5 @@ module Edoxen
|
|
|
7
7
|
attribute :type, :string, values: Enums::CONSIDERATION_TYPE
|
|
8
8
|
attribute :date_effective, ResolutionDate
|
|
9
9
|
attribute :message, :string
|
|
10
|
-
|
|
11
|
-
key_value do
|
|
12
|
-
map "type", to: :type
|
|
13
|
-
map "date_effective", to: :date_effective
|
|
14
|
-
map "message", to: :message
|
|
15
|
-
end
|
|
16
10
|
end
|
|
17
11
|
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Start + end pair for multi-day meetings. Both endpoints are Date.
|
|
5
|
+
# Either may be nil for one-sided ranges (theoretically), though the
|
|
6
|
+
# schema enforces both required when DateRange is present.
|
|
7
|
+
class DateRange < Lutaml::Model::Serializable
|
|
8
|
+
attribute :start, :date
|
|
9
|
+
attribute :end, :date
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# A time-bound requirement leading up to (or following) a meeting —
|
|
5
|
+
# registration cutoff, report-submission deadline, etc.
|
|
6
|
+
class Deadline < Lutaml::Model::Serializable
|
|
7
|
+
attribute :date, :date
|
|
8
|
+
attribute :description, :string
|
|
9
|
+
end
|
|
10
|
+
end
|
data/lib/edoxen/enums.rb
CHANGED
|
@@ -39,5 +39,29 @@ module Edoxen
|
|
|
39
39
|
APPROVAL_DEGREE = %w[unanimous majority minority].freeze
|
|
40
40
|
|
|
41
41
|
URL_KIND = %w[access report].freeze
|
|
42
|
+
|
|
43
|
+
# --- Meeting/Agenda side ----------------------------------------------
|
|
44
|
+
# Mirrors edoxen-model/models/{meeting,agenda,...}_*.lutaml.
|
|
45
|
+
# schema/meeting.yaml references these via $defs/<EnumName>.enum.
|
|
46
|
+
|
|
47
|
+
MEETING_TYPE = %w[plenary working_group task_group ad_hoc joint conference_session].freeze
|
|
48
|
+
|
|
49
|
+
MEETING_STATUS = %w[upcoming completed cancelled].freeze
|
|
50
|
+
|
|
51
|
+
AGENDA_STATUS = %w[draft final amended cancelled superseded].freeze
|
|
52
|
+
|
|
53
|
+
AGENDA_ITEM_KIND = %w[numbered unnumbered header opening closing].freeze
|
|
54
|
+
|
|
55
|
+
AGENDA_ITEM_OUTCOME = %w[discussed resolved deferred adopted withdrawn].freeze
|
|
56
|
+
|
|
57
|
+
HOST_TYPE = %w[national_body liaison associate organizer].freeze
|
|
58
|
+
|
|
59
|
+
MEETING_RELATION_TYPE = %w[
|
|
60
|
+
continues_from continues_to joint_with supersedes superseded_by rescheduled_from rescheduled_to
|
|
61
|
+
].freeze
|
|
62
|
+
|
|
63
|
+
SOURCE_URL_KIND = %w[
|
|
64
|
+
agenda_pdf minutes_pdf resolutions_pdf report_pdf register_url landing_page
|
|
65
|
+
].freeze
|
|
42
66
|
end
|
|
43
67
|
end
|
data/lib/edoxen/error.rb
CHANGED
|
@@ -1,7 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Edoxen
|
|
4
|
-
# Base class for any Edoxen-level error. Reserved for raise-on-failure paths
|
|
5
|
-
# schema validation errors live under SchemaValidator::ValidationError.
|
|
4
|
+
# Base class for any Edoxen-level error. Reserved for raise-on-failure paths.
|
|
6
5
|
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Unified validation failure shape. Produced by both:
|
|
8
|
+
# * SchemaValidator — JSON-Schema violations and YAML syntax errors
|
|
9
|
+
# carry source: :schema (or :syntax for Psych::SyntaxError).
|
|
10
|
+
# * ResolutionCollection.from_yaml rescues in the CLI — model parse
|
|
11
|
+
# failures carry source: :model.
|
|
12
|
+
#
|
|
13
|
+
# One type at the seam means callers (CLI, future renderers, tests)
|
|
14
|
+
# handle one shape instead of N.
|
|
15
|
+
class ValidationError < Error
|
|
16
|
+
SOURCE_SCHEMA = :schema
|
|
17
|
+
SOURCE_MODEL = :model
|
|
18
|
+
SOURCE_SYNTAX = :syntax
|
|
19
|
+
|
|
20
|
+
attr_reader :file, :line, :column, :pointer, :message_text, :source
|
|
21
|
+
|
|
22
|
+
def initialize(file:, line:, column:, message_text:, pointer: "", source: SOURCE_SCHEMA)
|
|
23
|
+
@file = file
|
|
24
|
+
@line = line
|
|
25
|
+
@column = column
|
|
26
|
+
@pointer = pointer.to_s
|
|
27
|
+
@message_text = message_text
|
|
28
|
+
@source = source
|
|
29
|
+
super(to_clickable_format)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_clickable_format
|
|
33
|
+
suffix = @pointer.empty? ? "" : " at `#{@pointer}`"
|
|
34
|
+
"#{@file}:#{@line}:#{@column}: #{@message_text}#{suffix}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
7
37
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Typed reference to an organization that hosts or co-organizes a
|
|
5
|
+
# meeting. `ref` is a stable identifier into an external registry
|
|
6
|
+
# (national_bodies.yml, liaisons.yml, associates.yml in ISO/TC 154);
|
|
7
|
+
# `type` discriminates which registry.
|
|
8
|
+
class HostRef < Lutaml::Model::Serializable
|
|
9
|
+
attribute :ref, :string
|
|
10
|
+
attribute :type, :string, values: Enums::HOST_TYPE
|
|
11
|
+
attribute :role, :string
|
|
12
|
+
attribute :contact, Person
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/edoxen/localization.rb
CHANGED
|
@@ -14,17 +14,5 @@ module Edoxen
|
|
|
14
14
|
attribute :considerations, Consideration, collection: true
|
|
15
15
|
attribute :approvals, Approval, collection: true
|
|
16
16
|
attribute :actions, Action, collection: true
|
|
17
|
-
|
|
18
|
-
key_value do
|
|
19
|
-
map "language_code", to: :language_code
|
|
20
|
-
map "script", to: :script
|
|
21
|
-
map "title", to: :title
|
|
22
|
-
map "subject", to: :subject
|
|
23
|
-
map "message", to: :message
|
|
24
|
-
map "considering", to: :considering
|
|
25
|
-
map "considerations", to: :considerations
|
|
26
|
-
map "approvals", to: :approvals
|
|
27
|
-
map "actions", to: :actions
|
|
28
|
-
end
|
|
29
17
|
end
|
|
30
18
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Venue geography. Multi-venue meetings (e.g., a plenary week with
|
|
5
|
+
# different hotels) carry one Location per venue.
|
|
6
|
+
class Location < Lutaml::Model::Serializable
|
|
7
|
+
attribute :name, :string
|
|
8
|
+
attribute :address, :string
|
|
9
|
+
attribute :link, :string
|
|
10
|
+
attribute :phone, :string
|
|
11
|
+
attribute :note, :string
|
|
12
|
+
attribute :lat, :float
|
|
13
|
+
attribute :lon, :float
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/edoxen/meeting.rb
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# A single Meeting — the event that produces Resolutions. Carries
|
|
5
|
+
# identity, time, venue, officers, agenda, schedule, deadlines, and
|
|
6
|
+
# URN links to one or more ResolutionCollection documents.
|
|
7
|
+
#
|
|
8
|
+
# Meetings and ResolutionCollections are kept as separate documents
|
|
9
|
+
# and joined by URN (Meeting.resolution_refs ↔
|
|
10
|
+
# ResolutionCollection.metadata.meeting_urn) because they have
|
|
11
|
+
# different lifetimes: agendas exist weeks before a meeting;
|
|
12
|
+
# resolutions only after adoption.
|
|
13
|
+
class Meeting < Lutaml::Model::Serializable
|
|
14
|
+
attribute :identifier, StructuredIdentifier, collection: true
|
|
15
|
+
attribute :urn, :string
|
|
16
|
+
attribute :ordinal, :integer
|
|
17
|
+
attribute :type, :string, values: Enums::MEETING_TYPE
|
|
18
|
+
attribute :status, :string, values: Enums::MEETING_STATUS
|
|
19
|
+
attribute :year, :integer
|
|
20
|
+
|
|
21
|
+
attribute :date_range, DateRange
|
|
22
|
+
|
|
23
|
+
attribute :committee, :string
|
|
24
|
+
attribute :committee_group, :string
|
|
25
|
+
|
|
26
|
+
attribute :venues, Location, collection: true
|
|
27
|
+
attribute :general_area, :string
|
|
28
|
+
attribute :city, :string
|
|
29
|
+
attribute :country_code, :string
|
|
30
|
+
attribute :virtual, :boolean
|
|
31
|
+
|
|
32
|
+
attribute :chair, Person
|
|
33
|
+
attribute :secretary, Person
|
|
34
|
+
attribute :host, :string
|
|
35
|
+
attribute :hosts, HostRef, collection: true
|
|
36
|
+
|
|
37
|
+
attribute :source_urls, SourceUrl, collection: true
|
|
38
|
+
attribute :landing_url, :string
|
|
39
|
+
attribute :registration_url, :string
|
|
40
|
+
|
|
41
|
+
attribute :agenda, Agenda
|
|
42
|
+
attribute :schedule, ScheduleItem, collection: true
|
|
43
|
+
attribute :deadlines, Deadline, collection: true
|
|
44
|
+
|
|
45
|
+
attribute :localizations, MeetingLocalization, collection: true
|
|
46
|
+
attribute :relations, MeetingRelation, collection: true
|
|
47
|
+
attribute :resolution_refs, :string, collection: true
|
|
48
|
+
|
|
49
|
+
# Lookup by ISO 639-3 language code. Mirrors Resolution#in_language.
|
|
50
|
+
def in_language(code, fallback: false)
|
|
51
|
+
match = localizations&.find { |loc| loc.language_code == code.to_s }
|
|
52
|
+
return match if match
|
|
53
|
+
|
|
54
|
+
fallback ? localizations&.first : nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# English when available, else the first declared localization.
|
|
58
|
+
def primary_localization
|
|
59
|
+
in_language("eng", fallback: true)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Find an agenda item by label. Returns nil when no agenda or no
|
|
63
|
+
# matching label.
|
|
64
|
+
def find_agenda_item(label)
|
|
65
|
+
agenda&.find_item(label)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Top-level container for many Meetings. Parallel to
|
|
5
|
+
# ResolutionCollection.
|
|
6
|
+
class MeetingCollection < Lutaml::Model::Serializable
|
|
7
|
+
attribute :metadata, MeetingCollectionMetadata
|
|
8
|
+
attribute :meetings, Meeting, collection: true
|
|
9
|
+
|
|
10
|
+
# Find a meeting by URN. Returns nil when no meeting matches.
|
|
11
|
+
def find_by_urn(urn)
|
|
12
|
+
meetings&.find { |m| m.urn == urn.to_s }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Find a meeting by StructuredIdentifier components.
|
|
16
|
+
# Returns the first meeting whose any identifier matches both
|
|
17
|
+
# prefix and number.
|
|
18
|
+
def find_by_identifier(prefix:, number:)
|
|
19
|
+
meetings&.find do |meeting|
|
|
20
|
+
meeting.identifier&.any? do |id|
|
|
21
|
+
id.prefix == prefix.to_s && id.number == number.to_s
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Top-level wrapper for many Meetings in a single YAML file. Parallel
|
|
5
|
+
# to ResolutionCollection. The metadata block carries display-level
|
|
6
|
+
# info (title, source); per-meeting identity lives on each Meeting.
|
|
7
|
+
class MeetingCollectionMetadata < Lutaml::Model::Serializable
|
|
8
|
+
attribute :title, :string
|
|
9
|
+
attribute :source, :string
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Per-language content for a Meeting. Mirrors the glossarist
|
|
5
|
+
# pattern used by `Localization` for Resolutions: language-agnostic
|
|
6
|
+
# admin fields live on the parent Meeting; per-language content
|
|
7
|
+
# lives here.
|
|
8
|
+
class MeetingLocalization < Lutaml::Model::Serializable
|
|
9
|
+
attribute :language_code, :string
|
|
10
|
+
attribute :script, :string
|
|
11
|
+
attribute :title, :string
|
|
12
|
+
attribute :general_area, :string
|
|
13
|
+
attribute :practical_info, :string
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Directed link between two meetings, identified by their
|
|
5
|
+
# StructuredIdentifier. Reuses the same relation-type vocabulary
|
|
6
|
+
# pattern as ResolutionRelation.
|
|
7
|
+
class MeetingRelation < Lutaml::Model::Serializable
|
|
8
|
+
attribute :source, StructuredIdentifier
|
|
9
|
+
attribute :destination, StructuredIdentifier
|
|
10
|
+
attribute :type, :string, values: Enums::MEETING_RELATION_TYPE
|
|
11
|
+
end
|
|
12
|
+
end
|