lutaml-model 0.8.36 → 0.8.38
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/bench/bench_sts.rb +9 -3
- data/bench/gate_config.rb +5 -4
- data/lib/compat/opal/lutaml_model_boot.rb +1 -0
- data/lib/lutaml/key_value/transform.rb +52 -5
- data/lib/lutaml/key_value/transformation.rb +10 -2
- data/lib/lutaml/model/adapter_resolver.rb +14 -2
- data/lib/lutaml/model/attribute.rb +15 -9
- data/lib/lutaml/model/choice.rb +6 -5
- data/lib/lutaml/model/format_registry.rb +3 -1
- data/lib/lutaml/model/liquefiable.rb +23 -2
- data/lib/lutaml/model/mapping/mapping_rule.rb +46 -11
- data/lib/lutaml/model/sequence.rb +47 -2
- data/lib/lutaml/model/serialize/attribute_definition.rb +176 -47
- data/lib/lutaml/model/serialize/deserialization_context.rb +1 -0
- data/lib/lutaml/model/serialize.rb +54 -10
- data/lib/lutaml/model/services/transformer.rb +30 -11
- data/lib/lutaml/model/store.rb +70 -32
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/format.rb +1 -0
- data/lib/lutaml/xml/html_entities.rb +290 -0
- data/lib/lutaml/xml/mapping.rb +72 -0
- data/lib/lutaml/xml/model_transform.rb +210 -72
- data/lib/lutaml/xml/parse_session.rb +38 -0
- data/lib/lutaml/yaml/format.rb +1 -0
- data/spec/lutaml/model/adapter_resolver_configured_spec.rb +38 -0
- data/spec/lutaml/model/custom_method_context_spec.rb +62 -0
- data/spec/lutaml/model/liquefiable_lazy_spec.rb +36 -0
- data/spec/lutaml/model/mapping_type_validation_spec.rb +61 -0
- data/spec/lutaml/model/sequence_choice_compositor_spec.rb +66 -0
- data/spec/lutaml/model/store_spec.rb +13 -11
- data/spec/lutaml/model/yaml_stream_spec.rb +70 -0
- data/spec/lutaml/xml/html_entities_spec.rb +45 -0
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ad19014fa289dad72ab50f6908d425ed7708b4f7046432ac05426b65087fc713
|
|
4
|
+
data.tar.gz: 4f2d95722e22e73b3691e6eec7903d3967ec1d9cf9ea2b641b02b77cc59911ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 575ac30c297c4d8059bccb27b42b1d1e942ffef3352072f1ebdbff55039073cffb228a2e6873c55afd940bc78aaac1830c5375df0b37ac1b4eb498cea3a73fe2
|
|
7
|
+
data.tar.gz: 4d1c80a2a3fd04e5c461aec33ba2248547ae5f6c877990f457d23160c272a4f67e17ef4ab05ad450265f5c2aa8c5c637deead3c27b20a2ea1978ab4815d6d397
|
data/bench/bench_sts.rb
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
# BENCH_JSON=/tmp/results.json bundle exec ruby tmp/bench/bench_sts.rb
|
|
10
10
|
|
|
11
11
|
require_relative "bench_common"
|
|
12
|
+
require_relative "gate_config"
|
|
12
13
|
include BenchCommon
|
|
13
14
|
|
|
14
15
|
print_header("STS Ruby Benchmark — ISO/NISO STS document parsing")
|
|
@@ -39,11 +40,16 @@ sts_files.each do |label, path|
|
|
|
39
40
|
end
|
|
40
41
|
end
|
|
41
42
|
|
|
43
|
+
# The informative print reads the configured gate (bench_compare is the
|
|
44
|
+
# enforcing layer). The old hardcoded 2.0s literal predated the
|
|
45
|
+
# gate_config split and mislabeled every CI run (#313, TODO.max-perf/09).
|
|
42
46
|
puts "\n Gate checks:"
|
|
43
47
|
if results["iso-13849-1MB"]
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
gate = GateConfig::GATES.dig(:sts, "iso-13849-1MB") || {}
|
|
49
|
+
limit = gate[:absolute_max]
|
|
50
|
+
avg = results["iso-13849-1MB"][:avg_time]
|
|
51
|
+
status = avg < limit ? "PASS" : "FAIL"
|
|
52
|
+
printf " ISO-13849-1MB < %.1fs: %s (%.3fs)\n", limit, status, avg
|
|
47
53
|
end
|
|
48
54
|
|
|
49
55
|
write_results_json(json_output_path, results) if json_output_path
|
data/bench/gate_config.rb
CHANGED
|
@@ -57,10 +57,11 @@ module GateConfig
|
|
|
57
57
|
"iso-13849-1MB" => {
|
|
58
58
|
alloc_ratio: 1.05,
|
|
59
59
|
time_ratio: 1.15,
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
|
|
60
|
+
# Post-TODO.max-perf/01-03 the fixture sits at ~4.88M allocs and
|
|
61
|
+
# 14.5-16.5s on GH-hosted ubuntu runners (runner-variance band);
|
|
62
|
+
# the old 15.0 missed by 0.06s on a healthy run. 30.0 keeps the
|
|
63
|
+
# 2x-catastrophe safety net over the healthy band (#313).
|
|
64
|
+
absolute_max: 30.0,
|
|
64
65
|
},
|
|
65
66
|
"din-iso-1.1MB" => {
|
|
66
67
|
alloc_ratio: 1.05,
|
|
@@ -146,6 +146,7 @@ require "lutaml/xml/encoding_normalizer"
|
|
|
146
146
|
require "lutaml/xml/format"
|
|
147
147
|
require "lutaml/xml/format_chooser"
|
|
148
148
|
require "lutaml/xml/hoisting_algorithm"
|
|
149
|
+
require "lutaml/xml/html_entities"
|
|
149
150
|
require "lutaml/xml/listener"
|
|
150
151
|
require "lutaml/xml/mapping"
|
|
151
152
|
require "lutaml/xml/mapping_rule"
|
|
@@ -68,10 +68,33 @@ module Lutaml
|
|
|
68
68
|
|
|
69
69
|
def process_mapping_for_instance(instance, hash, format, rule, options)
|
|
70
70
|
if rule.custom_methods[:to]
|
|
71
|
-
|
|
71
|
+
to_method = rule.custom_methods[:to]
|
|
72
|
+
# lutaml-model#550: custom methods may declare a third context
|
|
73
|
+
# parameter to receive the options passed to `to_*`.
|
|
74
|
+
if instance.method(to_method).parameters.size >= 3
|
|
75
|
+
return instance.public_send(to_method, instance, hash,
|
|
76
|
+
options[:context])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
return instance.public_send(to_method, instance, hash)
|
|
72
80
|
end
|
|
73
81
|
|
|
74
82
|
attribute = attributes[rule.to]
|
|
83
|
+
|
|
84
|
+
# TODO.max-perf/10: plain scalar/collection rules take a fast
|
|
85
|
+
# lane — memoized wire name, shared render? semantics, one
|
|
86
|
+
# serialize dispatch — collapsing the interpretive branch
|
|
87
|
+
# probes below. Rules with any special feature stay on the
|
|
88
|
+
# full path.
|
|
89
|
+
if (plan = kv_serialize_plan(rule, attribute, instance))
|
|
90
|
+
value = instance.public_send(attribute.name)
|
|
91
|
+
if rule.render?(value, instance)
|
|
92
|
+
hash[plan.wire] = attribute.serialize(value, format,
|
|
93
|
+
lutaml_register, options)
|
|
94
|
+
end
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
|
|
75
98
|
value = rule.serialize(instance)
|
|
76
99
|
|
|
77
100
|
if rule.can_transform_to?(attribute, format)
|
|
@@ -90,7 +113,9 @@ module Lutaml
|
|
|
90
113
|
end
|
|
91
114
|
|
|
92
115
|
# Use the format parameter passed in instead of hardcoding to :json
|
|
93
|
-
value = ExportTransformer.call(value, rule, attribute,
|
|
116
|
+
value = ExportTransformer.call(value, rule, attribute,
|
|
117
|
+
format: format,
|
|
118
|
+
context: options[:context])
|
|
94
119
|
|
|
95
120
|
value = serialize_value(value, rule, attribute, format, options)
|
|
96
121
|
|
|
@@ -247,7 +272,7 @@ format)
|
|
|
247
272
|
if (plan = kv_rule_plan(format, rule, attr)) &&
|
|
248
273
|
(value = kv_fast_extract(doc, plan))
|
|
249
274
|
rule.deserialize(instance, kv_fast_cast(value, plan, instance),
|
|
250
|
-
attributes, self)
|
|
275
|
+
attributes, self, options[:context])
|
|
251
276
|
return
|
|
252
277
|
end
|
|
253
278
|
|
|
@@ -261,7 +286,9 @@ format)
|
|
|
261
286
|
# only nil (non-existent) skips it (lutaml-model#746).
|
|
262
287
|
return if value.nil?
|
|
263
288
|
|
|
264
|
-
|
|
289
|
+
warn "PROBE-264 keys=#{options.keys.inspect} ctx=#{options[:context].inspect}"
|
|
290
|
+
return rule.deserialize(instance, value, attributes, model_class,
|
|
291
|
+
options[:context])
|
|
265
292
|
end
|
|
266
293
|
|
|
267
294
|
value = rule.transform_value(attr, value, :from, format)
|
|
@@ -279,7 +306,8 @@ format)
|
|
|
279
306
|
if attr.collection? || !instance.is_a?(Lutaml::Model::Serialize)
|
|
280
307
|
attr.valid_collection!(value, context)
|
|
281
308
|
end
|
|
282
|
-
rule.deserialize(instance, value, attributes, self
|
|
309
|
+
rule.deserialize(instance, value, attributes, self,
|
|
310
|
+
options[:context])
|
|
283
311
|
end
|
|
284
312
|
|
|
285
313
|
# Compiled rule plans (TODO.perf/07): for plain scalar rules the
|
|
@@ -291,6 +319,25 @@ format)
|
|
|
291
319
|
# unions, polymorphism) return nil and keep the interpretive path.
|
|
292
320
|
KvRulePlan = ::Struct.new(:wire, :klass, :collection)
|
|
293
321
|
|
|
322
|
+
# Memoized (per transform) wire name for plain rules — the from
|
|
323
|
+
# spelling never changes after definition.
|
|
324
|
+
KvSerializePlan = ::Struct.new(:wire)
|
|
325
|
+
|
|
326
|
+
def kv_serialize_plan(rule, attr, _instance)
|
|
327
|
+
return nil if rule.delegate || rule.raw_mapping? || rule.root_mapping? ||
|
|
328
|
+
rule.hash_mappings || rule.child_mappings ||
|
|
329
|
+
rule.has_custom_method_for_serialization? ||
|
|
330
|
+
rule.multiple_mappings? || polymorphic_rule?(rule) ||
|
|
331
|
+
(rule.transform.is_a?(Hash) && !rule.transform.empty?) ||
|
|
332
|
+
rule.transform.is_a?(Class) || attr.nil? || attr.derived? ||
|
|
333
|
+
attr.union? || attr.polymorphic? || attr.custom_collection? ||
|
|
334
|
+
attr.transform ||
|
|
335
|
+
rule.value_map(:to) != Lutaml::Model::Serialize::DEFAULT_VALUE_MAP
|
|
336
|
+
|
|
337
|
+
@kv_serialize_plans ||= {}.compare_by_identity
|
|
338
|
+
@kv_serialize_plans[rule] ||= KvSerializePlan.new(rule_from_name(rule))
|
|
339
|
+
end
|
|
340
|
+
|
|
294
341
|
def kv_rule_plan(format, rule, attr)
|
|
295
342
|
@kv_rule_plans ||= {}
|
|
296
343
|
@kv_rule_plans[[format, rule]] ||= build_kv_rule_plan(format, rule,
|
|
@@ -220,9 +220,17 @@ register = self.register)
|
|
|
220
220
|
|
|
221
221
|
# Handle custom serialization methods (e.g., with: { to: ... })
|
|
222
222
|
if rule.has_custom_methods? && rule.custom_methods[:to]
|
|
223
|
+
to_method = rule.custom_methods[:to]
|
|
224
|
+
# lutaml-model#550: custom methods may declare a third context
|
|
225
|
+
# parameter to receive the options passed to `to_*`.
|
|
226
|
+
if model_instance.method(to_method).parameters.size >= 3
|
|
227
|
+
return model_instance.public_send(to_method, model_instance,
|
|
228
|
+
parent, options[:context])
|
|
229
|
+
end
|
|
230
|
+
|
|
223
231
|
# Call custom method which directly modifies the parent element
|
|
224
|
-
return model_instance.public_send(
|
|
225
|
-
|
|
232
|
+
return model_instance.public_send(to_method, model_instance,
|
|
233
|
+
parent)
|
|
226
234
|
end
|
|
227
235
|
|
|
228
236
|
# Handle delegation - extract value from delegated object
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Model
|
|
5
|
+
# Marker stored by set_adapter_class: the resolved slot holds a
|
|
6
|
+
# pre-resolved class (or nil, meaning "unset — keep detecting").
|
|
7
|
+
FIXED_ADAPTER_TYPE = :__fixed__
|
|
8
|
+
|
|
5
9
|
# Single authority for adapter resolution: (format, type_name) → adapter class.
|
|
6
10
|
#
|
|
7
11
|
# Consolidates adapter metadata, loading, validation, and auto-detection
|
|
@@ -38,11 +42,19 @@ module Lutaml
|
|
|
38
42
|
return resolved_adapter_class(format, scope_override)
|
|
39
43
|
end
|
|
40
44
|
|
|
41
|
-
# 2. Explicitly configured type
|
|
45
|
+
# 2. Explicitly configured type — a named type is an explicit pin
|
|
46
|
+
# and must never fall through to auto-detection: detection loads
|
|
47
|
+
# optional engines (e.g. yeptris) into the process even when the
|
|
48
|
+
# user pinned a standard adapter, which crashes platforms where
|
|
49
|
+
# the engine's build is broken. FIXED_ADAPTER_TYPE with no
|
|
50
|
+
# resolved class means "unset" (set_adapter_class(format, nil))
|
|
51
|
+
# and keeps the old detect-through behavior.
|
|
42
52
|
configured = configured_type(format)
|
|
43
53
|
if configured
|
|
44
54
|
adapter = resolved[format]
|
|
45
55
|
return adapter if adapter
|
|
56
|
+
|
|
57
|
+
return load_and_cache(format, configured) if configured != FIXED_ADAPTER_TYPE
|
|
46
58
|
end
|
|
47
59
|
|
|
48
60
|
# 3. Lazy auto-detection (cached)
|
|
@@ -91,7 +103,7 @@ module Lutaml
|
|
|
91
103
|
# @return [void]
|
|
92
104
|
def set_adapter_class(format, adapter_class)
|
|
93
105
|
resolved[format] = adapter_class
|
|
94
|
-
configured_types[format] =
|
|
106
|
+
configured_types[format] = FIXED_ADAPTER_TYPE
|
|
95
107
|
end
|
|
96
108
|
|
|
97
109
|
# Load and resolve an adapter class by type name.
|
|
@@ -471,7 +471,11 @@ module Lutaml
|
|
|
471
471
|
@options[:ref_model_class], @options[:ref_key_attribute])
|
|
472
472
|
end
|
|
473
473
|
|
|
474
|
-
|
|
474
|
+
# The castability verdict is a class-hierarchy fact, immutable
|
|
475
|
+
# per resolved type — check once, not per value (TODO.max-perf/15).
|
|
476
|
+
checked = (@castable_types ||= {}.compare_by_identity)
|
|
477
|
+
validate_attr_type!(resolved_type) unless checked[resolved_type]
|
|
478
|
+
checked[resolved_type] = true
|
|
475
479
|
|
|
476
480
|
resolved_type.cast(value)
|
|
477
481
|
end
|
|
@@ -1029,6 +1033,16 @@ instance_object = nil)
|
|
|
1029
1033
|
@default_type_context = nil
|
|
1030
1034
|
end
|
|
1031
1035
|
|
|
1036
|
+
# Public validation contract: mappings check their targets at
|
|
1037
|
+
# finalize (lutaml-model#296), the cast path checks per value.
|
|
1038
|
+
def validate_attr_type!(resolved_type)
|
|
1039
|
+
return true if resolved_type <= Serializable || resolved_type <= Type::Value
|
|
1040
|
+
return true if resolved_type.include?(Serialize)
|
|
1041
|
+
|
|
1042
|
+
raise Lutaml::Model::InvalidAttributeTypeError.new(name,
|
|
1043
|
+
resolved_type.name)
|
|
1044
|
+
end
|
|
1045
|
+
|
|
1032
1046
|
private
|
|
1033
1047
|
|
|
1034
1048
|
attr_writer :raw, :validations
|
|
@@ -1040,14 +1054,6 @@ instance_object = nil)
|
|
|
1040
1054
|
match&.last
|
|
1041
1055
|
end
|
|
1042
1056
|
|
|
1043
|
-
def validate_attr_type!(resolved_type)
|
|
1044
|
-
return true if resolved_type <= Serializable || resolved_type <= Type::Value
|
|
1045
|
-
return true if resolved_type.include?(Serialize)
|
|
1046
|
-
|
|
1047
|
-
raise Lutaml::Model::InvalidAttributeTypeError.new(name,
|
|
1048
|
-
resolved_type.name)
|
|
1049
|
-
end
|
|
1050
|
-
|
|
1051
1057
|
# validated_range_object is provided by CollectionHandler module
|
|
1052
1058
|
|
|
1053
1059
|
def validate_name!(name, reserved_methods:)
|
data/lib/lutaml/model/choice.rb
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Model
|
|
5
5
|
class Choice
|
|
6
|
-
attr_reader :attributes,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
attr_reader :attributes, :min, :max
|
|
7
|
+
attr_accessor :model, :format
|
|
8
|
+
|
|
9
|
+
# Rebound at mapping finalize: sequence-nested choices are built
|
|
10
|
+
# against the mapping object, validation resolves through the
|
|
11
|
+
# mapper class (lutaml-model#687).
|
|
11
12
|
|
|
12
13
|
INTERNAL_ATTRIBUTES = %i[@flat_attributes].freeze
|
|
13
14
|
|
|
@@ -23,7 +23,8 @@ module Lutaml
|
|
|
23
23
|
# @param adapter_options [Hash, nil] { available: [...], default: :name }
|
|
24
24
|
def register(format, mapping_class:, adapter_class:, transformer:,
|
|
25
25
|
adapter_loader: nil, castable_type: nil, key_value: nil,
|
|
26
|
-
rdf: nil, error_types: nil, adapter_options: nil
|
|
26
|
+
rdf: nil, error_types: nil, adapter_options: nil,
|
|
27
|
+
stream_methods: false)
|
|
27
28
|
validate_registration!(format, mapping_class, transformer)
|
|
28
29
|
|
|
29
30
|
registered_formats[format] = {
|
|
@@ -44,6 +45,7 @@ module Lutaml
|
|
|
44
45
|
::Lutaml::Model::Serialize.register_format_mapping_method(format)
|
|
45
46
|
::Lutaml::Model::Serialize.register_from_format_method(format)
|
|
46
47
|
::Lutaml::Model::Serialize.register_to_format_method(format)
|
|
48
|
+
::Lutaml::Model::Serialize.register_stream_methods(format) if stream_methods
|
|
47
49
|
|
|
48
50
|
::Lutaml::Model::Attribute.format_specific_warn_names.push(:"to_#{format}")
|
|
49
51
|
|
|
@@ -25,6 +25,27 @@ module Lutaml
|
|
|
25
25
|
register_liquid_drop_class
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# lutaml-model#800: models defined before `require "liquid"` keep
|
|
29
|
+
# their `liquid do ... end` mappings; the drop class materializes
|
|
30
|
+
# on first use. Consumers that require Liquid first keep the
|
|
31
|
+
# eager path unchanged.
|
|
32
|
+
def ensure_liquid_registered!
|
|
33
|
+
if !Object.const_defined?(:Liquid) && !liquid_loadable?
|
|
34
|
+
raise Lutaml::Model::LiquidNotEnabledError
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
register_class_if_liquid_defined
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Load the Liquid gem on first use; a missing gem surfaces as
|
|
41
|
+
# LiquidNotEnabledError at the point a drop is actually needed.
|
|
42
|
+
def liquid_loadable?
|
|
43
|
+
require "liquid"
|
|
44
|
+
true
|
|
45
|
+
rescue LoadError
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
|
|
28
49
|
def liquid_class(class_name)
|
|
29
50
|
@custom_liquid_class_name = class_name
|
|
30
51
|
end
|
|
@@ -94,6 +115,7 @@ module Lutaml
|
|
|
94
115
|
end
|
|
95
116
|
|
|
96
117
|
def to_liquid_class
|
|
118
|
+
ensure_liquid_registered!
|
|
97
119
|
register_methods unless @methods_generated
|
|
98
120
|
|
|
99
121
|
base_drop_class
|
|
@@ -132,8 +154,6 @@ module Lutaml
|
|
|
132
154
|
end
|
|
133
155
|
|
|
134
156
|
def liquid(&block)
|
|
135
|
-
return unless Object.const_defined?(:Liquid)
|
|
136
|
-
|
|
137
157
|
mappings[:liquid] ||= ::Lutaml::Model::Liquid::Mapping.new
|
|
138
158
|
mappings[:liquid].instance_eval(&block) if block
|
|
139
159
|
mappings[:liquid]
|
|
@@ -155,6 +175,7 @@ module Lutaml
|
|
|
155
175
|
end
|
|
156
176
|
|
|
157
177
|
def to_liquid
|
|
178
|
+
self.class.ensure_liquid_registered!
|
|
158
179
|
self.class.validate_liquid!
|
|
159
180
|
|
|
160
181
|
self.class.register_methods
|
|
@@ -45,6 +45,13 @@ module Lutaml
|
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Normalized name spellings for matching, computed once. The name
|
|
49
|
+
# is fixed after construction (attr_reader only), so the memo is
|
|
50
|
+
# safe. Hot loops used to redo Array(name).map(&:to_s) per element.
|
|
51
|
+
def name_strings
|
|
52
|
+
@name_strings ||= Array(name).map(&:to_s).freeze
|
|
53
|
+
end
|
|
54
|
+
|
|
48
55
|
def initialize(
|
|
49
56
|
name,
|
|
50
57
|
to:,
|
|
@@ -261,9 +268,16 @@ module Lutaml
|
|
|
261
268
|
end
|
|
262
269
|
end
|
|
263
270
|
|
|
264
|
-
def serialize(model, parent = nil, doc = nil)
|
|
271
|
+
def serialize(model, parent = nil, doc = nil, context = nil)
|
|
265
272
|
if custom_methods[:to]
|
|
266
|
-
|
|
273
|
+
to_method = custom_methods[:to]
|
|
274
|
+
# lutaml-model#550: custom methods may declare a fourth
|
|
275
|
+
# context parameter to receive the options passed to `to_*`.
|
|
276
|
+
if model.method(to_method).parameters.size >= 4
|
|
277
|
+
model.public_send(to_method, model, parent, doc, context)
|
|
278
|
+
else
|
|
279
|
+
model.public_send(to_method, model, parent, doc)
|
|
280
|
+
end
|
|
267
281
|
else
|
|
268
282
|
value = to_value_for(model)
|
|
269
283
|
|
|
@@ -282,13 +296,14 @@ module Lutaml
|
|
|
282
296
|
end
|
|
283
297
|
end
|
|
284
298
|
|
|
285
|
-
def deserialize(model, value, attributes, mapper_class = nil
|
|
299
|
+
def deserialize(model, value, attributes, mapper_class = nil,
|
|
300
|
+
context = nil)
|
|
286
301
|
if @needs_full_deserialize
|
|
287
|
-
handle_custom_method(model, value, mapper_class) ||
|
|
302
|
+
handle_custom_method(model, value, mapper_class, context) ||
|
|
288
303
|
handle_delegate(model, value, attributes) ||
|
|
289
|
-
handle_transform_method(model, value, attributes)
|
|
304
|
+
handle_transform_method(model, value, attributes, context)
|
|
290
305
|
else
|
|
291
|
-
handle_transform_method(model, value, attributes)
|
|
306
|
+
handle_transform_method(model, value, attributes, context)
|
|
292
307
|
end
|
|
293
308
|
end
|
|
294
309
|
|
|
@@ -409,10 +424,29 @@ module Lutaml
|
|
|
409
424
|
end
|
|
410
425
|
end
|
|
411
426
|
|
|
412
|
-
def handle_custom_method(model, value, mapper_class)
|
|
413
|
-
|
|
427
|
+
def handle_custom_method(model, value, mapper_class, context = nil)
|
|
428
|
+
custom = custom_methods[:from]
|
|
429
|
+
return if !custom || value.nil?
|
|
414
430
|
|
|
415
|
-
|
|
431
|
+
if custom.is_a?(String) || custom.is_a?(Symbol)
|
|
432
|
+
target = mapper_class.new
|
|
433
|
+
# lutaml-model#550: custom methods may declare a third context
|
|
434
|
+
# parameter to receive the options passed to `from_*`.
|
|
435
|
+
if target.method(custom).parameters.size >= 3
|
|
436
|
+
target.public_send(custom, model, value, context)
|
|
437
|
+
else
|
|
438
|
+
target.public_send(custom, model, value)
|
|
439
|
+
end
|
|
440
|
+
else
|
|
441
|
+
# Callable (proc/lambda): transforms the value and the rule
|
|
442
|
+
# assigns it. Exact arity 2 receives the `from_*` context.
|
|
443
|
+
transformed = if custom.arity == 2
|
|
444
|
+
custom.call(value, context)
|
|
445
|
+
else
|
|
446
|
+
custom.call(value)
|
|
447
|
+
end
|
|
448
|
+
assign_value(model, transformed)
|
|
449
|
+
end
|
|
416
450
|
true
|
|
417
451
|
end
|
|
418
452
|
|
|
@@ -432,7 +466,7 @@ module Lutaml
|
|
|
432
466
|
attributes[delegate].type(model.lutaml_register).new)
|
|
433
467
|
end
|
|
434
468
|
|
|
435
|
-
def handle_transform_method(model, value, attributes)
|
|
469
|
+
def handle_transform_method(model, value, attributes, context = nil)
|
|
436
470
|
attr = attributes[to]
|
|
437
471
|
# Fast path: no transforms at all (covers 95%+ of rules)
|
|
438
472
|
# transform defaults to {} which is truthy but semantically empty
|
|
@@ -453,7 +487,8 @@ module Lutaml
|
|
|
453
487
|
assign_value(model, value)
|
|
454
488
|
else
|
|
455
489
|
# Hash/proc transformers need ImportTransformer
|
|
456
|
-
transformed = ImportTransformer.call(value, self, attr
|
|
490
|
+
transformed = ImportTransformer.call(value, self, attr,
|
|
491
|
+
context: context)
|
|
457
492
|
assign_value(model, transformed)
|
|
458
493
|
end
|
|
459
494
|
true
|
|
@@ -5,11 +5,12 @@ module Lutaml
|
|
|
5
5
|
class Sequence
|
|
6
6
|
include DeepDupable
|
|
7
7
|
|
|
8
|
-
attr_accessor :model, :attributes
|
|
8
|
+
attr_accessor :model, :attributes, :compositors
|
|
9
9
|
attr_reader :format
|
|
10
10
|
|
|
11
11
|
def initialize(model, format: nil)
|
|
12
12
|
@attributes = []
|
|
13
|
+
@compositors = []
|
|
13
14
|
@model = model
|
|
14
15
|
@format = format
|
|
15
16
|
end
|
|
@@ -19,6 +20,7 @@ module Lutaml
|
|
|
19
20
|
def deep_dup(new_model = nil)
|
|
20
21
|
dup_seq = Sequence.new(new_model || @model, format: @format)
|
|
21
22
|
dup_seq.attributes = Utils.deep_dup(@attributes)
|
|
23
|
+
dup_seq.compositors = Utils.deep_dup(@compositors)
|
|
22
24
|
dup_seq
|
|
23
25
|
end
|
|
24
26
|
|
|
@@ -32,7 +34,50 @@ module Lutaml
|
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def map_element(name, **options)
|
|
35
|
-
|
|
37
|
+
rule = @model.map_element(name, **options)
|
|
38
|
+
@attributes << rule
|
|
39
|
+
if (nested = @choice_in_progress)
|
|
40
|
+
nested.attributes << rule
|
|
41
|
+
end
|
|
42
|
+
rule
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# An xs:choice compositor nested inside an xs:sequence
|
|
46
|
+
# (lutaml-model#687). Element rules mapped in the block take
|
|
47
|
+
# their positional place in this sequence AND carry the choice
|
|
48
|
+
# on their model attributes, so validation applies order and
|
|
49
|
+
# exclusivity exactly as a model-level choice does. The choice
|
|
50
|
+
# itself is remembered in #compositors — the compositor shape
|
|
51
|
+
# stays queryable next to the flat rule order.
|
|
52
|
+
def choice(min: 1, max: 1, &block)
|
|
53
|
+
nested = Choice.new(@model, min, max, format: @format)
|
|
54
|
+
previous = @choice_in_progress
|
|
55
|
+
@choice_in_progress = nested
|
|
56
|
+
begin
|
|
57
|
+
instance_eval(&block)
|
|
58
|
+
ensure
|
|
59
|
+
@choice_in_progress = previous
|
|
60
|
+
end
|
|
61
|
+
@compositors << nested
|
|
62
|
+
nested
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Bind nested choice compositors to the mapper's model attributes
|
|
66
|
+
# (options[:choice]), so validation applies order and exclusivity
|
|
67
|
+
# the same way a model-level choice does. Runs at mapping finalize,
|
|
68
|
+
# when the mapper class is known — the DSL block runs earlier.
|
|
69
|
+
def bind_choice_compositors!(mapper_class)
|
|
70
|
+
@compositors.each do |choice|
|
|
71
|
+
choice.model = mapper_class
|
|
72
|
+
# The choice's contents become the model attributes its rules
|
|
73
|
+
# target — the same shape a model-level choice carries — so
|
|
74
|
+
# validation machinery works unchanged.
|
|
75
|
+
bound = choice.attributes.filter_map do |rule|
|
|
76
|
+
mapper_class.attributes[rule.to]
|
|
77
|
+
end.uniq
|
|
78
|
+
choice.attributes.replace(bound)
|
|
79
|
+
bound.each { |target| target.options[:choice] = choice }
|
|
80
|
+
end
|
|
36
81
|
end
|
|
37
82
|
|
|
38
83
|
def import_model_mappings(model, register = nil)
|