lutaml-model 0.8.17 → 0.8.18

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.
Files changed (192) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/downstream-performance.yml +27 -1
  3. data/.github/workflows/js-pr-check.yml +102 -0
  4. data/.github/workflows/js-sync.yml +51 -0
  5. data/.github/workflows/opal.yml +26 -6
  6. data/.github/workflows/performance.yml +32 -7
  7. data/.github/workflows/rake.yml +78 -32
  8. data/.gitmodules +6 -0
  9. data/.rubocop_todo.yml +8 -0
  10. data/Gemfile +16 -1
  11. data/README.adoc +267 -0
  12. data/Rakefile +166 -4
  13. data/docs/_guides/schema-import.adoc +42 -0
  14. data/docs/_guides/xml-mapping.adoc +21 -0
  15. data/docs/_pages/validation.adoc +65 -0
  16. data/lib/compat/opal/generate_boot.rb +123 -0
  17. data/lib/compat/opal/js_bundle_entry.rb +42 -0
  18. data/lib/compat/opal/lutaml_model_boot.rb +497 -0
  19. data/lib/compat/opal/moxml_boot.rb +68 -0
  20. data/lib/compat/opal/yaml_compat.rb +32 -0
  21. data/lib/lutaml/json.rb +1 -1
  22. data/lib/lutaml/key_value/transform.rb +3 -8
  23. data/lib/lutaml/key_value/transformation/value_serializer.rb +14 -1
  24. data/lib/lutaml/key_value/transformation.rb +17 -5
  25. data/lib/lutaml/model/adapter_resolver.rb +2 -2
  26. data/lib/lutaml/model/attribute.rb +88 -11
  27. data/lib/lutaml/model/cached_type_resolver.rb +1 -1
  28. data/lib/lutaml/model/error/union_schema_unsupported_error.rb +11 -0
  29. data/lib/lutaml/model/global_context.rb +11 -8
  30. data/lib/lutaml/model/instrumentation.rb +2 -2
  31. data/lib/lutaml/model/mapping/mapping_rule.rb +0 -6
  32. data/lib/lutaml/model/register.rb +2 -2
  33. data/lib/lutaml/model/runtime_compatibility.rb +50 -0
  34. data/lib/lutaml/model/schema/class_loader.rb +58 -0
  35. data/lib/lutaml/model/schema/compiled_output.rb +83 -0
  36. data/lib/lutaml/model/schema/definitions/attribute.rb +32 -0
  37. data/lib/lutaml/model/schema/definitions/choice.rb +20 -0
  38. data/lib/lutaml/model/schema/definitions/facet.rb +40 -0
  39. data/lib/lutaml/model/schema/definitions/group_import.rb +22 -0
  40. data/lib/lutaml/model/schema/definitions/member_walk.rb +33 -0
  41. data/lib/lutaml/model/schema/definitions/model.rb +40 -0
  42. data/lib/lutaml/model/schema/definitions/namespace.rb +23 -0
  43. data/lib/lutaml/model/schema/definitions/restricted_type.rb +30 -0
  44. data/lib/lutaml/model/schema/definitions/sequence.rb +19 -0
  45. data/lib/lutaml/model/schema/definitions/simple_content.rb +22 -0
  46. data/lib/lutaml/model/schema/definitions/transform_facet.rb +21 -0
  47. data/lib/lutaml/model/schema/definitions/type_ref.rb +20 -0
  48. data/lib/lutaml/model/schema/definitions/union_type.rb +28 -0
  49. data/lib/lutaml/model/schema/definitions/xml_root.rb +20 -0
  50. data/lib/lutaml/model/schema/definitions.rb +24 -0
  51. data/lib/lutaml/model/schema/file_writer.rb +70 -0
  52. data/lib/lutaml/model/schema/generator/definitions_collection.rb +16 -2
  53. data/lib/lutaml/model/schema/generator/property.rb +21 -4
  54. data/lib/lutaml/model/schema/module_nesting.rb +31 -0
  55. data/lib/lutaml/model/schema/namespace_naming.rb +46 -0
  56. data/lib/lutaml/model/schema/registry_generator.rb +115 -0
  57. data/lib/lutaml/model/schema/renderers/base.rb +40 -0
  58. data/lib/lutaml/model/schema/renderers/mappings.rb +72 -0
  59. data/lib/lutaml/model/schema/renderers/member_decls.rb +115 -0
  60. data/lib/lutaml/model/schema/renderers/model.rb +121 -0
  61. data/lib/lutaml/model/schema/renderers/namespace.rb +30 -0
  62. data/lib/lutaml/model/schema/renderers/registration.rb +64 -0
  63. data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +93 -0
  64. data/lib/lutaml/model/schema/renderers/restricted_type.rb +88 -0
  65. data/lib/lutaml/model/schema/renderers/union.rb +83 -0
  66. data/lib/lutaml/model/schema/renderers.rb +19 -0
  67. data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +47 -0
  68. data/lib/lutaml/model/schema/rnc_compiler.rb +62 -0
  69. data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +91 -0
  70. data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +392 -0
  71. data/lib/lutaml/model/schema/rng_compiler/member_collector.rb +30 -0
  72. data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +152 -0
  73. data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +125 -0
  74. data/lib/lutaml/model/schema/rng_compiler.rb +220 -0
  75. data/lib/lutaml/model/schema/templates.rb +162 -0
  76. data/lib/lutaml/model/schema/xml_compiler/element_order.rb +41 -0
  77. data/lib/lutaml/model/schema/xml_compiler/registry_generator.rb +31 -107
  78. data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +191 -0
  79. data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +216 -0
  80. data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +153 -0
  81. data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +167 -0
  82. data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +87 -0
  83. data/lib/lutaml/model/schema/xml_compiler.rb +38 -517
  84. data/lib/lutaml/model/schema.rb +19 -0
  85. data/lib/lutaml/model/serialize/attribute_definition.rb +14 -2
  86. data/lib/lutaml/model/serialize/builder.rb +15 -2
  87. data/lib/lutaml/model/serialize/value_mapping.rb +9 -57
  88. data/lib/lutaml/model/serialize.rb +6 -9
  89. data/lib/lutaml/model/services/rule_value_extractor.rb +2 -3
  90. data/lib/lutaml/model/services.rb +0 -2
  91. data/lib/lutaml/model/store.rb +2 -1
  92. data/lib/lutaml/model/toml.rb +1 -1
  93. data/lib/lutaml/model/transform.rb +17 -39
  94. data/lib/lutaml/model/transformation_registry.rb +1 -1
  95. data/lib/lutaml/model/type.rb +1 -0
  96. data/lib/lutaml/model/union.rb +320 -0
  97. data/lib/lutaml/model/utils.rb +6 -0
  98. data/lib/lutaml/model/validation.rb +1 -4
  99. data/lib/lutaml/model/version.rb +1 -1
  100. data/lib/lutaml/model.rb +18 -0
  101. data/lib/lutaml/toml.rb +1 -1
  102. data/lib/lutaml/xml/adapter.rb +1 -1
  103. data/lib/lutaml/xml/adapter_loader.rb +1 -1
  104. data/lib/lutaml/xml/builder/base.rb +1 -1
  105. data/lib/lutaml/xml/builder.rb +1 -1
  106. data/lib/lutaml/xml/error/schema_validation_error.rb +25 -0
  107. data/lib/lutaml/xml/model_transform.rb +5 -1
  108. data/lib/lutaml/xml/schema/relaxng_schema.rb +5 -0
  109. data/lib/lutaml/xml/schema/xsd_schema.rb +4 -0
  110. data/lib/lutaml/xml/serialization/format_conversion.rb +48 -0
  111. data/lib/lutaml/xml/serialization/instance_methods.rb +13 -0
  112. data/lib/lutaml/xml/transformation/element_builder.rb +21 -8
  113. data/lib/lutaml/xml/transformation/ordered_applier.rb +6 -0
  114. data/lib/lutaml/xml/xsd_validator.rb +66 -0
  115. data/lib/lutaml/xml.rb +53 -12
  116. data/lutaml-model.gemspec +4 -2
  117. data/spec/fixtures/xml/schema/rnc/address_book.rnc +15 -0
  118. data/spec/fixtures/xml/schema/rnc/book_features.rnc +18 -0
  119. data/spec/fixtures/xml/schema/rnc/includes/book.rnc +6 -0
  120. data/spec/fixtures/xml/schema/rnc/includes/circular_a.rnc +8 -0
  121. data/spec/fixtures/xml/schema/rnc/includes/circular_b.rnc +8 -0
  122. data/spec/fixtures/xml/schema/rnc/includes/library.rnc +8 -0
  123. data/spec/fixtures/xml/schema/rng/address_book.rng +16 -0
  124. data/spec/fixtures/xml/schema/rng/book.rng +27 -0
  125. data/spec/fixtures/xml/schema/rng/fixed_value.rng +11 -0
  126. data/spec/fixtures/xml/schema/rng/fragment_in_choice.rng +30 -0
  127. data/spec/fixtures/xml/schema/rng/full_name.rng +39 -0
  128. data/spec/fixtures/xml/schema/rng/integer_range.rng +32 -0
  129. data/spec/fixtures/xml/schema/rng/list_type.rng +12 -0
  130. data/spec/fixtures/xml/schema/rng/namespaced.rng +9 -0
  131. data/spec/fixtures/xml/schema/rng/paragraph.rng +13 -0
  132. data/spec/fixtures/xml/schema/rng/person.rng +23 -0
  133. data/spec/fixtures/xml/schema/rng/union.rng +14 -0
  134. data/spec/fixtures/xml/validate_xml_with_address.xsd +13 -0
  135. data/spec/fixtures/xml/validate_xml_with_contact.xsd +11 -0
  136. data/spec/fixtures/xml/validate_xml_with_person.xsd +11 -0
  137. data/spec/fixtures/xml/validate_xml_with_person_strict.xsd +17 -0
  138. data/spec/fixtures/xml/validate_xml_with_product.xsd +37 -0
  139. data/spec/lutaml/model/attribute_apply_value_map_spec.rb +134 -0
  140. data/spec/lutaml/model/attribute_spec.rb +169 -12
  141. data/spec/lutaml/model/boot_manifest_spec.rb +30 -0
  142. data/spec/lutaml/model/collection_spec.rb +18 -0
  143. data/spec/lutaml/model/mixed_content_spec.rb +171 -0
  144. data/spec/lutaml/model/opal_smoke_spec.rb +67 -2
  145. data/spec/lutaml/model/ordered_content_spec.rb +30 -0
  146. data/spec/lutaml/model/raw_element_spec.rb +125 -0
  147. data/spec/lutaml/model/rule_value_extractor_spec.rb +7 -14
  148. data/spec/lutaml/model/runtime_compatibility_spec.rb +25 -0
  149. data/spec/lutaml/model/schema/renderers/model_spec.rb +149 -0
  150. data/spec/lutaml/model/schema/renderers/namespace_spec.rb +36 -0
  151. data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +75 -0
  152. data/spec/lutaml/model/schema/renderers/union_spec.rb +58 -0
  153. data/spec/lutaml/model/transform_apply_value_map_spec.rb +64 -0
  154. data/spec/lutaml/model/union_attribute_spec.rb +658 -0
  155. data/spec/lutaml/model/union_spec.rb +257 -0
  156. data/spec/lutaml/model/utils_spec.rb +14 -0
  157. data/spec/lutaml/xml/opal_xml_spec.rb +22 -20
  158. data/spec/lutaml/xml/schema/compiler_spec.rb +21 -4
  159. data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +404 -0
  160. data/spec/lutaml/xml/schema/rng_compiler_spec.rb +893 -0
  161. data/spec/lutaml/xml/validate_xml_with_constructs_spec.rb +222 -0
  162. data/spec/lutaml/xml/validate_xml_with_spec.rb +186 -0
  163. data/spec/spec_helper.rb +28 -11
  164. data/spec/support/opal.rb +5 -2
  165. metadata +97 -31
  166. data/lib/lutaml/model/schema/xml_compiler/attribute.rb +0 -106
  167. data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +0 -47
  168. data/lib/lutaml/model/schema/xml_compiler/choice.rb +0 -67
  169. data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +0 -27
  170. data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +0 -34
  171. data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +0 -173
  172. data/lib/lutaml/model/schema/xml_compiler/element.rb +0 -121
  173. data/lib/lutaml/model/schema/xml_compiler/group.rb +0 -113
  174. data/lib/lutaml/model/schema/xml_compiler/restriction.rb +0 -104
  175. data/lib/lutaml/model/schema/xml_compiler/sequence.rb +0 -52
  176. data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +0 -40
  177. data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +0 -265
  178. data/lib/lutaml/model/schema/xml_compiler/xml_namespace_class.rb +0 -110
  179. data/lib/lutaml/model/services/default_value_resolver.rb +0 -60
  180. data/spec/lutaml/model/services/default_value_resolver_spec.rb +0 -162
  181. data/spec/lutaml/xml/schema/compiler/attribute_group_spec.rb +0 -71
  182. data/spec/lutaml/xml/schema/compiler/attribute_spec.rb +0 -67
  183. data/spec/lutaml/xml/schema/compiler/choice_spec.rb +0 -74
  184. data/spec/lutaml/xml/schema/compiler/complex_content_restriction_spec.rb +0 -60
  185. data/spec/lutaml/xml/schema/compiler/complex_content_spec.rb +0 -41
  186. data/spec/lutaml/xml/schema/compiler/complex_type_spec.rb +0 -202
  187. data/spec/lutaml/xml/schema/compiler/element_spec.rb +0 -68
  188. data/spec/lutaml/xml/schema/compiler/group_spec.rb +0 -90
  189. data/spec/lutaml/xml/schema/compiler/restriction_spec.rb +0 -77
  190. data/spec/lutaml/xml/schema/compiler/sequence_spec.rb +0 -65
  191. data/spec/lutaml/xml/schema/compiler/simple_content_spec.rb +0 -61
  192. data/spec/lutaml/xml/schema/compiler/simple_type_spec.rb +0 -184
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Lutaml
6
+ module Model
7
+ module Schema
8
+ # Compiles a parsed RNG grammar into Lutaml::Model::Serializable Ruby
9
+ # source. Mirrors XmlCompiler in API, options, and output format.
10
+ #
11
+ # Requires the `rng` gem (https://github.com/lutaml/rng) at runtime --
12
+ # that gem provides the Rng::Grammar parsing model. lutaml-model itself
13
+ # has no dependency on it. Registration of Schema.from_relaxng is done
14
+ # in lib/lutaml/xml.rb (alongside Schema.from_xml and Schema.to_relaxng).
15
+ #
16
+ # Entry point: Lutaml::Model::Schema::RngCompiler.to_models(rng, options)
17
+ module RngCompiler
18
+ extend self
19
+
20
+ autoload :MemberCollector, "#{__dir__}/rng_compiler/member_collector"
21
+ autoload :ElementVisitor, "#{__dir__}/rng_compiler/element_visitor"
22
+ autoload :DefineClassifier, "#{__dir__}/rng_compiler/define_classifier"
23
+ autoload :ValueTypeResolver, "#{__dir__}/rng_compiler/value_type_resolver"
24
+ autoload :RngHelpers, "#{__dir__}/rng_compiler/rng_helpers"
25
+
26
+ # Map RNG <data type="..."/> values to Lutaml::Model attribute type
27
+ # symbols. Mirrors Lutaml::Xml::Schema::RelaxngSchema.get_relaxng_type
28
+ # so RNG-generate + compile-back is a round-trip.
29
+ DATA_TYPE_MAP = {
30
+ "string" => :string,
31
+ "integer" => :integer,
32
+ "int" => :integer,
33
+ "long" => :integer,
34
+ "boolean" => :boolean,
35
+ "float" => :float,
36
+ "double" => :float,
37
+ "decimal" => :decimal,
38
+ "date" => :date,
39
+ "dateTime" => :date_time,
40
+ "time" => :time,
41
+ }.freeze
42
+
43
+ DEFAULT_DATA_TYPE = :string
44
+
45
+ def to_models(rng, options = {})
46
+ require_rng_parser!
47
+
48
+ opts = normalize_options(options)
49
+ output = compile(rng, opts)
50
+
51
+ dispatch_output(output, normalize_options(opts))
52
+ end
53
+
54
+ def compile(rng, options = {})
55
+ require_rng_parser!
56
+
57
+ opts = normalize_options(options)
58
+ grammar = parse_grammar(rng, opts)
59
+ classes, namespaces = compile_grammar(grammar)
60
+
61
+ entries =
62
+ classes.map { |name, spec| CompiledOutput::Entry.new(name, spec, :model) } +
63
+ namespaces.map { |name, spec| CompiledOutput::Entry.new(name, spec, :namespace) }
64
+
65
+ CompiledOutput.new(
66
+ entries: entries,
67
+ module_namespace: opts[:module_namespace],
68
+ register_id: opts[:register_id],
69
+ )
70
+ end
71
+
72
+ private
73
+
74
+ def require_rng_parser!
75
+ return if defined?(::Rng::Grammar)
76
+
77
+ raise "RNG schema compilation requires the rng gem. " \
78
+ "Add `gem \"rng\"` to your Gemfile."
79
+ end
80
+
81
+ def normalize_options(options)
82
+ opts = options.dup
83
+ opts[:indent] ||= 2
84
+ opts[:register_id] ||= :default
85
+ opts
86
+ end
87
+
88
+ def parse_grammar(rng, options)
89
+ return rng if rng.is_a?(::Rng::Grammar)
90
+
91
+ if options[:location]
92
+ ::Rng.parse(rng, location: options[:location], resolve_external: true)
93
+ else
94
+ ::Rng::Grammar.from_xml(rng)
95
+ end
96
+ end
97
+
98
+ def dispatch_output(output, options)
99
+ if options[:create_files]
100
+ dir = options.fetch(
101
+ :output_dir,
102
+ options.fetch(:default_output_dir, "rng_models_#{Time.now.to_i}"),
103
+ )
104
+ FileWriter.write(output, dir)
105
+ true
106
+ elsif options[:load_classes]
107
+ ClassLoader.load(output)
108
+ output.sources
109
+ else
110
+ output.sources
111
+ end
112
+ end
113
+
114
+ # Returns [classes_hash, namespaces_hash]. `classes` holds
115
+ # Definitions::Model / Definitions::RestrictedType / Definitions::UnionType.
116
+ # `namespaces` holds Definitions::Namespace.
117
+ def compile_grammar(grammar)
118
+ defines = grammar.define.to_h { |d| [d.name, d] }
119
+ classes = {}
120
+ namespaces = {}
121
+ registrar = ->(uri) { register_namespace(namespaces, uri) }
122
+
123
+ default_uri = grammar_default_ns(grammar)
124
+ visitor = ElementVisitor.new(
125
+ defines, classes,
126
+ default_namespace_class: default_uri && registrar.call(default_uri),
127
+ register_namespace: registrar
128
+ )
129
+
130
+ grammar.start.each do |start|
131
+ visitor.compile_element(start.element) if start.element
132
+ Array(start.ref).each do |ref|
133
+ target = defines[ref.name]
134
+ visitor.compile_define(target) if target
135
+ end
136
+ end
137
+
138
+ # Sweep: compile any <define> not reachable from <start>.
139
+ grammar.define.each { |define| visitor.compile_define(define) }
140
+
141
+ finalize_models(classes)
142
+
143
+ [classes, namespaces]
144
+ end
145
+
146
+ def grammar_default_ns(grammar)
147
+ uri = grammar.respond_to?(:ns) ? grammar.ns : nil
148
+ uri if uri.is_a?(String) && !uri.empty?
149
+ end
150
+
151
+ # Build-or-find the namespace for `uri`; returns its class_name.
152
+ # RNG/RNC name-prefixed elements are namespace-qualified, so descendant
153
+ # elements inherit the namespace via element_form_default :qualified.
154
+ def register_namespace(namespaces, uri)
155
+ existing = namespaces.values.find { |ns| ns.uri == uri }
156
+ return existing.class_name if existing
157
+
158
+ # Distinct URIs can map to the same generated class name AND prefix
159
+ # (e.g. both `urn:a` and `urn:b` -> "DefaultNamespace"/"ns"); uniquify
160
+ # both so one namespace does not silently overwrite the other or
161
+ # collide with its prefix at serialization.
162
+ class_name = RngHelpers.unique_class_name(
163
+ namespaces, NamespaceNaming.class_name_for(uri)
164
+ )
165
+ ns = Definitions::Namespace.new(
166
+ class_name: class_name,
167
+ uri: uri,
168
+ prefix_default: unique_prefix(namespaces, NamespaceNaming.prefix_for(uri)),
169
+ element_form_default: :qualified,
170
+ )
171
+ namespaces[class_name] = ns
172
+ class_name
173
+ end
174
+
175
+ # A prefix that does not collide with an already-registered namespace's
176
+ # prefix (appends 2, 3, ... on collision).
177
+ def unique_prefix(namespaces, base)
178
+ taken = namespaces.values.map(&:prefix_default)
179
+ return base unless taken.include?(base)
180
+
181
+ counter = 2
182
+ counter += 1 while taken.include?("#{base}#{counter}")
183
+ "#{base}#{counter}"
184
+ end
185
+
186
+ # Set `required_files` so each generated file requires its
187
+ # dependencies: a Model requires its imports/class_ref types/namespace;
188
+ # a namespaced RestrictedType requires its namespace class.
189
+ def finalize_models(classes)
190
+ classes.each_value do |spec|
191
+ case spec
192
+ when Definitions::Model
193
+ spec.required_files =
194
+ Renderers::RequiredFilesCalculator.class_names_for_rng(spec)
195
+ .map { |dep| require_relative_line(dep) }
196
+ when Definitions::RestrictedType
197
+ next unless spec.namespace_class_name
198
+
199
+ spec.required_files = namespaced_type_requires(spec)
200
+ end
201
+ end
202
+ end
203
+
204
+ # A namespaced RestrictedType requires its namespace class and, when it
205
+ # subclasses another generated type (a bare name, not a Lutaml built-in),
206
+ # that parent too.
207
+ def namespaced_type_requires(spec)
208
+ deps = [spec.namespace_class_name]
209
+ parent = spec.parent_class
210
+ deps << parent if parent && !parent.include?("::")
211
+ deps.map { |dep| require_relative_line(dep) }
212
+ end
213
+
214
+ def require_relative_line(class_name)
215
+ %(require_relative "#{Utils.snake_case(class_name)}")
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module Lutaml
6
+ module Model
7
+ module Schema
8
+ # Templates that are *actually* shared between schema compilers
9
+ # (XSD, RNG, future formats). Single-consumer templates live in
10
+ # the renderer that uses them — putting them here would just be
11
+ # relocation, not deduplication.
12
+ #
13
+ # Each constant is an `ERB.new(...)` evaluated via
14
+ # `TEMPLATE.result(binding)` from a renderer that exposes the
15
+ # required hook methods (documented above each template).
16
+ module Templates
17
+ # ----------------------------------------------------------------
18
+ # Single shape for any Lutaml::Model::Serializable subclass
19
+ # generated by ANY schema compiler. There is no per-format
20
+ # variant — adding a new xml directive here propagates to every
21
+ # compiler that uses this template. Consumed by:
22
+ # - Lutaml::Model::Schema::Renderers::Model
23
+ # (renders XSD complex types / groups and RNG generated classes)
24
+ #
25
+ # Required binding methods (renderer must expose all of these,
26
+ # returning "" / nil / false for inapplicable sections):
27
+ #
28
+ # from ClassBoilerplate:
29
+ # module_opening, module_closing,
30
+ # registration_methods, registration_execution,
31
+ # boilerplate_indent_str
32
+ #
33
+ # identity / parent:
34
+ # rendered_class_name String — CamelCase
35
+ # serializable_class_parent String — e.g. "Lutaml::Model::Serializable"
36
+ #
37
+ # header:
38
+ # serializable_class_required_files String — `require_relative` lines or ""
39
+ # serializable_class_documentation String — doc comment block or ""
40
+ #
41
+ # body — attribute declarations:
42
+ # serializable_class_attributes String — `attribute :foo, :bar` lines
43
+ # serializable_class_imports String — `import_model :foo` lines or ""
44
+ #
45
+ # body — xml block:
46
+ # xml_root_directive_line String or nil — `element "X"` /
47
+ # `type_name "X"` / nil to skip
48
+ # xml_namespace_line String or nil — `namespace ClassName` / nil
49
+ # xml_mixed_content? Boolean — emit `mixed_content`
50
+ # xml_text_content? Boolean — emit `map_content to: :content`
51
+ # xml_attribute_mappings String — `map_element`/`map_attribute` lines
52
+ # xml_extra_mappings String — extra mapping lines or ""
53
+ # (e.g. XSD's simple_content tail)
54
+ # ----------------------------------------------------------------
55
+ SERIALIZABLE_CLASS = ERB.new(<<~TMPL, trim_mode: "-")
56
+ # frozen_string_literal: true
57
+
58
+ require "lutaml/model"
59
+ <%= serializable_class_required_files -%>
60
+ <%= module_opening -%>
61
+ <%= serializable_class_documentation -%>
62
+ class <%= rendered_class_name %> < <%= serializable_class_parent %>
63
+ <%= serializable_class_attributes -%>
64
+ <%= serializable_class_imports -%>
65
+ <%= "\n" if serializable_class_attributes.length.positive? || serializable_class_imports.length.positive? -%>
66
+ <%= boilerplate_indent_str %>xml do
67
+ <%- if xml_root_directive_line -%>
68
+ <%= boilerplate_indent_str * 2 %><%= xml_root_directive_line %>
69
+ <%- end -%>
70
+ <%- if xml_namespace_line -%>
71
+ <%= boilerplate_indent_str * 2 %><%= xml_namespace_line %>
72
+ <%- end -%>
73
+ <%- if xml_mixed_content? -%>
74
+ <%= boilerplate_indent_str * 2 %>mixed_content
75
+ <%- end -%>
76
+ <%- if xml_text_content? -%>
77
+ <%= boilerplate_indent_str * 2 %>map_content to: :content
78
+ <%- end -%>
79
+ <%= xml_attribute_mappings -%>
80
+ <%= xml_extra_mappings -%>
81
+ <%= boilerplate_indent_str %>end
82
+ <%= registration_methods -%>
83
+ end
84
+ <%= module_closing -%>
85
+ <%= registration_execution -%>
86
+ TMPL
87
+
88
+ # XmlNamespace subclass. Consumed by:
89
+ # - Lutaml::Model::Schema::Renderers::Namespace
90
+ # Binding: module_opening, module_closing, class_name, uri, prefix_default.
91
+ XML_NAMESPACE = ERB.new(<<~TMPL, trim_mode: "-")
92
+ # frozen_string_literal: true
93
+
94
+ require "lutaml/model"
95
+
96
+ <%= module_opening -%>
97
+ # Namespace: <%= uri %>
98
+ class <%= class_name %> < Lutaml::Xml::W3c::XmlNamespace
99
+ uri <%= uri.inspect %>
100
+ prefix_default <%= prefix_default.inspect %>
101
+ <%- if element_form_default_line -%>
102
+ <%= element_form_default_line %>
103
+ <%- end -%>
104
+ end
105
+ <%= module_closing -%>
106
+ TMPL
107
+
108
+ # Restricted simple-type subclass (cast method that mutates options
109
+ # with facets then delegates to super). Consumed by:
110
+ # - Lutaml::Model::Schema::Renderers::RestrictedType
111
+ # Binding: module_opening, module_closing, registration_methods,
112
+ # registration_execution, rendered_class_name, parent_class,
113
+ # xml_namespace_line, restricted_simple_type_required_files,
114
+ # restricted_simple_type_cast_body, boilerplate_indent_str.
115
+ RESTRICTED_SIMPLE_TYPE = ERB.new(<<~TMPL, trim_mode: "-")
116
+ # frozen_string_literal: true
117
+ require "lutaml/model"
118
+ <%= restricted_simple_type_required_files -%>
119
+ <%= module_opening -%>
120
+ class <%= rendered_class_name %><%= " < \#{parent_class}" if parent_class %>
121
+ <%- if xml_namespace_line -%>
122
+ <%= boilerplate_indent_str %>xml do
123
+ <%= boilerplate_indent_str * 2 %><%= xml_namespace_line %>
124
+ <%= boilerplate_indent_str %>end
125
+ <%- end -%>
126
+ <%= boilerplate_indent_str %>def self.cast(value, options = {})
127
+ <%= boilerplate_indent_str * 2 %>return if value.nil?
128
+
129
+ <%= restricted_simple_type_cast_body -%>
130
+ <%= boilerplate_indent_str * 2 %>value = super(value, options)
131
+ <%= boilerplate_indent_str * 2 %>value
132
+ <%= boilerplate_indent_str %>end
133
+ <%= registration_methods -%>
134
+ end
135
+ <%= module_closing -%>
136
+ <%= registration_execution -%>
137
+ TMPL
138
+
139
+ # Lutaml::Model::Type::Value union subclass. Consumed by:
140
+ # - Lutaml::Model::Schema::Renderers::Union
141
+ # `union_cast_body` selects the cast strategy from the spec
142
+ # (XSD: `||`-chained resolve_type; RNG: each+rescue).
143
+ UNION_TYPE = ERB.new(<<~TMPL, trim_mode: "-")
144
+ # frozen_string_literal: true
145
+ require "lutaml/model"
146
+ <%= union_required_files -%>
147
+ <%= module_opening -%>
148
+ class <%= rendered_class_name %> < Lutaml::Model::Type::Value
149
+ <%= boilerplate_indent_str %>def self.cast(value, options = {})
150
+ <%= boilerplate_indent_str * 2 %>return if value.nil?
151
+
152
+ <%= union_cast_body -%>
153
+ <%= boilerplate_indent_str %>end
154
+ <%= registration_methods -%>
155
+ end
156
+ <%= module_closing -%>
157
+ <%= registration_execution -%>
158
+ TMPL
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module XmlCompiler
7
+ # Iteration helper for the Lutaml::Xml XSD AST. Wraps the
8
+ # `#element_order` array each AST node carries with the special
9
+ # cases the compiler relies on:
10
+ # * built-in `Xsd::Base` nodes already produce a resolved order
11
+ # * `text`-only entries and `<import>` / `<include>` siblings
12
+ # are skipped
13
+ # * placeholders in the order are mapped to the matching child
14
+ # accessor on the node so callers receive concrete AST values.
15
+ module ElementOrder
16
+ module_function
17
+
18
+ def resolved(object)
19
+ return [] if object.element_order.nil?
20
+
21
+ if object.is_a?(Lutaml::Xml::Schema::Xsd::Base)
22
+ return object.resolved_element_order
23
+ end
24
+
25
+ object.element_order.each_with_object(object.element_order.dup) do |builder, array|
26
+ next array.delete(builder) if builder.text? || ELEMENT_ORDER_IGNORABLE.include?(builder.name)
27
+
28
+ index = 0
29
+ array.each_with_index do |element, i|
30
+ next unless element == builder
31
+
32
+ array[i] = Array(object.public_send(Utils.snake_case(builder.name)))[index]
33
+ index += 1
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,124 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "erb"
4
-
5
3
  module Lutaml
6
4
  module Model
7
5
  module Schema
8
6
  module XmlCompiler
9
- class RegistryGenerator
10
- TEMPLATE = ERB.new(<<~TEMPLATE, trim_mode: "-")
11
- # frozen_string_literal: true
12
- # Auto-generated central registry for <%= @module_namespace %>
13
-
14
- <%= module_opening %>
15
- <%= autoload_declarations %>
16
-
17
- def self.register_all
18
- # Ensure context exists before using it
19
- context = Lutaml::Model::GlobalContext.context(:<%= @register_id %>) ||
20
- Lutaml::Model::GlobalContext.create_context(id: :<%= @register_id %>)
21
-
22
- # Phase 1: Register all models (no imports)
23
- <%= registration_calls %>
24
-
25
- # Phase 2: Resolve model, choice, and restrict imports
26
- <%= model_import_resolution_calls %>
27
-
28
- # Phase 3: Resolve mapping and sequence imports
29
- <%= mapping_import_resolution_calls %>
30
- end
31
- <%= module_closing %>
32
- TEMPLATE
33
-
34
- def self.generate(classes_hash, options = {})
35
- new(classes_hash, options).generate
36
- end
37
-
38
- def initialize(classes_hash, options)
39
- @classes = classes_hash
40
- @module_namespace = options[:module_namespace]
41
- @register_id = options[:register_id] || :default
42
- # Cache split modules for reuse
43
- @modules = @module_namespace&.split("::") || []
44
- end
45
-
46
- def generate
47
- return nil unless @module_namespace
48
-
49
- TEMPLATE.result(binding)
50
- end
51
-
7
+ # XSD registry generator. Extends the shared base with an extra
8
+ # phase for resolving model imports — XSD-generated classes use
9
+ # ensure_imports! hooks that need to run after every model is
10
+ # registered.
11
+ class RegistryGenerator < Lutaml::Model::Schema::RegistryGenerator
52
12
  private
53
13
 
54
- def module_opening
55
- return "" if @modules.empty?
56
-
57
- @modules.map.with_index do |mod, i|
58
- "#{' ' * i}module #{mod}"
59
- end.join("\n")
60
- end
61
-
62
- def module_closing
63
- return "" if @modules.empty?
64
-
65
- @modules.reverse.map.with_index do |_mod, i|
66
- "#{' ' * (@modules.size - i - 1)}end"
67
- end.join("\n")
68
- end
69
-
70
- def autoload_declarations
71
- indent = " " * module_depth
72
- @classes.keys.filter_map do |name|
73
- class_name = Utils.camel_case(name)
74
- file_name = Utils.snake_case(name)
75
- # Use relative path from registry file location
76
- module_subdir = @module_namespace.split("::").last.downcase
77
- "#{indent}autoload :#{class_name}, File.join(__dir__, \"#{module_subdir}\", \"#{file_name}\")"
78
- end.join("\n")
79
- end
80
-
81
- def registration_calls
82
- indent = " " * (module_depth + 1)
83
- @classes.keys.filter_map do |name|
84
- # Skip namespace classes (check the name, not the content)
85
- next if name.to_s.include?("Namespace")
86
-
87
- class_name = Utils.camel_case(name)
88
- id = Utils.snake_case(name).to_sym
89
- "#{indent}#{class_name} # Trigger autoload\n#{indent}context.registry.register(:#{id}, #{class_name})\n#{indent}#{class_name}.instance_variable_set(:@register, :#{@register_id})"
90
- end.join("\n")
14
+ # Override the shared `registration_body` hook with three phases.
15
+ def registration_body
16
+ [
17
+ "#{body_indent}# Phase 1: Register all models (no imports)",
18
+ registration_calls,
19
+ "",
20
+ "#{body_indent}# Phase 2: Resolve model, choice, and restrict imports",
21
+ model_import_resolution_calls,
22
+ "",
23
+ "#{body_indent}# Phase 3: Resolve serialization mapping imports (sequence imports)",
24
+ mapping_import_resolution_calls,
25
+ ].join("\n")
91
26
  end
92
27
 
93
28
  def model_import_resolution_calls
94
- indent = " " * (module_depth + 1)
95
- "\n#{indent}# Resolve model structure imports (attributes, choices)\n" +
96
- @classes.keys.filter_map do |name|
97
- next if name.to_s.include?("Namespace")
98
-
99
- class_name = Utils.camel_case(name)
100
- "#{indent}#{class_name}.ensure_imports!(:#{@register_id}) if #{class_name}.respond_to?(:ensure_imports!)"
101
- end.join("\n")
29
+ @classes.map do |entry|
30
+ class_name = Utils.camel_case(entry.name)
31
+ "#{body_indent}#{class_name}.ensure_imports!(:#{@register_id}) " \
32
+ "if #{class_name}.respond_to?(:ensure_imports!)"
33
+ end.join("\n")
102
34
  end
103
35
 
36
+ # Only Serializable subclasses have mappings; simple-type and
37
+ # union-type subclasses (Lutaml::Model::Type::Value) do not.
38
+ # The emitted code guards with respond_to? so it works
39
+ # uniformly across both kinds of generated class.
104
40
  def mapping_import_resolution_calls
105
- indent = " " * (module_depth + 1)
106
- "\n#{indent}# Resolve serialization mapping imports (sequence imports)\n" +
107
- @classes.keys.filter_map do |name|
108
- next if name.to_s.include?("Namespace")
109
- next unless @classes[name].is_a?(Class) && @classes[name].include?(Lutaml::Model::Serialize)
110
-
111
- class_name = Utils.camel_case(name)
112
- "#{indent}#{class_name}.mappings[:xml].ensure_mappings_imported!(:#{@register_id}) if #{class_name}.mappings[:xml]&.respond_to?(:ensure_mappings_imported!)"
113
- end.join("\n")
114
- end
115
-
116
- def module_depth
117
- @modules.size
118
- end
119
-
120
- def module_path
121
- @modules.map(&:downcase).join("/")
41
+ @classes.map do |entry|
42
+ class_name = Utils.camel_case(entry.name)
43
+ "#{body_indent}#{class_name}.mappings[:xml].ensure_mappings_imported!(:#{@register_id}) " \
44
+ "if #{class_name}.respond_to?(:mappings) && #{class_name}.mappings[:xml]&.respond_to?(:ensure_mappings_imported!)"
45
+ end.join("\n")
122
46
  end
123
47
  end
124
48
  end