lutaml-model 0.5.3 → 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +2 -0
  3. data/.rubocop_todo.yml +39 -13
  4. data/Gemfile +1 -0
  5. data/README.adoc +396 -23
  6. data/lib/lutaml/model/constants.rb +7 -0
  7. data/lib/lutaml/model/error/type/invalid_value_error.rb +19 -0
  8. data/lib/lutaml/model/error.rb +1 -0
  9. data/lib/lutaml/model/key_value_mapping.rb +31 -2
  10. data/lib/lutaml/model/mapping_hash.rb +8 -0
  11. data/lib/lutaml/model/mapping_rule.rb +4 -0
  12. data/lib/lutaml/model/schema/templates/simple_type.rb +247 -0
  13. data/lib/lutaml/model/schema/xml_compiler.rb +720 -0
  14. data/lib/lutaml/model/schema.rb +5 -0
  15. data/lib/lutaml/model/serialize.rb +24 -8
  16. data/lib/lutaml/model/toml_adapter/toml_rb_adapter.rb +1 -2
  17. data/lib/lutaml/model/type/hash.rb +11 -11
  18. data/lib/lutaml/model/version.rb +1 -1
  19. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +5 -1
  20. data/lib/lutaml/model/xml_adapter/xml_document.rb +11 -15
  21. data/lib/lutaml/model/xml_mapping.rb +4 -2
  22. data/lib/lutaml/model/xml_mapping_rule.rb +1 -4
  23. data/lib/lutaml/model.rb +1 -0
  24. data/spec/fixtures/xml/invalid_math_document.xml +4 -0
  25. data/spec/fixtures/xml/math_document_schema.xsd +56 -0
  26. data/spec/fixtures/xml/test_schema.xsd +53 -0
  27. data/spec/fixtures/xml/valid_math_document.xml +4 -0
  28. data/spec/lutaml/model/cdata_spec.rb +2 -2
  29. data/spec/lutaml/model/custom_model_spec.rb +7 -20
  30. data/spec/lutaml/model/key_value_mapping_spec.rb +27 -0
  31. data/spec/lutaml/model/map_all_spec.rb +188 -0
  32. data/spec/lutaml/model/mixed_content_spec.rb +15 -15
  33. data/spec/lutaml/model/schema/xml_compiler_spec.rb +1431 -0
  34. data/spec/lutaml/model/with_child_mapping_spec.rb +2 -2
  35. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +52 -0
  36. data/spec/lutaml/model/xml_mapping_spec.rb +108 -1
  37. metadata +12 -2
@@ -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 key = (namespaced_names & doc.keys).first
475
- doc[key]
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)
@@ -6,8 +6,7 @@ module Lutaml
6
6
  module TomlAdapter
7
7
  class TomlRbAdapter < TomlDocument
8
8
  def self.parse(toml, _options = {})
9
- data = TomlRB.parse(toml)
10
- new(data)
9
+ TomlRB.parse(toml)
11
10
  end
12
11
 
13
12
  def to_toml(*)
@@ -19,18 +19,18 @@ module Lutaml
19
19
 
20
20
  hash = hash.to_h if hash.is_a?(Lutaml::Model::MappingHash)
21
21
 
22
- hash = hash.except("text")
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)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.5.3"
5
+ VERSION = "0.5.4"
6
6
  end
7
7
  end
@@ -124,7 +124,11 @@ module Lutaml
124
124
  end
125
125
 
126
126
  attributes = {}
127
- node.attributes.transform_values do |attr|
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.each_with_object(result) do |child, hash|
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
- value = if child.text?
87
- child.text
88
- else
89
- parse_element(child, attr&.type || klass, format)
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.merge(attributes_hash(element))
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
- if attribute.type == Lutaml::Model::Type::Hash
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!("__raw_mapping", to, with, type: TYPES[:all_content])
152
+ validate!(Constants::RAW_MAPPING_KEY, to, with, type: TYPES[:all_content])
153
153
 
154
154
  rule = XmlMappingRule.new(
155
- "__raw_mapping",
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
@@ -10,6 +10,7 @@ require_relative "model/yaml_adapter/standard_yaml_adapter"
10
10
  require_relative "model/xml_adapter"
11
11
  require_relative "model/toml_adapter"
12
12
  require_relative "model/error"
13
+ require_relative "model/constants"
13
14
 
14
15
  module Lutaml
15
16
  module Model
@@ -0,0 +1,4 @@
1
+ <MathDocument>
2
+ <Title>Example Title</Title>
3
+ <IPV4Address>Example Address</IPV4Address>
4
+ </MathDocument>
@@ -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>
@@ -0,0 +1,4 @@
1
+ <MathDocument>
2
+ <Title>Example Title</Title>
3
+ <IPV4Address>192.168.1.1</IPV4Address>
4
+ </MathDocument>
@@ -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 &lt;p&gt;b&lt;/p&gt; B &lt;p&gt;c&lt;/p&gt; 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(expected_xml)
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