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
data/lib/lutaml/toml.rb CHANGED
@@ -30,7 +30,7 @@ Lutaml::Model::FormatRegistry.register(
30
30
  adapter_class: nil,
31
31
  transformer: Lutaml::Toml::Adapter::Transform,
32
32
  key_value: true,
33
- adapter_options: if Lutaml::Model::RuntimeCompatibility.opal?
33
+ adapter_options: if Lutaml::Model.opal?
34
34
  nil
35
35
  else
36
36
  {
@@ -13,11 +13,11 @@ module Lutaml
13
13
  autoload :NamespaceUriCollector,
14
14
  "#{__dir__}/adapter/namespace_uri_collector"
15
15
  autoload :OgaAdapter, "#{__dir__}/adapter/oga_adapter"
16
+ autoload :RexmlAdapter, "#{__dir__}/adapter/rexml_adapter"
16
17
  Lutaml::Model::RuntimeCompatibility.autoload_native(
17
18
  self,
18
19
  NokogiriAdapter: "#{__dir__}/adapter/nokogiri_adapter",
19
20
  OxAdapter: "#{__dir__}/adapter/ox_adapter",
20
- RexmlAdapter: "#{__dir__}/adapter/rexml_adapter",
21
21
  )
22
22
  end
23
23
  end
@@ -13,7 +13,7 @@ module Lutaml
13
13
  # @param _adapter [String] The adapter format name ("xml")
14
14
  # @param type [String] The normalized type name (e.g., "nokogiri_adapter")
15
15
  def self.load_adapter_file(_adapter, type)
16
- adapter_path = if Lutaml::Model::RuntimeCompatibility.opal?
16
+ adapter_path = if Lutaml::Model.opal?
17
17
  "lutaml/xml/adapter/#{type}"
18
18
  else
19
19
  File.join(File.dirname(__FILE__), "adapter", type)
@@ -14,7 +14,7 @@ module Lutaml
14
14
  class Base
15
15
  def self.build(options = {})
16
16
  context = Moxml.new(moxml_backend)
17
- if Lutaml::Model::RuntimeCompatibility.opal?
17
+ if Lutaml::Model.opal?
18
18
  context.config.namespace_validation_mode = :lenient
19
19
  end
20
20
 
@@ -6,11 +6,11 @@ module Lutaml
6
6
  module Builder
7
7
  autoload :Base, "#{__dir__}/builder/base"
8
8
  autoload :Oga, "#{__dir__}/builder/oga"
9
+ autoload :Rexml, "#{__dir__}/builder/rexml"
9
10
  Lutaml::Model::RuntimeCompatibility.autoload_native(
10
11
  self,
11
12
  Nokogiri: "#{__dir__}/builder/nokogiri",
12
13
  Ox: "#{__dir__}/builder/ox",
13
- Rexml: "#{__dir__}/builder/rexml",
14
14
  )
15
15
  end
16
16
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Xml
5
+ module Error
6
+ # Collected when a model's generated XML does not conform to the
7
+ # XSD schema configured via the `validate_xml_with` macro.
8
+ #
9
+ # One instance represents one schema violation reported by the
10
+ # validating parser, so Lutaml::Model::ValidationError#error_messages
11
+ # lists each violation individually.
12
+ #
13
+ # Not to be confused with Schema::Xsd::SchemaValidationError, which
14
+ # reports problems in an XSD document itself.
15
+ class SchemaValidationError < XmlError
16
+ attr_reader :schema_path
17
+
18
+ def initialize(message, schema_path)
19
+ @schema_path = schema_path
20
+ super("XML does not conform to schema #{schema_path}: #{message}")
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -708,7 +708,9 @@ _effective_register)
708
708
  base_cast_options[:resolved_type] = attr_type
709
709
 
710
710
  children.each do |child|
711
- if !rule_has_custom_method && attr_type_is_serializable
711
+ if !rule_has_custom_method &&
712
+ (attr_type_is_serializable ||
713
+ (attr&.union? && ::Lutaml::Model::Type::Union.xml_structured?(child)))
712
714
  # Performance: Build cast_options efficiently (dup + []= cheaper than merge)
713
715
  cast_options = if (child_namespace_uri = child.namespace_uri)
714
716
  ns_type = attr.type_with_namespace(effective_register,
@@ -984,6 +986,8 @@ effective_register = lutaml_register)
984
986
  # @return [Boolean] true if a matching child likely exists
985
987
  def child_matches_rule?(rule, child_names_set, default_namespace)
986
988
  rule_names = rule.namespaced_names(default_namespace)
989
+ # Array#intersect? requires an Array argument; child_names_set may
990
+ # be a Set, so use the block form which works for any includable.
987
991
  rule_names.any? { |rn| child_names_set.include?(rn) } ||
988
992
  child_names_set.include?(rule.name.to_s)
989
993
  end
@@ -31,6 +31,11 @@ module Lutaml
31
31
 
32
32
  def self.generate_attributes(xml, klass, register)
33
33
  klass.attributes.each do |name, attr|
34
+ if attr.union?
35
+ raise Lutaml::Model::UnionSchemaUnsupportedError.new(name,
36
+ "RelaxNG")
37
+ end
38
+
34
39
  attr_type = attr.type(register)
35
40
  if attr_type <= Lutaml::Model::Serialize
36
41
  xml.ref(name: attr_type.name)
@@ -409,6 +409,10 @@ attr_name)
409
409
 
410
410
  def self.get_attribute_xsd_type(attr, attr_type, register,
411
411
  _mapping_rule = nil)
412
+ if attr.union?
413
+ raise Lutaml::Model::UnionSchemaUnsupportedError.new(attr.name, "XSD")
414
+ end
415
+
412
416
  # 1. Check for deprecated attribute-level xsd_type override
413
417
  return attr.options[:xsd_type] if attr.options[:xsd_type]
414
418
 
@@ -44,6 +44,50 @@ module Lutaml
44
44
  @namespace_class&.prefix_default
45
45
  end
46
46
 
47
+ # Configure one or more XSD schemas that the model's generated
48
+ # XML must conform to. Checked during validate/validate!
49
+ # (issue #264). Repeated calls (including in subclasses) append.
50
+ #
51
+ # Relative paths resolve against the file that declares the macro,
52
+ # so a model works regardless of the process working directory.
53
+ #
54
+ # @param paths [Array<String, Pathname>] paths to XSD schema files
55
+ def validate_xml_with(*paths)
56
+ base = File.dirname(caller_locations(1, 1).first.path)
57
+ @own_xml_schema_paths = own_xml_schema_paths +
58
+ paths.flatten.map { |path| File.expand_path(path.to_s, base) }
59
+ end
60
+
61
+ # All configured XSD schema paths, parent-first: inherited paths
62
+ # come before paths declared on this class.
63
+ #
64
+ # @return [Array<String>]
65
+ def xml_schema_paths
66
+ inherited = if superclass.respond_to?(:xml_schema_paths)
67
+ superclass.xml_schema_paths
68
+ else
69
+ []
70
+ end
71
+ inherited + own_xml_schema_paths
72
+ end
73
+
74
+ # Validate a raw XML string against the configured schemas without
75
+ # instantiating a model — the explicit input-side counterpart to
76
+ # the validate/validate! output-side check.
77
+ #
78
+ # @param xml [String] an XML document
79
+ # @return [Array<Lutaml::Xml::Error::SchemaValidationError>]
80
+ def validate_xml(xml)
81
+ XsdValidator.validate(xml, xml_schema_paths)
82
+ end
83
+
84
+ # @raise [Lutaml::Model::ValidationError] when the XML does not
85
+ # conform to every configured schema
86
+ def validate_xml!(xml)
87
+ errors = validate_xml(xml)
88
+ raise Lutaml::Model::ValidationError.new(errors) if errors.any?
89
+ end
90
+
47
91
  # Override choice to set format: :xml so Choice knows which format it belongs to.
48
92
  def choice(min: 1, max: 1, format: :xml, &)
49
93
  super
@@ -204,6 +248,10 @@ module Lutaml
204
248
 
205
249
  private
206
250
 
251
+ def own_xml_schema_paths
252
+ @own_xml_schema_paths ||= []
253
+ end
254
+
207
255
  # Handle XML mapping class inheritance pattern (e.g., `xml SomeMapping`)
208
256
  #
209
257
  # When a mapping class is passed, inherit mappings from it directly.
@@ -263,6 +263,19 @@ module Lutaml
263
263
  self.class.mappings_for(:xml, register)&.element_sequence
264
264
  end
265
265
 
266
+ # Append XSD schema errors (validate_xml_with macro) to the
267
+ # standard validation errors. No-op unless schemas are configured.
268
+ #
269
+ # @see Lutaml::Model::Validation#validate
270
+ def validate(register: Lutaml::Model::Config.default_register)
271
+ errors = super
272
+ return errors if self.class.xml_schema_paths.empty?
273
+
274
+ errors.concat(
275
+ self.class.validate_xml(to_xml(register: register)),
276
+ )
277
+ end
278
+
266
279
  private
267
280
 
268
281
  # Build declaration plan from stored element reference (lazy mode).
@@ -28,13 +28,22 @@ register_id, register)
28
28
  # class AND the value is not a nil/empty marker from collection handlers.
29
29
  # Nil and empty string markers should create simple elements, not attempt
30
30
  # nested model transformation.
31
- is_nested_model = rule.attribute_type.is_a?(Class) &&
32
- rule.attribute_type < Lutaml::Model::Serialize &&
33
- !value.nil? &&
34
- !(value.is_a?(String) && value.empty?)
31
+ # Union: dispatch on the value's own class (stateless). A model member
32
+ # becomes a nested element; a scalar member a simple value element.
33
+ union = Lutaml::Model::Type::Union.rule?(rule)
34
+
35
+ is_nested_model = !value.nil? &&
36
+ !(value.is_a?(String) && value.empty?) &&
37
+ if union
38
+ value.is_a?(Lutaml::Model::Serialize)
39
+ else
40
+ rule.attribute_type.is_a?(Class) &&
41
+ rule.attribute_type < Lutaml::Model::Serialize
42
+ end
35
43
 
36
44
  if is_nested_model
37
- create_nested_model_element(rule, value, options, register)
45
+ create_nested_model_element(rule, value, options, register,
46
+ union: union)
38
47
  else
39
48
  create_simple_value_element(rule, value, options, model_class,
40
49
  register_id)
@@ -119,7 +128,8 @@ parent_element_form_default)
119
128
  # @param options [Hash] Options
120
129
  # @param register [Register, nil] The register
121
130
  # @return [::Lutaml::Xml::DataModel::XmlElement] The created element
122
- def create_nested_model_element(rule, value, options, register)
131
+ def create_nested_model_element(rule, value, options, register,
132
+ union: false)
123
133
  # Resolve polymorphic configuration
124
134
  polymorphic_config = rule.options[:polymorphic]
125
135
  is_polymorphic = polymorphic_config.is_a?(::Hash) ? !polymorphic_config.empty? : !!polymorphic_config
@@ -130,8 +140,11 @@ parent_element_form_default)
130
140
  actual_class = resolve_polymorphic_class(rule, value, is_polymorphic,
131
141
  is_polymorphic_subtype)
132
142
 
133
- # Get transformation for the actual class
134
- child_transformation = if is_polymorphic || is_polymorphic_subtype
143
+ # Get transformation for the actual class. Unions resolve the member
144
+ # from the value's own class, like polymorphism.
145
+ child_transformation = if union
146
+ value.class.transformation_for(:xml, register)
147
+ elsif is_polymorphic || is_polymorphic_subtype
135
148
  actual_class.transformation_for(:xml,
136
149
  register)
137
150
  else
@@ -81,6 +81,12 @@ model_class, register_id)
81
81
  # @param options [Hash] Options
82
82
  # @yield Block to create element for value
83
83
  def apply_element_rule_single(parent:, rule:, value:, options:)
84
+ # raw: :element — value is a complete XML element string, inject directly
85
+ if rule.raw == :element
86
+ add_raw_element_fragments(parent, value)
87
+ return
88
+ end
89
+
84
90
  # Extract parent's namespace info for element_form_default inheritance
85
91
  parent_ns_class = parent.namespace_class
86
92
  # Only pass element_form_default VALUE if it was explicitly set
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent" unless RUBY_ENGINE == "opal"
4
+
5
+ module Lutaml
6
+ module Xml
7
+ # Validates a serialized XML string against an XSD schema file.
8
+ #
9
+ # Backs the `validate_xml_with` class macro (issue #264). Nokogiri is
10
+ # required lazily so models that do not use XSD validation keep working
11
+ # on platforms/adapters without it (Opal, ox-only installs).
12
+ #
13
+ # Compiled schemas are memoized by absolute path: the XSD files are
14
+ # constant for the life of the process, so each is read and compiled once
15
+ # and reused across every validate / validate_xml call.
16
+ #
17
+ # Not to be confused with Schema::Xsd::SchemaValidator, which checks
18
+ # that an XSD document itself is well-formed XSD.
19
+ module XsdValidator
20
+ # Concurrent (and a working Nokogiri) are unavailable under Opal, so
21
+ # the memoization store is only built off-Opal. validate raises a clear
22
+ # NotImplementedError under Opal before this store is ever consulted.
23
+ @schemas = ::Concurrent::Map.new unless RUBY_ENGINE == "opal"
24
+
25
+ # @param xml [String] an XML document
26
+ # @param schema_paths [Array<String>] paths to XSD schema files
27
+ # @return [Array<Error::SchemaValidationError>] one error per violation
28
+ def self.validate(xml, schema_paths)
29
+ paths = Array(schema_paths)
30
+ return [] if paths.empty?
31
+
32
+ if RUBY_ENGINE == "opal"
33
+ raise NotImplementedError,
34
+ "XSD schema validation (validate_xml_with) requires the " \
35
+ "nokogiri gem, which is not available under Opal."
36
+ end
37
+
38
+ ensure_nokogiri!
39
+ # Strict parsing: libxml2 recovery would otherwise repair malformed
40
+ # input and report it as schema-valid. NONET remains on by default.
41
+ document = ::Nokogiri::XML(xml, &:strict)
42
+ paths.flat_map do |path|
43
+ schema_for(path)
44
+ .validate(document)
45
+ .map { |error| Error::SchemaValidationError.new(error.message, path) }
46
+ end
47
+ end
48
+
49
+ def self.schema_for(path)
50
+ @schemas.compute_if_absent(path) do
51
+ ::Nokogiri::XML::Schema(File.read(path))
52
+ end
53
+ end
54
+
55
+ def self.ensure_nokogiri!
56
+ require "nokogiri"
57
+ rescue LoadError
58
+ raise Error::XmlConfigurationError,
59
+ "XSD schema validation (validate_xml_with) requires the " \
60
+ "nokogiri gem; add `gem \"nokogiri\"` to your Gemfile"
61
+ end
62
+
63
+ private_class_method :ensure_nokogiri!, :schema_for
64
+ end
65
+ end
66
+ end
data/lib/lutaml/xml.rb CHANGED
@@ -11,6 +11,15 @@ require_relative "model"
11
11
  # XML requires moxml for parsing
12
12
  require "moxml"
13
13
 
14
+ # Under Opal, moxml's autoloads don't fire (Opal ignores autoload).
15
+ # Pull in the eager-load boot that ships at lib/compat/opal/moxml_boot.rb
16
+ # so Moxml::Adapter::{Oga,Rexml} etc. are actually defined. Without
17
+ # this, `Config.adapter = :oga` later fails with
18
+ # `NameError: uninitialized constant Moxml::Adapter::Oga`.
19
+ if Lutaml::Model.opal?
20
+ require "moxml_boot"
21
+ end
22
+
14
23
  module Lutaml
15
24
  module Xml
16
25
  # Error module for XML-specific errors
@@ -24,6 +33,8 @@ module Lutaml
24
33
  "#{__dir__}/xml/error/xml_configuration_error"
25
34
  autoload :NamespaceMismatchError,
26
35
  "#{__dir__}/xml/error/namespace_mismatch_error"
36
+ autoload :SchemaValidationError,
37
+ "#{__dir__}/xml/error/schema_validation_error"
27
38
  end
28
39
 
29
40
  # XML Configuration modules
@@ -86,6 +97,7 @@ module Lutaml
86
97
  autoload :CustomMethodWrapper,
87
98
  "#{__dir__}/xml/transformation/custom_method_wrapper"
88
99
  autoload :Transform, "#{__dir__}/xml/transform"
100
+ autoload :XsdValidator, "#{__dir__}/xml/xsd_validator"
89
101
  autoload :Adapter, "#{__dir__}/xml/adapter"
90
102
  autoload :XmlElement, "#{__dir__}/xml/xml_element"
91
103
  autoload :AdapterElement, "#{__dir__}/xml/adapter_element"
@@ -143,12 +155,12 @@ module Lutaml
143
155
  autoload :SharedDsl, "#{__dir__}/xml/shared_dsl"
144
156
 
145
157
  autoload :Oga, "#{__dir__}/xml/oga"
158
+ autoload :Rexml, "#{__dir__}/xml/rexml"
146
159
  Lutaml::Model::RuntimeCompatibility.autoload_native(
147
160
  self,
148
161
  NokogiriElement: "#{__dir__}/xml/nokogiri/element",
149
162
  Nokogiri: "#{__dir__}/xml/nokogiri",
150
163
  Ox: "#{__dir__}/xml/ox",
151
- Rexml: "#{__dir__}/xml/rexml",
152
164
  )
153
165
  end
154
166
  end
@@ -168,10 +180,15 @@ Lutaml::Model::FormatRegistry.register(
168
180
  Ox::ParseError
169
181
  REXML::ParseException
170
182
  ],
171
- adapter_options: if Lutaml::Model::RuntimeCompatibility.opal?
183
+ adapter_options: if Lutaml::Model.opal?
184
+ # Both Oga (vendored opal-oga fork, pure-Ruby lexer)
185
+ # and REXML (bundled stdlib gem, pure Ruby) work
186
+ # under Opal. Oga is the default because moxml's CI
187
+ # verifies it most thoroughly; REXML is selectable
188
+ # for callers that prefer a stdlib-only stack.
172
189
  {
173
- available: %i[rexml],
174
- default: :rexml,
190
+ available: %i[oga rexml],
191
+ default: :oga,
175
192
  }
176
193
  else
177
194
  {
@@ -210,18 +227,24 @@ Lutaml::Model::Serialize.prepend(
210
227
  # to classes that included it before the prepend. Serializable includes
211
228
  # Serialize during model boot, so make the XML instance API available on the
212
229
  # concrete base class as well.
213
- if Lutaml::Model::RuntimeCompatibility.opal?
230
+ #
231
+ # Anonymous model classes (Class.new { include Serialize }) extend
232
+ # Serialize::ClassMethods rather than inheriting from Serializable, so the
233
+ # ModelImportExt override (root?) must land on ClassMethods too. The other
234
+ # two (FormatConversion, InstanceMethods) are already prepended unconditionally
235
+ # above, so Opal's no-double-prepend rule means we don't repeat them here.
236
+ if Lutaml::Model.opal?
214
237
  Lutaml::Model::Serializable.singleton_class.prepend(
215
238
  Lutaml::Xml::Serialization::ModelImportExt,
216
239
  )
217
240
 
218
- Lutaml::Model::Serializable.singleton_class.prepend(
219
- Lutaml::Xml::Serialization::FormatConversion,
220
- )
221
-
222
241
  Lutaml::Model::Serializable.prepend(
223
242
  Lutaml::Xml::Serialization::InstanceMethods,
224
243
  )
244
+
245
+ Lutaml::Model::Serialize::ClassMethods.prepend(
246
+ Lutaml::Xml::Serialization::ModelImportExt,
247
+ )
225
248
  end
226
249
 
227
250
  # Register XML-specific attribute override warning names
@@ -240,7 +263,7 @@ Lutaml::Xml::Type::Serializers.register_all!
240
263
 
241
264
  # Register XML schema methods
242
265
  Lutaml::Model::Schema.register_method(:to_xsd) do |klass, options = {}|
243
- if Lutaml::Model::RuntimeCompatibility.opal?
266
+ if Lutaml::Model.opal?
244
267
  raise NotImplementedError,
245
268
  "XSD schema generation is not available under Opal."
246
269
  end
@@ -249,7 +272,7 @@ Lutaml::Model::Schema.register_method(:to_xsd) do |klass, options = {}|
249
272
  end
250
273
 
251
274
  Lutaml::Model::Schema.register_method(:to_relaxng) do |klass, options = {}|
252
- if Lutaml::Model::RuntimeCompatibility.opal?
275
+ if Lutaml::Model.opal?
253
276
  raise NotImplementedError,
254
277
  "RELAX NG schema generation requires Nokogiri, " \
255
278
  "which is not available under Opal."
@@ -259,7 +282,7 @@ Lutaml::Model::Schema.register_method(:to_relaxng) do |klass, options = {}|
259
282
  end
260
283
 
261
284
  Lutaml::Model::Schema.register_method(:from_xml) do |xml, options = {}|
262
- if Lutaml::Model::RuntimeCompatibility.opal?
285
+ if Lutaml::Model.opal?
263
286
  raise NotImplementedError,
264
287
  "XML schema compilation is not available under Opal."
265
288
  end
@@ -267,6 +290,24 @@ Lutaml::Model::Schema.register_method(:from_xml) do |xml, options = {}|
267
290
  Lutaml::Model::Schema::XmlCompiler.to_models(xml, options)
268
291
  end
269
292
 
293
+ Lutaml::Model::Schema.register_method(:from_relaxng) do |rng, options = {}|
294
+ if Lutaml::Model::RuntimeCompatibility.opal?
295
+ raise NotImplementedError,
296
+ "RELAX NG schema compilation is not available under Opal."
297
+ end
298
+
299
+ Lutaml::Model::Schema::RngCompiler.to_models(rng, options)
300
+ end
301
+
302
+ Lutaml::Model::Schema.register_method(:from_rnc) do |rnc, options = {}|
303
+ if Lutaml::Model::RuntimeCompatibility.opal?
304
+ raise NotImplementedError,
305
+ "RNC schema compilation is not available under Opal."
306
+ end
307
+
308
+ Lutaml::Model::Schema::RncCompiler.to_models(rnc, options)
309
+ end
310
+
270
311
  # Register XML namespace registry with GlobalContext
271
312
  Lutaml::Model::GlobalContext.register_format_registry(
272
313
  :xml, Lutaml::Xml::NamespaceClassRegistry.new
data/lutaml-model.gemspec CHANGED
@@ -23,9 +23,11 @@ Gem::Specification.new do |spec|
23
23
  # Specify which files should be added to the gem when it is released.
24
24
  # The `git ls-files -z` loads the files in the
25
25
  # RubyGem that have been added into git.
26
+ # vendor/ is excluded so the opal-oga / opal-ruby-ll submodule checkouts
27
+ # are not shipped in the published gem.
26
28
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
27
29
  `git ls-files -z`.split("\x0").reject do |f|
28
- f.match(%r{^(test|features)/})
30
+ f.match(%r{^(test|features|vendor/)/})
29
31
  end
30
32
  end
31
33
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
@@ -36,7 +38,7 @@ Gem::Specification.new do |spec|
36
38
  spec.add_dependency "canon"
37
39
  spec.add_dependency "concurrent-ruby"
38
40
  spec.add_dependency "liquid", ">= 4.0", "< 6.0"
39
- spec.add_dependency "moxml", "~> 0.1.23"
41
+ spec.add_dependency "moxml", "~> 0.1.25"
40
42
  spec.add_dependency "ostruct"
41
43
  spec.add_dependency "rubyzip", "~> 2.3"
42
44
  spec.add_dependency "thor"
@@ -0,0 +1,15 @@
1
+ addressBook =
2
+ element addressBook {
3
+ card*
4
+ }
5
+
6
+ card =
7
+ element card {
8
+ name,
9
+ email
10
+ }
11
+
12
+ name = element name { text }
13
+ email = element email { text }
14
+
15
+ start |= addressBook
@@ -0,0 +1,18 @@
1
+ namespace a = "http://relaxng.org/ns/compatibility/annotations/1.0"
2
+
3
+ book =
4
+ element book {
5
+ attribute xml:lang { text }?,
6
+ [ a:defaultValue = "paperback" ]
7
+ attribute format { "hardcover" | "paperback" }?,
8
+ attribute anchor { xsd:ID }?,
9
+ attribute indent { text | "adaptive" }?,
10
+ title,
11
+ \list
12
+ }
13
+
14
+ title = element title { text }
15
+ \list = element list { item+ }
16
+ item = element item { text }
17
+
18
+ start |= book
@@ -0,0 +1,6 @@
1
+ book =
2
+ element book {
3
+ title
4
+ }
5
+
6
+ title = element title { text }
@@ -0,0 +1,8 @@
1
+ include "circular_b.rnc"
2
+
3
+ circular_a =
4
+ element circularA {
5
+ empty
6
+ }
7
+
8
+ start |= circular_a
@@ -0,0 +1,8 @@
1
+ include "circular_a.rnc"
2
+
3
+ circular_b =
4
+ element circularB {
5
+ empty
6
+ }
7
+
8
+ start |= circular_b
@@ -0,0 +1,8 @@
1
+ include "book.rnc"
2
+
3
+ library =
4
+ element library {
5
+ book+
6
+ }
7
+
8
+ start |= library
@@ -0,0 +1,16 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <grammar xmlns="http://relaxng.org/ns/structure/1.0">
3
+ <start>
4
+ <element name="addressBook">
5
+ <zeroOrMore>
6
+ <element name="card">
7
+ <ref name="cardContent"/>
8
+ </element>
9
+ </zeroOrMore>
10
+ </element>
11
+ </start>
12
+ <define name="cardContent">
13
+ <element name="name"><text/></element>
14
+ <element name="email"><text/></element>
15
+ </define>
16
+ </grammar>
@@ -0,0 +1,27 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ Exercises: <attribute>, <optional>, <oneOrMore>, <group>, <choice>,
4
+ <empty>, <data type="..."/>
5
+ -->
6
+ <grammar xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
7
+ <start>
8
+ <element name="book">
9
+ <attribute name="isbn"><data type="string"/></attribute>
10
+ <element name="title"><text/></element>
11
+ <optional>
12
+ <element name="subtitle"><text/></element>
13
+ </optional>
14
+ <oneOrMore>
15
+ <element name="author"><text/></element>
16
+ </oneOrMore>
17
+ <group>
18
+ <element name="publisher"><text/></element>
19
+ <element name="year"><data type="integer"/></element>
20
+ </group>
21
+ <choice>
22
+ <element name="hardcover"><empty/></element>
23
+ <element name="paperback"><empty/></element>
24
+ </choice>
25
+ </element>
26
+ </start>
27
+ </grammar>
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <grammar xmlns="http://relaxng.org/ns/structure/1.0">
3
+ <start>
4
+ <element name="thing">
5
+ <attribute name="version">
6
+ <value>1.0</value>
7
+ </attribute>
8
+ <element name="name"><text/></element>
9
+ </element>
10
+ </start>
11
+ </grammar>