edoxen 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +202 -40
- data/README.adoc +408 -119
- data/edoxen.gemspec +7 -1
- data/exe/edoxen +6 -0
- data/lib/edoxen/action.rb +12 -45
- data/lib/edoxen/approval.rb +3 -19
- data/lib/edoxen/cli.rb +173 -0
- data/lib/edoxen/consideration.rb +9 -32
- data/lib/edoxen/metadata.rb +27 -0
- data/lib/edoxen/resolution.rb +19 -33
- data/lib/edoxen/resolution_date.rb +13 -29
- data/lib/edoxen/{resolution_relationship.rb → resolution_relation.rb} +1 -1
- data/lib/edoxen/resolution_set.rb +17 -0
- data/lib/edoxen/schema_validator.rb +299 -0
- data/lib/edoxen/url.rb +17 -0
- data/lib/edoxen/version.rb +1 -1
- data/lib/edoxen.rb +3 -7
- data/schema/edoxen.yaml +188 -53
- metadata +46 -15
- data/lib/edoxen/resolution_collection.rb +0 -25
- data/lib/edoxen/structured_identifier.rb +0 -20
- data/lib/edoxen/subject_body.rb +0 -20
- /data/lib/edoxen/{meeting_identfier.rb → meeting_identifier.rb} +0 -0
@@ -0,0 +1,299 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json_schemer"
|
4
|
+
require "yaml"
|
5
|
+
require "json"
|
6
|
+
require "psych"
|
7
|
+
|
8
|
+
module Edoxen
|
9
|
+
class SchemaValidator
|
10
|
+
class ValidationError < StandardError
|
11
|
+
attr_reader :file, :line, :column, :message, :data_pointer
|
12
|
+
|
13
|
+
def initialize(file, line, column, message, data_pointer = nil)
|
14
|
+
@file = file
|
15
|
+
@line = line
|
16
|
+
@column = column
|
17
|
+
@message = message
|
18
|
+
@data_pointer = data_pointer
|
19
|
+
super("#{file}:#{line}:#{column}: #{message}")
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_clickable_format
|
23
|
+
"#{file}:#{line}:#{column}: #{message}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Custom YAML handler to track line numbers
|
28
|
+
class LineTrackingHandler < Psych::Handler
|
29
|
+
attr_reader :line_map, :path_stack, :array_indices
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
@line_map = {}
|
33
|
+
@path_stack = []
|
34
|
+
@array_indices = {}
|
35
|
+
@current_path = ""
|
36
|
+
end
|
37
|
+
|
38
|
+
def start_document(version, tag_directives, implicit)
|
39
|
+
# Document start
|
40
|
+
end
|
41
|
+
|
42
|
+
def start_mapping(anchor, tag, implicit, style)
|
43
|
+
# Starting a new mapping/object
|
44
|
+
end
|
45
|
+
|
46
|
+
def start_sequence(_anchor, _tag, _implicit, _style)
|
47
|
+
# Starting a new sequence/array
|
48
|
+
parent_path = "/#{@path_stack.join("/")}"
|
49
|
+
parent_path = "" if parent_path == "/"
|
50
|
+
@array_indices[parent_path] = -1
|
51
|
+
end
|
52
|
+
|
53
|
+
def scalar(value, anchor, tag, plain, quoted, style)
|
54
|
+
# This is called for each scalar value
|
55
|
+
# We need to track this in context of the current path
|
56
|
+
end
|
57
|
+
|
58
|
+
def alias(anchor)
|
59
|
+
# Handle YAML aliases
|
60
|
+
end
|
61
|
+
|
62
|
+
def end_sequence
|
63
|
+
# End of sequence/array
|
64
|
+
end
|
65
|
+
|
66
|
+
def end_mapping
|
67
|
+
# End of mapping/object
|
68
|
+
end
|
69
|
+
|
70
|
+
def end_document(implicit)
|
71
|
+
# Document end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def initialize(schema_path = nil)
|
76
|
+
@schema_path = schema_path || File.join(__dir__, "..", "..", "schema", "edoxen.yaml")
|
77
|
+
@schemer = nil
|
78
|
+
load_schema
|
79
|
+
end
|
80
|
+
|
81
|
+
def validate_file(file_path)
|
82
|
+
return [ValidationError.new(file_path, 1, 1, "File not found")] unless File.exist?(file_path)
|
83
|
+
|
84
|
+
content = File.read(file_path)
|
85
|
+
validate_content(content, file_path)
|
86
|
+
end
|
87
|
+
|
88
|
+
def validate_content(content, file_path)
|
89
|
+
errors = []
|
90
|
+
|
91
|
+
begin
|
92
|
+
# Parse YAML and build line map
|
93
|
+
yaml_data, line_map = parse_yaml_with_line_tracking(content)
|
94
|
+
|
95
|
+
# Validate against schema
|
96
|
+
if @schemer
|
97
|
+
schema_errors = @schemer.validate(yaml_data).to_a
|
98
|
+
errors.concat(convert_schema_errors(schema_errors, file_path, line_map))
|
99
|
+
else
|
100
|
+
errors << ValidationError.new(file_path, 1, 1, "Schema not available for validation")
|
101
|
+
end
|
102
|
+
rescue Psych::SyntaxError => e
|
103
|
+
line = e.line || 1
|
104
|
+
column = e.column || 1
|
105
|
+
errors << ValidationError.new(file_path, line, column, "YAML syntax error: #{e.problem}")
|
106
|
+
rescue StandardError => e
|
107
|
+
errors << ValidationError.new(file_path, 1, 1, "Validation error: #{e.message}")
|
108
|
+
end
|
109
|
+
|
110
|
+
errors
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def load_schema
|
116
|
+
return unless File.exist?(@schema_path)
|
117
|
+
|
118
|
+
begin
|
119
|
+
schema_content = File.read(@schema_path)
|
120
|
+
schema_data = YAML.safe_load(schema_content)
|
121
|
+
@schemer = JSONSchemer.schema(schema_data)
|
122
|
+
rescue StandardError => e
|
123
|
+
warn "Warning: Could not load schema from #{@schema_path}: #{e.message}"
|
124
|
+
@schemer = nil
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def parse_yaml_with_line_tracking(content)
|
129
|
+
# Parse YAML normally
|
130
|
+
yaml_data = YAML.safe_load(content)
|
131
|
+
|
132
|
+
# Build line map by parsing the content line by line
|
133
|
+
line_map = build_line_map(content)
|
134
|
+
|
135
|
+
[yaml_data, line_map]
|
136
|
+
end
|
137
|
+
|
138
|
+
def build_line_map(content)
|
139
|
+
line_map = {}
|
140
|
+
lines = content.split("\n")
|
141
|
+
path_stack = []
|
142
|
+
array_indices = {}
|
143
|
+
|
144
|
+
lines.each_with_index do |line, index|
|
145
|
+
line_number = index + 1
|
146
|
+
stripped = line.strip
|
147
|
+
|
148
|
+
# Skip empty lines and comments
|
149
|
+
next if stripped.empty? || stripped.start_with?("#")
|
150
|
+
|
151
|
+
# Calculate indentation
|
152
|
+
indent = line.length - line.lstrip.length
|
153
|
+
level = indent / 2 # Assuming 2-space indentation
|
154
|
+
|
155
|
+
# Adjust path stack based on indentation level
|
156
|
+
path_stack = path_stack[0, level] if level < path_stack.length
|
157
|
+
|
158
|
+
if stripped.start_with?("- ")
|
159
|
+
# Array item
|
160
|
+
parent_path = path_stack.empty? ? "" : "/#{path_stack.join("/")}"
|
161
|
+
array_indices[parent_path] ||= -1
|
162
|
+
array_indices[parent_path] += 1
|
163
|
+
|
164
|
+
array_index = array_indices[parent_path]
|
165
|
+
current_path = "#{parent_path}/#{array_index}"
|
166
|
+
line_map[current_path] = line_number
|
167
|
+
|
168
|
+
# Check if array item has a key
|
169
|
+
item_content = stripped[2..].strip
|
170
|
+
if item_content.include?(":")
|
171
|
+
key = item_content.split(":").first.strip.gsub(/["']/, "")
|
172
|
+
if key.empty?
|
173
|
+
path_stack = path_stack[0, level] + [array_index.to_s]
|
174
|
+
else
|
175
|
+
key_path = "#{current_path}/#{key}"
|
176
|
+
line_map[key_path] = line_number
|
177
|
+
path_stack = path_stack[0, level] + [array_index.to_s, key]
|
178
|
+
end
|
179
|
+
else
|
180
|
+
path_stack = path_stack[0, level] + [array_index.to_s]
|
181
|
+
end
|
182
|
+
elsif stripped.include?(":")
|
183
|
+
# Regular key-value pair
|
184
|
+
key = stripped.split(":").first.strip.gsub(/["']/, "")
|
185
|
+
next if key.empty?
|
186
|
+
|
187
|
+
path_stack = path_stack[0, level] + [key]
|
188
|
+
current_path = "/#{path_stack.join("/")}"
|
189
|
+
line_map[current_path] = line_number
|
190
|
+
|
191
|
+
# Reset array indices for this path and deeper
|
192
|
+
array_indices.each_key do |path|
|
193
|
+
array_indices.delete(path) if path.start_with?(current_path)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
line_map
|
199
|
+
end
|
200
|
+
|
201
|
+
def convert_schema_errors(schema_errors, file_path, line_map)
|
202
|
+
schema_errors.map do |error|
|
203
|
+
data_pointer = error["data_pointer"] || ""
|
204
|
+
line = find_line_for_pointer(data_pointer, line_map)
|
205
|
+
column = 1
|
206
|
+
|
207
|
+
message = build_error_message(error)
|
208
|
+
|
209
|
+
ValidationError.new(file_path, line, column, message, data_pointer)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def find_line_for_pointer(pointer, line_map)
|
214
|
+
return 1 if pointer.empty?
|
215
|
+
|
216
|
+
# Try exact match first
|
217
|
+
return line_map[pointer] if line_map[pointer]
|
218
|
+
|
219
|
+
# Try progressively shorter paths
|
220
|
+
parts = pointer.split("/").reject(&:empty?)
|
221
|
+
(parts.length - 1).downto(0) do |i|
|
222
|
+
partial_path = "/#{parts[0..i].join("/")}"
|
223
|
+
return line_map[partial_path] if line_map[partial_path]
|
224
|
+
end
|
225
|
+
|
226
|
+
# For paths like /resolutions/0/actions/0/type, try to find the specific action
|
227
|
+
if parts.length >= 4 && parts[0] == "resolutions" && parts[2] == "actions"
|
228
|
+
# Try to find the specific action line
|
229
|
+
resolution_index = parts[1]
|
230
|
+
action_index = parts[3]
|
231
|
+
parts[4] if parts.length > 4
|
232
|
+
|
233
|
+
# Look for patterns like /resolutions/0/actions/0
|
234
|
+
action_path = "/resolutions/#{resolution_index}/actions/#{action_index}"
|
235
|
+
return line_map[action_path] if line_map[action_path]
|
236
|
+
|
237
|
+
# Look for the actions array start
|
238
|
+
actions_path = "/resolutions/#{resolution_index}/actions"
|
239
|
+
return line_map[actions_path] if line_map[actions_path]
|
240
|
+
end
|
241
|
+
|
242
|
+
# Try to find the closest match by looking for the last non-numeric part
|
243
|
+
if parts.any?
|
244
|
+
parts.reverse.each do |part|
|
245
|
+
next if part.match?(/^\d+$/) # Skip array indices
|
246
|
+
|
247
|
+
line_map.each do |path, line|
|
248
|
+
return line if path.end_with?("/#{part}")
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# Default to line 1
|
254
|
+
1
|
255
|
+
end
|
256
|
+
|
257
|
+
def build_error_message(error)
|
258
|
+
type = error["type"]
|
259
|
+
details = error["details"] || {}
|
260
|
+
data_pointer = error["data_pointer"] || ""
|
261
|
+
|
262
|
+
base_message = case type
|
263
|
+
when "required"
|
264
|
+
missing = details["missing_keys"] || []
|
265
|
+
if missing.length == 1
|
266
|
+
"object is missing required property: #{missing.first}"
|
267
|
+
else
|
268
|
+
"object is missing required properties: #{missing.join(", ")}"
|
269
|
+
end
|
270
|
+
when "additionalProperties"
|
271
|
+
extra = details["extra_keys"] || []
|
272
|
+
if extra.length == 1
|
273
|
+
"object property '#{extra.first}' is a disallowed additional property"
|
274
|
+
else
|
275
|
+
"object properties #{extra.map do |k|
|
276
|
+
"'#{k}'"
|
277
|
+
end.join(", ")} are disallowed additional properties"
|
278
|
+
end
|
279
|
+
when "enum"
|
280
|
+
value = details["value"]
|
281
|
+
valid_values = details["valid_values"] || []
|
282
|
+
"value '#{value}' is not one of: #{valid_values}"
|
283
|
+
when "type"
|
284
|
+
details["actual_type"]
|
285
|
+
expected = details["expected_types"] || []
|
286
|
+
"value is not #{expected.join(" or ")}"
|
287
|
+
else
|
288
|
+
error["error"] || "validation failed"
|
289
|
+
end
|
290
|
+
|
291
|
+
# Add data pointer for debugging if it's not empty
|
292
|
+
if !data_pointer.empty?
|
293
|
+
"#{base_message} at `#{data_pointer}`"
|
294
|
+
else
|
295
|
+
base_message
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
data/lib/edoxen/url.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "lutaml/model"
|
4
|
+
|
5
|
+
module Edoxen
|
6
|
+
class Url < Lutaml::Model::Serializable
|
7
|
+
attribute :kind, :string, values: %w[access report]
|
8
|
+
attribute :ref, :string
|
9
|
+
attribute :format, :string
|
10
|
+
|
11
|
+
key_value do
|
12
|
+
map "kind", to: :kind
|
13
|
+
map "ref", to: :ref
|
14
|
+
map "format", to: :format
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/edoxen/version.rb
CHANGED
data/lib/edoxen.rb
CHANGED
@@ -16,12 +16,8 @@ module Edoxen
|
|
16
16
|
require_relative "edoxen/action"
|
17
17
|
require_relative "edoxen/approval"
|
18
18
|
require_relative "edoxen/consideration"
|
19
|
-
require_relative "edoxen/
|
19
|
+
require_relative "edoxen/metadata"
|
20
20
|
require_relative "edoxen/resolution"
|
21
|
-
require_relative "edoxen/
|
22
|
-
require_relative "edoxen/
|
23
|
-
require_relative "edoxen/resolution_relationship"
|
24
|
-
require_relative "edoxen/structured_identifier"
|
25
|
-
require_relative "edoxen/subject_body"
|
26
|
-
require_relative "edoxen/meeting_identfier"
|
21
|
+
require_relative "edoxen/resolution_set"
|
22
|
+
require_relative "edoxen/cli"
|
27
23
|
end
|