lutaml-model 0.8.56 → 0.8.106

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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/.github/scripts/test_installed_gem.rb +58 -0
  3. data/.github/workflows/downstream-performance.yml +28 -3
  4. data/.github/workflows/js-pr-check.yml +4 -0
  5. data/.github/workflows/js-sync.yml +4 -0
  6. data/.github/workflows/opal.yml +9 -0
  7. data/.github/workflows/rake.yml +6 -41
  8. data/.github/workflows/release.yml +41 -0
  9. data/Gemfile +17 -3
  10. data/bench/bench_alloc_audit.rb +56 -0
  11. data/bench/bench_common.rb +11 -0
  12. data/bench/bench_merge_attempts.rb +49 -0
  13. data/docs/_guides/gem-integration.adoc +85 -0
  14. data/docs/_guides/grammar-backed-formats.adoc +69 -0
  15. data/docs/_guides/index.adoc +8 -1
  16. data/docs/_guides/native-engines.adoc +23 -5
  17. data/docs/_guides/schema-generation.adoc +25 -0
  18. data/docs/_guides/type-registers-contexts.adoc +126 -0
  19. data/lib/compat/opal/lutaml_model_boot.rb +7 -5
  20. data/lib/lutaml/json/adapter/yeptris_adapter.rb +24 -1
  21. data/lib/lutaml/key_value/data_model/element.rb +5 -1
  22. data/lib/lutaml/key_value/mapping_rule.rb +7 -0
  23. data/lib/lutaml/key_value/transform.rb +29 -24
  24. data/lib/lutaml/key_value/transformation/collection_serializer.rb +12 -29
  25. data/lib/lutaml/key_value/transformation/value_serializer.rb +29 -6
  26. data/lib/lutaml/key_value/transformation.rb +64 -116
  27. data/lib/lutaml/model/adapter_resolver.rb +7 -3
  28. data/lib/lutaml/model/attribute.rb +74 -15
  29. data/lib/lutaml/model/cached_type_resolver.rb +13 -6
  30. data/lib/lutaml/model/configuration.rb +13 -0
  31. data/lib/lutaml/model/mapping/mapping.rb +13 -0
  32. data/lib/lutaml/model/mapping/mapping_rule.rb +6 -1
  33. data/lib/lutaml/model/render_policy.rb +26 -8
  34. data/lib/lutaml/model/schema/base_schema.rb +15 -0
  35. data/lib/lutaml/model/schema/generator/definition.rb +52 -8
  36. data/lib/lutaml/model/schema/generator/definitions_collection.rb +35 -14
  37. data/lib/lutaml/model/schema.rb +4 -2
  38. data/lib/lutaml/model/serializable.rb +33 -0
  39. data/lib/lutaml/model/serialize/attribute_definition.rb +196 -2
  40. data/lib/lutaml/model/serialize/builder.rb +22 -0
  41. data/lib/lutaml/model/serialize/conversion_caching.rb +5 -0
  42. data/lib/lutaml/model/serialize/format_conversion.rb +49 -41
  43. data/lib/lutaml/model/serialize/initialization.rb +54 -7
  44. data/lib/lutaml/model/serialize/model_import.rb +60 -0
  45. data/lib/lutaml/model/serialize.rb +14 -6
  46. data/lib/lutaml/model/type/time.rb +6 -0
  47. data/lib/lutaml/model/type/value.rb +9 -7
  48. data/lib/lutaml/model/type_resolver.rb +7 -5
  49. data/lib/lutaml/model/utils.rb +18 -5
  50. data/lib/lutaml/model/version.rb +1 -1
  51. data/lib/lutaml/model.rb +7 -0
  52. data/lib/lutaml/xml/adapter/xml_parser.rb +5 -2
  53. data/lib/lutaml/xml/adapter_loader.rb +5 -2
  54. data/lib/lutaml/xml/format.rb +18 -16
  55. data/lib/lutaml/xml/leptris/native_emission.rb +187 -0
  56. data/lib/lutaml/xml/leptris/plan_compiler.rb +650 -0
  57. data/lib/lutaml/xml/leptris/plan_hydrator.rb +748 -0
  58. data/lib/lutaml/xml/leptris/plan_order.rb +55 -0
  59. data/lib/lutaml/xml/leptris/plan_serializer.rb +702 -0
  60. data/lib/lutaml/xml/leptris/plan_walk.rb +36 -0
  61. data/lib/lutaml/xml/leptris.rb +46 -0
  62. data/lib/lutaml/xml/mapping.rb +71 -18
  63. data/lib/lutaml/xml/model_transform.rb +231 -8
  64. data/lib/lutaml/xml/namespace.rb +18 -8
  65. data/lib/lutaml/xml/namespace_collector.rb +17 -1
  66. data/lib/lutaml/xml/namespace_needs.rb +77 -49
  67. data/lib/lutaml/xml/parse_session.rb +7 -19
  68. data/lib/lutaml/xml/schema/xsd/schema.rb +2 -1
  69. data/lib/lutaml/xml/serialization/format_conversion.rb +5 -1
  70. data/lib/lutaml/xml/transformation/ordered_applier.rb +21 -15
  71. data/lib/lutaml/xml/transformation.rb +15 -1
  72. data/lib/lutaml/xml/type/serializers.rb +8 -1
  73. data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +34 -2
  74. data/lib/tasks/performance_probe_plan_walk.rb +1 -1
  75. data/lutaml-model.gemspec +7 -4
  76. data/spec/lutaml/json/yeptris_adapter_spec.rb +7 -2
  77. data/spec/lutaml/key_value/transformation/value_serializer_spec.rb +9 -5
  78. data/spec/lutaml/key_value/transformation_spec.rb +87 -21
  79. data/spec/lutaml/model/any_element_mapping_spec.rb +41 -0
  80. data/spec/lutaml/model/collection_validation_spec.rb +265 -0
  81. data/spec/lutaml/model/desc_spec.rb +50 -0
  82. data/spec/lutaml/model/late_attribute_declaration_spec.rb +35 -0
  83. data/spec/lutaml/model/model_import_reentry_spec.rb +46 -0
  84. data/spec/lutaml/model/opal_smoke_spec.rb +17 -0
  85. data/spec/lutaml/model/schema/generate_many_spec.rb +61 -0
  86. data/spec/lutaml/model/schema/generation_regressions_spec.rb +139 -0
  87. data/spec/lutaml/model/schema/json_schema_spec.rb +1 -13
  88. data/spec/lutaml/model/schema/yaml_schema_spec.rb +0 -11
  89. data/spec/lutaml/model/serialize/attribute_definition_block_form_spec.rb +244 -0
  90. data/spec/lutaml/model/store_spec.rb +6 -2
  91. data/spec/lutaml/model/unregistered_context_resolution_spec.rb +34 -0
  92. data/spec/lutaml/model/writer_assigned_attribute_spec.rb +62 -0
  93. data/spec/lutaml/model/yeptris_error_parity_spec.rb +95 -0
  94. data/spec/lutaml/xml/adapter/empty_document_diagnostics_spec.rb +50 -0
  95. data/spec/lutaml/xml/adapter/over_cap_parse_spec.rb +105 -0
  96. data/spec/lutaml/xml/adapter_loader_spec.rb +14 -0
  97. data/spec/lutaml/xml/adapter_metadata_default_spec.rb +16 -0
  98. data/spec/lutaml/xml/attribute_ambiguity_surface_spec.rb +5 -0
  99. data/spec/lutaml/xml/clear_parse_state_spec.rb +6 -1
  100. data/spec/lutaml/xml/compiled_rule_namespace_spec.rb +5 -0
  101. data/spec/lutaml/xml/cross_register_plan_spec.rb +75 -0
  102. data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +16 -8
  103. data/spec/lutaml/xml/element_order_tracking_spec.rb +37 -0
  104. data/spec/lutaml/xml/leptris_string_encoding_spec.rb +65 -0
  105. data/spec/lutaml/xml/opal_xml_spec.rb +47 -0
  106. data/spec/lutaml/xml/parse_dom_retention_spec.rb +34 -0
  107. data/spec/lutaml/xml/plan_fast_path_spec.rb +122 -14
  108. data/spec/lutaml/xml/plan_namespaced_serialize_spec.rb +157 -0
  109. data/spec/lutaml/xml/plan_ns_less_document_spec.rb +86 -0
  110. data/spec/lutaml/xml/plan_partition_spec.rb +1 -1
  111. data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +131 -0
  112. data/spec/lutaml/xml/plan_uri_exact_dispatch_spec.rb +287 -0
  113. data/spec/lutaml/xml/plan_walk_spec.rb +6 -6
  114. data/spec/lutaml/xml/plan_wildcard_any_spec.rb +96 -0
  115. data/spec/lutaml/yaml/yeptris_adapter_spec.rb +29 -0
  116. data/spec/person_spec.rb +8 -0
  117. metadata +48 -17
  118. data/lib/lutaml/xml/plan_compiler.rb +0 -332
  119. data/lib/lutaml/xml/plan_hydrator.rb +0 -367
  120. data/lib/lutaml/xml/plan_order.rb +0 -53
  121. data/lib/lutaml/xml/plan_serializer.rb +0 -252
  122. data/lib/lutaml/xml/plan_walk.rb +0 -30
@@ -209,7 +209,10 @@ register = self.register)
209
209
  def transform(model_instance, options = {})
210
210
  # For key-value formats, we typically don't have a named root
211
211
  # Instead, we create an anonymous root that holds all attributes
212
- root = Lutaml::KeyValue::DataModel::Element.new("__root__")
212
+ # Hash-direct emission (TODO.perf 19): the __root__ Element
213
+ # tree was write-only in production — every consumer flattened
214
+ # it straight back to a hash. The output hash is built directly.
215
+ root = {}
213
216
 
214
217
  # Apply each compiled rule (with filtering support). A
215
218
  # when_attribute partition applies once for its whole key.
@@ -230,11 +233,7 @@ register = self.register)
230
233
 
231
234
  if ENV["DEBUG_KEYED_COLLECTION"]
232
235
  puts "=== TRANSFORM COMPLETE ==="
233
- puts "root.children.count: #{root.children.count}"
234
- root.children.each do |child|
235
- puts " child key=#{child.key.inspect}, value=#{child.value.inspect}, to_hash=#{child.to_hash.inspect}"
236
- end
237
- puts "root.to_hash: #{root.to_hash.inspect}"
236
+ puts "root: #{root.inspect}"
238
237
  end
239
238
 
240
239
  root
@@ -289,15 +288,9 @@ register = self.register)
289
288
  end
290
289
  return unless present
291
290
 
292
- element = Lutaml::KeyValue::DataModel::Element.new(
293
- group.first.serialized_name,
294
- )
295
- if items.empty?
296
- element.value = []
297
- else
298
- items.each { |item| element.add_child(item) }
299
- end
300
- parent.add_child(element)
291
+ # Items are stamped hashes or primitives: the tree's
292
+ # children_to_array semantics, expressed directly.
293
+ parent[group.first.serialized_name] = items
301
294
  end
302
295
 
303
296
  def stamp_discriminator(item, rule)
@@ -332,16 +325,33 @@ register = self.register)
332
325
  # Handle custom serialization methods (e.g., with: { to: ... })
333
326
  if rule.has_custom_methods? && rule.custom_methods[:to]
334
327
  to_method = rule.custom_methods[:to]
328
+ # Custom methods receive an Element "doc" and populate it
329
+ # (public contract, TODO.perf 19 keeps it): a scratch element
330
+ # is merged into the output hash afterwards.
331
+ doc = Lutaml::KeyValue::DataModel::Element.new("__root__")
332
+ # Custom methods may write children, or doc.value[key]=, or
333
+ # nothing at all (pubid 2.0 regression: an empty scratch
334
+ # flattened to {key => nil} and crashed the merge). Seed the
335
+ # value hash and merge only hash-shaped results.
336
+ doc.value = {}
335
337
  # lutaml-model#550: custom methods may declare a third context
336
338
  # parameter to receive the options passed to `to_*`.
337
339
  if model_instance.method(to_method).parameters.size >= 3
338
- return model_instance.public_send(to_method, model_instance,
339
- parent, options[:context])
340
+ model_instance.public_send(to_method, model_instance,
341
+ doc, options[:context])
342
+ else
343
+ model_instance.public_send(to_method, model_instance, doc)
340
344
  end
341
-
342
- # Call custom method which directly modifies the parent element
343
- return model_instance.public_send(to_method, model_instance,
344
- parent)
345
+ # The 0.8.68 contract: only the method's doc mutations count —
346
+ # its return value is ignored. (A mutation-style handler whose
347
+ # last statement is an assignment returns the assigned inner
348
+ # value; honoring it merged child content at the wrong level —
349
+ # lutaml-model#892.) Empty docs serialize as nothing, matching
350
+ # the pre-0.8.74 behavior.
351
+ merged = doc.to_hash
352
+ parent.update(merged["__root__"]) if merged.is_a?(::Hash) &&
353
+ merged["__root__"].is_a?(::Hash)
354
+ return
345
355
  end
346
356
 
347
357
  # Handle delegation - extract value from delegated object
@@ -450,17 +460,9 @@ converted_from_empty_to_nil: false, converted_from_nil_to_empty: false)
450
460
  to_map = (rule.option(:value_map) || {})[:to] || {}
451
461
  nil_mapping = to_map[:nil]
452
462
  if nil_mapping == :empty || converted_from_nil_to_empty
453
- # Render as empty collection
454
- element = Lutaml::KeyValue::DataModel::Element.new(
455
- rule.serialized_name, []
456
- )
457
- parent.add_child(element)
463
+ parent[rule.serialized_name] = []
458
464
  elsif nil_mapping == :nil || converted_from_empty_to_nil
459
- # Render nil collection as nil value
460
- element = Lutaml::KeyValue::DataModel::Element.new(
461
- rule.serialized_name, nil
462
- )
463
- parent.add_child(element)
465
+ parent[rule.serialized_name] = nil
464
466
  else
465
467
  # Skip nil collection
466
468
  end
@@ -519,10 +521,10 @@ converted_from_empty_to_nil: false, converted_from_nil_to_empty: false)
519
521
  # Transform the value to a KeyValueElement
520
522
  value_root = value_transformation.transform(attr_value,
521
523
  options)
522
- value_hash = value_root.to_hash
523
524
 
524
- # Extract the value from the __root__ wrapper
525
- parent.value[key_value.to_s] = value_hash["__root__"]
525
+ # transform emits the output hash directly now
526
+ # (TODO.perf 19) — no __root__ wrapper to unwrap.
527
+ parent[key_value.to_s] = value_root
526
528
  next
527
529
  end
528
530
  end
@@ -540,7 +542,7 @@ converted_from_empty_to_nil: false, converted_from_nil_to_empty: false)
540
542
  # Get the child transformation for the collection item type
541
543
  if item_type.is_a?(Class) && item_type < Lutaml::Model::Serialize
542
544
  # Create a temporary parent KeyValueElement to hold the collection
543
- temp_parent = Lutaml::KeyValue::DataModel::Element.new("__temp__")
545
+ temp_parent = {}
544
546
 
545
547
  # Convert to array
546
548
  coll_items = Array(attr_value)
@@ -577,18 +579,15 @@ converted_from_empty_to_nil: false, converted_from_nil_to_empty: false)
577
579
  coll_items, options)
578
580
  end
579
581
 
580
- # Extract the value from temp_parent
581
- temp_hash = temp_parent.to_hash
582
- inner_value = temp_hash["__temp__"]
583
- coll_value = inner_value&.[](value_attribute.to_s) if inner_value
582
+ coll_value = temp_parent[value_attribute.to_s]
584
583
 
585
- parent.value[key_value.to_s] = coll_value
584
+ parent[key_value.to_s] = coll_value
586
585
  next
587
586
  end
588
587
  end
589
588
 
590
589
  # Fallback: just use the value directly
591
- parent.value[key_value.to_s] = attr_value
590
+ parent[key_value.to_s] = attr_value
592
591
  end
593
592
  # Case 2: Only key attribute specified - serialize all other attributes
594
593
  elsif key_attribute
@@ -662,7 +661,7 @@ converted_from_empty_to_nil: false, converted_from_nil_to_empty: false)
662
661
  end
663
662
  end
664
663
 
665
- parent.value[key_value.to_s] = item_hash unless item_hash.empty?
664
+ parent[key_value.to_s] = item_hash unless item_hash.empty?
666
665
  end
667
666
  end
668
667
 
@@ -872,17 +871,7 @@ child_mappings, options)
872
871
  puts " keyed_hash: #{keyed_hash.inspect}"
873
872
  end
874
873
 
875
- # Create element with hash value
876
- element = Lutaml::KeyValue::DataModel::Element.new(
877
- rule.serialized_name, keyed_hash
878
- )
879
-
880
- if ENV["DEBUG_KEYED_COLLECTION"]
881
- puts " element created: key=#{element.key.inspect}, value=#{element.value.inspect}, children.count=#{element.children.count}"
882
- puts " element.to_hash: #{element.to_hash.inspect}"
883
- end
884
-
885
- parent.add_child(element)
874
+ parent[rule.serialized_name] = keyed_hash
886
875
  end
887
876
 
888
877
  # Create an array collection element (default)
@@ -892,23 +881,13 @@ child_mappings, options)
892
881
  # @param items [Array] The collection items
893
882
  # @param options [Hash] Options
894
883
  def create_array_collection_element(parent, rule, items, options)
895
- # Create an element for the collection
896
- coll_element = Lutaml::KeyValue::DataModel::Element.new(rule.serialized_name)
897
-
898
- if items.empty?
899
- # For empty collections, set value to empty array explicitly
900
- coll_element.value = []
901
- else
902
- # Add each item as a child
903
- items.each do |item|
904
- child_value = create_value_for_item(rule, item, options)
905
- # Use explicit nil check - `if child_value` would fail for boolean false!
906
- coll_element.add_child(child_value) unless child_value.nil?
907
- end
884
+ # children_to_array semantics directly: always an array,
885
+ # nil children skipped (explicit nil check — booleans matter).
886
+ built = items.each_with_object([]) do |item, memo|
887
+ child_value = create_value_for_item(rule, item, options)
888
+ memo << child_value unless child_value.nil?
908
889
  end
909
-
910
- # Always add collection element
911
- parent.add_child(coll_element)
890
+ parent[rule.serialized_name] = built
912
891
  end
913
892
 
914
893
  # Get the key attribute for a keyed collection from child_mappings
@@ -956,9 +935,7 @@ child_mappings, options)
956
935
  # Since we don't have direct access to the parent mapping here,
957
936
  # serialize as array for now (default behavior)
958
937
  items.map do |item|
959
- child_root = child_transformation.transform(item, options)
960
- child_hash = child_root.to_hash
961
- child_hash["__root__"]
938
+ child_transformation.transform(item, options)
962
939
  end
963
940
  end
964
941
  else
@@ -970,10 +947,7 @@ child_mappings, options)
970
947
  mapping = attr_type.mappings_for(format, register_id)
971
948
  child_transformation = self.class.new(attr_type, mapping, format,
972
949
  register)
973
- child_root = child_transformation.transform(value,
974
- options)
975
- child_hash = child_root.to_hash
976
- child_hash["__root__"]
950
+ child_transformation.transform(value, options)
977
951
  # Use the transformation to serialize the nested model
978
952
  elsif attr_type.is_a?(Class) && attr_type < Lutaml::Model::Type::Value
979
953
  wrapped_value = attr_type.new(value)
@@ -1001,16 +975,10 @@ child_mappings, options)
1001
975
  when :omitted
1002
976
  return
1003
977
  when :nil
1004
- element = Lutaml::KeyValue::DataModel::Element.new(
1005
- rule.serialized_name, nil
1006
- )
1007
- parent.add_child(element)
978
+ parent[rule.serialized_name] = nil
1008
979
  return
1009
980
  when :empty
1010
- element = Lutaml::KeyValue::DataModel::Element.new(
1011
- rule.serialized_name, ""
1012
- )
1013
- parent.add_child(element)
981
+ parent[rule.serialized_name] = ""
1014
982
  return
1015
983
  end
1016
984
 
@@ -1019,16 +987,10 @@ child_mappings, options)
1019
987
  elsif Lutaml::Model::Utils.uninitialized?(value)
1020
988
  omitted_mapping = to_map[:omitted]
1021
989
  if omitted_mapping == :nil
1022
- element = Lutaml::KeyValue::DataModel::Element.new(
1023
- rule.serialized_name, nil
1024
- )
1025
- parent.add_child(element)
990
+ parent[rule.serialized_name] = nil
1026
991
  return
1027
992
  elsif omitted_mapping == :empty
1028
- element = Lutaml::KeyValue::DataModel::Element.new(
1029
- rule.serialized_name, ""
1030
- )
1031
- parent.add_child(element)
993
+ parent[rule.serialized_name] = ""
1032
994
  return
1033
995
  end
1034
996
  # uninitialized with no/omitted mapping → skip (default behavior)
@@ -1036,10 +998,7 @@ child_mappings, options)
1036
998
  elsif Lutaml::Model::Utils.empty?(value)
1037
999
  empty_mapping = to_map[:empty]
1038
1000
  if empty_mapping == :nil
1039
- element = Lutaml::KeyValue::DataModel::Element.new(
1040
- rule.serialized_name, nil
1041
- )
1042
- parent.add_child(element)
1001
+ parent[rule.serialized_name] = nil
1043
1002
  return
1044
1003
  elsif empty_mapping == :omitted
1045
1004
  return
@@ -1050,12 +1009,7 @@ child_mappings, options)
1050
1009
  # Normal value serialization
1051
1010
  child_value = create_value_for_item(rule, value, options)
1052
1011
 
1053
- unless child_value.nil?
1054
- element = Lutaml::KeyValue::DataModel::Element.new(
1055
- rule.serialized_name, child_value
1056
- )
1057
- parent.add_child(element)
1058
- end
1012
+ parent[rule.serialized_name] = child_value unless child_value.nil?
1059
1013
  end
1060
1014
 
1061
1015
  # Create value for an item (handles nested models and primitives)
@@ -1125,13 +1079,10 @@ child_mappings, options)
1125
1079
 
1126
1080
  if child_transformation
1127
1081
  child_root = child_transformation.transform(value, options)
1128
- # Return the hash representation of the child
1129
- # Remove the __root__ wrapper and return just the content
1130
- child_hash = child_root.to_hash
1131
- result = child_hash["__root__"]
1132
- # If the nested model serialized to empty hash, return nil
1133
- # This allows render_nil: false to work correctly for nested models
1134
- result.nil? || result.empty? ? nil : result
1082
+ # transform emits the content hash directly (TODO.perf 19);
1083
+ # an empty serialization still returns nil so render_nil:
1084
+ # false keeps working for nested models.
1085
+ child_root.empty? ? nil : child_root
1135
1086
  else
1136
1087
  # Fallback: serialize as primitive
1137
1088
  serialize_value(value, rule)
@@ -1167,15 +1118,12 @@ child_mappings, options)
1167
1118
  # For each key-value pair in the parsed content, add as a child element
1168
1119
  if parsed_content.is_a?(::Hash)
1169
1120
  parsed_content.each do |key, val|
1170
- element = Lutaml::KeyValue::DataModel::Element.new(key.to_s, val)
1171
- parent.add_child(element)
1121
+ parent[key.to_s] = val
1172
1122
  end
1173
1123
  elsif parsed_content.is_a?(::Array)
1174
- # If the parsed content is an array, we can't merge it directly
1175
- # This shouldn't happen with valid map_all usage, but handle it gracefully
1176
- element = Lutaml::KeyValue::DataModel::Element.new("__root__",
1177
- parsed_content)
1178
- parent.add_child(element)
1124
+ # Graceful handling of unusual map_all content (parity with
1125
+ # the tree's __root__ wrapper)
1126
+ parent["__root__"] = parsed_content
1179
1127
  end
1180
1128
  end
1181
1129
 
@@ -163,7 +163,10 @@ module Lutaml
163
163
  #
164
164
  # @return [void]
165
165
  def reset!
166
- @metadata = nil
166
+ # metadata is registration data populated at format-registration
167
+ # time, not a resolution cache — wiping it here permanently broke
168
+ # every format's resolution after a reset (adapter_for fell
169
+ # through to nil). Resolution caches reset; registrations stay.
167
170
  @configured_types = nil
168
171
  @resolved = nil
169
172
  @detected_types = nil
@@ -250,8 +253,9 @@ module Lutaml
250
253
  adapter, type)
251
254
  end
252
255
  require adapter_path
253
- rescue LoadError
254
- raise UnknownAdapterTypeError.new(adapter, type), cause: nil
256
+ rescue LoadError => e
257
+ # Chain the cause so a missing engine gem is named, not masked.
258
+ raise UnknownAdapterTypeError.new(adapter, type), cause: e
255
259
  end
256
260
 
257
261
  # Load the Moxml adapter for XML and similar formats.
@@ -9,6 +9,17 @@ module Lutaml
9
9
  include DeepDupable
10
10
  include RestrictionValidation
11
11
 
12
+ # Symbol interpolation (:"to_#{format}") allocates a String and a
13
+ # Symbol per call on the serialization hot path; the table serves
14
+ # the known formats and the fallback keeps exotic ones working.
15
+ TO_FORMAT_METHODS = {
16
+ xml: :to_xml,
17
+ json: :to_json,
18
+ yaml: :to_yaml,
19
+ toml: :to_toml,
20
+ hash: :to_hash,
21
+ }.freeze
22
+
12
23
  ALLOWED_OPTIONS = %i[
13
24
  raw
14
25
  default
@@ -35,6 +46,7 @@ module Lutaml
35
46
  signed
36
47
  min_length
37
48
  max_length
49
+ desc
38
50
  ].freeze
39
51
 
40
52
  # Per-type-class memo of the custom from_xml/from_json probe used by #cast.
@@ -316,6 +328,16 @@ module Lutaml
316
328
  @unresolved_type || @type
317
329
  end
318
330
 
331
+ def reference_type?
332
+ (@reference_type ||= unresolved_type == Lutaml::Model::Type::Reference)
333
+ end
334
+
335
+ def value_map_boolean?
336
+ (@value_map_boolean ||=
337
+ type == Lutaml::Model::Type::Boolean ||
338
+ unresolved_type == Lutaml::Model::Type::Boolean)
339
+ end
340
+
319
341
  def polymorphic?
320
342
  @options[:polymorphic_class]
321
343
  end
@@ -438,8 +460,7 @@ module Lutaml
438
460
 
439
461
  option = value_map[key]
440
462
  if (option.is_a?(TrueClass) || option.is_a?(FalseClass)) &&
441
- (type == Lutaml::Model::Type::Boolean ||
442
- unresolved_type == Lutaml::Model::Type::Boolean)
463
+ value_map_boolean?
443
464
  return option
444
465
  end
445
466
 
@@ -507,7 +528,8 @@ module Lutaml
507
528
  return resolved_type.new(value) if value.is_a?(::Hash) && !hash_type?
508
529
 
509
530
  # Special handling for Reference types - pass the metadata
510
- if unresolved_type == Lutaml::Model::Type::Reference
531
+ # (the comparison is attribute-constant; memoize the verdict)
532
+ if reference_type?
511
533
  return resolved_type.cast_with_metadata(value,
512
534
  @options[:ref_model_class], @options[:ref_key_attribute])
513
535
  end
@@ -566,10 +588,15 @@ instance_object = nil)
566
588
  end
567
589
 
568
590
  def default_value(register, instance_object = nil)
569
- if delegate
570
- type(register).attributes(register)[to].default(register,
571
- instance_object)
572
- elsif options[:default].is_a?(Proc)
591
+ # The kind is fixed at definition; the common case (no default
592
+ # at all) paid three option checks per absent attribute per
593
+ # instantiation before the memo.
594
+ case (@default_kind ||= _default_kind)
595
+ when :none
596
+ Lutaml::Model::UninitializedClass.instance
597
+ when :static
598
+ options[:default]
599
+ when :proc
573
600
  if instance_object
574
601
  ::Lutaml::Model::Attribute.evaluating_default do
575
602
  instance_object.instance_exec(&options[:default])
@@ -577,13 +604,20 @@ instance_object = nil)
577
604
  else
578
605
  options[:default].call
579
606
  end
580
- elsif options.key?(:default)
581
- options[:default]
582
- else
583
- Lutaml::Model::UninitializedClass.instance
607
+ else # :delegate
608
+ type(register).attributes(register)[to].default(register,
609
+ instance_object)
584
610
  end
585
611
  end
586
612
 
613
+ def _default_kind
614
+ return :delegate if delegate
615
+ return :proc if options[:default].is_a?(Proc)
616
+ return :static if options.key?(:default)
617
+
618
+ :none
619
+ end
620
+
587
621
  # Marks the dynamic extent in which a default proc runs against a
588
622
  # model instance, so Serialize#method_missing can fall back to the
589
623
  # model class for methods the instance does not define (0.7.x
@@ -608,6 +642,12 @@ instance_object = nil)
608
642
  options[:pattern]
609
643
  end
610
644
 
645
+ # Documentation metadata (#356): the `:desc` attribute option,
646
+ # consumed by documentation and schema tooling.
647
+ def desc
648
+ options[:desc]
649
+ end
650
+
611
651
  # Performance: Frozen empty array to reduce allocations
612
652
  EMPTY_VALUES_ARRAY = [].freeze
613
653
 
@@ -816,11 +856,10 @@ instance_object = nil)
816
856
  return value if value.nil? || Utils.uninitialized?(value)
817
857
 
818
858
  resolved_type = options[:resolved_type] || type(register)
819
- serialize_options = options.merge(resolved_type: resolved_type)
820
859
  value = reference_key(value) if unresolved_type == Lutaml::Model::Type::Reference
821
860
  if collection_instance?(value)
822
861
  return serialize_array(value, format, register,
823
- serialize_options)
862
+ options.merge(resolved_type: resolved_type))
824
863
  end
825
864
  if resolved_type <= Serialize
826
865
  return serialize_model(value, format, register,
@@ -1201,8 +1240,28 @@ instance_object = nil)
1201
1240
  end
1202
1241
 
1203
1242
  def serialize_value(value, format, resolved_type)
1204
- value = wrap_in_type_if_needed(value, resolved_type)
1205
- value.public_send(:"to_#{format}")
1243
+ # The Value wrapper is a pass-through on the to_ side (its
1244
+ # to_<format> returns the wrapped raw value unless a custom
1245
+ # format serializer is registered), so the common path returns
1246
+ # the raw value and lets the caller stringify — one object
1247
+ # instead of three per serialized scalar. Reference and
1248
+ # custom-serializer rows keep the wrapped path.
1249
+ if resolved_type <= Type::Reference ||
1250
+ (registered_to = Type::Value.format_type_serializer_for(format, resolved_type)&.dig(:to))
1251
+ # String→String under the default :xml serializer is the
1252
+ # identity — the wrapper mint per string value on the XML
1253
+ # serialize path is pure waste. A consumer override of the
1254
+ # serializer disables the fast path via the proc comparison.
1255
+ if resolved_type == Type::String && value.is_a?(::String) &&
1256
+ registered_to == Lutaml::Xml::Type::Serializers::STRING_XML_TO
1257
+ return value
1258
+ end
1259
+
1260
+ value = wrap_in_type_if_needed(value, resolved_type)
1261
+ return value.public_send(TO_FORMAT_METHODS[format] || :"to_#{format}")
1262
+ end
1263
+
1264
+ value
1206
1265
  end
1207
1266
 
1208
1267
  def wrap_in_type_if_needed(value, resolved_type)
@@ -59,14 +59,14 @@ module Lutaml
59
59
  # updates so recursive type resolution can populate related keys.
60
60
  #
61
61
  # @param name [Symbol, String, Class] The type name or class to resolve
62
- # @param context [TypeContext] The resolution context
62
+ # @param context [TypeContext, nil] The resolution context
63
63
  # @return [Class] The resolved type class
64
64
  # @raise [UnknownTypeError] If type cannot be resolved
65
65
  def resolve(name, context, materialize: true)
66
66
  return @delegate.resolve(name, context, materialize: false) unless materialize
67
67
 
68
68
  cache_key = if name.is_a?(Class)
69
- [context.id, name]
69
+ [context_id_for(context), name]
70
70
  else
71
71
  build_cache_key(name, context)
72
72
  end
@@ -79,11 +79,11 @@ module Lutaml
79
79
  # Check if a type can be resolved, using cache if available.
80
80
  #
81
81
  # @param name [Symbol, String, Class] The type name or class to check
82
- # @param context [TypeContext] The resolution context
82
+ # @param context [TypeContext, nil] The resolution context
83
83
  # @return [Boolean] true if type can be resolved
84
84
  def resolvable?(name, context)
85
85
  cache_key = if name.is_a?(Class)
86
- [context.id, name]
86
+ [context_id_for(context), name]
87
87
  else
88
88
  build_cache_key(name, context)
89
89
  end
@@ -137,10 +137,17 @@ module Lutaml
137
137
  # Uses array for faster comparison than string concatenation.
138
138
  #
139
139
  # @param name [Symbol, String] The type name
140
- # @param context [TypeContext] The resolution context
140
+ # @param context [TypeContext, nil] The resolution context
141
141
  # @return [Array] The cache key (faster than string)
142
142
  def build_cache_key(name, context)
143
- [context.id, name.to_sym]
143
+ [context_id_for(context), name.to_sym]
144
+ end
145
+
146
+ # A nil context (e.g. a register ID that has no registered type
147
+ # context) is a legitimate "no namespace" resolution; the cache
148
+ # key just forgoes the namespace slot.
149
+ def context_id_for(context)
150
+ context&.id
144
151
  end
145
152
  end
146
153
  end
@@ -19,6 +19,17 @@ module Lutaml
19
19
  # compilable XML models (Leptris::XML::Descriptor walk). Opt-in
20
20
  # while the full semantics audit completes.
21
21
  attr_accessor :xml_plan_fast_path
22
+ # Ordered/mixed-content models on the plan path: opt-in. The
23
+ # per-node ordered hydration is FASTER than the interpretive
24
+ # lane on time but allocates above the downstream budget gates
25
+ # (niso pnas ratio 1.25); it ships dark until the allocation
26
+ # diet lands.
27
+ attr_accessor :xml_plan_ordered_path
28
+ # #909: when false, XML deserialization skips the per-instance
29
+ # element_order/attribute_order summaries — read-only consumers of
30
+ # large documents never serialize back and should not pay for
31
+ # hundreds of thousands of pinned order records.
32
+ attr_accessor :element_order_tracking
22
33
  attr_writer :conversion_cache
23
34
 
24
35
  def initialize
@@ -26,6 +37,8 @@ module Lutaml
26
37
  @conversion_cache = nil
27
38
  @configured = false
28
39
  @xml_plan_fast_path = true
40
+ @xml_plan_ordered_path = false
41
+ @element_order_tracking = true
29
42
  end
30
43
 
31
44
  def configure
@@ -3,6 +3,19 @@ module Lutaml
3
3
  class Mapping
4
4
  include DeepDupable
5
5
 
6
+ # Uniform interface across mapping families: every mapping
7
+ # answers these, so consumers call them unconditionally. The
8
+ # families that carry the feature override (KeyValue's
9
+ # root_mappings, Xml's namespace_class); the base defaults make
10
+ # the absence explicit instead of probed.
11
+ def root_mappings?
12
+ false
13
+ end
14
+
15
+ def namespace_class
16
+ nil
17
+ end
18
+
6
19
  attr_writer :mappings
7
20
 
8
21
  def initialize
@@ -182,13 +182,18 @@ module Lutaml
182
182
  alias from name
183
183
  alias render_default? render_default
184
184
 
185
+ # Frozen default: render? only reads options (render_nil?/render_empty?/
186
+ # render_omitted? policies); a fresh {} per call is pure churn on the
187
+ # serialization hot path.
188
+ EMPTY_RENDER_OPTIONS = {}.freeze
189
+
185
190
  # Whether this rule participates in serialization output. False
186
191
  # marks a hydrate-only mapping: consumed by from_* but never
187
192
  # rendered by to_*. (The value callback #serialize is separate.)
188
193
  def serialize? = @serialize_mapping
189
194
  alias attribute? attribute
190
195
 
191
- def render?(value, instance = nil, options = {})
196
+ def render?(value, instance = nil, options = EMPTY_RENDER_OPTIONS)
192
197
  if invalid_value?(value, options)
193
198
  false
194
199
  # FIXED: Check if collection was mutated after initialization