lutaml-model 0.5.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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 +33 -10
  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 +125 -35
  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 +6 -7
  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 +143 -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)
@@ -55,9 +55,8 @@ class CustomModelParentMapper < Lutaml::Model::Serializable
55
55
 
56
56
  def child_from_xml(model, value)
57
57
  model.child_mapper ||= CustomModelChild.new
58
-
59
- model.child_mapper.street = value["elements"]["street"].text
60
- model.child_mapper.city = value["elements"]["city"].text
58
+ model.child_mapper.street = value.find_child_by_name("street").text
59
+ model.child_mapper.city = value.find_child_by_name("city").text
61
60
  end
62
61
  end
63
62
 
@@ -126,10 +125,10 @@ module CustomModelSpecs
126
125
 
127
126
  def bibdata_from_xml(model, value)
128
127
  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"],
128
+ "type" => value.find_attribute_value("type"),
129
+ "title" => value.find_child_by_name("title"),
130
+ "language" => value.find_child_by_name("title").find_attribute_value("language"),
131
+ "schema_version" => value.find_attribute_value("schema-version"),
133
132
  )
134
133
  end
135
134
 
@@ -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|