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,280 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "model_generator/utils"
|
|
4
|
+
require_relative "model_generator/field_factory"
|
|
5
|
+
require_relative "model_generator/assembly_factory"
|
|
6
|
+
require_relative "model_generator/services/collapsibles_collapser"
|
|
7
|
+
require_relative "model_generator/services/field_serializer"
|
|
8
|
+
require_relative "model_generator/services/field_deserializer"
|
|
9
|
+
|
|
10
|
+
module Metaschema
|
|
11
|
+
# Generates Ruby classes (Lutaml::Model::Serializable subclasses) from
|
|
12
|
+
# NIST Metaschema definitions. The generated classes support XML and JSON
|
|
13
|
+
# round-tripping with full fidelity.
|
|
14
|
+
#
|
|
15
|
+
# Delegates field class creation to FieldFactory and assembly class
|
|
16
|
+
# creation to AssemblyFactory. This class handles import resolution,
|
|
17
|
+
# augment application, and shared utilities.
|
|
18
|
+
class ModelGenerator
|
|
19
|
+
class << self
|
|
20
|
+
def generate_from_file(metaschema_path, base_path: nil)
|
|
21
|
+
base_path ||= File.dirname(File.expand_path(metaschema_path))
|
|
22
|
+
generate_from_xml(File.read(metaschema_path), base_path: base_path)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def generate_from_xml(xml_string, base_path: nil)
|
|
26
|
+
metaschema = Metaschema::Root.from_xml(xml_string)
|
|
27
|
+
new.generate(metaschema, base_path: base_path)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def generate_from_metaschema(metaschema, base_path: nil)
|
|
31
|
+
new.generate(metaschema, base_path: base_path)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_ruby_source(metaschema_path, module_name:, base_path: nil,
|
|
35
|
+
split: false)
|
|
36
|
+
classes = generate_from_file(metaschema_path, base_path: base_path)
|
|
37
|
+
emitter = RubySourceEmitter.new(classes, module_name, self)
|
|
38
|
+
split ? emitter.emit_split : emitter.emit
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Shared state — accessed by FieldFactory and AssemblyFactory via @g
|
|
43
|
+
attr_reader :classes, :field_defs, :assembly_defs, :flag_defs
|
|
44
|
+
attr_accessor :current_assembly_name
|
|
45
|
+
|
|
46
|
+
def generate(metaschema, base_path: nil)
|
|
47
|
+
@classes = {}
|
|
48
|
+
@flag_defs = {}
|
|
49
|
+
@assembly_defs = {}
|
|
50
|
+
@field_defs = {}
|
|
51
|
+
@namespace = metaschema.namespace
|
|
52
|
+
@current_assembly_name = nil
|
|
53
|
+
|
|
54
|
+
# Resolve imports — merge definitions from imported modules
|
|
55
|
+
resolve_and_merge_imports(metaschema, base_path)
|
|
56
|
+
|
|
57
|
+
collect_flag_definitions(metaschema)
|
|
58
|
+
collect_definition_registries(metaschema)
|
|
59
|
+
|
|
60
|
+
# Apply augments — add docs/flags to imported definitions
|
|
61
|
+
apply_augments(metaschema)
|
|
62
|
+
|
|
63
|
+
# Phase 1: Create field classes for all definitions (top-level + imported)
|
|
64
|
+
@field_defs.each_value do |fd|
|
|
65
|
+
next if @classes.key?("Field_#{Utils.safe_attr(fd.name)}")
|
|
66
|
+
|
|
67
|
+
FieldFactory.new(fd, self).create
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Phase 1: Create assembly placeholders for all definitions
|
|
71
|
+
# Phase 2: Populate assembly classes for all definitions
|
|
72
|
+
@assembly_defs.each_value do |ad|
|
|
73
|
+
factory = AssemblyFactory.new(ad, self)
|
|
74
|
+
factory.create_placeholder
|
|
75
|
+
factory.populate
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
@classes
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# ── XML Element Name Resolution ──────────────────────────────────
|
|
82
|
+
|
|
83
|
+
def assembly_xml_element_name(assembly_ref)
|
|
84
|
+
ref_name = assembly_ref.ref
|
|
85
|
+
return ref_name unless ref_name
|
|
86
|
+
|
|
87
|
+
return assembly_ref.use_name.content if assembly_ref.use_name&.content
|
|
88
|
+
|
|
89
|
+
defn = @assembly_defs[ref_name]
|
|
90
|
+
return defn.use_name.content if defn&.use_name&.content
|
|
91
|
+
|
|
92
|
+
ref_name
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def field_xml_element_name(field_ref)
|
|
96
|
+
ref_name = field_ref.ref
|
|
97
|
+
return ref_name unless ref_name
|
|
98
|
+
|
|
99
|
+
return field_ref.use_name.content if field_ref.use_name&.content
|
|
100
|
+
|
|
101
|
+
defn = @field_defs[ref_name]
|
|
102
|
+
return defn.use_name.content if defn&.use_name&.content
|
|
103
|
+
|
|
104
|
+
ref_name
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# ── Shared Utilities (used by both factories) ──────────────────────
|
|
108
|
+
|
|
109
|
+
def add_inline_flag(klass, flag_def)
|
|
110
|
+
return unless flag_def.name
|
|
111
|
+
|
|
112
|
+
attr_name = Utils.safe_attr(flag_def.name)
|
|
113
|
+
type = TypeMapper.map(flag_def.as_type)
|
|
114
|
+
klass.attribute attr_name, type
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def add_flag_reference(klass, flag_ref)
|
|
118
|
+
return unless flag_ref.ref
|
|
119
|
+
|
|
120
|
+
flag_name = flag_ref.ref
|
|
121
|
+
flag_def = @flag_defs[flag_name]
|
|
122
|
+
attr_name = Utils.safe_attr(flag_name)
|
|
123
|
+
type = flag_def ? TypeMapper.map(flag_def.as_type) : :string
|
|
124
|
+
klass.attribute attr_name, type
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def scoped_field_name(field_name)
|
|
128
|
+
base = "Field_#{field_name.gsub('-', '_')}"
|
|
129
|
+
@current_assembly_name ? "#{base}_in_#{@current_assembly_name}" : base
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def create_placeholder_assembly(name)
|
|
133
|
+
key = "Assembly_#{name.gsub('-', '_')}"
|
|
134
|
+
@classes[key] ||= Class.new(Lutaml::Model::Serializable)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# ── Constraint Validation Integration ──────────────────────────────
|
|
138
|
+
|
|
139
|
+
def apply_constraint_validation(klass, constraint_def)
|
|
140
|
+
return unless constraint_def
|
|
141
|
+
|
|
142
|
+
klass.instance_variable_set(:@metaschema_constraints, constraint_def)
|
|
143
|
+
klass.define_singleton_method(:metaschema_constraints) do
|
|
144
|
+
@metaschema_constraints
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
klass.define_method(:validate_constraints) do
|
|
148
|
+
validator = ConstraintValidator.new
|
|
149
|
+
validator.validate(self, self.class.metaschema_constraints)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private
|
|
154
|
+
|
|
155
|
+
# ── Import Resolution ──────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
def resolve_and_merge_imports(metaschema, base_path)
|
|
158
|
+
imported_defs = resolve_imports(metaschema, base_path)
|
|
159
|
+
|
|
160
|
+
# Merge imported definitions — first definition wins (top-level takes priority)
|
|
161
|
+
imported_defs.each do |defs|
|
|
162
|
+
defs[:flags].each { |name, defn| @flag_defs[name] ||= defn }
|
|
163
|
+
defs[:assemblies].each { |name, defn| @assembly_defs[name] ||= defn }
|
|
164
|
+
defs[:fields].each { |name, defn| @field_defs[name] ||= defn }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def resolve_imports(metaschema, base_path, visited: Set.new)
|
|
169
|
+
imports = metaschema.import
|
|
170
|
+
return [] unless imports && !imports.empty?
|
|
171
|
+
|
|
172
|
+
imports.flat_map do |import_elem|
|
|
173
|
+
href = import_elem.href
|
|
174
|
+
next [] unless href
|
|
175
|
+
|
|
176
|
+
# Resolve relative to the importing file's directory
|
|
177
|
+
import_path = if base_path
|
|
178
|
+
File.expand_path(href,
|
|
179
|
+
base_path)
|
|
180
|
+
else
|
|
181
|
+
File.expand_path(href)
|
|
182
|
+
end
|
|
183
|
+
next [] unless File.exist?(import_path)
|
|
184
|
+
|
|
185
|
+
# Cycle detection — skip already-visited files
|
|
186
|
+
next [] if visited.include?(import_path)
|
|
187
|
+
|
|
188
|
+
visited.add(import_path)
|
|
189
|
+
|
|
190
|
+
# Parse the imported metaschema
|
|
191
|
+
imported = Metaschema::Root.from_xml(File.read(import_path))
|
|
192
|
+
|
|
193
|
+
# Recursively resolve transitive imports
|
|
194
|
+
transitive = resolve_imports(imported, File.dirname(import_path),
|
|
195
|
+
visited: visited)
|
|
196
|
+
|
|
197
|
+
# Collect definitions from this imported module
|
|
198
|
+
defs = { flags: {}, assemblies: {}, fields: {} }
|
|
199
|
+
imported.define_flag&.each { |f| defs[:flags][f.name] = f if f.name }
|
|
200
|
+
imported.define_assembly&.each do |a|
|
|
201
|
+
defs[:assemblies][a.name] = a if a.name
|
|
202
|
+
end
|
|
203
|
+
imported.define_field&.each { |f| defs[:fields][f.name] = f if f.name }
|
|
204
|
+
|
|
205
|
+
transitive + [defs]
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# ── Augment Application ─────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
def apply_augments(metaschema)
|
|
212
|
+
return unless metaschema.respond_to?(:augment)
|
|
213
|
+
|
|
214
|
+
augments = metaschema.augment
|
|
215
|
+
return unless augments && !augments.empty?
|
|
216
|
+
|
|
217
|
+
augments.each do |aug|
|
|
218
|
+
name = aug.name
|
|
219
|
+
next unless name
|
|
220
|
+
|
|
221
|
+
# Try to find the definition to augment
|
|
222
|
+
target = @assembly_defs[name] || @field_defs[name] || @flag_defs[name]
|
|
223
|
+
next unless target
|
|
224
|
+
|
|
225
|
+
# Apply documentation augmentations
|
|
226
|
+
apply_augment_docs(target, aug)
|
|
227
|
+
apply_augment_flags(target, aug)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def apply_augment_docs(target, augment)
|
|
232
|
+
if augment.formal_name && !target.formal_name
|
|
233
|
+
target.formal_name = augment.formal_name
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
if augment.description && (!target.respond_to?(:description) || !target.description) && target.respond_to?(:description=)
|
|
237
|
+
target.description = augment.description
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def apply_augment_flags(target, augment)
|
|
242
|
+
return unless augment.flag&.any? || augment.define_flag&.any?
|
|
243
|
+
|
|
244
|
+
if target.respond_to?(:flag)
|
|
245
|
+
existing_refs = (target.flag || []).map(&:ref)
|
|
246
|
+
augment.flag.each do |fr|
|
|
247
|
+
next if existing_refs.include?(fr.ref)
|
|
248
|
+
|
|
249
|
+
target.flag = (target.flag || []) + [fr]
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
if target.respond_to?(:define_flag)
|
|
254
|
+
existing_names = (target.define_flag || []).map(&:name)
|
|
255
|
+
augment.define_flag.each do |fd|
|
|
256
|
+
next if existing_names.include?(fd.name)
|
|
257
|
+
|
|
258
|
+
target.define_flag = (target.define_flag || []) + [fd]
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# ── Definition Collection ──────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
def collect_flag_definitions(metaschema)
|
|
266
|
+
metaschema.define_flag&.each do |flag_def|
|
|
267
|
+
@flag_defs[flag_def.name] = flag_def if flag_def.name
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def collect_definition_registries(metaschema)
|
|
272
|
+
metaschema.define_assembly&.each do |ad|
|
|
273
|
+
@assembly_defs[ad.name] = ad if ad.name
|
|
274
|
+
end
|
|
275
|
+
metaschema.define_field&.each do |fd|
|
|
276
|
+
@field_defs[fd.name] = fd if fd.name
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Metaschema
|
|
4
4
|
class PreformattedType < 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
|
data/lib/metaschema/root.rb
CHANGED
|
@@ -15,6 +15,7 @@ module Metaschema
|
|
|
15
15
|
attribute :define_assembly, GlobalAssemblyDefinitionType, collection: true
|
|
16
16
|
attribute :define_field, GlobalFieldDefinitionType, collection: true
|
|
17
17
|
attribute :define_flag, GlobalFlagDefinitionType, collection: true
|
|
18
|
+
attribute :augment, AugmentType, collection: true
|
|
18
19
|
|
|
19
20
|
xml do
|
|
20
21
|
element "METASCHEMA"
|
|
@@ -34,6 +35,7 @@ module Metaschema
|
|
|
34
35
|
map_element "define-assembly", to: :define_assembly
|
|
35
36
|
map_element "define-field", to: :define_field
|
|
36
37
|
map_element "define-flag", to: :define_flag
|
|
38
|
+
map_element "augment", to: :augment
|
|
37
39
|
end
|
|
38
40
|
end
|
|
39
41
|
end
|