lutaml-model 0.8.132 → 0.8.134
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/docs/key_value_data_model.adoc +54 -0
- data/lib/lutaml/model/type/hash.rb +20 -5
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/schema/xsd/redefine.rb +4 -4
- data/lib/lutaml/xml/serialization/instance_methods.rb +6 -1
- data/spec/lutaml/model/mixed_content_spec.rb +32 -0
- data/spec/lutaml/model/type/hash_spec.rb +39 -19
- data/spec/lutaml/xml/schema/xsd/mapping_integrity_spec.rb +92 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 250473959d5b49e40ab480a2acf1e708d2cac1f8d5b092995be03821bc758122
|
|
4
|
+
data.tar.gz: 59fc186602fae873d070d23baca950a2330a07abbb251acd16b3a63803877a93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82c2a11ef5c20e8342280054b0053e7d07fcca92b71effc4224d6b006889f3873026890d894b98a0791add12444e1cb87cddab51ece3eee47f2d08e8a53106c0
|
|
7
|
+
data.tar.gz: 95ec3912247f014bc20a9bf2f3ec39a3fc1b0bdf5eeb2b932f55612446b5f3c45af21e6b37c28682b52541e842bc68b7099ce361b6b8184f93265e8254779873
|
|
@@ -446,6 +446,60 @@ Model.to_json # Uses KeyValue::Transformation → KeyValueElement → Adapter
|
|
|
446
446
|
Model.to_json # Uses Hash transformation → Adapter
|
|
447
447
|
----
|
|
448
448
|
|
|
449
|
+
== The custom `to:` handler contract
|
|
450
|
+
|
|
451
|
+
A mapping rule that declares `with: { to: :method_name }` receives the
|
|
452
|
+
output document as its second argument and mutates it in place
|
|
453
|
+
(mutation-only — the handler's return value is ignored, lutaml-model
|
|
454
|
+
#892). Which object the handler receives depends on the serialization
|
|
455
|
+
lane:
|
|
456
|
+
|
|
457
|
+
* The transformation lane — the default path — hands the handler a
|
|
458
|
+
scratch `Lutaml::KeyValue::DataModel::Element` keyed with the rule's
|
|
459
|
+
serialized name. After the handler runs, only the element's payload
|
|
460
|
+
(the merged children, or the direct value hash) is merged into the
|
|
461
|
+
output.
|
|
462
|
+
* The legacy hash lane (used when no compiled transformation is
|
|
463
|
+
available, e.g. cyclic model graphs) hands the handler the output
|
|
464
|
+
`Hash` itself.
|
|
465
|
+
|
|
466
|
+
=== The portable subset
|
|
467
|
+
|
|
468
|
+
Write handlers against the hash-like subset — it behaves identically on
|
|
469
|
+
both lanes:
|
|
470
|
+
|
|
471
|
+
[source,ruby]
|
|
472
|
+
----
|
|
473
|
+
key_value do
|
|
474
|
+
map "locality", to: :type, with: { to: :locality_to_yaml }
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def locality_to_yaml(model, doc)
|
|
478
|
+
doc["locality"] = {}
|
|
479
|
+
doc["locality"]["type"] = model.locality.type
|
|
480
|
+
doc["locality"]["reference_from"] = model.locality.reference_from
|
|
481
|
+
end
|
|
482
|
+
----
|
|
483
|
+
|
|
484
|
+
On the transformation lane, `doc["locality"]` reads through to the
|
|
485
|
+
child's own value object, so chained writes mutate it in place; on the
|
|
486
|
+
legacy lane the same lines are plain Hash writes.
|
|
487
|
+
|
|
488
|
+
=== Element-only surface
|
|
489
|
+
|
|
490
|
+
`doc.value =`, `doc.add_child`, and reads of `doc.value` exist only on
|
|
491
|
+
the transformation lane's Element. A handler that uses them breaks if
|
|
492
|
+
the legacy lane ever serves the model (cyclic graphs, mapping
|
|
493
|
+
overrides). Prefer the hash-like subset unless the handler is known to
|
|
494
|
+
run only on the transformation lane.
|
|
495
|
+
|
|
496
|
+
=== Merge semantics
|
|
497
|
+
|
|
498
|
+
After the handler runs, the document's payload merges into the output
|
|
499
|
+
only when it is a `Hash` — an untouched document contributes nothing,
|
|
500
|
+
and scalar or array payloads are dropped (matching the pre-0.8.74
|
|
501
|
+
behavior).
|
|
502
|
+
|
|
449
503
|
== Serialization Flow
|
|
450
504
|
|
|
451
505
|
=== Complete Data Flow
|
|
@@ -18,22 +18,37 @@ module Lutaml
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def self.normalize_hash(hash)
|
|
21
|
-
|
|
21
|
+
xml = hash.is_a?(Lutaml::Model::MappingHash)
|
|
22
|
+
return hash["text"] if text_node?(hash)
|
|
22
23
|
|
|
23
|
-
hash = hash.to_h if
|
|
24
|
+
hash = hash.to_h if xml
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
normalized = hash.transform_values do |value|
|
|
26
27
|
normalize_value(value)
|
|
27
28
|
end
|
|
29
|
+
xml ? (normalized["elements"] || normalized) : normalized
|
|
30
|
+
end
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
# {"text" => string} is the XML text-node convention and
|
|
33
|
+
# collapses to the string. Any other "text" key — structured
|
|
34
|
+
# values, or appearing alongside other keys in non-XML data —
|
|
35
|
+
# is ordinary data and must round-trip verbatim
|
|
36
|
+
# (lutaml/lutaml-model#1013).
|
|
37
|
+
def self.text_node?(hash)
|
|
38
|
+
# ::String — unqualified String resolves to Type::String here
|
|
39
|
+
hash.is_a?(::Hash) && hash.keys == ["text"] &&
|
|
40
|
+
hash["text"].is_a?(::String)
|
|
30
41
|
end
|
|
31
42
|
|
|
32
43
|
def self.normalize_value(value)
|
|
33
44
|
return value unless value.is_a?(::Hash)
|
|
34
45
|
|
|
35
46
|
nested = normalize_hash(value)
|
|
36
|
-
|
|
47
|
+
if value.is_a?(Lutaml::Model::MappingHash) && nested.is_a?(::Hash)
|
|
48
|
+
nested.except("text")
|
|
49
|
+
else
|
|
50
|
+
nested
|
|
51
|
+
end
|
|
37
52
|
end
|
|
38
53
|
|
|
39
54
|
def self.serialize(value)
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -19,12 +19,12 @@ module Lutaml
|
|
|
19
19
|
namespace Lutaml::Xml::Schema::XsdNamespace
|
|
20
20
|
|
|
21
21
|
map_attribute :id, to: :id
|
|
22
|
-
map_attribute
|
|
22
|
+
map_attribute "schemaLocation", to: :schema_path
|
|
23
23
|
map_element :group, to: :group
|
|
24
24
|
map_element :annotation, to: :annotation
|
|
25
|
-
map_element :simpleType, to: :
|
|
26
|
-
map_element :complexType, to: :
|
|
27
|
-
map_element :attributeGroup, to: :
|
|
25
|
+
map_element :simpleType, to: :simple_type
|
|
26
|
+
map_element :complexType, to: :complex_type
|
|
27
|
+
map_element :attributeGroup, to: :attribute_group
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
Lutaml::Xml::Schema::Xsd.register_model(self, :redefine)
|
|
@@ -178,8 +178,13 @@ module Lutaml
|
|
|
178
178
|
elsif val.is_a?(Lutaml::Model::Serializable)
|
|
179
179
|
collection_indices[attr_name] += 1
|
|
180
180
|
collection_indices[attr_name] == 1 ? val : nil
|
|
181
|
+
else
|
|
182
|
+
# Scalar mapped elements (e.g. a string-valued
|
|
183
|
+
# <main>) are part of the mixed content too.
|
|
184
|
+
collection_indices[attr_name] += 1
|
|
185
|
+
val
|
|
181
186
|
end
|
|
182
|
-
yield(obj)
|
|
187
|
+
yield(obj) unless obj.nil? || (obj.is_a?(String) && obj.empty?)
|
|
183
188
|
end
|
|
184
189
|
end
|
|
185
190
|
|
|
@@ -1449,4 +1449,36 @@ RSpec.describe "MixedContent" do
|
|
|
1449
1449
|
described_class
|
|
1450
1450
|
end
|
|
1451
1451
|
end
|
|
1452
|
+
|
|
1453
|
+
describe "each_mixed_content with string-valued mapped elements" do
|
|
1454
|
+
it "yields scalar mapped elements, not only Arrays and Serializable children" do
|
|
1455
|
+
xml = "<title-wrap><main>Test Document</main></title-wrap>"
|
|
1456
|
+
model = MixedContentSpec::TitleWrap.from_xml(xml)
|
|
1457
|
+
|
|
1458
|
+
expect(model.main).to eq("Test Document")
|
|
1459
|
+
|
|
1460
|
+
yielded = model.each_mixed_content.to_a
|
|
1461
|
+
expect(yielded).to include("Test Document")
|
|
1462
|
+
end
|
|
1463
|
+
|
|
1464
|
+
it "skips empty scalar values" do
|
|
1465
|
+
xml = "<title-wrap><main></main></title-wrap>"
|
|
1466
|
+
model = MixedContentSpec::TitleWrap.from_xml(xml)
|
|
1467
|
+
|
|
1468
|
+
yielded = model.each_mixed_content.to_a
|
|
1469
|
+
expect(yielded).not_to include("")
|
|
1470
|
+
end
|
|
1471
|
+
end
|
|
1472
|
+
end
|
|
1473
|
+
|
|
1474
|
+
module MixedContentSpec
|
|
1475
|
+
class TitleWrap < Lutaml::Model::Serializable
|
|
1476
|
+
attribute :main, :string
|
|
1477
|
+
|
|
1478
|
+
xml do
|
|
1479
|
+
element "title-wrap"
|
|
1480
|
+
mixed_content
|
|
1481
|
+
map_element "main", to: :main
|
|
1482
|
+
end
|
|
1483
|
+
end
|
|
1452
1484
|
end
|
|
@@ -6,11 +6,30 @@ RSpec.describe Lutaml::Model::Type::Hash do
|
|
|
6
6
|
expect(described_class.cast(nil)).to be_nil
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
it "returns text content from
|
|
10
|
-
hash =
|
|
9
|
+
it "returns text content from a MappingHash with only text key" do
|
|
10
|
+
hash = Lutaml::Model::MappingHash.new
|
|
11
|
+
hash["text"] = "content"
|
|
11
12
|
expect(described_class.cast(hash)).to eq "content"
|
|
12
13
|
end
|
|
13
14
|
|
|
15
|
+
# lutaml/lutaml-model#1013: a "text" key holding structured data
|
|
16
|
+
# (or appearing alongside other keys in non-XML data) is ordinary
|
|
17
|
+
# data and must round-trip verbatim
|
|
18
|
+
it "preserves a structured text key from a plain hash" do
|
|
19
|
+
hash = { "text" => { "type" => "string", "description" => "y" } }
|
|
20
|
+
expect(described_class.cast(hash)).to eq hash
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "preserves text alongside other keys in a plain hash" do
|
|
24
|
+
hash = { "text" => "content", "other" => "value" }
|
|
25
|
+
expect(described_class.cast(hash)).to eq hash
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "preserves an elements key from a plain hash" do
|
|
29
|
+
hash = { "elements" => [{ "a" => 1 }] }
|
|
30
|
+
expect(described_class.cast(hash)).to eq hash
|
|
31
|
+
end
|
|
32
|
+
|
|
14
33
|
it "normalizes MappingHash to regular Hash" do
|
|
15
34
|
mapping_hash = Lutaml::Model::MappingHash.new
|
|
16
35
|
mapping_hash["key"] = "value"
|
|
@@ -18,28 +37,29 @@ RSpec.describe Lutaml::Model::Type::Hash do
|
|
|
18
37
|
end
|
|
19
38
|
|
|
20
39
|
context "with nested hashes" do
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
let(:expected_hash) do
|
|
35
|
-
{
|
|
40
|
+
it "filters out text keys from nested MappingHashes" do
|
|
41
|
+
child1 = Lutaml::Model::MappingHash.new
|
|
42
|
+
child1["text"] = "content1"
|
|
43
|
+
child1["other"] = "value1"
|
|
44
|
+
child2 = Lutaml::Model::MappingHash.new
|
|
45
|
+
child2["text"] = "content2"
|
|
46
|
+
child2["other"] = "value2"
|
|
47
|
+
input = Lutaml::Model::MappingHash.new
|
|
48
|
+
input["key1"] = child1
|
|
49
|
+
input["key2"] = child2
|
|
50
|
+
expected = {
|
|
36
51
|
"key1" => { "other" => "value1" },
|
|
37
52
|
"key2" => { "other" => "value2" },
|
|
38
53
|
}
|
|
54
|
+
expect(described_class.cast(input)).to eq(expected)
|
|
39
55
|
end
|
|
40
56
|
|
|
41
|
-
it "
|
|
42
|
-
|
|
57
|
+
it "preserves text keys in nested plain hashes" do
|
|
58
|
+
input = {
|
|
59
|
+
"key1" => { "text" => "content1", "other" => "value1" },
|
|
60
|
+
"key2" => { "text" => "content2", "other" => "value2" },
|
|
61
|
+
}
|
|
62
|
+
expect(described_class.cast(input)).to eq(input)
|
|
43
63
|
end
|
|
44
64
|
end
|
|
45
65
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/xml"
|
|
5
|
+
require "lutaml/xml/schema/xsd"
|
|
6
|
+
|
|
7
|
+
# The XSD schema-object classes carry hand-written XML mappings; a `to:`
|
|
8
|
+
# that does not name a declared attribute compiles fine and then crashes
|
|
9
|
+
# at parse time with "Attribute '...' not found" — for ANY document that
|
|
10
|
+
# contains the class, because the rule loop validates every mapping rule
|
|
11
|
+
# against the instance, not only the children present (#1025: redefine
|
|
12
|
+
# shipped to: :simpleType / :complexType / :attributeGroup against
|
|
13
|
+
# simple_type / complex_type / attribute_group). This sweep pins every
|
|
14
|
+
# registered model's element and attribute rules to declared attributes
|
|
15
|
+
# so the whole typo family fails here, at spec time.
|
|
16
|
+
RSpec.describe "XSD schema model mapping integrity" do
|
|
17
|
+
it "maps every element rule of every registered model to a declared attribute" do
|
|
18
|
+
broken = []
|
|
19
|
+
|
|
20
|
+
Lutaml::Xml::Schema::Xsd.register.models.each_value do |klass|
|
|
21
|
+
next unless klass.is_a?(Class) && klass.include?(Lutaml::Model::Serialize)
|
|
22
|
+
|
|
23
|
+
mapping = klass.mappings_for(:xml)
|
|
24
|
+
mapping.mapping_elements_hash.each_value do |rule_or_array|
|
|
25
|
+
Array(rule_or_array).each do |rule|
|
|
26
|
+
unless klass.attributes.key?(rule.to)
|
|
27
|
+
broken << "#{klass.name}: map_element #{rule.name.inspect} -> " \
|
|
28
|
+
"#{rule.to.inspect}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
expect(broken).to be_empty, <<~MSG
|
|
35
|
+
mapping rules point at undeclared attributes:
|
|
36
|
+
#{broken.join("\n")}
|
|
37
|
+
MSG
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "maps every attribute rule of every registered model to a declared attribute" do
|
|
41
|
+
broken = []
|
|
42
|
+
|
|
43
|
+
Lutaml::Xml::Schema::Xsd.register.models.each_value do |klass|
|
|
44
|
+
next unless klass.is_a?(Class) && klass.include?(Lutaml::Model::Serialize)
|
|
45
|
+
|
|
46
|
+
mapping = klass.mappings_for(:xml)
|
|
47
|
+
mapping.mapping_attributes_hash.each_value do |rule_or_array|
|
|
48
|
+
Array(rule_or_array).each do |rule|
|
|
49
|
+
unless klass.attributes.key?(rule.to)
|
|
50
|
+
broken << "#{klass.name}: map_attribute #{rule.name.inspect} -> " \
|
|
51
|
+
"#{rule.to.inspect}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
expect(broken).to be_empty, <<~MSG
|
|
58
|
+
mapping rules point at undeclared attributes:
|
|
59
|
+
#{broken.join("\n")}
|
|
60
|
+
MSG
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# #1025: the regression that motivated the sweep — a redefine with a
|
|
64
|
+
# group child parses, and each redefined member kind lands on its
|
|
65
|
+
# declared attribute.
|
|
66
|
+
it "parses xs:redefine with group, simpleType, complexType and attributeGroup" do
|
|
67
|
+
doc = Lutaml::Xml::Schema::Xsd.parse(<<~XSD)
|
|
68
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
|
69
|
+
<xsd:redefine schemaLocation="base.xsd">
|
|
70
|
+
<xsd:group name="g">
|
|
71
|
+
<xsd:choice><xsd:element name="e1"/></xsd:choice>
|
|
72
|
+
</xsd:group>
|
|
73
|
+
<xsd:simpleType name="st">
|
|
74
|
+
<xsd:restriction base="xsd:string"/>
|
|
75
|
+
</xsd:simpleType>
|
|
76
|
+
<xsd:complexType name="ct">
|
|
77
|
+
<xsd:sequence><xsd:element name="x"/></xsd:sequence>
|
|
78
|
+
</xsd:complexType>
|
|
79
|
+
<xsd:attributeGroup name="ag"/>
|
|
80
|
+
</xsd:redefine>
|
|
81
|
+
</xsd:schema>
|
|
82
|
+
XSD
|
|
83
|
+
|
|
84
|
+
redefined = doc.redefine.first
|
|
85
|
+
|
|
86
|
+
expect(redefined.group.name).to eq("g")
|
|
87
|
+
expect(redefined.simple_type.name).to eq("st")
|
|
88
|
+
expect(redefined.complex_type.name).to eq("ct")
|
|
89
|
+
expect(redefined.attribute_group.name).to eq("ag")
|
|
90
|
+
expect(redefined.schema_path).to eq("base.xsd")
|
|
91
|
+
end
|
|
92
|
+
end
|
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.134
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -2056,6 +2056,7 @@ files:
|
|
|
2056
2056
|
- spec/lutaml/xml/schema/xsd/liquid_methods/schema_spec.rb
|
|
2057
2057
|
- spec/lutaml/xml/schema/xsd/liquid_methods/simple_content_spec.rb
|
|
2058
2058
|
- spec/lutaml/xml/schema/xsd/list_simple_type_spec.rb
|
|
2059
|
+
- spec/lutaml/xml/schema/xsd/mapping_integrity_spec.rb
|
|
2059
2060
|
- spec/lutaml/xml/schema/xsd/namespace_uri_remapping_spec.rb
|
|
2060
2061
|
- spec/lutaml/xml/schema/xsd/schema_helper_methods_spec.rb
|
|
2061
2062
|
- spec/lutaml/xml/schema/xsd/schema_mapping_spec.rb
|