edoxen 0.3.0 → 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 +24 -10
- data/lib/edoxen/action.rb +0 -6
- data/lib/edoxen/approval.rb +0 -7
- data/lib/edoxen/cli.rb +129 -104
- data/lib/edoxen/consideration.rb +0 -6
- data/lib/edoxen/error.rb +32 -2
- data/lib/edoxen/localization.rb +0 -12
- data/lib/edoxen/meeting_identifier.rb +0 -5
- 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 +0 -10
- data/lib/edoxen/resolution_relation.rb +0 -6
- data/lib/edoxen/schema_validator.rb +12 -28
- data/lib/edoxen/source_url.rb +0 -6
- 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 +1 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a87eaf24182b1fd7400efbd2af10681f4059a35cd57c807759a8015a59a1c0f1
|
|
4
|
+
data.tar.gz: 53ede8ad87d1a25bbf766d7196e6437a8455f431af313ba5b9752cbd42d17050
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a14ac6664ffa8ec1f1fcd31ace23257a740dcad3fab41ead54d542440a53618ac8ad46bb495afeb44c62db7b850d35d789ee7a888245f4e6b3ac0afe092310ee
|
|
7
|
+
data.tar.gz: 991a18091cb7ec21ddfe70b2691244b3e8a6ba869135545f6249ffa35d56439afbf578a867d56bdce6c3555f177c59f43c73f8c27abb119527af36c42e6b17fd
|
data/CLAUDE.md
CHANGED
|
@@ -93,9 +93,13 @@ that propagate:
|
|
|
93
93
|
|
|
94
94
|
`lib/edoxen/schema_validator.rb` is intentionally small:
|
|
95
95
|
|
|
96
|
-
- `
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
- `Edoxen::ValidationError` (defined in `lib/edoxen/error.rb`) — the unified
|
|
97
|
+
validation shape. `SchemaValidator::ValidationError` is a back-compat
|
|
98
|
+
alias. Carries `file`, `line`, `column`, `pointer`, `message_text`, and
|
|
99
|
+
`source` (`:schema` / `:model` / `:syntax`) so renderers can branch on
|
|
100
|
+
failure origin. Rendered as `file:line:col: msg at \`/pointer\``.
|
|
101
|
+
- `#validate_file(path)` → `Array<Edoxen::ValidationError>`.
|
|
102
|
+
- `#validate_content(content, path)` → `Array<Edoxen::ValidationError>`.
|
|
99
103
|
- `SchemaValidator::LineMap` — builds an indent-heuristic
|
|
100
104
|
`{json_pointer => line_no}` map and resolves a JSON-Schema data
|
|
101
105
|
pointer to a line via longest-prefix match. **No path-shape
|
|
@@ -119,6 +123,16 @@ that propagate:
|
|
|
119
123
|
result back. Preserves any `# yaml-language-server: $schema=...`
|
|
120
124
|
directive in the first 5 lines.
|
|
121
125
|
|
|
126
|
+
Both commands delegate their expand/sort/empty/header/loop/tally/summary/exit
|
|
127
|
+
scaffold to `Edoxen::Cli::Batch` — the deep module behind the seam. The
|
|
128
|
+
command bodies are per-file blocks returning `Batch::Result.ok(msg)` or
|
|
129
|
+
`Batch::Result.bad(errors)`. Adding a third command (e.g. `lint`, `diff`)
|
|
130
|
+
is one block.
|
|
131
|
+
|
|
132
|
+
`Resolution#in_language(code, fallback:)` and `Resolution#primary_localization`
|
|
133
|
+
provide the canonical lookup interface — callers should not iterate
|
|
134
|
+
`localizations.find { |l| l.language_code == code }` directly.
|
|
135
|
+
|
|
122
136
|
Exit code is non-zero if any file fails.
|
|
123
137
|
|
|
124
138
|
## Build, test, lint
|
|
@@ -139,15 +153,15 @@ mkdir -p /tmp/out && bundle exec exe/edoxen normalize "spec/fixtures/*.yaml" --o
|
|
|
139
153
|
|
|
140
154
|
- **No `double()` in specs** — use real `Edoxen::*` instances and fixture files.
|
|
141
155
|
- **Serialization is framework-only.** Never hand-roll `to_h` / `from_h` /
|
|
142
|
-
`to_yaml` / `from_yaml` / `to_json` / `from_json` on a model class.
|
|
143
|
-
`attribute`
|
|
144
|
-
|
|
156
|
+
`to_yaml` / `from_yaml` / `to_json` / `from_json` on a model class. Declare
|
|
157
|
+
`attribute` only — lutaml-model auto-emits an identity map for each
|
|
158
|
+
attribute when no `key_value` block is present. Add a `key_value do … end`
|
|
159
|
+
block with `map "wire_name", to: :attr` only when the wire name differs
|
|
160
|
+
from the attribute name.
|
|
145
161
|
- **Wire names are `snake_case`.** Even when the LUTAML notation uses
|
|
146
162
|
camelCase, the YAML wire form is `snake_case` (`agenda_item`, not
|
|
147
|
-
`agendaItem`). The attribute name on the Ruby side is also `snake_case
|
|
148
|
-
|
|
149
|
-
checking lutaml-model supports it. (Today lutaml-model needs an explicit
|
|
150
|
-
`map` for `key_value`; do not delete them.)
|
|
163
|
+
`agendaItem`). The attribute name on the Ruby side is also `snake_case`,
|
|
164
|
+
and the wire name defaults to it.
|
|
151
165
|
- **Enums on the model and the schema must agree.** When you add or remove
|
|
152
166
|
a value to `Enums::*`, also update the matching `enum:` list in
|
|
153
167
|
`schema/edoxen.yaml` $defs. The `schema_enum_sync_spec` will catch any
|
data/lib/edoxen/action.rb
CHANGED
|
@@ -7,11 +7,5 @@ module Edoxen
|
|
|
7
7
|
attribute :type, :string, values: Enums::ACTION_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
|
data/lib/edoxen/approval.rb
CHANGED
|
@@ -8,12 +8,5 @@ module Edoxen
|
|
|
8
8
|
attribute :degree, :string, values: Enums::APPROVAL_DEGREE
|
|
9
9
|
attribute :date, ResolutionDate
|
|
10
10
|
attribute :message, :string
|
|
11
|
-
|
|
12
|
-
key_value do
|
|
13
|
-
map "type", to: :type
|
|
14
|
-
map "degree", to: :degree
|
|
15
|
-
map "date", to: :date
|
|
16
|
-
map "message", to: :message
|
|
17
|
-
end
|
|
18
11
|
end
|
|
19
12
|
end
|
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,34 @@ 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
|
-
|
|
73
|
-
|
|
74
|
-
files = expand_yaml_pattern(pattern)
|
|
75
|
-
if files.empty?
|
|
76
|
-
say "No files found matching pattern: #{pattern}", :red
|
|
77
|
-
exit 1
|
|
78
|
-
end
|
|
116
|
+
summary_extra = [
|
|
117
|
+
[" Output directory", options[:output]],
|
|
118
|
+
[" Mode", options[:inplace] ? "in place" : "--output"]
|
|
119
|
+
].compact
|
|
79
120
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
files.each do |file|
|
|
86
|
-
print " #{File.basename(file)}... "
|
|
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
|
|
91
|
-
|
|
92
|
-
if options[:inplace]
|
|
93
|
-
File.write(file, normalized)
|
|
94
|
-
say "✅ NORMALIZED", :green
|
|
95
|
-
else
|
|
96
|
-
out = File.join(options[:output], File.basename(file))
|
|
97
|
-
FileUtils.mkdir_p(File.dirname(out))
|
|
98
|
-
File.write(out, normalized)
|
|
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
|
|
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}"])
|
|
106
125
|
end
|
|
107
|
-
|
|
108
|
-
print_summary(files.size, success_count, error_count, validator_type: :lax,
|
|
109
|
-
extra: [
|
|
110
|
-
[" Output directory", options[:output]],
|
|
111
|
-
[" Mode", options[:inplace] ? "in place" : "--output"]
|
|
112
|
-
].compact)
|
|
113
|
-
exit(error_count.positive? ? 1 : 0)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
no_commands do
|
|
117
|
-
# Reserved for private Thor plumbing if we add it later.
|
|
118
126
|
end
|
|
119
127
|
|
|
120
128
|
private
|
|
121
129
|
|
|
122
|
-
def expand_yaml_pattern(pattern)
|
|
123
|
-
Dir.glob(pattern).select { |f| File.file?(f) && f.match?(/\.ya?ml\z/i) }.sort
|
|
124
|
-
end
|
|
125
|
-
|
|
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
130
|
def collect_model_errors(file)
|
|
131
131
|
ResolutionCollection.from_yaml(File.read(file))
|
|
132
132
|
[]
|
|
133
133
|
rescue StandardError => e
|
|
134
|
-
[
|
|
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
|
+
)]
|
|
135
139
|
end
|
|
136
140
|
|
|
137
141
|
def extract_yaml_language_server_comment(content)
|
|
@@ -139,18 +143,39 @@ module Edoxen
|
|
|
139
143
|
lines.find { |l| l.strip.match?(/\A#\s*yaml-language-server:\s*\$schema=/) }&.rstrip
|
|
140
144
|
end
|
|
141
145
|
|
|
142
|
-
def
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
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
|
|
154
179
|
end
|
|
155
180
|
end
|
|
156
181
|
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
|
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
|
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
|
data/lib/edoxen/resolution.rb
CHANGED
|
@@ -4,6 +4,11 @@ module Edoxen
|
|
|
4
4
|
# A formal Resolution. Language-agnostic admin fields live here; every
|
|
5
5
|
# translatable field is wrapped inside `localizations[]` (one entry per
|
|
6
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.
|
|
7
12
|
class Resolution < Lutaml::Model::Serializable
|
|
8
13
|
attribute :identifier, StructuredIdentifier, collection: true
|
|
9
14
|
attribute :type, :string, values: Enums::RESOLUTION_TYPE
|
|
@@ -17,18 +22,22 @@ module Edoxen
|
|
|
17
22
|
attribute :urls, Url, collection: true
|
|
18
23
|
attribute :localizations, Localization, collection: true
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
35
|
+
|
|
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)
|
|
32
41
|
end
|
|
33
42
|
end
|
|
34
43
|
end
|
|
@@ -6,10 +6,5 @@ module Edoxen
|
|
|
6
6
|
class ResolutionCollection < Lutaml::Model::Serializable
|
|
7
7
|
attribute :metadata, ResolutionMetadata
|
|
8
8
|
attribute :resolutions, Resolution, collection: true
|
|
9
|
-
|
|
10
|
-
key_value do
|
|
11
|
-
map "metadata", to: :metadata
|
|
12
|
-
map "resolutions", to: :resolutions
|
|
13
|
-
end
|
|
14
9
|
end
|
|
15
10
|
end
|
|
@@ -12,15 +12,5 @@ module Edoxen
|
|
|
12
12
|
attribute :source_urls, SourceUrl, collection: true
|
|
13
13
|
attribute :city, :string
|
|
14
14
|
attribute :country_code, :string
|
|
15
|
-
|
|
16
|
-
key_value do
|
|
17
|
-
map "title", to: :title
|
|
18
|
-
map "title_localized", to: :title_localized
|
|
19
|
-
map "date", to: :date
|
|
20
|
-
map "source", to: :source
|
|
21
|
-
map "source_urls", to: :source_urls
|
|
22
|
-
map "city", to: :city
|
|
23
|
-
map "country_code", to: :country_code
|
|
24
|
-
end
|
|
25
15
|
end
|
|
26
16
|
end
|
|
@@ -7,11 +7,5 @@ module Edoxen
|
|
|
7
7
|
attribute :source, StructuredIdentifier
|
|
8
8
|
attribute :destination, StructuredIdentifier
|
|
9
9
|
attribute :type, :string, values: Enums::RESOLUTION_RELATION_TYPE
|
|
10
|
-
|
|
11
|
-
key_value do
|
|
12
|
-
map "source", to: :source
|
|
13
|
-
map "destination", to: :destination
|
|
14
|
-
map "type", to: :type
|
|
15
|
-
end
|
|
16
10
|
end
|
|
17
11
|
end
|
|
@@ -21,46 +21,28 @@ module Edoxen
|
|
|
21
21
|
# hard-coded path shapes) so adding new collection fields never requires
|
|
22
22
|
# touching this class.
|
|
23
23
|
class SchemaValidator
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@file = file
|
|
29
|
-
@line = line
|
|
30
|
-
@column = column
|
|
31
|
-
@pointer = pointer
|
|
32
|
-
@message_text = message_text
|
|
33
|
-
super(format_line(file, line, column, message_text, pointer))
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def to_clickable_format
|
|
37
|
-
format_line(@file, @line, @column, @message_text, @pointer)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
def format_line(file, line, column, message_text, pointer)
|
|
43
|
-
suffix = pointer.to_s.empty? ? "" : " at `#{pointer}`"
|
|
44
|
-
"#{file}:#{line}:#{column}: #{message_text}#{suffix}"
|
|
45
|
-
end
|
|
46
|
-
end
|
|
24
|
+
# Back-compat alias. The canonical type is Edoxen::ValidationError;
|
|
25
|
+
# this constant lets existing callers keep writing
|
|
26
|
+
# `SchemaValidator::ValidationError` after the unification.
|
|
27
|
+
ValidationError = Edoxen::ValidationError
|
|
47
28
|
|
|
48
29
|
def initialize(schema_path = default_schema_path)
|
|
49
30
|
@schema_path = schema_path
|
|
50
31
|
@schemer = load_schemer(schema_path)
|
|
51
32
|
end
|
|
52
33
|
|
|
53
|
-
# Validate a YAML file. Returns an array of ValidationError
|
|
34
|
+
# Validate a YAML file. Returns an array of Edoxen::ValidationError
|
|
35
|
+
# (empty = ok).
|
|
54
36
|
def validate_file(file_path)
|
|
55
37
|
validate_content(File.read(file_path), file_path)
|
|
56
38
|
rescue Errno::ENOENT
|
|
57
39
|
[ValidationError.new(
|
|
58
40
|
file: file_path, line: 1, column: 1,
|
|
59
|
-
|
|
41
|
+
message_text: "File not found", source: Edoxen::ValidationError::SOURCE_SCHEMA
|
|
60
42
|
)]
|
|
61
43
|
end
|
|
62
44
|
|
|
63
|
-
# Validate a YAML string. Returns an array of ValidationError.
|
|
45
|
+
# Validate a YAML string. Returns an array of Edoxen::ValidationError.
|
|
64
46
|
def validate_content(content, file_path)
|
|
65
47
|
data = normalize_dates(YAML.safe_load(content, permitted_classes: [Date, Time]))
|
|
66
48
|
line_map = LineMap.build(content)
|
|
@@ -71,13 +53,15 @@ module Edoxen
|
|
|
71
53
|
line, column = LineMap.locate(pointer, line_map)
|
|
72
54
|
ValidationError.new(
|
|
73
55
|
file: file_path, line: line, column: column,
|
|
74
|
-
|
|
56
|
+
message_text: message, pointer: pointer,
|
|
57
|
+
source: Edoxen::ValidationError::SOURCE_SCHEMA
|
|
75
58
|
)
|
|
76
59
|
end
|
|
77
60
|
rescue Psych::SyntaxError => e
|
|
78
61
|
[ValidationError.new(
|
|
79
62
|
file: file_path, line: e.line || 1, column: e.column || 1,
|
|
80
|
-
|
|
63
|
+
message_text: "YAML syntax error: #{e.problem}",
|
|
64
|
+
source: Edoxen::ValidationError::SOURCE_SYNTAX
|
|
81
65
|
)]
|
|
82
66
|
end
|
|
83
67
|
|
data/lib/edoxen/source_url.rb
CHANGED
data/lib/edoxen/url.rb
CHANGED
data/lib/edoxen/version.rb
CHANGED
data/lib/edoxen.rb
CHANGED
|
@@ -21,6 +21,7 @@ module Edoxen
|
|
|
21
21
|
# (Resolution <-> Localization, ResolutionMetadata <-> Localization, etc.).
|
|
22
22
|
autoload :VERSION, "edoxen/version"
|
|
23
23
|
autoload :Error, "edoxen/error"
|
|
24
|
+
autoload :ValidationError, "edoxen/error"
|
|
24
25
|
autoload :Enums, "edoxen/enums"
|
|
25
26
|
|
|
26
27
|
# Information-model classes (one per file, one concept per class).
|