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.
- checksums.yaml +4 -4
- data/.github/scripts/test_installed_gem.rb +58 -0
- data/.github/workflows/downstream-performance.yml +28 -3
- data/.github/workflows/js-pr-check.yml +4 -0
- data/.github/workflows/js-sync.yml +4 -0
- data/.github/workflows/opal.yml +9 -0
- data/.github/workflows/rake.yml +6 -41
- data/.github/workflows/release.yml +41 -0
- data/Gemfile +17 -3
- data/bench/bench_alloc_audit.rb +56 -0
- data/bench/bench_common.rb +11 -0
- data/bench/bench_merge_attempts.rb +49 -0
- data/docs/_guides/gem-integration.adoc +85 -0
- data/docs/_guides/grammar-backed-formats.adoc +69 -0
- data/docs/_guides/index.adoc +8 -1
- data/docs/_guides/native-engines.adoc +23 -5
- data/docs/_guides/schema-generation.adoc +25 -0
- data/docs/_guides/type-registers-contexts.adoc +126 -0
- data/lib/compat/opal/lutaml_model_boot.rb +7 -5
- data/lib/lutaml/json/adapter/yeptris_adapter.rb +24 -1
- data/lib/lutaml/key_value/data_model/element.rb +5 -1
- data/lib/lutaml/key_value/mapping_rule.rb +7 -0
- data/lib/lutaml/key_value/transform.rb +29 -24
- data/lib/lutaml/key_value/transformation/collection_serializer.rb +12 -29
- data/lib/lutaml/key_value/transformation/value_serializer.rb +29 -6
- data/lib/lutaml/key_value/transformation.rb +64 -116
- data/lib/lutaml/model/adapter_resolver.rb +7 -3
- data/lib/lutaml/model/attribute.rb +74 -15
- data/lib/lutaml/model/cached_type_resolver.rb +13 -6
- data/lib/lutaml/model/configuration.rb +13 -0
- data/lib/lutaml/model/mapping/mapping.rb +13 -0
- data/lib/lutaml/model/mapping/mapping_rule.rb +6 -1
- data/lib/lutaml/model/render_policy.rb +26 -8
- data/lib/lutaml/model/schema/base_schema.rb +15 -0
- data/lib/lutaml/model/schema/generator/definition.rb +52 -8
- data/lib/lutaml/model/schema/generator/definitions_collection.rb +35 -14
- data/lib/lutaml/model/schema.rb +4 -2
- data/lib/lutaml/model/serializable.rb +33 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +196 -2
- data/lib/lutaml/model/serialize/builder.rb +22 -0
- data/lib/lutaml/model/serialize/conversion_caching.rb +5 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +49 -41
- data/lib/lutaml/model/serialize/initialization.rb +54 -7
- data/lib/lutaml/model/serialize/model_import.rb +60 -0
- data/lib/lutaml/model/serialize.rb +14 -6
- data/lib/lutaml/model/type/time.rb +6 -0
- data/lib/lutaml/model/type/value.rb +9 -7
- data/lib/lutaml/model/type_resolver.rb +7 -5
- data/lib/lutaml/model/utils.rb +18 -5
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +7 -0
- data/lib/lutaml/xml/adapter/xml_parser.rb +5 -2
- data/lib/lutaml/xml/adapter_loader.rb +5 -2
- data/lib/lutaml/xml/format.rb +18 -16
- data/lib/lutaml/xml/leptris/native_emission.rb +187 -0
- data/lib/lutaml/xml/leptris/plan_compiler.rb +650 -0
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +748 -0
- data/lib/lutaml/xml/leptris/plan_order.rb +55 -0
- data/lib/lutaml/xml/leptris/plan_serializer.rb +702 -0
- data/lib/lutaml/xml/leptris/plan_walk.rb +36 -0
- data/lib/lutaml/xml/leptris.rb +46 -0
- data/lib/lutaml/xml/mapping.rb +71 -18
- data/lib/lutaml/xml/model_transform.rb +231 -8
- data/lib/lutaml/xml/namespace.rb +18 -8
- data/lib/lutaml/xml/namespace_collector.rb +17 -1
- data/lib/lutaml/xml/namespace_needs.rb +77 -49
- data/lib/lutaml/xml/parse_session.rb +7 -19
- data/lib/lutaml/xml/schema/xsd/schema.rb +2 -1
- data/lib/lutaml/xml/serialization/format_conversion.rb +5 -1
- data/lib/lutaml/xml/transformation/ordered_applier.rb +21 -15
- data/lib/lutaml/xml/transformation.rb +15 -1
- data/lib/lutaml/xml/type/serializers.rb +8 -1
- data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +34 -2
- data/lib/tasks/performance_probe_plan_walk.rb +1 -1
- data/lutaml-model.gemspec +7 -4
- data/spec/lutaml/json/yeptris_adapter_spec.rb +7 -2
- data/spec/lutaml/key_value/transformation/value_serializer_spec.rb +9 -5
- data/spec/lutaml/key_value/transformation_spec.rb +87 -21
- data/spec/lutaml/model/any_element_mapping_spec.rb +41 -0
- data/spec/lutaml/model/collection_validation_spec.rb +265 -0
- data/spec/lutaml/model/desc_spec.rb +50 -0
- data/spec/lutaml/model/late_attribute_declaration_spec.rb +35 -0
- data/spec/lutaml/model/model_import_reentry_spec.rb +46 -0
- data/spec/lutaml/model/opal_smoke_spec.rb +17 -0
- data/spec/lutaml/model/schema/generate_many_spec.rb +61 -0
- data/spec/lutaml/model/schema/generation_regressions_spec.rb +139 -0
- data/spec/lutaml/model/schema/json_schema_spec.rb +1 -13
- data/spec/lutaml/model/schema/yaml_schema_spec.rb +0 -11
- data/spec/lutaml/model/serialize/attribute_definition_block_form_spec.rb +244 -0
- data/spec/lutaml/model/store_spec.rb +6 -2
- data/spec/lutaml/model/unregistered_context_resolution_spec.rb +34 -0
- data/spec/lutaml/model/writer_assigned_attribute_spec.rb +62 -0
- data/spec/lutaml/model/yeptris_error_parity_spec.rb +95 -0
- data/spec/lutaml/xml/adapter/empty_document_diagnostics_spec.rb +50 -0
- data/spec/lutaml/xml/adapter/over_cap_parse_spec.rb +105 -0
- data/spec/lutaml/xml/adapter_loader_spec.rb +14 -0
- data/spec/lutaml/xml/adapter_metadata_default_spec.rb +16 -0
- data/spec/lutaml/xml/attribute_ambiguity_surface_spec.rb +5 -0
- data/spec/lutaml/xml/clear_parse_state_spec.rb +6 -1
- data/spec/lutaml/xml/compiled_rule_namespace_spec.rb +5 -0
- data/spec/lutaml/xml/cross_register_plan_spec.rb +75 -0
- data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +16 -8
- data/spec/lutaml/xml/element_order_tracking_spec.rb +37 -0
- data/spec/lutaml/xml/leptris_string_encoding_spec.rb +65 -0
- data/spec/lutaml/xml/opal_xml_spec.rb +47 -0
- data/spec/lutaml/xml/parse_dom_retention_spec.rb +34 -0
- data/spec/lutaml/xml/plan_fast_path_spec.rb +122 -14
- data/spec/lutaml/xml/plan_namespaced_serialize_spec.rb +157 -0
- data/spec/lutaml/xml/plan_ns_less_document_spec.rb +86 -0
- data/spec/lutaml/xml/plan_partition_spec.rb +1 -1
- data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +131 -0
- data/spec/lutaml/xml/plan_uri_exact_dispatch_spec.rb +287 -0
- data/spec/lutaml/xml/plan_walk_spec.rb +6 -6
- data/spec/lutaml/xml/plan_wildcard_any_spec.rb +96 -0
- data/spec/lutaml/yaml/yeptris_adapter_spec.rb +29 -0
- data/spec/person_spec.rb +8 -0
- metadata +48 -17
- data/lib/lutaml/xml/plan_compiler.rb +0 -332
- data/lib/lutaml/xml/plan_hydrator.rb +0 -367
- data/lib/lutaml/xml/plan_order.rb +0 -53
- data/lib/lutaml/xml/plan_serializer.rb +0 -252
- 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
|
-
|
|
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
|
|
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
|
-
|
|
293
|
-
|
|
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
|
-
|
|
339
|
-
|
|
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
|
-
#
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
525
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
1129
|
-
#
|
|
1130
|
-
|
|
1131
|
-
|
|
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
|
-
|
|
1171
|
-
parent.add_child(element)
|
|
1121
|
+
parent[key.to_s] = val
|
|
1172
1122
|
end
|
|
1173
1123
|
elsif parsed_content.is_a?(::Array)
|
|
1174
|
-
#
|
|
1175
|
-
#
|
|
1176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1205
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|