metaschema 0.1.2 → 0.2.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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +19 -1
  3. data/.rubocop_todo.yml +558 -8
  4. data/CLAUDE.md +78 -0
  5. data/Rakefile +3 -3
  6. data/exe/metaschema +1 -2
  7. data/lib/metaschema/allowed_value_type.rb +18 -25
  8. data/lib/metaschema/allowed_values_type.rb +15 -22
  9. data/lib/metaschema/anchor_type.rb +15 -20
  10. data/lib/metaschema/any_type.rb +2 -4
  11. data/lib/metaschema/assembly.rb +18 -27
  12. data/lib/metaschema/assembly_model_type.rb +10 -19
  13. data/lib/metaschema/assembly_reference_type.rb +16 -24
  14. data/lib/metaschema/augment_type.rb +39 -0
  15. data/lib/metaschema/block_quote_type.rb +17 -25
  16. data/lib/metaschema/choice_type.rb +6 -13
  17. data/lib/metaschema/code_type.rb +17 -23
  18. data/lib/metaschema/constraint_let_type.rb +5 -9
  19. data/lib/metaschema/constraint_validator.rb +483 -0
  20. data/lib/metaschema/define_assembly_constraints_type.rb +17 -26
  21. data/lib/metaschema/define_field_constraints_type.rb +13 -19
  22. data/lib/metaschema/define_flag_constraints_type.rb +9 -17
  23. data/lib/metaschema/example_type.rb +6 -11
  24. data/lib/metaschema/expect_constraint_type.rb +12 -18
  25. data/lib/metaschema/field.rb +13 -20
  26. data/lib/metaschema/field_reference_type.rb +19 -27
  27. data/lib/metaschema/flag.rb +9 -18
  28. data/lib/metaschema/flag_reference_type.rb +14 -21
  29. data/lib/metaschema/formal_name.rb +9 -0
  30. data/lib/metaschema/global_assembly_definition_type.rb +21 -34
  31. data/lib/metaschema/global_field_definition_type.rb +26 -39
  32. data/lib/metaschema/global_flag_definition_type.rb +18 -27
  33. data/lib/metaschema/group_as_type.rb +7 -9
  34. data/lib/metaschema/grouped_assembly_reference_type.rb +11 -18
  35. data/lib/metaschema/grouped_choice_type.rb +16 -24
  36. data/lib/metaschema/grouped_field_reference_type.rb +11 -18
  37. data/lib/metaschema/grouped_inline_assembly_definition_type.rb +17 -29
  38. data/lib/metaschema/grouped_inline_field_definition_type.rb +26 -37
  39. data/lib/metaschema/image_type.rb +5 -7
  40. data/lib/metaschema/import.rb +3 -5
  41. data/lib/metaschema/index_has_key_constraint_type.rb +12 -19
  42. data/lib/metaschema/inline_assembly_definition_type.rb +25 -38
  43. data/lib/metaschema/inline_field_definition_type.rb +31 -43
  44. data/lib/metaschema/inline_flag_definition_type.rb +17 -25
  45. data/lib/metaschema/inline_markup_type.rb +17 -22
  46. data/lib/metaschema/insert_type.rb +4 -6
  47. data/lib/metaschema/json_base_uri.rb +9 -0
  48. data/lib/metaschema/json_key_type.rb +3 -5
  49. data/lib/metaschema/json_schema_generator.rb +456 -0
  50. data/lib/metaschema/json_value_key.rb +9 -0
  51. data/lib/metaschema/json_value_key_flag_type.rb +3 -5
  52. data/lib/metaschema/key_field.rb +5 -9
  53. data/lib/metaschema/list_item_type.rb +29 -39
  54. data/lib/metaschema/list_type.rb +3 -7
  55. data/lib/metaschema/markdown_doc_generator.rb +354 -0
  56. data/lib/metaschema/markup_line_datatype.rb +16 -23
  57. data/lib/metaschema/matches_constraint_type.rb +12 -18
  58. data/lib/metaschema/metapath_evaluator.rb +385 -0
  59. data/lib/metaschema/metaschema_constraints.rb +24 -0
  60. data/lib/metaschema/metaschema_import_type.rb +3 -5
  61. data/lib/metaschema/model_generator.rb +2175 -0
  62. data/lib/metaschema/namespace.rb +8 -0
  63. data/lib/metaschema/namespace_binding_type.rb +4 -6
  64. data/lib/metaschema/namespace_value.rb +9 -0
  65. data/lib/metaschema/ordered_list_type.rb +4 -8
  66. data/lib/metaschema/preformatted_type.rb +16 -23
  67. data/lib/metaschema/property_type.rb +6 -8
  68. data/lib/metaschema/remarks_type.rb +18 -27
  69. data/lib/metaschema/root.rb +23 -31
  70. data/lib/metaschema/root_name.rb +3 -5
  71. data/lib/metaschema/ruby_source_emitter.rb +869 -0
  72. data/lib/metaschema/schema_version.rb +9 -0
  73. data/lib/metaschema/scope.rb +7 -13
  74. data/lib/metaschema/short_name.rb +9 -0
  75. data/lib/metaschema/table_cell_type.rb +18 -25
  76. data/lib/metaschema/table_row_type.rb +4 -8
  77. data/lib/metaschema/table_type.rb +3 -7
  78. data/lib/metaschema/targeted_allowed_values_constraint_type.rb +16 -23
  79. data/lib/metaschema/targeted_expect_constraint_type.rb +13 -19
  80. data/lib/metaschema/targeted_has_cardinality_constraint_type.rb +13 -19
  81. data/lib/metaschema/targeted_index_constraint_type.rb +13 -20
  82. data/lib/metaschema/targeted_index_has_key_constraint_type.rb +13 -20
  83. data/lib/metaschema/targeted_key_constraint_type.rb +12 -19
  84. data/lib/metaschema/targeted_matches_constraint_type.rb +13 -19
  85. data/lib/metaschema/type_mapper.rb +82 -0
  86. data/lib/metaschema/use_name_type.rb +3 -5
  87. data/lib/metaschema/version.rb +1 -1
  88. data/lib/metaschema.rb +97 -9
  89. metadata +28 -95
  90. data/lib/metaschema/metaschemaconstraints.rb +0 -31
@@ -0,0 +1,385 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metaschema
4
+ # Evaluates Metapath (XPath subset) expressions against Ruby object instances.
5
+ #
6
+ # Supported patterns (covering OSCAL constraint targets):
7
+ # "." — current instance
8
+ # "@flag-name" — flag value
9
+ # "child-name" — child field value
10
+ # "child-name/@attr" — child's flag value
11
+ # "//descendant" — descendant values
12
+ # "child[@attr='val']" — filtered children
13
+ # "child[@attr='val']/@attr2" — filtered child's attribute
14
+ # "child[func(...)]/@attr" — function-based filter
15
+ # ".[condition]/path" — conditional navigation
16
+ # ".[condition]" — filter current instance
17
+ # "(.)[condition]/path" — parenthesized self with filter
18
+ #
19
+ # Supported predicate functions:
20
+ # has-oscal-namespace('uri') — checks prop/element ns attribute
21
+ # starts-with(@attr, 'prefix') — string prefix check
22
+ #
23
+ # Supported predicate operators:
24
+ # @attr='value' — attribute equals
25
+ # @attr=('v1','v2',...) — attribute in set
26
+ # and / or — logical operators
27
+ #
28
+ class MetapathEvaluator
29
+ OSCAL_NS = "http://csrc.nist.gov/ns/oscal"
30
+
31
+ attr_reader :context
32
+
33
+ def initialize(context)
34
+ @context = context
35
+ end
36
+
37
+ # Resolve a Metapath expression to values from the context instance.
38
+ # Returns an array of values.
39
+ def resolve(path)
40
+ return [extract_value(@context)] if path == "."
41
+
42
+ path = normalize_path(path)
43
+ steps = parse_steps(path)
44
+ evaluate_steps(@context, steps)
45
+ end
46
+
47
+ # Resolve a path to a collection of items (for uniqueness/cardinality checks).
48
+ def resolve_collection(path)
49
+ path = normalize_path(path)
50
+ steps = parse_steps(path)
51
+ evaluate_steps_collection(@context, steps)
52
+ end
53
+
54
+ private
55
+
56
+ # Normalize path patterns
57
+ def normalize_path(path)
58
+ # (.)[pred]/rest → .[pred]/rest
59
+ path.sub(/\A\(\.\)/, ".")
60
+ # // at start → descendant::
61
+ end
62
+
63
+ # Parse a Metapath expression into evaluation steps.
64
+ def parse_steps(path)
65
+ steps = []
66
+ remaining = path
67
+
68
+ while remaining && !remaining.empty?
69
+ # descendant-or-self //name
70
+ if remaining.start_with?(".//")
71
+ remaining = remaining[3..]
72
+ name, rest = split_step(remaining)
73
+ steps << { type: :descendant, name: name }
74
+ remaining = rest
75
+ next
76
+ end
77
+
78
+ # .[predicate]/rest
79
+ if remaining.start_with?(".[")
80
+ pred, rest = extract_predicate_block(remaining[1..])
81
+ inner_rest = extract_after_predicate(rest)
82
+ steps << { type: :filter_self, predicate: pred }
83
+ remaining = inner_rest
84
+ next
85
+ end
86
+
87
+ # @attr — attribute access
88
+ if remaining.start_with?("@")
89
+ name, rest = split_step(remaining[1..])
90
+ steps << { type: :attribute, name: name }
91
+ remaining = rest
92
+ next
93
+ end
94
+
95
+ # child[predicate]/@attr — filtered child
96
+ if remaining.match?(/\A[\w-]+\[/)
97
+ m = remaining.match(/\A([\w-]+)\[/)
98
+ child_name = m[1]
99
+ pred, rest = extract_predicate_block(remaining[m[1].length..])
100
+ steps << { type: :filtered_child, name: child_name, predicate: pred }
101
+ remaining = extract_after_predicate(rest)
102
+ next
103
+ end
104
+
105
+ # child-name — simple child access
106
+ if remaining.match?(/\A[\w-]+/)
107
+ name, rest = split_step(remaining)
108
+ steps << { type: :child, name: name }
109
+ remaining = rest
110
+ next
111
+ end
112
+
113
+ # Skip unrecognized prefix
114
+ remaining = remaining[1..]
115
+ end
116
+
117
+ steps
118
+ end
119
+
120
+ # Evaluate parsed steps against a context instance.
121
+ def evaluate_steps(context, steps)
122
+ return [extract_value(context)] if steps.empty?
123
+
124
+ current_items = [context]
125
+
126
+ steps.each do |step|
127
+ next_items = []
128
+ current_items.each do |item|
129
+ next_items.concat(evaluate_step(item, step))
130
+ end
131
+ current_items = next_items
132
+ end
133
+
134
+ current_items
135
+ end
136
+
137
+ def evaluate_steps_collection(context, steps)
138
+ return [context] if steps.empty?
139
+
140
+ current_items = [context]
141
+
142
+ steps.each do |step|
143
+ next_items = []
144
+ current_items.each do |item|
145
+ case step[:type]
146
+ when :child
147
+ children = get_children(item, step[:name])
148
+ next_items.concat(children)
149
+ when :descendant
150
+ next_items.concat(find_descendants(item, step[:name]))
151
+ when :attribute
152
+ next_items << resolve_attr(item, step[:name])
153
+ when :filtered_child
154
+ children = get_children(item, step[:name])
155
+ filtered = children.select do |c|
156
+ evaluate_predicate(c, step[:predicate])
157
+ end
158
+ next_items.concat(filtered)
159
+ when :filter_self
160
+ if evaluate_predicate(item, step[:predicate])
161
+ next_items << item
162
+ end
163
+ else
164
+ next_items << item
165
+ end
166
+ end
167
+ current_items = next_items
168
+ end
169
+
170
+ current_items
171
+ end
172
+
173
+ def evaluate_step(item, step)
174
+ case step[:type]
175
+ when :attribute
176
+ [resolve_attr(item, step[:name])]
177
+ when :child
178
+ children = get_children(item, step[:name])
179
+ children.map { |c| extract_value(c) }
180
+ when :descendant
181
+ find_descendants(item, step[:name]).map { |d| extract_value(d) }
182
+ when :filtered_child
183
+ children = get_children(item, step[:name])
184
+ children.select { |c| evaluate_predicate(c, step[:predicate]) }
185
+ when :filter_self
186
+ evaluate_predicate(item, step[:predicate]) ? [item] : []
187
+ else
188
+ [item]
189
+ end
190
+ end
191
+
192
+ # ── Predicate Evaluation ──────────────────────────────────────────
193
+
194
+ def evaluate_predicate(item, predicate)
195
+ return true unless predicate
196
+
197
+ # Handle "and" operators (simple split)
198
+ if predicate.include?(" and ")
199
+ parts = split_logical(predicate, " and ")
200
+ return parts.all? { |p| evaluate_single_predicate(item, p.strip) }
201
+ end
202
+
203
+ # Handle "or" operators
204
+ if predicate.include?(" or ")
205
+ parts = split_logical(predicate, " or ")
206
+ return parts.any? { |p| evaluate_single_predicate(item, p.strip) }
207
+ end
208
+
209
+ evaluate_single_predicate(item, predicate)
210
+ end
211
+
212
+ def evaluate_single_predicate(item, pred)
213
+ pred = pred.strip
214
+
215
+ # @attr='value' — simple attribute equals
216
+ if (m = pred.match(/\A@([\w-]+)\s*=\s*'([^']+)'\z/))
217
+ attr_val = resolve_attr(item, m[1])
218
+ return attr_val.to_s == m[2]
219
+ end
220
+
221
+ # @attr=('v1','v2',...) — value in set
222
+ if (m = pred.match(/\A@([\w-]+)\s*=\s*\(([^)]+)\)\z/))
223
+ attr_val = resolve_attr(item, m[1])
224
+ values = m[2].scan(/'([^']+)'/).flatten
225
+ return values.include?(attr_val.to_s)
226
+ end
227
+
228
+ # has-oscal-namespace('uri') — check ns attribute against OSCAL namespace
229
+ if (m = pred.match(/\Ahas-oscal-namespace\(\s*'([^']+)'\s*\)\z/))
230
+ ns_uri = m[1]
231
+ ns_val = resolve_attr(item, "ns")
232
+ return ns_val.to_s == ns_uri || (ns_uri == OSCAL_NS && (ns_val.nil? || ns_val.to_s.empty?))
233
+ end
234
+
235
+ # starts-with(@attr, 'prefix') — string prefix check
236
+ if (m = pred.match(/\Astarts-with\(\s*@([\w-]+)\s*,\s*'([^']+)'\s*\)\z/))
237
+ attr_val = resolve_attr(item, m[1])
238
+ return attr_val.to_s.start_with?(m[2])
239
+ end
240
+
241
+ # Combining functions with @attr='val' using 'and'
242
+ if pred.include?(" and ")
243
+ parts = split_logical(pred, " and ")
244
+ return parts.all? { |p| evaluate_single_predicate(item, p.strip) }
245
+ end
246
+
247
+ false
248
+ end
249
+
250
+ # ── Instance Navigation ───────────────────────────────────────────
251
+
252
+ def resolve_attr(instance, attr_name)
253
+ return instance unless instance.is_a?(Lutaml::Model::Serializable)
254
+
255
+ sym = attr_name.gsub("-", "_").to_sym
256
+ return instance.send(sym) if instance.respond_to?(sym)
257
+
258
+ nil
259
+ end
260
+
261
+ def get_children(instance, child_name)
262
+ return [] unless instance.is_a?(Lutaml::Model::Serializable)
263
+
264
+ sym = child_name.gsub("-", "_").to_sym
265
+ return [] unless instance.respond_to?(sym)
266
+
267
+ child = instance.send(sym)
268
+ case child
269
+ when Array then child
270
+ when nil then []
271
+ else [child]
272
+ end
273
+ end
274
+
275
+ def find_descendants(instance, name)
276
+ results = []
277
+ return results unless instance.is_a?(Lutaml::Model::Serializable)
278
+
279
+ sym = name.gsub("-", "_").to_sym
280
+
281
+ instance.class.attributes.each_key do |attr_name|
282
+ value = instance.send(attr_name)
283
+ next if value.nil?
284
+
285
+ items = value.is_a?(Array) ? value : [value]
286
+ items.each do |item|
287
+ next unless item.is_a?(Lutaml::Model::Serializable)
288
+
289
+ if attr_name == sym
290
+ results << item
291
+ end
292
+
293
+ results.concat(find_descendants(item, name))
294
+ end
295
+ end
296
+
297
+ results
298
+ end
299
+
300
+ def extract_value(item)
301
+ return item unless item.is_a?(Lutaml::Model::Serializable)
302
+ return item.content if item.respond_to?(:content) && item.content
303
+
304
+ item
305
+ end
306
+
307
+ # ── Parsing Helpers ───────────────────────────────────────────────
308
+
309
+ def split_step(path)
310
+ idx = path.index("/")
311
+ idx ? [path[0...idx], path[(idx + 1)..]] : [path, nil]
312
+ end
313
+
314
+ def extract_predicate_block(str)
315
+ # str starts with "[..."
316
+ depth = 0
317
+ i = 0
318
+ while i < str.length
319
+ case str[i]
320
+ when "["
321
+ depth += 1
322
+ when "]"
323
+ depth -= 1
324
+ return [str[1...i], str[(i + 1)..]] if depth.zero?
325
+ when "'"
326
+ # Skip string literal
327
+ i += 1
328
+ while i < str.length && str[i] != "'"
329
+ i += 1
330
+ end
331
+ end
332
+ i += 1
333
+ end
334
+ [str[1..], ""]
335
+ end
336
+
337
+ def extract_after_predicate(rest)
338
+ return nil unless rest
339
+
340
+ rest.start_with?("/") ? rest[1..] : rest
341
+ end
342
+
343
+ # Split on logical operators respecting parentheses and quotes
344
+ def split_logical(expr, op)
345
+ parts = []
346
+ depth = 0
347
+ current = +""
348
+ i = 0
349
+ in_string = false
350
+
351
+ while i < expr.length
352
+ ch = expr[i]
353
+
354
+ if ch == "'" && depth >= 0
355
+ in_string = !in_string
356
+ current << ch
357
+ i += 1
358
+ next
359
+ end
360
+
361
+ unless in_string
362
+ case ch
363
+ when "(", "["
364
+ depth += 1
365
+ when ")", "]"
366
+ depth -= 1
367
+ end
368
+
369
+ if depth.zero? && expr[i, op.length + 2] == " #{op} "
370
+ parts << current.strip
371
+ current = +""
372
+ i += op.length + 2
373
+ next
374
+ end
375
+ end
376
+
377
+ current << ch
378
+ i += 1
379
+ end
380
+
381
+ parts << current.strip
382
+ parts
383
+ end
384
+ end
385
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metaschema
4
+ class MetaschemaConstraints < Lutaml::Model::Serializable
5
+ attribute :name, :string
6
+ attribute :version, :string
7
+ attribute :import, Import, collection: true
8
+ attribute :namespace_binding, NamespaceBindingType, collection: true
9
+ attribute :scope, Scope, collection: true
10
+ attribute :remarks, RemarksType
11
+
12
+ xml do
13
+ element "METASCHEMA-CONSTRAINTS"
14
+ namespace ::Metaschema::Namespace
15
+
16
+ map_element "name", to: :name
17
+ map_element "version", to: :version
18
+ map_element "import", to: :import
19
+ map_element "namespace-binding", to: :namespace_binding
20
+ map_element "scope", to: :scope
21
+ map_element "remarks", to: :remarks
22
+ end
23
+ end
24
+ end
@@ -1,16 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'lutaml/model'
4
-
5
3
  module Metaschema
6
4
  class MetaschemaImportType < Lutaml::Model::Serializable
7
5
  attribute :href, :string
8
6
 
9
7
  xml do
10
- root 'MetaschemaImportType'
11
- namespace 'http://csrc.nist.gov/ns/oscal/metaschema/1.0'
8
+ element "MetaschemaImportType"
9
+ namespace ::Metaschema::Namespace
12
10
 
13
- map_attribute 'href', to: :href
11
+ map_attribute "href", to: :href
14
12
  end
15
13
  end
16
14
  end