lutaml-model 0.5.4 → 0.6.1

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +58 -21
  3. data/Gemfile +1 -0
  4. data/README.adoc +1112 -264
  5. data/lib/lutaml/model/attribute.rb +37 -15
  6. data/lib/lutaml/model/choice.rb +56 -0
  7. data/lib/lutaml/model/config.rb +1 -0
  8. data/lib/lutaml/model/error/choice_lower_bound_error.rb +9 -0
  9. data/lib/lutaml/model/error/choice_upper_bound_error.rb +9 -0
  10. data/lib/lutaml/model/error/import_model_with_root_error.rb +9 -0
  11. data/lib/lutaml/model/error/incorrect_sequence_error.rb +9 -0
  12. data/lib/lutaml/model/error/invalid_choice_range_error.rb +20 -0
  13. data/lib/lutaml/model/error/no_root_mapping_error.rb +9 -0
  14. data/lib/lutaml/model/error/no_root_namespace_error.rb +9 -0
  15. data/lib/lutaml/model/error/unknown_sequence_mapping_error.rb +9 -0
  16. data/lib/lutaml/model/error.rb +8 -0
  17. data/lib/lutaml/model/json_adapter/standard_json_adapter.rb +6 -1
  18. data/lib/lutaml/model/key_value_mapping.rb +3 -1
  19. data/lib/lutaml/model/key_value_mapping_rule.rb +4 -2
  20. data/lib/lutaml/model/liquefiable.rb +59 -0
  21. data/lib/lutaml/model/mapping_hash.rb +1 -1
  22. data/lib/lutaml/model/mapping_rule.rb +15 -2
  23. data/lib/lutaml/model/schema/xml_compiler.rb +68 -26
  24. data/lib/lutaml/model/schema_location.rb +7 -0
  25. data/lib/lutaml/model/sequence.rb +71 -0
  26. data/lib/lutaml/model/serialize.rb +126 -38
  27. data/lib/lutaml/model/type/decimal.rb +0 -4
  28. data/lib/lutaml/model/type/time.rb +3 -3
  29. data/lib/lutaml/model/utils.rb +19 -15
  30. data/lib/lutaml/model/validation.rb +12 -1
  31. data/lib/lutaml/model/version.rb +1 -1
  32. data/lib/lutaml/model/xml_adapter/builder/oga.rb +10 -7
  33. data/lib/lutaml/model/xml_adapter/builder/ox.rb +20 -13
  34. data/lib/lutaml/model/xml_adapter/element.rb +32 -0
  35. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +8 -8
  36. data/lib/lutaml/model/xml_adapter/oga/element.rb +14 -13
  37. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +86 -19
  38. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +19 -15
  39. data/lib/lutaml/model/xml_adapter/xml_document.rb +74 -13
  40. data/lib/lutaml/model/xml_adapter/xml_element.rb +57 -3
  41. data/lib/lutaml/model/xml_mapping.rb +49 -7
  42. data/lib/lutaml/model/xml_mapping_rule.rb +8 -3
  43. data/lib/lutaml/model.rb +1 -0
  44. data/lutaml-model.gemspec +5 -0
  45. data/spec/benchmarks/xml_parsing_benchmark_spec.rb +75 -0
  46. data/spec/ceramic_spec.rb +39 -0
  47. data/spec/fixtures/ceramic.rb +23 -0
  48. data/spec/fixtures/xml/address_example_260.xsd +9 -0
  49. data/spec/fixtures/xml/user.xsd +10 -0
  50. data/spec/lutaml/model/cdata_spec.rb +4 -5
  51. data/spec/lutaml/model/choice_spec.rb +168 -0
  52. data/spec/lutaml/model/collection_spec.rb +1 -1
  53. data/spec/lutaml/model/custom_model_spec.rb +55 -8
  54. data/spec/lutaml/model/custom_serialization_spec.rb +74 -2
  55. data/spec/lutaml/model/defaults_spec.rb +3 -1
  56. data/spec/lutaml/model/delegation_spec.rb +7 -5
  57. data/spec/lutaml/model/enum_spec.rb +35 -0
  58. data/spec/lutaml/model/group_spec.rb +160 -0
  59. data/spec/lutaml/model/inheritance_spec.rb +25 -0
  60. data/spec/lutaml/model/liquefiable_spec.rb +121 -0
  61. data/spec/lutaml/model/mixed_content_spec.rb +80 -41
  62. data/spec/lutaml/model/multiple_mapping_spec.rb +22 -10
  63. data/spec/lutaml/model/schema/xml_compiler_spec.rb +218 -25
  64. data/spec/lutaml/model/sequence_spec.rb +216 -0
  65. data/spec/lutaml/model/transformation_spec.rb +230 -0
  66. data/spec/lutaml/model/type_spec.rb +138 -31
  67. data/spec/lutaml/model/utils_spec.rb +32 -0
  68. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +11 -7
  69. data/spec/lutaml/model/xml_mapping_rule_spec.rb +51 -0
  70. data/spec/lutaml/model/xml_mapping_spec.rb +167 -112
  71. metadata +67 -2
data/lutaml-model.gemspec CHANGED
@@ -30,6 +30,11 @@ Gem::Specification.new do |spec|
30
30
  end
31
31
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
32
 
33
+ # TODO: remove once https://github.com/Shopify/liquid/issues/1772 is fixed
34
+ # needed for liquid with ruby 3.4
35
+ spec.add_dependency "base64"
36
+ spec.add_dependency "liquid", "~> 5"
37
+ spec.add_dependency "moxml", ">= 0.1.2"
33
38
  spec.add_dependency "thor"
34
39
  spec.metadata["rubygems_mfa_required"] = "true"
35
40
  end
@@ -0,0 +1,75 @@
1
+ require "benchmark"
2
+ require "benchmark/ips"
3
+ require "lutaml/model"
4
+ require "lutaml/model/xml_adapter/oga_adapter"
5
+
6
+ RSpec.describe "LutaML Model Performance" do
7
+ after do
8
+ Lutaml::Model::Config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
9
+ end
10
+
11
+ let(:large_xml) do
12
+ xml = "<root>\n"
13
+ 1000.times do |i|
14
+ xml += "<item id='#{i}'><name>Test #{i}</name><value>#{i}</value></item>\n"
15
+ end
16
+ xml += "</root>"
17
+ xml
18
+ end
19
+
20
+ class DeserializerItem < Lutaml::Model::Serializable
21
+ attribute :id, :integer
22
+ attribute :name, :string
23
+ attribute :value, :integer
24
+
25
+ xml do
26
+ map_attribute "id", to: :id
27
+ map_element "value", to: :value
28
+ map_element "name", to: :name
29
+ map_element "value", to: :value
30
+ end
31
+ end
32
+
33
+ class Deserializer < Lutaml::Model::Serializable
34
+ attribute :item, DeserializerItem, collection: true
35
+
36
+ xml do
37
+ root "root"
38
+ map_element "item", to: :item
39
+ end
40
+ end
41
+
42
+ it "measures parsing performance across adapters" do
43
+ report = Benchmark.ips do |x|
44
+ x.config(time: 5, warmup: 2)
45
+
46
+ x.report("Nokogiri Adapter") do
47
+ Deserializer.from_xml(large_xml)
48
+ end
49
+
50
+ x.report("Ox Adapter") do
51
+ Lutaml::Model::Config.xml_adapter = Lutaml::Model::XmlAdapter::OxAdapter
52
+ Deserializer.from_xml(large_xml)
53
+ end
54
+
55
+ x.report("Oga Adapter") do
56
+ Lutaml::Model::Config.xml_adapter = Lutaml::Model::XmlAdapter::OgaAdapter
57
+ Deserializer.from_xml(large_xml)
58
+ end
59
+
60
+ x.compare!
61
+ end
62
+
63
+ thresholds = {
64
+ "Nokogiri Adapter" => 5,
65
+ "Ox Adapter" => 15,
66
+ "Oga Adapter" => 5,
67
+ }
68
+
69
+ report.entries.each do |entry|
70
+ puts "#{entry.label} performance: #{entry.ips.round(2)} ips"
71
+ expect(entry.ips).to be >= thresholds[entry.label],
72
+ "#{entry.label} performance below threshold: got #{entry.ips.round(2)} ips, expected >= #{thresholds[entry.label]} ips"
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,39 @@
1
+ require "spec_helper"
2
+ require_relative "fixtures/ceramic"
3
+
4
+ RSpec.describe Ceramic do
5
+ xml = <<~XML
6
+ <ceramic kilnFiringTimeAttribute="2012-04-07T01:51:37.112+02:00">
7
+ <kilnFiringTime>2012-04-07T01:51:37.112+02:00</kilnFiringTime>
8
+ </ceramic>
9
+ XML
10
+
11
+ it "deserializes from XML with high-precision date-time" do
12
+ ceramic = described_class.from_xml(xml)
13
+ expect(ceramic.kiln_firing_time.strftime("%Y-%m-%dT%H:%M:%S.%L%:z")).to eq("2012-04-07T01:51:37.112+02:00")
14
+ end
15
+
16
+ it "serializes to XML with high-precision date-time" do
17
+ ceramic = described_class.from_xml(xml)
18
+ expect(ceramic.to_xml).to be_equivalent_to(xml)
19
+ end
20
+
21
+ it "deserializes from JSON with high-precision date-time" do
22
+ json = {
23
+ kilnFiringTime: "2012-04-07T01:51:37+02:00",
24
+ }.to_json
25
+
26
+ ceramic_from_json = described_class.from_json(json)
27
+ expect(ceramic_from_json.kiln_firing_time).to eq(DateTime.new(2012, 4, 7, 1, 51, 37, "+02:00"))
28
+ end
29
+
30
+ it "serializes to JSON with high-precision date-time" do
31
+ ceramic = described_class.from_xml(xml)
32
+ expected_json = {
33
+ kilnFiringTime: "2012-04-07T01:51:37+02:00",
34
+ kilnFiringTimeAttribute: "2012-04-07T01:51:37+02:00",
35
+ }.to_json
36
+
37
+ expect(ceramic.to_json).to eq(expected_json)
38
+ end
39
+ end
@@ -0,0 +1,23 @@
1
+ require "lutaml/model"
2
+
3
+ class HighPrecisionDateTime < Lutaml::Model::Type::DateTime
4
+ def to_xml
5
+ value.strftime("%Y-%m-%dT%H:%M:%S.%L%:z")
6
+ end
7
+ end
8
+
9
+ class Ceramic < Lutaml::Model::Serializable
10
+ attribute :kiln_firing_time, HighPrecisionDateTime
11
+ attribute :kiln_firing_time_attribute, HighPrecisionDateTime
12
+
13
+ xml do
14
+ root "ceramic"
15
+ map_element "kilnFiringTime", to: :kiln_firing_time
16
+ map_attribute "kilnFiringTimeAttribute", to: :kiln_firing_time_attribute
17
+ end
18
+
19
+ json do
20
+ map "kilnFiringTime", to: :kiln_firing_time
21
+ map "kilnFiringTimeAttribute", to: :kiln_firing_time_attribute
22
+ end
23
+ end
@@ -0,0 +1,9 @@
1
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" attributeFormDefault="qualified">
2
+ <xs:element name="Address" type="Address"/>
3
+ <xs:complexType name="Address" mixed="true">
4
+ <xs:sequence>
5
+ <xs:element name="City" type="xs:string" minOccurs="0"/>
6
+ <xs:element name="ZIP" type="xs:string" minOccurs="0"/>
7
+ </xs:sequence>
8
+ </xs:complexType>
9
+ </xs:schema>
@@ -0,0 +1,10 @@
1
+ <schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.openxmlformats.org/officeDocument/2006/math">
2
+ <xsd:complexType name="User">
3
+ <xsd:sequence>
4
+ <xsd:element name="id" type="xsd:nonNegativeInteger" minOccurs="1" />
5
+ <xsd:element name="age" type="xsd:unsignedLong" minOccurs="0" />
6
+ <xsd:element name="token" type="xsd:token" minOccurs="0" />
7
+ </xsd:sequence>
8
+ </xsd:complexType>
9
+ <xsd:element name="User" type="User"/>
10
+ </schema>
@@ -44,7 +44,7 @@ module CDATA
44
44
  end
45
45
 
46
46
  def house_from_xml(model, node)
47
- model.house = node
47
+ model.house = node.children.first.text
48
48
  end
49
49
 
50
50
  def house_to_xml(model, _parent, doc)
@@ -54,7 +54,7 @@ module CDATA
54
54
  end
55
55
 
56
56
  def city_from_xml(model, node)
57
- model.city = node
57
+ model.city = node.children.first.text
58
58
  end
59
59
 
60
60
  def city_to_xml(model, _parent, doc)
@@ -117,9 +117,8 @@ module CDATA
117
117
 
118
118
  def child_from_xml(model, value)
119
119
  model.child_mapper ||= CustomModelChild.new
120
-
121
- model.child_mapper.street = value["elements"]["street"].text
122
- model.child_mapper.city = value["elements"]["city"].text
120
+ model.child_mapper.street = value.find_child_by_name("street").text
121
+ model.child_mapper.city = value.find_child_by_name("city").text
123
122
  end
124
123
  end
125
124
 
@@ -0,0 +1,168 @@
1
+ require "spec_helper"
2
+ require "lutaml/model"
3
+
4
+ module ChoiceSpec
5
+ class CandidateType < Lutaml::Model::Serializable
6
+ attribute :id, :integer
7
+ attribute :name, :string
8
+
9
+ xml do
10
+ map_attribute "id", to: :id
11
+ map_attribute "name", to: :name
12
+ end
13
+ end
14
+
15
+ class DocumentState < Lutaml::Model::Serializable
16
+ choice(min: 1, max: 3) do
17
+ attribute :signed, :boolean
18
+ attribute :unsigned, :boolean
19
+ attribute :watermarked, :boolean
20
+ attribute :encrypted, :boolean
21
+ end
22
+
23
+ attribute :candidate, CandidateType
24
+
25
+ xml do
26
+ map_element "signed", to: :signed
27
+ map_element "unsigned", to: :unsigned
28
+ map_element "watermarked", to: :watermarked
29
+ map_element "encrypted", to: :encrypted
30
+ map_attribute "candidate", to: :candidate
31
+ end
32
+ end
33
+
34
+ class PersonDetails < Lutaml::Model::Serializable
35
+ choice(min: 1, max: 3) do
36
+ attribute :first_name, :string
37
+ attribute :middle_name, :string
38
+ choice(min: 2, max: 2) do
39
+ attribute :email, :string
40
+ attribute :phone, :string
41
+ attribute :check, :string
42
+ end
43
+ end
44
+
45
+ choice(min: 1, max: 2) do
46
+ attribute :fb, :string
47
+ choice(min: 1, max: 1) do
48
+ attribute :insta, :string
49
+ attribute :last_name, :string
50
+ end
51
+ end
52
+
53
+ key_value do
54
+ map :first_name, to: :first_name
55
+ map :email, to: :email
56
+ map :phone, to: :phone
57
+ map :fb, to: :fb
58
+ map :insta, to: :insta
59
+ map :last_name, to: :last_name
60
+ end
61
+ end
62
+ end
63
+
64
+ RSpec.describe "Choice" do
65
+ context "with choice option" do
66
+ let(:mapper) { ChoiceSpec::DocumentState }
67
+
68
+ it "returns an empty array for a valid choice instance" do
69
+ valid_instance = mapper.new(
70
+ signed: true,
71
+ unsigned: true,
72
+ watermarked: false,
73
+ candidate: ChoiceSpec::CandidateType.new(id: 1, name: "Smith"),
74
+ )
75
+
76
+ expect(valid_instance.validate).to be_empty
77
+ end
78
+
79
+ it "returns nil for a valid instance, if given attributes for choice are within defined range" do
80
+ valid_instance = mapper.new(
81
+ watermarked: false,
82
+ encrypted: true,
83
+ )
84
+
85
+ expect(valid_instance.validate!).to be_nil
86
+ end
87
+
88
+ it "raises error, if attributes given for choice are out of upper bound" do
89
+ valid_instance = mapper.new(
90
+ signed: true,
91
+ unsigned: false,
92
+ watermarked: false,
93
+ encrypted: true,
94
+ )
95
+
96
+ expect do
97
+ valid_instance.validate!
98
+ end.to raise_error(Lutaml::Model::ValidationError) do |error|
99
+ expect(error.error_messages.join("\n")).to include("Attributes `[:signed, :unsigned, :watermarked, :encrypted]` count exceeds the upper bound `3`")
100
+ end
101
+ end
102
+ end
103
+
104
+ context "with nested choice option" do
105
+ let(:mapper) { ChoiceSpec::PersonDetails }
106
+
107
+ it "returns an empty array for a valid instance" do
108
+ valid_instance = mapper.new(
109
+ first_name: "John",
110
+ middle_name: "S",
111
+ fb: "fb",
112
+ )
113
+
114
+ expect(valid_instance.validate).to be_empty
115
+ end
116
+
117
+ it "returns nil for a valid instance" do
118
+ valid_instance = mapper.new(
119
+ email: "email",
120
+ phone: "02344",
121
+ last_name: "last_name",
122
+ )
123
+
124
+ expect(valid_instance.validate!).to be_nil
125
+ end
126
+
127
+ it "raises error, if given attribute for choice are not within upper bound" do
128
+ valid_instance = mapper.new(
129
+ first_name: "Nick",
130
+ email: "email",
131
+ phone: "phone",
132
+ check: "check",
133
+ fb: "fb",
134
+ insta: "insta",
135
+ )
136
+
137
+ expect do
138
+ valid_instance.validate!
139
+ end.to raise_error(Lutaml::Model::ValidationError) do |error|
140
+ expect(error.error_messages.join("\n")).to eq("Attributes `[:email, :phone, :check]` count exceeds the upper bound `2`")
141
+ end
142
+ end
143
+
144
+ it "raises error, if given attribute for choice are not within lower bound" do
145
+ valid_instance = mapper.new(
146
+ fb: "fb",
147
+ insta: "insta",
148
+ )
149
+
150
+ expect do
151
+ valid_instance.validate!
152
+ end.to raise_error(Lutaml::Model::ValidationError) do |error|
153
+ expect(error.error_messages.join("\n")).to eq("Attributes `[]` count is less than the lower bound `1`")
154
+ end
155
+ end
156
+
157
+ it "raises error, if min, max is not positive" do
158
+ expect do
159
+ Class.new(Lutaml::Model::Serializable) do
160
+ choice(min: -1, max: -2) do
161
+ attribute :id, :integer
162
+ attribute :name, :string
163
+ end
164
+ end
165
+ end.to raise_error(Lutaml::Model::InvalidChoiceRangeError, "Choice lower bound `-1` must be positive")
166
+ end
167
+ end
168
+ end
@@ -47,7 +47,7 @@ module CollectionTests
47
47
  end
48
48
 
49
49
  def city_from_xml(model, node)
50
- model.city = node
50
+ model.city = node.text
51
51
  end
52
52
 
53
53
  def city_to_xml(model, parent, doc)
@@ -5,13 +5,53 @@ class CustomModelChild
5
5
  end
6
6
 
7
7
  class CustomModelParent
8
- attr_accessor :first_name, :middle_name, :last_name, :child_mapper
8
+ attr_accessor :first_name, :middle_name, :last_name, :child_mapper, :math
9
9
 
10
10
  def name
11
11
  "#{first_name} #{last_name}"
12
12
  end
13
13
  end
14
14
 
15
+ class GenericFormulaClass
16
+ attr_accessor :value
17
+ end
18
+
19
+ class Mi < Lutaml::Model::Serializable
20
+ model GenericFormulaClass
21
+
22
+ attribute :value, :string
23
+
24
+ xml do
25
+ root "mi"
26
+
27
+ map_content to: :value
28
+ end
29
+ end
30
+
31
+ class Mstyle < Lutaml::Model::Serializable
32
+ model GenericFormulaClass
33
+
34
+ attribute :value, Mi, collection: true
35
+
36
+ xml do
37
+ root "mstyle"
38
+
39
+ map_element :mi, to: :value
40
+ end
41
+ end
42
+
43
+ class MmlMath < Lutaml::Model::Serializable
44
+ model GenericFormulaClass
45
+
46
+ attribute :value, Mstyle, collection: true
47
+
48
+ xml do
49
+ root "math"
50
+
51
+ map_element :mstyle, to: :value
52
+ end
53
+ end
54
+
15
55
  class CustomModelChildMapper < Lutaml::Model::Serializable
16
56
  model CustomModelChild
17
57
 
@@ -31,6 +71,7 @@ class CustomModelParentMapper < Lutaml::Model::Serializable
31
71
  attribute :middle_name, Lutaml::Model::Type::String
32
72
  attribute :last_name, Lutaml::Model::Type::String
33
73
  attribute :child_mapper, CustomModelChildMapper
74
+ attribute :math, MmlMath
34
75
 
35
76
  xml do
36
77
  map_element :first_name, to: :first_name
@@ -38,6 +79,7 @@ class CustomModelParentMapper < Lutaml::Model::Serializable
38
79
  map_element :last_name, to: :last_name
39
80
  map_element :CustomModelChild,
40
81
  with: { to: :child_to_xml, from: :child_from_xml }
82
+ map_element :math, to: :math
41
83
  end
42
84
 
43
85
  def child_to_xml(model, parent, doc)
@@ -55,9 +97,8 @@ class CustomModelParentMapper < Lutaml::Model::Serializable
55
97
 
56
98
  def child_from_xml(model, value)
57
99
  model.child_mapper ||= CustomModelChild.new
58
-
59
- model.child_mapper.street = value["elements"]["street"].text
60
- model.child_mapper.city = value["elements"]["city"].text
100
+ model.child_mapper.street = value.find_child_by_name("street").text
101
+ model.child_mapper.city = value.find_child_by_name("city").text
61
102
  end
62
103
  end
63
104
 
@@ -126,10 +167,10 @@ module CustomModelSpecs
126
167
 
127
168
  def bibdata_from_xml(model, value)
128
169
  model.bibdata = BibliographicItem.new(
129
- "type" => value["attributes"]["type"],
130
- "title" => value["elements"]["title"],
131
- "language" => value["elements"]["title"]["attributes"]["language"],
132
- "schema_version" => value["attributes"]["schema-version"],
170
+ "type" => value.find_attribute_value("type"),
171
+ "title" => value.find_child_by_name("title"),
172
+ "language" => value.find_child_by_name("title").find_attribute_value("language"),
173
+ "schema_version" => value.find_attribute_value("schema-version"),
133
174
  )
134
175
  end
135
176
 
@@ -318,6 +359,11 @@ RSpec.describe "CustomModel" do
318
359
  <street>Oxford Street</street>
319
360
  <city>London</city>
320
361
  </CustomModelChild>
362
+ <math>
363
+ <mstyle>
364
+ <mi>Math</mi>
365
+ </mstyle>
366
+ </math>
321
367
  </CustomModelParent>
322
368
  XML
323
369
  end
@@ -334,6 +380,7 @@ RSpec.describe "CustomModel" do
334
380
  expect(instance.child_mapper.class).to eq(child_model)
335
381
  expect(instance.child_mapper.street).to eq("Oxford Street")
336
382
  expect(instance.child_mapper.city).to eq("London")
383
+ expect(instance.math.value.first.value.first.value).to eq("Math")
337
384
  end
338
385
  end
339
386
 
@@ -68,7 +68,7 @@ class CustomSerialization < Lutaml::Model::Serializable
68
68
  end
69
69
 
70
70
  def name_from_xml(model, value)
71
- model.full_name = value.sub(/^XML Masterpiece: /, "")
71
+ model.full_name = value.text.sub(/^XML Masterpiece: /, "")
72
72
  end
73
73
 
74
74
  def size_to_xml(model, parent, doc)
@@ -86,7 +86,7 @@ class CustomSerialization < Lutaml::Model::Serializable
86
86
  end
87
87
 
88
88
  def color_from_xml(model, value)
89
- model.color = value.downcase
89
+ model.color = value.text.downcase
90
90
  end
91
91
 
92
92
  def description_to_xml(model, parent, doc)
@@ -98,6 +98,37 @@ class CustomSerialization < Lutaml::Model::Serializable
98
98
  end
99
99
  end
100
100
 
101
+ class GrammarInfo < Lutaml::Model::Serializable
102
+ attribute :part_of_speech, :string, values: %w[user admin super_admin]
103
+
104
+ key_value do
105
+ map :part_of_speech, with: { to: :part_of_speech_to_key_value, from: :part_of_speech_from_key_value }
106
+ end
107
+
108
+ xml do
109
+ root "GrammarInfo"
110
+ map_element :part_of_speech, with: { to: :part_of_speech_to_xml, from: :part_of_speech_from_xml }
111
+ end
112
+
113
+ def part_of_speech_from_key_value(model, value)
114
+ model.part_of_speech = value
115
+ end
116
+
117
+ def part_of_speech_to_key_value(model, doc)
118
+ doc["part_of_speech"] = model.part_of_speech
119
+ end
120
+
121
+ def part_of_speech_from_xml(model, node)
122
+ model.part_of_speech = node.text
123
+ end
124
+
125
+ def part_of_speech_to_xml(model, parent, doc)
126
+ el = doc.create_element("part_of_speech")
127
+ doc.add_text(el, model.part_of_speech)
128
+ doc.add_element(parent, el)
129
+ end
130
+ end
131
+
101
132
  RSpec.describe CustomSerialization do
102
133
  let(:attributes) do
103
134
  {
@@ -183,4 +214,45 @@ RSpec.describe CustomSerialization do
183
214
  expect(ceramic.description).to eq(model.description)
184
215
  end
185
216
  end
217
+
218
+ context "when enum used with custom methods" do
219
+ let(:hash) do
220
+ {
221
+ "part_of_speech" => "user",
222
+ }
223
+ end
224
+
225
+ it "correctly persist value for yaml" do
226
+ instance = GrammarInfo.from_yaml(hash.to_yaml)
227
+ serialized = instance.to_yaml
228
+
229
+ expect(instance.part_of_speech).to eq("user")
230
+ expect(serialized).to eq(hash.to_yaml)
231
+ end
232
+
233
+ it "correctly persist value for json" do
234
+ instance = GrammarInfo.from_json(hash.to_json)
235
+ serialized = instance.to_json
236
+
237
+ expect(instance.part_of_speech).to eq("user")
238
+ expect(serialized).to eq(hash.to_json)
239
+ end
240
+
241
+ it "correctly handles value for xml" do
242
+ xml_input = <<~XML
243
+ <GrammarInfo>
244
+ <part_of_speech>user</part_of_speech>
245
+ </GrammarInfo>
246
+ XML
247
+
248
+ instance = GrammarInfo.from_xml(xml_input)
249
+ expect(instance.part_of_speech).to eq("user")
250
+ expect(instance.user?).to be true
251
+ expect(instance.admin?).to be false
252
+ expect(instance.super_admin?).to be false
253
+
254
+ serialized = instance.to_xml
255
+ expect(serialized).to be_equivalent_to(xml_input)
256
+ end
257
+ end
186
258
  end
@@ -115,7 +115,9 @@ module DefaultsSpec
115
115
  model Lang
116
116
 
117
117
  attribute :lang, :string, default: -> { "en" }
118
- attribute :content, :string, default: -> { "default value not render when render_default is false" }
118
+ attribute :content, :string, default: -> {
119
+ "default value not render when render_default is false"
120
+ }
119
121
 
120
122
  xml do
121
123
  root "CustomModelWithDefaultValue"
@@ -99,14 +99,16 @@ RSpec.describe Delegation do
99
99
  end
100
100
 
101
101
  it "serializes to JSON with pretty formatting" do
102
- expected_pretty_json = {
103
- type: "Vase",
104
- color: "Blue",
105
- }.to_json
102
+ expected_pretty_json = <<~JSON.chomp
103
+ {
104
+ "type": "Vase",
105
+ "color": "Blue"
106
+ }
107
+ JSON
106
108
 
107
109
  generated_json = delegation.to_json(only: %i[type color], pretty: true)
108
110
 
109
- expect(generated_json.strip).to eq(expected_pretty_json.strip)
111
+ expect(generated_json).to eq(expected_pretty_json)
110
112
  end
111
113
 
112
114
  it "serializes to XML with pretty formatting" do
@@ -43,6 +43,21 @@ RSpec.describe "Enum" do
43
43
  .from(nil)
44
44
  .to("user")
45
45
  end
46
+
47
+ it "sets and unsets all enum values correctly" do
48
+ object.user = true
49
+ object.admin = false
50
+ object.super_admin = false
51
+
52
+ expect(object.user?).to be true
53
+ expect(object.admin?).to be false
54
+ expect(object.super_admin?).to be false
55
+
56
+ expect { object.user = false }
57
+ .to change(object, :user?)
58
+ .from(true)
59
+ .to(false)
60
+ end
46
61
  end
47
62
 
48
63
  describe "#multi_value" do
@@ -59,6 +74,26 @@ RSpec.describe "Enum" do
59
74
  .from([])
60
75
  .to(%w[dual plural])
61
76
  end
77
+
78
+ it "sets and unsets all enum values correctly" do
79
+ object.singular = false
80
+ object.dual = true
81
+ object.plural = true
82
+
83
+ expect(object.singular?).to be false
84
+ expect(object.dual?).to be true
85
+ expect(object.plural?).to be true
86
+
87
+ expect { object.plural = false }
88
+ .to change(object, :plural?)
89
+ .from(true)
90
+ .to(false)
91
+
92
+ expect { object.dual = false }
93
+ .to change(object, :dual?)
94
+ .from(true)
95
+ .to(false)
96
+ end
62
97
  end
63
98
 
64
99
  EnumSpec::WithEnum.enums.each_value do |enum_attr|