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,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module XmlCompiler
7
+ class SpecBuilder
8
+ # Builds Definitions::Model specs for XSD <complexType>, named
9
+ # <group>, and <attributeGroup> nodes, plus the SimpleContent
10
+ # sidecar.
11
+ #
12
+ # Reads from the parent SpecBuilder:
13
+ # - @parent.attribute_groups for ref resolution
14
+ # - @parent.group_types as the destination for anonymous
15
+ # named groups discovered while walking complexType bodies
16
+ # - @parent.namespace_class_name to stamp on built Models
17
+ # Delegates leaf-member building (attribute/element/sequence/
18
+ # choice) back to @parent.members for shared resolution rules.
19
+ class ComplexTypes
20
+ def initialize(parent)
21
+ @parent = parent
22
+ end
23
+
24
+ # <xs:complexType> -> Definitions::Model.
25
+ def build(complex_type)
26
+ model = Definitions::Model.new(
27
+ class_name: Utils.camel_case(complex_type.name),
28
+ xml_root: Definitions::XmlRoot.new(kind: :element, name: complex_type.name),
29
+ mixed: !!complex_type.mixed,
30
+ namespace_class_name: @parent.namespace_class_name,
31
+ )
32
+
33
+ ElementOrder.resolved(complex_type).each do |element|
34
+ add_child(model, element)
35
+ end
36
+ model
37
+ end
38
+
39
+ # <xs:group> at top level -> Definitions::Model with
40
+ # module_wrappable: false. Used by both the registry walker
41
+ # and by `add_group_to_model` when it discovers a named-but-
42
+ # not-yet-registered anonymous group.
43
+ def build_group(group)
44
+ inner_members = []
45
+ if (built = build_group_inner(group))
46
+ inner_members << built
47
+ end
48
+
49
+ base_name = group.name || Utils.last_of_split(group.ref)
50
+ Definitions::Model.new(
51
+ class_name: Utils.camel_case(base_name),
52
+ xml_root: Definitions::XmlRoot.new(kind: :type_name, name: base_name),
53
+ members: inner_members,
54
+ module_wrappable: false,
55
+ lazy_register: true,
56
+ )
57
+ end
58
+
59
+ # <xs:attributeGroup> -> Array of Definitions::Attribute. Top
60
+ # level groups are registered for ref resolution; inline ones
61
+ # are flattened directly into a Model's members.
62
+ def build_attribute_group(attribute_group)
63
+ ref = attribute_group.ref
64
+ if ref && !attribute_group.name
65
+ target = @parent.attribute_groups[Utils.last_of_split(ref)]
66
+ return Array(target)
67
+ end
68
+
69
+ collect_attributes(attribute_group)
70
+ end
71
+
72
+ # <xs:simpleContent> -> Definitions::SimpleContent sidecar.
73
+ def build_simple_content(simple_content)
74
+ additional = []
75
+ base_class = nil
76
+ if simple_content.extension
77
+ ext = simple_content.extension
78
+ base_class = ext.base
79
+ additional = collect_attributes(ext)
80
+ elsif simple_content.restriction
81
+ base_class = simple_content.restriction.base
82
+ end
83
+
84
+ Definitions::SimpleContent.new(
85
+ base_class: base_class,
86
+ additional_attributes: additional,
87
+ )
88
+ end
89
+
90
+ private
91
+
92
+ # Flattens the <attribute> / <attributeGroup> children of
93
+ # `container` into an array of Definitions::Attribute.
94
+ def collect_attributes(container)
95
+ members = []
96
+ ElementOrder.resolved(container).each do |item|
97
+ case item
98
+ when Lutaml::Xml::Schema::Xsd::Attribute
99
+ attr = @parent.members_builder.build_attribute(item)
100
+ members << attr if attr
101
+ when Lutaml::Xml::Schema::Xsd::AttributeGroup
102
+ members.concat(build_attribute_group(item))
103
+ end
104
+ end
105
+ members
106
+ end
107
+
108
+ def add_child(model, element)
109
+ case element
110
+ when Lutaml::Xml::Schema::Xsd::Attribute
111
+ attr = @parent.members_builder.build_attribute(element)
112
+ model.members << attr if attr
113
+ when Lutaml::Xml::Schema::Xsd::Sequence
114
+ model.members << @parent.members_builder.build_sequence(element)
115
+ when Lutaml::Xml::Schema::Xsd::Choice
116
+ model.members << @parent.members_builder.build_choice(element)
117
+ when Lutaml::Xml::Schema::Xsd::ComplexContent
118
+ apply_complex_content(element, model)
119
+ when Lutaml::Xml::Schema::Xsd::AttributeGroup
120
+ model.members.concat(build_attribute_group(element))
121
+ when Lutaml::Xml::Schema::Xsd::Group
122
+ add_group_to_model(element, model)
123
+ when Lutaml::Xml::Schema::Xsd::SimpleContent
124
+ model.simple_content = build_simple_content(element)
125
+ end
126
+ end
127
+
128
+ def apply_complex_content(content, model)
129
+ model.mixed = true if content.mixed
130
+
131
+ if (ext = content.extension)
132
+ model.parent_class = qualified_class(ext.base)
133
+ ElementOrder.resolved(ext).each { |c| add_child(model, c) }
134
+ elsif (res = content.restriction)
135
+ model.parent_class = qualified_class(res.base)
136
+ ElementOrder.resolved(res).each do |c|
137
+ # XSD: restrictions on complex content inherit
138
+ # sequence/choice/group from base.
139
+ next if c.is_a?(Lutaml::Xml::Schema::Xsd::Sequence) ||
140
+ c.is_a?(Lutaml::Xml::Schema::Xsd::Choice) ||
141
+ c.is_a?(Lutaml::Xml::Schema::Xsd::Group)
142
+
143
+ add_child(model, c)
144
+ end
145
+ end
146
+ end
147
+
148
+ def qualified_class(raw)
149
+ return "Lutaml::Model::Serializable" if raw.nil?
150
+
151
+ Utils.camel_case(Utils.last_of_split(raw))
152
+ end
153
+
154
+ def add_group_to_model(group, model)
155
+ # Anonymous group OR named-but-not-referenced group: unwrap
156
+ # the inner sequence/choice inline into the model. (Matches
157
+ # the old XmlCompiler::Group rendering, which only emitted
158
+ # import_model_attributes/mappings when `ref` was set.)
159
+ if group.ref.nil?
160
+ add_anonymous_group_contents(group, model)
161
+ if group.name && !@parent.group_types.key?(group.name)
162
+ @parent.group_types[group.name] = build_group(group)
163
+ end
164
+ return
165
+ end
166
+
167
+ model.members << Definitions::GroupImport.new(
168
+ name: Utils.snake_case(Utils.last_of_split(group.ref)),
169
+ )
170
+ end
171
+
172
+ def add_anonymous_group_contents(group, model)
173
+ built = build_group_inner(group)
174
+ model.members << built if built
175
+ end
176
+
177
+ def build_group_inner(group)
178
+ inner = group.sequence || group.choice
179
+ return nil unless inner
180
+
181
+ case inner
182
+ when Lutaml::Xml::Schema::Xsd::Sequence then @parent.members_builder.build_sequence(inner)
183
+ when Lutaml::Xml::Schema::Xsd::Choice then @parent.members_builder.build_choice(inner)
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module XmlCompiler
7
+ class SpecBuilder
8
+ # Builds the leaf member specs that live inside generated
9
+ # classes: Attribute / Element (both surface as
10
+ # Definitions::Attribute with different `kind:` values),
11
+ # Sequence, Choice, GroupImport, plus the TypeRef wrapper.
12
+ #
13
+ # Holds @parent (SpecBuilder) so it can:
14
+ # - look up @parent.attributes / @parent.elements for refs
15
+ # - write anonymous simple/complex types into @parent's hashes
16
+ # - delegate complex-type building back to @parent for nested
17
+ # anonymous types
18
+ class Members
19
+ def initialize(parent)
20
+ @parent = parent
21
+ end
22
+
23
+ # ----- Attributes ------------------------------------------------
24
+
25
+ def build_attribute(attr)
26
+ return resolve_attribute_ref(attr) if attr.ref && !attr.name
27
+
28
+ Definitions::Attribute.new(
29
+ name: Utils.snake_case(attr.name),
30
+ type: build_type_ref(resolve_attribute_type(attr)),
31
+ xml_name: attr.name,
32
+ kind: :attribute,
33
+ default: attr.default,
34
+ )
35
+ end
36
+
37
+ def build_top_level_attribute(item, kind:)
38
+ return item unless item.respond_to?(:name)
39
+
40
+ type_str = top_level_type_str(item)
41
+ Definitions::Attribute.new(
42
+ name: Utils.snake_case(item.name.to_s),
43
+ type: build_type_ref(type_str || "string"),
44
+ xml_name: item.name.to_s,
45
+ kind: kind,
46
+ )
47
+ end
48
+
49
+ # ----- Elements --------------------------------------------------
50
+
51
+ def build_element(element)
52
+ return resolve_element_ref(element) if element.ref && !element.name
53
+
54
+ Definitions::Attribute.new(
55
+ name: Utils.snake_case(element.name),
56
+ type: build_type_ref(resolve_element_type(element)),
57
+ xml_name: element.name,
58
+ kind: :element,
59
+ collection: collection_from_occurs(element.min_occurs, element.max_occurs),
60
+ default: element.default,
61
+ render_default: !element.default.nil?,
62
+ render_empty: element_required?(element.min_occurs),
63
+ )
64
+ end
65
+
66
+ # ----- Sequence / Choice -----------------------------------------
67
+
68
+ def build_sequence(sequence)
69
+ members = ElementOrder.resolved(sequence).filter_map do |item|
70
+ next if item.is_a?(Lutaml::Xml::Schema::Xsd::Any)
71
+
72
+ build_member(item)
73
+ end
74
+ Definitions::Sequence.new(members: members)
75
+ end
76
+
77
+ def build_choice(choice)
78
+ alternatives = ElementOrder.resolved(choice).filter_map { |item| build_member(item) }
79
+ Definitions::Choice.new(
80
+ alternatives: alternatives,
81
+ header: choice_header(choice),
82
+ )
83
+ end
84
+
85
+ # A `<xs:group ref="..."/>` appearing inside a sequence/choice
86
+ # becomes an inline import directive. Anonymous in-place groups
87
+ # (no name, no ref) are flattened by the complex-type walker
88
+ # before they reach this method.
89
+ def build_group_member(group)
90
+ return nil if group.ref.nil?
91
+
92
+ Definitions::GroupImport.new(
93
+ name: Utils.snake_case(Utils.last_of_split(group.ref)),
94
+ )
95
+ end
96
+
97
+ # ----- TypeRef ---------------------------------------------------
98
+
99
+ def build_type_ref(raw_type)
100
+ return Definitions::TypeRef.new(kind: :symbol, value: "string") if raw_type.nil?
101
+ return Definitions::TypeRef.new(kind: :w3c, value: raw_type) if w3c_type?(raw_type)
102
+
103
+ local = Utils.last_of_split(raw_type)
104
+ Definitions::TypeRef.new(kind: :symbol, value: Utils.snake_case(local))
105
+ end
106
+
107
+ private
108
+
109
+ def build_member(item)
110
+ case item
111
+ when Lutaml::Xml::Schema::Xsd::Sequence then build_sequence(item)
112
+ when Lutaml::Xml::Schema::Xsd::Element then build_element(item)
113
+ when Lutaml::Xml::Schema::Xsd::Choice then build_choice(item)
114
+ when Lutaml::Xml::Schema::Xsd::Group then build_group_member(item)
115
+ end
116
+ end
117
+
118
+ def resolve_attribute_ref(attr)
119
+ target = @parent.attributes[Utils.last_of_split(attr.ref)]
120
+ return nil unless target
121
+
122
+ Definitions::Attribute.new(
123
+ name: target.name,
124
+ type: target.type,
125
+ xml_name: target.xml_name,
126
+ kind: :attribute,
127
+ default: target.default,
128
+ )
129
+ end
130
+
131
+ def resolve_attribute_type(attr)
132
+ return attr.type if attr.type
133
+
134
+ register_anonymous_simple_type("ST_#{attr.name}", attr.simple_type)
135
+ end
136
+
137
+ def resolve_element_ref(element)
138
+ target = @parent.elements[Utils.last_of_split(element.ref)]
139
+ return nil unless target
140
+
141
+ Definitions::Attribute.new(
142
+ name: target.name,
143
+ type: target.type,
144
+ xml_name: target.xml_name,
145
+ kind: :element,
146
+ collection: collection_from_occurs(element.min_occurs, element.max_occurs),
147
+ default: target.default,
148
+ render_default: !target.default.nil?,
149
+ render_empty: element_required?(element.min_occurs),
150
+ )
151
+ end
152
+
153
+ def resolve_element_type(element)
154
+ return element.type if element.type
155
+ return register_anonymous_simple_type("ST_#{element.name}", element.simple_type) if element.simple_type
156
+ return register_anonymous_complex_type("CT_#{element.name}", element.complex_type) if element.complex_type
157
+
158
+ "string"
159
+ end
160
+
161
+ def top_level_type_str(item)
162
+ return item.type if item.respond_to?(:type) && item.type
163
+ return register_anonymous_simple_type("ST_#{item.name}", item.simple_type) if item.respond_to?(:simple_type) && item.simple_type
164
+
165
+ register_anonymous_complex_type("CT_#{item.name}", item.complex_type) if item.respond_to?(:complex_type) && item.complex_type
166
+ end
167
+
168
+ def register_anonymous_simple_type(anon_name, anon_node)
169
+ anon_node.name = anon_name
170
+ @parent.simple_types[anon_name] = @parent.build_simple_type(anon_node)
171
+ anon_name
172
+ end
173
+
174
+ def register_anonymous_complex_type(anon_name, anon_node)
175
+ anon_node.name = anon_name
176
+ @parent.complex_types[anon_name] = @parent.build_complex_type(anon_node)
177
+ anon_name
178
+ end
179
+
180
+ def collection_from_occurs(min_occurs, max_occurs)
181
+ return false if min_occurs.nil? && max_occurs.nil?
182
+
183
+ min = min_occurs.nil? ? 1 : min_occurs.to_i
184
+ max = case max_occurs
185
+ when "unbounded" then Float::INFINITY
186
+ when NilClass then 1
187
+ else max_occurs.to_i
188
+ end
189
+ return false if min == 1 && max == 1
190
+
191
+ (min..max)
192
+ end
193
+
194
+ def element_required?(min_occurs)
195
+ min_occurs.nil? || min_occurs.to_i >= 1
196
+ end
197
+
198
+ def choice_header(choice)
199
+ min = choice.min_occurs.nil? ? 1 : choice.min_occurs.to_i
200
+ max = case choice.max_occurs
201
+ when "unbounded" then "Float::INFINITY"
202
+ when NilClass then 1
203
+ else choice.max_occurs.to_i
204
+ end
205
+ "choice(min: #{min}, max: #{max})"
206
+ end
207
+
208
+ def w3c_type?(raw_type)
209
+ raw_type.to_s.start_with?("Lutaml::Xml::W3c::")
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module XmlCompiler
7
+ class SpecBuilder
8
+ # Builds Definitions::RestrictedType / Definitions::UnionType
9
+ # specs from XSD <xs:simpleType> AST nodes, including the
10
+ # facet container that lives on the restricted type.
11
+ #
12
+ # Stateless. XSD built-in metadata comes from
13
+ # XmlCompiler::SupportedDataTypes; the back-reference to the
14
+ # parent SpecBuilder is kept for symmetry with other sub-builders.
15
+ class SimpleTypes
16
+ def initialize(parent)
17
+ @parent = parent
18
+ end
19
+
20
+ def build(simple_type)
21
+ if (union = simple_type.union)
22
+ build_union_type(simple_type.name, union.member_types.split)
23
+ else
24
+ build_restricted_type(simple_type.name, simple_type.restriction)
25
+ end
26
+ end
27
+
28
+ def build_supported(name, info)
29
+ base = Utils.base_class_snake_case(info[:class_name])
30
+ validations = info[:validations] || {}
31
+ facet = Definitions::Facet.new(
32
+ min_inclusive: validations[:min_inclusive],
33
+ max_inclusive: validations[:max_inclusive],
34
+ pattern: validations[:pattern],
35
+ )
36
+ transform = validations[:transform] &&
37
+ Definitions::TransformFacet.new(expression: validations[:transform])
38
+
39
+ Definitions::RestrictedType.new(
40
+ class_name: Utils.camel_case(name),
41
+ parent_class: restricted_parent_class(base),
42
+ facets: facet,
43
+ transform_facet: transform,
44
+ required_files: supported_required_files(base),
45
+ keep_register_when_namespaced: true,
46
+ )
47
+ end
48
+
49
+ private
50
+
51
+ def build_union_type(name, member_type_names)
52
+ type_refs = member_type_names.map do |raw|
53
+ snake = Utils.snake_case(Utils.last_of_split(raw))
54
+ Definitions::TypeRef.new(kind: :symbol, value: snake)
55
+ end
56
+ Definitions::UnionType.new(
57
+ class_name: Utils.camel_case(name),
58
+ members: type_refs,
59
+ cast_strategy: :resolve_type,
60
+ required_files: union_required_files(member_type_names),
61
+ lazy_register: true,
62
+ keep_register_when_namespaced: true,
63
+ )
64
+ end
65
+
66
+ def union_required_files(member_type_names)
67
+ member_type_names.filter_map do |raw|
68
+ local = Utils.last_of_split(raw)
69
+ next if skippable?(local)
70
+
71
+ %(require_relative "#{Utils.snake_case(local)}")
72
+ end
73
+ end
74
+
75
+ def build_restricted_type(name, restriction)
76
+ restriction_base = restriction&.base
77
+ base_class = restriction_base&.split(":")&.last
78
+ facet = restriction ? build_facet(restriction) : Definitions::Facet.new
79
+
80
+ Definitions::RestrictedType.new(
81
+ class_name: Utils.camel_case(name),
82
+ parent_class: restricted_parent_class(base_class),
83
+ facets: facet,
84
+ transform_facet: nil,
85
+ required_files: restricted_required_files(base_class),
86
+ keep_register_when_namespaced: true,
87
+ )
88
+ end
89
+
90
+ def restricted_parent_class(base_class)
91
+ type_info = SupportedDataTypes[base_class&.to_sym]
92
+ return type_info[:class_name] if type_info&.dig(:skippable)
93
+ return Utils.camel_case(base_class.to_s) if !type_info&.dig(:skippable) && Utils.present?(base_class)
94
+
95
+ "Lutaml::Model::Type::Value"
96
+ end
97
+
98
+ def restricted_required_files(base_class)
99
+ return [] if Utils.blank?(base_class)
100
+
101
+ return [%(require "bigdecimal")] if base_class == "decimal"
102
+ return [] if SupportedDataTypes.skippable?(base_class)
103
+
104
+ [%(require_relative "#{Utils.snake_case(base_class)}")]
105
+ end
106
+
107
+ def supported_required_files(base_class)
108
+ return [] if Utils.blank?(base_class) || SupportedDataTypes.skippable?(base_class)
109
+
110
+ [%(require_relative "#{Utils.snake_case(base_class)}")]
111
+ end
112
+
113
+ def skippable?(local)
114
+ SupportedDataTypes.skippable?(local)
115
+ end
116
+
117
+ # ----- Facets ---------------------------------------------------
118
+
119
+ def build_facet(restriction)
120
+ Definitions::Facet.new(
121
+ max_length: pick_minmax(restriction.max_length, :min),
122
+ min_length: pick_minmax(restriction.min_length, :max),
123
+ min_inclusive: pick_minmax(restriction.min_inclusive, :max),
124
+ max_inclusive: pick_minmax(restriction.max_inclusive, :min),
125
+ max_exclusive: pick_minmax(restriction.max_exclusive, :max),
126
+ min_exclusive: pick_minmax(restriction.min_exclusive, :min),
127
+ length: restriction.length&.any? ? restriction_length(restriction.length) : nil,
128
+ pattern: build_pattern(restriction.pattern),
129
+ enumerations: restriction.enumeration&.any? ? restriction.enumeration.map(&:value) : nil,
130
+ )
131
+ end
132
+
133
+ def pick_minmax(field_value, method)
134
+ return nil unless field_value&.any?
135
+
136
+ field_value.map(&:value).public_send(method).to_s
137
+ end
138
+
139
+ def restriction_length(lengths)
140
+ lengths.map { |l| { value: l.value, fixed: l.fixed } }
141
+ end
142
+
143
+ def build_pattern(patterns)
144
+ return nil if Utils.blank?(patterns)
145
+
146
+ patterns.map { |p| "(#{p.value})" }.join("|")
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end