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,392 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module RngCompiler
7
+ # Walks an RNG <element> / <define> subtree and produces
8
+ # Definitions::Model / Definitions::RestrictedType / Definitions::UnionType
9
+ # objects, registered in the shared `classes` hash.
10
+ #
11
+ # Supported constructs:
12
+ # element, attribute, ref, text, data, value, empty,
13
+ # optional, zeroOrMore, oneOrMore, group, choice, mixed, list,
14
+ # interleave
15
+ #
16
+ # Shape classification (define-wraps-data, define-wraps-enum-choice,
17
+ # union) is delegated to DefineClassifier. Value-shape resolution
18
+ # for individual children is delegated to ValueTypeResolver.
19
+ class ElementVisitor
20
+ DISPATCHABLE_KINDS = %i[
21
+ element attribute ref group optional oneOrMore zeroOrMore
22
+ choice list interleave empty
23
+ ].freeze
24
+
25
+ REPETITION_CTX = {
26
+ zeroOrMore: { collection: (0..Float::INFINITY), initialize_empty: true },
27
+ oneOrMore: { collection: (1..Float::INFINITY), initialize_empty: true },
28
+ list: { collection: (0..Float::INFINITY), initialize_empty: true },
29
+ }.freeze
30
+
31
+ HANDLERS = {
32
+ element: :handle_element,
33
+ attribute: :handle_attribute,
34
+ ref: :handle_ref,
35
+ group: :handle_group,
36
+ optional: :handle_optional,
37
+ oneOrMore: :handle_repeating,
38
+ zeroOrMore: :handle_repeating,
39
+ list: :handle_repeating,
40
+ choice: :handle_choice,
41
+ interleave: :handle_interleave,
42
+ empty: :handle_empty,
43
+ }.freeze
44
+
45
+ def initialize(defines, classes, default_namespace_class: nil,
46
+ register_namespace: nil)
47
+ @defines = defines
48
+ @classes = classes
49
+ @default_namespace_class = default_namespace_class
50
+ @register_namespace = register_namespace
51
+ @value_type_resolver = ValueTypeResolver.new(
52
+ defines, classes,
53
+ compile_define: method(:compile_define),
54
+ register_class: method(:register_class!)
55
+ )
56
+ end
57
+
58
+ # Compile a top-level <element>. Always becomes a rooted model.
59
+ def compile_element(element)
60
+ class_name = Utils.camel_case(element.attr_name)
61
+ model = @classes[class_name] ||= Definitions::Model.new(
62
+ class_name: class_name,
63
+ xml_root: Definitions::XmlRoot.new(kind: :element, name: element.attr_name),
64
+ documentation: documentation_text(element),
65
+ namespace_class_name: namespace_class_for(element),
66
+ )
67
+ visit_content(element, model)
68
+ model
69
+ end
70
+
71
+ # Compile a <define> into a class.
72
+ # - Define body matches a simple-type shape -> RestrictedType/UnionType.
73
+ # - Wraps exactly one element -> rooted Definitions::Model.
74
+ # - Multiple or zero wrapped elements -> fragment Definitions::Model.
75
+ def compile_define(define)
76
+ class_name = Utils.camel_case(define.name)
77
+ return @classes[class_name] if @classes.key?(class_name)
78
+
79
+ if (simple = DefineClassifier.build(define, class_name))
80
+ register_class!(simple)
81
+ return simple
82
+ end
83
+
84
+ build_complex_define(define, class_name)
85
+ end
86
+
87
+ private
88
+
89
+ def build_complex_define(define, class_name)
90
+ wrapping = define.element.size == 1 ? define.element.first : nil
91
+
92
+ xml_root = if wrapping
93
+ Definitions::XmlRoot.new(kind: :element, name: wrapping.attr_name)
94
+ else
95
+ Definitions::XmlRoot.new(kind: :fragment)
96
+ end
97
+
98
+ model = Definitions::Model.new(
99
+ class_name: class_name,
100
+ xml_root: xml_root,
101
+ documentation: documentation_text(define) || documentation_text(wrapping),
102
+ namespace_class_name: namespace_class_for(wrapping || define),
103
+ )
104
+ register_class!(model)
105
+ visit_content(wrapping || define, model)
106
+ model
107
+ end
108
+
109
+ def register_class!(klass)
110
+ @classes[klass.class_name] = klass
111
+ end
112
+
113
+ # A node's foreign XML namespace URI, or nil. An empty `ns` (RNC's
114
+ # `ns=""` no-namespace override) and the built-in "xml" prefix are
115
+ # both treated as not foreign, so `namespace_class_for` hands the
116
+ # node the grammar default namespace. lutaml expresses namespaces
117
+ # only at class level, so a per-node `ns=""` override cannot be
118
+ # represented distinctly from the default and is not separately
119
+ # honored -- excluding "" here just avoids registering a bogus
120
+ # empty-URI namespace that would clobber the grammar default.
121
+ def foreign_namespace_uri(node)
122
+ uri = node.respond_to?(:ns) ? node.ns : nil
123
+ uri if uri.is_a?(String) && !uri.empty? && uri != "xml"
124
+ end
125
+
126
+ # An element's own ns (from `element ex:name`) becomes its model's
127
+ # class-level namespace; otherwise it inherits the grammar default.
128
+ def namespace_class_for(node)
129
+ uri = foreign_namespace_uri(node)
130
+ uri ? @register_namespace.call(uri) : @default_namespace_class
131
+ end
132
+
133
+ def documentation_text(node)
134
+ return nil unless node.respond_to?(:documentation)
135
+
136
+ docs = Array(node.documentation).map(&:to_s).reject(&:empty?)
137
+ docs.empty? ? nil : docs.join("\n")
138
+ end
139
+
140
+ # Generic content walker.
141
+ def visit_content(node, model, ctx = default_ctx)
142
+ ordered_children(node).each do |kind, child|
143
+ dispatch(kind, child, model, ctx)
144
+ end
145
+
146
+ model.mixed = true if mixed?(node)
147
+ model.text_content = true if text?(node)
148
+ end
149
+
150
+ def default_ctx
151
+ { collection: nil, initialize_empty: false }
152
+ end
153
+
154
+ def ordered_children(node)
155
+ element_entries = element_order_entries(node)
156
+ return fallback_each_child_kind(node) if element_entries.empty?
157
+
158
+ arrays = {}
159
+ indices = ::Hash.new(0)
160
+ element_entries.each_with_object([]) do |entry, pairs|
161
+ kind = entry.name.to_sym
162
+ next unless DISPATCHABLE_KINDS.include?(kind) && node.respond_to?(kind)
163
+
164
+ children = arrays[kind] ||= Array(node.public_send(kind))
165
+ child = children[indices[kind]]
166
+ indices[kind] += 1
167
+ pairs << [kind, child] if child
168
+ end
169
+ end
170
+
171
+ def element_order_entries(node)
172
+ return [] unless node.respond_to?(:element_order)
173
+
174
+ Array(node.element_order).select do |e|
175
+ e.respond_to?(:node_type) && e.node_type == :element
176
+ end
177
+ end
178
+
179
+ def fallback_each_child_kind(node)
180
+ DISPATCHABLE_KINDS.each_with_object([]) do |attr_name, pairs|
181
+ next unless node.respond_to?(attr_name)
182
+
183
+ Array(node.public_send(attr_name)).each do |child|
184
+ pairs << [attr_name, child] unless child.nil?
185
+ end
186
+ end
187
+ end
188
+
189
+ def dispatch(kind, child, parent, ctx)
190
+ handler = HANDLERS[kind] or return
191
+
192
+ send(handler, kind, child, parent, ctx)
193
+ end
194
+
195
+ # --- handlers ---------------------------------------------------
196
+
197
+ def handle_element(_kind, child, parent, ctx)
198
+ doc = documentation_text(child)
199
+ value_type = @value_type_resolver.resolve(child)
200
+ type_ref = type_ref_for_element(child, value_type)
201
+ push_attribute(parent, build_attribute(child, type_ref, :element, ctx, doc))
202
+ end
203
+
204
+ def type_ref_for_element(child, value_type)
205
+ return Definitions::TypeRef.new(kind: :symbol, value: value_type.to_s) if value_type
206
+
207
+ compiled = compile_element(child)
208
+ Definitions::TypeRef.new(kind: :class_ref, value: compiled.class_name)
209
+ end
210
+
211
+ def handle_attribute(_kind, child, parent, ctx)
212
+ doc = documentation_text(child)
213
+ symbol = attribute_type_symbol(child)
214
+ type_ref = Definitions::TypeRef.new(kind: :symbol, value: symbol.to_s)
215
+ fixed = fixed_value_default(child)
216
+ push_attribute(parent, build_attribute(child, type_ref, :attribute, ctx, doc, default: fixed))
217
+ end
218
+
219
+ # The attribute's value type, as a Symbol. An attribute in a foreign
220
+ # namespace (a prefix other than the built-in "xml") must carry that
221
+ # namespace, which lutaml-model expresses only through a Type::Value
222
+ # subclass — so wrap the value in a generated namespaced type.
223
+ def attribute_type_symbol(child)
224
+ base = @value_type_resolver.resolve(child) || :string
225
+ uri = foreign_namespace_uri(child)
226
+ return base unless uri
227
+
228
+ namespaced_attribute_type(child, base, @register_namespace.call(uri))
229
+ end
230
+
231
+ # Give the attribute's value a namespace via a dedicated generated
232
+ # type — never by mutating the resolved type, which may be a named
233
+ # define or an inline type shared with unqualified members. When the
234
+ # resolved base is itself a generated RestrictedType, subclass it so
235
+ # its constraints are inherited; otherwise subclass the built-in base.
236
+ def namespaced_attribute_type(child, base, ns_class)
237
+ resolved = generated_type_for(base)
238
+ parent = resolved ? resolved.class_name : RngHelpers.parent_class_for(base)
239
+
240
+ type = Definitions::RestrictedType.new(
241
+ class_name: RngHelpers.unique_class_name(
242
+ @classes, "#{Utils.camel_case(child.attr_name)}Type"
243
+ ),
244
+ parent_class: parent,
245
+ facets: Definitions::Facet.new,
246
+ namespace_class_name: ns_class,
247
+ )
248
+ register_class!(type)
249
+ RngHelpers.type_symbol(type.class_name)
250
+ end
251
+
252
+ # The generated RestrictedType a resolved type symbol refers to, found
253
+ # by symbol identity (so acronym class names round-trip) — or nil for a
254
+ # built-in symbol. A built-in symbol always means the built-in type,
255
+ # even if a define shares its name (a custom `dateTime` must not shadow
256
+ # the primitive `:date_time`).
257
+ def generated_type_for(base)
258
+ return nil if Lutaml::Model::Type::TYPE_CODES.key?(base)
259
+
260
+ @classes.values.find do |klass|
261
+ klass.is_a?(Definitions::RestrictedType) &&
262
+ RngHelpers.type_symbol(klass.class_name) == base
263
+ end
264
+ end
265
+
266
+ def build_attribute(child, type_ref, kind, ctx, doc, default: nil)
267
+ xml_name = xml_name_for(child)
268
+ Definitions::Attribute.new(
269
+ name: Utils.snake_case(xml_name.tr(":", "_")),
270
+ type: type_ref,
271
+ xml_name: xml_name,
272
+ kind: kind,
273
+ collection: ctx[:collection] || false,
274
+ initialize_empty: ctx[:initialize_empty] || false,
275
+ documentation: doc,
276
+ default: default,
277
+ )
278
+ end
279
+
280
+ def xml_name_for(child)
281
+ name = child.attr_name
282
+ ns = child.respond_to?(:ns) ? child.ns : nil
283
+ return name unless ns == "xml"
284
+
285
+ "#{ns}:#{name}"
286
+ end
287
+
288
+ # <attribute name="x"><value>X</value></attribute> = fixed value.
289
+ # Match XSD compiler's behavior — emit `default: -> { "X" }` so
290
+ # the value round-trips even when omitted.
291
+ def fixed_value_default(container)
292
+ return nil unless container.respond_to?(:value)
293
+
294
+ values = Array(container.value)
295
+ return nil if values.size != 1
296
+ return nil if RngHelpers.structural_content?(container)
297
+
298
+ values.first.value.to_s
299
+ end
300
+
301
+ def handle_ref(_kind, ref, parent, ctx)
302
+ target_define = @defines[ref.name]
303
+ raise Lutaml::Model::Error, "ref to unknown define: #{ref.name}" unless target_define
304
+
305
+ target_class = compile_define(target_define)
306
+
307
+ if RngHelpers.simple_type?(target_class)
308
+ type_ref = Definitions::TypeRef.new(kind: :symbol, value: RngHelpers.type_symbol(target_class.class_name).to_s)
309
+ push_attribute(parent, build_ref_attribute(ref, type_ref, ref.name, ctx))
310
+ elsif RngHelpers.fragment_model?(target_class) && ctx[:collection].nil?
311
+ push_import(parent, target_class.class_name)
312
+ else
313
+ type_ref = Definitions::TypeRef.new(kind: :class_ref, value: target_class.class_name)
314
+ xml_name = target_class.xml_root.name || ref.name
315
+ push_attribute(parent, build_ref_attribute(ref, type_ref, xml_name, ctx))
316
+ end
317
+ end
318
+
319
+ def build_ref_attribute(ref, type_ref, xml_name, ctx)
320
+ Definitions::Attribute.new(
321
+ name: Utils.snake_case(ref.name),
322
+ type: type_ref,
323
+ xml_name: xml_name,
324
+ kind: :element,
325
+ collection: ctx[:collection] || false,
326
+ initialize_empty: ctx[:initialize_empty] || false,
327
+ )
328
+ end
329
+
330
+ # <group> = ordered sequence of items.
331
+ def handle_group(_kind, group, parent, ctx)
332
+ collector = MemberCollector.new
333
+ visit_content(group, collector, ctx)
334
+ return if collector.members.empty?
335
+
336
+ parent.members << Definitions::Sequence.new(members: collector.members)
337
+ collector.imports.each { |name| push_import(parent, name) }
338
+ end
339
+
340
+ def handle_optional(_kind, opt, parent, ctx)
341
+ visit_content(opt, parent, ctx)
342
+ end
343
+
344
+ def handle_interleave(_kind, interleave, parent, ctx)
345
+ visit_content(interleave, parent, ctx)
346
+ end
347
+
348
+ def handle_repeating(kind, node, parent, ctx)
349
+ visit_content(node, parent, ctx.merge(REPETITION_CTX.fetch(kind)))
350
+ end
351
+
352
+ def handle_choice(_kind, choice, parent, ctx)
353
+ return if RngHelpers.pure_value_choice?(choice)
354
+
355
+ collector = MemberCollector.new
356
+ visit_content(choice, collector, ctx)
357
+ return if collector.members.empty?
358
+
359
+ parent.members << Definitions::Choice.new(
360
+ alternatives: collector.members,
361
+ header: "choice",
362
+ )
363
+ collector.imports.each { |name| push_import(parent, name) }
364
+ end
365
+
366
+ def handle_empty(_kind, _child, _parent, _ctx)
367
+ # <empty/> contributes no content — intentionally no-op.
368
+ end
369
+
370
+ def push_attribute(parent, attr)
371
+ parent.members.reject! do |m|
372
+ m.is_a?(Definitions::Attribute) && m.name == attr.name
373
+ end
374
+ parent.members << attr
375
+ end
376
+
377
+ def push_import(parent, name)
378
+ parent.imports << name unless parent.imports.include?(name)
379
+ end
380
+
381
+ def mixed?(node)
382
+ node.respond_to?(:mixed) && Utils.present?(node.mixed)
383
+ end
384
+
385
+ def text?(node)
386
+ node.respond_to?(:text) && Utils.present?(node.text)
387
+ end
388
+ end
389
+ end
390
+ end
391
+ end
392
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module RngCompiler
7
+ # Lightweight accumulator used by ElementVisitor when collecting the
8
+ # children of a transient construct (`<group>`, `<choice>`) before
9
+ # promoting them into a Definitions::Sequence or Definitions::Choice on
10
+ # the real Definitions::Model.
11
+ #
12
+ # Mirrors the surface ElementVisitor expects on a model
13
+ # (members, imports, mixed, text_content) but with no XML root,
14
+ # documentation, or namespace — none of which apply to a scratch
15
+ # container.
16
+ class MemberCollector
17
+ attr_reader :members, :imports
18
+ attr_accessor :mixed, :text_content
19
+
20
+ def initialize
21
+ @members = []
22
+ @imports = []
23
+ @mixed = false
24
+ @text_content = false
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module RngCompiler
7
+ # RNG-specific helpers used by ElementVisitor, DefineClassifier, and
8
+ # ValueTypeResolver. Centralising these here avoids the three-way
9
+ # semantic drift the first refactor introduced (e.g. inconsistent
10
+ # `pure_value_choice?` implementations).
11
+ module RngHelpers
12
+ module_function
13
+
14
+ # True if the compiled define renders as a value type (RestrictedType
15
+ # or UnionType) rather than its own Serializable class. Single
16
+ # source of truth — used by ElementVisitor and ValueTypeResolver.
17
+ def simple_type?(klass)
18
+ klass.is_a?(Definitions::RestrictedType) ||
19
+ klass.is_a?(Definitions::UnionType)
20
+ end
21
+
22
+ # True iff `klass` is a fragment model (no rooted xml element).
23
+ def fragment_model?(klass)
24
+ klass.is_a?(Definitions::Model) && klass.xml_root.kind == :fragment
25
+ end
26
+
27
+ # Returns the only element of a collection, or nil if the
28
+ # collection is empty or has more than one element. Treats nil as
29
+ # an empty collection.
30
+ def single(collection)
31
+ arr = Array(collection)
32
+ arr.size == 1 ? arr.first : nil
33
+ end
34
+
35
+ # An RNG node has structural content when any of its element /
36
+ # ref / attribute / choice / group / optional / repetition
37
+ # children are populated.
38
+ def structural_content?(node)
39
+ node && present_children?(node, STRUCTURAL_CHILD_NAMES)
40
+ end
41
+
42
+ # True iff any of `attrs` is a populated array-child of `node`.
43
+ def present_children?(node, attrs)
44
+ attrs.any? do |attr|
45
+ node.respond_to?(attr) && Array(node.public_send(attr)).any?
46
+ end
47
+ end
48
+
49
+ # True when `node` has branching structure that needs its own
50
+ # class. Pure value-choices and `<ref>`s to simple types are NOT
51
+ # branching — they're typed leaves.
52
+ def branching_structural?(node)
53
+ return false unless node
54
+
55
+ non_leaf = STRUCTURAL_CHILD_NAMES - %i[choice ref]
56
+ return true if present_children?(node, non_leaf)
57
+
58
+ return false unless node.respond_to?(:choice)
59
+
60
+ Array(node.choice).any? { |c| !pure_value_choice?(c) }
61
+ end
62
+
63
+ # True iff `choice` is a <choice> consisting purely of <value>
64
+ # alternatives (no <element>, <ref>, <data>, or <text>). A <text>
65
+ # alternative subsumes the literals, so `text | "x"` is a plain
66
+ # string, not an enum restricted to "x".
67
+ def pure_value_choice?(choice)
68
+ return false unless choice
69
+ return false unless choice.respond_to?(:value) && Array(choice.value).any?
70
+ return false if choice.respond_to?(:text) && Utils.present?(choice.text)
71
+
72
+ !present_children?(choice, %i[element ref data])
73
+ end
74
+
75
+ # True iff `choice` is a union: at least 2 <data> alternatives
76
+ # and no other content.
77
+ def pure_union_choice?(choice)
78
+ return false unless choice
79
+ return false unless choice.respond_to?(:data) && Array(choice.data).size >= 2
80
+
81
+ !present_children?(choice, %i[element ref value])
82
+ end
83
+
84
+ # Build a Definitions::Facet from an RNG <data>'s <param> children.
85
+ def facet_from_data(data)
86
+ facet = Definitions::Facet.new
87
+ Array(data.param).each do |param|
88
+ apply_param(facet, param.name, param.value.to_s)
89
+ end
90
+ facet
91
+ end
92
+
93
+ # Build a Definitions::Facet from an array of <value> elements.
94
+ def facet_from_values(values)
95
+ Definitions::Facet.new(
96
+ enumerations: Array(values).map(&:value),
97
+ )
98
+ end
99
+
100
+ # Snake_case symbol used as the registry key for a generated
101
+ # simple-type/union-type/namespace class.
102
+ def type_symbol(class_name)
103
+ Utils.snake_case(class_name).to_sym
104
+ end
105
+
106
+ # A class name derived from base_name that does not collide with an
107
+ # existing key in `classes` (appends 2, 3, ... on collision).
108
+ def unique_class_name(classes, base_name)
109
+ return base_name unless classes.key?(base_name)
110
+
111
+ counter = 2
112
+ counter += 1 while classes.key?("#{base_name}#{counter}")
113
+ "#{base_name}#{counter}"
114
+ end
115
+
116
+ # Maps an RNG/XSD base symbol to its Ruby parent class string,
117
+ # defaulting to the string type when unknown.
118
+ def parent_class_for(base_symbol)
119
+ Lutaml::Model::Type::TYPE_CODES.fetch(
120
+ base_symbol, Lutaml::Model::Type::TYPE_CODES[:string]
121
+ ).to_s
122
+ end
123
+
124
+ def apply_param(facet, name, value)
125
+ case name
126
+ when "minInclusive" then facet.min_inclusive = numeric_or_string(value)
127
+ when "maxInclusive" then facet.max_inclusive = numeric_or_string(value)
128
+ when "minExclusive" then facet.min_exclusive = numeric_or_string(value)
129
+ when "maxExclusive" then facet.max_exclusive = numeric_or_string(value)
130
+ when "minLength" then facet.min_length = value.to_i
131
+ when "maxLength" then facet.max_length = value.to_i
132
+ when "length" then facet.length = value.to_i
133
+ when "pattern" then facet.pattern = value
134
+ end
135
+ end
136
+
137
+ def numeric_or_string(value)
138
+ return value.to_i if /\A-?\d+\z/.match?(value)
139
+ return value.to_f if /\A-?\d+\.\d+\z/.match?(value)
140
+
141
+ value
142
+ end
143
+
144
+ STRUCTURAL_CHILD_NAMES = %i[
145
+ element ref attribute choice group
146
+ optional zeroOrMore oneOrMore
147
+ ].freeze
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module RngCompiler
7
+ # Resolves the value-shape of an element or attribute container into a
8
+ # Symbol (built-in / registered type) or nil if the container has
9
+ # structural content needing its own class.
10
+ #
11
+ # Patterns handled:
12
+ # <element><text/></element> -> :string
13
+ # <element><data type="X"/></element> -> mapped primitive
14
+ # <element><data ...><param ...>/></data></element> -> anonymous RestrictedType (returns its registry symbol)
15
+ # <element><choice><value>...</value></choice></element> -> anonymous RestrictedType (enum) (returns its registry symbol)
16
+ # <element><ref name="StFoo"/></element> -> :st_foo (if simple)
17
+ #
18
+ # When an anonymous RestrictedType is needed, the resolver *builds* it
19
+ # (pure) and asks the supplied `register_class` callback to register
20
+ # it. Build and register are separate methods so unit testing can
21
+ # exercise the pure logic without side effects.
22
+ class ValueTypeResolver
23
+ def initialize(defines, classes, compile_define:, register_class:)
24
+ @defines = defines
25
+ @classes = classes
26
+ @compile_define = compile_define
27
+ @register_class = register_class
28
+ end
29
+
30
+ # Returns the resolved type symbol, or nil if `container` needs
31
+ # its own class. May register an anonymous RestrictedType as a
32
+ # side effect (delegated to the register_class callback).
33
+ def resolve(container)
34
+ return nil if RngHelpers.branching_structural?(container)
35
+
36
+ type_from_inline_simple(container) || primitive_or_ref(container)
37
+ end
38
+
39
+ private
40
+
41
+ def type_from_inline_simple(container)
42
+ simple = build_anonymous_simple_type(container)
43
+ return nil unless simple
44
+
45
+ @register_class.call(simple)
46
+ RngHelpers.type_symbol(simple.class_name)
47
+ end
48
+
49
+ def build_anonymous_simple_type(container)
50
+ anonymous_from_data(container) || anonymous_from_enum_choice(container)
51
+ end
52
+
53
+ def anonymous_from_data(container)
54
+ data = RngHelpers.single(container.data)
55
+ return nil unless data && Array(data.param).any?
56
+
57
+ base = RngCompiler::DATA_TYPE_MAP.fetch(
58
+ data.type, RngCompiler::DEFAULT_DATA_TYPE
59
+ )
60
+ anonymous_restricted_type(container, base, RngHelpers.facet_from_data(data))
61
+ end
62
+
63
+ def anonymous_from_enum_choice(container)
64
+ choice = RngHelpers.single(container.choice)
65
+ return nil unless choice && RngHelpers.pure_value_choice?(choice)
66
+
67
+ anonymous_restricted_type(container, :string, RngHelpers.facet_from_values(choice.value))
68
+ end
69
+
70
+ def anonymous_restricted_type(container, base, facet)
71
+ Definitions::RestrictedType.new(
72
+ class_name: RngHelpers.unique_class_name(
73
+ @classes, "#{Utils.camel_case(container.attr_name.to_s)}Type"
74
+ ),
75
+ parent_class: RngHelpers.parent_class_for(base),
76
+ facets: facet,
77
+ )
78
+ end
79
+
80
+ def primitive_or_ref(container)
81
+ primitive = detect_primitive_type(container)
82
+ return primitive if primitive
83
+
84
+ ref_to_simple_type_symbol(container)
85
+ end
86
+
87
+ def ref_to_simple_type_symbol(container)
88
+ ref = RngHelpers.single(container.ref)
89
+ return nil unless ref
90
+
91
+ target = @defines[ref.name]
92
+ return nil unless target
93
+
94
+ target_class = @compile_define.call(target)
95
+ return RngHelpers.type_symbol(target_class.class_name) if RngHelpers.simple_type?(target_class)
96
+
97
+ nil
98
+ end
99
+
100
+ def detect_primitive_type(child)
101
+ return nil if RngHelpers.structural_content?(child)
102
+
103
+ primitive_from_data(child) || primitive_from_text(child) || primitive_from_value(child)
104
+ end
105
+
106
+ def primitive_from_data(child)
107
+ return nil unless child.data
108
+
109
+ RngCompiler::DATA_TYPE_MAP.fetch(child.data.type, RngCompiler::DEFAULT_DATA_TYPE)
110
+ end
111
+
112
+ def primitive_from_text(child)
113
+ :string if Utils.present?(child.text)
114
+ end
115
+
116
+ def primitive_from_value(child)
117
+ # <element><choice><value>a</value>...</choice></element> falls
118
+ # through to this path as :string with no constraint emitted.
119
+ :string if Array(child.value).any?
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end