edoxen 0.1.2 → 0.3.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/CLAUDE.md +234 -0
- data/README.adoc +153 -498
- data/lib/edoxen/_metadata.rb.deprecated +57 -0
- data/lib/edoxen/action.rb +4 -25
- data/lib/edoxen/approval.rb +5 -16
- data/lib/edoxen/cli.rb +137 -129
- data/lib/edoxen/consideration.rb +4 -18
- data/lib/edoxen/enums.rb +43 -0
- data/lib/edoxen/error.rb +37 -0
- data/lib/edoxen/localization.rb +18 -0
- data/lib/edoxen/meeting_identifier.rb +2 -11
- data/lib/edoxen/resolution.rb +32 -36
- data/lib/edoxen/resolution_collection.rb +10 -0
- data/lib/edoxen/resolution_date.rb +5 -11
- data/lib/edoxen/resolution_metadata.rb +16 -0
- data/lib/edoxen/resolution_relation.rb +5 -45
- data/lib/edoxen/schema_validator.rb +158 -262
- data/lib/edoxen/source_url.rb +12 -0
- data/lib/edoxen/structured_identifier.rb +12 -0
- data/lib/edoxen/url.rb +2 -9
- data/lib/edoxen/version.rb +1 -1
- data/lib/edoxen.rb +37 -14
- data/schema/edoxen.yaml +300 -339
- metadata +11 -3
- data/lib/edoxen/metadata.rb +0 -27
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
require_relative "resolution_date"
|
|
5
|
+
require_relative "url"
|
|
6
|
+
require_relative "localization"
|
|
7
|
+
|
|
8
|
+
module Edoxen
|
|
9
|
+
# A per-language source-URL record. Carries the URL ref, its format
|
|
10
|
+
# (pdf, html, ...), and the language_code (ISO 639-3) for which the
|
|
11
|
+
# URL is the canonical source.
|
|
12
|
+
class SourceUrl < Lutaml::Model::Serializable
|
|
13
|
+
attribute :ref, :string
|
|
14
|
+
attribute :format, :string
|
|
15
|
+
attribute :language_code, :string
|
|
16
|
+
|
|
17
|
+
key_value do
|
|
18
|
+
map "ref", to: :ref
|
|
19
|
+
map "format", to: :format
|
|
20
|
+
map "language_code", to: :language_code
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class Metadata < Lutaml::Model::Serializable
|
|
25
|
+
attribute :title, :string
|
|
26
|
+
attribute :identifier, :string
|
|
27
|
+
attribute :dates, ResolutionDate, collection: true
|
|
28
|
+
attribute :source, :string
|
|
29
|
+
attribute :venue, :string
|
|
30
|
+
attribute :chair, :string
|
|
31
|
+
attribute :urls, Url, collection: true
|
|
32
|
+
|
|
33
|
+
# OIML extensions — see TODO.complete/14 for the glossarist-style
|
|
34
|
+
# i18n model. `title_localized` carries the per-language title
|
|
35
|
+
# parallel to Resolution#localizations. `source_urls` carries the
|
|
36
|
+
# per-language PDF URLs. `city`/`country_code` carry the IATA /
|
|
37
|
+
# ISO 3166-1 alpha-2 codes for the host venue.
|
|
38
|
+
attribute :title_localized, Localization, collection: true
|
|
39
|
+
attribute :source_urls, SourceUrl, collection: true
|
|
40
|
+
attribute :city, :string
|
|
41
|
+
attribute :country_code, :string
|
|
42
|
+
|
|
43
|
+
key_value do
|
|
44
|
+
map "title", to: :title
|
|
45
|
+
map "identifier", to: :identifier
|
|
46
|
+
map "dates", to: :dates
|
|
47
|
+
map "source", to: :source
|
|
48
|
+
map "venue", to: :venue
|
|
49
|
+
map "chair", to: :chair
|
|
50
|
+
map "urls", to: :urls
|
|
51
|
+
map "title_localized", to: :title_localized
|
|
52
|
+
map "source_urls", to: :source_urls
|
|
53
|
+
map "city", to: :city
|
|
54
|
+
map "country_code", to: :country_code
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/edoxen/action.rb
CHANGED
|
@@ -1,32 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "lutaml/model"
|
|
4
|
-
require_relative "resolution_date"
|
|
5
|
-
|
|
6
3
|
module Edoxen
|
|
4
|
+
# Verb + one effective date + human-readable message. Used inside a
|
|
5
|
+
# Localization to express the multilingual part of an action.
|
|
7
6
|
class Action < Lutaml::Model::Serializable
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
appreciation approves asks assigns chairs communicating confirms consults considers
|
|
11
|
-
creates decides defines delegates delivering directs disbands drafting elects
|
|
12
|
-
empowers encourages endorses estabilishes establishes gathering identifies
|
|
13
|
-
instructs investigates notes notifies recognises nominates
|
|
14
|
-
recognizes recommends registers regrets request requests resolves restates reminds replaces
|
|
15
|
-
scopes secures sends supports thanks welcomes withdraws
|
|
16
|
-
].freeze
|
|
17
|
-
|
|
18
|
-
attribute :type, :string, values: ACTION_TYPE_ENUM
|
|
19
|
-
attribute :dates, ResolutionDate, collection: true
|
|
7
|
+
attribute :type, :string, values: Enums::ACTION_TYPE
|
|
8
|
+
attribute :date_effective, ResolutionDate
|
|
20
9
|
attribute :message, :string
|
|
21
|
-
attribute :subject, :string
|
|
22
|
-
attribute :degree, :string
|
|
23
|
-
|
|
24
|
-
key_value do
|
|
25
|
-
map "type", to: :type
|
|
26
|
-
map "message", to: :message
|
|
27
|
-
map "subject", to: :subject
|
|
28
|
-
map "degree", to: :degree
|
|
29
|
-
map "dates", to: :dates
|
|
30
|
-
end
|
|
31
10
|
end
|
|
32
11
|
end
|
data/lib/edoxen/approval.rb
CHANGED
|
@@ -1,23 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "lutaml/model"
|
|
4
|
-
require_relative "resolution_date"
|
|
5
|
-
|
|
6
3
|
module Edoxen
|
|
4
|
+
# Approval record: type (affirmative / negative), degree (consensus level),
|
|
5
|
+
# date of the approval event, and a human-readable elaboration.
|
|
7
6
|
class Approval < Lutaml::Model::Serializable
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
attribute :type, :string, values: APPROVAL_TYPE_ENUM
|
|
12
|
-
attribute :degree, :string, values: APPROVAL_DEGREE_ENUM
|
|
13
|
-
attribute :dates, ResolutionDate, collection: true
|
|
7
|
+
attribute :type, :string, values: Enums::APPROVAL_TYPE
|
|
8
|
+
attribute :degree, :string, values: Enums::APPROVAL_DEGREE
|
|
9
|
+
attribute :date, ResolutionDate
|
|
14
10
|
attribute :message, :string
|
|
15
|
-
|
|
16
|
-
key_value do
|
|
17
|
-
map "type", to: :type
|
|
18
|
-
map "degree", to: :degree
|
|
19
|
-
map "dates", to: :dates
|
|
20
|
-
map "message", to: :message
|
|
21
|
-
end
|
|
22
11
|
end
|
|
23
12
|
end
|
data/lib/edoxen/cli.rb
CHANGED
|
@@ -2,172 +2,180 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require "fileutils"
|
|
5
|
-
require_relative "schema_validator"
|
|
6
5
|
|
|
7
6
|
module Edoxen
|
|
8
7
|
class Cli < Thor
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
12
27
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
28
|
+
def self.bad(errors)
|
|
29
|
+
new(:bad, nil, Array(errors))
|
|
30
|
+
end
|
|
16
31
|
end
|
|
17
32
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# Model parsing validation
|
|
31
|
-
model_errors = []
|
|
32
|
-
begin
|
|
33
|
-
content = File.read(file)
|
|
34
|
-
Edoxen::ResolutionSet.from_yaml(content)
|
|
35
|
-
rescue StandardError => e
|
|
36
|
-
model_errors << "Model parsing failed: #{e.message}"
|
|
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
|
|
37
45
|
end
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
say "❌ INVALID", :red
|
|
44
|
-
invalid_count += 1
|
|
45
|
-
|
|
46
|
-
# Show schema validation errors with clickable links
|
|
47
|
-
unless schema_errors.empty?
|
|
48
|
-
say " Schema Validation Errors:", :red
|
|
49
|
-
schema_errors.each do |error|
|
|
50
|
-
say " #{error.to_clickable_format}", :red
|
|
51
|
-
end
|
|
52
|
-
end
|
|
47
|
+
cli.say "#{header} #{files.size} file(s)...", :blue
|
|
48
|
+
|
|
49
|
+
ok_count = 0
|
|
50
|
+
bad_count = 0
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 }
|
|
60
63
|
end
|
|
61
64
|
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
say "\n📊 Validation Summary:", :blue
|
|
65
|
-
say " Valid files: #{valid_count}", :green
|
|
66
|
-
say " Invalid files: #{invalid_count}", invalid_count.positive? ? :red : :green
|
|
67
|
-
say " Success rate: #{((valid_count.to_f / files.size) * 100).round(1)}%", :blue
|
|
68
|
-
|
|
69
|
-
exit(invalid_count.positive? ? 1 : 0)
|
|
70
|
-
end
|
|
71
65
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
option :inplace, type: :boolean, desc: "Modify files in place (no backup)"
|
|
75
|
-
def normalize(pattern)
|
|
76
|
-
if options[:output] && options[:inplace]
|
|
77
|
-
say "Error: Cannot use both --output and --inplace options", :red
|
|
78
|
-
exit 1
|
|
66
|
+
print_summary(cli, files.size, ok_count, bad_count, summary_extra)
|
|
67
|
+
exit(bad_count.positive? ? 1 : 0)
|
|
79
68
|
end
|
|
80
69
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
exit 1
|
|
70
|
+
def expand(pattern)
|
|
71
|
+
Dir.glob(pattern).select { |f| File.file?(f) && f.match?(/\.ya?ml\z/i) }.sort
|
|
84
72
|
end
|
|
85
73
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
say "
|
|
90
|
-
|
|
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 }
|
|
91
82
|
end
|
|
83
|
+
end
|
|
92
84
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
success_count = 0
|
|
96
|
-
error_count = 0
|
|
97
|
-
|
|
98
|
-
files.each do |file|
|
|
99
|
-
print " #{File.basename(file)}... "
|
|
100
|
-
|
|
101
|
-
begin
|
|
102
|
-
# Load and parse the file
|
|
103
|
-
content = File.read(file)
|
|
104
|
-
|
|
105
|
-
# Extract yaml-language-server comment if present
|
|
106
|
-
yaml_language_server_comment = extract_yaml_language_server_comment(content)
|
|
85
|
+
package_name "edoxen"
|
|
107
86
|
|
|
108
|
-
|
|
87
|
+
desc "validate YAML_FILE_PATTERN",
|
|
88
|
+
"Validate one or more Edoxen YAML files against the schema and the model."
|
|
109
89
|
|
|
110
|
-
|
|
111
|
-
|
|
90
|
+
def validate(pattern)
|
|
91
|
+
validator = SchemaValidator.new
|
|
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
|
|
102
|
+
end
|
|
112
103
|
|
|
113
|
-
|
|
114
|
-
|
|
104
|
+
desc "normalize YAML_FILE_PATTERN",
|
|
105
|
+
"Round-trip YAML file(s) through the Edoxen model (--output DIR or --inplace)."
|
|
115
106
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
File.write(file, normalized_yaml)
|
|
119
|
-
say "✅ NORMALIZED", :green
|
|
120
|
-
else
|
|
121
|
-
# Write to output directory
|
|
122
|
-
output_file = File.join(options[:output], File.basename(file))
|
|
123
|
-
FileUtils.mkdir_p(File.dirname(output_file))
|
|
124
|
-
File.write(output_file, normalized_yaml)
|
|
125
|
-
say "✅ NORMALIZED → #{output_file}", :green
|
|
126
|
-
end
|
|
107
|
+
option :output, type: :string, desc: "Output directory for normalized files"
|
|
108
|
+
option :inplace, type: :boolean, desc: "Modify files in place (no backup)"
|
|
127
109
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
error_count += 1
|
|
133
|
-
end
|
|
110
|
+
def normalize(pattern)
|
|
111
|
+
unless valid_normalize_options?
|
|
112
|
+
say normalize_options_error, :red
|
|
113
|
+
exit 1
|
|
134
114
|
end
|
|
135
115
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
116
|
+
summary_extra = [
|
|
117
|
+
[" Output directory", options[:output]],
|
|
118
|
+
[" Mode", options[:inplace] ? "in place" : "--output"]
|
|
119
|
+
].compact
|
|
140
120
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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}"])
|
|
145
125
|
end
|
|
146
|
-
|
|
147
|
-
exit(error_count.positive? ? 1 : 0)
|
|
148
126
|
end
|
|
149
127
|
|
|
150
128
|
private
|
|
151
129
|
|
|
152
|
-
def
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
130
|
+
def collect_model_errors(file)
|
|
131
|
+
ResolutionCollection.from_yaml(File.read(file))
|
|
132
|
+
[]
|
|
133
|
+
rescue StandardError => e
|
|
134
|
+
[Edoxen::ValidationError.new(
|
|
135
|
+
file: file, line: 1, column: 1,
|
|
136
|
+
message_text: "Model parsing failed: #{e.message}",
|
|
137
|
+
source: Edoxen::ValidationError::SOURCE_MODEL
|
|
138
|
+
)]
|
|
158
139
|
end
|
|
159
140
|
|
|
160
141
|
def extract_yaml_language_server_comment(content)
|
|
161
|
-
lines = content.split("\n")
|
|
142
|
+
lines = content.split("\n").first(5)
|
|
143
|
+
lines.find { |l| l.strip.match?(/\A#\s*yaml-language-server:\s*\$schema=/) }&.rstrip
|
|
144
|
+
end
|
|
162
145
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
146
|
+
def valid_normalize_options?
|
|
147
|
+
return false if options[:output] && options[:inplace]
|
|
148
|
+
return false unless options[:output] || options[:inplace]
|
|
149
|
+
|
|
150
|
+
true
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def normalize_options_error
|
|
154
|
+
if options[:output] && options[:inplace]
|
|
155
|
+
"Error: Cannot use both --output and --inplace options"
|
|
156
|
+
else
|
|
157
|
+
"Error: Must specify either --output or --inplace option"
|
|
168
158
|
end
|
|
159
|
+
end
|
|
169
160
|
|
|
170
|
-
|
|
161
|
+
# Writes the normalized YAML either to the original file (--inplace)
|
|
162
|
+
# or under the --output directory. Returns a one-line status message
|
|
163
|
+
# for the batch runner to print after the ✅.
|
|
164
|
+
def normalize_file(file)
|
|
165
|
+
original = File.read(file)
|
|
166
|
+
yaml_language_server_comment = extract_yaml_language_server_comment(original)
|
|
167
|
+
normalized = ResolutionCollection.from_yaml(original).to_yaml
|
|
168
|
+
normalized = "#{yaml_language_server_comment}\n#{normalized}" if yaml_language_server_comment
|
|
169
|
+
|
|
170
|
+
if options[:inplace]
|
|
171
|
+
File.write(file, normalized)
|
|
172
|
+
"NORMALIZED"
|
|
173
|
+
else
|
|
174
|
+
out = File.join(options[:output], File.basename(file))
|
|
175
|
+
FileUtils.mkdir_p(File.dirname(out))
|
|
176
|
+
File.write(out, normalized)
|
|
177
|
+
"NORMALIZED → #{out}"
|
|
178
|
+
end
|
|
171
179
|
end
|
|
172
180
|
end
|
|
173
181
|
end
|
data/lib/edoxen/consideration.rb
CHANGED
|
@@ -1,25 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "lutaml/model"
|
|
4
|
-
require_relative "resolution_date"
|
|
5
|
-
|
|
6
3
|
module Edoxen
|
|
4
|
+
# Basis for a resolution: a verb (having, noting, considering, ...) plus
|
|
5
|
+
# one effective date and the elaborated reasoning.
|
|
7
6
|
class Consideration < Lutaml::Model::Serializable
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
recognising recognizing
|
|
11
|
-
].freeze
|
|
12
|
-
|
|
13
|
-
attribute :type, :string, values: CONSIDERATION_TYPE_ENUM
|
|
14
|
-
attribute :dates, ResolutionDate, collection: true
|
|
7
|
+
attribute :type, :string, values: Enums::CONSIDERATION_TYPE
|
|
8
|
+
attribute :date_effective, ResolutionDate
|
|
15
9
|
attribute :message, :string
|
|
16
|
-
attribute :subject, :string
|
|
17
|
-
|
|
18
|
-
key_value do
|
|
19
|
-
map "type", to: :type
|
|
20
|
-
map "dates", to: :dates
|
|
21
|
-
map "message", to: :message
|
|
22
|
-
map "subject", to: :subject
|
|
23
|
-
end
|
|
24
10
|
end
|
|
25
11
|
end
|
data/lib/edoxen/enums.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Single source of truth for every enum used by the Edoxen information model.
|
|
5
|
+
#
|
|
6
|
+
# Mirrors ../edoxen-model/models/*.lutaml, deduped.
|
|
7
|
+
# Both:
|
|
8
|
+
# * Ruby model attribute declarations (`attribute :type, :string, values: Enums::ACTION_TYPE`)
|
|
9
|
+
# * JSON-Schema (`schema/edoxen.yaml`)
|
|
10
|
+
# reference these constants.
|
|
11
|
+
#
|
|
12
|
+
# The schema <-> Ruby enum-sync spec asserts the YAML schema's enum arrays
|
|
13
|
+
# equal these arrays. If you change a constant here, change the schema in
|
|
14
|
+
# the same PR.
|
|
15
|
+
module Enums
|
|
16
|
+
ACTION_TYPE = %w[
|
|
17
|
+
adopts thanks approves decides declares asks invites
|
|
18
|
+
resolves confirms welcomes recommends requests congratulates
|
|
19
|
+
instructs urges appoints calls-upon encourages affirms elects
|
|
20
|
+
authorizes charges states remarks judges sanctions abrogates empowers
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
CONSIDERATION_TYPE = %w[
|
|
24
|
+
having noting recognizing acknowledging recalling reaffirming
|
|
25
|
+
considering taking-into-account pursuant-to bearing-in-mind
|
|
26
|
+
emphasizing concerned accepts observing referring acting empowers
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
RESOLUTION_TYPE = %w[resolution recommendation decision declaration].freeze
|
|
30
|
+
|
|
31
|
+
RESOLUTION_RELATION_TYPE = %w[
|
|
32
|
+
annexOf hasAnnex updates refines replaces considers
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
RESOLUTION_DATE_TYPE = %w[adoption drafted discussed].freeze
|
|
36
|
+
|
|
37
|
+
APPROVAL_TYPE = %w[affirmative negative].freeze
|
|
38
|
+
|
|
39
|
+
APPROVAL_DEGREE = %w[unanimous majority minority].freeze
|
|
40
|
+
|
|
41
|
+
URL_KIND = %w[access report].freeze
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/edoxen/error.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Base class for any Edoxen-level error. Reserved for raise-on-failure paths.
|
|
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
|
|
37
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# A monolingual rendering of a Resolution. Mirrors the glossarist
|
|
5
|
+
# LocalizedConcept pattern: language-agnostic fields live on the
|
|
6
|
+
# parent Resolution; per-language content lives here.
|
|
7
|
+
class Localization < Lutaml::Model::Serializable
|
|
8
|
+
attribute :language_code, :string
|
|
9
|
+
attribute :script, :string
|
|
10
|
+
attribute :title, :string
|
|
11
|
+
attribute :subject, :string
|
|
12
|
+
attribute :message, :string
|
|
13
|
+
attribute :considering, :string
|
|
14
|
+
attribute :considerations, Consideration, collection: true
|
|
15
|
+
attribute :approvals, Approval, collection: true
|
|
16
|
+
attribute :actions, Action, collection: true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# MeetingIdentifier {
|
|
4
|
-
# venue: String
|
|
5
|
-
# date: DateTime
|
|
6
|
-
# }
|
|
7
|
-
require "lutaml/model"
|
|
8
|
-
|
|
9
3
|
module Edoxen
|
|
4
|
+
# Identifier of a meeting (venue + date). Singular — the meeting a
|
|
5
|
+
# particular Resolution belongs to.
|
|
10
6
|
class MeetingIdentifier < Lutaml::Model::Serializable
|
|
11
7
|
attribute :venue, :string
|
|
12
8
|
attribute :date, :date
|
|
13
|
-
|
|
14
|
-
key_value do
|
|
15
|
-
map "venue", to: :venue
|
|
16
|
-
map "date", to: :date
|
|
17
|
-
end
|
|
18
9
|
end
|
|
19
10
|
end
|
data/lib/edoxen/resolution.rb
CHANGED
|
@@ -1,47 +1,43 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "lutaml/model"
|
|
4
|
-
require_relative "resolution_date"
|
|
5
|
-
require_relative "consideration"
|
|
6
|
-
require_relative "approval"
|
|
7
|
-
require_relative "action"
|
|
8
|
-
require_relative "meeting_identifier"
|
|
9
|
-
require_relative "resolution_relation"
|
|
10
|
-
|
|
11
3
|
module Edoxen
|
|
4
|
+
# A formal Resolution. Language-agnostic admin fields live here; every
|
|
5
|
+
# translatable field is wrapped inside `localizations[]` (one entry per
|
|
6
|
+
# available language; at least one is required by the schema).
|
|
7
|
+
#
|
|
8
|
+
# Wire names follow lutaml-model's default convention: each declared
|
|
9
|
+
# attribute serializes to its snake_case name on the wire. Override
|
|
10
|
+
# with an explicit `key_value do; map "wire", to: :attr; end` block
|
|
11
|
+
# only when the wire name differs.
|
|
12
12
|
class Resolution < Lutaml::Model::Serializable
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
attribute :identifier, StructuredIdentifier, collection: true
|
|
14
|
+
attribute :type, :string, values: Enums::RESOLUTION_TYPE
|
|
15
|
+
attribute :doi, :string
|
|
16
|
+
attribute :urn, :string
|
|
17
|
+
attribute :agenda_item, :string
|
|
15
18
|
attribute :dates, ResolutionDate, collection: true
|
|
16
|
-
attribute :subject, :string
|
|
17
|
-
attribute :title, :string
|
|
18
|
-
attribute :type, :string, values: RESOLUTION_TYPE_ENUM
|
|
19
|
-
attribute :identifier, :string
|
|
20
|
-
attribute :message, :string
|
|
21
|
-
attribute :considering, :string
|
|
22
|
-
attribute :considerations, Consideration, collection: true
|
|
23
|
-
attribute :approvals, Approval, collection: true
|
|
24
|
-
attribute :actions, Action, collection: true
|
|
25
|
-
attribute :meeting_identifier, MeetingIdentifier
|
|
26
|
-
attribute :relations, ResolutionRelation, collection: true
|
|
27
19
|
attribute :categories, :string, collection: true
|
|
20
|
+
attribute :meeting, MeetingIdentifier
|
|
21
|
+
attribute :relations, ResolutionRelation, collection: true
|
|
28
22
|
attribute :urls, Url, collection: true
|
|
23
|
+
attribute :localizations, Localization, collection: true
|
|
24
|
+
|
|
25
|
+
# Lookup by ISO 639-3 language code. Returns nil when no exact match
|
|
26
|
+
# exists and `fallback:` is false (the default); returns the first
|
|
27
|
+
# localization otherwise. Keeps the language-preference policy in
|
|
28
|
+
# one place so callers stop reimplementing `find { |l| ... }`.
|
|
29
|
+
def in_language(code, fallback: false)
|
|
30
|
+
match = localizations&.find { |loc| loc.language_code == code.to_s }
|
|
31
|
+
return match if match
|
|
32
|
+
|
|
33
|
+
fallback ? localizations&.first : nil
|
|
34
|
+
end
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
map "identifier", to: :identifier
|
|
36
|
-
map "message", to: :message
|
|
37
|
-
map "considering", to: :considering
|
|
38
|
-
map "considerations", to: :considerations
|
|
39
|
-
map "approvals", to: :approvals
|
|
40
|
-
map "actions", to: :actions
|
|
41
|
-
map "meeting_identifier", to: :meeting_identifier
|
|
42
|
-
map "relations", to: :relations
|
|
43
|
-
map "categories", to: :categories
|
|
44
|
-
map "urls", to: :urls
|
|
36
|
+
# The canonical rendering — English when available, else the first
|
|
37
|
+
# declared localization. Mirrors the glossarist LocalizedConcept
|
|
38
|
+
# "preferred language" notion.
|
|
39
|
+
def primary_localization
|
|
40
|
+
in_language("eng", fallback: true)
|
|
45
41
|
end
|
|
46
42
|
end
|
|
47
43
|
end
|