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,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "validate_xml_with XSD constructs" do
6
+ def fixture_path(name)
7
+ File.expand_path("../../fixtures/xml/#{name}", __dir__)
8
+ end
9
+
10
+ describe "kitchen-sink schema (pattern, enumeration, attributes, " \
11
+ "types, occurrence, nested complex type)" do
12
+ before do
13
+ path = fixture_path("validate_xml_with_product.xsd")
14
+
15
+ stub_const("XsdDimensions", Class.new(Lutaml::Model::Serializable) do
16
+ attribute :width, :string
17
+ attribute :height, :string
18
+
19
+ xml do
20
+ element "dimensions"
21
+ map_element "width", to: :width
22
+ map_element "height", to: :height
23
+ end
24
+ end)
25
+
26
+ stub_const("XsdProduct", Class.new(Lutaml::Model::Serializable) do
27
+ attribute :id, :string
28
+ attribute :status, :string
29
+ attribute :sku, :string
30
+ attribute :price, :string
31
+ attribute :released, :string
32
+ attribute :tags, :string, collection: true
33
+ attribute :note, :string
34
+ attribute :dimensions, XsdDimensions
35
+
36
+ xml do
37
+ element "product"
38
+ map_attribute "id", to: :id
39
+ map_attribute "status", to: :status
40
+ map_element "sku", to: :sku
41
+ map_element "price", to: :price
42
+ map_element "released", to: :released
43
+ map_element "tag", to: :tags
44
+ map_element "note", to: :note
45
+ map_element "dimensions", to: :dimensions
46
+ end
47
+
48
+ validate_xml_with path
49
+ end)
50
+ end
51
+
52
+ let(:valid_attributes) do
53
+ {
54
+ id: "p1",
55
+ status: "published",
56
+ sku: "ABC-1234",
57
+ price: "9.99",
58
+ released: "2026-06-11",
59
+ tags: %w[a b c],
60
+ note: "boundary case: three tags is maxOccurs",
61
+ dimensions: XsdDimensions.new(width: "10", height: "20"),
62
+ }
63
+ end
64
+
65
+ def product(**overrides)
66
+ XsdProduct.new(valid_attributes.merge(overrides))
67
+ end
68
+
69
+ it "accepts a document satisfying every construct" do
70
+ expect(product.validate).to be_empty
71
+ end
72
+
73
+ it "accepts a document omitting all optional parts" do
74
+ minimal = product(status: nil, tags: [], note: nil, dimensions: nil)
75
+
76
+ expect(minimal.validate).to be_empty
77
+ end
78
+
79
+ it "collects xs:pattern violations" do
80
+ errors = product(sku: "bad-sku").validate
81
+
82
+ expect(errors.map(&:message)).to include(a_string_including("sku"))
83
+ end
84
+
85
+ it "collects xs:enumeration violations" do
86
+ errors = product(status: "archived").validate
87
+
88
+ expect(errors.map(&:message)).to include(a_string_including("status"))
89
+ end
90
+
91
+ it "collects missing required attribute violations" do
92
+ errors = product(id: nil).validate
93
+
94
+ expect(errors.map(&:message)).to include(
95
+ a_string_including("attribute 'id' is required"),
96
+ )
97
+ end
98
+
99
+ it "collects xs:decimal lexical violations" do
100
+ errors = product(price: "free").validate
101
+
102
+ expect(errors.map(&:message)).to include(a_string_including("price"))
103
+ end
104
+
105
+ it "collects xs:date lexical violations" do
106
+ errors = product(released: "tomorrow").validate
107
+
108
+ expect(errors.map(&:message)).to include(a_string_including("released"))
109
+ end
110
+
111
+ it "collects maxOccurs violations" do
112
+ errors = product(tags: %w[a b c d]).validate
113
+
114
+ expect(errors.map(&:message)).to include(a_string_including("tag"))
115
+ end
116
+
117
+ it "collects violations inside nested complex types" do
118
+ bad_nested = XsdDimensions.new(width: "wide", height: "20")
119
+ errors = product(dimensions: bad_nested).validate
120
+
121
+ expect(errors.map(&:message)).to include(a_string_including("width"))
122
+ end
123
+ end
124
+
125
+ describe "multiple schema paths" do
126
+ before do
127
+ base = fixture_path("validate_xml_with_person.xsd")
128
+ strict = fixture_path("validate_xml_with_person_strict.xsd")
129
+
130
+ stub_const("XsdDualSchemaPerson",
131
+ Class.new(Lutaml::Model::Serializable) do
132
+ attribute :name, :string
133
+ attribute :age, :string
134
+
135
+ xml do
136
+ element "person"
137
+ map_element "name", to: :name
138
+ map_element "age", to: :age
139
+ end
140
+
141
+ validate_xml_with base, strict
142
+ end)
143
+ end
144
+
145
+ it "passes when the document conforms to every schema" do
146
+ person = XsdDualSchemaPerson.new(name: "Alice", age: "30")
147
+
148
+ expect(person.validate).to be_empty
149
+ end
150
+
151
+ it "attributes violations to the schema that rejected the document" do
152
+ person = XsdDualSchemaPerson.new(name: "Old", age: "200")
153
+ errors = person.validate
154
+
155
+ expect(errors.size).to eq(1)
156
+ expect(errors.first.schema_path).to eq(
157
+ fixture_path("validate_xml_with_person_strict.xsd"),
158
+ )
159
+ end
160
+ end
161
+
162
+ describe "targetNamespace with qualified elements" do
163
+ let(:schema_path) { fixture_path("validate_xml_with_address.xsd") }
164
+
165
+ before do
166
+ path = schema_path
167
+ stub_const("XsdNamespacedDoc",
168
+ Class.new(Lutaml::Model::Serializable) do
169
+ attribute :city, :string
170
+
171
+ xml do
172
+ element "address"
173
+ map_element "city", to: :city
174
+ end
175
+
176
+ validate_xml_with path
177
+ end)
178
+ end
179
+
180
+ it "accepts a document in the target namespace" do
181
+ xml = '<address xmlns="http://example.com/address">' \
182
+ "<city>Berlin</city></address>"
183
+
184
+ expect(XsdNamespacedDoc.validate_xml(xml)).to be_empty
185
+ end
186
+
187
+ it "rejects a document missing the target namespace" do
188
+ xml = "<address><city>Berlin</city></address>"
189
+
190
+ expect(XsdNamespacedDoc.validate_xml(xml)).not_to be_empty
191
+ end
192
+ end
193
+
194
+ describe "xs:choice" do
195
+ before do
196
+ path = fixture_path("validate_xml_with_contact.xsd")
197
+ stub_const("XsdChoiceDoc",
198
+ Class.new(Lutaml::Model::Serializable) do
199
+ attribute :email, :string
200
+
201
+ xml do
202
+ element "contact"
203
+ map_element "email", to: :email
204
+ end
205
+
206
+ validate_xml_with path
207
+ end)
208
+ end
209
+
210
+ it "accepts a document using exactly one branch" do
211
+ xml = "<contact><email>a@b.c</email></contact>"
212
+
213
+ expect(XsdChoiceDoc.validate_xml(xml)).to be_empty
214
+ end
215
+
216
+ it "rejects a document using both branches" do
217
+ xml = "<contact><email>a@b.c</email><phone>123</phone></contact>"
218
+
219
+ expect(XsdChoiceDoc.validate_xml(xml)).not_to be_empty
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "tmpdir"
5
+
6
+ RSpec.describe "validate_xml_with" do
7
+ let(:schema_path) do
8
+ File.expand_path("../../fixtures/xml/validate_xml_with_person.xsd",
9
+ __dir__)
10
+ end
11
+
12
+ before do
13
+ path = schema_path
14
+ stub_const("XsdValidatedPerson",
15
+ Class.new(Lutaml::Model::Serializable) do
16
+ attribute :name, :string
17
+ attribute :age, :string
18
+
19
+ xml do
20
+ element "person"
21
+ map_element "name", to: :name
22
+ map_element "age", to: :age
23
+ end
24
+
25
+ validate_xml_with path
26
+ end)
27
+ end
28
+
29
+ context "when the generated XML conforms to the schema" do
30
+ let(:person) { XsdValidatedPerson.new(name: "Alice", age: "30") }
31
+
32
+ it "returns no errors from validate" do
33
+ expect(person.validate).to be_empty
34
+ end
35
+
36
+ it "does not raise on validate!" do
37
+ expect { person.validate! }.not_to raise_error
38
+ end
39
+ end
40
+
41
+ context "when the generated XML violates the schema" do
42
+ let(:person) { XsdValidatedPerson.new(name: "Alice", age: "abc") }
43
+
44
+ it "collects schema validation errors" do
45
+ errors = person.validate
46
+
47
+ expect(errors).to all(
48
+ be_a(Lutaml::Xml::Error::SchemaValidationError),
49
+ )
50
+ expect(errors.first.message).to include(schema_path)
51
+ end
52
+
53
+ it "raises ValidationError with the schema message on validate!" do
54
+ expect { person.validate! }.to raise_error(
55
+ Lutaml::Model::ValidationError,
56
+ /age/,
57
+ )
58
+ end
59
+ end
60
+
61
+ describe ".validate_xml / .validate_xml!" do
62
+ let(:valid_xml) do
63
+ "<person><name>Alice</name><age>30</age></person>"
64
+ end
65
+ let(:invalid_xml) do
66
+ "<person><name>Alice</name><age>abc</age></person>"
67
+ end
68
+
69
+ it "returns no errors for a conforming raw XML string" do
70
+ expect(XsdValidatedPerson.validate_xml(valid_xml)).to be_empty
71
+ end
72
+
73
+ it "collects errors for a non-conforming raw XML string" do
74
+ errors = XsdValidatedPerson.validate_xml(invalid_xml)
75
+
76
+ expect(errors).to all(
77
+ be_a(Lutaml::Xml::Error::SchemaValidationError),
78
+ )
79
+ end
80
+
81
+ it "raises ValidationError for a non-conforming raw XML string" do
82
+ expect do
83
+ XsdValidatedPerson.validate_xml!(invalid_xml)
84
+ end.to raise_error(Lutaml::Model::ValidationError, /age/)
85
+ end
86
+
87
+ it "raises a parse error for malformed XML instead of recovering" do
88
+ expect do
89
+ XsdValidatedPerson.validate_xml("<person><name>Alice</name>")
90
+ end.to raise_error(Nokogiri::XML::SyntaxError)
91
+ end
92
+ end
93
+
94
+ context "with schema path inheritance" do
95
+ before do
96
+ stub_const("XsdValidatedEmployee", Class.new(XsdValidatedPerson))
97
+ stub_const("XsdAugmentedEmployee",
98
+ Class.new(XsdValidatedPerson) do
99
+ validate_xml_with "extra/employee.xsd"
100
+ end)
101
+ end
102
+
103
+ it "subclasses inherit the parent's schema" do
104
+ employee = XsdValidatedEmployee.new(name: "Bob", age: "abc")
105
+
106
+ expect(employee.validate).not_to be_empty
107
+ end
108
+
109
+ it "a subclass macro call appends parent-first" do
110
+ expect(XsdAugmentedEmployee.xml_schema_paths).to eq(
111
+ [schema_path, File.expand_path("extra/employee.xsd", __dir__)],
112
+ )
113
+ end
114
+
115
+ it "does not leak child paths back to the parent" do
116
+ expect(XsdValidatedPerson.xml_schema_paths).to eq([schema_path])
117
+ end
118
+ end
119
+
120
+ context "with a schema path relative to the declaring file" do
121
+ before do
122
+ stub_const("RelativeSchemaPerson",
123
+ Class.new(Lutaml::Model::Serializable) do
124
+ attribute :name, :string
125
+ attribute :age, :string
126
+
127
+ xml do
128
+ element "person"
129
+ map_element "name", to: :name
130
+ map_element "age", to: :age
131
+ end
132
+
133
+ validate_xml_with(
134
+ "../../fixtures/xml/validate_xml_with_person.xsd",
135
+ )
136
+ end)
137
+ end
138
+
139
+ it "resolves the schema next to the declaring file, not the CWD" do
140
+ Dir.chdir(Dir.tmpdir) do
141
+ person = RelativeSchemaPerson.new(name: "Alice", age: "abc")
142
+
143
+ expect(person.validate).not_to be_empty
144
+ end
145
+ end
146
+ end
147
+
148
+ context "when no schema is configured" do
149
+ before do
150
+ stub_const("PlainPerson", Class.new(Lutaml::Model::Serializable) do
151
+ attribute :name, :string
152
+
153
+ xml do
154
+ element "person"
155
+ map_element "name", to: :name
156
+ end
157
+ end)
158
+ end
159
+
160
+ it "validates without touching XSD machinery" do
161
+ expect(PlainPerson.new(name: "Alice").validate).to be_empty
162
+ end
163
+ end
164
+
165
+ context "when the schema file does not exist" do
166
+ before do
167
+ stub_const("MissingSchemaPerson",
168
+ Class.new(Lutaml::Model::Serializable) do
169
+ attribute :name, :string
170
+
171
+ xml do
172
+ element "person"
173
+ map_element "name", to: :name
174
+ end
175
+
176
+ validate_xml_with "nonexistent/schema.xsd"
177
+ end)
178
+ end
179
+
180
+ it "raises a configuration-level error" do
181
+ expect do
182
+ MissingSchemaPerson.new(name: "Alice").validate
183
+ end.to raise_error(Errno::ENOENT)
184
+ end
185
+ end
186
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "liquid"
3
+ require "liquid" unless RUBY_ENGINE == "opal"
4
4
  require "rspec/matchers"
5
- require "canon"
5
+ require "canon" unless RUBY_ENGINE == "opal"
6
6
 
7
7
  RSpec.configure do |config|
8
8
  # Enable flags like --only-failures and --next-failure
@@ -71,11 +71,27 @@ RSpec.configure do |config|
71
71
 
72
72
  end
73
73
  end
74
+
75
+ # Under Opal, exclude specs that require native-only adapters,
76
+ # filesystem, subprocess, schema compilation, or liquid templates.
77
+ if RUBY_ENGINE == "opal"
78
+ config.filter_run_excluding(
79
+ :native_adapter,
80
+ :native_fs,
81
+ :subprocess,
82
+ :performance,
83
+ :examples,
84
+ :consistency,
85
+ :xsd_schema,
86
+ :relaxng,
87
+ :liquid,
88
+ )
89
+ end
74
90
  end
75
91
 
76
92
  # configuration example
77
- require_relative "../lib/lutaml/model"
78
- require_relative "../lib/lutaml/xml"
93
+ require "lutaml/model"
94
+ require "lutaml/xml"
79
95
  require_relative "support/test_namespaces"
80
96
  require_relative "support/test_adapter_config"
81
97
  # require_relative "../lib/lutaml/xml/adapter/nokogiri_adapter"
@@ -83,16 +99,17 @@ require_relative "support/test_adapter_config"
83
99
  # require_relative "../lib/lutaml/model/toml_adapter/toml_rb_adapter"
84
100
 
85
101
  Lutaml::Model::Config.configure do |config|
86
- config.xml_adapter_type = :nokogiri
102
+ config.xml_adapter_type = RUBY_ENGINE == "opal" ? :oga : :nokogiri
87
103
  config.hash_adapter_type = :standard
88
104
  config.json_adapter_type = :standard
89
105
  config.yaml_adapter_type = :standard
90
- config.toml_adapter_type = :toml_rb
106
+ # toml_rb uses native ext; not registered as a lutaml format under Opal.
107
+ config.toml_adapter_type = :toml_rb unless RUBY_ENGINE == "opal"
91
108
  end
92
109
 
93
- # Configure Canon for XML equivalence testing
94
- # Use spec_friendly profile which includes structural_whitespace: :ignore
95
- # This allows pretty-printed XML to be compared with compact XML
96
- Canon::Config.configure do |config|
97
- config.xml.match.profile = :spec_friendly
110
+ # Configure Canon for XML equivalence testing (Canon is not loaded under Opal)
111
+ if defined?(Canon::Config)
112
+ Canon::Config.configure do |config|
113
+ config.xml.match.profile = :spec_friendly
114
+ end
98
115
  end
data/spec/support/opal.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Opal runtime patches for lutaml-model specs
3
+ # Opal runtime patches for lutaml-model specs.
4
+ # Oga (via moxml's vendored opal-oga fork) is the default XML adapter
5
+ # under Opal; REXML (bundled stdlib gem, pure Ruby) is also available.
6
+ # Smoke specs parameterize over both — see spec/lutaml/xml/opal_xml_spec.rb.
4
7
  if RUBY_ENGINE == "opal"
5
- Lutaml::Model::Config.xml_adapter_type = :rexml
8
+ Lutaml::Model::Config.xml_adapter_type = :oga
6
9
  end