metaschema 0.2.0 → 0.2.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 +155 -28
- data/README.adoc +54 -4
- data/lib/metaschema/allowed_value_type.rb +1 -1
- data/lib/metaschema/anchor_type.rb +1 -1
- data/lib/metaschema/augment_type.rb +39 -0
- data/lib/metaschema/code_type.rb +1 -1
- data/lib/metaschema/constraint_validator.rb +483 -0
- data/lib/metaschema/inline_markup_type.rb +1 -1
- data/lib/metaschema/json_schema_generator.rb +456 -0
- data/lib/metaschema/list_item_type.rb +1 -1
- data/lib/metaschema/markdown_doc_generator.rb +354 -0
- data/lib/metaschema/markup_line_datatype.rb +1 -1
- data/lib/metaschema/markup_multiline_datatype.rb +41 -0
- data/lib/metaschema/metapath_evaluator.rb +385 -0
- data/lib/metaschema/model_generator/assembly_factory.rb +1583 -0
- data/lib/metaschema/model_generator/field_factory.rb +275 -0
- data/lib/metaschema/model_generator/services/collapsibles_collapser.rb +82 -0
- data/lib/metaschema/model_generator/services/field_deserializer.rb +92 -0
- data/lib/metaschema/model_generator/services/field_serializer.rb +111 -0
- data/lib/metaschema/model_generator/utils.rb +64 -0
- data/lib/metaschema/model_generator.rb +280 -0
- data/lib/metaschema/preformatted_type.rb +1 -1
- data/lib/metaschema/root.rb +2 -0
- data/lib/metaschema/ruby_source_emitter.rb +875 -0
- data/lib/metaschema/table_cell_type.rb +1 -1
- data/lib/metaschema/type_mapper.rb +102 -0
- data/lib/metaschema/version.rb +1 -1
- data/lib/metaschema.rb +9 -0
- metadata +17 -2
|
@@ -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
|