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,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ module UnionSpec
6
+ class WithUnit < Lutaml::Model::Serializable
7
+ attribute :number, :float
8
+ attribute :unit, :string
9
+
10
+ key_value do
11
+ map "number", to: :number
12
+ map "unit", to: :unit
13
+ end
14
+ end
15
+
16
+ class Celsius < Lutaml::Model::Serializable
17
+ attribute :celsius, :float
18
+
19
+ key_value do
20
+ map "celsius", to: :celsius
21
+ end
22
+ end
23
+
24
+ # Member declaring an aliased key (two names for one attribute).
25
+ class AliasedName < Lutaml::Model::Serializable
26
+ attribute :name, :string
27
+
28
+ key_value do
29
+ map %w[name product_name], to: :name
30
+ end
31
+ end
32
+ end
33
+
34
+ RSpec.describe Lutaml::Model::Type::Union do
35
+ let(:int_str) do
36
+ [Lutaml::Model::Type::Integer, Lutaml::Model::Type::String]
37
+ end
38
+ let(:int_float) do
39
+ [Lutaml::Model::Type::Integer, Lutaml::Model::Type::Float]
40
+ end
41
+
42
+ def member_for(value, members, format: nil)
43
+ result = described_class.conforming_member(value, members,
44
+ format: format, register: nil)
45
+ result&.first
46
+ end
47
+
48
+ describe ".conforming_member (scalars)" do
49
+ it "matches Integer for an integer-lexical string" do
50
+ expect(member_for("42", int_str)).to eq(Lutaml::Model::Type::Integer)
51
+ end
52
+
53
+ it "falls through to String when Integer rejects" do
54
+ expect(member_for("hello", int_str)).to eq(Lutaml::Model::Type::String)
55
+ end
56
+
57
+ it "rejects a float-string for Integer (sound predicate, not Integer('3.7'))" do
58
+ expect(member_for("3.7", int_float)).to eq(Lutaml::Model::Type::Float)
59
+ end
60
+
61
+ it "accepts a numeric integer string for the Integer member first" do
62
+ expect(member_for("1300", int_float)).to eq(Lutaml::Model::Type::Integer)
63
+ end
64
+
65
+ it "matches a native Integer value" do
66
+ expect(member_for(42, int_str)).to eq(Lutaml::Model::Type::Integer)
67
+ end
68
+
69
+ it "matches a native Float value to the Float member" do
70
+ expect(member_for(3.7, int_float)).to eq(Lutaml::Model::Type::Float)
71
+ end
72
+
73
+ it "returns nil when no scalar member conforms" do
74
+ expect(member_for("abc", int_float)).to be_nil
75
+ end
76
+
77
+ it "rejects an Integer that Float cannot represent exactly" do
78
+ expect(member_for((2**60) + 1, [Lutaml::Model::Type::Float])).to be_nil
79
+ end
80
+
81
+ it "widens a Float-representable Integer into Float" do
82
+ member, casted = described_class.conforming_member(
83
+ 2**60, [Lutaml::Model::Type::Float], format: nil, register: nil
84
+ )
85
+ expect([member, casted]).to eq([Lutaml::Model::Type::Float, (2**60).to_f])
86
+ end
87
+
88
+ it "casts the value to the matched member" do
89
+ member, casted = described_class.conforming_member(
90
+ "42", int_str, format: nil, register: nil
91
+ )
92
+ expect([member, casted]).to eq([Lutaml::Model::Type::Integer, 42])
93
+ end
94
+ end
95
+
96
+ describe ".conforming_member (boolean lexical space)" do
97
+ let(:bool_str) do
98
+ [Lutaml::Model::Type::Boolean, Lutaml::Model::Type::String]
99
+ end
100
+
101
+ it "matches a true/false word to Boolean" do
102
+ expect(member_for("true", bool_str))
103
+ .to eq(Lutaml::Model::Type::Boolean)
104
+ end
105
+
106
+ it "falls through to String for a non-boolean word" do
107
+ expect(member_for("maybe", bool_str))
108
+ .to eq(Lutaml::Model::Type::String)
109
+ end
110
+ end
111
+
112
+ describe ".conforming_member (models by key-coverage)" do
113
+ let(:members) { [UnionSpec::WithUnit, UnionSpec::Celsius] }
114
+
115
+ it "selects the model whose fields cover the input keys" do
116
+ member, = described_class.conforming_member(
117
+ { "number" => 1300.0, "unit" => "C" }, members,
118
+ format: :yaml, register: nil
119
+ )
120
+ expect(member).to eq(UnionSpec::WithUnit)
121
+ end
122
+
123
+ it "selects the disjoint-key model" do
124
+ member, = described_class.conforming_member(
125
+ { "celsius" => 1200.0 }, members, format: :yaml, register: nil
126
+ )
127
+ expect(member).to eq(UnionSpec::Celsius)
128
+ end
129
+
130
+ it "deserializes the selected model member" do
131
+ _, value = described_class.conforming_member(
132
+ { "celsius" => 1200.0 }, members, format: :yaml, register: nil
133
+ )
134
+ expect(value).to be_a(UnionSpec::Celsius)
135
+ expect(value.celsius).to eq(1200.0)
136
+ end
137
+
138
+ it "returns nil when no model covers the keys" do
139
+ result = described_class.conforming_member(
140
+ { "unknown" => 1 }, members, format: :yaml, register: nil
141
+ )
142
+ expect(result).to be_nil
143
+ end
144
+
145
+ it "selects a member matched through an aliased key" do
146
+ member, value = described_class.conforming_member(
147
+ { "product_name" => "Vase" }, [UnionSpec::AliasedName],
148
+ format: :yaml, register: nil
149
+ )
150
+ expect(member).to eq(UnionSpec::AliasedName)
151
+ expect(value.name).to eq("Vase")
152
+ end
153
+
154
+ it "returns an existing member instance unchanged (idempotent)" do
155
+ instance = UnionSpec::Celsius.new(celsius: 1200.0)
156
+ member, value = described_class.conforming_member(
157
+ instance, members, format: :yaml, register: nil
158
+ )
159
+ expect(member).to eq(UnionSpec::Celsius)
160
+ expect(value).to be(instance)
161
+ end
162
+ end
163
+
164
+ describe "nil passthrough" do
165
+ it "returns nil for nil input" do
166
+ expect(member_for(nil, int_str)).to be_nil
167
+ end
168
+ end
169
+
170
+ describe ".validate_members!" do
171
+ it "raises on an empty member list" do
172
+ expect { described_class.validate_members!([]) }
173
+ .to raise_error(ArgumentError)
174
+ end
175
+
176
+ it "raises when a member is not a valid type" do
177
+ expect { described_class.validate_members!([Object]) }
178
+ .to raise_error(ArgumentError)
179
+ end
180
+
181
+ it "raises when a catch-all is not last" do
182
+ expect do
183
+ described_class.validate_members!(
184
+ [Lutaml::Model::Type::String, Lutaml::Model::Type::Integer],
185
+ )
186
+ end.to raise_error(ArgumentError)
187
+ end
188
+
189
+ it "accepts a valid ordered member list" do
190
+ expect do
191
+ described_class.validate_members!(
192
+ [Lutaml::Model::Type::Integer, Lutaml::Model::Type::String],
193
+ )
194
+ end.not_to raise_error
195
+ end
196
+ end
197
+
198
+ describe ".validate_combo!" do
199
+ it "raises when combined with polymorphic" do
200
+ expect { described_class.validate_combo!(polymorphic: {}) }
201
+ .to raise_error(ArgumentError)
202
+ end
203
+
204
+ it "raises when combined with raw" do
205
+ expect { described_class.validate_combo!(raw: true) }
206
+ .to raise_error(ArgumentError)
207
+ end
208
+
209
+ it "accepts a plain options hash" do
210
+ expect { described_class.validate_combo!(collection: true) }
211
+ .not_to raise_error
212
+ end
213
+ end
214
+
215
+ describe "schema export" do
216
+ let(:klass) do
217
+ Class.new(Lutaml::Model::Serializable) do
218
+ attribute :v,
219
+ [Lutaml::Model::Type::Integer, Lutaml::Model::Type::String]
220
+ xml do
221
+ element "union_schema_sample"
222
+ map_element "v", to: :v
223
+ end
224
+ key_value { map "v", to: :v }
225
+
226
+ def self.name
227
+ "UnionSchemaSample"
228
+ end
229
+ end
230
+ end
231
+
232
+ it "emits anyOf for JSON Schema (valid if at least one member matches)" do
233
+ schema = JSON.parse(Lutaml::Model::Schema::JsonSchema.generate(klass))
234
+ defs = schema["$defs"] || schema["definitions"] || {}
235
+ property = defs.dig("UnionSchemaSample", "properties", "v") ||
236
+ schema.dig("properties", "v")
237
+
238
+ expect(property).to eq(
239
+ "anyOf" => [
240
+ { "type" => "integer" },
241
+ { "type" => "string" },
242
+ { "type" => "null" },
243
+ ],
244
+ )
245
+ end
246
+
247
+ it "raises UnionSchemaUnsupportedError for XSD" do
248
+ expect { Lutaml::Xml::Schema::XsdSchema.generate(klass) }
249
+ .to raise_error(Lutaml::Model::UnionSchemaUnsupportedError)
250
+ end
251
+
252
+ it "raises UnionSchemaUnsupportedError for RelaxNG" do
253
+ expect { Lutaml::Xml::Schema::RelaxngSchema.generate(klass) }
254
+ .to raise_error(Lutaml::Model::UnionSchemaUnsupportedError)
255
+ end
256
+ end
257
+ end
@@ -56,6 +56,20 @@ RSpec.describe Lutaml::Model::Utils do
56
56
  it_behaves_like "string conversion", :snake_case, SNAKE_CASE_EXAMPLES
57
57
  it_behaves_like "string conversion", :pluralize, PLURALIZE_EXAMPLES
58
58
 
59
+ describe ".last_of_split" do
60
+ it "returns the segment after the colon" do
61
+ expect(utils.last_of_split("foo:Bar")).to eq("Bar")
62
+ end
63
+
64
+ it "returns the input unchanged when there is no colon" do
65
+ expect(utils.last_of_split("Bar")).to eq("Bar")
66
+ end
67
+
68
+ it "returns nil for nil input" do
69
+ expect(utils.last_of_split(nil)).to be_nil
70
+ end
71
+ end
72
+
59
73
  describe ".deep_dup" do
60
74
  let(:original_hash) do
61
75
  {
@@ -85,7 +85,7 @@ RSpec.describe "XSD Form Default Patterns" do
85
85
  xml = instance.to_xml
86
86
 
87
87
  # The element should be unqualified (no prefix) in instance
88
- expect(xml).to match(%r{<premium})
88
+ expect(xml).to include("<premium")
89
89
  expect(xml).to include("<elementFormUnqualified")
90
90
  end
91
91
  end
@@ -281,7 +281,7 @@ RSpec.describe "XSD Form Default Patterns" do
281
281
  # and the namespace is already declared on an ancestor
282
282
  # The child element should be in the blank namespace without xmlns="" declaration
283
283
  # because the parent uses prefix format (xmlns:ex) or the namespace is already declared.
284
- expect(result).not_to match(%r{xmlns=""})
284
+ expect(result).not_to include('xmlns=""')
285
285
  end
286
286
 
287
287
  it "serializes root element with namespace declaration" do
@@ -43,7 +43,7 @@ RSpec.describe "XSD Three Pattern Architecture" do
43
43
 
44
44
  expect(xsd).to include('<element name="item">')
45
45
  expect(xsd).to include("<complexType>")
46
- expect(xsd).not_to match(/<complexType name=/)
46
+ expect(xsd).not_to include("<complexType name=")
47
47
  expect(xsd).to include('<attribute name="id" type="xs:string"/>')
48
48
  end
49
49
  end
@@ -68,7 +68,7 @@ RSpec.describe "XSD Three Pattern Architecture" do
68
68
  expect(xsd).to include('<element name="name" type="xs:string"/>')
69
69
  expect(xsd).to include('<element name="value" type="xs:integer"/>')
70
70
  # Should NOT generate standalone element declaration
71
- expect(xsd).not_to match(/<element name="product"/)
71
+ expect(xsd).not_to include('<element name="product"')
72
72
  end
73
73
 
74
74
  it "type-only model can be referenced by other elements" do
@@ -233,7 +233,7 @@ RSpec.describe "XSD Three Pattern Architecture" do
233
233
 
234
234
  xsd = Lutaml::Model::Schema.to_xsd(klass)
235
235
  expect(xsd).to include('<element name="simple">')
236
- expect(xsd).not_to match(/<element name="simple" type=/)
236
+ expect(xsd).not_to include('<element name="simple" type=')
237
237
  end
238
238
 
239
239
  it "Pattern 2: type_name only -> named reusable" do
@@ -252,7 +252,7 @@ RSpec.describe "XSD Three Pattern Architecture" do
252
252
  xsd = Lutaml::Model::Schema.to_xsd(klass)
253
253
  expect(xsd).to include('<complexType name="SimpleType">')
254
254
  # Should not have standalone element declaration (only child elements in sequence are OK)
255
- expect(xsd).not_to match(/<element name="SimpleType"/)
255
+ expect(xsd).not_to include('<element name="SimpleType"')
256
256
  end
257
257
 
258
258
  it "Pattern 3: both -> element with named type" do
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "spec_helper"
4
- require "lutaml/jsonld"
4
+ require "lutaml/rdf"
5
5
 
6
- RSpec.describe Lutaml::JsonLd::Context do
6
+ RSpec.describe Lutaml::Rdf::Context do
7
7
  subject(:ctx) { described_class.new }
8
8
 
9
9
  describe "empty context" do
@@ -2,8 +2,9 @@
2
2
 
3
3
  require "spec_helper"
4
4
  require "lutaml/jsonld"
5
+ require "lutaml/yamlld"
5
6
 
6
- RSpec.describe Lutaml::JsonLd::Transform do
7
+ RSpec.describe Lutaml::Rdf::LinkedDataTransform do
7
8
  before do
8
9
  stub_const("TestSkosNs", Class.new(Lutaml::Rdf::Namespace) do
9
10
  uri "http://www.w3.org/2004/02/skos/core#"
@@ -138,6 +139,19 @@ RSpec.describe Lutaml::JsonLd::Transform do
138
139
  expect(parsed["@context"]["skos"]).to eq("http://www.w3.org/2004/02/skos/core#")
139
140
  expect(parsed["@context"]["ex"]).to eq("http://example.org/")
140
141
  end
142
+
143
+ it "preserves data through model → YAML-LD → model" do
144
+ yaml = instance.to_yamlld
145
+ restored = JsonLdTestModel.from_yamlld(yaml)
146
+ expect(restored.name).to eq("test")
147
+ expect(restored.description).to eq("A test concept")
148
+ end
149
+
150
+ it "produces the same data model from YAML-LD as JSON-LD" do
151
+ from_yamlld = YAML.safe_load(instance.to_yamlld)
152
+ from_jsonld = JSON.parse(instance.to_jsonld)
153
+ expect(from_yamlld).to eq(from_jsonld)
154
+ end
141
155
  end
142
156
 
143
157
  describe "nil attribute values" do
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "spec_helper"
4
- require "lutaml/jsonld"
4
+ require "lutaml/rdf"
5
5
 
6
- RSpec.describe Lutaml::JsonLd::TermDefinition do
6
+ RSpec.describe Lutaml::Rdf::TermDefinition do
7
7
  it "simple term maps to name => id" do
8
8
  td = described_class.new(name: "name", id: "http://example.org/name")
9
9
  expect(td.to_context_hash).to eq("name" => "http://example.org/name")
@@ -119,13 +119,13 @@ RSpec.describe Lutaml::Turtle::Transform do
119
119
  it "serializes integers as native Turtle literals" do
120
120
  instance = TypedModel.new(label: "test", count: 42, active: true)
121
121
  result = instance.to_turtle
122
- expect(result).to match(/skos:notation 42/)
122
+ expect(result).to include("skos:notation 42")
123
123
  end
124
124
 
125
125
  it "serializes booleans as native Turtle literals" do
126
126
  instance = TypedModel.new(label: "test", count: 1, active: true)
127
127
  result = instance.to_turtle
128
- expect(result).to match(/skos:note true/)
128
+ expect(result).to include("skos:note true")
129
129
  end
130
130
  end
131
131
 
@@ -187,6 +187,235 @@ RSpec.describe "Enhanced XML Mapping Features" do
187
187
  expect(name_rule.form).to eq(:unqualified)
188
188
  end
189
189
  end
190
+
191
+ context "with Serializable child and form: :qualified" do
192
+ # Regression: form option on the parent's map_element rule must be
193
+ # propagated to the child XmlElement when the child is a Serializable
194
+ # (which goes through create_transformed_nested_element). Otherwise the
195
+ # ElementFormOptionRule cannot fire and the element is serialized
196
+ # without the namespace prefix.
197
+ let(:child_class) do
198
+ ns = namespace_class
199
+ Class.new(Lutaml::Model::Serializable) do
200
+ attribute :label, :string
201
+
202
+ xml do
203
+ element "child"
204
+ namespace ns
205
+ map_element "label", to: :label
206
+ end
207
+ end
208
+ end
209
+
210
+ let(:model_class) do
211
+ ns = namespace_class
212
+ child = child_class
213
+ Class.new(Lutaml::Model::Serializable) do
214
+ attribute :child, child
215
+
216
+ xml do
217
+ element "item"
218
+ namespace ns
219
+ map_element "child", to: :child, form: :qualified
220
+ end
221
+ end
222
+ end
223
+
224
+ it "stores form: :qualified on the parent rule" do
225
+ mapping = model_class.mappings_for(:xml)
226
+ child_rule = mapping.find_element(:child)
227
+ expect(child_rule.form).to eq(:qualified)
228
+ end
229
+
230
+ it "serializes the nested Serializable with a namespace prefix" do
231
+ instance = model_class.new(child: child_class.new(label: "x"))
232
+ doc = Nokogiri::XML(instance.to_xml)
233
+ qualified = doc.at_xpath("//*[name()='ex:child']")
234
+ expect(qualified).not_to be_nil
235
+ expect(qualified.at_xpath("./xmlns:label").text).to eq("x")
236
+ end
237
+
238
+ it "round-trips the qualified Serializable child" do
239
+ original = model_class.new(child: child_class.new(label: "round-trip"))
240
+ restored = model_class.from_xml(original.to_xml)
241
+ expect(restored.child.label).to eq("round-trip")
242
+ end
243
+ end
244
+
245
+ context "with Serializable child and form: :unqualified" do
246
+ # Symmetric to the :qualified case: form: :unqualified must propagate
247
+ # to the child XmlElement so ElementFormOptionRule can force the
248
+ # default (unprefixed) form.
249
+ let(:child_class) do
250
+ ns = namespace_class
251
+ Class.new(Lutaml::Model::Serializable) do
252
+ attribute :label, :string
253
+
254
+ xml do
255
+ element "child"
256
+ namespace ns
257
+ map_element "label", to: :label
258
+ end
259
+ end
260
+ end
261
+
262
+ let(:model_class) do
263
+ ns = namespace_class
264
+ child = child_class
265
+ Class.new(Lutaml::Model::Serializable) do
266
+ attribute :child, child
267
+
268
+ xml do
269
+ element "item"
270
+ namespace ns
271
+ map_element "child", to: :child, form: :unqualified
272
+ end
273
+ end
274
+ end
275
+
276
+ it "propagates form: :unqualified and emits an unprefixed child" do
277
+ instance = model_class.new(child: child_class.new(label: "x"))
278
+ doc = Nokogiri::XML(instance.to_xml)
279
+ expect(doc.xpath("//*[name()='ex:child']")).to be_empty
280
+ expect(doc.at_xpath("//*[local-name()='child']")).not_to be_nil
281
+ end
282
+ end
283
+
284
+ context "with a collection of Serializable children" do
285
+ # The form option must propagate to every element produced for a
286
+ # collection attribute, not just the first.
287
+ let(:child_class) do
288
+ ns = namespace_class
289
+ Class.new(Lutaml::Model::Serializable) do
290
+ attribute :label, :string
291
+
292
+ xml do
293
+ element "child"
294
+ namespace ns
295
+ map_element "label", to: :label
296
+ end
297
+ end
298
+ end
299
+
300
+ let(:model_class) do
301
+ ns = namespace_class
302
+ child = child_class
303
+ Class.new(Lutaml::Model::Serializable) do
304
+ attribute :children, child, collection: true
305
+
306
+ xml do
307
+ element "item"
308
+ namespace ns
309
+ map_element "child", to: :children, form: :qualified
310
+ end
311
+ end
312
+ end
313
+
314
+ it "qualifies every child in the collection" do
315
+ instance = model_class.new(
316
+ children: [child_class.new(label: "a"), child_class.new(label: "b")],
317
+ )
318
+ doc = Nokogiri::XML(instance.to_xml)
319
+ qualified = doc.xpath("//*[name()='ex:child']")
320
+ labels = qualified.map { |c| c.at_xpath("./xmlns:label").text }
321
+ expect(labels).to contain_exactly("a", "b")
322
+ end
323
+ end
324
+
325
+ context "with a Serializable child containing a Serializable grandchild" do
326
+ # form: :qualified is a per-rule override; it must NOT propagate
327
+ # transitively to grandchildren. Each level applies its own rule and
328
+ # its own parent_element_form_default.
329
+ let(:grandchild_class) do
330
+ ns = namespace_class
331
+ Class.new(Lutaml::Model::Serializable) do
332
+ attribute :value, :string
333
+
334
+ xml do
335
+ element "grandchild"
336
+ namespace ns
337
+ map_element "value", to: :value
338
+ end
339
+ end
340
+ end
341
+
342
+ let(:child_class) do
343
+ ns = namespace_class
344
+ grandchild = grandchild_class
345
+ Class.new(Lutaml::Model::Serializable) do
346
+ attribute :inner, grandchild
347
+
348
+ xml do
349
+ element "child"
350
+ namespace ns
351
+ map_element "grandchild", to: :inner
352
+ end
353
+ end
354
+ end
355
+
356
+ let(:model_class) do
357
+ ns = namespace_class
358
+ child = child_class
359
+ Class.new(Lutaml::Model::Serializable) do
360
+ attribute :child, child
361
+
362
+ xml do
363
+ element "item"
364
+ namespace ns
365
+ map_element "child", to: :child, form: :qualified
366
+ end
367
+ end
368
+ end
369
+
370
+ it "qualifies the child but leaves the grandchild unprefixed" do
371
+ instance = model_class.new(
372
+ child: child_class.new(inner: grandchild_class.new(value: "x")),
373
+ )
374
+ doc = Nokogiri::XML(instance.to_xml)
375
+ expect(doc.at_xpath("//*[name()='ex:child']")).not_to be_nil
376
+ expect(doc.xpath("//*[name()='ex:grandchild']")).to be_empty
377
+ expect(doc.at_xpath("//*[local-name()='grandchild']")).not_to be_nil
378
+ end
379
+ end
380
+
381
+ context "with Serializable child but no form override" do
382
+ # Sanity: when no form: :qualified is set on the rule, the W3C
383
+ # element_form_default :unqualified override should still kick in and
384
+ # the nested Serializable should serialize unprefixed.
385
+ let(:child_class) do
386
+ ns = namespace_class
387
+ Class.new(Lutaml::Model::Serializable) do
388
+ attribute :label, :string
389
+
390
+ xml do
391
+ element "child"
392
+ namespace ns
393
+ map_element "label", to: :label
394
+ end
395
+ end
396
+ end
397
+
398
+ let(:model_class) do
399
+ ns = namespace_class
400
+ child = child_class
401
+ Class.new(Lutaml::Model::Serializable) do
402
+ attribute :child, child
403
+
404
+ xml do
405
+ element "item"
406
+ namespace ns
407
+ map_element "child", to: :child
408
+ end
409
+ end
410
+ end
411
+
412
+ it "honors element_form_default :unqualified by leaving the child unprefixed" do
413
+ instance = model_class.new(child: child_class.new(label: "x"))
414
+ doc = Nokogiri::XML(instance.to_xml)
415
+ expect(doc.xpath("//*[name()='ex:child']")).to be_empty
416
+ expect(doc.at_xpath("//*[local-name()='child']")).not_to be_nil
417
+ end
418
+ end
190
419
  end
191
420
 
192
421
  describe "form option on map_attribute" do
@@ -222,7 +451,7 @@ RSpec.describe "Enhanced XML Mapping Features" do
222
451
  instance = model_class.new(id: "ABC123")
223
452
  xml = instance.to_xml
224
453
  # Form affects qualification but implementation varies by adapter
225
- expect(xml).to match(/id="ABC123"/)
454
+ expect(xml).to include('id="ABC123"')
226
455
  end
227
456
  end
228
457
 
@@ -86,16 +86,16 @@ module XmlMappingSpec
86
86
  end
87
87
 
88
88
  class Address < Lutaml::Model::Serializable
89
- attribute :street, ::Lutaml::Model::Type::String, raw: true
90
- attribute :city, :string, raw: true
89
+ attribute :street, ::Lutaml::Model::Type::String
90
+ attribute :city, :string
91
91
  attribute :text, :string
92
92
  attribute :address, Address
93
93
 
94
94
  xml do
95
95
  element "address"
96
96
 
97
- map_element "street", to: :street
98
- map_element "city", to: :city
97
+ map_element "street", to: :street, raw: :content
98
+ map_element "city", to: :city, raw: :content
99
99
  map_element "text", to: :text
100
100
  end
101
101
  end