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,456 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Metaschema
|
|
6
|
+
# Generates JSON Schema (draft-07) from a parsed Metaschema document.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# ms = Metaschema::Root.from_xml(File.read("metaschema.xml"))
|
|
10
|
+
# schema = JsonSchemaGenerator.generate(ms)
|
|
11
|
+
# puts JSON.pretty_generate(schema)
|
|
12
|
+
#
|
|
13
|
+
# The generator walks the metaschema definition tree and emits a JSON Schema
|
|
14
|
+
# with a top-level object for each root assembly, and shared $defs for all
|
|
15
|
+
# referenced types.
|
|
16
|
+
class JsonSchemaGenerator
|
|
17
|
+
SCHEMA_URI = "http://json-schema.org/draft-07/schema#"
|
|
18
|
+
|
|
19
|
+
# Maps metaschema as-type to JSON Schema type.
|
|
20
|
+
TYPE_MAP = {
|
|
21
|
+
"string" => { "type" => "string" },
|
|
22
|
+
"markup-line" => { "type" => "string" },
|
|
23
|
+
"markup-multiline" => { "type" => "string" },
|
|
24
|
+
"boolean" => { "type" => "boolean" },
|
|
25
|
+
"integer" => { "type" => "integer" },
|
|
26
|
+
"positive-integer" => { "type" => "integer", "minimum" => 1 },
|
|
27
|
+
"non-negative-integer" => { "type" => "integer", "minimum" => 0 },
|
|
28
|
+
"decimal" => { "type" => "number" },
|
|
29
|
+
"date" => { "type" => "string", "format" => "date" },
|
|
30
|
+
"date-time" => { "type" => "string", "format" => "date-time" },
|
|
31
|
+
"dateTime" => { "type" => "string", "format" => "date-time" },
|
|
32
|
+
"dateTime-with-timezone" => { "type" => "string",
|
|
33
|
+
"format" => "date-time" },
|
|
34
|
+
"uri" => { "type" => "string", "format" => "uri" },
|
|
35
|
+
"uri-reference" => { "type" => "string" },
|
|
36
|
+
"uuid" => { "type" => "string",
|
|
37
|
+
"pattern" => "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" },
|
|
38
|
+
"base64" => { "type" => "string", "contentEncoding" => "base64" },
|
|
39
|
+
"token" => { "type" => "string" },
|
|
40
|
+
"email" => { "type" => "string", "format" => "email" },
|
|
41
|
+
"ip-v4-address" => { "type" => "string", "format" => "ipv4" },
|
|
42
|
+
"ip-v6-address" => { "type" => "string", "format" => "ipv6" },
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
def self.generate(metaschema, id: nil)
|
|
46
|
+
new(metaschema, id: id).generate
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def initialize(metaschema, id: nil)
|
|
50
|
+
@metaschema = metaschema
|
|
51
|
+
@id = id
|
|
52
|
+
@definitions = {}
|
|
53
|
+
@field_defs = {}
|
|
54
|
+
@assembly_defs = {}
|
|
55
|
+
@flag_defs = {}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def generate
|
|
59
|
+
collect_definitions
|
|
60
|
+
|
|
61
|
+
@metaschema.define_assembly&.each { |a| build_assembly_schema(a) }
|
|
62
|
+
@metaschema.define_field&.each { |f| build_field_def_schema(f) }
|
|
63
|
+
@metaschema.define_flag&.each { |f| build_flag_schema(f) }
|
|
64
|
+
|
|
65
|
+
root_assemblies = (@metaschema.define_assembly || []).select do |a|
|
|
66
|
+
a.root_name&.content
|
|
67
|
+
end
|
|
68
|
+
if root_assemblies.one?
|
|
69
|
+
root = root_assemblies.first
|
|
70
|
+
root_name = root.root_name.content
|
|
71
|
+
@definitions[root.name] || { "type" => "object" }
|
|
72
|
+
|
|
73
|
+
schema = {
|
|
74
|
+
"$schema" => SCHEMA_URI,
|
|
75
|
+
"$id" => @id,
|
|
76
|
+
"type" => "object",
|
|
77
|
+
"properties" => { root_name => { "$ref" => "#/$defs/#{root.name}" } },
|
|
78
|
+
"required" => [root_name],
|
|
79
|
+
"additionalProperties" => false,
|
|
80
|
+
"$defs" => @definitions,
|
|
81
|
+
}
|
|
82
|
+
schema.delete("$id") unless @id
|
|
83
|
+
schema
|
|
84
|
+
else
|
|
85
|
+
{
|
|
86
|
+
"$schema" => SCHEMA_URI,
|
|
87
|
+
"$id" => @id,
|
|
88
|
+
"$defs" => @definitions,
|
|
89
|
+
}.compact
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def collect_definitions
|
|
96
|
+
@metaschema.define_assembly&.each do |a|
|
|
97
|
+
@assembly_defs[a.name] = a if a.name
|
|
98
|
+
end
|
|
99
|
+
@metaschema.define_field&.each { |f| @field_defs[f.name] = f if f.name }
|
|
100
|
+
@metaschema.define_flag&.each { |f| @flag_defs[f.name] = f if f.name }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# ── Assembly ───────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
def build_assembly_schema(assembly_def)
|
|
106
|
+
return @definitions[assembly_def.name] if @definitions.key?(assembly_def.name)
|
|
107
|
+
|
|
108
|
+
# Placeholder to prevent cycles
|
|
109
|
+
@definitions[assembly_def.name] = { "type" => "object" }
|
|
110
|
+
|
|
111
|
+
props = {}
|
|
112
|
+
required = []
|
|
113
|
+
pattern_props = {}
|
|
114
|
+
|
|
115
|
+
# Flags → object properties
|
|
116
|
+
(assembly_def.define_flag || []).each do |fl|
|
|
117
|
+
name = fl.name
|
|
118
|
+
next unless name
|
|
119
|
+
|
|
120
|
+
props[name] = build_flag_type_schema(fl)
|
|
121
|
+
required << name if fl.required == "yes"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
(assembly_def.flag || []).each do |fr|
|
|
125
|
+
ref = fr.ref
|
|
126
|
+
next unless ref
|
|
127
|
+
|
|
128
|
+
fd = @flag_defs[ref]
|
|
129
|
+
next unless fd
|
|
130
|
+
|
|
131
|
+
props[ref] = build_flag_type_schema(fd)
|
|
132
|
+
required << ref if fr.required == "yes"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Model children
|
|
136
|
+
if assembly_def.model
|
|
137
|
+
model = assembly_def.model
|
|
138
|
+
collect_model_children(model, props, required, pattern_props)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
schema = { "type" => "object", "properties" => props }
|
|
142
|
+
schema["required"] = required unless required.empty?
|
|
143
|
+
schema["additionalProperties"] = false
|
|
144
|
+
schema["patternProperties"] = pattern_props unless pattern_props.empty?
|
|
145
|
+
|
|
146
|
+
if assembly_def.formal_name && !assembly_def.formal_name.is_a?(TrueClass)
|
|
147
|
+
title = assembly_def.formal_name.is_a?(String) ? assembly_def.formal_name : assembly_def.formal_name.content
|
|
148
|
+
schema["title"] = title if title && !title.empty?
|
|
149
|
+
end
|
|
150
|
+
if assembly_def.description.respond_to?(:content)
|
|
151
|
+
desc = assembly_def.description.content
|
|
152
|
+
schema["description"] = desc if desc && !desc.empty?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
@definitions[assembly_def.name] = schema
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def collect_model_children(model, props, required, pattern_props)
|
|
159
|
+
(model.field || []).each do |fr|
|
|
160
|
+
add_field_ref(fr, props, required, pattern_props)
|
|
161
|
+
end
|
|
162
|
+
(model.assembly || []).each do |ar|
|
|
163
|
+
add_assembly_ref(ar, props, required, pattern_props)
|
|
164
|
+
end
|
|
165
|
+
(model.define_field || []).each do |fd|
|
|
166
|
+
add_inline_field(fd, props, required)
|
|
167
|
+
end
|
|
168
|
+
(model.define_assembly || []).each do |ad|
|
|
169
|
+
add_inline_assembly(ad, props, required)
|
|
170
|
+
end
|
|
171
|
+
(model.choice || []).each do |c|
|
|
172
|
+
collect_choice_children(c, props, required, pattern_props)
|
|
173
|
+
end
|
|
174
|
+
(model.choice_group || []).each do |cg|
|
|
175
|
+
collect_choice_group_children(cg, props, required, pattern_props)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def collect_choice_children(choice, props, required, pattern_props)
|
|
180
|
+
(choice.field || []).each do |fr|
|
|
181
|
+
add_field_ref(fr, props, required, pattern_props)
|
|
182
|
+
end
|
|
183
|
+
(choice.assembly || []).each do |ar|
|
|
184
|
+
add_assembly_ref(ar, props, required, pattern_props)
|
|
185
|
+
end
|
|
186
|
+
(choice.define_field || []).each do |fd|
|
|
187
|
+
add_inline_field(fd, props, required)
|
|
188
|
+
end
|
|
189
|
+
(choice.define_assembly || []).each do |ad|
|
|
190
|
+
add_inline_assembly(ad, props, required)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def collect_choice_group_children(cg, props, _required, pattern_props)
|
|
195
|
+
group_as = cg.group_as
|
|
196
|
+
json_name = group_as&.name
|
|
197
|
+
|
|
198
|
+
child_field_refs = cg.field || []
|
|
199
|
+
child_asm_refs = cg.assembly || []
|
|
200
|
+
|
|
201
|
+
if group_as&.in_json == "BY_KEY" && json_name
|
|
202
|
+
# BY_KEY: object with pattern properties
|
|
203
|
+
inner_props = {}
|
|
204
|
+
child_field_refs.each do |fr|
|
|
205
|
+
ref = fr.ref
|
|
206
|
+
fd = @field_defs[ref]
|
|
207
|
+
inner_props.merge!(build_field_by_key_schema(fd)) if fd
|
|
208
|
+
end
|
|
209
|
+
pattern_props[json_name] = inner_props unless inner_props.empty?
|
|
210
|
+
elsif group_as&.in_json == "ARRAY" && json_name && child_field_refs.one?
|
|
211
|
+
# Array of single field type
|
|
212
|
+
fr = child_field_refs.first
|
|
213
|
+
ref = fr.ref
|
|
214
|
+
fd = @field_defs[ref]
|
|
215
|
+
if fd
|
|
216
|
+
items = build_field_items_schema(fd)
|
|
217
|
+
props[json_name] = { "type" => "array", "items" => items }
|
|
218
|
+
end
|
|
219
|
+
elsif group_as&.in_json == "ARRAY" && json_name && child_asm_refs.one?
|
|
220
|
+
ar = child_asm_refs.first
|
|
221
|
+
ref = ar.ref
|
|
222
|
+
props[json_name] =
|
|
223
|
+
{ "type" => "array", "items" => { "$ref" => "#/$defs/#{ref}" } }
|
|
224
|
+
build_assembly_schema(@assembly_defs[ref]) if @assembly_defs[ref]
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# ── Field Ref ──────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
def add_field_ref(fr, props, required, pattern_props)
|
|
231
|
+
ref = fr.ref
|
|
232
|
+
return unless ref
|
|
233
|
+
|
|
234
|
+
fd = @field_defs[ref]
|
|
235
|
+
return unless fd
|
|
236
|
+
|
|
237
|
+
group_as = fr.group_as
|
|
238
|
+
json_name = fr.use_name&.content || group_as&.name || ref
|
|
239
|
+
|
|
240
|
+
if group_as&.in_json == "BY_KEY"
|
|
241
|
+
key_flag = fd.json_key&.flag_ref
|
|
242
|
+
if key_flag
|
|
243
|
+
inner = build_field_object_schema(fd)
|
|
244
|
+
pattern_props[".*"] = inner
|
|
245
|
+
end
|
|
246
|
+
elsif group_as && %w[ARRAY SINGLETON_OR_ARRAY].include?(group_as.in_json)
|
|
247
|
+
items = build_field_items_schema(fd)
|
|
248
|
+
arr = { "type" => "array", "items" => items }
|
|
249
|
+
if group_as.in_json == "SINGLETON_OR_ARRAY"
|
|
250
|
+
arr = { "oneOf" => [items, arr] }
|
|
251
|
+
end
|
|
252
|
+
props[json_name] = arr
|
|
253
|
+
else
|
|
254
|
+
# Singleton field
|
|
255
|
+
has_flags = (fd.define_flag || []).any? || (fd.flag || []).any?
|
|
256
|
+
has_vk = fd.json_value_key || fd.json_value_key_flag
|
|
257
|
+
props[json_name] = if has_flags || has_vk
|
|
258
|
+
build_field_object_schema(fd)
|
|
259
|
+
else
|
|
260
|
+
build_field_scalar_schema(fd)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
required << json_name if fr.min_occurs&.to_i&.> 0
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# ── Assembly Ref ───────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
def add_assembly_ref(ar, props, required, _pattern_props)
|
|
270
|
+
ref = ar.ref
|
|
271
|
+
return unless ref
|
|
272
|
+
|
|
273
|
+
build_assembly_schema(@assembly_defs[ref]) if @assembly_defs[ref]
|
|
274
|
+
|
|
275
|
+
group_as = ar.group_as
|
|
276
|
+
json_name = group_as&.name || ref
|
|
277
|
+
|
|
278
|
+
if group_as&.in_json == "BY_KEY"
|
|
279
|
+
# BY_KEY: object whose keys are dynamic
|
|
280
|
+
props[json_name] = {
|
|
281
|
+
"type" => "object",
|
|
282
|
+
"additionalProperties" => { "$ref" => "#/$defs/#{ref}" },
|
|
283
|
+
}
|
|
284
|
+
elsif group_as && %w[ARRAY SINGLETON_OR_ARRAY].include?(group_as.in_json)
|
|
285
|
+
arr = { "type" => "array", "items" => { "$ref" => "#/$defs/#{ref}" } }
|
|
286
|
+
if group_as.in_json == "SINGLETON_OR_ARRAY"
|
|
287
|
+
arr = { "oneOf" => [{ "$ref" => "#/$defs/#{ref}" }, arr] }
|
|
288
|
+
end
|
|
289
|
+
props[json_name] = arr
|
|
290
|
+
else
|
|
291
|
+
props[json_name] = { "$ref" => "#/$defs/#{ref}" }
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
required << json_name if ar.min_occurs&.to_i&.> 0
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# ── Inline Definitions ─────────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
def add_inline_field(fd, props, _required)
|
|
300
|
+
return unless fd.name
|
|
301
|
+
|
|
302
|
+
name = fd.name
|
|
303
|
+
props[name] = build_field_scalar_schema(fd)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def add_inline_assembly(ad, props, _required)
|
|
307
|
+
return unless ad.name
|
|
308
|
+
|
|
309
|
+
name = ad.name
|
|
310
|
+
build_assembly_schema(ad) if ad.model
|
|
311
|
+
props[name] = { "$ref" => "#/$defs/#{ad.name}" }
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# ── Field Schema Builders ──────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
def build_field_scalar_schema(fd)
|
|
317
|
+
schema = type_for(fd.as_type)
|
|
318
|
+
apply_field_constraints(schema, fd)
|
|
319
|
+
schema
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def build_field_items_schema(fd)
|
|
323
|
+
has_flags = (fd.define_flag || []).any? || (fd.flag || []).any?
|
|
324
|
+
if has_flags
|
|
325
|
+
build_field_object_schema(fd)
|
|
326
|
+
else
|
|
327
|
+
build_field_scalar_schema(fd)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def build_field_object_schema(fd)
|
|
332
|
+
obj = { "type" => "object", "properties" => {} }
|
|
333
|
+
required = []
|
|
334
|
+
value_key = fd.json_value_key || "STRVALUE"
|
|
335
|
+
|
|
336
|
+
# Value property
|
|
337
|
+
value_schema = type_for(fd.as_type)
|
|
338
|
+
apply_field_constraints(value_schema, fd)
|
|
339
|
+
obj["properties"][value_key] = value_schema
|
|
340
|
+
required << value_key
|
|
341
|
+
|
|
342
|
+
# Flags
|
|
343
|
+
(fd.define_flag || []).each do |fl|
|
|
344
|
+
next unless fl.name
|
|
345
|
+
|
|
346
|
+
obj["properties"][fl.name] = build_flag_type_schema(fl)
|
|
347
|
+
required << fl.name if fl.required == "yes"
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
(fd.flag || []).each do |fr|
|
|
351
|
+
next unless fr.ref
|
|
352
|
+
|
|
353
|
+
fdef = @flag_defs[fr.ref]
|
|
354
|
+
obj["properties"][fr.ref] =
|
|
355
|
+
fdef ? build_flag_type_schema(fdef) : { "type" => "string" }
|
|
356
|
+
required << fr.ref if fr.required == "yes"
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
obj["required"] = required unless required.empty?
|
|
360
|
+
obj["additionalProperties"] = false
|
|
361
|
+
obj
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def build_field_by_key_schema(fd)
|
|
365
|
+
build_field_object_schema(fd)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# ── Field Schema Builder (standalone definitions) ──────────────────
|
|
369
|
+
|
|
370
|
+
def build_field_def_schema(fd)
|
|
371
|
+
return @definitions[fd.name] if @definitions.key?(fd.name)
|
|
372
|
+
|
|
373
|
+
has_flags = (fd.define_flag || []).any? || (fd.flag || []).any?
|
|
374
|
+
schema = if has_flags
|
|
375
|
+
build_field_object_schema(fd)
|
|
376
|
+
else
|
|
377
|
+
build_field_scalar_schema(fd)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
if fd.formal_name && !fd.formal_name.is_a?(TrueClass)
|
|
381
|
+
title = fd.formal_name.is_a?(String) ? fd.formal_name : fd.formal_name.content
|
|
382
|
+
schema["title"] = title if title && !title.empty?
|
|
383
|
+
end
|
|
384
|
+
if fd.description.respond_to?(:content)
|
|
385
|
+
desc = fd.description.content
|
|
386
|
+
schema["description"] = desc if desc && !desc.empty?
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
@definitions[fd.name] = schema
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# ── Flag Schema Builders ───────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
def build_flag_schema(flag_def)
|
|
395
|
+
return @definitions[flag_def.name] if @definitions.key?(flag_def.name)
|
|
396
|
+
|
|
397
|
+
schema = build_flag_type_schema(flag_def)
|
|
398
|
+
@definitions[flag_def.name] = schema
|
|
399
|
+
schema
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def build_flag_type_schema(flag_or_def)
|
|
403
|
+
schema = type_for(flag_or_def.as_type)
|
|
404
|
+
|
|
405
|
+
# Apply constraints
|
|
406
|
+
constraint = flag_or_def.constraint
|
|
407
|
+
if constraint
|
|
408
|
+
apply_allowed_values(schema, constraint.allowed_values)
|
|
409
|
+
apply_matches(schema, constraint.matches)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
if flag_or_def.formal_name && !flag_or_def.formal_name.is_a?(TrueClass)
|
|
413
|
+
title = flag_or_def.formal_name.is_a?(String) ? flag_or_def.formal_name : flag_or_def.formal_name.content
|
|
414
|
+
schema["title"] = title if title && !title.empty?
|
|
415
|
+
end
|
|
416
|
+
if flag_or_def.description.respond_to?(:content)
|
|
417
|
+
desc = flag_or_def.description.content
|
|
418
|
+
schema["description"] = desc if desc && !desc.empty?
|
|
419
|
+
end
|
|
420
|
+
schema
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# ── Constraints ────────────────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
def apply_field_constraints(schema, fd)
|
|
426
|
+
constraint = fd.constraint
|
|
427
|
+
return unless constraint
|
|
428
|
+
|
|
429
|
+
apply_allowed_values(schema, constraint.allowed_values)
|
|
430
|
+
apply_matches(schema, constraint.matches)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def apply_allowed_values(schema, constraints)
|
|
434
|
+
return unless constraints
|
|
435
|
+
|
|
436
|
+
Array(constraints).each do |c|
|
|
437
|
+
enum_values = Array(c.enum).filter_map(&:value)
|
|
438
|
+
schema["enum"] = enum_values unless enum_values.empty?
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def apply_matches(schema, constraints)
|
|
443
|
+
return unless constraints
|
|
444
|
+
|
|
445
|
+
Array(constraints).each do |c|
|
|
446
|
+
schema["pattern"] = c.regex if c.regex
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# ── Type Mapping ───────────────────────────────────────────────────
|
|
451
|
+
|
|
452
|
+
def type_for(as_type)
|
|
453
|
+
TYPE_MAP[as_type]&.dup || { "type" => "string" }
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
end
|
|
@@ -6,7 +6,7 @@ module Metaschema
|
|
|
6
6
|
class BlockQuoteType < Lutaml::Model::Serializable; end
|
|
7
7
|
|
|
8
8
|
class ListItemType < Lutaml::Model::Serializable
|
|
9
|
-
attribute :content, :string
|
|
9
|
+
attribute :content, :string, collection: true
|
|
10
10
|
attribute :a, AnchorType, collection: true
|
|
11
11
|
attribute :insert, InsertType, collection: true
|
|
12
12
|
attribute :br, :string, collection: true
|