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
@@ -22,7 +22,7 @@ RSpec.describe Lutaml::Model::Serializable, "namespace directive" do
22
22
  model_class = Class.new do
23
23
  include Lutaml::Model::Serialize
24
24
  end
25
- model_class.namespace(test_namespace)
25
+ expect { model_class.namespace(test_namespace) }.to output.to_stderr
26
26
 
27
27
  expect(model_class.namespace).to eq(test_namespace)
28
28
  end
@@ -49,7 +49,7 @@ RSpec.describe Lutaml::Model::Serializable, "namespace directive" do
49
49
  model_class = Class.new do
50
50
  include Lutaml::Model::Serialize
51
51
  end
52
- model_class.namespace(test_namespace)
52
+ expect { model_class.namespace(test_namespace) }.to output.to_stderr
53
53
  model_class.namespace(other_namespace)
54
54
 
55
55
  expect(model_class.namespace).to eq(other_namespace)
@@ -61,7 +61,7 @@ RSpec.describe Lutaml::Model::Serializable, "namespace directive" do
61
61
  model_class = Class.new do
62
62
  include Lutaml::Model::Serialize
63
63
  end
64
- model_class.namespace(test_namespace)
64
+ expect { model_class.namespace(test_namespace) }.to output.to_stderr
65
65
 
66
66
  expect(model_class.namespace_uri).to eq("https://example.com/model")
67
67
  end
@@ -80,7 +80,7 @@ RSpec.describe Lutaml::Model::Serializable, "namespace directive" do
80
80
  model_class = Class.new do
81
81
  include Lutaml::Model::Serialize
82
82
  end
83
- model_class.namespace(test_namespace)
83
+ expect { model_class.namespace(test_namespace) }.to output.to_stderr
84
84
 
85
85
  expect(model_class.namespace_prefix).to eq("model")
86
86
  end
@@ -103,7 +103,7 @@ RSpec.describe Lutaml::Model::Serializable, "namespace directive" do
103
103
  attribute :value, :integer
104
104
  end
105
105
 
106
- model_class.namespace(test_namespace)
106
+ expect { model_class.namespace(test_namespace) }.to output.to_stderr
107
107
 
108
108
  expect(model_class.namespace).to eq(test_namespace)
109
109
  expect(model_class.attributes.keys).to include(:name, :value)
@@ -113,7 +113,7 @@ RSpec.describe Lutaml::Model::Serializable, "namespace directive" do
113
113
  parent_class = Class.new do
114
114
  include Lutaml::Model::Serialize
115
115
  end
116
- parent_class.namespace(test_namespace)
116
+ expect { parent_class.namespace(test_namespace) }.to output.to_stderr
117
117
 
118
118
  child_class = Class.new(parent_class)
119
119
 
@@ -453,7 +453,7 @@ RSpec.describe "Type Namespace Examples" do
453
453
  serialized = props.to_xml
454
454
 
455
455
  # xsi:type attribute should use xsi: namespace from XsiTypeType
456
- expect(serialized).to match(/xsi:type="dcterms:W3CDTF"/)
456
+ expect(serialized).to include('xsi:type="dcterms:W3CDTF"')
457
457
  end
458
458
 
459
459
  it "handles multiple Type namespaces in single document" do
@@ -189,8 +189,8 @@ RSpec.describe "Type-level namespace integration" do
189
189
  expect(xml).to include('id="doc1"')
190
190
  expect(xml).to include('title="Test"')
191
191
  # Should not have any namespace prefix
192
- expect(xml).not_to match(/:id=/)
193
- expect(xml).not_to match(/:title=/)
192
+ expect(xml).not_to include(":id=")
193
+ expect(xml).not_to include(":title=")
194
194
  end
195
195
  end
196
196
 
@@ -268,11 +268,11 @@ RSpec.describe "Type-level namespace integration" do
268
268
  document_class = Class.new do
269
269
  include Lutaml::Model::Serialize
270
270
 
271
- namespace model_namespace
272
271
  attribute :special_field, special_type
273
272
 
274
273
  xml do
275
274
  element "document"
275
+ namespace model_namespace
276
276
  map_element "special_field", to: :special_field
277
277
  end
278
278
 
@@ -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
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/yamlld"
5
+
6
+ RSpec.describe Lutaml::YamlLd::Adapter do
7
+ let(:yamlld_hash) do
8
+ {
9
+ "@context" => { "name" => "http://example.org/name" },
10
+ "@type" => "Thing",
11
+ "name" => "test",
12
+ }
13
+ end
14
+
15
+ describe ".parse" do
16
+ it "parses valid YAML-LD string to hash" do
17
+ yaml = YAML.dump(yamlld_hash)
18
+ result = described_class.parse(yaml)
19
+ expect(result).to eq(yamlld_hash)
20
+ end
21
+ end
22
+
23
+ describe "#to_yamlld" do
24
+ it "generates YAML-LD string from hash" do
25
+ adapter = described_class.new(yamlld_hash)
26
+ result = adapter.to_yamlld
27
+ parsed = YAML.safe_load(result)
28
+ expect(parsed["@context"]).to eq({ "name" => "http://example.org/name" })
29
+ end
30
+ end
31
+
32
+ it "round-trips parse → generate" do
33
+ yaml = YAML.dump(yamlld_hash)
34
+ parsed = described_class.parse(yaml)
35
+ adapter = described_class.new(parsed)
36
+ result = adapter.to_yamlld
37
+ round_tripped = YAML.safe_load(result)
38
+ expect(round_tripped).to eq(yamlld_hash)
39
+ end
40
+
41
+ describe "safe_load enforcement" do
42
+ it "rejects YAML with disallowed class tags" do
43
+ unsafe = "--- !ruby/object:Object {}\n"
44
+ expect do
45
+ described_class.parse(unsafe)
46
+ end.to raise_error(Psych::DisallowedClass)
47
+ end
48
+
49
+ it "rejects malformed YAML with Psych::SyntaxError" do
50
+ malformed = "@context: [unterminated"
51
+ expect do
52
+ described_class.parse(malformed)
53
+ end.to raise_error(Psych::SyntaxError)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/jsonld"
5
+ require "lutaml/yamlld"
6
+
7
+ RSpec.describe ":yamlld format registration" do
8
+ it "is registered with FormatRegistry as an RDF format" do
9
+ expect(Lutaml::Model::FormatRegistry.rdf_formats).to include(:yamlld)
10
+ end
11
+
12
+ it "uses Rdf::Mapping as the mapping class" do
13
+ expect(Lutaml::Model::FormatRegistry.mappings_class_for(:yamlld))
14
+ .to eq(Lutaml::Rdf::Mapping)
15
+ end
16
+
17
+ it "uses YamlLd::Adapter as the adapter class" do
18
+ expect(Lutaml::Model::FormatRegistry.adapter_class_for(:yamlld))
19
+ .to eq(Lutaml::YamlLd::Adapter)
20
+ end
21
+
22
+ it "shares LinkedDataTransform with :jsonld" do
23
+ expect(Lutaml::Model::FormatRegistry.transformer_for(:yamlld))
24
+ .to eq(Lutaml::Rdf::LinkedDataTransform)
25
+ expect(Lutaml::Model::FormatRegistry.transformer_for(:yamlld))
26
+ .to eq(Lutaml::Model::FormatRegistry.transformer_for(:jsonld))
27
+ end
28
+
29
+ it "declares Psych::SyntaxError as a parse error type" do
30
+ expect(Lutaml::Model::FormatRegistry.error_types_for(:yamlld))
31
+ .to include("Psych::SyntaxError")
32
+ end
33
+ 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