lutaml-model 0.5.3 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/dependent-tests.yml +2 -0
- data/.rubocop_todo.yml +86 -23
- data/Gemfile +2 -0
- data/README.adoc +1441 -220
- 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/constants.rb +7 -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/type/invalid_value_error.rb +19 -0
- data/lib/lutaml/model/error/unknown_sequence_mapping_error.rb +9 -0
- data/lib/lutaml/model/error.rb +9 -0
- data/lib/lutaml/model/json_adapter/standard_json_adapter.rb +6 -1
- data/lib/lutaml/model/key_value_mapping.rb +34 -3
- 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 +9 -1
- data/lib/lutaml/model/mapping_rule.rb +19 -2
- data/lib/lutaml/model/schema/templates/simple_type.rb +247 -0
- data/lib/lutaml/model/schema/xml_compiler.rb +762 -0
- data/lib/lutaml/model/schema.rb +5 -0
- data/lib/lutaml/model/schema_location.rb +7 -0
- data/lib/lutaml/model/sequence.rb +71 -0
- data/lib/lutaml/model/serialize.rb +139 -33
- data/lib/lutaml/model/toml_adapter/toml_rb_adapter.rb +1 -2
- data/lib/lutaml/model/type/decimal.rb +0 -4
- data/lib/lutaml/model/type/hash.rb +11 -11
- 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 +13 -9
- 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 +82 -25
- data/lib/lutaml/model/xml_adapter/xml_element.rb +57 -3
- data/lib/lutaml/model/xml_mapping.rb +53 -9
- data/lib/lutaml/model/xml_mapping_rule.rb +8 -6
- data/lib/lutaml/model.rb +2 -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/invalid_math_document.xml +4 -0
- data/spec/fixtures/xml/math_document_schema.xsd +56 -0
- data/spec/fixtures/xml/test_schema.xsd +53 -0
- data/spec/fixtures/xml/user.xsd +10 -0
- data/spec/fixtures/xml/valid_math_document.xml +4 -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 +7 -21
- 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/key_value_mapping_spec.rb +27 -0
- data/spec/lutaml/model/liquefiable_spec.rb +121 -0
- data/spec/lutaml/model/map_all_spec.rb +188 -0
- data/spec/lutaml/model/mixed_content_spec.rb +95 -56
- data/spec/lutaml/model/multiple_mapping_spec.rb +22 -10
- data/spec/lutaml/model/schema/xml_compiler_spec.rb +1624 -0
- 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/with_child_mapping_spec.rb +2 -2
- data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +11 -7
- data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +52 -0
- data/spec/lutaml/model/xml_mapping_rule_spec.rb +51 -0
- data/spec/lutaml/model/xml_mapping_spec.rb +250 -112
- metadata +77 -2
@@ -0,0 +1,216 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "lutaml/model"
|
3
|
+
|
4
|
+
module SequenceSpec
|
5
|
+
class Ceramic < Lutaml::Model::Serializable
|
6
|
+
attribute :id, :string
|
7
|
+
attribute :name, :string
|
8
|
+
attribute :type, :string
|
9
|
+
attribute :color, :string
|
10
|
+
attribute :bold, :string
|
11
|
+
attribute :text, :string
|
12
|
+
attribute :usage, :string
|
13
|
+
attribute :size, :string
|
14
|
+
attribute :first_name, :string
|
15
|
+
attribute :last_name, :string
|
16
|
+
attribute :tag, :string
|
17
|
+
attribute :temperature, :string
|
18
|
+
|
19
|
+
xml do
|
20
|
+
root "Ceramic"
|
21
|
+
map_element :tag, to: :tag
|
22
|
+
|
23
|
+
sequence do
|
24
|
+
map_element :id, to: :id
|
25
|
+
map_element :name, to: :name
|
26
|
+
map_element :type, to: :type
|
27
|
+
map_element :color, to: :color
|
28
|
+
map_element :bold, to: :bold
|
29
|
+
map_element :text, to: :text
|
30
|
+
sequence do
|
31
|
+
map_element :usage, to: :usage
|
32
|
+
map_element :size, to: :size
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
sequence do
|
37
|
+
map_element :first_name, to: :first_name
|
38
|
+
map_element :last_name, to: :last_name
|
39
|
+
end
|
40
|
+
|
41
|
+
map_element :temperature, to: :temperature
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class CeramicCollection < Lutaml::Model::Serializable
|
46
|
+
attribute :ceramic, Ceramic, collection: 1..2
|
47
|
+
|
48
|
+
xml do
|
49
|
+
root "collection"
|
50
|
+
map_element "ceramic", to: :ceramic
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
RSpec.describe "Sequence" do
|
56
|
+
context "with nesting sequence" do
|
57
|
+
let(:mapper) { SequenceSpec::Ceramic }
|
58
|
+
|
59
|
+
it "don't raise error for a valid instance, if given attribute for sequence has correct order" do
|
60
|
+
xml = <<~XML
|
61
|
+
<Ceramic>
|
62
|
+
<tag>Nik</tag>
|
63
|
+
<id>1</id>
|
64
|
+
<name>Vase</name>
|
65
|
+
<type>Decorative</type>
|
66
|
+
<color>Blue</color>
|
67
|
+
<bold>Heading</bold>
|
68
|
+
<text>Header</text>
|
69
|
+
<usage>Indoor</usage>
|
70
|
+
<size>Medium</size>
|
71
|
+
<first_name>Dale</first_name>
|
72
|
+
<last_name>Steyn</last_name>
|
73
|
+
<temperature>Normal</temperature>
|
74
|
+
</Ceramic>
|
75
|
+
XML
|
76
|
+
|
77
|
+
expect { mapper.from_xml(xml) }.not_to raise_error
|
78
|
+
end
|
79
|
+
|
80
|
+
it "don't raise error for a valid instance with sequence range, if given attribute for sequence has correct order" do
|
81
|
+
xml = <<~XML
|
82
|
+
<collection>
|
83
|
+
<ceramic>
|
84
|
+
<tag>Nik</tag>
|
85
|
+
<id>1</id>
|
86
|
+
<name>Vase</name>
|
87
|
+
<type>Decorative</type>
|
88
|
+
<color>Blue</color>
|
89
|
+
<bold>Heading</bold>
|
90
|
+
<text>Header</text>
|
91
|
+
<usage>Indoor</usage>
|
92
|
+
<size>Medium</size>
|
93
|
+
<first_name>Dale</first_name>
|
94
|
+
<last_name>Steyn</last_name>
|
95
|
+
<temperature>Normal</temperature>
|
96
|
+
</ceramic>
|
97
|
+
<ceramic>
|
98
|
+
<tag>Nik</tag>
|
99
|
+
<id>1</id>
|
100
|
+
<name>Vase</name>
|
101
|
+
<type>Decorative</type>
|
102
|
+
<color>Blue</color>
|
103
|
+
<bold>Heading</bold>
|
104
|
+
<text>Header</text>
|
105
|
+
<usage>Indoor</usage>
|
106
|
+
<size>Medium</size>
|
107
|
+
<first_name>Dale</first_name>
|
108
|
+
<last_name>Steyn</last_name>
|
109
|
+
<temperature>Normal</temperature>
|
110
|
+
</ceramic>
|
111
|
+
</collection>
|
112
|
+
XML
|
113
|
+
|
114
|
+
expect do
|
115
|
+
SequenceSpec::CeramicCollection.from_xml(xml)
|
116
|
+
end.not_to raise_error
|
117
|
+
end
|
118
|
+
|
119
|
+
it "raises error, if given attributes order is incorrect in sequence" do
|
120
|
+
xml = <<~XML
|
121
|
+
<Ceramic>
|
122
|
+
<tag>Nik</tag>
|
123
|
+
<temperature>High</temperature>
|
124
|
+
<first_name>Micheal</first_name>
|
125
|
+
<id>1</id>
|
126
|
+
<name>Vase</name>
|
127
|
+
<type>Decorative</type>
|
128
|
+
<color>Blue</color>
|
129
|
+
<bold>Heading</bold>
|
130
|
+
<usage>Indoor</usage>
|
131
|
+
<size>Medium</size>
|
132
|
+
<last_name>Johnson</last_name>
|
133
|
+
<text>Header</text>
|
134
|
+
</Ceramic>
|
135
|
+
XML
|
136
|
+
|
137
|
+
expect do
|
138
|
+
mapper.from_xml(xml)
|
139
|
+
end.to raise_error(Lutaml::Model::IncorrectSequenceError) do |error|
|
140
|
+
expect(error.message).to eq("Element `usage` does not match the expected sequence order element `text`")
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
it "raises error with sequence range, if given attributes order is incorrect in sequence" do
|
145
|
+
invalid_xml = <<~XML
|
146
|
+
<collection>
|
147
|
+
<ceramic>
|
148
|
+
<id>1</id>
|
149
|
+
<name>Vase</name>
|
150
|
+
<type>Decorative</type>
|
151
|
+
<color>Blue</color>
|
152
|
+
<bold>Heading</bold>
|
153
|
+
<text>Header</text>
|
154
|
+
<usage>Indoor</usage>
|
155
|
+
<size>Medium</size>
|
156
|
+
<first_name>Dale</first_name>
|
157
|
+
<last_name>Steyn</last_name>
|
158
|
+
<temperature>Normal</temperature>
|
159
|
+
<tag>Nik</tag>
|
160
|
+
</ceramic>
|
161
|
+
|
162
|
+
<ceramic>
|
163
|
+
<id>2</id>
|
164
|
+
<name>Nick</name>
|
165
|
+
<type>Unique</type>
|
166
|
+
<color>Red</color>
|
167
|
+
<bold>Name</bold>
|
168
|
+
<text>Body</text>
|
169
|
+
<usage>Outdoor</usage>
|
170
|
+
<size>Small</size>
|
171
|
+
<first_name>Smith</first_name>
|
172
|
+
<last_name>Ash</last_name>
|
173
|
+
<temperature>High</temperature>
|
174
|
+
<tag>Adid</tag>
|
175
|
+
</ceramic>
|
176
|
+
|
177
|
+
<ceramic>
|
178
|
+
<id>3</id>
|
179
|
+
<name>Starc</name>
|
180
|
+
<type>Int</type>
|
181
|
+
<color>White</color>
|
182
|
+
<bold>Act</bold>
|
183
|
+
<text>Footer</text>
|
184
|
+
<usage>Nothing</usage>
|
185
|
+
<size>Large</size>
|
186
|
+
<first_name>Dale</first_name>
|
187
|
+
<last_name>Steyn</last_name>
|
188
|
+
<temperature>Normal</temperature>
|
189
|
+
<tag>Bet</tag>
|
190
|
+
</ceramic>
|
191
|
+
</collection>
|
192
|
+
XML
|
193
|
+
|
194
|
+
expect do
|
195
|
+
SequenceSpec::CeramicCollection.from_xml(invalid_xml).validate!
|
196
|
+
end.to raise_error(Lutaml::Model::ValidationError) do |error|
|
197
|
+
expect(error).to include(Lutaml::Model::CollectionCountOutOfRangeError)
|
198
|
+
expect(error.error_messages).to eq(["ceramic count is 3, must be between 1 and 2"])
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
it "raises error, if mapping other map_element are defined in sequence" do
|
203
|
+
expect do
|
204
|
+
Class.new(Lutaml::Model::Serializable) do
|
205
|
+
attribute :type, :string
|
206
|
+
|
207
|
+
xml do
|
208
|
+
sequence do
|
209
|
+
map_attribute :type, to: :type
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end.to raise_error(Lutaml::Model::UnknownSequenceMappingError, "map_attribute is not allowed in sequence")
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1,230 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe "Value Transformations" do
|
4
|
+
module TransformationSpec
|
5
|
+
# Class with only attribute-level transformations
|
6
|
+
class AttributeTransformPerson < Lutaml::Model::Serializable
|
7
|
+
attribute :name, :string, transform: {
|
8
|
+
export: ->(value) { value.to_s.upcase },
|
9
|
+
}
|
10
|
+
attribute :email, :string, transform: {
|
11
|
+
import: ->(value) { "#{value}@example.com" },
|
12
|
+
}
|
13
|
+
attribute :tags, :string, collection: true, transform: {
|
14
|
+
export: ->(value) { value.map(&:upcase) },
|
15
|
+
import: ->(value) { value.map { |v| "#{v}-1" } },
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
# Class with only mapping-level transformations
|
20
|
+
class MappingTransformPerson < Lutaml::Model::Serializable
|
21
|
+
attribute :name, :string
|
22
|
+
attribute :email, :string
|
23
|
+
attribute :tags, :string, collection: true
|
24
|
+
|
25
|
+
json do
|
26
|
+
map "fullName", to: :name, transform: {
|
27
|
+
export: ->(value) { "Dr. #{value}" },
|
28
|
+
}
|
29
|
+
map "emailAddress", to: :email, transform: {
|
30
|
+
import: ->(value) { value.gsub("at", "@") },
|
31
|
+
}
|
32
|
+
map "labels", to: :tags, transform: {
|
33
|
+
export: ->(value) { value.join("-|-") },
|
34
|
+
import: ->(value) { value.split("|") },
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
xml do
|
39
|
+
root "person"
|
40
|
+
map_element "full-name", to: :name, transform: {
|
41
|
+
export: ->(value) { "Dr. #{value}" },
|
42
|
+
}
|
43
|
+
map_element "email-address", to: :email, transform: {
|
44
|
+
import: ->(value) { value.gsub("at", "@") },
|
45
|
+
}
|
46
|
+
map_element "labels", to: :tags, transform: {
|
47
|
+
export: ->(value) { value.join("-|-") },
|
48
|
+
import: ->(value) { value.split("|") },
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Class with both attribute and mapping transformations
|
54
|
+
class CombinedTransformPerson < Lutaml::Model::Serializable
|
55
|
+
attribute :name, :string, transform: {
|
56
|
+
export: ->(value) { value.to_s.capitalize },
|
57
|
+
import: ->(value) { value.to_s.downcase },
|
58
|
+
}
|
59
|
+
attribute :email, :string, transform: {
|
60
|
+
export: lambda(&:downcase),
|
61
|
+
import: lambda(&:downcase),
|
62
|
+
}
|
63
|
+
attribute :tags, :string, collection: true, transform: {
|
64
|
+
export: ->(value) { value.map(&:upcase) },
|
65
|
+
import: ->(value) { value.map { |v| "#{v}-1" } },
|
66
|
+
}
|
67
|
+
|
68
|
+
json do
|
69
|
+
map "fullName", to: :name, transform: {
|
70
|
+
export: ->(value) { "Prof. #{value}" },
|
71
|
+
import: ->(value) { value.gsub("Prof. ", "") },
|
72
|
+
}
|
73
|
+
map "contactEmail", to: :email, transform: {
|
74
|
+
export: ->(value) { "contact+#{value}" },
|
75
|
+
import: ->(value) { value.gsub("contact+", "") },
|
76
|
+
}
|
77
|
+
map "skills", to: :tags
|
78
|
+
end
|
79
|
+
|
80
|
+
xml do
|
81
|
+
root "person"
|
82
|
+
map_element "full-name", to: :name, transform: {
|
83
|
+
export: ->(value) { "Prof. #{value}" },
|
84
|
+
import: ->(value) { value.gsub("Prof. ", "") },
|
85
|
+
}
|
86
|
+
map_element "contact-email", to: :email, transform: {
|
87
|
+
export: ->(value) { "contact+#{value}" },
|
88
|
+
import: ->(value) { value.gsub("contact+", "") },
|
89
|
+
}
|
90
|
+
map_element "skills", to: :tags
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "Attribute-only transformations" do
|
96
|
+
let(:attribute_person) do
|
97
|
+
TransformationSpec::AttributeTransformPerson.new(
|
98
|
+
name: "john",
|
99
|
+
email: "smith",
|
100
|
+
tags: ["ruby", "rails"],
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
let(:expected_xml) do
|
105
|
+
<<~XML
|
106
|
+
<AttributeTransformPerson>
|
107
|
+
<name>JOHN</name>
|
108
|
+
<email>smith</email>
|
109
|
+
<tags>RUBY</tags>
|
110
|
+
<tags>RAILS</tags>
|
111
|
+
</AttributeTransformPerson>
|
112
|
+
XML
|
113
|
+
end
|
114
|
+
|
115
|
+
it "applies attribute transformations during serialization" do
|
116
|
+
parsed_json = JSON.parse(attribute_person.to_json)
|
117
|
+
expect(parsed_json["name"]).to eq("JOHN")
|
118
|
+
expect(parsed_json["email"]).to eq("smith")
|
119
|
+
expect(parsed_json["tags"]).to eq(["RUBY", "RAILS"])
|
120
|
+
end
|
121
|
+
|
122
|
+
it "applies attribute transformations during deserialization" do
|
123
|
+
json = {
|
124
|
+
"name" => "jane",
|
125
|
+
"email" => "doe",
|
126
|
+
"tags" => ["python", "django"],
|
127
|
+
}.to_json
|
128
|
+
|
129
|
+
parsed = TransformationSpec::AttributeTransformPerson.from_json(json)
|
130
|
+
expect(parsed.name).to eq("jane")
|
131
|
+
expect(parsed.email).to eq("doe@example.com")
|
132
|
+
expect(parsed.tags).to eq(["python-1", "django-1"])
|
133
|
+
end
|
134
|
+
|
135
|
+
it "applies attribute transformations during XML serialization" do
|
136
|
+
xml = attribute_person.to_xml
|
137
|
+
expect(xml).to be_equivalent_to(expected_xml)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe "Mapping-only transformations" do
|
142
|
+
let(:mapping_person) do
|
143
|
+
TransformationSpec::MappingTransformPerson.new(
|
144
|
+
name: "alice",
|
145
|
+
email: "aliceattest.com",
|
146
|
+
tags: ["developer", "architect"],
|
147
|
+
)
|
148
|
+
end
|
149
|
+
|
150
|
+
let(:expected_xml) do
|
151
|
+
<<~XML
|
152
|
+
<person>
|
153
|
+
<full-name>Dr. alice</full-name>
|
154
|
+
<email-address>aliceattest.com</email-address>
|
155
|
+
<labels>developer-|-architect</labels>
|
156
|
+
</person>
|
157
|
+
XML
|
158
|
+
end
|
159
|
+
|
160
|
+
it "applies mapping transformations during JSON serialization" do
|
161
|
+
json = mapping_person.to_json
|
162
|
+
parsed = JSON.parse(json)
|
163
|
+
expect(parsed["fullName"]).to eq("Dr. alice")
|
164
|
+
expect(parsed["emailAddress"]).to eq("aliceattest.com")
|
165
|
+
expect(parsed["labels"]).to eq("developer-|-architect")
|
166
|
+
end
|
167
|
+
|
168
|
+
it "applies mapping transformations during JSON deserialization" do
|
169
|
+
json = {
|
170
|
+
"fullName" => "Dr. bob",
|
171
|
+
"emailAddress" => "bobattest.com",
|
172
|
+
"labels" => "senior|lead",
|
173
|
+
}.to_json
|
174
|
+
|
175
|
+
parsed = TransformationSpec::MappingTransformPerson.from_json(json)
|
176
|
+
expect(parsed.name).to eq("Dr. bob")
|
177
|
+
expect(parsed.email).to eq("bob@test.com")
|
178
|
+
expect(parsed.tags).to eq(["senior", "lead"])
|
179
|
+
end
|
180
|
+
|
181
|
+
it "applies mapping transformations during XML serialization" do
|
182
|
+
xml = mapping_person.to_xml
|
183
|
+
expect(xml).to be_equivalent_to(expected_xml)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe "Combined transformations" do
|
188
|
+
let(:combined_person) do
|
189
|
+
TransformationSpec::CombinedTransformPerson.new(
|
190
|
+
name: "carol",
|
191
|
+
email: "CAROL@TEST.COM",
|
192
|
+
tags: ["manager", "agile"],
|
193
|
+
)
|
194
|
+
end
|
195
|
+
|
196
|
+
let(:expected_xml) do
|
197
|
+
<<~XML
|
198
|
+
<person>
|
199
|
+
<full-name>Prof. carol</full-name>
|
200
|
+
<contact-email>contact+CAROL@TEST.COM</contact-email>
|
201
|
+
<skills>MANAGER-1</skills>
|
202
|
+
<skills>AGILE-1</skills>
|
203
|
+
</person>
|
204
|
+
XML
|
205
|
+
end
|
206
|
+
|
207
|
+
it "applies both transformations with correct precedence in JSON" do
|
208
|
+
json = combined_person.to_json
|
209
|
+
parsed = JSON.parse(json)
|
210
|
+
|
211
|
+
expect(parsed["fullName"]).to eq("Prof. carol")
|
212
|
+
expect(parsed["contactEmail"]).to eq("contact+CAROL@TEST.COM")
|
213
|
+
end
|
214
|
+
|
215
|
+
it "handles round-trip transformations across formats" do
|
216
|
+
# JSON -> Key-Value -> JSON cycle
|
217
|
+
json = combined_person.to_json
|
218
|
+
parsed_json = TransformationSpec::CombinedTransformPerson.from_json(json)
|
219
|
+
expect(parsed_json.name).to eq("carol")
|
220
|
+
expect(parsed_json.email).to eq("CAROL@TEST.COM")
|
221
|
+
expect(parsed_json.tags).to eq(["MANAGER-1", "AGILE-1"])
|
222
|
+
end
|
223
|
+
|
224
|
+
it "applies both transformations with correct precedence in XML" do
|
225
|
+
xml = combined_person.to_xml
|
226
|
+
parsed_xml = TransformationSpec::CombinedTransformPerson.from_xml(xml)
|
227
|
+
expect(parsed_xml.to_xml).to be_equivalent_to(expected_xml)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
@@ -2,16 +2,47 @@
|
|
2
2
|
require "spec_helper"
|
3
3
|
require "bigdecimal"
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
class CustomSerializationType < Lutaml::Model::Type::Value
|
6
|
+
def self.from_xml(_xml_string)
|
7
|
+
"from_xml_overrided"
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.from_json(_value)
|
11
|
+
"from_json_overrided"
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.serialize(_value)
|
15
|
+
"serialize_overrided"
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_xml
|
19
|
+
"to_xml_overrided"
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_json(*_args)
|
23
|
+
"to_json_overrided"
|
9
24
|
end
|
10
25
|
end
|
11
26
|
|
12
|
-
class
|
13
|
-
|
14
|
-
|
27
|
+
class SampleModel < Lutaml::Model::Serializable
|
28
|
+
attribute :custom_type, CustomSerializationType
|
29
|
+
xml do
|
30
|
+
root "sample"
|
31
|
+
map_element "custom_type", to: :custom_type
|
32
|
+
end
|
33
|
+
json do
|
34
|
+
map_element "custom_type", to: :custom_type
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class SampleModelAttribute < Lutaml::Model::Serializable
|
39
|
+
attribute :custom_type, CustomSerializationType
|
40
|
+
xml do
|
41
|
+
root "sample"
|
42
|
+
map_attribute "custom_type", to: :custom_type
|
43
|
+
end
|
44
|
+
json do
|
45
|
+
map_element "custom_type", to: :custom_type
|
15
46
|
end
|
16
47
|
end
|
17
48
|
|
@@ -19,6 +50,17 @@ RSpec.describe Lutaml::Model::Type do
|
|
19
50
|
describe "Type System" do
|
20
51
|
describe ".register and .lookup" do
|
21
52
|
context "with valid types" do
|
53
|
+
before do
|
54
|
+
# Test class for type registration scenarios
|
55
|
+
custom_type = Class.new(Lutaml::Model::Type::Value) do
|
56
|
+
def self.cast(value)
|
57
|
+
value.to_s.upcase
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
stub_const("CustomType", custom_type)
|
62
|
+
end
|
63
|
+
|
22
64
|
it "registers and looks up a custom type" do
|
23
65
|
described_class.register(:custom, CustomType)
|
24
66
|
expect(described_class.lookup(:custom)).to eq(CustomType)
|
@@ -33,6 +75,16 @@ RSpec.describe Lutaml::Model::Type do
|
|
33
75
|
end
|
34
76
|
|
35
77
|
context "with invalid types" do
|
78
|
+
before do
|
79
|
+
invalid_type = Class.new do
|
80
|
+
def self.cast(value)
|
81
|
+
value
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
stub_const("InvalidType", invalid_type)
|
86
|
+
end
|
87
|
+
|
36
88
|
it "raises TypeError when registering non-Type::Value class" do
|
37
89
|
expect do
|
38
90
|
described_class.register(:invalid,
|
@@ -159,31 +211,35 @@ RSpec.describe Lutaml::Model::Type do
|
|
159
211
|
end
|
160
212
|
|
161
213
|
describe "Type Usage in Models" do
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
214
|
+
before do
|
215
|
+
type_test_model = Class.new(Lutaml::Model::Serializable) do
|
216
|
+
attribute :string_symbol, :string
|
217
|
+
attribute :string_class, Lutaml::Model::Type::String
|
218
|
+
attribute :integer_value, :integer
|
219
|
+
attribute :float_value, :float
|
220
|
+
attribute :date_value, :date
|
221
|
+
attribute :time_value, :time
|
222
|
+
attribute :time_without_date_value, :time_without_date
|
223
|
+
attribute :date_time_value, :date_time
|
224
|
+
attribute :boolean_value, :boolean
|
225
|
+
attribute :hash_value, :hash
|
226
|
+
|
227
|
+
xml do
|
228
|
+
root "test"
|
229
|
+
map_element "string_symbol", to: :string_symbol
|
230
|
+
map_element "string_class", to: :string_class
|
231
|
+
map_element "integer", to: :integer_value
|
232
|
+
map_element "float", to: :float_value
|
233
|
+
map_element "date", to: :date_value
|
234
|
+
map_element "time", to: :time_value
|
235
|
+
map_element "time_without_date", to: :time_without_date_value
|
236
|
+
map_element "date_time", to: :date_time_value
|
237
|
+
map_element "boolean", to: :boolean_value
|
238
|
+
map_element "hash", to: :hash_value
|
239
|
+
end
|
186
240
|
end
|
241
|
+
|
242
|
+
stub_const("TypeTestModel", type_test_model)
|
187
243
|
end
|
188
244
|
|
189
245
|
let(:test_instance) do
|
@@ -272,5 +328,56 @@ RSpec.describe Lutaml::Model::Type do
|
|
272
328
|
expect(deserialized.hash_value).to eq({ "key" => "value" })
|
273
329
|
end
|
274
330
|
end
|
331
|
+
|
332
|
+
describe "Serialization Of Custom Type" do
|
333
|
+
let(:xml) do
|
334
|
+
<<~XML
|
335
|
+
<sample>
|
336
|
+
<custom_type>test_string</custom_type>
|
337
|
+
</sample>
|
338
|
+
XML
|
339
|
+
end
|
340
|
+
|
341
|
+
let(:xml_attribute) do
|
342
|
+
<<~XML
|
343
|
+
<sample custom_type="test_string"/>
|
344
|
+
XML
|
345
|
+
end
|
346
|
+
|
347
|
+
let(:sample_instance) { SampleModel.from_xml(xml) }
|
348
|
+
let(:sample_instance_attribute) do
|
349
|
+
SampleModelAttribute.from_xml(xml_attribute)
|
350
|
+
end
|
351
|
+
|
352
|
+
it "correctly serializes to XML" do
|
353
|
+
expected_xml = <<~XML
|
354
|
+
<sample>
|
355
|
+
<custom_type>to_xml_overrided</custom_type>
|
356
|
+
</sample>
|
357
|
+
XML
|
358
|
+
expect(sample_instance.to_xml).to be_equivalent_to(expected_xml)
|
359
|
+
end
|
360
|
+
|
361
|
+
it "correctly serializes to XML attribute" do
|
362
|
+
expected_xml = <<~XML
|
363
|
+
<sample custom_type="to_xml_overrided"/>
|
364
|
+
XML
|
365
|
+
expect(sample_instance_attribute.to_xml).to be_equivalent_to(expected_xml)
|
366
|
+
end
|
367
|
+
|
368
|
+
it "correctly serializes to JSON" do
|
369
|
+
expect(sample_instance.to_json).to eq('{"custom_type":"to_json_overrided"}')
|
370
|
+
end
|
371
|
+
|
372
|
+
it "correctly deserializes from XML" do
|
373
|
+
expect(sample_instance.custom_type).to eq("from_xml_overrided")
|
374
|
+
end
|
375
|
+
|
376
|
+
it "correctly deserializes from JSON" do
|
377
|
+
json_sample_instance = SampleModel.from_json('{"custom_type":"test_string"}')
|
378
|
+
json_sample_instance.to_json
|
379
|
+
expect(json_sample_instance.custom_type).to eq("from_json_overrided")
|
380
|
+
end
|
381
|
+
end
|
275
382
|
end
|
276
383
|
end
|
@@ -54,7 +54,19 @@ RSpec.describe Lutaml::Model::Utils do
|
|
54
54
|
}
|
55
55
|
end
|
56
56
|
|
57
|
+
let(:original_array) do
|
58
|
+
[
|
59
|
+
"one", [
|
60
|
+
"one_one", [
|
61
|
+
"one_one1", "one_one2"
|
62
|
+
],
|
63
|
+
"one_two"
|
64
|
+
]
|
65
|
+
]
|
66
|
+
end
|
67
|
+
|
57
68
|
let(:duplicate_hash) { utils.deep_dup(original_hash) }
|
69
|
+
let(:duplicate_array) { utils.deep_dup(original_array) }
|
58
70
|
|
59
71
|
it "creates deep duplicate of hash" do
|
60
72
|
expect(original_hash).to eq(duplicate_hash)
|
@@ -75,5 +87,25 @@ RSpec.describe Lutaml::Model::Utils do
|
|
75
87
|
expect(original_hash[:one][:one_two]).to eq(duplicate_hash[:one][:one_two])
|
76
88
|
expect(original_hash[:one][:one_two].object_id).not_to eq(duplicate_hash[:one][:one_two].object_id)
|
77
89
|
end
|
90
|
+
|
91
|
+
it "creates deep duplicate of array" do
|
92
|
+
expect(original_array).to eq(duplicate_array)
|
93
|
+
expect(original_array.object_id).not_to eq(duplicate_array.object_id)
|
94
|
+
|
95
|
+
expect(original_array[0]).to eq(duplicate_array[0])
|
96
|
+
expect(original_array[0].object_id).not_to eq(duplicate_array[0].object_id)
|
97
|
+
|
98
|
+
expect(original_array[1][0]).to eq(duplicate_array[1][0])
|
99
|
+
expect(original_array[1][0].object_id).not_to eq(duplicate_array[1][0].object_id)
|
100
|
+
|
101
|
+
expect(original_array[1][1][0]).to eq(duplicate_array[1][1][0])
|
102
|
+
expect(original_array[1][1][0].object_id).not_to eq(duplicate_array[1][1][0].object_id)
|
103
|
+
|
104
|
+
expect(original_array[1][1][1]).to eq(duplicate_array[1][1][1])
|
105
|
+
expect(original_array[1][1][1].object_id).not_to eq(duplicate_array[1][1][1].object_id)
|
106
|
+
|
107
|
+
expect(original_array[1][2]).to eq(duplicate_array[1][2])
|
108
|
+
expect(original_array[1][2].object_id).not_to eq(duplicate_array[1][2].object_id)
|
109
|
+
end
|
78
110
|
end
|
79
111
|
end
|