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,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module RncCompiler
7
+ # Resolves RNC input text and the base directory used for relative
8
+ # include paths.
9
+ module SourceResolver
10
+ ResolvedSource =
11
+ Struct.new(:text, :base_dir, :path, keyword_init: true)
12
+
13
+ module_function
14
+
15
+ def resolve(input, options)
16
+ location = options[:location].to_s
17
+ return string_source(input) if location.empty?
18
+
19
+ expanded = File.expand_path(location)
20
+ if File.file?(expanded)
21
+ return file_source(expanded) if input.nil? || input.to_s.empty?
22
+
23
+ return string_source(input, base_dir: File.dirname(expanded), path: expanded)
24
+ end
25
+ return string_source(input, base_dir: expanded) if File.directory?(expanded)
26
+
27
+ string_source(input)
28
+ end
29
+
30
+ def string_source(input, base_dir: nil, path: nil)
31
+ ResolvedSource.new(text: input.to_s, base_dir: base_dir, path: path)
32
+ end
33
+
34
+ def file_source(path)
35
+ ResolvedSource.new(
36
+ text: File.read(path),
37
+ base_dir: File.dirname(path),
38
+ path: path,
39
+ )
40
+ end
41
+
42
+ private_class_method :string_source, :file_source
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ # Compiles RELAX NG Compact Syntax (RNC) into Lutaml::Model Ruby source.
7
+ #
8
+ # RNC support is a thin adapter: the rng gem parses the compact syntax
9
+ # into Rng::Grammar — resolving includes, bracket annotations, and
10
+ # non-leading `start` definitions natively — then RngCompiler performs all
11
+ # model generation and output handling.
12
+ module RncCompiler
13
+ extend self
14
+
15
+ autoload :SourceResolver, "#{__dir__}/rnc_compiler/source_resolver"
16
+
17
+ DEFAULT_OUTPUT_DIR_PREFIX = "rnc_models"
18
+
19
+ def to_models(rnc, options = {})
20
+ require_rnc_parser!
21
+
22
+ grammar = parse_grammar(rnc, options)
23
+
24
+ RngCompiler.to_models(
25
+ grammar,
26
+ options.merge(default_output_dir: default_output_dir),
27
+ )
28
+ end
29
+
30
+ def parse_grammar(rnc, options)
31
+ return rnc if rnc.is_a?(::Rng::Grammar)
32
+
33
+ source = SourceResolver.resolve(rnc, options)
34
+ ::Rng.parse_rnc(source.text, location: source.path || source.base_dir)
35
+ end
36
+
37
+ def require_rnc_parser!
38
+ return if rnc_parser_available?
39
+
40
+ raise "RNC schema compilation requires an rng gem whose " \
41
+ "`Rng.parse_rnc` accepts a `location:` option (for native " \
42
+ "include resolution). Point your Gemfile at an rng version " \
43
+ "that provides it."
44
+ end
45
+
46
+ # rng gained the `location:` option (native include resolution) after
47
+ # the 0.3.7 release; the released parse_rnc takes only `(rnc)`.
48
+ def rnc_parser_available?
49
+ defined?(::Rng::Grammar) && ::Rng.respond_to?(:parse_rnc) &&
50
+ ::Rng.method(:parse_rnc).parameters.any? { |_type, name| name == :location }
51
+ end
52
+
53
+ def default_output_dir
54
+ "#{DEFAULT_OUTPUT_DIR_PREFIX}_#{Time.now.to_i}"
55
+ end
56
+
57
+ private :parse_grammar, :require_rnc_parser!, :rnc_parser_available?,
58
+ :default_output_dir
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module RngCompiler
7
+ # Detects the shape of a <define> body and builds the corresponding
8
+ # Definitions::* spec for it.
9
+ #
10
+ # <data type="X"><param ...>...</param></data> -> RestrictedType
11
+ # <choice><value>a</value><value>b</value></choice> -> RestrictedType (enum)
12
+ # <choice><data type="A"/><data type="B"/></choice> -> UnionType
13
+ #
14
+ # Returns nil when the define is structural (contains elements or
15
+ # attributes) — caller falls back to building a Definitions::Model.
16
+ class DefineClassifier
17
+ def self.build(define, class_name)
18
+ new(define, class_name).build
19
+ end
20
+
21
+ def initialize(define, class_name)
22
+ @define = define
23
+ @class_name = class_name
24
+ end
25
+
26
+ def build
27
+ return nil if structural_define?
28
+
29
+ union_type || data_restricted_type || enum_restricted_type
30
+ end
31
+
32
+ private
33
+
34
+ def structural_define?
35
+ @define.element.any? || @define.attribute.any?
36
+ end
37
+
38
+ def union_type
39
+ choice = value_choice
40
+ return nil unless RngHelpers.pure_union_choice?(choice)
41
+
42
+ type_refs = Array(choice.data).map do |d|
43
+ symbol = RngCompiler::DATA_TYPE_MAP.fetch(
44
+ d.type, RngCompiler::DEFAULT_DATA_TYPE
45
+ )
46
+ Definitions::TypeRef.new(
47
+ kind: :class_ref,
48
+ value: RngHelpers.parent_class_for(symbol),
49
+ )
50
+ end
51
+
52
+ Definitions::UnionType.new(
53
+ class_name: @class_name,
54
+ members: type_refs,
55
+ cast_strategy: :class_refs,
56
+ )
57
+ end
58
+
59
+ def data_restricted_type
60
+ data = RngHelpers.single(@define.data)
61
+ return nil unless data
62
+
63
+ base = RngCompiler::DATA_TYPE_MAP.fetch(
64
+ data.type, RngCompiler::DEFAULT_DATA_TYPE
65
+ )
66
+ Definitions::RestrictedType.new(
67
+ class_name: @class_name,
68
+ parent_class: RngHelpers.parent_class_for(base),
69
+ facets: RngHelpers.facet_from_data(data),
70
+ )
71
+ end
72
+
73
+ def enum_restricted_type
74
+ choice = value_choice
75
+ return nil unless RngHelpers.pure_value_choice?(choice)
76
+
77
+ Definitions::RestrictedType.new(
78
+ class_name: @class_name,
79
+ parent_class: RngHelpers.parent_class_for(:string),
80
+ facets: RngHelpers.facet_from_values(choice.value),
81
+ )
82
+ end
83
+
84
+ def value_choice
85
+ RngHelpers.single(@define.choice)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -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