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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Edoxen
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
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/meeting"
19
+ require_relative "edoxen/metadata"
20
20
  require_relative "edoxen/resolution"
21
- require_relative "edoxen/resolution_collection"
22
- require_relative "edoxen/resolution_date"
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