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.
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