lutaml-model 0.8.100 → 0.8.114
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/Gemfile +6 -5
- data/lib/compat/opal/lutaml_model_boot.rb +1 -0
- data/lib/lutaml/json/adapter/yeptris_adapter.rb +11 -0
- data/lib/lutaml/key_value/transform.rb +4 -0
- data/lib/lutaml/key_value/transformation.rb +10 -0
- data/lib/lutaml/model/attribute.rb +43 -3
- data/lib/lutaml/model/cached_type_resolver.rb +13 -6
- data/lib/lutaml/model/configuration.rb +13 -0
- data/lib/lutaml/model/serialize/builder.rb +22 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +21 -3
- data/lib/lutaml/model/serialize/initialization.rb +14 -1
- data/lib/lutaml/model/type_resolver.rb +7 -5
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/adapter/leptris_adapter.rb +1 -0
- data/lib/lutaml/xml/adapter/xml_parser.rb +5 -45
- data/lib/lutaml/xml/leptris/native_emission.rb +187 -0
- data/lib/lutaml/xml/leptris/plan_compiler.rb +161 -102
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +276 -58
- data/lib/lutaml/xml/leptris/plan_serializer.rb +298 -45
- data/lib/lutaml/xml/leptris/plan_walk.rb +6 -3
- data/lib/lutaml/xml/leptris.rb +25 -84
- data/lib/lutaml/xml/model_transform.rb +5 -0
- data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +15 -6
- data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +11 -0
- data/lutaml-model.gemspec +3 -3
- data/spec/lutaml/json/yeptris_adapter_spec.rb +3 -10
- data/spec/lutaml/model/adapter_resolver_configured_spec.rb +32 -8
- data/spec/lutaml/model/collection_strict_assignment_spec.rb +55 -0
- data/spec/lutaml/model/singular_tolerance_spec.rb +159 -0
- data/spec/lutaml/model/unregistered_context_resolution_spec.rb +34 -0
- data/spec/lutaml/xml/adapter/empty_document_diagnostics_spec.rb +50 -0
- data/spec/lutaml/xml/plan_fast_path_spec.rb +12 -6
- data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +0 -11
- data/spec/lutaml/xml/plan_uri_exact_dispatch_spec.rb +287 -0
- data/spec/lutaml/xml/plan_wildcard_any_spec.rb +0 -9
- data/spec/lutaml/yaml/yeptris_adapter_spec.rb +3 -10
- metadata +16 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 381cba1519b14ff18965d44164e7d75e4564ac92c803815dad95277b7076ef70
|
|
4
|
+
data.tar.gz: acd3466cbc30ed27ce60ce62b181732ab957abde427da1e609003ced88c47a67
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8f48a3c4bc8a0242a714d945aae2aaca68ae6c0a8a87cedb038a1229363545bb41f1a3c24f833db86c89e3468cc216c77536af23385cce37e6ce20f8a2e3b142
|
|
7
|
+
data.tar.gz: 9ba5616b479da6952fd44284792ce708712ed6576ba90fa11507581aefd344f30ccd22e5887e3200c714feff15e7e8c95742149f49ac2a5618b2d5ea0f0b742a
|
data/Gemfile
CHANGED
|
@@ -44,11 +44,12 @@ gem "rexml"
|
|
|
44
44
|
# Native engines stay development-only: every adapter detects and
|
|
45
45
|
# degrades gracefully at runtime (safe requires + prefer-if-available).
|
|
46
46
|
# yeptris ships no Windows prebuilt platform gem.
|
|
47
|
-
# leptris floor: the plan
|
|
48
|
-
# (
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
|
|
47
|
+
# leptris floor: the single capability line for the plan fast path
|
|
48
|
+
# (see Lutaml::Xml::Leptris::MIN_LEPTRIS_VERSION) — child snapshots,
|
|
49
|
+
# exact-URI attribute rows, nested-attribute capture, wildcard rows,
|
|
50
|
+
# walk-side ns retention and leniency, :unqualified rows, and the
|
|
51
|
+
# address-based text attach all ride it.
|
|
52
|
+
gem "leptris", ">= 1.9.351.1"
|
|
52
53
|
gem "teptris"
|
|
53
54
|
gem "yeptris" unless Gem.win_platform?
|
|
54
55
|
# TODO: revert rng branch to main when lutaml/rng#32 is merged
|
|
@@ -382,6 +382,7 @@ require "lutaml/xml/error/namespace_mismatch_error"
|
|
|
382
382
|
require "lutaml/xml/error/schema_validation_error"
|
|
383
383
|
require "lutaml/xml/error/xml_configuration_error"
|
|
384
384
|
require "lutaml/xml/error/xml_error"
|
|
385
|
+
require "lutaml/xml/leptris/native_emission"
|
|
385
386
|
require "lutaml/xml/leptris/plan_compiler"
|
|
386
387
|
require "lutaml/xml/leptris/plan_hydrator"
|
|
387
388
|
require "lutaml/xml/leptris/plan_order"
|
|
@@ -26,6 +26,17 @@ module Lutaml
|
|
|
26
26
|
end
|
|
27
27
|
private_class_method :require_engine
|
|
28
28
|
|
|
29
|
+
# True where the yeptris engine loads (libyeptris present for
|
|
30
|
+
# this platform). Idempotent; a missing or broken install
|
|
31
|
+
# reports false instead of raising, so optional-engine callers
|
|
32
|
+
# can gate on it.
|
|
33
|
+
def self.available?
|
|
34
|
+
require_engine
|
|
35
|
+
true
|
|
36
|
+
rescue LoadError
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
|
|
29
40
|
# The parse-error contract is the standard adapter's: invalid
|
|
30
41
|
# input raises JSON::ParserError regardless of the engine, so
|
|
31
42
|
# consumers' rescue ladders (and this gem's own
|
|
@@ -687,6 +687,10 @@ partition = nil)
|
|
|
687
687
|
# attribute is not a collection, so this guard defers exactly that case
|
|
688
688
|
# to `.validate` while leaving declared ranges checked at parse. A mapped
|
|
689
689
|
# PORO has no `.validate` to defer into, so it keeps the eager check.
|
|
690
|
+
# singular_tolerance: :first on the KV lane (#971): collapse an
|
|
691
|
+
# over-ingested collection before the writer stores it whole.
|
|
692
|
+
value = value.first if attr.tolerates_singular_overflow?(value)
|
|
693
|
+
|
|
690
694
|
if attr.collection? || !instance.is_a?(Lutaml::Model::Serialize)
|
|
691
695
|
attr.valid_collection!(value, context)
|
|
692
696
|
end
|
|
@@ -1031,6 +1031,16 @@ child_mappings, options)
|
|
|
1031
1031
|
rule.attribute_type < Lutaml::Model::Serialize
|
|
1032
1032
|
end
|
|
1033
1033
|
|
|
1034
|
+
if is_nested_model && !value.is_a?(Lutaml::Model::Serialize)
|
|
1035
|
+
# The KV mirror of the XML lane's #971 collapse: a tolerated
|
|
1036
|
+
# singular attribute holding an over-assigned collection
|
|
1037
|
+
# serializes its leading value instead of raising.
|
|
1038
|
+
attr = if model_class.respond_to?(:attributes)
|
|
1039
|
+
model_class.attributes[rule.attribute_name.to_sym]
|
|
1040
|
+
end
|
|
1041
|
+
value = value.first if attr&.tolerates_singular_overflow?(value)
|
|
1042
|
+
end
|
|
1043
|
+
|
|
1034
1044
|
if is_nested_model
|
|
1035
1045
|
# Validate that value matches the expected type
|
|
1036
1046
|
# There are three valid cases:
|
|
@@ -47,6 +47,7 @@ module Lutaml
|
|
|
47
47
|
min_length
|
|
48
48
|
max_length
|
|
49
49
|
desc
|
|
50
|
+
singular_tolerance
|
|
50
51
|
].freeze
|
|
51
52
|
|
|
52
53
|
# Per-type-class memo of the custom from_xml/from_json probe used by #cast.
|
|
@@ -389,12 +390,34 @@ module Lutaml
|
|
|
389
390
|
|
|
390
391
|
def cast_value(value, register)
|
|
391
392
|
if list_like?(value)
|
|
392
|
-
|
|
393
|
+
items = cast_items(value, register)
|
|
393
394
|
elsif coerce_to_collection?(value)
|
|
394
|
-
|
|
395
|
+
items = cast_items([value], register)
|
|
395
396
|
else
|
|
396
|
-
cast_element(value, register)
|
|
397
|
+
return cast_element(value, register)
|
|
397
398
|
end
|
|
399
|
+
# cast_items already returns a fresh array, so the plain-Array
|
|
400
|
+
# collection needs no re-wrap — build_collection's
|
|
401
|
+
# collection_class.new would copy it again.
|
|
402
|
+
assert_flat_collection_items!(items)
|
|
403
|
+
return items if !custom_collection? && collection_class == ::Array
|
|
404
|
+
|
|
405
|
+
build_collection(items, register: register)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Fail loudly on nested arrays. A collection's declared element
|
|
409
|
+
# type never admits an Array as an item; the legacy
|
|
410
|
+
# build_collection flatten silently absorbed mis-spliced builder
|
|
411
|
+
# output (expressir#506 surfaced it three frames away in a
|
|
412
|
+
# formatter). The error must land at the assignment boundary.
|
|
413
|
+
def assert_flat_collection_items!(items)
|
|
414
|
+
return unless items.any?(::Array)
|
|
415
|
+
|
|
416
|
+
raise TypeError,
|
|
417
|
+
"collection '#{name}': item " \
|
|
418
|
+
"#{items.index { |item| item.is_a?(::Array) }} is an Array " \
|
|
419
|
+
"where the declared element type is expected; nested arrays " \
|
|
420
|
+
"are not flattened — splice list results before assignment"
|
|
398
421
|
end
|
|
399
422
|
|
|
400
423
|
# Plain Arrays count as list input. On singular attributes
|
|
@@ -805,6 +828,17 @@ instance_object = nil)
|
|
|
805
828
|
end
|
|
806
829
|
end
|
|
807
830
|
|
|
831
|
+
# Singular-overflow tolerance (#971): ingest can hand a singular
|
|
832
|
+
# attribute a collection — relaton's upstream documents carry
|
|
833
|
+
# multiple <formattedref> elements per bibitem. With
|
|
834
|
+
# singular_tolerance: :first the leading value is kept instead of
|
|
835
|
+
# raising CollectionTrueMissingError; the default stays strict.
|
|
836
|
+
def tolerates_singular_overflow?(value)
|
|
837
|
+
!collection? && @options[:singular_tolerance] == :first &&
|
|
838
|
+
collection_instance?(value) &&
|
|
839
|
+
!value_policy.whole_value?(unresolved_type)
|
|
840
|
+
end
|
|
841
|
+
|
|
808
842
|
def valid_collection!(value, caller)
|
|
809
843
|
if collection_instance?(value) && !collection? &&
|
|
810
844
|
!value_policy.whole_value?(unresolved_type)
|
|
@@ -1019,6 +1053,12 @@ instance_object = nil)
|
|
|
1019
1053
|
"Use map_element \"name\", to: :#{name}, raw: :content instead."
|
|
1020
1054
|
end
|
|
1021
1055
|
@validations = @options[:validations]
|
|
1056
|
+
tolerance = @options[:singular_tolerance]
|
|
1057
|
+
unless tolerance.nil? || tolerance == :first
|
|
1058
|
+
raise ArgumentError,
|
|
1059
|
+
"Invalid singular_tolerance: #{tolerance.inspect} for " \
|
|
1060
|
+
"attribute '#{name}'. Supported: :first"
|
|
1061
|
+
end
|
|
1022
1062
|
set_default_for_collection if collection?
|
|
1023
1063
|
end
|
|
1024
1064
|
|
|
@@ -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,12 @@ 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
|
|
22
28
|
# #909: when false, XML deserialization skips the per-instance
|
|
23
29
|
# element_order/attribute_order summaries — read-only consumers of
|
|
24
30
|
# large documents never serialize back and should not pay for
|
|
@@ -31,6 +37,13 @@ module Lutaml
|
|
|
31
37
|
@conversion_cache = nil
|
|
32
38
|
@configured = false
|
|
33
39
|
@xml_plan_fast_path = true
|
|
40
|
+
# On by default since the engine fix: leptris#1743 (the
|
|
41
|
+
# exponential flatten on diamond-shared plan graphs) shipped in
|
|
42
|
+
# 1.9.366.2's identity-memo flatten_plan — the ISO_13849 corpus
|
|
43
|
+
# that hung for 28+ CPU-min now parses in ~8s warm. The floor
|
|
44
|
+
# bump (MIN_LEPTRIS_VERSION = 1.9.366.2) keeps consumers on
|
|
45
|
+
# memo-less engines off this lane.
|
|
46
|
+
@xml_plan_ordered_path = true
|
|
34
47
|
@element_order_tracking = true
|
|
35
48
|
end
|
|
36
49
|
|
|
@@ -84,6 +84,26 @@ module Lutaml
|
|
|
84
84
|
@__order_tracking__ ? true : false
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
# Whether any writer setter ran on this instance. Parsed
|
|
88
|
+
# instances start pristine — every set element value is covered
|
|
89
|
+
# by the parse-built element_order, so ordered serialization
|
|
90
|
+
# can skip order reconciliation; the first writer-set marks the
|
|
91
|
+
# instance and reconciliation engages (writer-added values sit
|
|
92
|
+
# outside the parse order).
|
|
93
|
+
# @return [Boolean]
|
|
94
|
+
def mutated_after_parse?
|
|
95
|
+
@__mutated_after_parse__ ? true : false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Construction-time reset: hydration routes every attribute
|
|
99
|
+
# through the generated writers (which mark mutations); a
|
|
100
|
+
# freshly built instance is definitionally not
|
|
101
|
+
# mutated-after-parse.
|
|
102
|
+
# @return [void]
|
|
103
|
+
def clear_mutated_after_parse!
|
|
104
|
+
@__mutated_after_parse__ = false
|
|
105
|
+
end
|
|
106
|
+
|
|
87
107
|
# Record a singular attribute mutation in element_order.
|
|
88
108
|
#
|
|
89
109
|
# No-op unless order tracking is enabled (i.e. the instance was
|
|
@@ -102,6 +122,7 @@ module Lutaml
|
|
|
102
122
|
# content for content-mapped attributes, ignored otherwise
|
|
103
123
|
# @return [Object] the passed value
|
|
104
124
|
def record_mutation(attribute_name, value = nil)
|
|
125
|
+
@__mutated_after_parse__ = true
|
|
105
126
|
return value unless @__order_tracking__
|
|
106
127
|
|
|
107
128
|
track_order(attribute_name, value, nil)
|
|
@@ -122,6 +143,7 @@ module Lutaml
|
|
|
122
143
|
# @param value [Object, nil] The value assigned to the collection
|
|
123
144
|
# @return [Object] the passed value
|
|
124
145
|
def record_mutation_collection(attribute_name, value)
|
|
146
|
+
@__mutated_after_parse__ = true
|
|
125
147
|
return value unless @__order_tracking__
|
|
126
148
|
return value if value.nil? || Lutaml::Model::Utils.uninitialized?(value)
|
|
127
149
|
|
|
@@ -138,19 +138,37 @@ module Lutaml
|
|
|
138
138
|
register = Lutaml::Model::Config.default_register
|
|
139
139
|
plan = Lutaml::Xml::Leptris::PlanCompiler.compile(self, register)
|
|
140
140
|
return nil unless plan
|
|
141
|
-
return nil unless Lutaml::Xml::Leptris::PlanCompiler.engine_walk_safe?(plan)
|
|
142
141
|
|
|
143
|
-
|
|
142
|
+
# strict: the from_xml contract raises on malformed input —
|
|
143
|
+
# the FFI lane parses leniently by default and would answer a
|
|
144
|
+
# best-effort tree where every other adapter raises. The
|
|
145
|
+
# kwarg spellings vary by engine vintage (strict: landed
|
|
146
|
+
# after 1.9.353, recover: on 1.9.344+), so probe the resolved
|
|
147
|
+
# parse method and pass the strictest spelling it accepts.
|
|
148
|
+
root = ::Leptris::XML.parse(data.to_s, **leptris_strict_kwargs).root
|
|
144
149
|
return nil if root.nil?
|
|
145
150
|
return nil unless root.name == plan[:tree][:name]
|
|
146
151
|
|
|
147
152
|
Lutaml::Xml::Leptris::PlanHydrator.call(self, plan,
|
|
148
153
|
plan[:descriptor].walk(root),
|
|
149
|
-
node: root
|
|
154
|
+
node: root,
|
|
155
|
+
declaration_plan_mode:
|
|
156
|
+
options.fetch(
|
|
157
|
+
:import_declaration_plan, :eager
|
|
158
|
+
))
|
|
150
159
|
rescue ::Leptris::XML::ParseError => e
|
|
151
160
|
raise Lutaml::Model::InvalidFormatError.new(:xml, e.message)
|
|
152
161
|
end
|
|
153
162
|
|
|
163
|
+
def leptris_strict_kwargs
|
|
164
|
+
params = ::Leptris::XML.method(:parse).parameters
|
|
165
|
+
.map { |_, key| key }
|
|
166
|
+
kwargs = {}
|
|
167
|
+
kwargs[:recover] = false if params.include?(:recover)
|
|
168
|
+
kwargs[:strict] = true if params.include?(:strict)
|
|
169
|
+
kwargs
|
|
170
|
+
end
|
|
171
|
+
|
|
154
172
|
# Get list of error types that can be raised during format parsing.
|
|
155
173
|
# Core errors are always included; format-specific errors come from
|
|
156
174
|
# FormatRegistry registrations.
|
|
@@ -223,6 +223,7 @@ module Lutaml
|
|
|
223
223
|
# Clear memoized attribute merge (see .attributes)
|
|
224
224
|
@merged_attributes_cache = nil
|
|
225
225
|
@instantiate_writers = nil
|
|
226
|
+
@instantiate_attr_pairs = nil
|
|
226
227
|
|
|
227
228
|
# Clear per-Attribute type caches (stale entries from GC'd TypeContext objects)
|
|
228
229
|
class_attributes.each_value(&:clear_type_cache)
|
|
@@ -402,7 +403,10 @@ module Lutaml
|
|
|
402
403
|
attrs_by = attributes(register_id)
|
|
403
404
|
writers = instantiate_writers(register_id)
|
|
404
405
|
given = 0
|
|
405
|
-
|
|
406
|
+
# Memoized pair array: Hash#each mints a [name, attr] pair
|
|
407
|
+
# per attribute per instance (~5 allocs/instance on
|
|
408
|
+
# hydration-heavy corpora); Array#each reuses its elements.
|
|
409
|
+
instantiate_attr_pairs(register_id).each do |name, attr|
|
|
406
410
|
next if attr.derived?
|
|
407
411
|
|
|
408
412
|
# Symbol keys hit directly (native-extension callers);
|
|
@@ -455,6 +459,7 @@ module Lutaml
|
|
|
455
459
|
unknown = attrs.keys.find { |k| !known.include?(k.to_sym) }
|
|
456
460
|
raise Error, "unknown attribute '#{unknown}' for #{self}"
|
|
457
461
|
end
|
|
462
|
+
instance.clear_mutated_after_parse!
|
|
458
463
|
instance
|
|
459
464
|
end
|
|
460
465
|
|
|
@@ -467,6 +472,14 @@ module Lutaml
|
|
|
467
472
|
end
|
|
468
473
|
end
|
|
469
474
|
|
|
475
|
+
# [name, attr] pairs in attributes order — instantiate's loop
|
|
476
|
+
# iterates this instead of the attributes Hash so the per-call
|
|
477
|
+
# pair array disappears (see the instantiate comment).
|
|
478
|
+
def instantiate_attr_pairs(register_id = nil)
|
|
479
|
+
(@instantiate_attr_pairs ||= {})[register_id] ||=
|
|
480
|
+
attributes(register_id).to_a.freeze
|
|
481
|
+
end
|
|
482
|
+
|
|
470
483
|
# Define register-specific attribute methods on the class itself.
|
|
471
484
|
#
|
|
472
485
|
# Called once per (class, register) combination. Replaces per-instance
|
|
@@ -44,7 +44,9 @@ module Lutaml
|
|
|
44
44
|
# 5. Raise UnknownTypeError if not found anywhere
|
|
45
45
|
#
|
|
46
46
|
# @param name [Symbol, String, Class] The type name or class to resolve
|
|
47
|
-
# @param context [TypeContext] The resolution context
|
|
47
|
+
# @param context [TypeContext, nil] The resolution context (nil means
|
|
48
|
+
# no primary registry and no fallbacks — an id registered in the
|
|
49
|
+
# Register namespace but not as a type context)
|
|
48
50
|
# @return [Class] The resolved type class
|
|
49
51
|
# @raise [UnknownTypeError] If type cannot be resolved
|
|
50
52
|
#
|
|
@@ -66,11 +68,11 @@ module Lutaml
|
|
|
66
68
|
type_name = name.to_sym
|
|
67
69
|
|
|
68
70
|
# 1. Check primary registry
|
|
69
|
-
type = context
|
|
71
|
+
type = context&.lookup_local(type_name)
|
|
70
72
|
return apply_substitutions(type, context) if type
|
|
71
73
|
|
|
72
74
|
# 2. Check fallback contexts in order
|
|
73
|
-
if context
|
|
75
|
+
if context&.has_fallbacks?
|
|
74
76
|
context.fallback_contexts.each do |fallback_context|
|
|
75
77
|
type = resolve_from_fallback(type_name, fallback_context)
|
|
76
78
|
return apply_substitutions(type, context) if type
|
|
@@ -97,8 +99,8 @@ module Lutaml
|
|
|
97
99
|
# 5. Type not found - raise error
|
|
98
100
|
raise UnknownTypeError.new(
|
|
99
101
|
type_name,
|
|
100
|
-
context_id: context
|
|
101
|
-
available_types: available_type_names(context),
|
|
102
|
+
context_id: context&.id,
|
|
103
|
+
available_types: context ? available_type_names(context) : [],
|
|
102
104
|
)
|
|
103
105
|
end
|
|
104
106
|
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -20,10 +20,9 @@ module Lutaml
|
|
|
20
20
|
raw_xml = xml
|
|
21
21
|
xml = normalize_xml_for_parse(xml)
|
|
22
22
|
parsed = parse_with_moxml(xml, parse_encoding)
|
|
23
|
-
assert_no_truncated_recovered_parse!(parsed)
|
|
24
23
|
root_element = parsed.root
|
|
25
24
|
|
|
26
|
-
raise_empty_document_error if root_element.nil?
|
|
25
|
+
raise_empty_document_error(parsed.parse_errors) if root_element.nil?
|
|
27
26
|
|
|
28
27
|
root = self::PARSED_ELEMENT_CLASS.new(root_element)
|
|
29
28
|
doc_pis = extract_document_processing_instructions(parsed)
|
|
@@ -31,31 +30,6 @@ module Lutaml
|
|
|
31
30
|
new(root, parse_encoding, **parse_document_options(raw_xml))
|
|
32
31
|
end
|
|
33
32
|
|
|
34
|
-
# Recover-mode parsing must never masquerade input truncation as
|
|
35
|
-
# success.
|
|
36
|
-
#
|
|
37
|
-
# moxml releases before 0.5.84 parsed Nokogiri documents without
|
|
38
|
-
# XML_PARSE_HUGE, so libxml2 capped its input buffer at 10 MB;
|
|
39
|
-
# longer documents tripped a fatal "Resource limit exceeded:
|
|
40
|
-
# Buffer size limit exceeded, try XML_PARSE_HUGE" error mid-input.
|
|
41
|
-
# In recover mode Nokogiri records that error on the document and
|
|
42
|
-
# returns the partial tree, so every node past the limit silently
|
|
43
|
-
# vanishes (lutaml-model#871). A resource-limit fatal means the
|
|
44
|
-
# engine stopped early with input left — refuse to deserialize the
|
|
45
|
-
# partial document instead of dropping trailing content without a
|
|
46
|
-
# trace.
|
|
47
|
-
#
|
|
48
|
-
# moxml 0.5.84 and later always pass XML_PARSE_HUGE, so the
|
|
49
|
-
# Nokogiri path no longer truncates and this guard stays silent;
|
|
50
|
-
# it remains for older moxml releases and for any other adapter
|
|
51
|
-
# that reports the resource-limit family.
|
|
52
|
-
#
|
|
53
|
-
# Other recover-mode fatals (e.g. an XML declaration after leading
|
|
54
|
-
# whitespace) do not drop content and keep the long-standing
|
|
55
|
-
# recover behavior; only the resource-limit family is truncating.
|
|
56
|
-
TRUNCATION_FATAL_MARKER =
|
|
57
|
-
/Resource limit exceeded|Buffer size limit exceeded/
|
|
58
|
-
|
|
59
33
|
private
|
|
60
34
|
|
|
61
35
|
def normalize_xml_for_parse(xml)
|
|
@@ -88,23 +62,6 @@ module Lutaml
|
|
|
88
62
|
end
|
|
89
63
|
end
|
|
90
64
|
|
|
91
|
-
def assert_no_truncated_recovered_parse!(parsed)
|
|
92
|
-
errors = parsed.parse_errors
|
|
93
|
-
return if errors.nil? || errors.empty?
|
|
94
|
-
|
|
95
|
-
fatal = errors.find do |message|
|
|
96
|
-
message.match?(TRUNCATION_FATAL_MARKER)
|
|
97
|
-
end
|
|
98
|
-
return if fatal.nil?
|
|
99
|
-
|
|
100
|
-
raise Lutaml::Model::InvalidFormatError.new(
|
|
101
|
-
:xml,
|
|
102
|
-
"the XML engine reported a fatal resource limit and recovered " \
|
|
103
|
-
"with a truncated document; refusing to deserialize " \
|
|
104
|
-
"partial input (#{fatal})",
|
|
105
|
-
)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
65
|
def parse_document_options(xml)
|
|
109
66
|
{
|
|
110
67
|
doctype: extract_doctype_from_xml(xml),
|
|
@@ -112,8 +69,11 @@ module Lutaml
|
|
|
112
69
|
}
|
|
113
70
|
end
|
|
114
71
|
|
|
115
|
-
def raise_empty_document_error
|
|
72
|
+
def raise_empty_document_error(parse_errors = nil)
|
|
116
73
|
message = self::EMPTY_DOCUMENT_ERROR_MESSAGE
|
|
74
|
+
if parse_errors && !parse_errors.empty?
|
|
75
|
+
message += " (the XML engine reported: #{parse_errors.join('; ')})"
|
|
76
|
+
end
|
|
117
77
|
|
|
118
78
|
case self::EMPTY_DOCUMENT_ERROR_TYPE
|
|
119
79
|
when :parse_exception
|