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
@@ -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
@@ -36,6 +36,36 @@ module Lutaml
36
36
  @attributes << @model.attribute(name, type, options)
37
37
  end
38
38
 
39
+ # Restrict options on a predefined or imported attribute within this choice
40
+ #
41
+ # @param name [Symbol] The attribute name to restrict
42
+ # @param options [Hash] New options to merge
43
+ # @return [Symbol] The attribute name
44
+ def restrict(name, options = {})
45
+ @model.restrict(name, options)
46
+ attr = @model.attributes[name]
47
+ unless @attributes.include?(attr)
48
+ attr.options[:choice] = self
49
+ @attributes << attr
50
+ end
51
+ invalidate_cache!
52
+ name
53
+ end
54
+
55
+ # Remove an attribute from this choice block
56
+ #
57
+ # @param name [Symbol] The attribute name to remove
58
+ # @return [Boolean] true if the attribute was removed
59
+ def remove_attribute(name)
60
+ attr = @attributes.find { |a| !a.is_a?(Choice) && a.name == name }
61
+ return nil unless attr
62
+
63
+ @attributes.delete(attr)
64
+ attr.options.delete(:choice)
65
+ invalidate_cache!
66
+ attr
67
+ end
68
+
39
69
  def choice(min: 1, max: 1, &block)
40
70
  @attributes << Choice.new(@model, min, max, format: @format).tap do |c|
41
71
  c.instance_eval(&block)
@@ -163,6 +193,10 @@ register = nil)
163
193
 
164
194
  private
165
195
 
196
+ def invalidate_cache!
197
+ @flat_attributes = nil
198
+ end
199
+
166
200
  def raise_errors(choices_hash)
167
201
  flat_attr_names = flat_attributes.map { |attr| attr.name.to_s }
168
202
  choices_hash.each do |choice_attr, count|
@@ -98,6 +98,13 @@ module Lutaml
98
98
  !custom_methods.empty?
99
99
  end
100
100
 
101
+ # Check if form is explicitly set
102
+ #
103
+ # @return [Boolean] true if form option was provided
104
+ def form_set?
105
+ !form.nil?
106
+ end
107
+
101
108
  # Check if a name matches this rule (including alias names for multiple mappings)
102
109
  #
103
110
  # @param name [String, Symbol] The name to check
@@ -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
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ # Pure value object holding the output of compilation. No I/O — just
7
+ # data. FileWriter / ClassLoader collaborators consume this to perform
8
+ # the impure parts of `to_models`. Shared across schema compilers (RNG,
9
+ # XSD, future formats).
10
+ #
11
+ # Entries are an ordered list of tagged records, each carrying a
12
+ # CamelCase class name, a payload (either a Definitions::* spec or a
13
+ # pre-rendered Ruby source String), and a `kind` of `:model` or
14
+ # `:namespace`.
15
+ #
16
+ # `:model` entries are written to disk AND registered with
17
+ # `register_all`. `:namespace` entries are written and autoloaded but
18
+ # never registered.
19
+ class CompiledOutput
20
+ Entry = Struct.new(:name, :payload, :kind)
21
+
22
+ attr_reader :entries, :module_namespace, :register_id,
23
+ :source_module_namespace
24
+
25
+ # `module_namespace` is what the central registry file wraps in
26
+ # (and where `register_all` lives). `source_module_namespace`
27
+ # is what the per-class files wrap in — defaults to the same,
28
+ # but ClassLoader can pass a different value when the caller
29
+ # asked for unwrapped classes but a registry constant is still
30
+ # needed to drive autoload.
31
+ def initialize(entries:, module_namespace: nil, register_id: :default,
32
+ source_module_namespace: :inherit)
33
+ @entries = entries
34
+ @module_namespace = module_namespace
35
+ @register_id = register_id
36
+ @source_module_namespace =
37
+ source_module_namespace == :inherit ? module_namespace : source_module_namespace
38
+ end
39
+
40
+ # Model-only entries — what the registry generator and ClassLoader
41
+ # iterate when emitting `register_all` calls.
42
+ def models
43
+ @models ||= entries.select { |e| e.kind == :model }
44
+ end
45
+
46
+ # Namespace-only entries — autoloaded but never registered.
47
+ def namespaces
48
+ @namespaces ||= entries.select { |e| e.kind == :namespace }
49
+ end
50
+
51
+ # Flat `name => Ruby source` hash covering every entry (models +
52
+ # namespaces). Used by FileWriter for the per-file write loop and
53
+ # by RNG / XSD compilers as the public `to_models` return value.
54
+ # Class files are rendered against `source_module_namespace`,
55
+ # which usually equals `module_namespace` but differs when the
56
+ # caller wanted unwrapped class files inside a registry module.
57
+ def sources
58
+ @sources ||= entries.to_h { |e| [e.name, source_for(e)] }
59
+ end
60
+
61
+ private
62
+
63
+ def source_for(entry)
64
+ renderer_for(entry.payload).render(
65
+ entry.payload,
66
+ module_namespace: @source_module_namespace,
67
+ register_id: @register_id,
68
+ )
69
+ end
70
+
71
+ def renderer_for(spec)
72
+ case spec
73
+ when Definitions::Model then Renderers::Model
74
+ when Definitions::RestrictedType then Renderers::RestrictedType
75
+ when Definitions::UnionType then Renderers::Union
76
+ when Definitions::Namespace then Renderers::Namespace
77
+ else raise ArgumentError, "Unknown spec type: #{spec.class}"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Definitions
7
+ # One attribute on a generated class.
8
+ class Attribute
9
+ attr_accessor :name, :type, :xml_name, :kind, :collection,
10
+ :default, :documentation, :initialize_empty,
11
+ :render_default, :render_empty
12
+
13
+ def initialize(name:, type:, xml_name:, kind:,
14
+ collection: false, default: nil,
15
+ documentation: nil, initialize_empty: false,
16
+ render_default: false, render_empty: false)
17
+ @name = name
18
+ @type = type
19
+ @xml_name = xml_name
20
+ @kind = kind
21
+ @collection = collection
22
+ @default = default
23
+ @documentation = documentation
24
+ @initialize_empty = initialize_empty
25
+ @render_default = render_default
26
+ @render_empty = render_empty
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end