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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83ece4646bc4f2ecd052f6ba5edf994cb512b4be3a08e8a1778c5b357daab424
4
- data.tar.gz: 9bac20d16559163de764a7148974724c199cacbce0d88d945b932c7d36ccaa19
3
+ metadata.gz: 250473959d5b49e40ab480a2acf1e708d2cac1f8d5b092995be03821bc758122
4
+ data.tar.gz: 59fc186602fae873d070d23baca950a2330a07abbb251acd16b3a63803877a93
5
5
  SHA512:
6
- metadata.gz: 27120683de1ff8d9c95049e683dffdf302aa43edf9a4e5e6debda189df3903fdf98f4722200eb4106285e86dfabb170b77fcaea3313c6e1a8e8e548ec2db0eab
7
- data.tar.gz: 26a84cc1b288f7b829c1bdb44360adc7211843e506d1d51c1f416c411636fa6099dee79fd26d4d41a7e22b5eaf3ef1510d9580dbe49318ac8c2b9e02b80974bc
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
- return hash["text"] if hash.keys == ["text"]
21
+ xml = hash.is_a?(Lutaml::Model::MappingHash)
22
+ return hash["text"] if text_node?(hash)
22
23
 
23
- hash = hash.to_h if hash.is_a?(Lutaml::Model::MappingHash)
24
+ hash = hash.to_h if xml
24
25
 
25
- normalized_hash = hash.transform_values do |value|
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
- normalized_hash["elements"] || normalized_hash
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
- nested.is_a?(::Hash) ? nested.except("text") : nested
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)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.132"
5
+ VERSION = "0.8.134"
6
6
  end
7
7
  end
@@ -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 :schema_location, to: :schema_path
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: :simpleType
26
- map_element :complexType, to: :complexType
27
- map_element :attributeGroup, to: :attributeGroup
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) if 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 hash with only text key" do
10
- hash = { "text" => "content" }
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
- let(:input) do
22
- {
23
- "key1" => {
24
- "text" => "content1",
25
- "other" => "value1",
26
- },
27
- "key2" => {
28
- "text" => "content2",
29
- "other" => "value2",
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 "filters out text keys from nested hashes" do
42
- expect(described_class.cast(input)).to eq(expected_hash)
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.132
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