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.
@@ -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
@@ -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