lutaml-model 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +58 -21
- data/Gemfile +1 -0
- data/README.adoc +1112 -264
- data/lib/lutaml/model/attribute.rb +33 -10
- data/lib/lutaml/model/choice.rb +56 -0
- data/lib/lutaml/model/config.rb +1 -0
- data/lib/lutaml/model/error/choice_lower_bound_error.rb +9 -0
- data/lib/lutaml/model/error/choice_upper_bound_error.rb +9 -0
- data/lib/lutaml/model/error/import_model_with_root_error.rb +9 -0
- data/lib/lutaml/model/error/incorrect_sequence_error.rb +9 -0
- data/lib/lutaml/model/error/invalid_choice_range_error.rb +20 -0
- data/lib/lutaml/model/error/no_root_mapping_error.rb +9 -0
- data/lib/lutaml/model/error/no_root_namespace_error.rb +9 -0
- data/lib/lutaml/model/error/unknown_sequence_mapping_error.rb +9 -0
- data/lib/lutaml/model/error.rb +8 -0
- data/lib/lutaml/model/json_adapter/standard_json_adapter.rb +6 -1
- data/lib/lutaml/model/key_value_mapping.rb +3 -1
- data/lib/lutaml/model/key_value_mapping_rule.rb +4 -2
- data/lib/lutaml/model/liquefiable.rb +59 -0
- data/lib/lutaml/model/mapping_hash.rb +1 -1
- data/lib/lutaml/model/mapping_rule.rb +15 -2
- data/lib/lutaml/model/schema/xml_compiler.rb +68 -26
- data/lib/lutaml/model/schema_location.rb +7 -0
- data/lib/lutaml/model/sequence.rb +71 -0
- data/lib/lutaml/model/serialize.rb +125 -35
- data/lib/lutaml/model/type/decimal.rb +0 -4
- data/lib/lutaml/model/type/time.rb +3 -3
- data/lib/lutaml/model/utils.rb +19 -15
- data/lib/lutaml/model/validation.rb +12 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model/xml_adapter/builder/oga.rb +10 -7
- data/lib/lutaml/model/xml_adapter/builder/ox.rb +20 -13
- data/lib/lutaml/model/xml_adapter/element.rb +32 -0
- data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +8 -8
- data/lib/lutaml/model/xml_adapter/oga/element.rb +14 -13
- data/lib/lutaml/model/xml_adapter/oga_adapter.rb +86 -19
- data/lib/lutaml/model/xml_adapter/ox_adapter.rb +19 -15
- data/lib/lutaml/model/xml_adapter/xml_document.rb +74 -13
- data/lib/lutaml/model/xml_adapter/xml_element.rb +57 -3
- data/lib/lutaml/model/xml_mapping.rb +49 -7
- data/lib/lutaml/model/xml_mapping_rule.rb +8 -3
- data/lib/lutaml/model.rb +1 -0
- data/lutaml-model.gemspec +5 -0
- data/spec/benchmarks/xml_parsing_benchmark_spec.rb +75 -0
- data/spec/ceramic_spec.rb +39 -0
- data/spec/fixtures/ceramic.rb +23 -0
- data/spec/fixtures/xml/address_example_260.xsd +9 -0
- data/spec/fixtures/xml/user.xsd +10 -0
- data/spec/lutaml/model/cdata_spec.rb +4 -5
- data/spec/lutaml/model/choice_spec.rb +168 -0
- data/spec/lutaml/model/collection_spec.rb +1 -1
- data/spec/lutaml/model/custom_model_spec.rb +6 -7
- data/spec/lutaml/model/custom_serialization_spec.rb +74 -2
- data/spec/lutaml/model/defaults_spec.rb +3 -1
- data/spec/lutaml/model/delegation_spec.rb +7 -5
- data/spec/lutaml/model/enum_spec.rb +35 -0
- data/spec/lutaml/model/group_spec.rb +160 -0
- data/spec/lutaml/model/inheritance_spec.rb +25 -0
- data/spec/lutaml/model/liquefiable_spec.rb +121 -0
- data/spec/lutaml/model/mixed_content_spec.rb +80 -41
- data/spec/lutaml/model/multiple_mapping_spec.rb +22 -10
- data/spec/lutaml/model/schema/xml_compiler_spec.rb +218 -25
- data/spec/lutaml/model/sequence_spec.rb +216 -0
- data/spec/lutaml/model/transformation_spec.rb +230 -0
- data/spec/lutaml/model/type_spec.rb +138 -31
- data/spec/lutaml/model/utils_spec.rb +32 -0
- data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +11 -7
- data/spec/lutaml/model/xml_mapping_rule_spec.rb +51 -0
- data/spec/lutaml/model/xml_mapping_spec.rb +143 -112
- 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.
|
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
|
@@ -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.
|
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
|
130
|
-
"title" => value
|
131
|
-
"language" => value
|
132
|
-
"schema_version" => value
|
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: -> {
|
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
|
-
|
104
|
-
|
105
|
-
|
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
|
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|
|