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
|
@@ -95,17 +95,28 @@ module Lutaml
|
|
|
95
95
|
options.key?(:mappings) || options.key?(:adapter) ||
|
|
96
96
|
options.key?(:_adapter_override) ||
|
|
97
97
|
options.key?(:indent) || options.key?(:xml_declaration) ||
|
|
98
|
-
options.key?(:declaration) || options.key?(:doctype)
|
|
98
|
+
options.key?(:declaration) || options.key?(:doctype) ||
|
|
99
|
+
# prefix requests ride the interpretive writer's use_prefix
|
|
100
|
+
# option (planner Tier-1) — the plan emission has no
|
|
101
|
+
# option-aware spelling. The instance-level option prep
|
|
102
|
+
# rewrites :prefix to :use_prefix before this gate.
|
|
103
|
+
options.key?(:prefix) || options.key?(:use_prefix)
|
|
99
104
|
|
|
100
105
|
adapter_name = Lutaml::Model::Config.adapter_for(:xml)
|
|
101
106
|
adapter_name &&= adapter_name.name
|
|
102
107
|
return nil unless adapter_name.to_s.end_with?("LeptrisAdapter")
|
|
103
108
|
|
|
104
109
|
register = Lutaml::Model::Config.default_register
|
|
105
|
-
plan = Lutaml::Xml::PlanCompiler.compile(self, register)
|
|
106
|
-
return nil unless plan &&
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
plan = Lutaml::Xml::Leptris::PlanCompiler.compile(self, register)
|
|
111
|
+
return nil unless plan &&
|
|
112
|
+
Lutaml::Xml::Leptris::PlanSerializer.serializable?(plan)
|
|
113
|
+
|
|
114
|
+
# Namespaced plans ride the plan serializer too: the DOM
|
|
115
|
+
# emission spells prefixed names and hoists declarations
|
|
116
|
+
# (the #847 xmlns drop is fixed in PlanSerializer); any
|
|
117
|
+
# unspellable shape answers nil there and falls back
|
|
118
|
+
# interpretively.
|
|
119
|
+
Lutaml::Xml::Leptris::PlanSerializer.call(instance, plan)
|
|
109
120
|
end
|
|
110
121
|
|
|
111
122
|
# Whole-document native materialization (Phase 5): compile the
|
|
@@ -125,16 +136,20 @@ module Lutaml
|
|
|
125
136
|
return nil unless adapter_name.to_s.end_with?("LeptrisAdapter")
|
|
126
137
|
|
|
127
138
|
register = Lutaml::Model::Config.default_register
|
|
128
|
-
plan = Lutaml::Xml::PlanCompiler.compile(self, register)
|
|
139
|
+
plan = Lutaml::Xml::Leptris::PlanCompiler.compile(self, register)
|
|
129
140
|
return nil unless plan
|
|
130
141
|
|
|
131
142
|
root = ::Leptris::XML.parse(data.to_s).root
|
|
132
143
|
return nil if root.nil?
|
|
133
144
|
return nil unless root.name == plan[:tree][:name]
|
|
134
145
|
|
|
135
|
-
Lutaml::Xml::PlanHydrator.call(self, plan,
|
|
146
|
+
Lutaml::Xml::Leptris::PlanHydrator.call(self, plan,
|
|
136
147
|
plan[:descriptor].walk(root),
|
|
137
|
-
node: root
|
|
148
|
+
node: root,
|
|
149
|
+
declaration_plan_mode:
|
|
150
|
+
options.fetch(
|
|
151
|
+
:import_declaration_plan, :eager
|
|
152
|
+
))
|
|
138
153
|
rescue ::Leptris::XML::ParseError => e
|
|
139
154
|
raise Lutaml::Model::InvalidFormatError.new(:xml, e.message)
|
|
140
155
|
end
|
|
@@ -149,48 +164,41 @@ module Lutaml
|
|
|
149
164
|
#
|
|
150
165
|
# @return [Array<Class>] List of error classes
|
|
151
166
|
def format_error_types
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
167
|
+
# Resolved per call, not memoized: adapter engines load lazily,
|
|
168
|
+
# and a cache built before an engine loads permanently drops
|
|
169
|
+
# that engine's parse errors from the rescue list (an
|
|
170
|
+
# REXML::ParseException leaked un-wrapped when any earlier
|
|
171
|
+
# parse ran before rexml was required). The constantize pass
|
|
172
|
+
# is a handful of lookups — negligible against a parse.
|
|
173
|
+
compatibility = Lutaml::Model::RuntimeCompatibility
|
|
174
|
+
errors = [
|
|
175
|
+
compatibility.safe_constantize("Psych::SyntaxError"),
|
|
176
|
+
compatibility.safe_constantize("JSON::ParserError"),
|
|
177
|
+
NoMethodError,
|
|
178
|
+
TypeError,
|
|
179
|
+
ArgumentError,
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
FormatRegistry.all.each_value do |info|
|
|
183
|
+
next unless info[:error_types]
|
|
184
|
+
|
|
185
|
+
info[:error_types].each do |error_class|
|
|
186
|
+
errors << if error_class.is_a?(String)
|
|
187
|
+
compatibility.safe_constantize(error_class)
|
|
188
|
+
else
|
|
189
|
+
error_class
|
|
190
|
+
end
|
|
174
191
|
end
|
|
175
|
-
|
|
176
|
-
errors.compact.freeze
|
|
177
192
|
end
|
|
178
193
|
|
|
179
194
|
# Legacy TOML error types are lazy, so check them on each call.
|
|
180
|
-
|
|
181
|
-
toml_errors = compatibility.safe_constantize("TomlRB::ParseError")
|
|
182
|
-
toml_errors = Array(toml_errors)
|
|
195
|
+
toml_errors = Array(compatibility.safe_constantize("TomlRB::ParseError"))
|
|
183
196
|
tomllib_err = compatibility.safe_constantize("Tomlib::ParseError")
|
|
184
197
|
toml_errors << tomllib_err if tomllib_err
|
|
185
198
|
teptris_err = compatibility.safe_constantize("Teptris::ParseError")
|
|
186
199
|
toml_errors << teptris_err if teptris_err
|
|
187
200
|
|
|
188
|
-
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# Reset cached error types (for test isolation)
|
|
192
|
-
def reset_format_error_types_cache!
|
|
193
|
-
@format_error_types_base = nil
|
|
201
|
+
(errors + toml_errors).compact
|
|
194
202
|
end
|
|
195
203
|
|
|
196
204
|
# Create a model instance from a parsed document
|
|
@@ -172,12 +172,30 @@ module Lutaml
|
|
|
172
172
|
#
|
|
173
173
|
# @param register [Symbol, nil] The register context
|
|
174
174
|
def ensure_imports!(register = nil)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
#
|
|
179
|
-
#
|
|
180
|
-
|
|
175
|
+
# Bounded re-entry for the resolution chain. Each leg's
|
|
176
|
+
# record_import_attempt tail calls attributes(), which calls
|
|
177
|
+
# back into ensure_imports!; nested passes converge imports
|
|
178
|
+
# whose targets only become resolvable after an earlier leg
|
|
179
|
+
# (so they must not be dropped), but with a permanently
|
|
180
|
+
# pending import the tail re-entered unboundedly and crashed
|
|
181
|
+
# with SystemStackError (#938). Two levels allow one nested
|
|
182
|
+
# convergence pass; deeper re-entry is suppressed per
|
|
183
|
+
# register and the counter unwinds on error.
|
|
184
|
+
register_id = extract_register_id(register)
|
|
185
|
+
(@ensure_imports_depth ||= {})[register_id] ||= 0
|
|
186
|
+
return if @ensure_imports_depth[register_id] >= 2
|
|
187
|
+
|
|
188
|
+
@ensure_imports_depth[register_id] += 1
|
|
189
|
+
begin
|
|
190
|
+
ensure_model_imports!(register)
|
|
191
|
+
ensure_choice_imports!(register)
|
|
192
|
+
ensure_restrict_attributes!(register)
|
|
193
|
+
# Hook for format-specific mapping import resolution.
|
|
194
|
+
# XML overrides this to call mappings[:xml]&.ensure_mappings_imported!(register)
|
|
195
|
+
ensure_format_mapping_imports!(register)
|
|
196
|
+
ensure
|
|
197
|
+
@ensure_imports_depth[register_id] -= 1
|
|
198
|
+
end
|
|
181
199
|
end
|
|
182
200
|
|
|
183
201
|
# Hook for format-specific mapping import resolution.
|
|
@@ -345,8 +363,20 @@ module Lutaml
|
|
|
345
363
|
# @param register [Symbol, nil] The register context
|
|
346
364
|
# @return [Object] The allocated instance
|
|
347
365
|
def allocate_for_deserialization(register = nil)
|
|
348
|
-
instance = allocate
|
|
349
366
|
register_id = extract_register_id(register)
|
|
367
|
+
# Deferred imports (symbol-form import_model_attributes) must
|
|
368
|
+
# resolve before state defaults compile: the compiled seeding
|
|
369
|
+
# and the raw readers cover the post-import attribute set, and
|
|
370
|
+
# the transform's records already carry the mapping's rules
|
|
371
|
+
# for imported names — CTR's <m:br> parsed before resolution
|
|
372
|
+
# had no reader (#933). The constructor path ensures the same
|
|
373
|
+
# way (initialize_attributes). Guarded: deep object graphs
|
|
374
|
+
# (XSD schema trees) allocate recursively, and the ensure
|
|
375
|
+
# chain's frames per level pushed that recursion past the
|
|
376
|
+
# stack limit — models without pending deferred imports skip
|
|
377
|
+
# it entirely.
|
|
378
|
+
ensure_imports!(register_id) if finalized? && deferred_imports_pending?(register_id)
|
|
379
|
+
instance = allocate
|
|
350
380
|
instance.finalize_deserialization(register_id)
|
|
351
381
|
instance
|
|
352
382
|
end
|
|
@@ -392,6 +422,22 @@ module Lutaml
|
|
|
392
422
|
# set (nil when no default), never left as the
|
|
393
423
|
# uninitialized sentinel — readers and formatters touch
|
|
394
424
|
# absent attributes freely.
|
|
425
|
+
#
|
|
426
|
+
# lutaml-model#922: unless a custom writer already
|
|
427
|
+
# populated it. An earlier kwarg's writer may have
|
|
428
|
+
# assigned this attribute as a side effect (relaton's ICS
|
|
429
|
+
# populates <text> from the Isoics dataset inside code=);
|
|
430
|
+
# the raw state is the truth — still at its initialized
|
|
431
|
+
# marker (sentinel / lazy collection) means unset, and
|
|
432
|
+
# only then is the default seeded. The writer's own
|
|
433
|
+
# value_set_for (inside the compiled writer) already
|
|
434
|
+
# marked it set, so it renders.
|
|
435
|
+
raw = instance.public_send(:"__lutaml_raw_#{name}")
|
|
436
|
+
unset = raw.nil? ||
|
|
437
|
+
raw.equal?(Lutaml::Model::UninitializedClass.instance) ||
|
|
438
|
+
raw.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION)
|
|
439
|
+
next unless unset
|
|
440
|
+
|
|
395
441
|
default = attr.default_value(register_id, instance)
|
|
396
442
|
value = if Lutaml::Model::Utils.uninitialized?(default)
|
|
397
443
|
nil
|
|
@@ -409,6 +455,7 @@ module Lutaml
|
|
|
409
455
|
unknown = attrs.keys.find { |k| !known.include?(k.to_sym) }
|
|
410
456
|
raise Error, "unknown attribute '#{unknown}' for #{self}"
|
|
411
457
|
end
|
|
458
|
+
instance.clear_mutated_after_parse!
|
|
412
459
|
instance
|
|
413
460
|
end
|
|
414
461
|
|
|
@@ -60,6 +60,10 @@ module Lutaml
|
|
|
60
60
|
model.attributes.each_value { |attr| define_attribute_methods(attr) }
|
|
61
61
|
@attributes.merge!(Utils.deep_dup(model.attributes))
|
|
62
62
|
@choice_attributes.concat(deep_duplicate_choice_attributes(model))
|
|
63
|
+
# Imported attributes grow the state-defaults surface (the
|
|
64
|
+
# raw readers live there) — a first use may already have
|
|
65
|
+
# compiled it for the pre-import attribute set (#933).
|
|
66
|
+
invalidate_state_defaults!
|
|
63
67
|
# Ensure @models_imported is a hash; migrate from legacy nil/false state
|
|
64
68
|
@models_imported ||= {}
|
|
65
69
|
@models_imported[reg] = true
|
|
@@ -122,6 +126,9 @@ module Lutaml
|
|
|
122
126
|
deep_duplicate_choice_attributes(model, register_id),
|
|
123
127
|
)
|
|
124
128
|
@merged_attributes_cache = nil
|
|
129
|
+
# See import_model_attributes: late-registered attributes
|
|
130
|
+
# must recompile the register's state defaults (#933).
|
|
131
|
+
invalidate_state_defaults!
|
|
125
132
|
end
|
|
126
133
|
private :register_only_import_model_attributes
|
|
127
134
|
|
|
@@ -153,6 +160,43 @@ module Lutaml
|
|
|
153
160
|
# Ensure all model imports are resolved for a specific register
|
|
154
161
|
#
|
|
155
162
|
# @param register_id [Symbol, nil] The register context
|
|
163
|
+
# Whether deferred (symbol-form) imports are still pending —
|
|
164
|
+
# gates the allocation-time ensure chain (#933).
|
|
165
|
+
# The attribute-set fingerprint at the moment an import
|
|
166
|
+
# resolution attempt ran — deferred_imports_pending? compares
|
|
167
|
+
# against it to skip re-resolution on stable sets.
|
|
168
|
+
def record_import_attempt(register_id)
|
|
169
|
+
# Raw stores only — the lazy attributes() re-enters
|
|
170
|
+
# ensure_imports! and, when @models_imported was just set to
|
|
171
|
+
# false (unresolved imports), cycles through this very
|
|
172
|
+
# method forever (the uniword register_in SystemStackError).
|
|
173
|
+
register_attrs = (@register_records ||= {})[register_id]
|
|
174
|
+
(@import_attempt_fingerprints ||= {})[register_id] =
|
|
175
|
+
@attributes.size + @choice_attributes.to_a.size +
|
|
176
|
+
(register_attrs ? register_attrs[:attributes].to_a.size : 0)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def deferred_imports_pending?(register_id = nil)
|
|
180
|
+
register_id ||= Lutaml::Model::Config.default_register
|
|
181
|
+
has_importables = importable_models.any? { |_m, models| models.any? } ||
|
|
182
|
+
importable_choices.any? do |_c, imports|
|
|
183
|
+
imports.any? { |_m, models| models.any? }
|
|
184
|
+
end || restrict_attributes.any?
|
|
185
|
+
return false unless has_importables
|
|
186
|
+
|
|
187
|
+
# Unresolvable imports stay pending forever; re-running the
|
|
188
|
+
# ensure chain (and its state-defaults invalidation) on every
|
|
189
|
+
# allocation thrashed import-heavy models quadratically.
|
|
190
|
+
# Resolution re-arms only when the attribute set CHANGED
|
|
191
|
+
# since the last attempt — a late declaration after a failed
|
|
192
|
+
# resolution re-attempts; a stable set skips.
|
|
193
|
+
attempted = (@import_attempt_fingerprints ||= {})[register_id]
|
|
194
|
+
return true if attempted.nil?
|
|
195
|
+
|
|
196
|
+
current = attributes(register_id).size + @choice_attributes.to_a.size
|
|
197
|
+
current != attempted
|
|
198
|
+
end
|
|
199
|
+
|
|
156
200
|
def ensure_model_imports!(register_id = nil)
|
|
157
201
|
register_id ||= Lutaml::Model::Config.default_register
|
|
158
202
|
@models_imported = {} if @models_imported.nil? || @models_imported == false
|
|
@@ -184,6 +228,7 @@ module Lutaml
|
|
|
184
228
|
end
|
|
185
229
|
|
|
186
230
|
@models_imported[register_id] = all_resolved
|
|
231
|
+
record_import_attempt(register_id)
|
|
187
232
|
end
|
|
188
233
|
|
|
189
234
|
# Ensure all choice imports are resolved for a specific register
|
|
@@ -197,6 +242,7 @@ module Lutaml
|
|
|
197
242
|
@choices_imported[register_id] = true
|
|
198
243
|
all_resolved = true
|
|
199
244
|
|
|
245
|
+
resolved_any = false
|
|
200
246
|
importable_choices.each do |choice, choice_imports|
|
|
201
247
|
choice_imports.each do |method, models|
|
|
202
248
|
models.uniq.each do |model|
|
|
@@ -216,11 +262,20 @@ module Lutaml
|
|
|
216
262
|
end
|
|
217
263
|
|
|
218
264
|
choice.public_send(method, model_class, register_id)
|
|
265
|
+
resolved_any = true
|
|
219
266
|
end
|
|
220
267
|
end
|
|
221
268
|
end
|
|
222
269
|
|
|
223
270
|
@choices_imported[register_id] = true if all_resolved
|
|
271
|
+
record_import_attempt(register_id)
|
|
272
|
+
# Choice imports can add attributes without passing through
|
|
273
|
+
# define_attribute_methods — recompile state defaults so the
|
|
274
|
+
# seeding and raw readers cover them (#933). Only when an
|
|
275
|
+
# import actually resolved: ensure_choice_imports! runs on
|
|
276
|
+
# every ensure_imports! (allocation-time), and a recompile
|
|
277
|
+
# per allocation would churn.
|
|
278
|
+
invalidate_state_defaults! if resolved_any
|
|
224
279
|
end
|
|
225
280
|
|
|
226
281
|
# Ensure all restrict attributes are applied
|
|
@@ -243,6 +298,11 @@ module Lutaml
|
|
|
243
298
|
# Uses TracePoint to detect when a class definition is complete,
|
|
244
299
|
# allowing deferred import resolution.
|
|
245
300
|
def setup_trace_point
|
|
301
|
+
# Opal has no TracePoint. Symbol imports still resolve lazily
|
|
302
|
+
# on first use (ensure_imports!), so there the class is marked
|
|
303
|
+
# finalized at once instead of at the end of its body.
|
|
304
|
+
return @finalized = true if Lutaml::Model.opal?
|
|
305
|
+
|
|
246
306
|
@trace ||= TracePoint.new(:end) do |_tp|
|
|
247
307
|
if include?(Lutaml::Model::Serialize)
|
|
248
308
|
@finalized = true
|
|
@@ -197,8 +197,11 @@ module Lutaml
|
|
|
197
197
|
end
|
|
198
198
|
|
|
199
199
|
def attr_value(attrs, name, attribute)
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
# The default is computed only on absence: it used to be
|
|
201
|
+
# evaluated eagerly as the fetch fallback on EVERY value,
|
|
202
|
+
# paying proc/static default dispatch per present attribute.
|
|
203
|
+
value = Utils.fetch_str_or_sym(attrs, name, Utils::MISSING)
|
|
204
|
+
value = attribute.default(lutaml_register, self) if value.equal?(Utils::MISSING)
|
|
202
205
|
attribute.cast_value(value, lutaml_register)
|
|
203
206
|
end
|
|
204
207
|
|
|
@@ -368,15 +371,20 @@ module Lutaml
|
|
|
368
371
|
value = self.class.apply_value_map(value, vmap, attr)
|
|
369
372
|
# Performance: Only call ensure_utf8 for string values
|
|
370
373
|
value = self.class.ensure_utf8(value) if value.is_a?(::String)
|
|
371
|
-
|
|
374
|
+
# Interpolating the writer symbol allocated a String per
|
|
375
|
+
# attribute per instance; the per-register writer table is
|
|
376
|
+
# interned once per class.
|
|
377
|
+
public_send(self.class.instantiate_writers(lutaml_register)[name], value)
|
|
372
378
|
using_default_for(name) if default
|
|
373
379
|
end
|
|
374
380
|
end
|
|
375
381
|
|
|
376
382
|
def determine_value(attrs, name, attr)
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
383
|
+
# One hash pass: a sentinel fetch distinguishes absent keys
|
|
384
|
+
# from present-nil values, replacing two key probes plus the
|
|
385
|
+
# re-fetch inside attr_value (four hash ops per attribute).
|
|
386
|
+
value = Utils.fetch_str_or_sym(attrs, name, Utils::MISSING)
|
|
387
|
+
return attr.cast_value(value, lutaml_register) unless value.equal?(Utils::MISSING)
|
|
380
388
|
|
|
381
389
|
resolved = attr.default_value(lutaml_register, self)
|
|
382
390
|
return Lutaml::Model::UninitializedClass.instance if Lutaml::Model::Utils.uninitialized?(resolved)
|
|
@@ -13,6 +13,12 @@ module Lutaml
|
|
|
13
13
|
case value
|
|
14
14
|
when ::Time then value
|
|
15
15
|
when ::DateTime then value.to_time
|
|
16
|
+
when ::Integer
|
|
17
|
+
# YAML 1.1 sexagesimal: psych resolves an unquoted hh:mm:ss
|
|
18
|
+
# scalar to seconds since midnight (25200 for "07:00:00").
|
|
19
|
+
# Feeding that integer through Time.parse yields a garbage
|
|
20
|
+
# date — reconstruct the wall-clock time of the current day.
|
|
21
|
+
::Date.today.to_time + value
|
|
16
22
|
else ::Time.parse(value.to_s)
|
|
17
23
|
end
|
|
18
24
|
rescue ArgumentError
|
|
@@ -67,12 +67,14 @@ from: nil)
|
|
|
67
67
|
# @param type_class [Class] the type class to look up
|
|
68
68
|
# @return [Hash, nil] { to: Proc, from: Proc } or nil
|
|
69
69
|
def format_type_serializer_for(format, type_class)
|
|
70
|
-
# Hot path:
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
# Hot path: memoized per format -> class with NO key
|
|
71
|
+
# allocation — the previous [format, class] array key
|
|
72
|
+
# allocated on every lookup (2k arrays per 200 serializes in
|
|
73
|
+
# the object-mode profile). Identity-keyed nested maps; the
|
|
74
|
+
# hierarchy walk below only runs on a miss, and format
|
|
75
|
+
# serializers register at load time, so a hit is stable.
|
|
76
|
+
per_format = (SERIALIZER_LOOKUP_CACHE[format] ||= {}.compare_by_identity)
|
|
77
|
+
return per_format[type_class] if per_format.key?(type_class)
|
|
76
78
|
|
|
77
79
|
klass = type_class
|
|
78
80
|
found = nil
|
|
@@ -85,7 +87,7 @@ from: nil)
|
|
|
85
87
|
|
|
86
88
|
klass = klass.superclass
|
|
87
89
|
end
|
|
88
|
-
|
|
90
|
+
per_format[type_class] = found
|
|
89
91
|
found
|
|
90
92
|
end
|
|
91
93
|
|
|
@@ -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/utils.rb
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Model
|
|
5
5
|
module Utils
|
|
6
|
+
# Fetch sentinel distinguishing absent keys from present-nil
|
|
7
|
+
# values; shared by the deserialization fast paths.
|
|
8
|
+
MISSING = Object.new.freeze
|
|
9
|
+
|
|
6
10
|
class << self
|
|
7
11
|
UNINITIALIZED = Lutaml::Model::UninitializedClass.instance
|
|
8
12
|
|
|
@@ -175,19 +179,28 @@ module Lutaml
|
|
|
175
179
|
if hash.key?(key)
|
|
176
180
|
hash[key]
|
|
177
181
|
else
|
|
178
|
-
|
|
182
|
+
sym = key.to_sym
|
|
183
|
+
hash.key?(sym) ? hash[sym] : default
|
|
179
184
|
end
|
|
180
185
|
when Symbol
|
|
181
186
|
if hash.key?(key)
|
|
182
187
|
hash[key]
|
|
183
188
|
else
|
|
184
|
-
|
|
189
|
+
# Symbol#name is the frozen string — no allocation per probe.
|
|
190
|
+
# This branch is the hot path of the serialization walk
|
|
191
|
+
# (Symbol attribute names against String-keyed hashes), and
|
|
192
|
+
# the double key.to_s here was a top string allocation site
|
|
193
|
+
# in large-document metanorma compiles.
|
|
194
|
+
str = key.name
|
|
195
|
+
hash.key?(str) ? hash[str] : default
|
|
185
196
|
end
|
|
186
197
|
else
|
|
187
|
-
|
|
188
|
-
|
|
198
|
+
str = key.to_s
|
|
199
|
+
if hash.key?(str)
|
|
200
|
+
hash[str]
|
|
189
201
|
else
|
|
190
|
-
|
|
202
|
+
sym = key.to_sym
|
|
203
|
+
hash.key?(sym) ? hash[sym] : default
|
|
191
204
|
end
|
|
192
205
|
end
|
|
193
206
|
end
|
data/lib/lutaml/model/version.rb
CHANGED
data/lib/lutaml/model.rb
CHANGED
|
@@ -294,3 +294,10 @@ require "#{__dir__}/xml/format"
|
|
|
294
294
|
# Builder must be prepended AFTER XML so its initialize runs first
|
|
295
295
|
# (Builder -> XML InstanceMethods -> Serialize)
|
|
296
296
|
Lutaml::Model::Serialize.prepend(Lutaml::Model::Serialize::Builder)
|
|
297
|
+
|
|
298
|
+
# Opal does not propagate a module prepended into an already-included
|
|
299
|
+
# module to classes that included it earlier (see lib/lutaml/xml/format.rb).
|
|
300
|
+
# Serializable included Serialize at boot, so Builder goes onto it directly.
|
|
301
|
+
if Lutaml::Model.opal?
|
|
302
|
+
Lutaml::Model::Serializable.prepend(Lutaml::Model::Serialize::Builder)
|
|
303
|
+
end
|
|
@@ -22,7 +22,7 @@ module Lutaml
|
|
|
22
22
|
parsed = parse_with_moxml(xml, parse_encoding)
|
|
23
23
|
root_element = parsed.root
|
|
24
24
|
|
|
25
|
-
raise_empty_document_error if root_element.nil?
|
|
25
|
+
raise_empty_document_error(parsed.parse_errors) if root_element.nil?
|
|
26
26
|
|
|
27
27
|
root = self::PARSED_ELEMENT_CLASS.new(root_element)
|
|
28
28
|
doc_pis = extract_document_processing_instructions(parsed)
|
|
@@ -69,8 +69,11 @@ module Lutaml
|
|
|
69
69
|
}
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
def raise_empty_document_error
|
|
72
|
+
def raise_empty_document_error(parse_errors = nil)
|
|
73
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
|
|
74
77
|
|
|
75
78
|
case self::EMPTY_DOCUMENT_ERROR_TYPE
|
|
76
79
|
when :parse_exception
|
|
@@ -19,9 +19,12 @@ module Lutaml
|
|
|
19
19
|
File.join(File.dirname(__FILE__), "adapter", type)
|
|
20
20
|
end
|
|
21
21
|
require adapter_path
|
|
22
|
-
rescue LoadError
|
|
22
|
+
rescue LoadError => e
|
|
23
|
+
# Chain the cause: a known adapter whose underlying engine gem is
|
|
24
|
+
# missing (e.g. no nokogiri in the Gemfile) must not present as a
|
|
25
|
+
# bare unknown-type error — the LoadError names the real gap.
|
|
23
26
|
raise Lutaml::Model::UnknownAdapterTypeError.new("xml", type),
|
|
24
|
-
cause:
|
|
27
|
+
cause: e
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
# Load the Moxml adapter for XML
|
data/lib/lutaml/xml/format.rb
CHANGED
|
@@ -22,11 +22,9 @@ end
|
|
|
22
22
|
module Lutaml
|
|
23
23
|
module Xml
|
|
24
24
|
autoload :HtmlEntities, "#{__dir__}/html_entities"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
autoload :
|
|
28
|
-
autoload :PlanSerializer, "#{__dir__}/plan_serializer"
|
|
29
|
-
autoload :PlanWalk, "#{__dir__}/plan_walk"
|
|
25
|
+
# The leptris plan fast path lives under its own namespace; its
|
|
26
|
+
# autoloads and the leptris version gate live in lutaml/xml/leptris.
|
|
27
|
+
autoload :Leptris, "#{__dir__}/leptris"
|
|
30
28
|
# Error module for XML-specific errors
|
|
31
29
|
module Error
|
|
32
30
|
autoload :XmlError, "#{__dir__}/error/xml_error"
|
|
@@ -202,7 +200,13 @@ Lutaml::Model::FormatRegistry.register(
|
|
|
202
200
|
else
|
|
203
201
|
{
|
|
204
202
|
available: %i[nokogiri ox oga rexml leptris],
|
|
205
|
-
|
|
203
|
+
# #908: the metadata default is the LAST resort —
|
|
204
|
+
# explicit configuration and moxml runtime detection
|
|
205
|
+
# answer first. When resolution falls this far, prefer
|
|
206
|
+
# leptris, consistent with moxml's runtime preference
|
|
207
|
+
# (leptris whenever present); bundles without it
|
|
208
|
+
# resolve through auto-detection long before here.
|
|
209
|
+
default: :leptris,
|
|
206
210
|
}
|
|
207
211
|
end,
|
|
208
212
|
)
|
|
@@ -239,9 +243,11 @@ Lutaml::Model::Serialize.prepend(
|
|
|
239
243
|
#
|
|
240
244
|
# Anonymous model classes (Class.new { include Serialize }) extend
|
|
241
245
|
# Serialize::ClassMethods rather than inheriting from Serializable, so the
|
|
242
|
-
# ModelImportExt override (root?) must land on ClassMethods too.
|
|
243
|
-
#
|
|
244
|
-
#
|
|
246
|
+
# ModelImportExt override (root?) must land on ClassMethods too.
|
|
247
|
+
#
|
|
248
|
+
# On Opal (and TruffleRuby, below) the ClassMethods.prepend(FormatConversion)
|
|
249
|
+
# above does not reach Serializable's singleton chain, which extended
|
|
250
|
+
# ClassMethods before it ran, so it is prepended onto that singleton directly.
|
|
245
251
|
#
|
|
246
252
|
# TruffleRuby (34.0.1) has the same propagation gap, on both the include
|
|
247
253
|
# side and the extend/singleton side — a module prepended into an
|
|
@@ -269,13 +275,9 @@ if Lutaml::Model.opal? || RUBY_ENGINE == "truffleruby"
|
|
|
269
275
|
Lutaml::Xml::Serialization::ModelImportExt,
|
|
270
276
|
)
|
|
271
277
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
Lutaml::Model::Serializable.singleton_class.prepend(
|
|
276
|
-
Lutaml::Xml::Serialization::FormatConversion,
|
|
277
|
-
)
|
|
278
|
-
end
|
|
278
|
+
Lutaml::Model::Serializable.singleton_class.prepend(
|
|
279
|
+
Lutaml::Xml::Serialization::FormatConversion,
|
|
280
|
+
)
|
|
279
281
|
end
|
|
280
282
|
|
|
281
283
|
# Register XML-specific attribute override warning names
|