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,23 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ Multi-define grammar with cross-references. Exercises the "one class per
4
+ define" path with a ref-from-start and nested element references.
5
+ -->
6
+ <grammar xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
7
+ <start>
8
+ <ref name="Person"/>
9
+ </start>
10
+ <define name="Person">
11
+ <element name="Person">
12
+ <element name="name"><data type="string"/></element>
13
+ <element name="age"><data type="integer"/></element>
14
+ <ref name="Address"/>
15
+ </element>
16
+ </define>
17
+ <define name="Address">
18
+ <element name="Address">
19
+ <element name="street"><data type="string"/></element>
20
+ <element name="city"><data type="string"/></element>
21
+ </element>
22
+ </define>
23
+ </grammar>
@@ -0,0 +1,14 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <grammar xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
3
+ <start>
4
+ <element name="thing">
5
+ <element name="count"><ref name="IntOrString"/></element>
6
+ </element>
7
+ </start>
8
+ <define name="IntOrString">
9
+ <choice>
10
+ <data type="integer"/>
11
+ <data type="string"/>
12
+ </choice>
13
+ </define>
14
+ </grammar>
@@ -0,0 +1,13 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
3
+ xmlns="http://example.com/address"
4
+ targetNamespace="http://example.com/address"
5
+ elementFormDefault="qualified">
6
+ <xs:element name="address">
7
+ <xs:complexType>
8
+ <xs:sequence>
9
+ <xs:element name="city" type="xs:string"/>
10
+ </xs:sequence>
11
+ </xs:complexType>
12
+ </xs:element>
13
+ </xs:schema>
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
3
+ <xs:element name="contact">
4
+ <xs:complexType>
5
+ <xs:choice>
6
+ <xs:element name="email" type="xs:string"/>
7
+ <xs:element name="phone" type="xs:string"/>
8
+ </xs:choice>
9
+ </xs:complexType>
10
+ </xs:element>
11
+ </xs:schema>
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
3
+ <xs:element name="person">
4
+ <xs:complexType>
5
+ <xs:sequence>
6
+ <xs:element name="name" type="xs:string"/>
7
+ <xs:element name="age" type="xs:integer"/>
8
+ </xs:sequence>
9
+ </xs:complexType>
10
+ </xs:element>
11
+ </xs:schema>
@@ -0,0 +1,17 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
3
+ <xs:element name="person">
4
+ <xs:complexType>
5
+ <xs:sequence>
6
+ <xs:element name="name" type="xs:string"/>
7
+ <xs:element name="age">
8
+ <xs:simpleType>
9
+ <xs:restriction base="xs:integer">
10
+ <xs:maxInclusive value="150"/>
11
+ </xs:restriction>
12
+ </xs:simpleType>
13
+ </xs:element>
14
+ </xs:sequence>
15
+ </xs:complexType>
16
+ </xs:element>
17
+ </xs:schema>
@@ -0,0 +1,37 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
3
+ <xs:element name="product">
4
+ <xs:complexType>
5
+ <xs:sequence>
6
+ <xs:element name="sku">
7
+ <xs:simpleType>
8
+ <xs:restriction base="xs:string">
9
+ <xs:pattern value="[A-Z]{3}-[0-9]{4}"/>
10
+ </xs:restriction>
11
+ </xs:simpleType>
12
+ </xs:element>
13
+ <xs:element name="price" type="xs:decimal"/>
14
+ <xs:element name="released" type="xs:date"/>
15
+ <xs:element name="tag" type="xs:string" minOccurs="0" maxOccurs="3"/>
16
+ <xs:element name="note" type="xs:string" minOccurs="0"/>
17
+ <xs:element name="dimensions" minOccurs="0">
18
+ <xs:complexType>
19
+ <xs:sequence>
20
+ <xs:element name="width" type="xs:integer"/>
21
+ <xs:element name="height" type="xs:integer"/>
22
+ </xs:sequence>
23
+ </xs:complexType>
24
+ </xs:element>
25
+ </xs:sequence>
26
+ <xs:attribute name="id" type="xs:string" use="required"/>
27
+ <xs:attribute name="status">
28
+ <xs:simpleType>
29
+ <xs:restriction base="xs:string">
30
+ <xs:enumeration value="draft"/>
31
+ <xs:enumeration value="published"/>
32
+ </xs:restriction>
33
+ </xs:simpleType>
34
+ </xs:attribute>
35
+ </xs:complexType>
36
+ </xs:element>
37
+ </xs:schema>
@@ -0,0 +1,134 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
4
+ def attribute(type, **opts)
5
+ described_class.new("attr", type, **opts)
6
+ end
7
+
8
+ let(:uninit) { Lutaml::Model::UninitializedClass.instance }
9
+
10
+ describe "nil handling" do
11
+ it "maps nil through value_map[:nil] => :nil" do
12
+ expect(attribute(:string).apply_value_map(nil, { nil: :nil })).to be_nil
13
+ end
14
+
15
+ it "maps nil through value_map[:nil] => :empty to '' (scalar)" do
16
+ expect(attribute(:string).apply_value_map(nil, { nil: :empty })).to eq("")
17
+ end
18
+
19
+ it "maps nil through value_map[:nil] => :empty to [] (collection)" do
20
+ expect(
21
+ attribute(:string, collection: true).apply_value_map(nil, { nil: :empty }),
22
+ ).to eq([])
23
+ end
24
+
25
+ it "maps nil through value_map[:nil] => :empty to a custom collection class instance" do
26
+ # Regression: pre-consolidation Serialize::ValueMapping#empty_object used
27
+ # attr.build_collection (preserving custom collection class). The
28
+ # consolidated Attribute#apply_value_map must also use build_collection,
29
+ # not literal [], so attributes typed with a Lutaml::Model::Collection
30
+ # subclass don't silently downgrade to plain Array during model init.
31
+ member_class = Class.new(Lutaml::Model::Serializable) do
32
+ attribute :name, :string
33
+ end
34
+ custom_collection_class = Class.new(Lutaml::Model::Collection) do
35
+ instances :items, member_class
36
+ end
37
+ attr = described_class.new("items", member_class, collection: custom_collection_class)
38
+ result = attr.apply_value_map(nil, { nil: :empty })
39
+ expect(result).to be_a(custom_collection_class)
40
+ end
41
+
42
+ it "maps nil through value_map[:nil] => :omitted to UninitializedClass" do
43
+ expect(attribute(:string).apply_value_map(nil, { nil: :omitted })).to eq(uninit)
44
+ end
45
+ end
46
+
47
+ describe "empty handling" do
48
+ it "maps '' through value_map[:empty] => :nil" do
49
+ expect(attribute(:string).apply_value_map("", { empty: :nil })).to be_nil
50
+ end
51
+
52
+ it "maps '' through value_map[:empty] => :empty preserving '' for scalar" do
53
+ expect(attribute(:string).apply_value_map("", { empty: :empty })).to eq("")
54
+ end
55
+
56
+ it "maps [] through value_map[:empty] => :empty preserving [] for collection" do
57
+ expect(
58
+ attribute(:string, collection: true).apply_value_map([], { empty: :empty }),
59
+ ).to eq([])
60
+ end
61
+
62
+ it "maps {} through value_map[:empty] => :empty preserving {} for Hash attribute" do
63
+ # Regression: must preserve the input type when source key matches the
64
+ # :empty mapping. Original value_for_option used `empty_value ||
65
+ # empty_object(attr)` which returned the truthy {} unchanged.
66
+ expect(attribute(:hash).apply_value_map({}, { empty: :empty })).to eq({})
67
+ end
68
+ end
69
+
70
+ describe "omitted handling" do
71
+ it "maps UninitializedClass through value_map[:omitted] => :nil" do
72
+ expect(attribute(:string).apply_value_map(uninit, { omitted: :nil })).to be_nil
73
+ end
74
+
75
+ it "maps UninitializedClass through value_map[:omitted] => :empty" do
76
+ expect(attribute(:string).apply_value_map(uninit, { omitted: :empty })).to eq("")
77
+ end
78
+
79
+ it "does NOT route UninitializedClass through the :empty branch" do
80
+ # Regression: Utils.empty?(UninitializedClass.instance) is false per
81
+ # utils.rb:132-137. Ensure dispatch order matches the originals: uninit
82
+ # values go to :omitted, not :empty.
83
+ expect(
84
+ attribute(:string).apply_value_map(uninit, { empty: :nil, omitted: :empty }),
85
+ ).to eq("")
86
+ end
87
+ end
88
+
89
+ describe "Boolean bare-form (Boolean attribute type required)" do
90
+ it "returns false directly for Boolean attribute, value_map[:empty] = false" do
91
+ expect(attribute(:boolean).apply_value_map("", { empty: false })).to be(false)
92
+ end
93
+
94
+ it "returns true directly for Boolean attribute, value_map[:omitted] = true" do
95
+ expect(attribute(:boolean).apply_value_map(uninit, { omitted: true })).to be(true)
96
+ end
97
+
98
+ it "does NOT apply bare-boolean form to a non-Boolean attribute (documented tightening)" do
99
+ # Plan §"Hidden behavior change" — bare-boolean form requires Boolean type.
100
+ # Falls through to the :empty option dispatch, which for an unknown option
101
+ # returns UninitializedClass.instance.
102
+ result = attribute(:string).apply_value_map("", { empty: false })
103
+ expect(result).to eq(uninit)
104
+ end
105
+ end
106
+
107
+ describe "Boolean nested-form (value_map[:from][:empty] / [:omitted])" do
108
+ it "returns false from value_map[:from][:empty] unconditionally for Boolean attribute" do
109
+ expect(attribute(:boolean).apply_value_map("", { from: { empty: false } })).to be(false)
110
+ end
111
+
112
+ it "returns true from value_map[:from][:omitted] unconditionally for Boolean attribute" do
113
+ expect(attribute(:boolean).apply_value_map(uninit, { from: { omitted: true } })).to be(true)
114
+ end
115
+
116
+ it "returns false from nested form even for non-Boolean attribute (no type gate)" do
117
+ expect(attribute(:string).apply_value_map("", { from: { empty: false } })).to be(false)
118
+ end
119
+ end
120
+
121
+ describe "pass-through" do
122
+ it "returns the value untouched when present and non-empty" do
123
+ expect(attribute(:string).apply_value_map("hello", {})).to eq("hello")
124
+ end
125
+
126
+ it "returns Boolean false untouched (real value, not 'empty')" do
127
+ expect(attribute(:boolean).apply_value_map(false, {})).to be(false)
128
+ end
129
+
130
+ it "returns Boolean true untouched" do
131
+ expect(attribute(:boolean).apply_value_map(true, {})).to be(true)
132
+ end
133
+ end
134
+ end
@@ -216,9 +216,19 @@ RSpec.describe Lutaml::Model::Attribute do
216
216
  age_attr.send(:validate_options!, { pattern: /[A-Za-z ]/ })
217
217
  end.to raise_error(
218
218
  StandardError,
219
- "Invalid option `pattern` given for `age`, `pattern` is only allowed for :string type",
219
+ "Invalid option `pattern` given for `age`, `pattern` is only " \
220
+ "allowed for :string type or a union including a :string member",
220
221
  )
221
222
  end
223
+
224
+ it "allows pattern on a union that includes a :string member" do
225
+ attr = described_class.new(
226
+ "code", Lutaml::Model::Type::Union,
227
+ union_member_types: %i[integer string], pattern: /\A\d+\z/
228
+ )
229
+
230
+ expect(attr.pattern).to eq(/\A\d+\z/)
231
+ end
222
232
  end
223
233
 
224
234
  describe "#derived?" do
@@ -249,46 +259,193 @@ RSpec.describe Lutaml::Model::Attribute do
249
259
  end
250
260
  end
251
261
 
252
- describe "#default via DefaultValueResolver" do
262
+ describe "#default" do
253
263
  let(:register) { Lutaml::Model::Config.default_register }
254
-
255
- def create_resolver(attribute)
256
- Lutaml::Model::Services::DefaultValueResolver.new(attribute, register,
257
- nil)
258
- end
264
+ let(:instance) { nil }
259
265
 
260
266
  context "when default is not set" do
261
267
  let(:attribute) { described_class.new("name", :string) }
262
268
 
263
269
  it "returns uninitialized" do
264
- expect(create_resolver(attribute).default).to be(Lutaml::Model::UninitializedClass.instance)
270
+ expect(attribute.default(register, instance)).to be(Lutaml::Model::UninitializedClass.instance)
265
271
  end
266
272
  end
267
273
 
268
274
  context "when default is set as a proc" do
269
275
  it "returns the value" do
270
276
  attribute = described_class.new("name", :string, default: -> { "John" })
271
- expect(create_resolver(attribute).default).to eq("John")
277
+ expect(attribute.default(register, instance)).to eq("John")
272
278
  end
273
279
 
274
280
  it "returns the value casted to correct type" do
275
281
  file = Pathname.new("avatar.png")
276
282
  attribute = described_class.new("image", :string, default: -> { file })
277
283
 
278
- expect(create_resolver(attribute).default).to eq("avatar.png")
284
+ expect(attribute.default(register, instance)).to eq("avatar.png")
279
285
  end
280
286
  end
281
287
 
282
288
  context "when default is set as value" do
283
289
  it "returns the value" do
284
290
  attribute = described_class.new("name", :string, default: "John Doe")
285
- expect(create_resolver(attribute).default).to eq("John Doe")
291
+ expect(attribute.default(register, instance)).to eq("John Doe")
286
292
  end
287
293
 
288
294
  it "returns the value casted to correct type" do
289
295
  attribute = described_class.new("age", :integer, default: "24")
290
- expect(create_resolver(attribute).default).to eq(24)
296
+ expect(attribute.default(register, instance)).to eq(24)
297
+ end
298
+
299
+ it "casts a String default to Integer for :integer attribute" do
300
+ attribute = described_class.new("count", :integer, default: "42")
301
+ expect(attribute.default(register, instance)).to eq(42)
302
+ end
303
+ end
304
+
305
+ context "with instance context" do
306
+ # Exercise instance_exec: the proc reads `name` from the instance,
307
+ # which is only resolvable when the proc is executed in the
308
+ # instance's context (`instance_object.instance_exec(&proc)`).
309
+ let(:model_class) do
310
+ Class.new(Lutaml::Model::Serializable) do
311
+ attribute :name, :string
312
+ attribute :greeting, :string, default: -> { "Hello, #{name}" }
313
+ end
314
+ end
315
+
316
+ it "executes and casts the proc in instance context" do
317
+ instance = model_class.new(name: "Alice")
318
+ attribute = model_class.attributes[:greeting]
319
+ expect(attribute.default(register, instance)).to eq("Hello, Alice")
320
+ end
321
+
322
+ it "bypasses the default cache when instance_object is given" do
323
+ attribute = model_class.attributes[:greeting]
324
+ first = attribute.default(register, model_class.new(name: "Alice"))
325
+ second = attribute.default(register, model_class.new(name: "Bob"))
326
+ expect(first).to eq("Hello, Alice")
327
+ expect(second).to eq("Hello, Bob")
328
+ end
329
+ end
330
+ end
331
+
332
+ describe "#default_value" do
333
+ let(:register) { Lutaml::Model::Config.default_register }
334
+ let(:instance) { nil }
335
+
336
+ context "when default is a static value" do
337
+ it "returns the static value uncast" do
338
+ attribute = described_class.new("name", :string, default: "John")
339
+ expect(attribute.default_value(register, instance)).to eq("John")
340
+ end
341
+ end
342
+
343
+ context "when default is a proc without instance context" do
344
+ it "executes the proc" do
345
+ attribute = described_class.new("count", :integer, default: -> { 42 })
346
+ expect(attribute.default_value(register, instance)).to eq(42)
347
+ end
348
+ end
349
+
350
+ context "when default is a proc with instance context" do
351
+ let(:model_class) do
352
+ Class.new(Lutaml::Model::Serializable) do
353
+ attribute :name, :string
354
+ attribute :greeting, :string, default: -> { "Hello, #{name}" }
355
+ end
291
356
  end
357
+
358
+ it "executes the proc in the instance context (reads instance attribute)" do
359
+ instance = model_class.new(name: "Alice")
360
+ attribute = model_class.attributes[:greeting]
361
+ expect(attribute.default_value(register, instance)).to eq("Hello, Alice")
362
+ end
363
+ end
364
+ end
365
+
366
+ describe "#default_set?" do
367
+ let(:register) { Lutaml::Model::Config.default_register }
368
+ let(:instance) { nil }
369
+
370
+ it "returns false when default is not set" do
371
+ attribute = described_class.new("name", :string)
372
+ expect(attribute.default_set?(register, instance)).to be(false)
373
+ end
374
+
375
+ it "returns true when default is a static value" do
376
+ attribute = described_class.new("name", :string, default: "John")
377
+ expect(attribute.default_set?(register, instance)).to be(true)
378
+ end
379
+
380
+ it "returns true when default is a proc" do
381
+ attribute = described_class.new("name", :string, default: -> { "John" })
382
+ expect(attribute.default_set?(register, instance)).to be(true)
383
+ end
384
+ end
385
+
386
+ describe "#validate_value!" do
387
+ let(:register) { Lutaml::Model::Config.default_register }
388
+ let(:model) { Class.new(Lutaml::Model::Serializable).new }
389
+
390
+ context "when value is nil and the attribute has no default" do
391
+ it "treats nil-no-default as a permissible non-required value" do
392
+ attr = described_class.new("name", :string)
393
+ expect { attr.validate_value!(nil, register, instance_object: model) }
394
+ .not_to raise_error
395
+ end
396
+ end
397
+
398
+ context "when value is nil and the attribute has a default" do
399
+ it "uses the default value for validation" do
400
+ attr = described_class.new("name", :string, default: "John")
401
+ expect { attr.validate_value!(nil, register, instance_object: model) }
402
+ .not_to raise_error
403
+ end
404
+
405
+ it "executes a proc default in the instance context (instance_exec preserved)" do
406
+ # Regression: validate_value! must thread instance_object through
407
+ # default(register, instance_object) which calls instance_exec on
408
+ # the Proc. Verify by having the Proc read an instance attribute.
409
+ model_class = Class.new(Lutaml::Model::Serializable) do
410
+ attribute :prefix, :string, values: %w[Mr Ms Dr]
411
+ attribute :name, :string, values: %w[Alice Bob],
412
+ default: -> { "#{prefix}_default" }
413
+ end
414
+ instance = model_class.new(prefix: "Dr")
415
+ attr = model_class.attributes[:name]
416
+ expect { attr.validate_value!(nil, register, instance_object: instance) }
417
+ .to raise_error(Lutaml::Model::InvalidValueError) do |error|
418
+ # The error carries the value that was used for validation.
419
+ # If instance_exec ran, value is "Dr_default"; if not, error
420
+ # would never fire (proc would crash before reaching validation).
421
+ expect(error.message).to include("Dr_default")
422
+ end
423
+ end
424
+ end
425
+
426
+ context "when value is uninitialized" do
427
+ it "short-circuits without raising" do
428
+ uninit = Lutaml::Model::UninitializedClass.instance
429
+ attr = described_class.new("name", :string)
430
+ expect { attr.validate_value!(uninit, register, instance_object: model) }
431
+ .not_to raise_error
432
+ end
433
+ end
434
+ end
435
+
436
+ describe "Model initialization with proc defaults" do
437
+ # Regression: Serialize#determine_value previously called default_set?
438
+ # + default separately, executing the Proc twice for each omitted
439
+ # attribute. Verify single-execution by counting side effects.
440
+ it "executes a proc default exactly once when the attribute is omitted" do
441
+ counter = 0
442
+ model_class = Class.new(Lutaml::Model::Serializable) do
443
+ attribute :count, :integer, default: -> { counter += 1 }
444
+ end
445
+
446
+ instance = model_class.new
447
+ expect(instance.count).to eq(1)
448
+ expect(counter).to eq(1)
292
449
  end
293
450
  end
294
451
 
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "tmpdir"
5
+
6
+ # Guard: the generated Opal boot manifest must stay in sync with the autoloads
7
+ # in lib/lutaml/**. Opal ignores `autoload`, so lutaml_model_boot.rb explicitly
8
+ # requires every autoload target; a deleted/renamed file leaves a stale require
9
+ # that only fails the Opal CI job (`rake spec:opal`) — MRI's autoload hides it.
10
+ # Regenerating here and comparing catches that drift in the fast MRI suite.
11
+ RSpec.describe "Opal boot manifest" do
12
+ let(:generator) do
13
+ File.expand_path("../../../lib/compat/opal/generate_boot.rb", __dir__)
14
+ end
15
+ let(:manifest) do
16
+ File.expand_path("../../../lib/compat/opal/lutaml_model_boot.rb", __dir__)
17
+ end
18
+
19
+ it "matches the generator output (regenerate: ruby lib/compat/opal/generate_boot.rb)" do
20
+ Dir.mktmpdir do |dir|
21
+ out = File.join(dir, "boot.rb")
22
+ ok = system({ "OPAL_BOOT_OUTPUT" => out }, RbConfig.ruby, generator,
23
+ out: File::NULL, err: File::NULL)
24
+ expect(ok).to be(true), "generate_boot.rb failed to run"
25
+ expect(File.read(manifest)).to eq(File.read(out)),
26
+ "lib/compat/opal/lutaml_model_boot.rb is out of date — run " \
27
+ "`ruby lib/compat/opal/generate_boot.rb` after adding/removing autoloads"
28
+ end
29
+ end
30
+ end
@@ -323,6 +323,24 @@ RSpec.describe Lutaml::Model::Collection do
323
323
  end
324
324
  end
325
325
 
326
+ context "when a range collection allows zero elements" do
327
+ it "validates when the collection is given as empty" do
328
+ kiln = CollectionTests::Kiln.new(attributes.merge(pots: []))
329
+
330
+ expect { kiln.validate! }.not_to raise_error
331
+ end
332
+
333
+ it "validates when the collection is omitted entirely" do
334
+ kiln = CollectionTests::Kiln.new(attributes.except(:pots))
335
+
336
+ expect { kiln.validate! }.not_to raise_error
337
+ end
338
+
339
+ it "does not flag the empty min-zero collection when built with no arguments" do
340
+ expect { CollectionTests::Kiln.new.validate! }.not_to raise_error
341
+ end
342
+ end
343
+
326
344
  context "when collection with unbounded maximum exceeds minimum" do
327
345
  let(:valid_attributes) do
328
346
  attributes.merge(operators: ["John", "Jane", "Jim", "Jessica"])