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,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Opal-only boot for moxml — mirrors moxml's shipped
4
+ # `lib/compat/opal/moxml_boot.rb` so both Oga and REXML adapters are
5
+ # eager-loaded (Opal ignores autoload; without this, nested Moxml
6
+ # constants NameError at runtime).
7
+ #
8
+ # Requires the REXML gem's lib dir to be on Opal's load path so
9
+ # `require "rexml/formatters/pretty"` (pulled in by moxml's
10
+ # customized_rexml/formatter) resolves. The Rakefile sets that up.
11
+ #
12
+ # moxml's own boot loads the same list. We maintain our own copy so we
13
+ # can reorder or gate entries if a future moxml release introduces an
14
+ # Opal-incompatible file before its shipped boot catches up.
15
+
16
+ if RUBY_ENGINE == "opal"
17
+ require "moxml/version"
18
+ require "moxml/error"
19
+ require "moxml/native_attachment"
20
+ require "moxml/native_attachment/opal"
21
+ require "moxml/xml_utils"
22
+ require "moxml/xml_utils/encoder"
23
+ require "moxml/node"
24
+ require "moxml/node_set"
25
+ require "moxml/document"
26
+ require "moxml/element"
27
+ require "moxml/attribute"
28
+ require "moxml/text"
29
+ require "moxml/cdata"
30
+ require "moxml/comment"
31
+ require "moxml/processing_instruction"
32
+ require "moxml/declaration"
33
+ require "moxml/namespace"
34
+ require "moxml/doctype"
35
+ require "moxml/entity_reference"
36
+ require "moxml/entity_registry"
37
+ require "moxml/adapter"
38
+ require "moxml/adapter/base"
39
+ require "moxml/adapter/customized_rexml"
40
+ require "moxml/adapter/customized_rexml/entity_reference"
41
+ require "moxml/adapter/customized_rexml/formatter"
42
+ require "moxml/sax"
43
+ require "moxml/sax/handler"
44
+ require "moxml/sax/element_handler"
45
+ require "moxml/sax/block_handler"
46
+ require "moxml/sax/namespace_splitter"
47
+ require "moxml/adapter/rexml"
48
+ require "moxml/adapter/customized_oga"
49
+ require "moxml/adapter/customized_oga/xml_declaration"
50
+ require "moxml/adapter/customized_oga/xml_generator"
51
+ require "moxml/adapter/oga"
52
+ require "moxml/document_builder"
53
+ require "moxml/builder"
54
+ require "moxml/context"
55
+ require "moxml/config"
56
+ require "moxml/xpath"
57
+ require "moxml/xpath/engine"
58
+ require "moxml/xpath/context"
59
+ require "moxml/xpath/conversion"
60
+ require "moxml/xpath/cache"
61
+ require "moxml/xpath/lexer"
62
+ require "moxml/xpath/parser"
63
+ require "moxml/xpath/compiler"
64
+ require "moxml/xpath/errors"
65
+ require "moxml/xpath/ast/node"
66
+ require "moxml/xpath/ruby/node"
67
+ require "moxml/xpath/ruby/generator"
68
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Opal runtime YAML compat.
4
+ #
5
+ # Opal's `nodejs/yaml` ships `YAML.load` only. lutaml-model's YAML adapter
6
+ # uses `YAML.safe_load(src, permitted_classes: [...])` and `YAML.dump(obj)`.
7
+ # Provide both as thin wrappers — nodejs/yaml is already backed by
8
+ # jsyaml.safeLoad, so `load` semantics are equivalent to MRI's safe_load
9
+ # with the default permitted classes (the perm_classes argument is
10
+ # accepted but ignored, matching Opal's stronger-by-default posture).
11
+
12
+ if RUBY_ENGINE == "opal"
13
+ require "nodejs/yaml"
14
+
15
+ module ::YAML
16
+ class << self
17
+ # Accept MRI's full safe_load kwargs for API parity; nodejs/yaml's
18
+ # safeLoad is already safe-by-default so the kwargs are unused.
19
+ def safe_load(yaml, **_kwargs)
20
+ load(yaml)
21
+ end
22
+
23
+ def dump(obj, io = nil)
24
+ dumped = `#{@__yaml__}.safeDump(#{obj})`
25
+ return dumped unless io
26
+
27
+ io.write(dumped)
28
+ io
29
+ end
30
+ end
31
+ end
32
+ end
data/lib/lutaml/json.rb CHANGED
@@ -34,7 +34,7 @@ module Lutaml
34
34
 
35
35
  # Detect available JSON adapters
36
36
  def self.detect_adapter
37
- return :standard if defined?(::JSON) && Lutaml::Model::RuntimeCompatibility.opal?
37
+ return :standard if defined?(::JSON) && Lutaml::Model.opal?
38
38
  return :oj if defined?(::Oj)
39
39
  return :multi_json if defined?(::MultiJson)
40
40
  return :standard if defined?(::JSON)
@@ -249,8 +249,9 @@ format)
249
249
  value = apply_value_map(value, rule.value_map(:from, options), attr)
250
250
 
251
251
  if rule.has_custom_method_for_deserialization?
252
- return process_custom_method(rule, instance,
253
- value)
252
+ return unless Lutaml::Model::Utils.present?(value)
253
+
254
+ return rule.deserialize(instance, value, attributes, model_class)
254
255
  end
255
256
 
256
257
  value = rule.transform_value(attr, value, :from, format)
@@ -265,12 +266,6 @@ format)
265
266
  rule.deserialize(instance, value, attributes, self)
266
267
  end
267
268
 
268
- def process_custom_method(rule, instance, value)
269
- return unless Lutaml::Model::Utils.present?(value)
270
-
271
- model_class.new.public_send(rule.custom_methods[:from], instance, value)
272
- end
273
-
274
269
  def cast_value(value, attr, format, rule, instance)
275
270
  cast_options = rule.polymorphic ? { polymorphic: rule.polymorphic } : {}
276
271
  cast_options[:lutaml_parent] = instance if instance
@@ -63,13 +63,26 @@ model_class: nil)
63
63
  return serialize_reference(value, rule)
64
64
  end
65
65
 
66
- if nested_model?(rule)
66
+ if Lutaml::Model::Type::Union.rule?(rule)
67
+ serialize_union(value, options)
68
+ elsif nested_model?(rule)
67
69
  serialize_nested_model(value, rule, options)
68
70
  else
69
71
  serialize_primitive(value, rule)
70
72
  end
71
73
  end
72
74
 
75
+ # Union: dispatch on the value's own class (stateless). A model member
76
+ # serializes via its class transformation; a scalar emits as-is.
77
+ def serialize_union(value, options)
78
+ unless value.is_a?(Lutaml::Model::Serialize)
79
+ return Lutaml::Model::Type::Union.serialize_scalar(value, format)
80
+ end
81
+
82
+ transform_nested_model(value, create_transformation(value.class),
83
+ options)
84
+ end
85
+
73
86
  # Check if a value is a nested model based on the rule.
74
87
  #
75
88
  # @param rule [CompiledRule] The compiled rule
@@ -936,9 +936,15 @@ child_mappings, options)
936
936
  def create_value_for_item(rule, value, options)
937
937
  return nil if value.nil?
938
938
 
939
- # Check if this is a nested model
940
- is_nested_model = rule.attribute_type.is_a?(Class) &&
941
- rule.attribute_type < Lutaml::Model::Serialize
939
+ # Union: dispatch on the value's own class (stateless, like polymorphism).
940
+ # A model member serializes via its class; a scalar member emits as-is.
941
+ union = Lutaml::Model::Type::Union.rule?(rule)
942
+ is_nested_model = if union
943
+ value.is_a?(Lutaml::Model::Serialize)
944
+ else
945
+ rule.attribute_type.is_a?(Class) &&
946
+ rule.attribute_type < Lutaml::Model::Serialize
947
+ end
942
948
 
943
949
  if is_nested_model
944
950
  # Validate that value matches the expected type
@@ -970,8 +976,9 @@ child_mappings, options)
970
976
  # If value is a subclass of the declared type, use its mappings instead
971
977
  # Also handle type substitution the same way
972
978
  actual_type = value.class
973
- uses_polymorphism = (actual_type != rule.attribute_type &&
974
- actual_type < rule.attribute_type) || uses_type_substitution
979
+ uses_polymorphism = union ||
980
+ (actual_type != rule.attribute_type &&
981
+ actual_type < rule.attribute_type) || uses_type_substitution
975
982
 
976
983
  # Get child transformation - may be cached or need to create
977
984
  child_transformation = rule.child_transformation
@@ -998,6 +1005,11 @@ child_mappings, options)
998
1005
  # Fallback: serialize as primitive
999
1006
  serialize_value(value, rule)
1000
1007
  end
1008
+ elsif union
1009
+ # Union scalar: serialize through the value type matching the value's
1010
+ # own class so output is canonical (e.g. BigDecimal -> "1.5"), not a
1011
+ # raw Ruby object.
1012
+ Lutaml::Model::Type::Union.serialize_scalar(value, format)
1001
1013
  else
1002
1014
  # Serialize primitive value
1003
1015
  serialize_value(value, rule)
@@ -231,7 +231,7 @@ module Lutaml
231
231
  end
232
232
 
233
233
  # Default key-value adapter loading
234
- adapter_path = if RuntimeCompatibility.opal?
234
+ adapter_path = if Lutaml::Model.opal?
235
235
  "lutaml/key_value/adapter/#{adapter}/#{type}"
236
236
  else
237
237
  File.join(File.dirname(__FILE__), "../key_value/adapter",
@@ -382,7 +382,7 @@ module Lutaml
382
382
  #
383
383
  # @return [Symbol, nil] :tomlib, :toml_rb, or nil
384
384
  def detect_toml_adapter
385
- return nil if RuntimeCompatibility.opal?
385
+ return nil if Lutaml::Model.opal?
386
386
 
387
387
  if RuntimeCompatibility.windows?
388
388
  return :toml_rb if Utils.safe_load("toml-rb", :TomlRb)
@@ -28,6 +28,7 @@ module Lutaml
28
28
  ref_model_class
29
29
  ref_key_attribute
30
30
  xsd_type
31
+ union_member_types
31
32
  ].freeze
32
33
 
33
34
  MODEL_STRINGS = [
@@ -239,6 +240,15 @@ module Lutaml
239
240
  @options[:polymorphic_class]
240
241
  end
241
242
 
243
+ def union?
244
+ unresolved_type == Lutaml::Model::Type::Union
245
+ end
246
+
247
+ def union_member_types
248
+ @union_member_types ||=
249
+ @options[:union_member_types].map { |member| cast_type!(member) }
250
+ end
251
+
242
252
  def derived?
243
253
  !method_name.nil?
244
254
  end
@@ -276,6 +286,43 @@ module Lutaml
276
286
  build_collection(value.map { |v| cast_element(v, register) })
277
287
  end
278
288
 
289
+ # Apply a value map to transform a value.
290
+ #
291
+ # value_map keys (:nil, :empty, :omitted) each map either to a symbolic
292
+ # option (:nil, :empty, :omitted) or, for Boolean attributes, directly
293
+ # to true/false. The nested form value_map[:from][key] = true/false is
294
+ # accepted unconditionally; the bare form value_map[key] = true/false
295
+ # is gated on the attribute being a Boolean type.
296
+ def apply_value_map(value, value_map)
297
+ key = if value.nil? then :nil
298
+ elsif Utils.empty?(value) then :empty
299
+ elsif Utils.uninitialized?(value) then :omitted
300
+ end
301
+ return value unless key
302
+
303
+ nested = value_map.dig(:from, key)
304
+ return nested if nested.is_a?(TrueClass) || nested.is_a?(FalseClass)
305
+
306
+ option = value_map[key]
307
+ if (option.is_a?(TrueClass) || option.is_a?(FalseClass)) &&
308
+ (type == Lutaml::Model::Type::Boolean ||
309
+ unresolved_type == Lutaml::Model::Type::Boolean)
310
+ return option
311
+ end
312
+
313
+ case option
314
+ when :nil
315
+ nil
316
+ when :empty
317
+ if key == :empty then value
318
+ elsif collection? then build_collection
319
+ else ""
320
+ end
321
+ else
322
+ Lutaml::Model::UninitializedClass.instance
323
+ end
324
+ end
325
+
279
326
  def required_value_set?(value)
280
327
  return true unless options[:required]
281
328
  return false if value.nil?
@@ -286,6 +333,7 @@ module Lutaml
286
333
 
287
334
  def cast_element(value, register)
288
335
  resolved_type = type(register)
336
+ return cast_union(value, nil, register) if union?
289
337
  return resolved_type.new(value) if value.is_a?(::Hash) && !hash_type?
290
338
 
291
339
  # Special handling for Reference types - pass the metadata
@@ -398,12 +446,19 @@ instance_object = nil)
398
446
  options[:values].include?(value)
399
447
  end
400
448
 
449
+ # Pattern binds string values only. For a plain :string attribute that is
450
+ # the resolved type; for a union, the pattern applies to a value that took
451
+ # the :string branch (a non-string member is exempt). Collections check
452
+ # each element, so a mix of members validates only its string entries.
401
453
  def valid_pattern!(value, resolved_type)
402
- return true unless resolved_type == Lutaml::Model::Type::String
403
454
  return true unless pattern
404
455
 
405
- unless pattern.match?(value)
406
- raise Lutaml::Model::PatternNotMatchedError.new(name, pattern, value)
456
+ Array(value).each do |item|
457
+ next unless resolved_type == Lutaml::Model::Type::String ||
458
+ item.is_a?(::String)
459
+ next if pattern.match?(item)
460
+
461
+ raise Lutaml::Model::PatternNotMatchedError.new(name, pattern, item)
407
462
  end
408
463
 
409
464
  true
@@ -419,11 +474,11 @@ instance_object = nil)
419
474
  # 2. Value count should be between the collection range if defined
420
475
  # e.g if collection: 0..5 is set then the value greater then 5
421
476
  # will raise `Lutaml::Model::CollectionCountOutOfRangeError`
422
- def validate_value!(value, register, resolver = nil)
477
+ def validate_value!(value, register, instance_object: nil)
423
478
  # Use the default value if the value is nil
424
479
  validate_required!(value)
425
480
 
426
- value = resolver&.default if value.nil?
481
+ value = cast_value(default_value(register, instance_object), register) if value.nil?
427
482
  resolved_type = type(register)
428
483
 
429
484
  valid_value!(value) &&
@@ -587,6 +642,8 @@ instance_object = nil)
587
642
 
588
643
  return value if already_serialized?(resolved_type, value)
589
644
 
645
+ return cast_union(value, format, register) if union?
646
+
590
647
  # Special handling for Reference types - pass the metadata
591
648
  # Check @options[:ref_model_class] which is set when type is { ref: [...] }
592
649
  if @options[:ref_model_class] && resolved_type == Lutaml::Model::Type::Reference
@@ -664,6 +721,11 @@ instance_object = nil)
664
721
 
665
722
  def process_options!
666
723
  validate_options!(@options)
724
+ if union?
725
+ @options[:union_member_types] =
726
+ Lutaml::Model::Type::Union.validate_members!(@options[:union_member_types])
727
+ Lutaml::Model::Type::Union.validate_combo!(@options)
728
+ end
667
729
  @raw = !!@options[:raw]
668
730
  if @raw
669
731
  warn "[DEPRECATED] attribute :#{name}, :string, raw: true is deprecated. " \
@@ -776,6 +838,13 @@ instance_object = nil)
776
838
 
777
839
  attr_writer :raw, :validations
778
840
 
841
+ def cast_union(value, format, register)
842
+ match = Lutaml::Model::Type::Union.conforming_member(
843
+ value, union_member_types, format: format, register: register
844
+ )
845
+ match&.last
846
+ end
847
+
779
848
  def validate_attr_type!(resolved_type)
780
849
  return true if resolved_type <= Serializable || resolved_type <= Type::Value
781
850
  return true if resolved_type.include?(Serialize)
@@ -931,12 +1000,20 @@ instance_object = nil)
931
1000
  "Called from #{caller(1..1).first}"
932
1001
  end
933
1002
 
934
- # No need to change user register#get_class, only checks if type is LutaML-Model string.
935
- # Using MODEL_STRINGS since pattern is only supported for String type.
936
- if options.key?(:pattern) && !MODEL_STRINGS.include?(type)
937
- raise StandardError,
938
- "Invalid option `pattern` given for `#{name}`, " \
939
- "`pattern` is only allowed for :string type"
1003
+ # pattern applies to string values: a plain :string attribute, or a
1004
+ # union that includes a :string member (the pattern binds that branch).
1005
+ # Guarded by the pattern key so non-pattern attributes never resolve
1006
+ # their type here (custom types may be absent from the default register).
1007
+ if options.key?(:pattern)
1008
+ union_with_string =
1009
+ type == Lutaml::Model::Type::Union &&
1010
+ Array(options[:union_member_types]).intersect?(MODEL_STRINGS)
1011
+ unless MODEL_STRINGS.include?(type) || union_with_string
1012
+ raise StandardError,
1013
+ "Invalid option `pattern` given for `#{name}`, " \
1014
+ "`pattern` is only allowed for :string type or a union " \
1015
+ "including a :string member"
1016
+ end
940
1017
  end
941
1018
 
942
1019
  if initialize_empty? && !collection?
@@ -36,7 +36,7 @@ module Lutaml
36
36
  attr_reader :delegate, :cache_backend
37
37
 
38
38
  def self.default_cache_backend
39
- if RuntimeCompatibility.opal?
39
+ if Lutaml::Model.opal?
40
40
  MutexHashCache.new
41
41
  else
42
42
  ConcurrentMapCache.new
@@ -0,0 +1,11 @@
1
+ module Lutaml
2
+ module Model
3
+ class UnionSchemaUnsupportedError < Error
4
+ def initialize(attribute, format)
5
+ super("Union-typed attribute `#{attribute}` cannot be exported to " \
6
+ "#{format}; union types are only representable in JSON Schema " \
7
+ "(anyOf).")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -324,10 +324,13 @@ context_id = nil)
324
324
  end
325
325
 
326
326
  class << self
327
- # Performance: Define delegation methods without closures
328
- # Using class_eval with string avoids closure allocation per call
327
+ # Block form: avoids Opal's runtime string parsing requirement
328
+ # (MRI's class_eval(string) needs `require 'opal-parser'` at
329
+ # runtime under Opal, which isn't bundled). Closures are
330
+ # allocated once at file load, not per call, so the perf cost
331
+ # over the previous string form is negligible.
329
332
 
330
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
333
+ class_eval do
331
334
  def registry
332
335
  instance.registry
333
336
  end
@@ -356,8 +359,8 @@ context_id = nil)
356
359
  instance.default_context
357
360
  end
358
361
 
359
- def context(*args, **kwargs, &block)
360
- instance.context(*args, **kwargs, &block)
362
+ def context(id = nil)
363
+ instance.context(id)
361
364
  end
362
365
 
363
366
  def clear_xml_namespace_registry!
@@ -404,8 +407,8 @@ context_id = nil)
404
407
  instance.resolvable?(name, ctx)
405
408
  end
406
409
 
407
- def with_context(ctx_id)
408
- instance.with_context(ctx_id) { yield }
410
+ def with_context(ctx_id, &block)
411
+ instance.with_context(ctx_id, &block)
409
412
  end
410
413
 
411
414
  def register_format_registry(format, registry)
@@ -440,7 +443,7 @@ context_id = nil)
440
443
  def namespace_register_map
441
444
  instance.namespace_register_map
442
445
  end
443
- RUBY
446
+ end
444
447
  end
445
448
  end
446
449
  end
@@ -155,7 +155,7 @@ module Lutaml
155
155
  #
156
156
  # @return [Float] the current monotonic time in seconds
157
157
  def monotonic_time
158
- return Time.now.to_f if Lutaml::Model::RuntimeCompatibility.opal?
158
+ return Time.now.to_f if Lutaml::Model.opal?
159
159
 
160
160
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
161
161
  end
@@ -164,7 +164,7 @@ module Lutaml
164
164
  #
165
165
  # @return [Integer, nil] memory in bytes, or nil if not available
166
166
  def memory_usage
167
- return nil if Lutaml::Model::RuntimeCompatibility.opal?
167
+ return nil if Lutaml::Model.opal?
168
168
  return nil unless defined?(GC)
169
169
 
170
170
  GC.start if RUBY_VERSION >= "2.7"
@@ -248,12 +248,6 @@ module Lutaml
248
248
  polymorphic_map && !polymorphic_map.empty?
249
249
  end
250
250
 
251
- def serialize_attribute(model, element, doc)
252
- if custom_methods[:to]
253
- model.public_send(custom_methods[:to], model, element, doc)
254
- end
255
- end
256
-
257
251
  def to_value_for(model)
258
252
  if delegate
259
253
  model.public_send(delegate).public_send(to)
@@ -48,7 +48,7 @@ module Lutaml
48
48
  # Cache for resolve_for_child results.
49
49
  # Key: [child_class.object_id, parent_register] → Value: resolved register Symbol or nil.
50
50
  # Classes are singletons so object_id is stable for the process lifetime.
51
- RESOLVE_CACHE = if RuntimeCompatibility.opal?
51
+ RESOLVE_CACHE = if Lutaml::Model.opal?
52
52
  {}
53
53
  else
54
54
  Concurrent::Map.new
@@ -82,7 +82,7 @@ module Lutaml
82
82
  def self.resolve_for_child(child_class, parent_register)
83
83
  cache_key = [child_class.object_id, parent_register]
84
84
 
85
- if RuntimeCompatibility.opal?
85
+ if Lutaml::Model.opal?
86
86
  return RESOLVE_CACHE[cache_key] if RESOLVE_CACHE.key?(cache_key)
87
87
 
88
88
  RESOLVE_CACHE[cache_key] =
@@ -91,4 +91,54 @@ if Lutaml::Model::RuntimeCompatibility.opal?
91
91
  end
92
92
  end
93
93
  end
94
+
95
+ # Opal has no `weakref` stdlib. Store uses WeakRef to let go of
96
+ # registered instances automatically once they go out of scope.
97
+ # Under Opal (browser JS GC) we substitute a strong reference —
98
+ # correct behavior, weaker memory profile, no API surface change.
99
+ unless defined?(WeakRef)
100
+ class ::WeakRef
101
+ class RefError < ::StandardError; end
102
+
103
+ def initialize(obj)
104
+ @__target__ = obj
105
+ end
106
+
107
+ def __getobj__
108
+ @__target__
109
+ end
110
+
111
+ def weakref_alive?
112
+ true
113
+ end
114
+
115
+ # Minimal method_missing so the WeakRef quacks like its target
116
+ # for the Store's index lookups.
117
+ def method_missing(name, *, &)
118
+ @__target__.public_send(name, *, &)
119
+ end
120
+
121
+ def respond_to_missing?(name, include_private = false)
122
+ @__target__.respond_to?(name, include_private)
123
+ end
124
+ end
125
+ end
126
+
127
+ # Opal's Module#prepend raises "Prepending a module multiple times
128
+ # is not supported" when a module is already in the target's ancestor
129
+ # chain. MRI's prepend is idempotent in that case (no-op, no error).
130
+ #
131
+ # Our top-level lib files (lib/lutaml/model.rb, lib/lutaml/xml.rb)
132
+ # can be re-evaluated under Opal's eager loader, which would trigger
133
+ # the raise on the second pass. Align Opal with MRI by making prepend
134
+ # a no-op when the module is already present.
135
+ class ::Module
136
+ unless method_defined?(:__lm_original_prepend__)
137
+ alias_method :__lm_original_prepend__, :prepend
138
+
139
+ def prepend(mod)
140
+ __lm_original_prepend__(mod) unless self <= mod
141
+ end
142
+ end
143
+ end
94
144
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ unless RUBY_ENGINE == "opal"
4
+ require "fileutils"
5
+ require "tmpdir"
6
+ end
7
+
8
+ module Lutaml
9
+ module Model
10
+ module Schema
11
+ # Writes a CompiledOutput to a temporary directory, requires the
12
+ # registry, and runs `register_all` so the generated classes are
13
+ # loaded into a real module the caller can use immediately. Shared
14
+ # between XSD and RNG compilers.
15
+ class ClassLoader
16
+ DEFAULT_NAMESPACE = "GeneratedModels"
17
+
18
+ def self.load(output, registry_generator: RegistryGenerator)
19
+ new(output, registry_generator).load
20
+ end
21
+
22
+ def initialize(output, registry_generator)
23
+ # ClassLoader needs a module to host the registry constant
24
+ # (so `register_all` has a home). If the caller wanted
25
+ # unwrapped class files, we still force a registry module —
26
+ # but we pin `source_module_namespace` to what they asked for,
27
+ # so per-class files keep their original wrapping.
28
+ @output = CompiledOutput.new(
29
+ entries: output.entries,
30
+ module_namespace: output.module_namespace || DEFAULT_NAMESPACE,
31
+ register_id: output.register_id,
32
+ source_module_namespace: output.module_namespace,
33
+ )
34
+ @registry_generator = registry_generator
35
+ end
36
+
37
+ def load
38
+ Dir.mktmpdir do |dir|
39
+ FileWriter.write(@output, dir, registry_generator: @registry_generator)
40
+ require File.join(dir, "#{registry_basename}_registry")
41
+ call_register_all
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def registry_basename
48
+ @output.module_namespace.split("::").last.downcase
49
+ end
50
+
51
+ def call_register_all
52
+ mod = Object.const_get(@output.module_namespace)
53
+ mod.register_all if mod.respond_to?(:register_all)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end