lutaml-model 0.8.17 → 0.8.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/downstream-performance.yml +27 -1
  3. data/.github/workflows/js-pr-check.yml +102 -0
  4. data/.github/workflows/js-sync.yml +51 -0
  5. data/.github/workflows/opal.yml +26 -6
  6. data/.github/workflows/performance.yml +32 -7
  7. data/.github/workflows/rake.yml +78 -32
  8. data/.gitmodules +6 -0
  9. data/.rubocop_todo.yml +8 -0
  10. data/Gemfile +16 -1
  11. data/README.adoc +267 -0
  12. data/Rakefile +166 -4
  13. data/docs/_guides/schema-import.adoc +42 -0
  14. data/docs/_guides/xml-mapping.adoc +21 -0
  15. data/docs/_pages/validation.adoc +65 -0
  16. data/lib/compat/opal/generate_boot.rb +123 -0
  17. data/lib/compat/opal/js_bundle_entry.rb +42 -0
  18. data/lib/compat/opal/lutaml_model_boot.rb +497 -0
  19. data/lib/compat/opal/moxml_boot.rb +68 -0
  20. data/lib/compat/opal/yaml_compat.rb +32 -0
  21. data/lib/lutaml/json.rb +1 -1
  22. data/lib/lutaml/key_value/transform.rb +3 -8
  23. data/lib/lutaml/key_value/transformation/value_serializer.rb +14 -1
  24. data/lib/lutaml/key_value/transformation.rb +17 -5
  25. data/lib/lutaml/model/adapter_resolver.rb +2 -2
  26. data/lib/lutaml/model/attribute.rb +88 -11
  27. data/lib/lutaml/model/cached_type_resolver.rb +1 -1
  28. data/lib/lutaml/model/error/union_schema_unsupported_error.rb +11 -0
  29. data/lib/lutaml/model/global_context.rb +11 -8
  30. data/lib/lutaml/model/instrumentation.rb +2 -2
  31. data/lib/lutaml/model/mapping/mapping_rule.rb +0 -6
  32. data/lib/lutaml/model/register.rb +2 -2
  33. data/lib/lutaml/model/runtime_compatibility.rb +50 -0
  34. data/lib/lutaml/model/schema/class_loader.rb +58 -0
  35. data/lib/lutaml/model/schema/compiled_output.rb +83 -0
  36. data/lib/lutaml/model/schema/definitions/attribute.rb +32 -0
  37. data/lib/lutaml/model/schema/definitions/choice.rb +20 -0
  38. data/lib/lutaml/model/schema/definitions/facet.rb +40 -0
  39. data/lib/lutaml/model/schema/definitions/group_import.rb +22 -0
  40. data/lib/lutaml/model/schema/definitions/member_walk.rb +33 -0
  41. data/lib/lutaml/model/schema/definitions/model.rb +40 -0
  42. data/lib/lutaml/model/schema/definitions/namespace.rb +23 -0
  43. data/lib/lutaml/model/schema/definitions/restricted_type.rb +30 -0
  44. data/lib/lutaml/model/schema/definitions/sequence.rb +19 -0
  45. data/lib/lutaml/model/schema/definitions/simple_content.rb +22 -0
  46. data/lib/lutaml/model/schema/definitions/transform_facet.rb +21 -0
  47. data/lib/lutaml/model/schema/definitions/type_ref.rb +20 -0
  48. data/lib/lutaml/model/schema/definitions/union_type.rb +28 -0
  49. data/lib/lutaml/model/schema/definitions/xml_root.rb +20 -0
  50. data/lib/lutaml/model/schema/definitions.rb +24 -0
  51. data/lib/lutaml/model/schema/file_writer.rb +70 -0
  52. data/lib/lutaml/model/schema/generator/definitions_collection.rb +16 -2
  53. data/lib/lutaml/model/schema/generator/property.rb +21 -4
  54. data/lib/lutaml/model/schema/module_nesting.rb +31 -0
  55. data/lib/lutaml/model/schema/namespace_naming.rb +46 -0
  56. data/lib/lutaml/model/schema/registry_generator.rb +115 -0
  57. data/lib/lutaml/model/schema/renderers/base.rb +40 -0
  58. data/lib/lutaml/model/schema/renderers/mappings.rb +72 -0
  59. data/lib/lutaml/model/schema/renderers/member_decls.rb +115 -0
  60. data/lib/lutaml/model/schema/renderers/model.rb +121 -0
  61. data/lib/lutaml/model/schema/renderers/namespace.rb +30 -0
  62. data/lib/lutaml/model/schema/renderers/registration.rb +64 -0
  63. data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +93 -0
  64. data/lib/lutaml/model/schema/renderers/restricted_type.rb +88 -0
  65. data/lib/lutaml/model/schema/renderers/union.rb +83 -0
  66. data/lib/lutaml/model/schema/renderers.rb +19 -0
  67. data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +47 -0
  68. data/lib/lutaml/model/schema/rnc_compiler.rb +62 -0
  69. data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +91 -0
  70. data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +392 -0
  71. data/lib/lutaml/model/schema/rng_compiler/member_collector.rb +30 -0
  72. data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +152 -0
  73. data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +125 -0
  74. data/lib/lutaml/model/schema/rng_compiler.rb +220 -0
  75. data/lib/lutaml/model/schema/templates.rb +162 -0
  76. data/lib/lutaml/model/schema/xml_compiler/element_order.rb +41 -0
  77. data/lib/lutaml/model/schema/xml_compiler/registry_generator.rb +31 -107
  78. data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +191 -0
  79. data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +216 -0
  80. data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +153 -0
  81. data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +167 -0
  82. data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +87 -0
  83. data/lib/lutaml/model/schema/xml_compiler.rb +38 -517
  84. data/lib/lutaml/model/schema.rb +19 -0
  85. data/lib/lutaml/model/serialize/attribute_definition.rb +14 -2
  86. data/lib/lutaml/model/serialize/builder.rb +15 -2
  87. data/lib/lutaml/model/serialize/value_mapping.rb +9 -57
  88. data/lib/lutaml/model/serialize.rb +6 -9
  89. data/lib/lutaml/model/services/rule_value_extractor.rb +2 -3
  90. data/lib/lutaml/model/services.rb +0 -2
  91. data/lib/lutaml/model/store.rb +2 -1
  92. data/lib/lutaml/model/toml.rb +1 -1
  93. data/lib/lutaml/model/transform.rb +17 -39
  94. data/lib/lutaml/model/transformation_registry.rb +1 -1
  95. data/lib/lutaml/model/type.rb +1 -0
  96. data/lib/lutaml/model/union.rb +320 -0
  97. data/lib/lutaml/model/utils.rb +6 -0
  98. data/lib/lutaml/model/validation.rb +1 -4
  99. data/lib/lutaml/model/version.rb +1 -1
  100. data/lib/lutaml/model.rb +18 -0
  101. data/lib/lutaml/toml.rb +1 -1
  102. data/lib/lutaml/xml/adapter.rb +1 -1
  103. data/lib/lutaml/xml/adapter_loader.rb +1 -1
  104. data/lib/lutaml/xml/builder/base.rb +1 -1
  105. data/lib/lutaml/xml/builder.rb +1 -1
  106. data/lib/lutaml/xml/error/schema_validation_error.rb +25 -0
  107. data/lib/lutaml/xml/model_transform.rb +5 -1
  108. data/lib/lutaml/xml/schema/relaxng_schema.rb +5 -0
  109. data/lib/lutaml/xml/schema/xsd_schema.rb +4 -0
  110. data/lib/lutaml/xml/serialization/format_conversion.rb +48 -0
  111. data/lib/lutaml/xml/serialization/instance_methods.rb +13 -0
  112. data/lib/lutaml/xml/transformation/element_builder.rb +21 -8
  113. data/lib/lutaml/xml/transformation/ordered_applier.rb +6 -0
  114. data/lib/lutaml/xml/xsd_validator.rb +66 -0
  115. data/lib/lutaml/xml.rb +53 -12
  116. data/lutaml-model.gemspec +4 -2
  117. data/spec/fixtures/xml/schema/rnc/address_book.rnc +15 -0
  118. data/spec/fixtures/xml/schema/rnc/book_features.rnc +18 -0
  119. data/spec/fixtures/xml/schema/rnc/includes/book.rnc +6 -0
  120. data/spec/fixtures/xml/schema/rnc/includes/circular_a.rnc +8 -0
  121. data/spec/fixtures/xml/schema/rnc/includes/circular_b.rnc +8 -0
  122. data/spec/fixtures/xml/schema/rnc/includes/library.rnc +8 -0
  123. data/spec/fixtures/xml/schema/rng/address_book.rng +16 -0
  124. data/spec/fixtures/xml/schema/rng/book.rng +27 -0
  125. data/spec/fixtures/xml/schema/rng/fixed_value.rng +11 -0
  126. data/spec/fixtures/xml/schema/rng/fragment_in_choice.rng +30 -0
  127. data/spec/fixtures/xml/schema/rng/full_name.rng +39 -0
  128. data/spec/fixtures/xml/schema/rng/integer_range.rng +32 -0
  129. data/spec/fixtures/xml/schema/rng/list_type.rng +12 -0
  130. data/spec/fixtures/xml/schema/rng/namespaced.rng +9 -0
  131. data/spec/fixtures/xml/schema/rng/paragraph.rng +13 -0
  132. data/spec/fixtures/xml/schema/rng/person.rng +23 -0
  133. data/spec/fixtures/xml/schema/rng/union.rng +14 -0
  134. data/spec/fixtures/xml/validate_xml_with_address.xsd +13 -0
  135. data/spec/fixtures/xml/validate_xml_with_contact.xsd +11 -0
  136. data/spec/fixtures/xml/validate_xml_with_person.xsd +11 -0
  137. data/spec/fixtures/xml/validate_xml_with_person_strict.xsd +17 -0
  138. data/spec/fixtures/xml/validate_xml_with_product.xsd +37 -0
  139. data/spec/lutaml/model/attribute_apply_value_map_spec.rb +134 -0
  140. data/spec/lutaml/model/attribute_spec.rb +169 -12
  141. data/spec/lutaml/model/boot_manifest_spec.rb +30 -0
  142. data/spec/lutaml/model/collection_spec.rb +18 -0
  143. data/spec/lutaml/model/mixed_content_spec.rb +171 -0
  144. data/spec/lutaml/model/opal_smoke_spec.rb +67 -2
  145. data/spec/lutaml/model/ordered_content_spec.rb +30 -0
  146. data/spec/lutaml/model/raw_element_spec.rb +125 -0
  147. data/spec/lutaml/model/rule_value_extractor_spec.rb +7 -14
  148. data/spec/lutaml/model/runtime_compatibility_spec.rb +25 -0
  149. data/spec/lutaml/model/schema/renderers/model_spec.rb +149 -0
  150. data/spec/lutaml/model/schema/renderers/namespace_spec.rb +36 -0
  151. data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +75 -0
  152. data/spec/lutaml/model/schema/renderers/union_spec.rb +58 -0
  153. data/spec/lutaml/model/transform_apply_value_map_spec.rb +64 -0
  154. data/spec/lutaml/model/union_attribute_spec.rb +658 -0
  155. data/spec/lutaml/model/union_spec.rb +257 -0
  156. data/spec/lutaml/model/utils_spec.rb +14 -0
  157. data/spec/lutaml/xml/opal_xml_spec.rb +22 -20
  158. data/spec/lutaml/xml/schema/compiler_spec.rb +21 -4
  159. data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +404 -0
  160. data/spec/lutaml/xml/schema/rng_compiler_spec.rb +893 -0
  161. data/spec/lutaml/xml/validate_xml_with_constructs_spec.rb +222 -0
  162. data/spec/lutaml/xml/validate_xml_with_spec.rb +186 -0
  163. data/spec/spec_helper.rb +28 -11
  164. data/spec/support/opal.rb +5 -2
  165. metadata +97 -31
  166. data/lib/lutaml/model/schema/xml_compiler/attribute.rb +0 -106
  167. data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +0 -47
  168. data/lib/lutaml/model/schema/xml_compiler/choice.rb +0 -67
  169. data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +0 -27
  170. data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +0 -34
  171. data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +0 -173
  172. data/lib/lutaml/model/schema/xml_compiler/element.rb +0 -121
  173. data/lib/lutaml/model/schema/xml_compiler/group.rb +0 -113
  174. data/lib/lutaml/model/schema/xml_compiler/restriction.rb +0 -104
  175. data/lib/lutaml/model/schema/xml_compiler/sequence.rb +0 -52
  176. data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +0 -40
  177. data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +0 -265
  178. data/lib/lutaml/model/schema/xml_compiler/xml_namespace_class.rb +0 -110
  179. data/lib/lutaml/model/services/default_value_resolver.rb +0 -60
  180. data/spec/lutaml/model/services/default_value_resolver_spec.rb +0 -162
  181. data/spec/lutaml/xml/schema/compiler/attribute_group_spec.rb +0 -71
  182. data/spec/lutaml/xml/schema/compiler/attribute_spec.rb +0 -67
  183. data/spec/lutaml/xml/schema/compiler/choice_spec.rb +0 -74
  184. data/spec/lutaml/xml/schema/compiler/complex_content_restriction_spec.rb +0 -60
  185. data/spec/lutaml/xml/schema/compiler/complex_content_spec.rb +0 -41
  186. data/spec/lutaml/xml/schema/compiler/complex_type_spec.rb +0 -202
  187. data/spec/lutaml/xml/schema/compiler/element_spec.rb +0 -68
  188. data/spec/lutaml/xml/schema/compiler/group_spec.rb +0 -90
  189. data/spec/lutaml/xml/schema/compiler/restriction_spec.rb +0 -77
  190. data/spec/lutaml/xml/schema/compiler/sequence_spec.rb +0 -65
  191. data/spec/lutaml/xml/schema/compiler/simple_content_spec.rb +0 -61
  192. data/spec/lutaml/xml/schema/compiler/simple_type_spec.rb +0 -184
@@ -0,0 +1,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
  {
@@ -2,9 +2,13 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- RSpec.describe "XML with REXML under Opal", if: RUBY_ENGINE == "opal" do
5
+ # Adapter-agnostic XML round-trip scenarios, shared between Oga and
6
+ # REXML smoke runs. Both adapters are pure Ruby and work under Opal;
7
+ # this shared example is the contract that any future Opal-capable
8
+ # adapter would also need to satisfy.
9
+ RSpec.shared_examples "opal XML adapter round-trip" do |adapter_name|
6
10
  before do
7
- Lutaml::Model::Config.xml_adapter_type = :rexml
11
+ Lutaml::Model::Config.xml_adapter_type = adapter_name
8
12
  end
9
13
 
10
14
  it "round-trips XML through parse and serialize" do
@@ -118,28 +122,26 @@ RSpec.describe "XML with REXML under Opal", if: RUBY_ENGINE == "opal" do
118
122
  expect(xml).to include("<title>Widget</title>")
119
123
  expect(xml).to include("<count>5</count>")
120
124
  end
125
+ end
121
126
 
122
- it "handles XmlOrderable on plain model classes" do
123
- plain_model = Class.new do
124
- attr_accessor :data
125
- end
127
+ # Pure-Ruby XML adapters that ship in the Opal bundle. Each nested
128
+ # group runs the shared contract for its adapter; the top-level guard
129
+ # gates the whole file under Opal.
130
+ RSpec.describe "XML adapters under Opal", if: RUBY_ENGINE == "opal" do
131
+ describe "with Oga (default)" do
132
+ it_behaves_like "opal XML adapter round-trip", :oga
126
133
 
127
- Class.new do
128
- include Lutaml::Model::Serialize
129
-
130
- const_set(:MODEL_CLASS, plain_model)
131
- model plain_model
132
- attribute :data, :string
133
- xml do
134
- element "data"
135
- map_element "data", to: :data
136
- end
134
+ it "uses Oga adapter by default under Opal" do
135
+ expect(Lutaml::Model::Config.xml_adapter_type).to eq(:oga)
137
136
  end
138
-
139
- expect(plain_model.ancestors).to include(Lutaml::Xml::XmlOrderable)
140
137
  end
141
138
 
142
- it "uses REXML adapter by default under Opal" do
143
- expect(Lutaml::Model::Config.xml_adapter_type).to eq(:rexml)
139
+ describe "with REXML" do
140
+ it_behaves_like "opal XML adapter round-trip", :rexml
141
+
142
+ it "selects REXML when explicitly configured" do
143
+ Lutaml::Model::Config.xml_adapter_type = :rexml
144
+ expect(Lutaml::Model::Config.xml_adapter_type).to eq(:rexml)
145
+ end
144
146
  end
145
147
  end
@@ -41,8 +41,9 @@ RSpec.describe Lutaml::Model::Schema::XmlCompiler do
41
41
  it "keeps built-in W3C XML attribute types when processing xml.xsd" do
42
42
  described_class.as_models(xml_namespace_schema)
43
43
 
44
- expect(described_class.attributes["id"].type)
45
- .to eq("Lutaml::Xml::W3c::XmlIdType")
44
+ type_ref = described_class.builder.attributes["id"].type
45
+ expect(type_ref.kind).to eq(:w3c)
46
+ expect(type_ref.value).to eq("Lutaml::Xml::W3c::XmlIdType")
46
47
  end
47
48
  end
48
49
 
@@ -227,8 +228,7 @@ RSpec.describe Lutaml::Model::Schema::XmlCompiler do
227
228
  end
228
229
 
229
230
  let(:expected_classes) do
230
- types = described_class::SimpleType::SUPPORTED_DATA_TYPES
231
- classes = types.filter_map do |name, value|
231
+ classes = described_class::SupportedDataTypes::TABLE.filter_map do |name, value|
232
232
  name.to_s unless value[:skippable]
233
233
  end
234
234
  classes << "User"
@@ -751,4 +751,21 @@ RSpec.describe Lutaml::Model::Schema::XmlCompiler do
751
751
  end
752
752
  end
753
753
  end
754
+
755
+ describe "SupportedDataTypes.skippable?" do
756
+ # Attribute TypeRefs store the snake_case rendering, simple_content
757
+ # keeps the XSD spelling; skippable? must accept both for dateTime,
758
+ # the one skippable name that changes under snake_case.
759
+ it "matches both the XSD and snake_case spelling of skippable types" do
760
+ sdt = described_class::SupportedDataTypes
761
+ expect(sdt.skippable?("dateTime")).to be true
762
+ expect(sdt.skippable?("date_time")).to be true
763
+ end
764
+
765
+ it "still rejects non-skippable types in both spellings" do
766
+ sdt = described_class::SupportedDataTypes
767
+ expect(sdt.skippable?("nonNegativeInteger")).to be false
768
+ expect(sdt.skippable?("non_negative_integer")).to be false
769
+ end
770
+ end
754
771
  end