lutaml-model 0.5.3 → 0.5.4
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/.github/workflows/dependent-tests.yml +2 -0
- data/.rubocop_todo.yml +39 -13
- data/Gemfile +1 -0
- data/README.adoc +396 -23
- data/lib/lutaml/model/constants.rb +7 -0
- data/lib/lutaml/model/error/type/invalid_value_error.rb +19 -0
- data/lib/lutaml/model/error.rb +1 -0
- data/lib/lutaml/model/key_value_mapping.rb +31 -2
- data/lib/lutaml/model/mapping_hash.rb +8 -0
- data/lib/lutaml/model/mapping_rule.rb +4 -0
- data/lib/lutaml/model/schema/templates/simple_type.rb +247 -0
- data/lib/lutaml/model/schema/xml_compiler.rb +720 -0
- data/lib/lutaml/model/schema.rb +5 -0
- data/lib/lutaml/model/serialize.rb +24 -8
- data/lib/lutaml/model/toml_adapter/toml_rb_adapter.rb +1 -2
- data/lib/lutaml/model/type/hash.rb +11 -11
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +5 -1
- data/lib/lutaml/model/xml_adapter/xml_document.rb +11 -15
- data/lib/lutaml/model/xml_mapping.rb +4 -2
- data/lib/lutaml/model/xml_mapping_rule.rb +1 -4
- data/lib/lutaml/model.rb +1 -0
- data/spec/fixtures/xml/invalid_math_document.xml +4 -0
- data/spec/fixtures/xml/math_document_schema.xsd +56 -0
- data/spec/fixtures/xml/test_schema.xsd +53 -0
- data/spec/fixtures/xml/valid_math_document.xml +4 -0
- data/spec/lutaml/model/cdata_spec.rb +2 -2
- data/spec/lutaml/model/custom_model_spec.rb +7 -20
- data/spec/lutaml/model/key_value_mapping_spec.rb +27 -0
- data/spec/lutaml/model/map_all_spec.rb +188 -0
- data/spec/lutaml/model/mixed_content_spec.rb +15 -15
- data/spec/lutaml/model/schema/xml_compiler_spec.rb +1431 -0
- data/spec/lutaml/model/with_child_mapping_spec.rb +2 -2
- data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +52 -0
- data/spec/lutaml/model/xml_mapping_spec.rb +108 -1
- metadata +12 -2
data/lib/lutaml/model/schema.rb
CHANGED
@@ -2,6 +2,7 @@ require_relative "schema/json_schema"
|
|
2
2
|
require_relative "schema/xsd_schema"
|
3
3
|
require_relative "schema/relaxng_schema"
|
4
4
|
require_relative "schema/yaml_schema"
|
5
|
+
require_relative "schema/xml_compiler"
|
5
6
|
|
6
7
|
module Lutaml
|
7
8
|
module Model
|
@@ -21,6 +22,10 @@ module Lutaml
|
|
21
22
|
def self.to_yaml(klass, options = {})
|
22
23
|
YamlSchema.generate(klass, options)
|
23
24
|
end
|
25
|
+
|
26
|
+
def self.from_xml(xml, options = {})
|
27
|
+
XmlCompiler.to_models(xml, options)
|
28
|
+
end
|
24
29
|
end
|
25
30
|
end
|
26
31
|
end
|
@@ -269,6 +269,11 @@ module Lutaml
|
|
269
269
|
|
270
270
|
value = instance.send(name)
|
271
271
|
|
272
|
+
if rule.raw_mapping?
|
273
|
+
adapter = Lutaml::Model::Config.send(:"#{format}_adapter")
|
274
|
+
return adapter.parse(value, options)
|
275
|
+
end
|
276
|
+
|
272
277
|
attribute = attributes[name]
|
273
278
|
|
274
279
|
next hash.merge!(generate_hash_from_child_mappings(attribute, value, format, rule.root_mappings)) if rule.root_mapping?
|
@@ -450,11 +455,11 @@ module Lutaml
|
|
450
455
|
instance.mixed = mappings_for(:xml).mixed_content? || options[:mixed_content]
|
451
456
|
end
|
452
457
|
|
453
|
-
if doc["__schema_location"
|
458
|
+
if doc["attributes"]&.key?("__schema_location")
|
454
459
|
instance.schema_location = Lutaml::Model::SchemaLocation.new(
|
455
|
-
schema_location: doc["__schema_location"][:schema_location],
|
456
|
-
prefix: doc["__schema_location"][:prefix],
|
457
|
-
namespace: doc["__schema_location"][:namespace],
|
460
|
+
schema_location: doc["attributes"]["__schema_location"][:schema_location],
|
461
|
+
prefix: doc["attributes"]["__schema_location"][:prefix],
|
462
|
+
namespace: doc["attributes"]["__schema_location"][:namespace],
|
458
463
|
)
|
459
464
|
end
|
460
465
|
|
@@ -465,18 +470,17 @@ module Lutaml
|
|
465
470
|
|
466
471
|
attr = attribute_for_rule(rule)
|
467
472
|
|
468
|
-
namespaced_names = rule.namespaced_names(options[:default_namespace])
|
469
|
-
|
470
473
|
value = if rule.raw_mapping?
|
471
474
|
doc.node.inner_xml
|
472
475
|
elsif rule.content_mapping?
|
473
476
|
doc[rule.content_key]
|
474
|
-
elsif
|
475
|
-
|
477
|
+
elsif val = value_for_rule(doc, rule, options)
|
478
|
+
val
|
476
479
|
else
|
477
480
|
defaults_used << rule.to
|
478
481
|
attr&.default || rule.to_value_for(instance)
|
479
482
|
end
|
483
|
+
|
480
484
|
value = normalize_xml_value(value, rule, attr, options)
|
481
485
|
rule.deserialize(instance, value, attributes, self)
|
482
486
|
end
|
@@ -488,6 +492,15 @@ module Lutaml
|
|
488
492
|
instance
|
489
493
|
end
|
490
494
|
|
495
|
+
def value_for_rule(doc, rule, options)
|
496
|
+
rule_names = rule.namespaced_names(options[:default_namespace])
|
497
|
+
hash = rule.attribute? ? doc["attributes"] : doc["elements"]
|
498
|
+
return unless hash
|
499
|
+
|
500
|
+
value_key = rule_names.find { |name| hash.key_exist?(name) }
|
501
|
+
hash.fetch(value_key) if value_key
|
502
|
+
end
|
503
|
+
|
491
504
|
def apply_hash_mapping(doc, instance, format, options = {})
|
492
505
|
mappings = options[:mappings] || mappings_for(format).mappings
|
493
506
|
mappings.each do |rule|
|
@@ -500,6 +513,9 @@ module Lutaml
|
|
500
513
|
value = names.collect do |rule_name|
|
501
514
|
if rule.root_mapping?
|
502
515
|
doc
|
516
|
+
elsif rule.raw_mapping?
|
517
|
+
adapter = Lutaml::Model::Config.public_send(:"#{format}_adapter")
|
518
|
+
adapter.new(doc).public_send(:"to_#{format}")
|
503
519
|
elsif doc.key?(rule_name.to_s)
|
504
520
|
doc[rule_name.to_s]
|
505
521
|
elsif doc.key?(rule_name.to_sym)
|
@@ -19,18 +19,18 @@ module Lutaml
|
|
19
19
|
|
20
20
|
hash = hash.to_h if hash.is_a?(Lutaml::Model::MappingHash)
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
hash.transform_values do |value|
|
25
|
-
if value.is_a?(::Hash)
|
26
|
-
# Only process if value is a Hash
|
27
|
-
nested = normalize_hash(value)
|
28
|
-
# Only include non-text nodes in nested hashes if it's a hash
|
29
|
-
nested.is_a?(::Hash) ? nested.except("text") : nested
|
30
|
-
else
|
31
|
-
value
|
32
|
-
end
|
22
|
+
normalized_hash = hash.transform_values do |value|
|
23
|
+
normalize_value(value)
|
33
24
|
end
|
25
|
+
|
26
|
+
normalized_hash["elements"] || normalized_hash
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.normalize_value(value)
|
30
|
+
return value unless value.is_a?(::Hash)
|
31
|
+
|
32
|
+
nested = normalize_hash(value)
|
33
|
+
nested.is_a?(::Hash) ? nested.except("text") : nested
|
34
34
|
end
|
35
35
|
|
36
36
|
def self.serialize(value)
|
data/lib/lutaml/model/version.rb
CHANGED
@@ -124,7 +124,11 @@ module Lutaml
|
|
124
124
|
end
|
125
125
|
|
126
126
|
attributes = {}
|
127
|
-
|
127
|
+
|
128
|
+
# Using `attribute_nodes` instead of `attributes` because
|
129
|
+
# `attribute_nodes` handles name collisions as well
|
130
|
+
# More info: https://devdocs.io/nokogiri/nokogiri/xml/node#method-i-attributes
|
131
|
+
node.attribute_nodes.each do |attr|
|
128
132
|
name = if attr.namespace
|
129
133
|
"#{attr.namespace.prefix}:#{attr.name}"
|
130
134
|
else
|
@@ -77,26 +77,21 @@ module Lutaml
|
|
77
77
|
result.node = element
|
78
78
|
result.item_order = element.order
|
79
79
|
|
80
|
-
element.children.
|
80
|
+
element.children.each do |child|
|
81
81
|
if klass&.<= Serialize
|
82
82
|
attr = klass.attribute_for_child(child.name,
|
83
83
|
format)
|
84
84
|
end
|
85
85
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
end
|
91
|
-
|
92
|
-
hash[child.namespaced_name] = if hash[child.namespaced_name]
|
93
|
-
[hash[child.namespaced_name], value].flatten
|
94
|
-
else
|
95
|
-
value
|
96
|
-
end
|
86
|
+
next result.assign_or_append_value(child.name, child.text) if child.text?
|
87
|
+
|
88
|
+
result["elements"] ||= Lutaml::Model::MappingHash.new
|
89
|
+
result["elements"].assign_or_append_value(child.namespaced_name, parse_element(child, attr&.type || klass, format))
|
97
90
|
end
|
98
91
|
|
99
|
-
result
|
92
|
+
result["attributes"] = attributes_hash(element) if element.attributes&.any?
|
93
|
+
|
94
|
+
result
|
100
95
|
end
|
101
96
|
|
102
97
|
def attributes_hash(element)
|
@@ -167,8 +162,9 @@ module Lutaml
|
|
167
162
|
def add_value(xml, value, attribute, cdata: false)
|
168
163
|
if !value.nil?
|
169
164
|
serialized_value = attribute.type.serialize(value)
|
170
|
-
|
171
|
-
|
165
|
+
if attribute.raw?
|
166
|
+
xml.add_xml_fragment(xml, value)
|
167
|
+
elsif attribute.type == Lutaml::Model::Type::Hash
|
172
168
|
serialized_value.each do |key, val|
|
173
169
|
xml.create_and_add_element(key) do |element|
|
174
170
|
element.text(val)
|
@@ -149,10 +149,10 @@ module Lutaml
|
|
149
149
|
prefix: (prefix_set = false
|
150
150
|
nil)
|
151
151
|
)
|
152
|
-
validate!(
|
152
|
+
validate!(Constants::RAW_MAPPING_KEY, to, with, type: TYPES[:all_content])
|
153
153
|
|
154
154
|
rule = XmlMappingRule.new(
|
155
|
-
|
155
|
+
Constants::RAW_MAPPING_KEY,
|
156
156
|
to: to,
|
157
157
|
render_nil: render_nil,
|
158
158
|
render_default: render_default,
|
@@ -168,6 +168,8 @@ module Lutaml
|
|
168
168
|
@raw_mapping = rule
|
169
169
|
end
|
170
170
|
|
171
|
+
alias map_all_content map_all
|
172
|
+
|
171
173
|
def validate!(key, to, with, type: nil)
|
172
174
|
validate_mappings!(type)
|
173
175
|
|
@@ -59,10 +59,6 @@ module Lutaml
|
|
59
59
|
name.nil?
|
60
60
|
end
|
61
61
|
|
62
|
-
def raw_mapping?
|
63
|
-
name == "__raw_mapping"
|
64
|
-
end
|
65
|
-
|
66
62
|
def content_key
|
67
63
|
cdata ? "#cdata-section" : "text"
|
68
64
|
end
|
@@ -111,6 +107,7 @@ module Lutaml
|
|
111
107
|
prefix: prefix.dup,
|
112
108
|
mixed_content: mixed_content,
|
113
109
|
namespace_set: namespace_set?,
|
110
|
+
attribute: attribute,
|
114
111
|
prefix_set: prefix_set?,
|
115
112
|
default_namespace: default_namespace.dup,
|
116
113
|
)
|
data/lib/lutaml/model.rb
CHANGED
@@ -0,0 +1,56 @@
|
|
1
|
+
<schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.openxmlformats.org/officeDocument/2006/math">
|
2
|
+
<xs:simpleType name="StringDatatype">
|
3
|
+
<xs:annotation>
|
4
|
+
<xs:documentation>
|
5
|
+
A string data type to be used for the example.
|
6
|
+
</xs:documentation>
|
7
|
+
</xs:annotation>
|
8
|
+
<xs:restriction base="xs:string"></xs:restriction>
|
9
|
+
</xs:simpleType>
|
10
|
+
|
11
|
+
<xs:simpleType name="IPV4AddressDatatype">
|
12
|
+
<xs:annotation>
|
13
|
+
<xs:documentation>
|
14
|
+
An Internet Protocol version 4 address represented using dotted-quad syntax as defined in section 3.2 of RFC2673.
|
15
|
+
</xs:documentation>
|
16
|
+
</xs:annotation>
|
17
|
+
<xs:restriction base="StringDatatype">
|
18
|
+
<xs:pattern value="((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9]).){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])" />
|
19
|
+
</xs:restriction>
|
20
|
+
</xs:simpleType>
|
21
|
+
|
22
|
+
<xs:simpleType name="whiteSpaces">
|
23
|
+
<xs:annotation>
|
24
|
+
<xs:documentation>
|
25
|
+
A non-empty string of Unicode characters with leading and trailing whitespace
|
26
|
+
disallowed. Whitespace is: U+9, U+10, U+32 or [ \n\t]+
|
27
|
+
</xs:documentation>
|
28
|
+
</xs:annotation>
|
29
|
+
<xs:restriction base="xs:string">
|
30
|
+
<xs:annotation>
|
31
|
+
<xs:documentation>
|
32
|
+
The 'string' datatype restricts the XSD type by prohibiting leading
|
33
|
+
and trailing whitespace, and something (not only whitespace) is required.
|
34
|
+
</xs:documentation>
|
35
|
+
</xs:annotation>
|
36
|
+
<xs:pattern value="\S(.*\S)?">
|
37
|
+
<xs:annotation>
|
38
|
+
<xs:documentation>
|
39
|
+
This pattern ensures that leading and trailing whitespace is
|
40
|
+
disallowed. This helps to even the user experience between implementations
|
41
|
+
related to whitespace.
|
42
|
+
</xs:documentation>
|
43
|
+
</xs:annotation>
|
44
|
+
</xs:pattern>
|
45
|
+
</xs:restriction>
|
46
|
+
</xs:simpleType>
|
47
|
+
|
48
|
+
<xs:complexType name="MathDocument">
|
49
|
+
<xs:choice>
|
50
|
+
<xs:element name="Title" type="whiteSpaces"/>
|
51
|
+
<xs:element name="IPV4Address" type="IPV4AddressDatatype"/>
|
52
|
+
</xs:choice>
|
53
|
+
</xs:complexType>
|
54
|
+
|
55
|
+
<xs:element name="MathDocument" type="MathDocument"/>
|
56
|
+
</schema>
|
@@ -0,0 +1,53 @@
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
2
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" attributeFormDefault="qualified" blockDefault="#all" targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
3
|
+
<xsd:simpleType name="ST_Integer255">
|
4
|
+
<xsd:annotation>
|
5
|
+
<xsd:documentation>Integer value (1 to 255)</xsd:documentation>
|
6
|
+
</xsd:annotation>
|
7
|
+
<xsd:restriction base="xsd:integer">
|
8
|
+
<xsd:minInclusive value="1" />
|
9
|
+
<xsd:maxInclusive value="255" />
|
10
|
+
</xsd:restriction>
|
11
|
+
</xsd:simpleType>
|
12
|
+
<xsd:complexType name="CT_Integer255">
|
13
|
+
<xsd:attribute name="val" type="ST_Integer255" use="required">
|
14
|
+
<xsd:annotation>
|
15
|
+
<xsd:documentation>Value</xsd:documentation>
|
16
|
+
</xsd:annotation>
|
17
|
+
</xsd:attribute>
|
18
|
+
</xsd:complexType>
|
19
|
+
<xsd:simpleType name="ST_Integer2">
|
20
|
+
<xsd:annotation>
|
21
|
+
<xsd:documentation>Integer value (-2 to 2)</xsd:documentation>
|
22
|
+
</xsd:annotation>
|
23
|
+
<xsd:restriction base="xsd:integer">
|
24
|
+
<xsd:minInclusive value="-2" />
|
25
|
+
<xsd:maxInclusive value="2" />
|
26
|
+
</xsd:restriction>
|
27
|
+
</xsd:simpleType>
|
28
|
+
<xsd:complexType name="CT_Integer2">
|
29
|
+
<xsd:attribute name="val" type="ST_Integer2" use="required">
|
30
|
+
<xsd:annotation>
|
31
|
+
<xsd:documentation>Value</xsd:documentation>
|
32
|
+
</xsd:annotation>
|
33
|
+
</xsd:attribute>
|
34
|
+
</xsd:complexType>
|
35
|
+
<xsd:element name="MathTest" type="CT_Integer255">
|
36
|
+
<xsd:annotation>
|
37
|
+
<xsd:documentation>Main Test class</xsd:documentation>
|
38
|
+
</xsd:annotation>
|
39
|
+
</xsd:element>
|
40
|
+
<xsd:element name="MathTest1" type="CT_Integer2">
|
41
|
+
<xsd:annotation>
|
42
|
+
<xsd:documentation>Main Test class</xsd:documentation>
|
43
|
+
</xsd:annotation>
|
44
|
+
</xsd:element>
|
45
|
+
<xsd:complexType name="CT_MathTest">
|
46
|
+
<xsd:group>
|
47
|
+
<xsd:sequence>
|
48
|
+
<xsd:element ref="MathTest" />
|
49
|
+
<xsd:element ref="MathTest1" />
|
50
|
+
</xsd:sequence>
|
51
|
+
</xsd:group>
|
52
|
+
</xsd:complexType>
|
53
|
+
</xsd:schema>
|
@@ -118,8 +118,8 @@ module CDATA
|
|
118
118
|
def child_from_xml(model, value)
|
119
119
|
model.child_mapper ||= CustomModelChild.new
|
120
120
|
|
121
|
-
model.child_mapper.street = value["street"].text
|
122
|
-
model.child_mapper.city = value["city"].text
|
121
|
+
model.child_mapper.street = value["elements"]["street"].text
|
122
|
+
model.child_mapper.city = value["elements"]["city"].text
|
123
123
|
end
|
124
124
|
end
|
125
125
|
|
@@ -56,8 +56,8 @@ class CustomModelParentMapper < Lutaml::Model::Serializable
|
|
56
56
|
def child_from_xml(model, value)
|
57
57
|
model.child_mapper ||= CustomModelChild.new
|
58
58
|
|
59
|
-
model.child_mapper.street = value["street"].text
|
60
|
-
model.child_mapper.city = value["city"].text
|
59
|
+
model.child_mapper.street = value["elements"]["street"].text
|
60
|
+
model.child_mapper.city = value["elements"]["city"].text
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
@@ -126,10 +126,10 @@ module CustomModelSpecs
|
|
126
126
|
|
127
127
|
def bibdata_from_xml(model, value)
|
128
128
|
model.bibdata = BibliographicItem.new(
|
129
|
-
"type" => value["type"],
|
130
|
-
"title" => value["title"],
|
131
|
-
"language" => value["title"]["language"],
|
132
|
-
"schema_version" => value["schema-version"],
|
129
|
+
"type" => value["attributes"]["type"],
|
130
|
+
"title" => value["elements"]["title"],
|
131
|
+
"language" => value["elements"]["title"]["attributes"]["language"],
|
132
|
+
"schema_version" => value["attributes"]["schema-version"],
|
133
133
|
)
|
134
134
|
end
|
135
135
|
|
@@ -410,22 +410,9 @@ RSpec.describe "CustomModel" do
|
|
410
410
|
</MixedWithNestedContent>
|
411
411
|
XML
|
412
412
|
|
413
|
-
expected_xml = <<~XML
|
414
|
-
<MixedWithNestedContent>
|
415
|
-
<street>
|
416
|
-
A <p>b</p> B <p>c</p> C
|
417
|
-
</street>
|
418
|
-
<bibdata type="collection" schema-version="v1.2.8">
|
419
|
-
<title language="en">
|
420
|
-
JCGM Collection 1
|
421
|
-
</title>
|
422
|
-
</bibdata>
|
423
|
-
</MixedWithNestedContent>
|
424
|
-
XML
|
425
|
-
|
426
413
|
bibdata = CustomModelSpecs::MixedWithNestedContent.from_xml(xml)
|
427
414
|
|
428
|
-
expect(bibdata.to_xml).to be_equivalent_to(
|
415
|
+
expect(bibdata.to_xml).to be_equivalent_to(xml)
|
429
416
|
end
|
430
417
|
end
|
431
418
|
end
|
@@ -83,4 +83,31 @@ RSpec.describe Lutaml::Model::KeyValueMapping do
|
|
83
83
|
expect(m.custom_methods.object_id).not_to eq(dup_m.custom_methods.object_id)
|
84
84
|
end
|
85
85
|
end
|
86
|
+
|
87
|
+
context "with map_all option" do
|
88
|
+
before do
|
89
|
+
mapping.map_all(
|
90
|
+
render_nil: true,
|
91
|
+
delegate: :container,
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "handles JSON mapping" do
|
96
|
+
expect(mapping.mappings[0].render_nil).to be true
|
97
|
+
expect(mapping.mappings[0].delegate).to eq(:container)
|
98
|
+
expect(mapping.mappings[0].raw_mapping?).to be true
|
99
|
+
end
|
100
|
+
|
101
|
+
it "handles YAML mapping" do
|
102
|
+
expect(mapping.mappings[0].render_nil).to be true
|
103
|
+
expect(mapping.mappings[0].delegate).to eq(:container)
|
104
|
+
expect(mapping.mappings[0].raw_mapping?).to be true
|
105
|
+
end
|
106
|
+
|
107
|
+
it "handles TOML mapping" do
|
108
|
+
expect(mapping.mappings[0].render_nil).to be true
|
109
|
+
expect(mapping.mappings[0].delegate).to eq(:container)
|
110
|
+
expect(mapping.mappings[0].raw_mapping?).to be true
|
111
|
+
end
|
112
|
+
end
|
86
113
|
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module MapAllSpec
|
4
|
+
class Document < Lutaml::Model::Serializable
|
5
|
+
attribute :content, :string
|
6
|
+
|
7
|
+
xml do
|
8
|
+
root "document"
|
9
|
+
map_all to: :content
|
10
|
+
end
|
11
|
+
|
12
|
+
json do
|
13
|
+
map_all to: :content
|
14
|
+
end
|
15
|
+
|
16
|
+
yaml do
|
17
|
+
map_all to: :content
|
18
|
+
end
|
19
|
+
|
20
|
+
toml do
|
21
|
+
map_all to: :content
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class InvalidDocument < Lutaml::Model::Serializable
|
26
|
+
attribute :content, :string
|
27
|
+
attribute :title, :string
|
28
|
+
|
29
|
+
json do
|
30
|
+
map_all to: :content
|
31
|
+
end
|
32
|
+
|
33
|
+
yaml do
|
34
|
+
map_element "title", to: :title
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
RSpec.describe "MapAll" do
|
39
|
+
describe "XML serialization" do
|
40
|
+
let(:xml_content) do
|
41
|
+
<<~XML
|
42
|
+
<document>
|
43
|
+
Content with <b>tags</b> and <i>formatting</i>.
|
44
|
+
<metadata>
|
45
|
+
<author>John Doe</author>
|
46
|
+
<date>2024-01-15</date>
|
47
|
+
</metadata>
|
48
|
+
</document>
|
49
|
+
XML
|
50
|
+
end
|
51
|
+
|
52
|
+
let(:sub_xml_content) do
|
53
|
+
<<~XML
|
54
|
+
Content with <b>tags</b> and <i>formatting</i>.
|
55
|
+
<metadata>
|
56
|
+
<author>John Doe</author>
|
57
|
+
<date>2024-01-15</date>
|
58
|
+
</metadata>
|
59
|
+
XML
|
60
|
+
end
|
61
|
+
|
62
|
+
it "captures all XML content" do
|
63
|
+
doc = Document.from_xml(xml_content)
|
64
|
+
expect(doc.content).to be_equivalent_to(sub_xml_content)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "preserves XML content through round trip" do
|
68
|
+
doc = Document.from_xml(xml_content)
|
69
|
+
regenerated = doc.to_xml
|
70
|
+
expect(regenerated).to be_equivalent_to(xml_content)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "JSON serialization" do
|
75
|
+
let(:json_content) do
|
76
|
+
{
|
77
|
+
"sections" => [
|
78
|
+
{ "title" => "Introduction", "text" => "Chapter 1" },
|
79
|
+
{ "title" => "Conclusion", "text" => "Final chapter" },
|
80
|
+
],
|
81
|
+
"metadata" => {
|
82
|
+
"author" => "John Doe",
|
83
|
+
"date" => "2024-01-15",
|
84
|
+
},
|
85
|
+
}.to_json
|
86
|
+
end
|
87
|
+
|
88
|
+
it "captures all JSON content" do
|
89
|
+
doc = Document.from_json(json_content)
|
90
|
+
parsed = JSON.parse(doc.content)
|
91
|
+
expect(parsed["sections"].first["title"]).to eq("Introduction")
|
92
|
+
expect(parsed["metadata"]["author"]).to eq("John Doe")
|
93
|
+
end
|
94
|
+
|
95
|
+
it "preserves JSON content through round trip" do
|
96
|
+
doc = Document.from_json(json_content)
|
97
|
+
regenerated = doc.to_json
|
98
|
+
expect(JSON.parse(regenerated)).to eq(JSON.parse(json_content))
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe "YAML serialization" do
|
103
|
+
let(:yaml_content) do
|
104
|
+
<<~YAML
|
105
|
+
sections:
|
106
|
+
- title: Introduction
|
107
|
+
text: Chapter 1
|
108
|
+
- title: Conclusion
|
109
|
+
text: Final chapter
|
110
|
+
metadata:
|
111
|
+
author: John Doe
|
112
|
+
date: 2024-01-15
|
113
|
+
YAML
|
114
|
+
end
|
115
|
+
|
116
|
+
it "captures all YAML content" do
|
117
|
+
doc = Document.from_yaml(yaml_content)
|
118
|
+
parsed = YAML.safe_load(doc.content, permitted_classes: [Date])
|
119
|
+
expect(parsed["sections"].first["title"]).to eq("Introduction")
|
120
|
+
expect(parsed["metadata"]["author"]).to eq("John Doe")
|
121
|
+
end
|
122
|
+
|
123
|
+
it "preserves YAML content through round trip" do
|
124
|
+
doc = Document.from_yaml(yaml_content)
|
125
|
+
regenerated = doc.to_yaml
|
126
|
+
expect(YAML.safe_load(regenerated, permitted_classes: [Date])).to eq(YAML.safe_load(yaml_content, permitted_classes: [Date]))
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe "TOML serialization" do
|
131
|
+
let(:toml_content) do
|
132
|
+
<<~TOML
|
133
|
+
title = "Document Title"
|
134
|
+
|
135
|
+
[metadata]
|
136
|
+
author = "John Doe"
|
137
|
+
date = "2024-01-15"
|
138
|
+
|
139
|
+
[[sections]]
|
140
|
+
title = "Introduction"
|
141
|
+
text = "Chapter 1"
|
142
|
+
|
143
|
+
[[sections]]
|
144
|
+
title = "Conclusion"
|
145
|
+
text = "Final chapter"
|
146
|
+
TOML
|
147
|
+
end
|
148
|
+
|
149
|
+
it "captures all TOML content" do
|
150
|
+
doc = Document.from_toml(toml_content)
|
151
|
+
parsed = TomlRB.parse(doc.content)
|
152
|
+
expect(parsed["sections"].first["title"]).to eq("Introduction")
|
153
|
+
expect(parsed["metadata"]["author"]).to eq("John Doe")
|
154
|
+
end
|
155
|
+
|
156
|
+
it "preserves TOML content through round trip" do
|
157
|
+
doc = Document.from_toml(toml_content)
|
158
|
+
regenerated = doc.to_toml
|
159
|
+
expect(TomlRB.parse(regenerated)).to eq(TomlRB.parse(toml_content))
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
describe "invalid mapping combinations" do
|
164
|
+
it "raises error when combining map_all with other mappings" do
|
165
|
+
expect do
|
166
|
+
InvalidDocument.json do
|
167
|
+
map_element "title", to: :title
|
168
|
+
end
|
169
|
+
end.to raise_error(
|
170
|
+
StandardError,
|
171
|
+
"map_all is not allowed with other mappings",
|
172
|
+
)
|
173
|
+
end
|
174
|
+
|
175
|
+
it "raises error when combining other mappings are used with map_all" do
|
176
|
+
expect do
|
177
|
+
InvalidDocument.yaml do
|
178
|
+
map_element "title", to: :title
|
179
|
+
map_all to: :content
|
180
|
+
end
|
181
|
+
end.to raise_error(
|
182
|
+
StandardError,
|
183
|
+
"map_all is not allowed with other mappings",
|
184
|
+
)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|