lutaml-model 0.8.17 → 0.8.19

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 (195) 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/builder-dsl.adoc +127 -0
  14. data/docs/_guides/schema-import.adoc +42 -0
  15. data/docs/_guides/xml-mapping.adoc +21 -0
  16. data/docs/_pages/validation.adoc +65 -0
  17. data/lib/compat/opal/generate_boot.rb +123 -0
  18. data/lib/compat/opal/js_bundle_entry.rb +42 -0
  19. data/lib/compat/opal/lutaml_model_boot.rb +497 -0
  20. data/lib/compat/opal/moxml_boot.rb +68 -0
  21. data/lib/compat/opal/yaml_compat.rb +32 -0
  22. data/lib/lutaml/json.rb +1 -1
  23. data/lib/lutaml/key_value/transform.rb +3 -8
  24. data/lib/lutaml/key_value/transformation/value_serializer.rb +14 -1
  25. data/lib/lutaml/key_value/transformation.rb +17 -5
  26. data/lib/lutaml/model/adapter_resolver.rb +2 -2
  27. data/lib/lutaml/model/attribute.rb +88 -11
  28. data/lib/lutaml/model/cached_type_resolver.rb +1 -1
  29. data/lib/lutaml/model/error/union_schema_unsupported_error.rb +11 -0
  30. data/lib/lutaml/model/global_context.rb +11 -8
  31. data/lib/lutaml/model/instrumentation.rb +2 -2
  32. data/lib/lutaml/model/mapping/mapping_rule.rb +0 -6
  33. data/lib/lutaml/model/register.rb +2 -2
  34. data/lib/lutaml/model/runtime_compatibility.rb +50 -0
  35. data/lib/lutaml/model/schema/class_loader.rb +58 -0
  36. data/lib/lutaml/model/schema/compiled_output.rb +83 -0
  37. data/lib/lutaml/model/schema/definitions/attribute.rb +32 -0
  38. data/lib/lutaml/model/schema/definitions/choice.rb +20 -0
  39. data/lib/lutaml/model/schema/definitions/facet.rb +40 -0
  40. data/lib/lutaml/model/schema/definitions/group_import.rb +22 -0
  41. data/lib/lutaml/model/schema/definitions/member_walk.rb +33 -0
  42. data/lib/lutaml/model/schema/definitions/model.rb +40 -0
  43. data/lib/lutaml/model/schema/definitions/namespace.rb +23 -0
  44. data/lib/lutaml/model/schema/definitions/restricted_type.rb +30 -0
  45. data/lib/lutaml/model/schema/definitions/sequence.rb +19 -0
  46. data/lib/lutaml/model/schema/definitions/simple_content.rb +22 -0
  47. data/lib/lutaml/model/schema/definitions/transform_facet.rb +21 -0
  48. data/lib/lutaml/model/schema/definitions/type_ref.rb +20 -0
  49. data/lib/lutaml/model/schema/definitions/union_type.rb +28 -0
  50. data/lib/lutaml/model/schema/definitions/xml_root.rb +20 -0
  51. data/lib/lutaml/model/schema/definitions.rb +24 -0
  52. data/lib/lutaml/model/schema/file_writer.rb +70 -0
  53. data/lib/lutaml/model/schema/generator/definitions_collection.rb +16 -2
  54. data/lib/lutaml/model/schema/generator/property.rb +21 -4
  55. data/lib/lutaml/model/schema/module_nesting.rb +31 -0
  56. data/lib/lutaml/model/schema/namespace_naming.rb +46 -0
  57. data/lib/lutaml/model/schema/registry_generator.rb +115 -0
  58. data/lib/lutaml/model/schema/renderers/base.rb +40 -0
  59. data/lib/lutaml/model/schema/renderers/mappings.rb +72 -0
  60. data/lib/lutaml/model/schema/renderers/member_decls.rb +115 -0
  61. data/lib/lutaml/model/schema/renderers/model.rb +121 -0
  62. data/lib/lutaml/model/schema/renderers/namespace.rb +30 -0
  63. data/lib/lutaml/model/schema/renderers/registration.rb +64 -0
  64. data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +93 -0
  65. data/lib/lutaml/model/schema/renderers/restricted_type.rb +88 -0
  66. data/lib/lutaml/model/schema/renderers/union.rb +83 -0
  67. data/lib/lutaml/model/schema/renderers.rb +19 -0
  68. data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +47 -0
  69. data/lib/lutaml/model/schema/rnc_compiler.rb +62 -0
  70. data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +91 -0
  71. data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +392 -0
  72. data/lib/lutaml/model/schema/rng_compiler/member_collector.rb +30 -0
  73. data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +152 -0
  74. data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +125 -0
  75. data/lib/lutaml/model/schema/rng_compiler.rb +220 -0
  76. data/lib/lutaml/model/schema/templates.rb +162 -0
  77. data/lib/lutaml/model/schema/xml_compiler/element_order.rb +41 -0
  78. data/lib/lutaml/model/schema/xml_compiler/registry_generator.rb +31 -107
  79. data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +191 -0
  80. data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +216 -0
  81. data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +153 -0
  82. data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +167 -0
  83. data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +87 -0
  84. data/lib/lutaml/model/schema/xml_compiler.rb +38 -517
  85. data/lib/lutaml/model/schema.rb +19 -0
  86. data/lib/lutaml/model/serialize/attribute_definition.rb +23 -11
  87. data/lib/lutaml/model/serialize/builder.rb +70 -2
  88. data/lib/lutaml/model/serialize/initialization.rb +3 -2
  89. data/lib/lutaml/model/serialize/value_mapping.rb +9 -57
  90. data/lib/lutaml/model/serialize.rb +6 -9
  91. data/lib/lutaml/model/services/rule_value_extractor.rb +2 -3
  92. data/lib/lutaml/model/services.rb +0 -2
  93. data/lib/lutaml/model/store.rb +2 -1
  94. data/lib/lutaml/model/toml.rb +1 -1
  95. data/lib/lutaml/model/transform.rb +17 -39
  96. data/lib/lutaml/model/transformation_registry.rb +1 -1
  97. data/lib/lutaml/model/type.rb +1 -0
  98. data/lib/lutaml/model/union.rb +320 -0
  99. data/lib/lutaml/model/utils.rb +6 -0
  100. data/lib/lutaml/model/validation.rb +1 -4
  101. data/lib/lutaml/model/version.rb +1 -1
  102. data/lib/lutaml/model.rb +18 -0
  103. data/lib/lutaml/toml.rb +1 -1
  104. data/lib/lutaml/xml/adapter.rb +1 -1
  105. data/lib/lutaml/xml/adapter_loader.rb +1 -1
  106. data/lib/lutaml/xml/builder/base.rb +1 -1
  107. data/lib/lutaml/xml/builder.rb +1 -1
  108. data/lib/lutaml/xml/error/schema_validation_error.rb +25 -0
  109. data/lib/lutaml/xml/model_transform.rb +5 -1
  110. data/lib/lutaml/xml/schema/relaxng_schema.rb +5 -0
  111. data/lib/lutaml/xml/schema/xsd_schema.rb +4 -0
  112. data/lib/lutaml/xml/serialization/format_conversion.rb +48 -0
  113. data/lib/lutaml/xml/serialization/instance_methods.rb +13 -0
  114. data/lib/lutaml/xml/transformation/element_builder.rb +21 -8
  115. data/lib/lutaml/xml/transformation/ordered_applier.rb +104 -4
  116. data/lib/lutaml/xml/xsd_validator.rb +66 -0
  117. data/lib/lutaml/xml.rb +53 -12
  118. data/lutaml-model.gemspec +4 -2
  119. data/spec/fixtures/xml/schema/rnc/address_book.rnc +15 -0
  120. data/spec/fixtures/xml/schema/rnc/book_features.rnc +18 -0
  121. data/spec/fixtures/xml/schema/rnc/includes/book.rnc +6 -0
  122. data/spec/fixtures/xml/schema/rnc/includes/circular_a.rnc +8 -0
  123. data/spec/fixtures/xml/schema/rnc/includes/circular_b.rnc +8 -0
  124. data/spec/fixtures/xml/schema/rnc/includes/library.rnc +8 -0
  125. data/spec/fixtures/xml/schema/rng/address_book.rng +16 -0
  126. data/spec/fixtures/xml/schema/rng/book.rng +27 -0
  127. data/spec/fixtures/xml/schema/rng/fixed_value.rng +11 -0
  128. data/spec/fixtures/xml/schema/rng/fragment_in_choice.rng +30 -0
  129. data/spec/fixtures/xml/schema/rng/full_name.rng +39 -0
  130. data/spec/fixtures/xml/schema/rng/integer_range.rng +32 -0
  131. data/spec/fixtures/xml/schema/rng/list_type.rng +12 -0
  132. data/spec/fixtures/xml/schema/rng/namespaced.rng +9 -0
  133. data/spec/fixtures/xml/schema/rng/paragraph.rng +13 -0
  134. data/spec/fixtures/xml/schema/rng/person.rng +23 -0
  135. data/spec/fixtures/xml/schema/rng/union.rng +14 -0
  136. data/spec/fixtures/xml/validate_xml_with_address.xsd +13 -0
  137. data/spec/fixtures/xml/validate_xml_with_contact.xsd +11 -0
  138. data/spec/fixtures/xml/validate_xml_with_person.xsd +11 -0
  139. data/spec/fixtures/xml/validate_xml_with_person_strict.xsd +17 -0
  140. data/spec/fixtures/xml/validate_xml_with_product.xsd +37 -0
  141. data/spec/lutaml/model/attribute_apply_value_map_spec.rb +134 -0
  142. data/spec/lutaml/model/attribute_spec.rb +169 -12
  143. data/spec/lutaml/model/boot_manifest_spec.rb +30 -0
  144. data/spec/lutaml/model/collection_spec.rb +18 -0
  145. data/spec/lutaml/model/mixed_content_spec.rb +171 -0
  146. data/spec/lutaml/model/opal_smoke_spec.rb +67 -2
  147. data/spec/lutaml/model/ordered_content_spec.rb +41 -0
  148. data/spec/lutaml/model/raw_element_spec.rb +125 -0
  149. data/spec/lutaml/model/rule_value_extractor_spec.rb +7 -14
  150. data/spec/lutaml/model/runtime_compatibility_spec.rb +25 -0
  151. data/spec/lutaml/model/schema/renderers/model_spec.rb +149 -0
  152. data/spec/lutaml/model/schema/renderers/namespace_spec.rb +36 -0
  153. data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +75 -0
  154. data/spec/lutaml/model/schema/renderers/union_spec.rb +58 -0
  155. data/spec/lutaml/model/serialize/builder_spec.rb +358 -0
  156. data/spec/lutaml/model/transform_apply_value_map_spec.rb +64 -0
  157. data/spec/lutaml/model/union_attribute_spec.rb +658 -0
  158. data/spec/lutaml/model/union_spec.rb +257 -0
  159. data/spec/lutaml/model/utils_spec.rb +14 -0
  160. data/spec/lutaml/xml/opal_xml_spec.rb +22 -20
  161. data/spec/lutaml/xml/schema/compiler_spec.rb +21 -4
  162. data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +404 -0
  163. data/spec/lutaml/xml/schema/rng_compiler_spec.rb +893 -0
  164. data/spec/lutaml/xml/validate_xml_with_constructs_spec.rb +222 -0
  165. data/spec/lutaml/xml/validate_xml_with_spec.rb +186 -0
  166. data/spec/spec_helper.rb +28 -11
  167. data/spec/support/opal.rb +5 -2
  168. metadata +99 -31
  169. data/lib/lutaml/model/schema/xml_compiler/attribute.rb +0 -106
  170. data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +0 -47
  171. data/lib/lutaml/model/schema/xml_compiler/choice.rb +0 -67
  172. data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +0 -27
  173. data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +0 -34
  174. data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +0 -173
  175. data/lib/lutaml/model/schema/xml_compiler/element.rb +0 -121
  176. data/lib/lutaml/model/schema/xml_compiler/group.rb +0 -113
  177. data/lib/lutaml/model/schema/xml_compiler/restriction.rb +0 -104
  178. data/lib/lutaml/model/schema/xml_compiler/sequence.rb +0 -52
  179. data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +0 -40
  180. data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +0 -265
  181. data/lib/lutaml/model/schema/xml_compiler/xml_namespace_class.rb +0 -110
  182. data/lib/lutaml/model/services/default_value_resolver.rb +0 -60
  183. data/spec/lutaml/model/services/default_value_resolver_spec.rb +0 -162
  184. data/spec/lutaml/xml/schema/compiler/attribute_group_spec.rb +0 -71
  185. data/spec/lutaml/xml/schema/compiler/attribute_spec.rb +0 -67
  186. data/spec/lutaml/xml/schema/compiler/choice_spec.rb +0 -74
  187. data/spec/lutaml/xml/schema/compiler/complex_content_restriction_spec.rb +0 -60
  188. data/spec/lutaml/xml/schema/compiler/complex_content_spec.rb +0 -41
  189. data/spec/lutaml/xml/schema/compiler/complex_type_spec.rb +0 -202
  190. data/spec/lutaml/xml/schema/compiler/element_spec.rb +0 -68
  191. data/spec/lutaml/xml/schema/compiler/group_spec.rb +0 -90
  192. data/spec/lutaml/xml/schema/compiler/restriction_spec.rb +0 -77
  193. data/spec/lutaml/xml/schema/compiler/sequence_spec.rb +0 -65
  194. data/spec/lutaml/xml/schema/compiler/simple_content_spec.rb +0 -61
  195. data/spec/lutaml/xml/schema/compiler/simple_type_spec.rb +0 -184
@@ -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
@@ -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