lutaml-model 0.8.92 → 0.8.94
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/lib/lutaml/model/serialize/attribute_definition.rb +49 -0
- data/lib/lutaml/model/serialize/initialization.rb +16 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/model_transform.rb +65 -10
- data/spec/lutaml/model/late_attribute_declaration_spec.rb +35 -0
- data/spec/lutaml/model/writer_assigned_attribute_spec.rb +62 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '082f3275306fcd89a94f69f68b53d38b0c4c644de743dff261b6bc5166b12ec7'
|
|
4
|
+
data.tar.gz: 968f9d94e179035695be49649bd9c0b0c2e7e83192661ba3f6d95bbd5803a813
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f06573be9b9da0d12a561522a63d2d14b771a3ff7357fdb92c8d06213fa997efe19470f0d6b6ab39d9c672038715cc6efa1d607cb77dce5487e2134b56335a1
|
|
7
|
+
data.tar.gz: 275f2d2c74931c84024278dbc5bddcfe18cc267990c0516080605ce1fd2170c5513efcdd141ffbb72b9967cb91fd7ed74b338936a0e1a793aeba2411f8405d09
|
|
@@ -62,6 +62,28 @@ module Lutaml
|
|
|
62
62
|
#{lines}
|
|
63
63
|
end
|
|
64
64
|
RUBY
|
|
65
|
+
|
|
66
|
+
# lutaml-model#922: raw readers for the deserializer's
|
|
67
|
+
# unmatched-rule handling. A custom writer that assigns a
|
|
68
|
+
# *different* attribute mid-parse (relaton's ICS Isoics
|
|
69
|
+
# fallback; any derived-value pattern) is invisible to the
|
|
70
|
+
# value_set tracker -- public setters don't mark it -- so the
|
|
71
|
+
# tracked "still default" state lies. The deserializer needs
|
|
72
|
+
# the raw stored state to know whether an attribute was
|
|
73
|
+
# writer-populated: an attribute still holding its
|
|
74
|
+
# initialized marker (sentinel / lazy collection) is unset;
|
|
75
|
+
# anything else was written since init and must never be
|
|
76
|
+
# overwritten by an unmatched rule's default assignment.
|
|
77
|
+
raw_lines = attrs.map do |name, _attr|
|
|
78
|
+
<<~RUBY
|
|
79
|
+
def __lutaml_raw_#{name}
|
|
80
|
+
@#{name}
|
|
81
|
+
end
|
|
82
|
+
RUBY
|
|
83
|
+
end.join
|
|
84
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
85
|
+
#{raw_lines}
|
|
86
|
+
RUBY
|
|
65
87
|
end
|
|
66
88
|
|
|
67
89
|
# Historical getter shape for punctuation-named attributes and
|
|
@@ -110,6 +132,28 @@ module Lutaml
|
|
|
110
132
|
name = attr.name
|
|
111
133
|
register_id = extract_register_id(register)
|
|
112
134
|
|
|
135
|
+
# lutaml-model#922: raw-state reader for the deserializer's
|
|
136
|
+
# unmatched-rule handling. A custom writer that assigns a
|
|
137
|
+
# *different* attribute mid-parse (relaton's ICS Isoics
|
|
138
|
+
# fallback; any derived-value pattern) is invisible to the
|
|
139
|
+
# value_set tracker -- public setters don't mark it -- so the
|
|
140
|
+
# tracked "still default" state lies. The deserializer needs
|
|
141
|
+
# the raw stored state to know whether an attribute was
|
|
142
|
+
# writer-populated: still holding the initialized marker
|
|
143
|
+
# (sentinel / lazy collection) means unset; anything else was
|
|
144
|
+
# written since init and must never be overwritten by an
|
|
145
|
+
# unmatched rule's default assignment. Declared here (not in
|
|
146
|
+
# the state-defaults compilation) so late declarations get it
|
|
147
|
+
# too -- XMI's extension loader declares attributes after
|
|
148
|
+
# first parse.
|
|
149
|
+
if name.to_s.match?(PLAIN_NAME)
|
|
150
|
+
model.class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
151
|
+
def __lutaml_raw_#{name}
|
|
152
|
+
@#{name}
|
|
153
|
+
end
|
|
154
|
+
RUBY
|
|
155
|
+
end
|
|
156
|
+
|
|
113
157
|
if attr.enum?
|
|
114
158
|
add_enum_methods_to_model(
|
|
115
159
|
model,
|
|
@@ -354,6 +398,11 @@ module Lutaml
|
|
|
354
398
|
attr = Attribute.new(name, type, options)
|
|
355
399
|
@attributes[name] = attr
|
|
356
400
|
@merged_attributes_cache = nil
|
|
401
|
+
# Attributes can be declared after the model has been
|
|
402
|
+
# instantiated (forward references, post-hoc declarations);
|
|
403
|
+
# the interned writer table from the first instantiation must
|
|
404
|
+
# not outlive the attribute set it was built from.
|
|
405
|
+
@instantiate_writers = nil
|
|
357
406
|
invalidate_state_defaults!
|
|
358
407
|
define_attribute_methods(attr)
|
|
359
408
|
|
|
@@ -392,6 +392,22 @@ module Lutaml
|
|
|
392
392
|
# set (nil when no default), never left as the
|
|
393
393
|
# uninitialized sentinel — readers and formatters touch
|
|
394
394
|
# absent attributes freely.
|
|
395
|
+
#
|
|
396
|
+
# lutaml-model#922: unless a custom writer already
|
|
397
|
+
# populated it. An earlier kwarg's writer may have
|
|
398
|
+
# assigned this attribute as a side effect (relaton's ICS
|
|
399
|
+
# populates <text> from the Isoics dataset inside code=);
|
|
400
|
+
# the raw state is the truth — still at its initialized
|
|
401
|
+
# marker (sentinel / lazy collection) means unset, and
|
|
402
|
+
# only then is the default seeded. The writer's own
|
|
403
|
+
# value_set_for (inside the compiled writer) already
|
|
404
|
+
# marked it set, so it renders.
|
|
405
|
+
raw = instance.public_send(:"__lutaml_raw_#{name}")
|
|
406
|
+
unset = raw.nil? ||
|
|
407
|
+
raw.equal?(Lutaml::Model::UninitializedClass.instance) ||
|
|
408
|
+
raw.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION)
|
|
409
|
+
next unless unset
|
|
410
|
+
|
|
395
411
|
default = attr.default_value(register_id, instance)
|
|
396
412
|
value = if Lutaml::Model::Utils.uninitialized?(default)
|
|
397
413
|
nil
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -345,12 +345,11 @@ module Lutaml
|
|
|
345
345
|
mappings.each do |rule|
|
|
346
346
|
record = records&.[](rule)
|
|
347
347
|
|
|
348
|
-
# #903: replay-or-drop unmatched
|
|
349
|
-
#
|
|
350
|
-
if replay_unmatched_rule(child_names_set, record, rule,
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
end
|
|
348
|
+
# #903/#922: replay-or-drop unmatched rules; never clobber
|
|
349
|
+
# a mid-parse custom-writer assignment.
|
|
350
|
+
next if replay_unmatched_rule(child_names_set, record, rule,
|
|
351
|
+
instance, defaults_used,
|
|
352
|
+
xml_mapping)
|
|
354
353
|
|
|
355
354
|
rule_to = rule.to
|
|
356
355
|
|
|
@@ -436,11 +435,19 @@ module Lutaml
|
|
|
436
435
|
# NOTE: do not "read the instance" here (to_value_for) —
|
|
437
436
|
# the compiled getters materialize lazy collections on
|
|
438
437
|
# first read, which tripled parse allocations when tried.
|
|
438
|
+
if writer_populated?(record, instance)
|
|
439
|
+
instance.value_set_for(rule_to)
|
|
440
|
+
next
|
|
441
|
+
end
|
|
439
442
|
if instance.using_default?(rule_to) || rule.render_default
|
|
440
443
|
defaults_used << rule_to
|
|
441
444
|
attr&.default(effective_register) || rule.to_value_for(instance)
|
|
442
445
|
else
|
|
443
|
-
|
|
446
|
+
# The attribute already holds an explicit value --
|
|
447
|
+
# assigned by a matched rule or by a custom writer
|
|
448
|
+
# mid-parse (lutaml-model#922). An unmatched rule
|
|
449
|
+
# must not overwrite it with nil.
|
|
450
|
+
next
|
|
444
451
|
end
|
|
445
452
|
else
|
|
446
453
|
raise "ELSE-BODY" if ENV["CATCHDBG"] && rule.name.nil?
|
|
@@ -857,7 +864,7 @@ _effective_register)
|
|
|
857
864
|
|
|
858
865
|
Record = Struct.new(:attr, :derived, :group_skip, :valid,
|
|
859
866
|
:unmatched_mode, :replay_writer,
|
|
860
|
-
:unmatched_names)
|
|
867
|
+
:unmatched_names, :raw_reader)
|
|
861
868
|
|
|
862
869
|
def compiled_rule_records(model_class, register, grouped_plain_rules)
|
|
863
870
|
return {}.compare_by_identity unless model_class.include?(Lutaml::Model::Serialize)
|
|
@@ -946,6 +953,15 @@ _effective_register)
|
|
|
946
953
|
unmatched_mode,
|
|
947
954
|
replay_writer,
|
|
948
955
|
unmatched_names,
|
|
956
|
+
# lutaml-model#922: raw-state reader for the attribute,
|
|
957
|
+
# when the compiled init seeds it (plain names -- the same
|
|
958
|
+
# set compile_state_defaults! writes). Delegated rules name
|
|
959
|
+
# an attribute on the delegate object, not on this
|
|
960
|
+
# instance, so they get no raw reader.
|
|
961
|
+
if attr && rule.delegate.nil? &&
|
|
962
|
+
rule.to.to_s.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/)
|
|
963
|
+
:"__lutaml_raw_#{rule.to}"
|
|
964
|
+
end,
|
|
949
965
|
)
|
|
950
966
|
end
|
|
951
967
|
entry = Struct.new(:version, :records).new(version, records)
|
|
@@ -1630,6 +1646,24 @@ effective_register = lutaml_register)
|
|
|
1630
1646
|
# (the visit ended at the omission gate or was a guarded next)
|
|
1631
1647
|
# writes nothing; :write_nil replays the nil the default value
|
|
1632
1648
|
# map produces, plus the flag churn. Returns true when handled.
|
|
1649
|
+
# lutaml-model#922: whether the attribute holds anything other
|
|
1650
|
+
# than its initialized marker (uninitialized sentinel / lazy
|
|
1651
|
+
# collection) — i.e. a custom writer populated it mid-parse.
|
|
1652
|
+
def writer_populated?(record, instance)
|
|
1653
|
+
return false unless record&.raw_reader
|
|
1654
|
+
|
|
1655
|
+
!raw_initialized?(record, instance)
|
|
1656
|
+
end
|
|
1657
|
+
|
|
1658
|
+
# True when the attribute still holds the value the compiled init
|
|
1659
|
+
# seeded (uninitialized sentinel / lazy collection) — i.e. nothing
|
|
1660
|
+
# has written it since.
|
|
1661
|
+
def raw_initialized?(record, instance)
|
|
1662
|
+
raw = instance.public_send(record.raw_reader)
|
|
1663
|
+
raw.equal?(::Lutaml::Model::UninitializedClass.instance) ||
|
|
1664
|
+
raw.equal?(::Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION)
|
|
1665
|
+
end
|
|
1666
|
+
|
|
1633
1667
|
def replay_unmatched_rule(child_names_set, record, rule, instance,
|
|
1634
1668
|
defaults_used, xml_mapping)
|
|
1635
1669
|
return false unless child_names_set && record
|
|
@@ -1647,9 +1681,30 @@ effective_register = lutaml_register)
|
|
|
1647
1681
|
|
|
1648
1682
|
return true if mode == :drop
|
|
1649
1683
|
|
|
1650
|
-
|
|
1684
|
+
# The visited path picks the default branch only while the
|
|
1685
|
+
# attribute still reads as default-valued; once another rule
|
|
1686
|
+
# or a custom writer set it mid-parse, the omission gate ends
|
|
1687
|
+
# the visit with no write. Mirror that: replay writes only
|
|
1688
|
+
# while unset, and drops otherwise (a custom writer may have
|
|
1689
|
+
# populated the attribute — relaton's ICS Isoics-fallback
|
|
1690
|
+
# pattern).
|
|
1691
|
+
rule_to = rule.to
|
|
1692
|
+
unless instance.using_default?(rule_to)
|
|
1693
|
+
return true
|
|
1694
|
+
end
|
|
1695
|
+
|
|
1696
|
+
# lutaml-model#922: a custom writer may have populated the
|
|
1697
|
+
# attribute mid-parse without going through the tracker (the
|
|
1698
|
+
# compiled setters are direct ivar writes), so the tracked
|
|
1699
|
+
# flag above can lie. The raw state is the truth: still at its
|
|
1700
|
+
# initialized marker (sentinel / lazy collection) means unset
|
|
1701
|
+
# -- the visited path wrote the default there; anything else
|
|
1702
|
+
# is a writer assignment this replay must not clobber.
|
|
1703
|
+
return true unless raw_initialized?(record, instance)
|
|
1704
|
+
|
|
1705
|
+
defaults_used << rule_to
|
|
1651
1706
|
instance.public_send(record.replay_writer, nil)
|
|
1652
|
-
instance.value_set_for(
|
|
1707
|
+
instance.value_set_for(rule_to)
|
|
1653
1708
|
true
|
|
1654
1709
|
end
|
|
1655
1710
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Attributes can be declared after the model has been instantiated
|
|
6
|
+
# (forward references, post-hoc declarations). The interned writer
|
|
7
|
+
# table built during the first instantiation must not outlive the
|
|
8
|
+
# attribute set it was built from.
|
|
9
|
+
RSpec.describe "attributes declared after first instantiation" do
|
|
10
|
+
it "initializes attributes added after an earlier instantiation" do
|
|
11
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
12
|
+
attribute :name, :string
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
klass.new(name: "first")
|
|
16
|
+
|
|
17
|
+
klass.attribute :size, :string
|
|
18
|
+
instance = klass.new(name: "second", size: "L")
|
|
19
|
+
|
|
20
|
+
expect(instance.name).to eq("second")
|
|
21
|
+
expect(instance.size).to eq("L")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "keeps earlier instances unaffected" do
|
|
25
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
26
|
+
attribute :name, :string
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
first = klass.new(name: "first")
|
|
30
|
+
klass.attribute :size, :string
|
|
31
|
+
|
|
32
|
+
expect(first.name).to eq("first")
|
|
33
|
+
expect(first.respond_to?(:size)).to be(true)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
module WriterAssigned
|
|
6
|
+
class Plain < Lutaml::Model::Serializable
|
|
7
|
+
attribute :code, :string
|
|
8
|
+
attribute :text, :string
|
|
9
|
+
|
|
10
|
+
xml do
|
|
11
|
+
element "probe"
|
|
12
|
+
map_element "code", to: :code
|
|
13
|
+
map_element "text", to: :text
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def code=(val)
|
|
17
|
+
super
|
|
18
|
+
self.text = "derived-#{val}" if val
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Defaulted < Lutaml::Model::Serializable
|
|
23
|
+
attribute :code, :string
|
|
24
|
+
attribute :text, :string, default: -> { "" }
|
|
25
|
+
|
|
26
|
+
xml do
|
|
27
|
+
element "probe"
|
|
28
|
+
map_element "code", to: :code
|
|
29
|
+
map_element "text", to: :text
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def code=(val)
|
|
33
|
+
super
|
|
34
|
+
self.text = "derived-#{val}" if val
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# lutaml-model#922: a custom writer that assigns a *different* attribute
|
|
40
|
+
# mid-parse (relaton's Relaton::Bib::ICS populates <text> from the Isoics
|
|
41
|
+
# dataset inside code=) must survive the deserializer's unmatched-rule
|
|
42
|
+
# handling: the default seeding in Initialization#instantiate and the
|
|
43
|
+
# unmatched-rule replay must never overwrite a value a writer assigned.
|
|
44
|
+
RSpec.describe "attributes assigned inside another attribute's writer" do
|
|
45
|
+
%i[Plain Defaulted].each do |klass_name|
|
|
46
|
+
klass = WriterAssigned.const_get(klass_name)
|
|
47
|
+
|
|
48
|
+
describe "with #{klass_name.to_s.downcase} text attribute" do
|
|
49
|
+
it "keeps the derived value through from_xml" do
|
|
50
|
+
instance = klass.from_xml("<probe><code>67.060</code></probe>")
|
|
51
|
+
|
|
52
|
+
expect(instance.text).to eq("derived-67.060")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "renders the derived value on to_xml" do
|
|
56
|
+
instance = klass.from_xml("<probe><code>67.060</code></probe>")
|
|
57
|
+
|
|
58
|
+
expect(instance.to_xml).to include("<text>derived-67.060</text>")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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.94
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -1759,6 +1759,7 @@ files:
|
|
|
1759
1759
|
- spec/lutaml/model/jsonl_spec.rb
|
|
1760
1760
|
- spec/lutaml/model/key_value_data_model/key_value_element_spec.rb
|
|
1761
1761
|
- spec/lutaml/model/key_value_mapping_spec.rb
|
|
1762
|
+
- spec/lutaml/model/late_attribute_declaration_spec.rb
|
|
1762
1763
|
- spec/lutaml/model/lazy_collection_spec.rb
|
|
1763
1764
|
- spec/lutaml/model/liquefiable_lazy_spec.rb
|
|
1764
1765
|
- spec/lutaml/model/liquefiable_spec.rb
|
|
@@ -1872,6 +1873,7 @@ files:
|
|
|
1872
1873
|
- spec/lutaml/model/value_policy_spec.rb
|
|
1873
1874
|
- spec/lutaml/model/value_transformation_comprehensive_spec.rb
|
|
1874
1875
|
- spec/lutaml/model/with_child_mapping_spec.rb
|
|
1876
|
+
- spec/lutaml/model/writer_assigned_attribute_spec.rb
|
|
1875
1877
|
- spec/lutaml/model/xml_decoupling_spec.rb
|
|
1876
1878
|
- spec/lutaml/model/xsd_form_default_patterns_spec.rb
|
|
1877
1879
|
- spec/lutaml/model/xsd_patterns_spec.rb
|