lutaml-model 0.8.36 → 0.8.37
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/lib/lutaml/model/adapter_resolver.rb +14 -2
- data/lib/lutaml/model/attribute.rb +10 -8
- data/lib/lutaml/model/choice.rb +6 -5
- data/lib/lutaml/model/liquefiable.rb +23 -2
- data/lib/lutaml/model/mapping/mapping_rule.rb +7 -0
- data/lib/lutaml/model/sequence.rb +47 -2
- data/lib/lutaml/model/serialize/attribute_definition.rb +54 -0
- data/lib/lutaml/model/serialize.rb +14 -9
- data/lib/lutaml/model/store.rb +70 -32
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/mapping.rb +53 -0
- data/lib/lutaml/xml/model_transform.rb +59 -42
- data/lib/lutaml/xml/parse_session.rb +25 -0
- data/spec/lutaml/model/adapter_resolver_configured_spec.rb +38 -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
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ec31a25915a5fd6ceeb31c2e61456c90ae223e565eab14051f8ddb40e373fe9e
|
|
4
|
+
data.tar.gz: a2e6027e8684bfcc63e58650f979d880e43836ea5521f0ee0ba5e30205bd89cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a101732ad5730070d1c7629b9bf0e72e6573d01a716cf165846b8474f5b7dca7e2415d80ed7a1f8d526fea61363ecbba414e9ecb819f8f2b42b9f09636e81d03
|
|
7
|
+
data.tar.gz: 42e60f78f63d5d8033edba244858479c95dc1147cf9cc2698ab410d259bd8485a2d2df9825482dc3f895d673177212c1e686a143b5afdde2850d78a706241170
|
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
|
|
@@ -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.
|
|
@@ -1029,6 +1029,16 @@ instance_object = nil)
|
|
|
1029
1029
|
@default_type_context = nil
|
|
1030
1030
|
end
|
|
1031
1031
|
|
|
1032
|
+
# Public validation contract: mappings check their targets at
|
|
1033
|
+
# finalize (lutaml-model#296), the cast path checks per value.
|
|
1034
|
+
def validate_attr_type!(resolved_type)
|
|
1035
|
+
return true if resolved_type <= Serializable || resolved_type <= Type::Value
|
|
1036
|
+
return true if resolved_type.include?(Serialize)
|
|
1037
|
+
|
|
1038
|
+
raise Lutaml::Model::InvalidAttributeTypeError.new(name,
|
|
1039
|
+
resolved_type.name)
|
|
1040
|
+
end
|
|
1041
|
+
|
|
1032
1042
|
private
|
|
1033
1043
|
|
|
1034
1044
|
attr_writer :raw, :validations
|
|
@@ -1040,14 +1050,6 @@ instance_object = nil)
|
|
|
1040
1050
|
match&.last
|
|
1041
1051
|
end
|
|
1042
1052
|
|
|
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
1053
|
# validated_range_object is provided by CollectionHandler module
|
|
1052
1054
|
|
|
1053
1055
|
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
|
|
|
@@ -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:,
|
|
@@ -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)
|
|
@@ -12,6 +12,59 @@ module Lutaml
|
|
|
12
12
|
#
|
|
13
13
|
# @param attr [Attribute] The attribute to define methods for
|
|
14
14
|
# @param register [Symbol, nil] The register for type resolution
|
|
15
|
+
# Compile (per class, cached) the method that seeds every attribute
|
|
16
|
+
# with its "no data arrived" sentinel: collections share the frozen
|
|
17
|
+
# LAZY_EMPTY_COLLECTION, scalars share the UninitializedClass
|
|
18
|
+
# singleton. Compiled once instead of walked per instance — the
|
|
19
|
+
# grammars-compile-don't-interpret rule applied to object state.
|
|
20
|
+
# Compile (per class, on demand) the method that seeds every
|
|
21
|
+
# attribute with its "no data arrived" sentinel: collections share
|
|
22
|
+
# the frozen LAZY_EMPTY_COLLECTION, scalars share the
|
|
23
|
+
# UninitializedClass singleton. Compiled once instead of walked
|
|
24
|
+
# per instance — grammars compile, they don't interpret.
|
|
25
|
+
def compile_state_defaults!(_register = nil)
|
|
26
|
+
attrs = attributes
|
|
27
|
+
|
|
28
|
+
if attrs.empty?
|
|
29
|
+
define_method(:__init_deserialized_state_defaults) do
|
|
30
|
+
# no attributes to seed
|
|
31
|
+
end
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
lines = attrs.map do |name, attr|
|
|
36
|
+
if attr.collection?
|
|
37
|
+
"@#{name} = Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION"
|
|
38
|
+
else
|
|
39
|
+
"@#{name} = Lutaml::Model::UninitializedClass.instance"
|
|
40
|
+
end
|
|
41
|
+
end.join("\n")
|
|
42
|
+
|
|
43
|
+
# class_eval interpolates per-attribute `@name = <sentinel>` lines:
|
|
44
|
+
# def __init_deserialized_state_defaults
|
|
45
|
+
# @id = Lutaml::Model::UninitializedClass.instance
|
|
46
|
+
# @items = Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION
|
|
47
|
+
# end
|
|
48
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
49
|
+
def __init_deserialized_state_defaults
|
|
50
|
+
#{lines}
|
|
51
|
+
end
|
|
52
|
+
RUBY
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def invalidate_state_defaults!
|
|
56
|
+
# Opal's method_defined? takes no inherit flag (see the
|
|
57
|
+
# setter_defined check in define_regular_attribute_methods).
|
|
58
|
+
defined_now = if Lutaml::Model.opal?
|
|
59
|
+
method_defined?(:__init_deserialized_state_defaults)
|
|
60
|
+
else
|
|
61
|
+
method_defined?(:__init_deserialized_state_defaults, false)
|
|
62
|
+
end
|
|
63
|
+
return unless defined_now
|
|
64
|
+
|
|
65
|
+
remove_method(:__init_deserialized_state_defaults)
|
|
66
|
+
end
|
|
67
|
+
|
|
15
68
|
def define_attribute_methods(attr, register = nil)
|
|
16
69
|
name = attr.name
|
|
17
70
|
register_id = extract_register_id(register)
|
|
@@ -200,6 +253,7 @@ module Lutaml
|
|
|
200
253
|
attr = Attribute.new(name, type, options)
|
|
201
254
|
@attributes[name] = attr
|
|
202
255
|
@merged_attributes_cache = nil
|
|
256
|
+
invalidate_state_defaults!
|
|
203
257
|
define_attribute_methods(attr)
|
|
204
258
|
|
|
205
259
|
attr
|
|
@@ -108,15 +108,20 @@ module Lutaml
|
|
|
108
108
|
@using_default = nil
|
|
109
109
|
@lutaml_register = register
|
|
110
110
|
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
111
|
+
# Default register: seed through the per-class compiled method
|
|
112
|
+
# (plain `@x = sentinel` writes). The previous per-instance
|
|
113
|
+
# instance_variable_set loop was a measured hot spot — one send
|
|
114
|
+
# and one symbol allocation per attribute per instance. Register-
|
|
115
|
+
# specific attribute sets are rare and keep the generic walk.
|
|
116
|
+
if register.nil? || register == :default
|
|
117
|
+
klass = self.class
|
|
118
|
+
klass.compile_state_defaults! unless klass.method_defined?(:__init_deserialized_state_defaults)
|
|
119
|
+
__init_deserialized_state_defaults
|
|
120
|
+
else
|
|
121
|
+
self.class.attributes(register).each do |name, attr|
|
|
122
|
+
instance_variable_set(:"@#{name}",
|
|
123
|
+
attr.collection? ? LAZY_EMPTY_COLLECTION : Lutaml::Model::UninitializedClass.instance)
|
|
124
|
+
end
|
|
120
125
|
end
|
|
121
126
|
end
|
|
122
127
|
|
data/lib/lutaml/model/store.rb
CHANGED
|
@@ -6,15 +6,28 @@ require "weakref" unless Lutaml::Model.opal?
|
|
|
6
6
|
module Lutaml
|
|
7
7
|
module Model
|
|
8
8
|
class Store
|
|
9
|
-
# Compact dead
|
|
9
|
+
# Compact dead index entries once a class bucket grows past this size.
|
|
10
10
|
COMPACTION_THRESHOLD = 1000
|
|
11
11
|
|
|
12
12
|
# Once the threshold is exceeded, only compact every Nth subsequent
|
|
13
|
-
# register call. Amortises the O(N)
|
|
13
|
+
# register call. Amortises the O(N) prune over N inserts so
|
|
14
14
|
# register stays O(1) per call rather than O(N) per call (O(N^2)
|
|
15
15
|
# cumulatively for the class).
|
|
16
16
|
COMPACTION_INTERVAL = 1000
|
|
17
17
|
|
|
18
|
+
WeakBucket = if Lutaml::Model.opal?
|
|
19
|
+
# Opal has neither WeakRef nor ObjectSpace::WeakMap; documents in
|
|
20
|
+
# the browser are small, so strong references are the fallback.
|
|
21
|
+
::Hash
|
|
22
|
+
else
|
|
23
|
+
# CRuby: WeakMap holds keys weakly with zero Ruby-level
|
|
24
|
+
# allocation per insert and no per-object finalizer — the WeakRef
|
|
25
|
+
# approach allocated one finalizer-registering object per
|
|
26
|
+
# instance (lutaml-model#695) and dominated GC cycles on
|
|
27
|
+
# instance-heavy parses.
|
|
28
|
+
::ObjectSpace::WeakMap
|
|
29
|
+
end
|
|
30
|
+
|
|
18
31
|
class << self
|
|
19
32
|
def instance
|
|
20
33
|
@instance ||= new
|
|
@@ -42,8 +55,10 @@ module Lutaml
|
|
|
42
55
|
end
|
|
43
56
|
|
|
44
57
|
def initialize
|
|
45
|
-
|
|
46
|
-
#
|
|
58
|
+
# WeakMap-based buckets: value presence marks liveness, no
|
|
59
|
+
# per-instance allocation (lutaml-model#695).
|
|
60
|
+
@store = ::Hash.new { |hash, key| hash[key] = WeakBucket.new }
|
|
61
|
+
# Nested index: { model_key => { reference_key => { value => object } } }
|
|
47
62
|
# Grouped by model_key so register only iterates this class's own indices.
|
|
48
63
|
@index = {}
|
|
49
64
|
@inserts_since_compaction = ::Hash.new(0)
|
|
@@ -52,11 +67,10 @@ module Lutaml
|
|
|
52
67
|
|
|
53
68
|
def register(object)
|
|
54
69
|
model_key = object.class.to_s
|
|
55
|
-
|
|
56
|
-
refs << WeakRef.new(object)
|
|
70
|
+
@store[model_key][object] = true
|
|
57
71
|
@inserts_since_compaction[model_key] += 1
|
|
58
72
|
|
|
59
|
-
compact_if_needed(
|
|
73
|
+
compact_if_needed(model_key)
|
|
60
74
|
|
|
61
75
|
update_existing_indices(object, model_key)
|
|
62
76
|
end
|
|
@@ -78,25 +92,26 @@ module Lutaml
|
|
|
78
92
|
obj
|
|
79
93
|
end
|
|
80
94
|
|
|
95
|
+
def live_objects(model_key)
|
|
96
|
+
bucket = @store[model_key]
|
|
97
|
+
return [] unless bucket
|
|
98
|
+
|
|
99
|
+
live_objects_for(bucket)
|
|
100
|
+
end
|
|
101
|
+
|
|
81
102
|
def clear
|
|
82
|
-
@store = ::Hash.new { |hash, key| hash[key] =
|
|
103
|
+
@store = ::Hash.new { |hash, key| hash[key] = WeakBucket.new }
|
|
83
104
|
@index = {}
|
|
84
105
|
@inserts_since_compaction = ::Hash.new(0)
|
|
85
106
|
@compaction_count = 0
|
|
86
107
|
end
|
|
87
108
|
|
|
88
109
|
def store
|
|
89
|
-
@store.transform_values
|
|
90
|
-
refs.each_with_object([]) do |ref, alive|
|
|
91
|
-
alive << ref.__getobj__ if ref.weakref_alive?
|
|
92
|
-
rescue WeakRef::RefError
|
|
93
|
-
nil
|
|
94
|
-
end
|
|
95
|
-
end
|
|
110
|
+
@store.transform_values { |refs| live_objects_for(refs) }
|
|
96
111
|
end
|
|
97
112
|
|
|
98
113
|
def refs_for(model_key)
|
|
99
|
-
|
|
114
|
+
LiveView.new(self, model_key)
|
|
100
115
|
end
|
|
101
116
|
|
|
102
117
|
def inserts_since_compaction
|
|
@@ -117,15 +132,15 @@ module Lutaml
|
|
|
117
132
|
@index[model_key] ||= {}
|
|
118
133
|
end
|
|
119
134
|
|
|
120
|
-
# Build index for a (model_class, reference_key) pair by scanning
|
|
135
|
+
# Build index for a (model_class, reference_key) pair by scanning
|
|
136
|
+
# live instances. Index values hold WeakRefs: the index must not
|
|
137
|
+
# pin objects (only the rare classes with resolved reference keys
|
|
138
|
+
# pay this; the per-instance bucket stays allocation-free WeakMap).
|
|
121
139
|
def build_index(model_indices, model_key, reference_key)
|
|
122
140
|
entries = model_indices[reference_key] = {}
|
|
123
|
-
|
|
124
|
-
obj = ref.__getobj__
|
|
141
|
+
each_live(model_key) do |obj|
|
|
125
142
|
value = obj.public_send(reference_key)
|
|
126
143
|
entries[value] = WeakRef.new(obj) if value
|
|
127
|
-
rescue WeakRef::RefError
|
|
128
|
-
next
|
|
129
144
|
end
|
|
130
145
|
end
|
|
131
146
|
|
|
@@ -136,11 +151,9 @@ module Lutaml
|
|
|
136
151
|
model_indices = @index[model_key]
|
|
137
152
|
return unless model_indices
|
|
138
153
|
|
|
139
|
-
model_indices.
|
|
154
|
+
model_indices.each_key do |reference_key|
|
|
140
155
|
value = object.public_send(reference_key)
|
|
141
|
-
|
|
142
|
-
rescue WeakRef::RefError
|
|
143
|
-
next
|
|
156
|
+
model_indices[reference_key][value] = WeakRef.new(object) if value
|
|
144
157
|
end
|
|
145
158
|
end
|
|
146
159
|
|
|
@@ -150,17 +163,11 @@ module Lutaml
|
|
|
150
163
|
nil
|
|
151
164
|
end
|
|
152
165
|
|
|
153
|
-
def compact_if_needed(
|
|
154
|
-
return unless refs.size > COMPACTION_THRESHOLD
|
|
166
|
+
def compact_if_needed(model_key)
|
|
155
167
|
return unless @inserts_since_compaction[model_key] >= COMPACTION_INTERVAL
|
|
156
168
|
|
|
157
169
|
@inserts_since_compaction[model_key] = 0
|
|
158
170
|
@compaction_count += 1
|
|
159
|
-
refs.reject! do |ref|
|
|
160
|
-
!ref.weakref_alive?
|
|
161
|
-
rescue WeakRef::RefError
|
|
162
|
-
true
|
|
163
|
-
end
|
|
164
171
|
prune_index(model_key)
|
|
165
172
|
end
|
|
166
173
|
|
|
@@ -177,6 +184,37 @@ module Lutaml
|
|
|
177
184
|
entries.empty?
|
|
178
185
|
end
|
|
179
186
|
end
|
|
187
|
+
|
|
188
|
+
def each_live(model_key, &block)
|
|
189
|
+
@store[model_key]&.each_key(&block)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def live_objects_for(bucket)
|
|
193
|
+
# WeakMap#each_key requires a block on CRuby; WeakMap#each
|
|
194
|
+
# yields key/value pairs, and Hash buckets enumerate the same
|
|
195
|
+
# way (value is `true` for WeakMap, the object for the Opal
|
|
196
|
+
# fallback keyed by object identity).
|
|
197
|
+
bucket.map { |key, _value| key }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Array-shaped live view over a class's WeakMap bucket, for
|
|
201
|
+
# specs/debugging (refs_for used to expose the raw array).
|
|
202
|
+
class LiveView
|
|
203
|
+
include Enumerable
|
|
204
|
+
|
|
205
|
+
def initialize(store, model_key)
|
|
206
|
+
@store = store
|
|
207
|
+
@model_key = model_key
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def each(&block)
|
|
211
|
+
@store.live_objects(@model_key).each(&block)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def size
|
|
215
|
+
@store.live_objects(@model_key).size
|
|
216
|
+
end
|
|
217
|
+
end
|
|
180
218
|
end
|
|
181
219
|
end
|
|
182
220
|
end
|
data/lib/lutaml/model/version.rb
CHANGED
data/lib/lutaml/xml/mapping.rb
CHANGED
|
@@ -84,6 +84,7 @@ module Lutaml
|
|
|
84
84
|
@cached_elements = {}
|
|
85
85
|
@cached_attributes = {}
|
|
86
86
|
@cached_mappings = {}
|
|
87
|
+
@cached_attribute_name_counts = {}
|
|
87
88
|
@finalized = false
|
|
88
89
|
end
|
|
89
90
|
|
|
@@ -98,6 +99,17 @@ module Lutaml
|
|
|
98
99
|
# Resolve any deferred mapping imports before finalizing
|
|
99
100
|
ensure_mappings_imported!
|
|
100
101
|
|
|
102
|
+
# lutaml-model#687: nested sequence choices tag their target
|
|
103
|
+
# model attributes now that the mapper class is known.
|
|
104
|
+
@element_sequence.each do |seq|
|
|
105
|
+
seq.bind_choice_compositors!(mapper_class)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# lutaml-model#296: a mapped rule whose attribute type can never
|
|
109
|
+
# accept the serialization shape fails here, with the attribute
|
|
110
|
+
# and type named, instead of surfacing mid-parse as a cast oddity.
|
|
111
|
+
validate_mapped_attribute_types!(mapper_class)
|
|
112
|
+
|
|
101
113
|
# Validate mixed content requires collection attribute for content mapping
|
|
102
114
|
validate_mixed_content_collection!(mapper_class)
|
|
103
115
|
|
|
@@ -108,6 +120,7 @@ module Lutaml
|
|
|
108
120
|
@cached_elements.clear
|
|
109
121
|
@cached_attributes.clear
|
|
110
122
|
@cached_mappings.clear
|
|
123
|
+
@cached_attribute_name_counts.clear
|
|
111
124
|
@finalized = true
|
|
112
125
|
end
|
|
113
126
|
|
|
@@ -1028,6 +1041,26 @@ module Lutaml
|
|
|
1028
1041
|
end
|
|
1029
1042
|
end
|
|
1030
1043
|
|
|
1044
|
+
# Fire InvalidAttributeTypeError at definition time for mapped
|
|
1045
|
+
# rules whose target attribute type is neither a Type::Value nor
|
|
1046
|
+
# Serializable (lutaml-model#296). Undeclared types are left to
|
|
1047
|
+
# deferred-import resolution.
|
|
1048
|
+
def validate_mapped_attribute_types!(mapper_class)
|
|
1049
|
+
(elements + attributes).each do |rule|
|
|
1050
|
+
target = mapper_class.attributes[rule.to]
|
|
1051
|
+
next unless target
|
|
1052
|
+
|
|
1053
|
+
begin
|
|
1054
|
+
resolved = target.type(Lutaml::Model::Config.default_register)
|
|
1055
|
+
rescue Lutaml::Model::UnknownTypeError
|
|
1056
|
+
next
|
|
1057
|
+
end
|
|
1058
|
+
next unless resolved.is_a?(Class)
|
|
1059
|
+
|
|
1060
|
+
target.validate_attr_type!(resolved)
|
|
1061
|
+
end
|
|
1062
|
+
end
|
|
1063
|
+
|
|
1031
1064
|
def elements(register_id = nil)
|
|
1032
1065
|
reg_key = register_id || :default
|
|
1033
1066
|
cached = @cached_elements[reg_key]
|
|
@@ -1060,6 +1093,26 @@ module Lutaml
|
|
|
1060
1093
|
@raw_mapping
|
|
1061
1094
|
end
|
|
1062
1095
|
|
|
1096
|
+
# lutaml-model#790/#791: whether `rule` is the only attribute rule
|
|
1097
|
+
# claiming its exact name spelling. The answer is a property of the
|
|
1098
|
+
# mapping, not the document, so it is counted once per register
|
|
1099
|
+
# (rules sharing a spelling make each other non-sole).
|
|
1100
|
+
def sole_local_attribute_claimant?(rule, register_id = nil)
|
|
1101
|
+
reg_key = register_id || :default
|
|
1102
|
+
counts = @cached_attribute_name_counts[reg_key]
|
|
1103
|
+
unless counts
|
|
1104
|
+
counts = {}
|
|
1105
|
+
attributes(register_id).each do |r|
|
|
1106
|
+
key = r.name_strings
|
|
1107
|
+
counts[key] = (counts[key] || 0) + 1
|
|
1108
|
+
end
|
|
1109
|
+
counts.freeze if @finalized
|
|
1110
|
+
@cached_attribute_name_counts[reg_key] = counts
|
|
1111
|
+
end
|
|
1112
|
+
|
|
1113
|
+
(counts[rule.name_strings] || 1) == 1
|
|
1114
|
+
end
|
|
1115
|
+
|
|
1063
1116
|
def mappings(register_id = nil)
|
|
1064
1117
|
reg_key = register_id || :default
|
|
1065
1118
|
cached = @cached_mappings[reg_key]
|
|
@@ -12,6 +12,15 @@ module Lutaml
|
|
|
12
12
|
# - Used by model's serialization pipeline via Transform.for(:xml)
|
|
13
13
|
#
|
|
14
14
|
class ModelTransform < ::Lutaml::Model::Transform
|
|
15
|
+
# Namespaced rule name -> [local_name, rule_uri]. Pure string
|
|
16
|
+
# splitting, deterministic per spelling, shared across parses.
|
|
17
|
+
# Concurrent::Map under threaded MRI, plain Hash under Opal.
|
|
18
|
+
NAMESPACED_NAME_PARTS = if Lutaml::Model.opal?
|
|
19
|
+
{}
|
|
20
|
+
else
|
|
21
|
+
Lutaml::Model::RuntimeCompatibility.require_native("concurrent")
|
|
22
|
+
Concurrent::Map.new
|
|
23
|
+
end
|
|
15
24
|
include NestedCollectionAttribute
|
|
16
25
|
|
|
17
26
|
# Performance: Frozen empty hash to reduce allocations
|
|
@@ -487,16 +496,11 @@ _effective_register)
|
|
|
487
496
|
mapping = klass.mappings_for(:xml, effective_register)
|
|
488
497
|
return true unless mapping
|
|
489
498
|
|
|
490
|
-
|
|
491
|
-
mapping.mappings.none? do |sib|
|
|
492
|
-
next false unless sib.attribute? && !sib.equal?(rule)
|
|
493
|
-
|
|
494
|
-
Array(sib.name).map(&:to_s) == expected
|
|
495
|
-
end
|
|
499
|
+
mapping.sole_local_attribute_claimant?(rule, effective_register)
|
|
496
500
|
end
|
|
497
501
|
|
|
498
502
|
def value_for_xml_attribute(doc, rule, rule_names,
|
|
499
|
-
flexible_local: false)
|
|
503
|
+
flexible_local: false, session: nil)
|
|
500
504
|
# For attributes, rule_names may contain namespaced names, but find_attribute_value
|
|
501
505
|
# expects prefix:name or just name, so we need to convert namespaced names to prefix format.
|
|
502
506
|
#
|
|
@@ -524,7 +528,8 @@ _effective_register)
|
|
|
524
528
|
# (e.g., v:ext without xmlns:v="...").
|
|
525
529
|
if value.nil? && rule_names.any? { |rn| rn.include?(":") }
|
|
526
530
|
value = find_attribute_by_local_name(doc, rule_names,
|
|
527
|
-
flexible_local: flexible_local
|
|
531
|
+
flexible_local: flexible_local,
|
|
532
|
+
session: session)
|
|
528
533
|
end
|
|
529
534
|
|
|
530
535
|
value = value&.split(rule.delimiter) if rule.delimiter
|
|
@@ -588,49 +593,61 @@ _effective_register)
|
|
|
588
593
|
# @param rule_names [Array<String>] the rule names to match
|
|
589
594
|
# @return [String, nil] the attribute value or nil
|
|
590
595
|
def find_attribute_by_local_name(doc, rule_names,
|
|
591
|
-
flexible_local: false)
|
|
596
|
+
flexible_local: false, session: nil)
|
|
597
|
+
root_index = session&.root_local_attribute_index
|
|
598
|
+
|
|
592
599
|
rule_names.each do |rn|
|
|
593
600
|
next unless rn.include?(":")
|
|
594
601
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
# - an attribute whose resolved namespace IS one of the rule's
|
|
604
|
-
# namespace URIs (prefix rebinding, lutaml-model#744).
|
|
605
|
-
# Everything else — including a definitively namespace-less
|
|
606
|
-
# attribute and an attribute from a foreign namespace — must
|
|
607
|
-
# not satisfy the fallback, or a plain rule steals a
|
|
608
|
-
# namespaced sibling attribute (lutaml-model#758). An
|
|
609
|
-
# XmlAttribute with a resolved namespace has namespaced_name
|
|
610
|
-
# "URI:local", so this comparison is allocation-free.
|
|
611
|
-
if flexible_local
|
|
612
|
-
# Sole claimant: any qualification of the local name binds
|
|
613
|
-
# (element namespace, foreign extension namespace, none).
|
|
614
|
-
attr.unprefixed_name == local_name ||
|
|
615
|
-
((colon = attr.name.rindex(":")) ? attr.name[(colon + 1)..] : attr.name) == local_name
|
|
616
|
-
elsif attr.namespace.nil?
|
|
617
|
-
next false if attr.namespace_prefix.nil? &&
|
|
618
|
-
!attr.name.include?(":")
|
|
619
|
-
|
|
620
|
-
attr.unprefixed_name == local_name ||
|
|
621
|
-
((colon = attr.name.rindex(":")) ? attr.name[(colon + 1)..] : attr.name) == local_name
|
|
622
|
-
else
|
|
623
|
-
# Prefixed attributes carry namespaced_name "prefix:local",
|
|
624
|
-
# which a URI-form rule name can never equal — match by the
|
|
625
|
-
# attribute's resolved namespace instead.
|
|
626
|
-
attr.namespace == rule_uri && attr.unprefixed_name == local_name
|
|
627
|
-
end
|
|
602
|
+
# Splitting a namespaced rule name is pure string work repeated
|
|
603
|
+
# per element; memoized per spelling (bounded by the model's
|
|
604
|
+
# distinct rule names).
|
|
605
|
+
parts = NAMESPACED_NAME_PARTS[rn]
|
|
606
|
+
unless parts
|
|
607
|
+
last_colon_index = rn.rindex(":")
|
|
608
|
+
parts = [rn[(last_colon_index + 1)..], rn[0...last_colon_index]]
|
|
609
|
+
NAMESPACED_NAME_PARTS[rn] = parts
|
|
628
610
|
end
|
|
611
|
+
local_name, rule_uri = parts
|
|
612
|
+
|
|
613
|
+
candidates = root_index&.[](local_name)
|
|
614
|
+
matched_attr = if candidates
|
|
615
|
+
candidates.find do |attr|
|
|
616
|
+
local_name_match?(attr, local_name, rule_uri,
|
|
617
|
+
flexible_local)
|
|
618
|
+
end
|
|
619
|
+
else
|
|
620
|
+
doc.root.attributes.each_value.find do |attr|
|
|
621
|
+
local_name_match?(attr, local_name, rule_uri,
|
|
622
|
+
flexible_local)
|
|
623
|
+
end
|
|
624
|
+
end
|
|
629
625
|
return matched_attr&.value if matched_attr
|
|
630
626
|
end
|
|
631
627
|
nil
|
|
632
628
|
end
|
|
633
629
|
|
|
630
|
+
# Namespace discipline of the lenient local-name fallback
|
|
631
|
+
# (#744/#758/#790): sole claimants bind any qualification; shared
|
|
632
|
+
# names must resolve to the rule's namespace.
|
|
633
|
+
def local_name_match?(attr, local_name, rule_uri, flexible_local)
|
|
634
|
+
if flexible_local
|
|
635
|
+
attr.unprefixed_name == local_name ||
|
|
636
|
+
attr_name_local(attr) == local_name
|
|
637
|
+
elsif attr.namespace.nil?
|
|
638
|
+
return false if attr.namespace_prefix.nil? && !attr.name.include?(":")
|
|
639
|
+
|
|
640
|
+
attr.unprefixed_name == local_name ||
|
|
641
|
+
attr_name_local(attr) == local_name
|
|
642
|
+
else
|
|
643
|
+
attr.namespace == rule_uri && attr.unprefixed_name == local_name
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def attr_name_local(attr)
|
|
648
|
+
(colon = attr.name.rindex(":")) ? attr.name[(colon + 1)..] : attr.name
|
|
649
|
+
end
|
|
650
|
+
|
|
634
651
|
def value_for_rule(session, rule, options, cached_attr = nil,
|
|
635
652
|
extra_rule_names = nil)
|
|
636
653
|
doc = session.doc
|
|
@@ -24,6 +24,25 @@ module Lutaml
|
|
|
24
24
|
@instance_is_serialize ||= instance.is_a?(::Lutaml::Model::Serialize)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Local-name -> [attributes] index over the root element's
|
|
28
|
+
# attributes, built on first lenient lookup (TODO.max-perf/08).
|
|
29
|
+
# The fallback used to rescan every attribute per missed rule;
|
|
30
|
+
# elements are parse-frozen, so the index builds at most once per
|
|
31
|
+
# session. Each attribute is filed under every local-name spelling
|
|
32
|
+
# the matcher compares (unprefixed name and colon-split local).
|
|
33
|
+
def root_local_attribute_index
|
|
34
|
+
@root_local_attribute_index ||= begin
|
|
35
|
+
index = ::Hash.new { |h, k| h[k] = [] }
|
|
36
|
+
doc.root.attributes.each_value do |attr|
|
|
37
|
+
local_names(attr).each do |key|
|
|
38
|
+
entries = index[key]
|
|
39
|
+
entries << attr unless entries.include?(attr)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
index
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
27
46
|
def model_class
|
|
28
47
|
@model_class ||= instance.class
|
|
29
48
|
end
|
|
@@ -57,3 +76,9 @@ module Lutaml
|
|
|
57
76
|
end
|
|
58
77
|
end
|
|
59
78
|
end
|
|
79
|
+
|
|
80
|
+
def local_names(attr)
|
|
81
|
+
colon = attr.name.rindex(":")
|
|
82
|
+
split_local = colon ? attr.name[(colon + 1)..] : attr.name
|
|
83
|
+
[attr.unprefixed_name, split_local].compact.uniq
|
|
84
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Lutaml::Model::AdapterResolver, ".adapter_for with a configured type" do
|
|
6
|
+
after do
|
|
7
|
+
Lutaml::Model::Config.configure do |config|
|
|
8
|
+
config.xml_adapter_type = :nokogiri
|
|
9
|
+
config.yaml_adapter_type = :standard
|
|
10
|
+
config.json_adapter_type = :standard
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "resolves the configured adapter without running auto-detection" do
|
|
15
|
+
Lutaml::Model::Config.configure do |config|
|
|
16
|
+
config.json_adapter_type = :standard
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
adapter = described_class.adapter_for(:json)
|
|
20
|
+
|
|
21
|
+
expect(described_class.configured_type(:json)).to eq(:standard)
|
|
22
|
+
expect(adapter.name).to include("Json::Standard")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "does not load the optional yeptris engine when a standard adapter is configured" do
|
|
26
|
+
skip "yeptris already loaded by an earlier spec" if Object.const_defined?(:Yeptris)
|
|
27
|
+
|
|
28
|
+
Lutaml::Model::Config.configure do |config|
|
|
29
|
+
config.yaml_adapter_type = :standard
|
|
30
|
+
config.json_adapter_type = :standard
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
described_class.adapter_for(:yaml)
|
|
34
|
+
described_class.adapter_for(:json)
|
|
35
|
+
|
|
36
|
+
expect(Object.const_defined?(:Yeptris)).to be(false)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# lutaml-model#800: `liquid do ... end` blocks survive a boot order in
|
|
6
|
+
# which Liquid is required after lutaml-model; the drop class
|
|
7
|
+
# materializes on first use instead of requiring Liquid at definition.
|
|
8
|
+
RSpec.describe "Liquefiable lazy registration" do
|
|
9
|
+
it "materializes the drop class on first to_liquid" do
|
|
10
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
11
|
+
attribute :name, :string
|
|
12
|
+
|
|
13
|
+
liquid do
|
|
14
|
+
map "full_name", to: :name
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Mapping stored at definition; drop registration is lazy
|
|
19
|
+
expect(klass.liquid_mappings.mappings).not_to be_empty
|
|
20
|
+
|
|
21
|
+
drop = klass.new(name: "Apollo").to_liquid
|
|
22
|
+
expect(drop).to be_a(Liquid::Drop)
|
|
23
|
+
expect(drop.full_name).to eq("Apollo")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "keeps ensure_liquid_registered! idempotent" do
|
|
27
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
28
|
+
attribute :name, :string
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
klass.ensure_liquid_registered!
|
|
32
|
+
first = klass.base_drop_class
|
|
33
|
+
klass.ensure_liquid_registered!
|
|
34
|
+
expect(klass.base_drop_class).to equal(first)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# lutaml-model#296: a mapped rule whose attribute type can never accept
|
|
6
|
+
# the serialization shape fails at mapping definition, naming the
|
|
7
|
+
# attribute and the type, instead of surfacing mid-parse.
|
|
8
|
+
RSpec.describe "Mapping-time attribute type validation" do
|
|
9
|
+
it "raises InvalidAttributeTypeError at definition for a non-castable type" do
|
|
10
|
+
expect do
|
|
11
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
12
|
+
attribute :ends, Object
|
|
13
|
+
|
|
14
|
+
xml do
|
|
15
|
+
element "doc"
|
|
16
|
+
map_element "ends", to: :ends
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end.to raise_error(Lutaml::Model::InvalidAttributeTypeError) do |err|
|
|
20
|
+
expect(err.message).to include("attribute `ends`")
|
|
21
|
+
expect(err.message).to include("Object")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "accepts Value, Serializable, and collection types" do
|
|
26
|
+
item = Class.new(Lutaml::Model::Serializable) do
|
|
27
|
+
attribute :name, :string
|
|
28
|
+
xml do
|
|
29
|
+
element "item"
|
|
30
|
+
map_element "name", to: :name
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
expect do
|
|
35
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
36
|
+
attribute :count, :integer
|
|
37
|
+
attribute :name, :string
|
|
38
|
+
attribute :items, item, collection: true
|
|
39
|
+
|
|
40
|
+
xml do
|
|
41
|
+
element "doc"
|
|
42
|
+
map_element "count", to: :count
|
|
43
|
+
map_element "items", to: :items
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end.not_to raise_error
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "leaves undeclared (deferred) types to import resolution" do
|
|
50
|
+
expect do
|
|
51
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
52
|
+
attribute :later, :missing_type_name
|
|
53
|
+
|
|
54
|
+
xml do
|
|
55
|
+
element "doc"
|
|
56
|
+
map_element "later", to: :later
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end.not_to raise_error
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# lutaml-model#687: an xs:sequence remembers a nested xs:choice
|
|
6
|
+
# compositor. Element rules inside `choice` take their position in the
|
|
7
|
+
# sequence and carry the choice on their model attributes, so validation
|
|
8
|
+
# applies order and exclusivity.
|
|
9
|
+
RSpec.describe "Sequence with nested choice" do
|
|
10
|
+
let(:model) do
|
|
11
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
12
|
+
attribute :title, :string
|
|
13
|
+
attribute :hardcover, :string
|
|
14
|
+
attribute :paperback, :string
|
|
15
|
+
attribute :author, :string, collection: true
|
|
16
|
+
|
|
17
|
+
xml do
|
|
18
|
+
element "book"
|
|
19
|
+
sequence do
|
|
20
|
+
map_element "title", to: :title
|
|
21
|
+
choice do
|
|
22
|
+
map_element "hardcover", to: :hardcover
|
|
23
|
+
map_element "papercover", to: :paperback
|
|
24
|
+
map_element "paperback", to: :paperback
|
|
25
|
+
end
|
|
26
|
+
map_element "author", to: :author
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "records the choice as a compositor alongside the flat order" do
|
|
33
|
+
sequence = model.mappings_for(:xml).element_sequence.first
|
|
34
|
+
expect(sequence.compositors).to contain_exactly(
|
|
35
|
+
an_instance_of(Lutaml::Model::Choice),
|
|
36
|
+
)
|
|
37
|
+
choice = sequence.compositors.first
|
|
38
|
+
# After finalize the choice carries the model attributes its rules
|
|
39
|
+
# target — the shape validation machinery expects.
|
|
40
|
+
expect(choice.attributes.map(&:name)).to eq(%i[hardcover paperback])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "keeps the model attributes tagged with the nested choice" do
|
|
44
|
+
choice = model.mappings_for(:xml).element_sequence.first.compositors.first
|
|
45
|
+
expect(model.attributes[:hardcover].options[:choice]).to equal(choice)
|
|
46
|
+
expect(model.attributes[:paperback].options[:choice]).to equal(choice)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "validates order and exclusivity" do
|
|
50
|
+
valid = model.from_xml(
|
|
51
|
+
"<book><title>T</title><paperback>PB</paperback><author>A</author></book>",
|
|
52
|
+
)
|
|
53
|
+
expect(valid.validate).to be_empty
|
|
54
|
+
|
|
55
|
+
out_of_order = model.from_xml(
|
|
56
|
+
"<book><author>A</author><title>T</title><hardcover>HB</hardcover></book>",
|
|
57
|
+
)
|
|
58
|
+
expect(out_of_order.validate).not_to be_empty
|
|
59
|
+
|
|
60
|
+
both_branches = model.from_xml(
|
|
61
|
+
"<book><title>T</title><hardcover>HB</hardcover>" \
|
|
62
|
+
"<paperback>PB</paperback><author>A</author></book>",
|
|
63
|
+
)
|
|
64
|
+
expect(both_branches.validate).not_to be_empty
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -202,12 +202,10 @@ RSpec.describe Lutaml::Model::Store do
|
|
|
202
202
|
# After compaction, the refs array should be smaller than before
|
|
203
203
|
# (some dead refs removed). Exact count depends on GC timing,
|
|
204
204
|
# but the live refs must still be present.
|
|
205
|
-
live
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
false
|
|
210
|
-
end
|
|
205
|
+
# refs_for yields live objects directly under the WeakMap buckets
|
|
206
|
+
# (dead keys never surface), so the live count is bounded by the
|
|
207
|
+
# post-GC population regardless of compaction timing.
|
|
208
|
+
alive_count = instance.refs_for(model_class.to_s).count
|
|
211
209
|
expect(alive_count).to be < (threshold + 1 + interval)
|
|
212
210
|
end
|
|
213
211
|
|
|
@@ -225,14 +223,18 @@ RSpec.describe Lutaml::Model::Store do
|
|
|
225
223
|
expect(counters[other_class.to_s]).to eq(3)
|
|
226
224
|
end
|
|
227
225
|
|
|
228
|
-
it "
|
|
229
|
-
|
|
226
|
+
it "compacts only on interval multiples" do
|
|
227
|
+
interval = Lutaml::Model::Store::COMPACTION_INTERVAL
|
|
230
228
|
instance = described_class.instance
|
|
231
229
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
#
|
|
230
|
+
# WeakMap buckets shed dead keys without help, so compaction is
|
|
231
|
+
# purely index maintenance on a fixed cadence: the counter fires
|
|
232
|
+
# exactly at each interval boundary, never between.
|
|
233
|
+
(interval - 1).times { |i| model_class.new(id: "edge-#{i}") }
|
|
235
234
|
expect(instance.compaction_count).to eq(0)
|
|
235
|
+
|
|
236
|
+
model_class.new(id: "edge-trigger")
|
|
237
|
+
expect(instance.compaction_count).to eq(1)
|
|
236
238
|
end
|
|
237
239
|
end
|
|
238
240
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-model
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.37
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -1686,6 +1686,7 @@ files:
|
|
|
1686
1686
|
- spec/lutaml/key_value/transformation/value_serializer_spec.rb
|
|
1687
1687
|
- spec/lutaml/key_value/transformation_spec.rb
|
|
1688
1688
|
- spec/lutaml/model/absent_value_contracts_spec.rb
|
|
1689
|
+
- spec/lutaml/model/adapter_resolver_configured_spec.rb
|
|
1689
1690
|
- spec/lutaml/model/attribute_apply_value_map_spec.rb
|
|
1690
1691
|
- spec/lutaml/model/attribute_collection_spec.rb
|
|
1691
1692
|
- spec/lutaml/model/attribute_default_cache_spec.rb
|
|
@@ -1743,6 +1744,7 @@ files:
|
|
|
1743
1744
|
- spec/lutaml/model/key_value_data_model/key_value_element_spec.rb
|
|
1744
1745
|
- spec/lutaml/model/key_value_mapping_spec.rb
|
|
1745
1746
|
- spec/lutaml/model/lazy_collection_spec.rb
|
|
1747
|
+
- spec/lutaml/model/liquefiable_lazy_spec.rb
|
|
1746
1748
|
- spec/lutaml/model/liquefiable_spec.rb
|
|
1747
1749
|
- spec/lutaml/model/liquid/indexed_access_spec.rb
|
|
1748
1750
|
- spec/lutaml/model/liquid_compatibility_spec.rb
|
|
@@ -1750,6 +1752,7 @@ files:
|
|
|
1750
1752
|
- spec/lutaml/model/map_all_spec.rb
|
|
1751
1753
|
- spec/lutaml/model/map_content_spec.rb
|
|
1752
1754
|
- spec/lutaml/model/mapping_rule_perf_guard_spec.rb
|
|
1755
|
+
- spec/lutaml/model/mapping_type_validation_spec.rb
|
|
1753
1756
|
- spec/lutaml/model/mixed_content_spec.rb
|
|
1754
1757
|
- spec/lutaml/model/model_transformer_spec.rb
|
|
1755
1758
|
- spec/lutaml/model/multiple_mapping_spec.rb
|
|
@@ -1783,6 +1786,7 @@ files:
|
|
|
1783
1786
|
- spec/lutaml/model/schema/renderers/union_spec.rb
|
|
1784
1787
|
- spec/lutaml/model/schema/xsd_schema_spec.rb
|
|
1785
1788
|
- spec/lutaml/model/schema/yaml_schema_spec.rb
|
|
1789
|
+
- spec/lutaml/model/sequence_choice_compositor_spec.rb
|
|
1786
1790
|
- spec/lutaml/model/sequence_spec.rb
|
|
1787
1791
|
- spec/lutaml/model/serializable_spec.rb
|
|
1788
1792
|
- spec/lutaml/model/serializable_validation_spec.rb
|