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,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module RngCompiler
7
+ # RNG-specific helpers used by ElementVisitor, DefineClassifier, and
8
+ # ValueTypeResolver. Centralising these here avoids the three-way
9
+ # semantic drift the first refactor introduced (e.g. inconsistent
10
+ # `pure_value_choice?` implementations).
11
+ module RngHelpers
12
+ module_function
13
+
14
+ # True if the compiled define renders as a value type (RestrictedType
15
+ # or UnionType) rather than its own Serializable class. Single
16
+ # source of truth — used by ElementVisitor and ValueTypeResolver.
17
+ def simple_type?(klass)
18
+ klass.is_a?(Definitions::RestrictedType) ||
19
+ klass.is_a?(Definitions::UnionType)
20
+ end
21
+
22
+ # True iff `klass` is a fragment model (no rooted xml element).
23
+ def fragment_model?(klass)
24
+ klass.is_a?(Definitions::Model) && klass.xml_root.kind == :fragment
25
+ end
26
+
27
+ # Returns the only element of a collection, or nil if the
28
+ # collection is empty or has more than one element. Treats nil as
29
+ # an empty collection.
30
+ def single(collection)
31
+ arr = Array(collection)
32
+ arr.size == 1 ? arr.first : nil
33
+ end
34
+
35
+ # An RNG node has structural content when any of its element /
36
+ # ref / attribute / choice / group / optional / repetition
37
+ # children are populated.
38
+ def structural_content?(node)
39
+ node && present_children?(node, STRUCTURAL_CHILD_NAMES)
40
+ end
41
+
42
+ # True iff any of `attrs` is a populated array-child of `node`.
43
+ def present_children?(node, attrs)
44
+ attrs.any? do |attr|
45
+ node.respond_to?(attr) && Array(node.public_send(attr)).any?
46
+ end
47
+ end
48
+
49
+ # True when `node` has branching structure that needs its own
50
+ # class. Pure value-choices and `<ref>`s to simple types are NOT
51
+ # branching — they're typed leaves.
52
+ def branching_structural?(node)
53
+ return false unless node
54
+
55
+ non_leaf = STRUCTURAL_CHILD_NAMES - %i[choice ref]
56
+ return true if present_children?(node, non_leaf)
57
+
58
+ return false unless node.respond_to?(:choice)
59
+
60
+ Array(node.choice).any? { |c| !pure_value_choice?(c) }
61
+ end
62
+
63
+ # True iff `choice` is a <choice> consisting purely of <value>
64
+ # alternatives (no <element>, <ref>, <data>, or <text>). A <text>
65
+ # alternative subsumes the literals, so `text | "x"` is a plain
66
+ # string, not an enum restricted to "x".
67
+ def pure_value_choice?(choice)
68
+ return false unless choice
69
+ return false unless choice.respond_to?(:value) && Array(choice.value).any?
70
+ return false if choice.respond_to?(:text) && Utils.present?(choice.text)
71
+
72
+ !present_children?(choice, %i[element ref data])
73
+ end
74
+
75
+ # True iff `choice` is a union: at least 2 <data> alternatives
76
+ # and no other content.
77
+ def pure_union_choice?(choice)
78
+ return false unless choice
79
+ return false unless choice.respond_to?(:data) && Array(choice.data).size >= 2
80
+
81
+ !present_children?(choice, %i[element ref value])
82
+ end
83
+
84
+ # Build a Definitions::Facet from an RNG <data>'s <param> children.
85
+ def facet_from_data(data)
86
+ facet = Definitions::Facet.new
87
+ Array(data.param).each do |param|
88
+ apply_param(facet, param.name, param.value.to_s)
89
+ end
90
+ facet
91
+ end
92
+
93
+ # Build a Definitions::Facet from an array of <value> elements.
94
+ def facet_from_values(values)
95
+ Definitions::Facet.new(
96
+ enumerations: Array(values).map(&:value),
97
+ )
98
+ end
99
+
100
+ # Snake_case symbol used as the registry key for a generated
101
+ # simple-type/union-type/namespace class.
102
+ def type_symbol(class_name)
103
+ Utils.snake_case(class_name).to_sym
104
+ end
105
+
106
+ # A class name derived from base_name that does not collide with an
107
+ # existing key in `classes` (appends 2, 3, ... on collision).
108
+ def unique_class_name(classes, base_name)
109
+ return base_name unless classes.key?(base_name)
110
+
111
+ counter = 2
112
+ counter += 1 while classes.key?("#{base_name}#{counter}")
113
+ "#{base_name}#{counter}"
114
+ end
115
+
116
+ # Maps an RNG/XSD base symbol to its Ruby parent class string,
117
+ # defaulting to the string type when unknown.
118
+ def parent_class_for(base_symbol)
119
+ Lutaml::Model::Type::TYPE_CODES.fetch(
120
+ base_symbol, Lutaml::Model::Type::TYPE_CODES[:string]
121
+ ).to_s
122
+ end
123
+
124
+ def apply_param(facet, name, value)
125
+ case name
126
+ when "minInclusive" then facet.min_inclusive = numeric_or_string(value)
127
+ when "maxInclusive" then facet.max_inclusive = numeric_or_string(value)
128
+ when "minExclusive" then facet.min_exclusive = numeric_or_string(value)
129
+ when "maxExclusive" then facet.max_exclusive = numeric_or_string(value)
130
+ when "minLength" then facet.min_length = value.to_i
131
+ when "maxLength" then facet.max_length = value.to_i
132
+ when "length" then facet.length = value.to_i
133
+ when "pattern" then facet.pattern = value
134
+ end
135
+ end
136
+
137
+ def numeric_or_string(value)
138
+ return value.to_i if /\A-?\d+\z/.match?(value)
139
+ return value.to_f if /\A-?\d+\.\d+\z/.match?(value)
140
+
141
+ value
142
+ end
143
+
144
+ STRUCTURAL_CHILD_NAMES = %i[
145
+ element ref attribute choice group
146
+ optional zeroOrMore oneOrMore
147
+ ].freeze
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module RngCompiler
7
+ # Resolves the value-shape of an element or attribute container into a
8
+ # Symbol (built-in / registered type) or nil if the container has
9
+ # structural content needing its own class.
10
+ #
11
+ # Patterns handled:
12
+ # <element><text/></element> -> :string
13
+ # <element><data type="X"/></element> -> mapped primitive
14
+ # <element><data ...><param ...>/></data></element> -> anonymous RestrictedType (returns its registry symbol)
15
+ # <element><choice><value>...</value></choice></element> -> anonymous RestrictedType (enum) (returns its registry symbol)
16
+ # <element><ref name="StFoo"/></element> -> :st_foo (if simple)
17
+ #
18
+ # When an anonymous RestrictedType is needed, the resolver *builds* it
19
+ # (pure) and asks the supplied `register_class` callback to register
20
+ # it. Build and register are separate methods so unit testing can
21
+ # exercise the pure logic without side effects.
22
+ class ValueTypeResolver
23
+ def initialize(defines, classes, compile_define:, register_class:)
24
+ @defines = defines
25
+ @classes = classes
26
+ @compile_define = compile_define
27
+ @register_class = register_class
28
+ end
29
+
30
+ # Returns the resolved type symbol, or nil if `container` needs
31
+ # its own class. May register an anonymous RestrictedType as a
32
+ # side effect (delegated to the register_class callback).
33
+ def resolve(container)
34
+ return nil if RngHelpers.branching_structural?(container)
35
+
36
+ type_from_inline_simple(container) || primitive_or_ref(container)
37
+ end
38
+
39
+ private
40
+
41
+ def type_from_inline_simple(container)
42
+ simple = build_anonymous_simple_type(container)
43
+ return nil unless simple
44
+
45
+ @register_class.call(simple)
46
+ RngHelpers.type_symbol(simple.class_name)
47
+ end
48
+
49
+ def build_anonymous_simple_type(container)
50
+ anonymous_from_data(container) || anonymous_from_enum_choice(container)
51
+ end
52
+
53
+ def anonymous_from_data(container)
54
+ data = RngHelpers.single(container.data)
55
+ return nil unless data && Array(data.param).any?
56
+
57
+ base = RngCompiler::DATA_TYPE_MAP.fetch(
58
+ data.type, RngCompiler::DEFAULT_DATA_TYPE
59
+ )
60
+ anonymous_restricted_type(container, base, RngHelpers.facet_from_data(data))
61
+ end
62
+
63
+ def anonymous_from_enum_choice(container)
64
+ choice = RngHelpers.single(container.choice)
65
+ return nil unless choice && RngHelpers.pure_value_choice?(choice)
66
+
67
+ anonymous_restricted_type(container, :string, RngHelpers.facet_from_values(choice.value))
68
+ end
69
+
70
+ def anonymous_restricted_type(container, base, facet)
71
+ Definitions::RestrictedType.new(
72
+ class_name: RngHelpers.unique_class_name(
73
+ @classes, "#{Utils.camel_case(container.attr_name.to_s)}Type"
74
+ ),
75
+ parent_class: RngHelpers.parent_class_for(base),
76
+ facets: facet,
77
+ )
78
+ end
79
+
80
+ def primitive_or_ref(container)
81
+ primitive = detect_primitive_type(container)
82
+ return primitive if primitive
83
+
84
+ ref_to_simple_type_symbol(container)
85
+ end
86
+
87
+ def ref_to_simple_type_symbol(container)
88
+ ref = RngHelpers.single(container.ref)
89
+ return nil unless ref
90
+
91
+ target = @defines[ref.name]
92
+ return nil unless target
93
+
94
+ target_class = @compile_define.call(target)
95
+ return RngHelpers.type_symbol(target_class.class_name) if RngHelpers.simple_type?(target_class)
96
+
97
+ nil
98
+ end
99
+
100
+ def detect_primitive_type(child)
101
+ return nil if RngHelpers.structural_content?(child)
102
+
103
+ primitive_from_data(child) || primitive_from_text(child) || primitive_from_value(child)
104
+ end
105
+
106
+ def primitive_from_data(child)
107
+ return nil unless child.data
108
+
109
+ RngCompiler::DATA_TYPE_MAP.fetch(child.data.type, RngCompiler::DEFAULT_DATA_TYPE)
110
+ end
111
+
112
+ def primitive_from_text(child)
113
+ :string if Utils.present?(child.text)
114
+ end
115
+
116
+ def primitive_from_value(child)
117
+ # <element><choice><value>a</value>...</choice></element> falls
118
+ # through to this path as :string with no constraint emitted.
119
+ :string if Array(child.value).any?
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Lutaml
6
+ module Model
7
+ module Schema
8
+ # Compiles a parsed RNG grammar into Lutaml::Model::Serializable Ruby
9
+ # source. Mirrors XmlCompiler in API, options, and output format.
10
+ #
11
+ # Requires the `rng` gem (https://github.com/lutaml/rng) at runtime --
12
+ # that gem provides the Rng::Grammar parsing model. lutaml-model itself
13
+ # has no dependency on it. Registration of Schema.from_relaxng is done
14
+ # in lib/lutaml/xml.rb (alongside Schema.from_xml and Schema.to_relaxng).
15
+ #
16
+ # Entry point: Lutaml::Model::Schema::RngCompiler.to_models(rng, options)
17
+ module RngCompiler
18
+ extend self
19
+
20
+ autoload :MemberCollector, "#{__dir__}/rng_compiler/member_collector"
21
+ autoload :ElementVisitor, "#{__dir__}/rng_compiler/element_visitor"
22
+ autoload :DefineClassifier, "#{__dir__}/rng_compiler/define_classifier"
23
+ autoload :ValueTypeResolver, "#{__dir__}/rng_compiler/value_type_resolver"
24
+ autoload :RngHelpers, "#{__dir__}/rng_compiler/rng_helpers"
25
+
26
+ # Map RNG <data type="..."/> values to Lutaml::Model attribute type
27
+ # symbols. Mirrors Lutaml::Xml::Schema::RelaxngSchema.get_relaxng_type
28
+ # so RNG-generate + compile-back is a round-trip.
29
+ DATA_TYPE_MAP = {
30
+ "string" => :string,
31
+ "integer" => :integer,
32
+ "int" => :integer,
33
+ "long" => :integer,
34
+ "boolean" => :boolean,
35
+ "float" => :float,
36
+ "double" => :float,
37
+ "decimal" => :decimal,
38
+ "date" => :date,
39
+ "dateTime" => :date_time,
40
+ "time" => :time,
41
+ }.freeze
42
+
43
+ DEFAULT_DATA_TYPE = :string
44
+
45
+ def to_models(rng, options = {})
46
+ require_rng_parser!
47
+
48
+ opts = normalize_options(options)
49
+ output = compile(rng, opts)
50
+
51
+ dispatch_output(output, normalize_options(opts))
52
+ end
53
+
54
+ def compile(rng, options = {})
55
+ require_rng_parser!
56
+
57
+ opts = normalize_options(options)
58
+ grammar = parse_grammar(rng, opts)
59
+ classes, namespaces = compile_grammar(grammar)
60
+
61
+ entries =
62
+ classes.map { |name, spec| CompiledOutput::Entry.new(name, spec, :model) } +
63
+ namespaces.map { |name, spec| CompiledOutput::Entry.new(name, spec, :namespace) }
64
+
65
+ CompiledOutput.new(
66
+ entries: entries,
67
+ module_namespace: opts[:module_namespace],
68
+ register_id: opts[:register_id],
69
+ )
70
+ end
71
+
72
+ private
73
+
74
+ def require_rng_parser!
75
+ return if defined?(::Rng::Grammar)
76
+
77
+ raise "RNG schema compilation requires the rng gem. " \
78
+ "Add `gem \"rng\"` to your Gemfile."
79
+ end
80
+
81
+ def normalize_options(options)
82
+ opts = options.dup
83
+ opts[:indent] ||= 2
84
+ opts[:register_id] ||= :default
85
+ opts
86
+ end
87
+
88
+ def parse_grammar(rng, options)
89
+ return rng if rng.is_a?(::Rng::Grammar)
90
+
91
+ if options[:location]
92
+ ::Rng.parse(rng, location: options[:location], resolve_external: true)
93
+ else
94
+ ::Rng::Grammar.from_xml(rng)
95
+ end
96
+ end
97
+
98
+ def dispatch_output(output, options)
99
+ if options[:create_files]
100
+ dir = options.fetch(
101
+ :output_dir,
102
+ options.fetch(:default_output_dir, "rng_models_#{Time.now.to_i}"),
103
+ )
104
+ FileWriter.write(output, dir)
105
+ true
106
+ elsif options[:load_classes]
107
+ ClassLoader.load(output)
108
+ output.sources
109
+ else
110
+ output.sources
111
+ end
112
+ end
113
+
114
+ # Returns [classes_hash, namespaces_hash]. `classes` holds
115
+ # Definitions::Model / Definitions::RestrictedType / Definitions::UnionType.
116
+ # `namespaces` holds Definitions::Namespace.
117
+ def compile_grammar(grammar)
118
+ defines = grammar.define.to_h { |d| [d.name, d] }
119
+ classes = {}
120
+ namespaces = {}
121
+ registrar = ->(uri) { register_namespace(namespaces, uri) }
122
+
123
+ default_uri = grammar_default_ns(grammar)
124
+ visitor = ElementVisitor.new(
125
+ defines, classes,
126
+ default_namespace_class: default_uri && registrar.call(default_uri),
127
+ register_namespace: registrar
128
+ )
129
+
130
+ grammar.start.each do |start|
131
+ visitor.compile_element(start.element) if start.element
132
+ Array(start.ref).each do |ref|
133
+ target = defines[ref.name]
134
+ visitor.compile_define(target) if target
135
+ end
136
+ end
137
+
138
+ # Sweep: compile any <define> not reachable from <start>.
139
+ grammar.define.each { |define| visitor.compile_define(define) }
140
+
141
+ finalize_models(classes)
142
+
143
+ [classes, namespaces]
144
+ end
145
+
146
+ def grammar_default_ns(grammar)
147
+ uri = grammar.respond_to?(:ns) ? grammar.ns : nil
148
+ uri if uri.is_a?(String) && !uri.empty?
149
+ end
150
+
151
+ # Build-or-find the namespace for `uri`; returns its class_name.
152
+ # RNG/RNC name-prefixed elements are namespace-qualified, so descendant
153
+ # elements inherit the namespace via element_form_default :qualified.
154
+ def register_namespace(namespaces, uri)
155
+ existing = namespaces.values.find { |ns| ns.uri == uri }
156
+ return existing.class_name if existing
157
+
158
+ # Distinct URIs can map to the same generated class name AND prefix
159
+ # (e.g. both `urn:a` and `urn:b` -> "DefaultNamespace"/"ns"); uniquify
160
+ # both so one namespace does not silently overwrite the other or
161
+ # collide with its prefix at serialization.
162
+ class_name = RngHelpers.unique_class_name(
163
+ namespaces, NamespaceNaming.class_name_for(uri)
164
+ )
165
+ ns = Definitions::Namespace.new(
166
+ class_name: class_name,
167
+ uri: uri,
168
+ prefix_default: unique_prefix(namespaces, NamespaceNaming.prefix_for(uri)),
169
+ element_form_default: :qualified,
170
+ )
171
+ namespaces[class_name] = ns
172
+ class_name
173
+ end
174
+
175
+ # A prefix that does not collide with an already-registered namespace's
176
+ # prefix (appends 2, 3, ... on collision).
177
+ def unique_prefix(namespaces, base)
178
+ taken = namespaces.values.map(&:prefix_default)
179
+ return base unless taken.include?(base)
180
+
181
+ counter = 2
182
+ counter += 1 while taken.include?("#{base}#{counter}")
183
+ "#{base}#{counter}"
184
+ end
185
+
186
+ # Set `required_files` so each generated file requires its
187
+ # dependencies: a Model requires its imports/class_ref types/namespace;
188
+ # a namespaced RestrictedType requires its namespace class.
189
+ def finalize_models(classes)
190
+ classes.each_value do |spec|
191
+ case spec
192
+ when Definitions::Model
193
+ spec.required_files =
194
+ Renderers::RequiredFilesCalculator.class_names_for_rng(spec)
195
+ .map { |dep| require_relative_line(dep) }
196
+ when Definitions::RestrictedType
197
+ next unless spec.namespace_class_name
198
+
199
+ spec.required_files = namespaced_type_requires(spec)
200
+ end
201
+ end
202
+ end
203
+
204
+ # A namespaced RestrictedType requires its namespace class and, when it
205
+ # subclasses another generated type (a bare name, not a Lutaml built-in),
206
+ # that parent too.
207
+ def namespaced_type_requires(spec)
208
+ deps = [spec.namespace_class_name]
209
+ parent = spec.parent_class
210
+ deps << parent if parent && !parent.include?("::")
211
+ deps.map { |dep| require_relative_line(dep) }
212
+ end
213
+
214
+ def require_relative_line(class_name)
215
+ %(require_relative "#{Utils.snake_case(class_name)}")
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module Lutaml
6
+ module Model
7
+ module Schema
8
+ # Templates that are *actually* shared between schema compilers
9
+ # (XSD, RNG, future formats). Single-consumer templates live in
10
+ # the renderer that uses them — putting them here would just be
11
+ # relocation, not deduplication.
12
+ #
13
+ # Each constant is an `ERB.new(...)` evaluated via
14
+ # `TEMPLATE.result(binding)` from a renderer that exposes the
15
+ # required hook methods (documented above each template).
16
+ module Templates
17
+ # ----------------------------------------------------------------
18
+ # Single shape for any Lutaml::Model::Serializable subclass
19
+ # generated by ANY schema compiler. There is no per-format
20
+ # variant — adding a new xml directive here propagates to every
21
+ # compiler that uses this template. Consumed by:
22
+ # - Lutaml::Model::Schema::Renderers::Model
23
+ # (renders XSD complex types / groups and RNG generated classes)
24
+ #
25
+ # Required binding methods (renderer must expose all of these,
26
+ # returning "" / nil / false for inapplicable sections):
27
+ #
28
+ # from ClassBoilerplate:
29
+ # module_opening, module_closing,
30
+ # registration_methods, registration_execution,
31
+ # boilerplate_indent_str
32
+ #
33
+ # identity / parent:
34
+ # rendered_class_name String — CamelCase
35
+ # serializable_class_parent String — e.g. "Lutaml::Model::Serializable"
36
+ #
37
+ # header:
38
+ # serializable_class_required_files String — `require_relative` lines or ""
39
+ # serializable_class_documentation String — doc comment block or ""
40
+ #
41
+ # body — attribute declarations:
42
+ # serializable_class_attributes String — `attribute :foo, :bar` lines
43
+ # serializable_class_imports String — `import_model :foo` lines or ""
44
+ #
45
+ # body — xml block:
46
+ # xml_root_directive_line String or nil — `element "X"` /
47
+ # `type_name "X"` / nil to skip
48
+ # xml_namespace_line String or nil — `namespace ClassName` / nil
49
+ # xml_mixed_content? Boolean — emit `mixed_content`
50
+ # xml_text_content? Boolean — emit `map_content to: :content`
51
+ # xml_attribute_mappings String — `map_element`/`map_attribute` lines
52
+ # xml_extra_mappings String — extra mapping lines or ""
53
+ # (e.g. XSD's simple_content tail)
54
+ # ----------------------------------------------------------------
55
+ SERIALIZABLE_CLASS = ERB.new(<<~TMPL, trim_mode: "-")
56
+ # frozen_string_literal: true
57
+
58
+ require "lutaml/model"
59
+ <%= serializable_class_required_files -%>
60
+ <%= module_opening -%>
61
+ <%= serializable_class_documentation -%>
62
+ class <%= rendered_class_name %> < <%= serializable_class_parent %>
63
+ <%= serializable_class_attributes -%>
64
+ <%= serializable_class_imports -%>
65
+ <%= "\n" if serializable_class_attributes.length.positive? || serializable_class_imports.length.positive? -%>
66
+ <%= boilerplate_indent_str %>xml do
67
+ <%- if xml_root_directive_line -%>
68
+ <%= boilerplate_indent_str * 2 %><%= xml_root_directive_line %>
69
+ <%- end -%>
70
+ <%- if xml_namespace_line -%>
71
+ <%= boilerplate_indent_str * 2 %><%= xml_namespace_line %>
72
+ <%- end -%>
73
+ <%- if xml_mixed_content? -%>
74
+ <%= boilerplate_indent_str * 2 %>mixed_content
75
+ <%- end -%>
76
+ <%- if xml_text_content? -%>
77
+ <%= boilerplate_indent_str * 2 %>map_content to: :content
78
+ <%- end -%>
79
+ <%= xml_attribute_mappings -%>
80
+ <%= xml_extra_mappings -%>
81
+ <%= boilerplate_indent_str %>end
82
+ <%= registration_methods -%>
83
+ end
84
+ <%= module_closing -%>
85
+ <%= registration_execution -%>
86
+ TMPL
87
+
88
+ # XmlNamespace subclass. Consumed by:
89
+ # - Lutaml::Model::Schema::Renderers::Namespace
90
+ # Binding: module_opening, module_closing, class_name, uri, prefix_default.
91
+ XML_NAMESPACE = ERB.new(<<~TMPL, trim_mode: "-")
92
+ # frozen_string_literal: true
93
+
94
+ require "lutaml/model"
95
+
96
+ <%= module_opening -%>
97
+ # Namespace: <%= uri %>
98
+ class <%= class_name %> < Lutaml::Xml::W3c::XmlNamespace
99
+ uri <%= uri.inspect %>
100
+ prefix_default <%= prefix_default.inspect %>
101
+ <%- if element_form_default_line -%>
102
+ <%= element_form_default_line %>
103
+ <%- end -%>
104
+ end
105
+ <%= module_closing -%>
106
+ TMPL
107
+
108
+ # Restricted simple-type subclass (cast method that mutates options
109
+ # with facets then delegates to super). Consumed by:
110
+ # - Lutaml::Model::Schema::Renderers::RestrictedType
111
+ # Binding: module_opening, module_closing, registration_methods,
112
+ # registration_execution, rendered_class_name, parent_class,
113
+ # xml_namespace_line, restricted_simple_type_required_files,
114
+ # restricted_simple_type_cast_body, boilerplate_indent_str.
115
+ RESTRICTED_SIMPLE_TYPE = ERB.new(<<~TMPL, trim_mode: "-")
116
+ # frozen_string_literal: true
117
+ require "lutaml/model"
118
+ <%= restricted_simple_type_required_files -%>
119
+ <%= module_opening -%>
120
+ class <%= rendered_class_name %><%= " < \#{parent_class}" if parent_class %>
121
+ <%- if xml_namespace_line -%>
122
+ <%= boilerplate_indent_str %>xml do
123
+ <%= boilerplate_indent_str * 2 %><%= xml_namespace_line %>
124
+ <%= boilerplate_indent_str %>end
125
+ <%- end -%>
126
+ <%= boilerplate_indent_str %>def self.cast(value, options = {})
127
+ <%= boilerplate_indent_str * 2 %>return if value.nil?
128
+
129
+ <%= restricted_simple_type_cast_body -%>
130
+ <%= boilerplate_indent_str * 2 %>value = super(value, options)
131
+ <%= boilerplate_indent_str * 2 %>value
132
+ <%= boilerplate_indent_str %>end
133
+ <%= registration_methods -%>
134
+ end
135
+ <%= module_closing -%>
136
+ <%= registration_execution -%>
137
+ TMPL
138
+
139
+ # Lutaml::Model::Type::Value union subclass. Consumed by:
140
+ # - Lutaml::Model::Schema::Renderers::Union
141
+ # `union_cast_body` selects the cast strategy from the spec
142
+ # (XSD: `||`-chained resolve_type; RNG: each+rescue).
143
+ UNION_TYPE = ERB.new(<<~TMPL, trim_mode: "-")
144
+ # frozen_string_literal: true
145
+ require "lutaml/model"
146
+ <%= union_required_files -%>
147
+ <%= module_opening -%>
148
+ class <%= rendered_class_name %> < Lutaml::Model::Type::Value
149
+ <%= boilerplate_indent_str %>def self.cast(value, options = {})
150
+ <%= boilerplate_indent_str * 2 %>return if value.nil?
151
+
152
+ <%= union_cast_body -%>
153
+ <%= boilerplate_indent_str %>end
154
+ <%= registration_methods -%>
155
+ end
156
+ <%= module_closing -%>
157
+ <%= registration_execution -%>
158
+ TMPL
159
+ end
160
+ end
161
+ end
162
+ end