lutaml-model 0.8.16 → 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 (230) 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/.github/workflows/release.yml +0 -3
  9. data/.gitmodules +6 -0
  10. data/.rubocop_todo.yml +76 -12
  11. data/Gemfile +16 -1
  12. data/README.adoc +267 -0
  13. data/Rakefile +166 -4
  14. data/docs/_guides/schema-import.adoc +42 -0
  15. data/docs/_guides/xml/namespace-semantics.adoc +2 -0
  16. data/docs/_guides/xml-mapping.adoc +21 -0
  17. data/docs/_guides/xml-namespace-qualification.adoc +142 -0
  18. data/docs/_pages/validation.adoc +65 -0
  19. data/docs/_tutorials/xml-element-attribute-namespace-guide.adoc +2 -0
  20. data/docs/_tutorials/xml-schema-primer-style-guide.adoc +2 -0
  21. data/docs/namespace-management.adoc +2 -0
  22. data/docs/xml-schema-qualification.md +6 -0
  23. data/lib/compat/opal/generate_boot.rb +123 -0
  24. data/lib/compat/opal/js_bundle_entry.rb +42 -0
  25. data/lib/compat/opal/lutaml_model_boot.rb +497 -0
  26. data/lib/compat/opal/moxml_boot.rb +68 -0
  27. data/lib/compat/opal/yaml_compat.rb +32 -0
  28. data/lib/lutaml/json.rb +1 -1
  29. data/lib/lutaml/jsonld.rb +1 -4
  30. data/lib/lutaml/key_value/transform.rb +3 -8
  31. data/lib/lutaml/key_value/transformation/value_serializer.rb +14 -1
  32. data/lib/lutaml/key_value/transformation.rb +17 -5
  33. data/lib/lutaml/model/adapter_resolver.rb +2 -2
  34. data/lib/lutaml/model/attribute.rb +88 -11
  35. data/lib/lutaml/model/cached_type_resolver.rb +1 -1
  36. data/lib/lutaml/model/choice.rb +34 -0
  37. data/lib/lutaml/model/compiled_rule.rb +7 -0
  38. data/lib/lutaml/model/error/union_schema_unsupported_error.rb +11 -0
  39. data/lib/lutaml/model/global_context.rb +11 -8
  40. data/lib/lutaml/model/instrumentation.rb +2 -2
  41. data/lib/lutaml/model/mapping/mapping_rule.rb +0 -6
  42. data/lib/lutaml/model/register.rb +2 -2
  43. data/lib/lutaml/model/runtime_compatibility.rb +50 -0
  44. data/lib/lutaml/model/schema/class_loader.rb +58 -0
  45. data/lib/lutaml/model/schema/compiled_output.rb +83 -0
  46. data/lib/lutaml/model/schema/definitions/attribute.rb +32 -0
  47. data/lib/lutaml/model/schema/definitions/choice.rb +20 -0
  48. data/lib/lutaml/model/schema/definitions/facet.rb +40 -0
  49. data/lib/lutaml/model/schema/definitions/group_import.rb +22 -0
  50. data/lib/lutaml/model/schema/definitions/member_walk.rb +33 -0
  51. data/lib/lutaml/model/schema/definitions/model.rb +40 -0
  52. data/lib/lutaml/model/schema/definitions/namespace.rb +23 -0
  53. data/lib/lutaml/model/schema/definitions/restricted_type.rb +30 -0
  54. data/lib/lutaml/model/schema/definitions/sequence.rb +19 -0
  55. data/lib/lutaml/model/schema/definitions/simple_content.rb +22 -0
  56. data/lib/lutaml/model/schema/definitions/transform_facet.rb +21 -0
  57. data/lib/lutaml/model/schema/definitions/type_ref.rb +20 -0
  58. data/lib/lutaml/model/schema/definitions/union_type.rb +28 -0
  59. data/lib/lutaml/model/schema/definitions/xml_root.rb +20 -0
  60. data/lib/lutaml/model/schema/definitions.rb +24 -0
  61. data/lib/lutaml/model/schema/file_writer.rb +70 -0
  62. data/lib/lutaml/model/schema/generator/definitions_collection.rb +16 -2
  63. data/lib/lutaml/model/schema/generator/property.rb +21 -4
  64. data/lib/lutaml/model/schema/module_nesting.rb +31 -0
  65. data/lib/lutaml/model/schema/namespace_naming.rb +46 -0
  66. data/lib/lutaml/model/schema/registry_generator.rb +115 -0
  67. data/lib/lutaml/model/schema/renderers/base.rb +40 -0
  68. data/lib/lutaml/model/schema/renderers/mappings.rb +72 -0
  69. data/lib/lutaml/model/schema/renderers/member_decls.rb +115 -0
  70. data/lib/lutaml/model/schema/renderers/model.rb +121 -0
  71. data/lib/lutaml/model/schema/renderers/namespace.rb +30 -0
  72. data/lib/lutaml/model/schema/renderers/registration.rb +64 -0
  73. data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +93 -0
  74. data/lib/lutaml/model/schema/renderers/restricted_type.rb +88 -0
  75. data/lib/lutaml/model/schema/renderers/union.rb +83 -0
  76. data/lib/lutaml/model/schema/renderers.rb +19 -0
  77. data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +47 -0
  78. data/lib/lutaml/model/schema/rnc_compiler.rb +62 -0
  79. data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +91 -0
  80. data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +392 -0
  81. data/lib/lutaml/model/schema/rng_compiler/member_collector.rb +30 -0
  82. data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +152 -0
  83. data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +125 -0
  84. data/lib/lutaml/model/schema/rng_compiler.rb +220 -0
  85. data/lib/lutaml/model/schema/templates.rb +162 -0
  86. data/lib/lutaml/model/schema/xml_compiler/element_order.rb +41 -0
  87. data/lib/lutaml/model/schema/xml_compiler/registry_generator.rb +31 -107
  88. data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +191 -0
  89. data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +216 -0
  90. data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +153 -0
  91. data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +167 -0
  92. data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +87 -0
  93. data/lib/lutaml/model/schema/xml_compiler.rb +38 -517
  94. data/lib/lutaml/model/schema.rb +19 -0
  95. data/lib/lutaml/model/serialize/attribute_definition.rb +14 -2
  96. data/lib/lutaml/model/serialize/builder.rb +15 -2
  97. data/lib/lutaml/model/serialize/value_mapping.rb +9 -57
  98. data/lib/lutaml/model/serialize.rb +6 -9
  99. data/lib/lutaml/model/services/rule_value_extractor.rb +2 -3
  100. data/lib/lutaml/model/services.rb +0 -2
  101. data/lib/lutaml/model/store.rb +2 -1
  102. data/lib/lutaml/model/toml.rb +1 -1
  103. data/lib/lutaml/model/transform.rb +17 -39
  104. data/lib/lutaml/model/transformation_registry.rb +1 -1
  105. data/lib/lutaml/model/type.rb +1 -0
  106. data/lib/lutaml/model/union.rb +320 -0
  107. data/lib/lutaml/model/utils.rb +6 -0
  108. data/lib/lutaml/model/validation.rb +1 -4
  109. data/lib/lutaml/model/version.rb +1 -1
  110. data/lib/lutaml/model.rb +19 -0
  111. data/lib/lutaml/{jsonld → rdf}/context.rb +1 -1
  112. data/lib/lutaml/{jsonld/transform.rb → rdf/linked_data_transform.rb} +33 -35
  113. data/lib/lutaml/{jsonld → rdf}/term_definition.rb +1 -1
  114. data/lib/lutaml/rdf.rb +3 -0
  115. data/lib/lutaml/toml.rb +1 -1
  116. data/lib/lutaml/xml/adapter.rb +1 -1
  117. data/lib/lutaml/xml/adapter_element.rb +2 -2
  118. data/lib/lutaml/xml/adapter_loader.rb +1 -1
  119. data/lib/lutaml/xml/builder/base.rb +1 -1
  120. data/lib/lutaml/xml/builder.rb +1 -1
  121. data/lib/lutaml/xml/error/schema_validation_error.rb +25 -0
  122. data/lib/lutaml/xml/model_transform.rb +5 -1
  123. data/lib/lutaml/xml/schema/relaxng_schema.rb +5 -0
  124. data/lib/lutaml/xml/schema/xsd_schema.rb +4 -0
  125. data/lib/lutaml/xml/serialization/format_conversion.rb +48 -0
  126. data/lib/lutaml/xml/serialization/instance_methods.rb +13 -0
  127. data/lib/lutaml/xml/transformation/element_builder.rb +59 -31
  128. data/lib/lutaml/xml/transformation/ordered_applier.rb +6 -0
  129. data/lib/lutaml/xml/xsd_validator.rb +66 -0
  130. data/lib/lutaml/xml.rb +53 -12
  131. data/lib/lutaml/yamlld/adapter.rb +25 -0
  132. data/lib/lutaml/yamlld.rb +25 -0
  133. data/lutaml-model.gemspec +4 -2
  134. data/spec/fixtures/xml/schema/rnc/address_book.rnc +15 -0
  135. data/spec/fixtures/xml/schema/rnc/book_features.rnc +18 -0
  136. data/spec/fixtures/xml/schema/rnc/includes/book.rnc +6 -0
  137. data/spec/fixtures/xml/schema/rnc/includes/circular_a.rnc +8 -0
  138. data/spec/fixtures/xml/schema/rnc/includes/circular_b.rnc +8 -0
  139. data/spec/fixtures/xml/schema/rnc/includes/library.rnc +8 -0
  140. data/spec/fixtures/xml/schema/rng/address_book.rng +16 -0
  141. data/spec/fixtures/xml/schema/rng/book.rng +27 -0
  142. data/spec/fixtures/xml/schema/rng/fixed_value.rng +11 -0
  143. data/spec/fixtures/xml/schema/rng/fragment_in_choice.rng +30 -0
  144. data/spec/fixtures/xml/schema/rng/full_name.rng +39 -0
  145. data/spec/fixtures/xml/schema/rng/integer_range.rng +32 -0
  146. data/spec/fixtures/xml/schema/rng/list_type.rng +12 -0
  147. data/spec/fixtures/xml/schema/rng/namespaced.rng +9 -0
  148. data/spec/fixtures/xml/schema/rng/paragraph.rng +13 -0
  149. data/spec/fixtures/xml/schema/rng/person.rng +23 -0
  150. data/spec/fixtures/xml/schema/rng/union.rng +14 -0
  151. data/spec/fixtures/xml/validate_xml_with_address.xsd +13 -0
  152. data/spec/fixtures/xml/validate_xml_with_contact.xsd +11 -0
  153. data/spec/fixtures/xml/validate_xml_with_person.xsd +11 -0
  154. data/spec/fixtures/xml/validate_xml_with_person_strict.xsd +17 -0
  155. data/spec/fixtures/xml/validate_xml_with_product.xsd +37 -0
  156. data/spec/lutaml/integration/multi_format_spec.rb +23 -0
  157. data/spec/lutaml/model/attribute_apply_value_map_spec.rb +134 -0
  158. data/spec/lutaml/model/attribute_spec.rb +177 -13
  159. data/spec/lutaml/model/boot_manifest_spec.rb +30 -0
  160. data/spec/lutaml/model/choice_restrict_spec.rb +225 -0
  161. data/spec/lutaml/model/collection_spec.rb +18 -0
  162. data/spec/lutaml/model/custom_model_spec.rb +4 -4
  163. data/spec/lutaml/model/mixed_content_spec.rb +173 -2
  164. data/spec/lutaml/model/multiple_mapping_spec.rb +4 -4
  165. data/spec/lutaml/model/opal_smoke_spec.rb +67 -2
  166. data/spec/lutaml/model/ordered_content_spec.rb +33 -3
  167. data/spec/lutaml/model/raw_element_spec.rb +125 -0
  168. data/spec/lutaml/model/rule_value_extractor_spec.rb +7 -14
  169. data/spec/lutaml/model/runtime_compatibility_spec.rb +25 -0
  170. data/spec/lutaml/model/schema/renderers/model_spec.rb +149 -0
  171. data/spec/lutaml/model/schema/renderers/namespace_spec.rb +36 -0
  172. data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +75 -0
  173. data/spec/lutaml/model/schema/renderers/union_spec.rb +58 -0
  174. data/spec/lutaml/model/transform_apply_value_map_spec.rb +64 -0
  175. data/spec/lutaml/model/uninitialized_class_spec.rb +1 -1
  176. data/spec/lutaml/model/union_attribute_spec.rb +658 -0
  177. data/spec/lutaml/model/union_spec.rb +257 -0
  178. data/spec/lutaml/model/utils_spec.rb +14 -0
  179. data/spec/lutaml/model/xsd_form_default_patterns_spec.rb +2 -2
  180. data/spec/lutaml/model/xsd_patterns_spec.rb +4 -4
  181. data/spec/lutaml/{jsonld → rdf}/context_spec.rb +2 -2
  182. data/spec/lutaml/{jsonld/transform_spec.rb → rdf/linked_data_transform_spec.rb} +15 -1
  183. data/spec/lutaml/{jsonld → rdf}/term_definition_spec.rb +2 -2
  184. data/spec/lutaml/turtle/transform_spec.rb +2 -2
  185. data/spec/lutaml/xml/enhanced_mapping_spec.rb +230 -1
  186. data/spec/lutaml/xml/mapping_spec.rb +4 -4
  187. data/spec/lutaml/xml/namespace_placement_spec.rb +11 -8
  188. data/spec/lutaml/xml/namespace_three_phase_spec.rb +1 -1
  189. data/spec/lutaml/xml/opal_xml_spec.rb +22 -20
  190. data/spec/lutaml/xml/prefix_control_spec.rb +4 -4
  191. data/spec/lutaml/xml/schema/compiler_spec.rb +21 -4
  192. data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +404 -0
  193. data/spec/lutaml/xml/schema/rng_compiler_spec.rb +893 -0
  194. data/spec/lutaml/xml/serializable_namespace_spec.rb +6 -6
  195. data/spec/lutaml/xml/type_namespace_examples_spec.rb +1 -1
  196. data/spec/lutaml/xml/type_namespace_integration_spec.rb +3 -3
  197. data/spec/lutaml/xml/validate_xml_with_constructs_spec.rb +222 -0
  198. data/spec/lutaml/xml/validate_xml_with_spec.rb +186 -0
  199. data/spec/lutaml/yamlld/adapter_spec.rb +56 -0
  200. data/spec/lutaml/yamlld/registration_spec.rb +33 -0
  201. data/spec/spec_helper.rb +28 -11
  202. data/spec/support/opal.rb +5 -2
  203. metadata +108 -37
  204. data/lib/lutaml/model/schema/xml_compiler/attribute.rb +0 -106
  205. data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +0 -47
  206. data/lib/lutaml/model/schema/xml_compiler/choice.rb +0 -67
  207. data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +0 -27
  208. data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +0 -34
  209. data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +0 -173
  210. data/lib/lutaml/model/schema/xml_compiler/element.rb +0 -121
  211. data/lib/lutaml/model/schema/xml_compiler/group.rb +0 -113
  212. data/lib/lutaml/model/schema/xml_compiler/restriction.rb +0 -104
  213. data/lib/lutaml/model/schema/xml_compiler/sequence.rb +0 -52
  214. data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +0 -40
  215. data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +0 -265
  216. data/lib/lutaml/model/schema/xml_compiler/xml_namespace_class.rb +0 -110
  217. data/lib/lutaml/model/services/default_value_resolver.rb +0 -60
  218. data/spec/lutaml/model/services/default_value_resolver_spec.rb +0 -162
  219. data/spec/lutaml/xml/schema/compiler/attribute_group_spec.rb +0 -71
  220. data/spec/lutaml/xml/schema/compiler/attribute_spec.rb +0 -67
  221. data/spec/lutaml/xml/schema/compiler/choice_spec.rb +0 -74
  222. data/spec/lutaml/xml/schema/compiler/complex_content_restriction_spec.rb +0 -60
  223. data/spec/lutaml/xml/schema/compiler/complex_content_spec.rb +0 -41
  224. data/spec/lutaml/xml/schema/compiler/complex_type_spec.rb +0 -202
  225. data/spec/lutaml/xml/schema/compiler/element_spec.rb +0 -68
  226. data/spec/lutaml/xml/schema/compiler/group_spec.rb +0 -90
  227. data/spec/lutaml/xml/schema/compiler/restriction_spec.rb +0 -77
  228. data/spec/lutaml/xml/schema/compiler/sequence_spec.rb +0 -65
  229. data/spec/lutaml/xml/schema/compiler/simple_content_spec.rb +0 -61
  230. data/spec/lutaml/xml/schema/compiler/simple_type_spec.rb +0 -184
@@ -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>
@@ -3,6 +3,7 @@
3
3
  require "spec_helper"
4
4
  require "lutaml/turtle"
5
5
  require "lutaml/jsonld"
6
+ require "lutaml/yamlld"
6
7
 
7
8
  RSpec.describe "Multi-format model" do
8
9
  before do
@@ -103,4 +104,26 @@ RSpec.describe "Multi-format model" do
103
104
  expect(turtle).to include("@prefix")
104
105
  end
105
106
  end
107
+
108
+ describe "YAML-LD format" do
109
+ it "serializes with @type and @id" do
110
+ yamlld = instance.to_yamlld
111
+ parsed = YAML.safe_load(yamlld)
112
+ expect(parsed["@type"]).to eq("skos:Concept")
113
+ expect(parsed["@id"]).to eq("http://example.org/concept/42")
114
+ expect(parsed["prefLabel"]).to eq("test")
115
+ end
116
+
117
+ it "round-trips" do
118
+ restored = MultiFormatModel.from_yamlld(instance.to_yamlld)
119
+ expect(restored.name).to eq("test")
120
+ expect(restored.code).to eq("42")
121
+ end
122
+
123
+ it "produces the same data model as JSON-LD" do
124
+ from_yamlld = YAML.safe_load(instance.to_yamlld)
125
+ from_jsonld = JSON.parse(instance.to_jsonld)
126
+ expect(from_yamlld).to eq(from_jsonld)
127
+ end
128
+ end
106
129
  end
@@ -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
@@ -191,7 +191,14 @@ RSpec.describe Lutaml::Model::Attribute do
191
191
 
192
192
  Lutaml::Model::Attribute::ALLOWED_OPTIONS.each do |option|
193
193
  it "return true if option is `#{option}`" do
194
- expect(validate_options.call({ option => "value" })).to be(true)
194
+ if option == :xsd_type
195
+ expect do
196
+ result = validate_options.call({ option => "value" })
197
+ expect(result).to be(true)
198
+ end.to output.to_stderr
199
+ else
200
+ expect(validate_options.call({ option => "value" })).to be(true)
201
+ end
195
202
  end
196
203
  end
197
204
 
@@ -209,9 +216,19 @@ RSpec.describe Lutaml::Model::Attribute do
209
216
  age_attr.send(:validate_options!, { pattern: /[A-Za-z ]/ })
210
217
  end.to raise_error(
211
218
  StandardError,
212
- "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",
213
221
  )
214
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
215
232
  end
216
233
 
217
234
  describe "#derived?" do
@@ -242,46 +259,193 @@ RSpec.describe Lutaml::Model::Attribute do
242
259
  end
243
260
  end
244
261
 
245
- describe "#default via DefaultValueResolver" do
262
+ describe "#default" do
246
263
  let(:register) { Lutaml::Model::Config.default_register }
247
-
248
- def create_resolver(attribute)
249
- Lutaml::Model::Services::DefaultValueResolver.new(attribute, register,
250
- nil)
251
- end
264
+ let(:instance) { nil }
252
265
 
253
266
  context "when default is not set" do
254
267
  let(:attribute) { described_class.new("name", :string) }
255
268
 
256
269
  it "returns uninitialized" do
257
- expect(create_resolver(attribute).default).to be(Lutaml::Model::UninitializedClass.instance)
270
+ expect(attribute.default(register, instance)).to be(Lutaml::Model::UninitializedClass.instance)
258
271
  end
259
272
  end
260
273
 
261
274
  context "when default is set as a proc" do
262
275
  it "returns the value" do
263
276
  attribute = described_class.new("name", :string, default: -> { "John" })
264
- expect(create_resolver(attribute).default).to eq("John")
277
+ expect(attribute.default(register, instance)).to eq("John")
265
278
  end
266
279
 
267
280
  it "returns the value casted to correct type" do
268
281
  file = Pathname.new("avatar.png")
269
282
  attribute = described_class.new("image", :string, default: -> { file })
270
283
 
271
- expect(create_resolver(attribute).default).to eq("avatar.png")
284
+ expect(attribute.default(register, instance)).to eq("avatar.png")
272
285
  end
273
286
  end
274
287
 
275
288
  context "when default is set as value" do
276
289
  it "returns the value" do
277
290
  attribute = described_class.new("name", :string, default: "John Doe")
278
- expect(create_resolver(attribute).default).to eq("John Doe")
291
+ expect(attribute.default(register, instance)).to eq("John Doe")
279
292
  end
280
293
 
281
294
  it "returns the value casted to correct type" do
282
295
  attribute = described_class.new("age", :integer, default: "24")
283
- 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
284
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)
285
449
  end
286
450
  end
287
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