lutaml-model 0.8.17 → 0.8.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/downstream-performance.yml +27 -1
  3. data/.github/workflows/js-pr-check.yml +102 -0
  4. data/.github/workflows/js-sync.yml +51 -0
  5. data/.github/workflows/opal.yml +26 -6
  6. data/.github/workflows/performance.yml +32 -7
  7. data/.github/workflows/rake.yml +78 -32
  8. data/.gitmodules +6 -0
  9. data/.rubocop_todo.yml +8 -0
  10. data/Gemfile +16 -1
  11. data/README.adoc +267 -0
  12. data/Rakefile +166 -4
  13. data/docs/_guides/schema-import.adoc +42 -0
  14. data/docs/_guides/xml-mapping.adoc +21 -0
  15. data/docs/_pages/validation.adoc +65 -0
  16. data/lib/compat/opal/generate_boot.rb +123 -0
  17. data/lib/compat/opal/js_bundle_entry.rb +42 -0
  18. data/lib/compat/opal/lutaml_model_boot.rb +497 -0
  19. data/lib/compat/opal/moxml_boot.rb +68 -0
  20. data/lib/compat/opal/yaml_compat.rb +32 -0
  21. data/lib/lutaml/json.rb +1 -1
  22. data/lib/lutaml/key_value/transform.rb +3 -8
  23. data/lib/lutaml/key_value/transformation/value_serializer.rb +14 -1
  24. data/lib/lutaml/key_value/transformation.rb +17 -5
  25. data/lib/lutaml/model/adapter_resolver.rb +2 -2
  26. data/lib/lutaml/model/attribute.rb +88 -11
  27. data/lib/lutaml/model/cached_type_resolver.rb +1 -1
  28. data/lib/lutaml/model/error/union_schema_unsupported_error.rb +11 -0
  29. data/lib/lutaml/model/global_context.rb +11 -8
  30. data/lib/lutaml/model/instrumentation.rb +2 -2
  31. data/lib/lutaml/model/mapping/mapping_rule.rb +0 -6
  32. data/lib/lutaml/model/register.rb +2 -2
  33. data/lib/lutaml/model/runtime_compatibility.rb +50 -0
  34. data/lib/lutaml/model/schema/class_loader.rb +58 -0
  35. data/lib/lutaml/model/schema/compiled_output.rb +83 -0
  36. data/lib/lutaml/model/schema/definitions/attribute.rb +32 -0
  37. data/lib/lutaml/model/schema/definitions/choice.rb +20 -0
  38. data/lib/lutaml/model/schema/definitions/facet.rb +40 -0
  39. data/lib/lutaml/model/schema/definitions/group_import.rb +22 -0
  40. data/lib/lutaml/model/schema/definitions/member_walk.rb +33 -0
  41. data/lib/lutaml/model/schema/definitions/model.rb +40 -0
  42. data/lib/lutaml/model/schema/definitions/namespace.rb +23 -0
  43. data/lib/lutaml/model/schema/definitions/restricted_type.rb +30 -0
  44. data/lib/lutaml/model/schema/definitions/sequence.rb +19 -0
  45. data/lib/lutaml/model/schema/definitions/simple_content.rb +22 -0
  46. data/lib/lutaml/model/schema/definitions/transform_facet.rb +21 -0
  47. data/lib/lutaml/model/schema/definitions/type_ref.rb +20 -0
  48. data/lib/lutaml/model/schema/definitions/union_type.rb +28 -0
  49. data/lib/lutaml/model/schema/definitions/xml_root.rb +20 -0
  50. data/lib/lutaml/model/schema/definitions.rb +24 -0
  51. data/lib/lutaml/model/schema/file_writer.rb +70 -0
  52. data/lib/lutaml/model/schema/generator/definitions_collection.rb +16 -2
  53. data/lib/lutaml/model/schema/generator/property.rb +21 -4
  54. data/lib/lutaml/model/schema/module_nesting.rb +31 -0
  55. data/lib/lutaml/model/schema/namespace_naming.rb +46 -0
  56. data/lib/lutaml/model/schema/registry_generator.rb +115 -0
  57. data/lib/lutaml/model/schema/renderers/base.rb +40 -0
  58. data/lib/lutaml/model/schema/renderers/mappings.rb +72 -0
  59. data/lib/lutaml/model/schema/renderers/member_decls.rb +115 -0
  60. data/lib/lutaml/model/schema/renderers/model.rb +121 -0
  61. data/lib/lutaml/model/schema/renderers/namespace.rb +30 -0
  62. data/lib/lutaml/model/schema/renderers/registration.rb +64 -0
  63. data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +93 -0
  64. data/lib/lutaml/model/schema/renderers/restricted_type.rb +88 -0
  65. data/lib/lutaml/model/schema/renderers/union.rb +83 -0
  66. data/lib/lutaml/model/schema/renderers.rb +19 -0
  67. data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +47 -0
  68. data/lib/lutaml/model/schema/rnc_compiler.rb +62 -0
  69. data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +91 -0
  70. data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +392 -0
  71. data/lib/lutaml/model/schema/rng_compiler/member_collector.rb +30 -0
  72. data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +152 -0
  73. data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +125 -0
  74. data/lib/lutaml/model/schema/rng_compiler.rb +220 -0
  75. data/lib/lutaml/model/schema/templates.rb +162 -0
  76. data/lib/lutaml/model/schema/xml_compiler/element_order.rb +41 -0
  77. data/lib/lutaml/model/schema/xml_compiler/registry_generator.rb +31 -107
  78. data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +191 -0
  79. data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +216 -0
  80. data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +153 -0
  81. data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +167 -0
  82. data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +87 -0
  83. data/lib/lutaml/model/schema/xml_compiler.rb +38 -517
  84. data/lib/lutaml/model/schema.rb +19 -0
  85. data/lib/lutaml/model/serialize/attribute_definition.rb +14 -2
  86. data/lib/lutaml/model/serialize/builder.rb +15 -2
  87. data/lib/lutaml/model/serialize/value_mapping.rb +9 -57
  88. data/lib/lutaml/model/serialize.rb +6 -9
  89. data/lib/lutaml/model/services/rule_value_extractor.rb +2 -3
  90. data/lib/lutaml/model/services.rb +0 -2
  91. data/lib/lutaml/model/store.rb +2 -1
  92. data/lib/lutaml/model/toml.rb +1 -1
  93. data/lib/lutaml/model/transform.rb +17 -39
  94. data/lib/lutaml/model/transformation_registry.rb +1 -1
  95. data/lib/lutaml/model/type.rb +1 -0
  96. data/lib/lutaml/model/union.rb +320 -0
  97. data/lib/lutaml/model/utils.rb +6 -0
  98. data/lib/lutaml/model/validation.rb +1 -4
  99. data/lib/lutaml/model/version.rb +1 -1
  100. data/lib/lutaml/model.rb +18 -0
  101. data/lib/lutaml/toml.rb +1 -1
  102. data/lib/lutaml/xml/adapter.rb +1 -1
  103. data/lib/lutaml/xml/adapter_loader.rb +1 -1
  104. data/lib/lutaml/xml/builder/base.rb +1 -1
  105. data/lib/lutaml/xml/builder.rb +1 -1
  106. data/lib/lutaml/xml/error/schema_validation_error.rb +25 -0
  107. data/lib/lutaml/xml/model_transform.rb +5 -1
  108. data/lib/lutaml/xml/schema/relaxng_schema.rb +5 -0
  109. data/lib/lutaml/xml/schema/xsd_schema.rb +4 -0
  110. data/lib/lutaml/xml/serialization/format_conversion.rb +48 -0
  111. data/lib/lutaml/xml/serialization/instance_methods.rb +13 -0
  112. data/lib/lutaml/xml/transformation/element_builder.rb +21 -8
  113. data/lib/lutaml/xml/transformation/ordered_applier.rb +6 -0
  114. data/lib/lutaml/xml/xsd_validator.rb +66 -0
  115. data/lib/lutaml/xml.rb +53 -12
  116. data/lutaml-model.gemspec +4 -2
  117. data/spec/fixtures/xml/schema/rnc/address_book.rnc +15 -0
  118. data/spec/fixtures/xml/schema/rnc/book_features.rnc +18 -0
  119. data/spec/fixtures/xml/schema/rnc/includes/book.rnc +6 -0
  120. data/spec/fixtures/xml/schema/rnc/includes/circular_a.rnc +8 -0
  121. data/spec/fixtures/xml/schema/rnc/includes/circular_b.rnc +8 -0
  122. data/spec/fixtures/xml/schema/rnc/includes/library.rnc +8 -0
  123. data/spec/fixtures/xml/schema/rng/address_book.rng +16 -0
  124. data/spec/fixtures/xml/schema/rng/book.rng +27 -0
  125. data/spec/fixtures/xml/schema/rng/fixed_value.rng +11 -0
  126. data/spec/fixtures/xml/schema/rng/fragment_in_choice.rng +30 -0
  127. data/spec/fixtures/xml/schema/rng/full_name.rng +39 -0
  128. data/spec/fixtures/xml/schema/rng/integer_range.rng +32 -0
  129. data/spec/fixtures/xml/schema/rng/list_type.rng +12 -0
  130. data/spec/fixtures/xml/schema/rng/namespaced.rng +9 -0
  131. data/spec/fixtures/xml/schema/rng/paragraph.rng +13 -0
  132. data/spec/fixtures/xml/schema/rng/person.rng +23 -0
  133. data/spec/fixtures/xml/schema/rng/union.rng +14 -0
  134. data/spec/fixtures/xml/validate_xml_with_address.xsd +13 -0
  135. data/spec/fixtures/xml/validate_xml_with_contact.xsd +11 -0
  136. data/spec/fixtures/xml/validate_xml_with_person.xsd +11 -0
  137. data/spec/fixtures/xml/validate_xml_with_person_strict.xsd +17 -0
  138. data/spec/fixtures/xml/validate_xml_with_product.xsd +37 -0
  139. data/spec/lutaml/model/attribute_apply_value_map_spec.rb +134 -0
  140. data/spec/lutaml/model/attribute_spec.rb +169 -12
  141. data/spec/lutaml/model/boot_manifest_spec.rb +30 -0
  142. data/spec/lutaml/model/collection_spec.rb +18 -0
  143. data/spec/lutaml/model/mixed_content_spec.rb +171 -0
  144. data/spec/lutaml/model/opal_smoke_spec.rb +67 -2
  145. data/spec/lutaml/model/ordered_content_spec.rb +30 -0
  146. data/spec/lutaml/model/raw_element_spec.rb +125 -0
  147. data/spec/lutaml/model/rule_value_extractor_spec.rb +7 -14
  148. data/spec/lutaml/model/runtime_compatibility_spec.rb +25 -0
  149. data/spec/lutaml/model/schema/renderers/model_spec.rb +149 -0
  150. data/spec/lutaml/model/schema/renderers/namespace_spec.rb +36 -0
  151. data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +75 -0
  152. data/spec/lutaml/model/schema/renderers/union_spec.rb +58 -0
  153. data/spec/lutaml/model/transform_apply_value_map_spec.rb +64 -0
  154. data/spec/lutaml/model/union_attribute_spec.rb +658 -0
  155. data/spec/lutaml/model/union_spec.rb +257 -0
  156. data/spec/lutaml/model/utils_spec.rb +14 -0
  157. data/spec/lutaml/xml/opal_xml_spec.rb +22 -20
  158. data/spec/lutaml/xml/schema/compiler_spec.rb +21 -4
  159. data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +404 -0
  160. data/spec/lutaml/xml/schema/rng_compiler_spec.rb +893 -0
  161. data/spec/lutaml/xml/validate_xml_with_constructs_spec.rb +222 -0
  162. data/spec/lutaml/xml/validate_xml_with_spec.rb +186 -0
  163. data/spec/spec_helper.rb +28 -11
  164. data/spec/support/opal.rb +5 -2
  165. metadata +97 -31
  166. data/lib/lutaml/model/schema/xml_compiler/attribute.rb +0 -106
  167. data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +0 -47
  168. data/lib/lutaml/model/schema/xml_compiler/choice.rb +0 -67
  169. data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +0 -27
  170. data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +0 -34
  171. data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +0 -173
  172. data/lib/lutaml/model/schema/xml_compiler/element.rb +0 -121
  173. data/lib/lutaml/model/schema/xml_compiler/group.rb +0 -113
  174. data/lib/lutaml/model/schema/xml_compiler/restriction.rb +0 -104
  175. data/lib/lutaml/model/schema/xml_compiler/sequence.rb +0 -52
  176. data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +0 -40
  177. data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +0 -265
  178. data/lib/lutaml/model/schema/xml_compiler/xml_namespace_class.rb +0 -110
  179. data/lib/lutaml/model/services/default_value_resolver.rb +0 -60
  180. data/spec/lutaml/model/services/default_value_resolver_spec.rb +0 -162
  181. data/spec/lutaml/xml/schema/compiler/attribute_group_spec.rb +0 -71
  182. data/spec/lutaml/xml/schema/compiler/attribute_spec.rb +0 -67
  183. data/spec/lutaml/xml/schema/compiler/choice_spec.rb +0 -74
  184. data/spec/lutaml/xml/schema/compiler/complex_content_restriction_spec.rb +0 -60
  185. data/spec/lutaml/xml/schema/compiler/complex_content_spec.rb +0 -41
  186. data/spec/lutaml/xml/schema/compiler/complex_type_spec.rb +0 -202
  187. data/spec/lutaml/xml/schema/compiler/element_spec.rb +0 -68
  188. data/spec/lutaml/xml/schema/compiler/group_spec.rb +0 -90
  189. data/spec/lutaml/xml/schema/compiler/restriction_spec.rb +0 -77
  190. data/spec/lutaml/xml/schema/compiler/sequence_spec.rb +0 -65
  191. data/spec/lutaml/xml/schema/compiler/simple_content_spec.rb +0 -61
  192. data/spec/lutaml/xml/schema/compiler/simple_type_spec.rb +0 -184
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ # Pure value object holding the output of compilation. No I/O — just
7
+ # data. FileWriter / ClassLoader collaborators consume this to perform
8
+ # the impure parts of `to_models`. Shared across schema compilers (RNG,
9
+ # XSD, future formats).
10
+ #
11
+ # Entries are an ordered list of tagged records, each carrying a
12
+ # CamelCase class name, a payload (either a Definitions::* spec or a
13
+ # pre-rendered Ruby source String), and a `kind` of `:model` or
14
+ # `:namespace`.
15
+ #
16
+ # `:model` entries are written to disk AND registered with
17
+ # `register_all`. `:namespace` entries are written and autoloaded but
18
+ # never registered.
19
+ class CompiledOutput
20
+ Entry = Struct.new(:name, :payload, :kind)
21
+
22
+ attr_reader :entries, :module_namespace, :register_id,
23
+ :source_module_namespace
24
+
25
+ # `module_namespace` is what the central registry file wraps in
26
+ # (and where `register_all` lives). `source_module_namespace`
27
+ # is what the per-class files wrap in — defaults to the same,
28
+ # but ClassLoader can pass a different value when the caller
29
+ # asked for unwrapped classes but a registry constant is still
30
+ # needed to drive autoload.
31
+ def initialize(entries:, module_namespace: nil, register_id: :default,
32
+ source_module_namespace: :inherit)
33
+ @entries = entries
34
+ @module_namespace = module_namespace
35
+ @register_id = register_id
36
+ @source_module_namespace =
37
+ source_module_namespace == :inherit ? module_namespace : source_module_namespace
38
+ end
39
+
40
+ # Model-only entries — what the registry generator and ClassLoader
41
+ # iterate when emitting `register_all` calls.
42
+ def models
43
+ @models ||= entries.select { |e| e.kind == :model }
44
+ end
45
+
46
+ # Namespace-only entries — autoloaded but never registered.
47
+ def namespaces
48
+ @namespaces ||= entries.select { |e| e.kind == :namespace }
49
+ end
50
+
51
+ # Flat `name => Ruby source` hash covering every entry (models +
52
+ # namespaces). Used by FileWriter for the per-file write loop and
53
+ # by RNG / XSD compilers as the public `to_models` return value.
54
+ # Class files are rendered against `source_module_namespace`,
55
+ # which usually equals `module_namespace` but differs when the
56
+ # caller wanted unwrapped class files inside a registry module.
57
+ def sources
58
+ @sources ||= entries.to_h { |e| [e.name, source_for(e)] }
59
+ end
60
+
61
+ private
62
+
63
+ def source_for(entry)
64
+ renderer_for(entry.payload).render(
65
+ entry.payload,
66
+ module_namespace: @source_module_namespace,
67
+ register_id: @register_id,
68
+ )
69
+ end
70
+
71
+ def renderer_for(spec)
72
+ case spec
73
+ when Definitions::Model then Renderers::Model
74
+ when Definitions::RestrictedType then Renderers::RestrictedType
75
+ when Definitions::UnionType then Renderers::Union
76
+ when Definitions::Namespace then Renderers::Namespace
77
+ else raise ArgumentError, "Unknown spec type: #{spec.class}"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Definitions
7
+ # One attribute on a generated class.
8
+ class Attribute
9
+ attr_accessor :name, :type, :xml_name, :kind, :collection,
10
+ :default, :documentation, :initialize_empty,
11
+ :render_default, :render_empty
12
+
13
+ def initialize(name:, type:, xml_name:, kind:,
14
+ collection: false, default: nil,
15
+ documentation: nil, initialize_empty: false,
16
+ render_default: false, render_empty: false)
17
+ @name = name
18
+ @type = type
19
+ @xml_name = xml_name
20
+ @kind = kind
21
+ @collection = collection
22
+ @default = default
23
+ @documentation = documentation
24
+ @initialize_empty = initialize_empty
25
+ @render_default = render_default
26
+ @render_empty = render_empty
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Definitions
7
+ # A `choice do ... end` block. Alternatives may be Attribute,
8
+ # Sequence, or nested Choice specs.
9
+ class Choice
10
+ attr_accessor :alternatives, :header
11
+
12
+ def initialize(alternatives:, header:)
13
+ @alternatives = alternatives
14
+ @header = header
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Definitions
7
+ # Facet container for restricted simple types. Every field is
8
+ # optional; nil means "facet not present".
9
+ class Facet
10
+ attr_accessor :min_inclusive, :max_inclusive,
11
+ :min_exclusive, :max_exclusive,
12
+ :pattern, :enumerations,
13
+ :min_length, :max_length, :length,
14
+ :total_digits, :fraction_digits,
15
+ :white_space
16
+
17
+ def initialize(min_inclusive: nil, max_inclusive: nil,
18
+ min_exclusive: nil, max_exclusive: nil,
19
+ pattern: nil, enumerations: nil,
20
+ min_length: nil, max_length: nil, length: nil,
21
+ total_digits: nil, fraction_digits: nil,
22
+ white_space: nil)
23
+ @min_inclusive = min_inclusive
24
+ @max_inclusive = max_inclusive
25
+ @min_exclusive = min_exclusive
26
+ @max_exclusive = max_exclusive
27
+ @pattern = pattern
28
+ @enumerations = enumerations
29
+ @min_length = min_length
30
+ @max_length = max_length
31
+ @length = length
32
+ @total_digits = total_digits
33
+ @fraction_digits = fraction_digits
34
+ @white_space = white_space
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Definitions
7
+ # An inline `<xs:group ref="..."/>` reference appearing as a
8
+ # member inside a Choice / Sequence / Model. Renders as
9
+ # `import_model_attributes :name` in the attribute-decl block
10
+ # and `import_model_mappings :name` in the xml-mapping block.
11
+ # name is the snake_case symbol form (without leading colon).
12
+ class GroupImport
13
+ attr_accessor :name
14
+
15
+ def initialize(name:)
16
+ @name = name
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Definitions
7
+ # Stateless traversal helpers for the heterogeneous `members`
8
+ # array of a Definitions::Model. Definitions stay as pure data;
9
+ # the walk logic lives here so consumers (XmlCompiler /
10
+ # RngCompiler dependency collection, future visitors) don't each
11
+ # re-implement the same case-dispatch.
12
+ module MemberWalk
13
+ module_function
14
+
15
+ # Yields every Attribute leaf, recursing into Sequence members
16
+ # and Choice alternatives. Returns an Enumerator when called
17
+ # without a block.
18
+ def each_attribute(members, &block)
19
+ return enum_for(:each_attribute, members) unless block
20
+
21
+ members.each do |member|
22
+ case member
23
+ when Attribute then yield member
24
+ when Sequence then each_attribute(member.members, &block)
25
+ when Choice then each_attribute(member.alternatives, &block)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Definitions
7
+ # One generated Lutaml::Model::Serializable subclass.
8
+ class Model
9
+ attr_accessor :class_name, :xml_root, :members,
10
+ :parent_class, :namespace_class_name,
11
+ :mixed, :text_content, :imports,
12
+ :documentation, :simple_content, :required_files,
13
+ :module_wrappable, :lazy_register
14
+
15
+ def initialize(class_name:, xml_root:, members: [],
16
+ parent_class: "Lutaml::Model::Serializable",
17
+ namespace_class_name: nil,
18
+ mixed: false, text_content: false,
19
+ imports: [], documentation: nil,
20
+ simple_content: nil, required_files: [],
21
+ module_wrappable: true, lazy_register: false)
22
+ @class_name = class_name
23
+ @xml_root = xml_root
24
+ @members = members
25
+ @parent_class = parent_class
26
+ @namespace_class_name = namespace_class_name
27
+ @mixed = mixed
28
+ @text_content = text_content
29
+ @imports = imports
30
+ @documentation = documentation
31
+ @simple_content = simple_content
32
+ @required_files = required_files
33
+ @module_wrappable = module_wrappable
34
+ @lazy_register = lazy_register
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Definitions
7
+ # An XML namespace declaration (subclass of
8
+ # Lutaml::Xml::W3c::XmlNamespace).
9
+ class Namespace
10
+ attr_accessor :class_name, :uri, :prefix_default, :element_form_default
11
+
12
+ def initialize(class_name:, uri:, prefix_default: nil,
13
+ element_form_default: nil)
14
+ @class_name = class_name
15
+ @uri = uri
16
+ @prefix_default = prefix_default
17
+ @element_form_default = element_form_default
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Definitions
7
+ # Restricted simple type — `class X < Lutaml::Model::Type::Y`
8
+ # with a cast body that applies facets and delegates to super.
9
+ class RestrictedType
10
+ attr_accessor :class_name, :parent_class, :facets,
11
+ :transform_facet, :required_files,
12
+ :keep_register_when_namespaced, :namespace_class_name
13
+
14
+ def initialize(class_name:, facets:, parent_class: nil,
15
+ transform_facet: nil, required_files: [],
16
+ keep_register_when_namespaced: false,
17
+ namespace_class_name: nil)
18
+ @class_name = class_name
19
+ @parent_class = parent_class
20
+ @facets = facets
21
+ @transform_facet = transform_facet
22
+ @required_files = required_files
23
+ @keep_register_when_namespaced = keep_register_when_namespaced
24
+ @namespace_class_name = namespace_class_name
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Definitions
7
+ # A `sequence do ... end` group. Transparent in attribute
8
+ # declarations; wraps with `sequence do` only in xml mappings.
9
+ class Sequence
10
+ attr_accessor :members
11
+
12
+ def initialize(members:)
13
+ @members = members
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Definitions
7
+ # XSD-only: a complex type with simple content (a typed text
8
+ # body plus optional decorating attributes). The renderer emits
9
+ # `attribute :content, :<base_type>` and any additional
10
+ # attributes after the regular members. RNG leaves this nil.
11
+ class SimpleContent
12
+ attr_accessor :base_class, :additional_attributes
13
+
14
+ def initialize(base_class:, additional_attributes: [])
15
+ @base_class = base_class
16
+ @additional_attributes = additional_attributes
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Definitions
7
+ # XSD-only value transform applied during cast. The expression
8
+ # is raw Ruby emitted as `value = <expression>` before super.
9
+ # E.g. "value.gsub(/[\\r\\n\\t]/, ' ')" or "value.upcase". RNG
10
+ # leaves this nil.
11
+ class TransformFacet
12
+ attr_accessor :expression
13
+
14
+ def initialize(expression:)
15
+ @expression = expression
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Definitions
7
+ # Reference to a Ruby type used in a generated attribute.
8
+ # kind ∈ {:symbol, :class_ref, :w3c}.
9
+ class TypeRef
10
+ attr_accessor :kind, :value
11
+
12
+ def initialize(kind:, value:)
13
+ @kind = kind
14
+ @value = value
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Definitions
7
+ # Union type — Lutaml::Model::Type::Value subclass whose cast
8
+ # tries each member type and returns the first that parses.
9
+ # cast_strategy ∈ {:resolve_type, :class_refs}.
10
+ class UnionType
11
+ attr_accessor :class_name, :members, :cast_strategy, :required_files,
12
+ :lazy_register, :keep_register_when_namespaced
13
+
14
+ def initialize(class_name:, members:, cast_strategy:,
15
+ required_files: [], lazy_register: false,
16
+ keep_register_when_namespaced: false)
17
+ @class_name = class_name
18
+ @members = members
19
+ @cast_strategy = cast_strategy
20
+ @required_files = required_files
21
+ @lazy_register = lazy_register
22
+ @keep_register_when_namespaced = keep_register_when_namespaced
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Definitions
7
+ # XML root directive of a generated class.
8
+ # kind ∈ {:element, :type_name, :fragment}. name is nil for :fragment.
9
+ class XmlRoot
10
+ attr_accessor :kind, :name
11
+
12
+ def initialize(kind:, name: nil)
13
+ @kind = kind
14
+ @name = name
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Definitions
7
+ autoload :GroupImport, "#{__dir__}/definitions/group_import"
8
+ autoload :MemberWalk, "#{__dir__}/definitions/member_walk"
9
+ autoload :TypeRef, "#{__dir__}/definitions/type_ref"
10
+ autoload :XmlRoot, "#{__dir__}/definitions/xml_root"
11
+ autoload :Attribute, "#{__dir__}/definitions/attribute"
12
+ autoload :Choice, "#{__dir__}/definitions/choice"
13
+ autoload :Sequence, "#{__dir__}/definitions/sequence"
14
+ autoload :Facet, "#{__dir__}/definitions/facet"
15
+ autoload :TransformFacet, "#{__dir__}/definitions/transform_facet"
16
+ autoload :SimpleContent, "#{__dir__}/definitions/simple_content"
17
+ autoload :Model, "#{__dir__}/definitions/model"
18
+ autoload :RestrictedType, "#{__dir__}/definitions/restricted_type"
19
+ autoload :UnionType, "#{__dir__}/definitions/union_type"
20
+ autoload :Namespace, "#{__dir__}/definitions/namespace"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Lutaml
6
+ module Model
7
+ module Schema
8
+ # Writes a CompiledOutput to disk: per-class .rb files in a module
9
+ # subdirectory plus a central `*_registry.rb` (when a module
10
+ # namespace is set). Shared between XSD and RNG compilers.
11
+ #
12
+ # The `registry_generator:` parameter selects which RegistryGenerator
13
+ # subclass to use (XSD has a 2-phase template; RNG uses the default).
14
+ class FileWriter
15
+ def self.write(output, dir, registry_generator: RegistryGenerator)
16
+ new(output, dir, registry_generator).write
17
+ end
18
+
19
+ def initialize(output, dir, registry_generator)
20
+ @output = output
21
+ @dir = dir
22
+ @registry_generator = registry_generator
23
+ end
24
+
25
+ def write
26
+ if @output.module_namespace
27
+ write_namespaced
28
+ else
29
+ write_flat
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def write_namespaced
36
+ module_path = @output.module_namespace.split("::").map(&:downcase).join("/")
37
+ full_dir = File.join(@dir, module_path)
38
+ FileUtils.mkdir_p(full_dir)
39
+
40
+ registry = @registry_generator.generate(
41
+ @output.models,
42
+ module_namespace: @output.module_namespace,
43
+ register_id: @output.register_id,
44
+ namespaces: @output.namespaces,
45
+ )
46
+ if registry
47
+ registry_name = module_path.split("/").last
48
+ File.write(File.join(@dir, "#{registry_name}_registry.rb"), registry)
49
+ end
50
+
51
+ write_sources_to(full_dir)
52
+ end
53
+
54
+ def write_flat
55
+ FileUtils.mkdir_p(@dir)
56
+ write_sources_to(@dir)
57
+ end
58
+
59
+ def write_sources_to(target_dir)
60
+ @output.sources.each { |name, src| write_source(target_dir, name, src) }
61
+ end
62
+
63
+ def write_source(target_dir, name, src)
64
+ file_name = Utils.snake_case(Utils.last_of_split(name.to_s))
65
+ File.write(File.join(target_dir, "#{file_name}.rb"), src)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -19,9 +19,23 @@ module Lutaml
19
19
  def process_attributes(collection, klass)
20
20
  register = extract_register_from(klass)
21
21
  klass.attributes.each_value do |attribute|
22
- next unless attribute.serializable?(register)
22
+ if attribute.union?
23
+ process_union_members(collection, attribute)
24
+ elsif attribute.serializable?(register)
25
+ process_attribute(collection, attribute, register)
26
+ end
27
+ end
28
+ end
29
+
30
+ # A union attribute's type is Type::Union, not a model, so collect
31
+ # definitions for each of its Serializable members directly (a
32
+ # union schema emits a $ref per model member).
33
+ def process_union_members(collection, attribute)
34
+ attribute.union_member_types.each do |member|
35
+ next unless member.is_a?(::Class) &&
36
+ member.include?(Lutaml::Model::Serialize)
23
37
 
24
- process_attribute(collection, attribute, register)
38
+ collection.merge(DefinitionsCollection.from_class(member))
25
39
  end
26
40
  end
27
41
 
@@ -24,6 +24,8 @@ module Lutaml
24
24
  inside_collection = options.fetch(:inside_collection, false)
25
25
  if attr.collection? && !inside_collection
26
26
  collection_schema(attr)
27
+ elsif attr.union?
28
+ union_schema(attr, include_null: include_null)
27
29
  elsif attr.serializable?(register) && polymorphic?(attr)
28
30
  polymorphic_schema(attr)
29
31
  elsif attr.serializable?(register)
@@ -55,6 +57,22 @@ module Lutaml
55
57
  schema["maxItems"] = range.end if range.end
56
58
  end
57
59
 
60
+ # Union: anyOf (valid if the value matches at least one member), not
61
+ # oneOf (which would wrongly demand exactly one). A null branch is
62
+ # added for nullable attributes, mirroring primitive_schema, so an
63
+ # optional union still accepts null.
64
+ def union_schema(attr, include_null: true)
65
+ members = attr.union_member_types.map do |member|
66
+ if member.include?(Lutaml::Model::Serialize)
67
+ Ref.new(member).to_schema
68
+ else
69
+ { "type" => get_type(member) }
70
+ end
71
+ end
72
+ members << { "type" => "null" } if include_null
73
+ { "anyOf" => members }.merge(get_constraints(attr))
74
+ end
75
+
58
76
  def polymorphic_schema(attr)
59
77
  ref_schemas = attr.options[:polymorphic].map do |type|
60
78
  Ref.new(type).to_schema
@@ -82,10 +100,9 @@ module Lutaml
82
100
  constraints["pattern"] = attr.pattern.source if attr.pattern
83
101
 
84
102
  # Add default value
85
- resolver = Lutaml::Model::Services::DefaultValueResolver.new(attr,
86
- register, nil)
87
- if resolver.default_set?
88
- constraints["default"] = resolver.default
103
+ resolved = attr.default_value(register, nil)
104
+ unless Lutaml::Model::Utils.uninitialized?(resolved)
105
+ constraints["default"] = attr.cast_value(resolved, register)
89
106
  end
90
107
 
91
108
  # Add enumeration values
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ # Stateless helpers for emitting `module Foo ... end` wrappers around
7
+ # generated code, given an Array of module-name segments.
8
+ #
9
+ # Both helpers return a single newline-terminated block (no trailing
10
+ # newline) so ERB templates should use `<%= ... -%>` to avoid
11
+ # doubling the line break.
12
+ module ModuleNesting
13
+ module_function
14
+
15
+ def opening(modules)
16
+ return "" if modules.empty?
17
+
18
+ modules.map.with_index { |m, i| "#{' ' * i}module #{m}\n" }.join
19
+ end
20
+
21
+ def closing(modules)
22
+ return "" if modules.empty?
23
+
24
+ modules.reverse.map.with_index do |_m, i|
25
+ "#{' ' * (modules.size - i - 1)}end\n"
26
+ end.join
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end