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,354 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metaschema
|
|
4
|
+
# Generates human-readable Markdown documentation from a parsed Metaschema document.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# ms = Metaschema::Root.from_xml(File.read("metaschema.xml"))
|
|
8
|
+
# markdown = MarkdownDocGenerator.generate(ms)
|
|
9
|
+
# File.write("docs.md", markdown)
|
|
10
|
+
#
|
|
11
|
+
# The generator walks the metaschema definition tree and emits Markdown with:
|
|
12
|
+
# - Schema title and version
|
|
13
|
+
# - Table of contents
|
|
14
|
+
# - Assembly, field, and flag definitions with descriptions
|
|
15
|
+
# - Property tables showing types, constraints, and cardinality
|
|
16
|
+
# - Examples from <example> elements
|
|
17
|
+
class MarkdownDocGenerator
|
|
18
|
+
def self.generate(metaschema)
|
|
19
|
+
new(metaschema).generate
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(metaschema)
|
|
23
|
+
@metaschema = metaschema
|
|
24
|
+
@output = []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def generate
|
|
28
|
+
header
|
|
29
|
+
table_of_contents
|
|
30
|
+
definitions
|
|
31
|
+
@output.join("\n")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def header
|
|
37
|
+
title = extract_text(@metaschema.schema_name) || "Metaschema"
|
|
38
|
+
version = extract_text(@metaschema.schema_version)
|
|
39
|
+
@output << "# #{title}"
|
|
40
|
+
@output << ""
|
|
41
|
+
@output << "**Version:** #{version}" if version
|
|
42
|
+
@output << "" if version
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def table_of_contents
|
|
46
|
+
assemblies = @metaschema.define_assembly || []
|
|
47
|
+
fields = @metaschema.define_field || []
|
|
48
|
+
flags = @metaschema.define_flag || []
|
|
49
|
+
|
|
50
|
+
items = assemblies.map do |a|
|
|
51
|
+
"- [#{a.name} (Assembly)](##{anchor(a.name)})"
|
|
52
|
+
end
|
|
53
|
+
fields.each { |f| items << "- [#{f.name} (Field)](##{anchor(f.name)})" }
|
|
54
|
+
flags.each { |f| items << "- [#{f.name} (Flag)](##{anchor(f.name)})" }
|
|
55
|
+
|
|
56
|
+
return if items.empty?
|
|
57
|
+
|
|
58
|
+
@output << "## Table of Contents"
|
|
59
|
+
@output << ""
|
|
60
|
+
items.each { |i| @output << i }
|
|
61
|
+
@output << ""
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def definitions
|
|
65
|
+
(@metaschema.define_assembly || []).each { |a| assembly_section(a) }
|
|
66
|
+
(@metaschema.define_field || []).each { |f| field_section(f) }
|
|
67
|
+
(@metaschema.define_flag || []).each { |f| flag_section(f) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# ── Assembly ───────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
def assembly_section(asm)
|
|
73
|
+
@output << "## #{asm.name}"
|
|
74
|
+
@output << ""
|
|
75
|
+
formal_name_and_description(asm)
|
|
76
|
+
|
|
77
|
+
# Flags
|
|
78
|
+
flag_rows = (asm.define_flag || []).map do |f|
|
|
79
|
+
flag_row(f, inline: true)
|
|
80
|
+
end
|
|
81
|
+
(asm.flag || []).each do |f|
|
|
82
|
+
flag_rows << ["`#{f.ref}`", "flag", f.required == "yes" ? "Yes" : "No",
|
|
83
|
+
"-"]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Model children
|
|
87
|
+
model = asm.model
|
|
88
|
+
child_rows = []
|
|
89
|
+
if model
|
|
90
|
+
(model.field || []).each { |fr| child_rows << field_ref_row(fr) }
|
|
91
|
+
(model.assembly || []).each { |ar| child_rows << assembly_ref_row(ar) }
|
|
92
|
+
(model.define_field || []).each do |fd|
|
|
93
|
+
child_rows << inline_field_row(fd)
|
|
94
|
+
end
|
|
95
|
+
(model.define_assembly || []).each do |ad|
|
|
96
|
+
child_rows << inline_assembly_row(ad)
|
|
97
|
+
end
|
|
98
|
+
(model.choice || []).each do |c|
|
|
99
|
+
(c.field || []).each do |fr|
|
|
100
|
+
child_rows << field_ref_row(fr, choice: true)
|
|
101
|
+
end
|
|
102
|
+
(c.assembly || []).each do |ar|
|
|
103
|
+
child_rows << assembly_ref_row(ar, choice: true)
|
|
104
|
+
end
|
|
105
|
+
(c.define_field || []).each do |fd|
|
|
106
|
+
child_rows << inline_field_row(fd, choice: true)
|
|
107
|
+
end
|
|
108
|
+
(c.define_assembly || []).each do |ad|
|
|
109
|
+
child_rows << inline_assembly_row(ad, choice: true)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
(model.choice_group || []).each do |cg|
|
|
113
|
+
child_rows << choice_group_row(cg)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
unless flag_rows.empty? && child_rows.empty?
|
|
118
|
+
@output << "### Properties"
|
|
119
|
+
@output << ""
|
|
120
|
+
@output << "| Name | Type | Required | Description |"
|
|
121
|
+
@output << "|------|------|----------|-------------|"
|
|
122
|
+
flag_rows.each { |r| @output << "| #{r.join(' | ')} |" }
|
|
123
|
+
child_rows.each { |r| @output << "| #{r.join(' | ')} |" }
|
|
124
|
+
@output << ""
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
constraints_section(asm.constraint)
|
|
128
|
+
examples_section(asm.example)
|
|
129
|
+
|
|
130
|
+
@output << "---"
|
|
131
|
+
@output << ""
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# ── Field ──────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
def field_section(fd)
|
|
137
|
+
@output << "## #{fd.name}"
|
|
138
|
+
@output << ""
|
|
139
|
+
formal_name_and_description(fd)
|
|
140
|
+
|
|
141
|
+
@output << "- **Type:** `#{fd.as_type || 'string'}`"
|
|
142
|
+
@output << "- **Collapsible:** #{fd.collapsible == 'yes' ? 'Yes' : 'No'}" if fd.collapsible == "yes"
|
|
143
|
+
|
|
144
|
+
# Flags on this field
|
|
145
|
+
flag_rows = (fd.define_flag || []).map { |f| flag_row(f, inline: true) }
|
|
146
|
+
(fd.flag || []).each do |f|
|
|
147
|
+
flag_rows << ["`#{f.ref}`", "flag", f.required == "yes" ? "Yes" : "No",
|
|
148
|
+
"-"]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
if flag_rows.any?
|
|
152
|
+
@output << ""
|
|
153
|
+
@output << "### Flags"
|
|
154
|
+
@output << ""
|
|
155
|
+
@output << "| Name | Type | Required | Description |"
|
|
156
|
+
@output << "|------|------|----------|-------------|"
|
|
157
|
+
flag_rows.each { |r| @output << "| #{r.join(' | ')} |" }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
@output << ""
|
|
161
|
+
constraints_section(fd.constraint)
|
|
162
|
+
examples_section(fd.example)
|
|
163
|
+
|
|
164
|
+
@output << "---"
|
|
165
|
+
@output << ""
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# ── Flag ───────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
def flag_section(fl)
|
|
171
|
+
@output << "## #{fl.name}"
|
|
172
|
+
@output << ""
|
|
173
|
+
formal_name_and_description(fl)
|
|
174
|
+
|
|
175
|
+
@output << "- **Type:** `#{fl.as_type || 'string'}`"
|
|
176
|
+
@output << ""
|
|
177
|
+
|
|
178
|
+
constraints_section(fl.constraint)
|
|
179
|
+
@output << "---"
|
|
180
|
+
@output << ""
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# ── Constraint helpers ─────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
def constraints_section(constraint)
|
|
186
|
+
return unless constraint
|
|
187
|
+
|
|
188
|
+
allowed = constraint.allowed_values
|
|
189
|
+
matches = constraint.matches
|
|
190
|
+
|
|
191
|
+
parts = []
|
|
192
|
+
|
|
193
|
+
if allowed
|
|
194
|
+
Array(allowed).each do |av|
|
|
195
|
+
target = av.respond_to?(:target) ? (av.target || ".") : "."
|
|
196
|
+
values = Array(av.enum).filter_map(&:value)
|
|
197
|
+
allow_other = av.allow_other == "yes"
|
|
198
|
+
level = av.level || "ERROR"
|
|
199
|
+
next if values.empty?
|
|
200
|
+
|
|
201
|
+
desc = "Allowed values for `#{target}`: #{values.map do |v|
|
|
202
|
+
"`#{v}`"
|
|
203
|
+
end.join(', ')}"
|
|
204
|
+
desc += " (or other)" if allow_other
|
|
205
|
+
desc += " [#{level}]"
|
|
206
|
+
parts << desc
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
if matches
|
|
211
|
+
Array(matches).each do |m|
|
|
212
|
+
target = m.target || "."
|
|
213
|
+
if m.regex
|
|
214
|
+
parts << "Matches regex `/#{m.regex}/` on `#{target}`"
|
|
215
|
+
elsif m.datatype
|
|
216
|
+
parts << "Matches datatype `#{m.datatype}` on `#{target}`"
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
return if parts.empty?
|
|
222
|
+
|
|
223
|
+
@output << "### Constraints"
|
|
224
|
+
@output << ""
|
|
225
|
+
parts.each { |p| @output << "- #{p}" }
|
|
226
|
+
@output << ""
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# ── Examples ───────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
def examples_section(examples)
|
|
232
|
+
return unless examples && !examples.empty?
|
|
233
|
+
|
|
234
|
+
@output << "### Examples"
|
|
235
|
+
@output << ""
|
|
236
|
+
|
|
237
|
+
Array(examples).each_with_index do |ex, i|
|
|
238
|
+
name = ex.description&.content || "Example #{i + 1}"
|
|
239
|
+
@output << "#### #{name}"
|
|
240
|
+
@output << ""
|
|
241
|
+
if ex.remarks&.content
|
|
242
|
+
@output << ex.remarks.content
|
|
243
|
+
@output << ""
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# ── Row builders ───────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
def field_ref_row(fr, choice: false)
|
|
251
|
+
ref = fr.ref
|
|
252
|
+
group_as = fr.group_as
|
|
253
|
+
json_name = group_as&.name || fr.use_name&.content || ref
|
|
254
|
+
cardinality = cardinality_str(fr.min_occurs, fr.max_occurs, group_as)
|
|
255
|
+
prefix = choice ? "*choice* " : ""
|
|
256
|
+
["`#{json_name}`", "#{prefix}field `#{ref}`", cardinality, ""]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def assembly_ref_row(ar, choice: false)
|
|
260
|
+
ref = ar.ref
|
|
261
|
+
group_as = ar.group_as
|
|
262
|
+
json_name = group_as&.name || ref
|
|
263
|
+
cardinality = cardinality_str(ar.min_occurs, ar.max_occurs, group_as)
|
|
264
|
+
prefix = choice ? "*choice* " : ""
|
|
265
|
+
["`#{json_name}`", "#{prefix}assembly `#{ref}`", cardinality, ""]
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def inline_field_row(fd, choice: false)
|
|
269
|
+
return [] unless fd.name
|
|
270
|
+
|
|
271
|
+
prefix = choice ? "*choice* " : ""
|
|
272
|
+
["`#{fd.name}`", "#{prefix}field (inline)", "-", ""]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def inline_assembly_row(ad, choice: false)
|
|
276
|
+
return [] unless ad.name
|
|
277
|
+
|
|
278
|
+
prefix = choice ? "*choice* " : ""
|
|
279
|
+
["`#{ad.name}`", "#{prefix}assembly (inline)", "-", ""]
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def choice_group_row(cg)
|
|
283
|
+
group_as = cg.group_as
|
|
284
|
+
json_name = group_as&.name || "choice-group"
|
|
285
|
+
["`#{json_name}`", "choice group", cardinality_str(nil, nil, group_as),
|
|
286
|
+
""]
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def flag_row(fl, inline: false)
|
|
290
|
+
name = fl.name
|
|
291
|
+
type = fl.as_type || "string"
|
|
292
|
+
desc = extract_description(fl)
|
|
293
|
+
["`#{name}`", "flag `#{type}`", fl.required == "yes" ? "Yes" : "No", desc]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# ── Helpers ────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
def formal_name_and_description(defn)
|
|
299
|
+
formal = defn.formal_name
|
|
300
|
+
if formal && !formal.is_a?(TrueClass)
|
|
301
|
+
text = formal.is_a?(String) ? formal : formal.content
|
|
302
|
+
@output << "**#{text}**" if text && !text.empty?
|
|
303
|
+
@output << ""
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
desc = extract_description(defn)
|
|
307
|
+
if desc && !desc.empty?
|
|
308
|
+
@output << desc
|
|
309
|
+
@output << ""
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def extract_description(defn)
|
|
314
|
+
return nil unless defn.respond_to?(:description) && defn.description
|
|
315
|
+
|
|
316
|
+
if defn.description.respond_to?(:content)
|
|
317
|
+
defn.description.content
|
|
318
|
+
else
|
|
319
|
+
defn.description.to_s
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def extract_text(value)
|
|
324
|
+
return nil unless value
|
|
325
|
+
|
|
326
|
+
if value.respond_to?(:content)
|
|
327
|
+
value.content
|
|
328
|
+
elsif value.is_a?(String)
|
|
329
|
+
value
|
|
330
|
+
else
|
|
331
|
+
value.to_s
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def cardinality_str(min, max, group_as)
|
|
336
|
+
min_val = min.to_i
|
|
337
|
+
max_val = max == "unbounded" ? nil : max&.to_i
|
|
338
|
+
|
|
339
|
+
if group_as
|
|
340
|
+
"1..#{max_val || '*'}" if min_val >= 0
|
|
341
|
+
elsif min_val.positive? && max_val
|
|
342
|
+
"#{min_val}..#{max_val}"
|
|
343
|
+
elsif min_val.positive?
|
|
344
|
+
"#{min_val}..*"
|
|
345
|
+
else
|
|
346
|
+
"0..1"
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def anchor(name)
|
|
351
|
+
name.downcase.gsub(/[^a-z0-9-]/, "-")
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Metaschema
|
|
4
4
|
class MarkupLineDatatype < Lutaml::Model::Serializable
|
|
5
|
-
attribute :content, :string
|
|
5
|
+
attribute :content, :string, collection: true
|
|
6
6
|
attribute :a, AnchorType, collection: true
|
|
7
7
|
attribute :insert, InsertType, collection: true
|
|
8
8
|
attribute :br, :string, collection: true
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metaschema
|
|
4
|
+
class MarkupMultilineDatatype < Lutaml::Model::Serializable
|
|
5
|
+
attribute :h1, InlineMarkupType, collection: true
|
|
6
|
+
attribute :h2, InlineMarkupType, collection: true
|
|
7
|
+
attribute :h3, InlineMarkupType, collection: true
|
|
8
|
+
attribute :h4, InlineMarkupType, collection: true
|
|
9
|
+
attribute :h5, InlineMarkupType, collection: true
|
|
10
|
+
attribute :h6, InlineMarkupType, collection: true
|
|
11
|
+
attribute :ul, ListType, collection: true
|
|
12
|
+
attribute :ol, OrderedListType, collection: true
|
|
13
|
+
attribute :pre, PreformattedType, collection: true
|
|
14
|
+
attribute :hr, :string, collection: true
|
|
15
|
+
attribute :blockquote, BlockQuoteType, collection: true
|
|
16
|
+
attribute :p, InlineMarkupType, collection: true
|
|
17
|
+
attribute :table, TableType, collection: true
|
|
18
|
+
attribute :img, ImageType, collection: true
|
|
19
|
+
|
|
20
|
+
xml do
|
|
21
|
+
element "MarkupMultilineDatatype"
|
|
22
|
+
namespace ::Metaschema::Namespace
|
|
23
|
+
mixed_content
|
|
24
|
+
|
|
25
|
+
map_element "h1", to: :h1
|
|
26
|
+
map_element "h2", to: :h2
|
|
27
|
+
map_element "h3", to: :h3
|
|
28
|
+
map_element "h4", to: :h4
|
|
29
|
+
map_element "h5", to: :h5
|
|
30
|
+
map_element "h6", to: :h6
|
|
31
|
+
map_element "ul", to: :ul
|
|
32
|
+
map_element "ol", to: :ol
|
|
33
|
+
map_element "pre", to: :pre
|
|
34
|
+
map_element "hr", to: :hr
|
|
35
|
+
map_element "blockquote", to: :blockquote
|
|
36
|
+
map_element "p", to: :p
|
|
37
|
+
map_element "table", to: :table
|
|
38
|
+
map_element "img", to: :img
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|