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,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model/schema/definitions"
5
+ require "lutaml/model/schema/renderers/model"
6
+
7
+ RSpec.describe Lutaml::Model::Schema::Renderers::Model do
8
+ let(:defs) { Lutaml::Model::Schema::Definitions }
9
+
10
+ it "renders a rooted element with one element attribute" do
11
+ spec = defs::Model.new(
12
+ class_name: "Person",
13
+ xml_root: defs::XmlRoot.new(kind: :element, name: "Person"),
14
+ members: [
15
+ defs::Attribute.new(
16
+ name: "name",
17
+ type: defs::TypeRef.new(kind: :symbol, value: "string"),
18
+ xml_name: "name",
19
+ kind: :element,
20
+ ),
21
+ ],
22
+ )
23
+
24
+ output = described_class.render(spec)
25
+
26
+ expect(output).to include("class Person < Lutaml::Model::Serializable")
27
+ expect(output).to include("attribute :name, :string")
28
+ expect(output).to include("xml do")
29
+ expect(output).to include('element "Person"')
30
+ expect(output).to include('map_element "name", to: :name')
31
+ expect(output).to include("def self.register")
32
+ expect(output).to include("Person.register_class_with_id")
33
+ end
34
+
35
+ it "renders a fragment (no element / type_name directive)" do
36
+ spec = defs::Model.new(
37
+ class_name: "Inner",
38
+ xml_root: defs::XmlRoot.new(kind: :fragment, name: nil),
39
+ )
40
+
41
+ output = described_class.render(spec)
42
+
43
+ expect(output).not_to include("element ")
44
+ expect(output).not_to include("type_name ")
45
+ end
46
+
47
+ it "renders an attribute mapping with map_attribute" do
48
+ spec = defs::Model.new(
49
+ class_name: "Item",
50
+ xml_root: defs::XmlRoot.new(kind: :element, name: "Item"),
51
+ members: [
52
+ defs::Attribute.new(
53
+ name: "id",
54
+ type: defs::TypeRef.new(kind: :symbol, value: "string"),
55
+ xml_name: "id",
56
+ kind: :attribute,
57
+ ),
58
+ ],
59
+ )
60
+
61
+ output = described_class.render(spec)
62
+
63
+ expect(output).to include('map_attribute "id", to: :id')
64
+ end
65
+
66
+ it "renders a class reference type with no leading colon" do
67
+ spec = defs::Model.new(
68
+ class_name: "Owner",
69
+ xml_root: defs::XmlRoot.new(kind: :element, name: "Owner"),
70
+ members: [
71
+ defs::Attribute.new(
72
+ name: "address",
73
+ type: defs::TypeRef.new(kind: :class_ref, value: "Address"),
74
+ xml_name: "address",
75
+ kind: :element,
76
+ ),
77
+ ],
78
+ )
79
+
80
+ output = described_class.render(spec)
81
+
82
+ expect(output).to include("attribute :address, Address")
83
+ end
84
+
85
+ it "renders a w3c type with the ::-prefixed reference" do
86
+ spec = defs::Model.new(
87
+ class_name: "Para",
88
+ xml_root: defs::XmlRoot.new(kind: :element, name: "Para"),
89
+ members: [
90
+ defs::Attribute.new(
91
+ name: "lang",
92
+ type: defs::TypeRef.new(kind: :w3c, value: "Lutaml::Xml::W3c::Lang"),
93
+ xml_name: "lang",
94
+ kind: :attribute,
95
+ ),
96
+ ],
97
+ )
98
+
99
+ output = described_class.render(spec)
100
+
101
+ expect(output).to include("attribute :lang, ::Lutaml::Xml::W3c::Lang")
102
+ end
103
+
104
+ it "skips register methods inside a module_namespace" do
105
+ spec = defs::Model.new(
106
+ class_name: "Inside",
107
+ xml_root: defs::XmlRoot.new(kind: :element, name: "Inside"),
108
+ )
109
+
110
+ output = described_class.render(spec, module_namespace: "Mod::Sub")
111
+
112
+ expect(output).to include("module Mod")
113
+ expect(output).to include("module Sub")
114
+ expect(output).not_to include("def self.register")
115
+ expect(output).not_to include("register_class_with_id")
116
+ end
117
+
118
+ it "renders a choice block with attributes inside" do
119
+ spec = defs::Model.new(
120
+ class_name: "Either",
121
+ xml_root: defs::XmlRoot.new(kind: :element, name: "Either"),
122
+ members: [
123
+ defs::Choice.new(
124
+ header: "choice",
125
+ alternatives: [
126
+ defs::Attribute.new(
127
+ name: "alpha",
128
+ type: defs::TypeRef.new(kind: :symbol, value: "string"),
129
+ xml_name: "alpha",
130
+ kind: :element,
131
+ ),
132
+ defs::Attribute.new(
133
+ name: "beta",
134
+ type: defs::TypeRef.new(kind: :symbol, value: "string"),
135
+ xml_name: "beta",
136
+ kind: :element,
137
+ ),
138
+ ],
139
+ ),
140
+ ],
141
+ )
142
+
143
+ output = described_class.render(spec)
144
+
145
+ expect(output).to include("choice do")
146
+ expect(output).to include("attribute :alpha, :string")
147
+ expect(output).to include("attribute :beta, :string")
148
+ end
149
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model/schema/definitions"
5
+ require "lutaml/model/schema/renderers/namespace"
6
+
7
+ RSpec.describe Lutaml::Model::Schema::Renderers::Namespace do
8
+ let(:defs) { Lutaml::Model::Schema::Definitions }
9
+
10
+ it "renders a namespace class with uri and prefix_default" do
11
+ spec = defs::Namespace.new(
12
+ class_name: "ExampleNs",
13
+ uri: "http://example.com/ns",
14
+ prefix_default: "ex",
15
+ )
16
+
17
+ output = described_class.render(spec)
18
+
19
+ expect(output).to include("class ExampleNs < Lutaml::Xml::W3c::XmlNamespace")
20
+ expect(output).to include(%(uri "http://example.com/ns"))
21
+ expect(output).to include(%(prefix_default "ex"))
22
+ end
23
+
24
+ it "wraps in module when module_namespace is supplied" do
25
+ spec = defs::Namespace.new(
26
+ class_name: "Inner",
27
+ uri: "http://example.com/inner",
28
+ prefix_default: nil,
29
+ )
30
+
31
+ output = described_class.render(spec, module_namespace: "Mod::Sub")
32
+
33
+ expect(output).to include("module Mod")
34
+ expect(output).to include("module Sub")
35
+ end
36
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model/schema/definitions"
5
+ require "lutaml/model/schema/renderers/restricted_type"
6
+
7
+ RSpec.describe Lutaml::Model::Schema::Renderers::RestrictedType do
8
+ let(:defs) { Lutaml::Model::Schema::Definitions }
9
+
10
+ it "renders min/max facets in the cast body" do
11
+ spec = defs::RestrictedType.new(
12
+ class_name: "PositiveInt",
13
+ parent_class: "Lutaml::Model::Type::Integer",
14
+ facets: defs::Facet.new(min_inclusive: 1, max_inclusive: 100),
15
+ )
16
+
17
+ output = described_class.render(spec)
18
+
19
+ expect(output).to include("class PositiveInt < Lutaml::Model::Type::Integer")
20
+ expect(output).to include("def self.cast(value, options = {})")
21
+ expect(output).to include("options[:min] = 1")
22
+ expect(output).to include("options[:max] = 100")
23
+ expect(output).to include("value = super(value, options)")
24
+ end
25
+
26
+ it "renders enumerations facet" do
27
+ spec = defs::RestrictedType.new(
28
+ class_name: "Color",
29
+ parent_class: "Lutaml::Model::Type::String",
30
+ facets: defs::Facet.new(enumerations: %w[red green blue]),
31
+ )
32
+
33
+ output = described_class.render(spec)
34
+
35
+ expect(output).to include('options[:values] = [super("red"), super("green"), super("blue")]')
36
+ expect(output).not_to include("options[:min]")
37
+ end
38
+
39
+ it "renders pattern facet as a regexp literal" do
40
+ spec = defs::RestrictedType.new(
41
+ class_name: "Slug",
42
+ parent_class: "Lutaml::Model::Type::String",
43
+ facets: defs::Facet.new(pattern: "[a-z0-9-]+"),
44
+ )
45
+
46
+ output = described_class.render(spec)
47
+
48
+ expect(output).to include("options[:pattern] = %r{[a-z0-9-]+}")
49
+ end
50
+
51
+ it "renders transform facet" do
52
+ spec = defs::RestrictedType.new(
53
+ class_name: "Upper",
54
+ parent_class: "Lutaml::Model::Type::String",
55
+ facets: defs::Facet.new,
56
+ transform_facet: defs::TransformFacet.new(expression: "value.upcase"),
57
+ )
58
+
59
+ output = described_class.render(spec)
60
+
61
+ expect(output).to include("value = value.upcase")
62
+ end
63
+
64
+ it "uses lazy register memoisation" do
65
+ spec = defs::RestrictedType.new(
66
+ class_name: "Tiny",
67
+ parent_class: "Lutaml::Model::Type::Integer",
68
+ facets: defs::Facet.new(max_inclusive: 5),
69
+ )
70
+
71
+ output = described_class.render(spec)
72
+
73
+ expect(output).to include("@register ||= Lutaml::Model::Config.default_register")
74
+ end
75
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model/schema/definitions"
5
+ require "lutaml/model/schema/renderers/union"
6
+
7
+ RSpec.describe Lutaml::Model::Schema::Renderers::Union do
8
+ let(:defs) { Lutaml::Model::Schema::Definitions }
9
+
10
+ it "renders the :class_refs strategy as an each/begin/rescue loop" do
11
+ spec = defs::UnionType.new(
12
+ class_name: "NumberOrString",
13
+ members: [
14
+ defs::TypeRef.new(kind: :class_ref, value: "Integer"),
15
+ defs::TypeRef.new(kind: :class_ref, value: "String"),
16
+ ],
17
+ cast_strategy: :class_refs,
18
+ )
19
+
20
+ output = described_class.render(spec)
21
+
22
+ expect(output).to include("class NumberOrString < Lutaml::Model::Type::Value")
23
+ expect(output).to include("[Integer, String].each do |t|")
24
+ expect(output).to include("casted = t.cast(value, options)")
25
+ expect(output).to include("return casted unless casted.nil?")
26
+ expect(output).to include("rescue StandardError")
27
+ end
28
+
29
+ it "renders the :resolve_type strategy as a ||-chained resolve_type call" do
30
+ spec = defs::UnionType.new(
31
+ class_name: "IntegerOrToken",
32
+ members: [
33
+ defs::TypeRef.new(kind: :symbol, value: "integer"),
34
+ defs::TypeRef.new(kind: :symbol, value: "token"),
35
+ ],
36
+ cast_strategy: :resolve_type,
37
+ )
38
+
39
+ output = described_class.render(spec)
40
+
41
+ expect(output).to include("Lutaml::Model::GlobalContext.resolve_type(:integer, @register).cast(value, options)")
42
+ expect(output).to include("Lutaml::Model::GlobalContext.resolve_type(:token, @register).cast(value, options)")
43
+ expect(output).to include(" ||\n")
44
+ end
45
+
46
+ it "emits required_files lines above the module wrap" do
47
+ spec = defs::UnionType.new(
48
+ class_name: "AnyOf",
49
+ members: [defs::TypeRef.new(kind: :class_ref, value: "Integer")],
50
+ cast_strategy: :class_refs,
51
+ required_files: ['require_relative "integer"'],
52
+ )
53
+
54
+ output = described_class.render(spec)
55
+
56
+ expect(output).to include('require_relative "integer"')
57
+ end
58
+ end
@@ -0,0 +1,64 @@
1
+ require "spec_helper"
2
+
3
+ # Regression: Transform#apply_value_map must handle the nil-attr case used
4
+ # by custom-method-only key-value rules. Pre-consolidation behavior:
5
+ # the default { omitted: :nil, nil: :nil, empty: :empty } map converted
6
+ # UninitializedClass.instance to nil so the downstream Utils.present? guard
7
+ # in KeyValue::Transform#process_mapping_rule would skip the custom method
8
+ # for absent input keys.
9
+ RSpec.describe Lutaml::Model::Transform, "#apply_value_map (nil attr)" do
10
+ let(:transform_subclass) do
11
+ Class.new(described_class) do
12
+ # Expose the protected delegator for direct testing.
13
+ public :apply_value_map
14
+ end
15
+ end
16
+
17
+ let(:context) { Class.new(Lutaml::Model::Serializable) }
18
+ let(:register) { Lutaml::Model::Config.default_register }
19
+ let(:transform) { transform_subclass.new(context, register) }
20
+
21
+ let(:uninit) { Lutaml::Model::UninitializedClass.instance }
22
+ let(:default_vmap) { { omitted: :nil, nil: :nil, empty: :empty } }
23
+
24
+ describe "default mapping with nil attr" do
25
+ it "converts UninitializedClass to nil via the :omitted => :nil mapping" do
26
+ # Before the fix: returned uninit unchanged, causing Utils.present? to
27
+ # be true and the custom method to run for absent fields.
28
+ expect(transform.apply_value_map(uninit, default_vmap, nil)).to be_nil
29
+ end
30
+
31
+ it "converts nil to nil via the :nil => :nil mapping" do
32
+ expect(transform.apply_value_map(nil, default_vmap, nil)).to be_nil
33
+ end
34
+
35
+ it "passes initialized values through unchanged" do
36
+ expect(transform.apply_value_map("hello", default_vmap, nil)).to eq("hello")
37
+ end
38
+ end
39
+
40
+ describe "explicit :omitted => :omitted with nil attr" do
41
+ let(:vmap) { { omitted: :omitted, nil: :nil, empty: :empty } }
42
+
43
+ it "preserves UninitializedClass for explicit omitted: :omitted" do
44
+ expect(transform.apply_value_map(uninit, vmap, nil)).to eq(uninit)
45
+ end
46
+ end
47
+
48
+ # Behavior-preservation: the pre-consolidation attribute-less path mapped the
49
+ # :omitted option to UninitializedClass.instance (present? => true, so the
50
+ # custom method runs), and resolved :nil/:empty consistently for empty inputs.
51
+ describe "non-:nil options with nil attr" do
52
+ it "maps a nil value to uninitialized via nil: :omitted" do
53
+ expect(transform.apply_value_map(nil, { nil: :omitted }, nil)).to eq(uninit)
54
+ end
55
+
56
+ it "maps an empty value to uninitialized via empty: :omitted" do
57
+ expect(transform.apply_value_map("", { empty: :omitted }, nil)).to eq(uninit)
58
+ end
59
+
60
+ it "maps an empty value to nil via empty: :nil" do
61
+ expect(transform.apply_value_map("", { empty: :nil }, nil)).to be_nil
62
+ end
63
+ end
64
+ end