lutaml-model 0.7.2 → 0.7.5
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/.envrc +1 -0
- data/.github/workflows/release.yml +3 -0
- data/.gitignore +6 -1
- data/.irbrc +1 -0
- data/.pryrc +1 -0
- data/.rubocop_todo.yml +25 -52
- data/README.adoc +2177 -192
- data/docs/custom_registers.adoc +228 -0
- data/docs/schema_generation.adoc +898 -0
- data/docs/schema_import.adoc +364 -0
- data/flake.lock +114 -0
- data/flake.nix +103 -0
- data/lib/lutaml/model/attribute.rb +230 -94
- data/lib/lutaml/model/choice.rb +30 -0
- data/lib/lutaml/model/collection.rb +194 -0
- data/lib/lutaml/model/comparable_model.rb +3 -3
- data/lib/lutaml/model/config.rb +26 -3
- data/lib/lutaml/model/constants.rb +2 -0
- data/lib/lutaml/model/error/element_count_out_of_range_error.rb +29 -0
- data/lib/lutaml/model/error/invalid_attribute_name_error.rb +15 -0
- data/lib/lutaml/model/error/invalid_attribute_options_error.rb +16 -0
- data/lib/lutaml/model/error/invalid_choice_range_error.rb +3 -5
- data/lib/lutaml/model/error/register/not_registrable_class_error.rb +11 -0
- data/lib/lutaml/model/error/type/invalid_value_error.rb +5 -3
- data/lib/lutaml/model/error/type/max_bound_error.rb +20 -0
- data/lib/lutaml/model/error/type/max_length_error.rb +20 -0
- data/lib/lutaml/model/error/type/min_bound_error.rb +20 -0
- data/lib/lutaml/model/error/type/min_length_error.rb +20 -0
- data/lib/lutaml/model/error/type/pattern_not_matched_error.rb +18 -0
- data/lib/lutaml/model/error/validation_failed_error.rb +9 -0
- data/lib/lutaml/model/error.rb +10 -0
- data/lib/lutaml/model/errors.rb +36 -0
- data/lib/lutaml/model/format_registry.rb +5 -2
- data/lib/lutaml/model/global_register.rb +41 -0
- data/lib/lutaml/model/{hash.rb → hash_adapter.rb} +5 -5
- data/lib/lutaml/model/jsonl/document.rb +14 -0
- data/lib/lutaml/model/jsonl/mapping.rb +19 -0
- data/lib/lutaml/model/jsonl/mapping_rule.rb +9 -0
- data/lib/lutaml/model/jsonl/standard_adapter.rb +33 -0
- data/lib/lutaml/model/jsonl/transform.rb +19 -0
- data/lib/lutaml/model/jsonl.rb +21 -0
- data/lib/lutaml/model/key_value_document.rb +3 -2
- data/lib/lutaml/model/mapping/key_value_mapping.rb +64 -4
- data/lib/lutaml/model/mapping/key_value_mapping_rule.rb +4 -0
- data/lib/lutaml/model/mapping/mapping_rule.rb +8 -3
- data/lib/lutaml/model/register.rb +105 -0
- data/lib/lutaml/model/registrable.rb +6 -0
- data/lib/lutaml/model/schema/base_schema.rb +64 -0
- data/lib/lutaml/model/schema/decorators/attribute.rb +114 -0
- data/lib/lutaml/model/schema/decorators/choices.rb +31 -0
- data/lib/lutaml/model/schema/decorators/class_definition.rb +85 -0
- data/lib/lutaml/model/schema/decorators/definition_collection.rb +97 -0
- data/lib/lutaml/model/schema/generator/definition.rb +53 -0
- data/lib/lutaml/model/schema/generator/definitions_collection.rb +81 -0
- data/lib/lutaml/model/schema/generator/properties_collection.rb +63 -0
- data/lib/lutaml/model/schema/generator/property.rb +110 -0
- data/lib/lutaml/model/schema/generator/ref.rb +24 -0
- data/lib/lutaml/model/schema/helpers/template_helper.rb +49 -0
- data/lib/lutaml/model/schema/json_schema.rb +42 -49
- data/lib/lutaml/model/schema/relaxng_schema.rb +14 -10
- data/lib/lutaml/model/schema/renderer.rb +36 -0
- data/lib/lutaml/model/schema/shared_methods.rb +24 -0
- data/lib/lutaml/model/schema/templates/model.erb +9 -0
- data/lib/lutaml/model/schema/xml_compiler/attribute.rb +85 -0
- data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +45 -0
- data/lib/lutaml/model/schema/xml_compiler/choice.rb +65 -0
- data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +27 -0
- data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +34 -0
- data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +136 -0
- data/lib/lutaml/model/schema/xml_compiler/element.rb +104 -0
- data/lib/lutaml/model/schema/xml_compiler/group.rb +97 -0
- data/lib/lutaml/model/schema/xml_compiler/restriction.rb +101 -0
- data/lib/lutaml/model/schema/xml_compiler/sequence.rb +50 -0
- data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +36 -0
- data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +189 -0
- data/lib/lutaml/model/schema/xml_compiler.rb +231 -587
- data/lib/lutaml/model/schema/xsd_schema.rb +12 -8
- data/lib/lutaml/model/schema/yaml_schema.rb +41 -35
- data/lib/lutaml/model/schema.rb +1 -0
- data/lib/lutaml/model/sequence.rb +60 -30
- data/lib/lutaml/model/serialize.rb +175 -53
- data/lib/lutaml/model/services/base.rb +11 -0
- data/lib/lutaml/model/services/logger.rb +2 -2
- data/lib/lutaml/model/services/rule_value_extractor.rb +92 -0
- data/lib/lutaml/model/services/type/validator/number.rb +25 -0
- data/lib/lutaml/model/services/type/validator/string.rb +52 -0
- data/lib/lutaml/model/services/type/validator.rb +43 -0
- data/lib/lutaml/model/services/validator.rb +145 -0
- data/lib/lutaml/model/services.rb +3 -0
- data/lib/lutaml/model/transform/key_value_transform.rb +60 -50
- data/lib/lutaml/model/transform/xml_transform.rb +46 -57
- data/lib/lutaml/model/transform.rb +22 -8
- data/lib/lutaml/model/type/boolean.rb +1 -1
- data/lib/lutaml/model/type/date.rb +1 -1
- data/lib/lutaml/model/type/date_time.rb +1 -1
- data/lib/lutaml/model/type/decimal.rb +11 -9
- data/lib/lutaml/model/type/float.rb +2 -1
- data/lib/lutaml/model/type/integer.rb +24 -21
- data/lib/lutaml/model/type/string.rb +4 -2
- data/lib/lutaml/model/type/time.rb +1 -1
- data/lib/lutaml/model/type/time_without_date.rb +1 -1
- data/lib/lutaml/model/type/value.rb +5 -1
- data/lib/lutaml/model/type.rb +5 -2
- data/lib/lutaml/model/utils.rb +30 -8
- data/lib/lutaml/model/validation.rb +6 -4
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model/xml/document.rb +37 -19
- data/lib/lutaml/model/xml/mapping.rb +74 -13
- data/lib/lutaml/model/xml/mapping_rule.rb +10 -2
- data/lib/lutaml/model/xml/nokogiri_adapter.rb +5 -3
- data/lib/lutaml/model/xml/oga/element.rb +4 -1
- data/lib/lutaml/model/xml/oga_adapter.rb +4 -3
- data/lib/lutaml/model/xml/ox_adapter.rb +20 -6
- data/lib/lutaml/model/xml/xml_element.rb +3 -28
- data/lib/lutaml/model/xml.rb +1 -1
- data/lib/lutaml/model/xml_adapter/element.rb +1 -1
- data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +1 -1
- data/lib/lutaml/model/xml_adapter/oga_adapter.rb +1 -1
- data/lib/lutaml/model/xml_adapter/ox_adapter.rb +1 -1
- data/lib/lutaml/model/yamls/document.rb +14 -0
- data/lib/lutaml/model/yamls/mapping.rb +19 -0
- data/lib/lutaml/model/yamls/mapping_rule.rb +9 -0
- data/lib/lutaml/model/yamls/standard_adapter.rb +34 -0
- data/lib/lutaml/model/yamls/transform.rb +19 -0
- data/lib/lutaml/model/yamls.rb +21 -0
- data/lib/lutaml/model.rb +7 -31
- data/spec/benchmarks/xml_parsing_benchmark_spec.rb +4 -5
- data/spec/fixtures/xml/advanced_test_schema.xsd +134 -0
- data/spec/fixtures/xml/examples/nested_categories.xml +55 -0
- data/spec/fixtures/xml/examples/valid_catalog.xml +43 -0
- data/spec/fixtures/xml/product_catalog.xsd +151 -0
- data/spec/fixtures/xml/specifications_schema.xsd +38 -0
- data/spec/lutaml/model/attribute_collection_spec.rb +101 -0
- data/spec/lutaml/model/attribute_spec.rb +41 -44
- data/spec/lutaml/model/choice_spec.rb +44 -0
- data/spec/lutaml/model/custom_collection_spec.rb +830 -0
- data/spec/lutaml/model/custom_model_spec.rb +15 -3
- data/spec/lutaml/model/custom_vobject_adapter_spec.rb +6 -6
- data/spec/lutaml/model/defaults_spec.rb +5 -1
- data/spec/lutaml/model/global_register_spec.rb +108 -0
- data/spec/lutaml/model/group_spec.rb +9 -3
- data/spec/lutaml/model/jsonl/standard_adapter_spec.rb +91 -0
- data/spec/lutaml/model/jsonl_spec.rb +229 -0
- data/spec/lutaml/model/multiple_mapping_spec.rb +1 -1
- data/spec/lutaml/model/register/key_value_spec.rb +275 -0
- data/spec/lutaml/model/register/xml_spec.rb +187 -0
- data/spec/lutaml/model/register_spec.rb +147 -0
- data/spec/lutaml/model/rule_value_extractor_spec.rb +162 -0
- data/spec/lutaml/model/schema/generator/definitions_collection_spec.rb +120 -0
- data/spec/lutaml/model/schema/json_schema_spec.rb +412 -51
- data/spec/lutaml/model/schema/json_schema_to_models_spec.rb +383 -0
- data/spec/lutaml/model/schema/xml_compiler/attribute_group_spec.rb +65 -0
- data/spec/lutaml/model/schema/xml_compiler/attribute_spec.rb +63 -0
- data/spec/lutaml/model/schema/xml_compiler/choice_spec.rb +71 -0
- data/spec/lutaml/model/schema/xml_compiler/complex_content_restriction_spec.rb +55 -0
- data/spec/lutaml/model/schema/xml_compiler/complex_content_spec.rb +37 -0
- data/spec/lutaml/model/schema/xml_compiler/complex_type_spec.rb +173 -0
- data/spec/lutaml/model/schema/xml_compiler/element_spec.rb +63 -0
- data/spec/lutaml/model/schema/xml_compiler/group_spec.rb +86 -0
- data/spec/lutaml/model/schema/xml_compiler/restriction_spec.rb +76 -0
- data/spec/lutaml/model/schema/xml_compiler/sequence_spec.rb +59 -0
- data/spec/lutaml/model/schema/xml_compiler/simple_content_spec.rb +55 -0
- data/spec/lutaml/model/schema/xml_compiler/simple_type_spec.rb +181 -0
- data/spec/lutaml/model/schema/xml_compiler_spec.rb +503 -1804
- data/spec/lutaml/model/schema/yaml_schema_spec.rb +249 -26
- data/spec/lutaml/model/sequence_spec.rb +36 -0
- data/spec/lutaml/model/serializable_spec.rb +31 -0
- data/spec/lutaml/model/type_spec.rb +8 -4
- data/spec/lutaml/model/utils_spec.rb +3 -3
- data/spec/lutaml/model/xml/derived_attributes_spec.rb +1 -1
- data/spec/lutaml/model/xml/xml_element_spec.rb +7 -1
- data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +6 -6
- data/spec/lutaml/model/xml_adapter_spec.rb +24 -0
- data/spec/lutaml/model/xml_mapping_rule_spec.rb +11 -4
- data/spec/lutaml/model/xml_mapping_spec.rb +1 -1
- data/spec/lutaml/model/xml_spec.rb +6 -6
- data/spec/lutaml/model/yamls/standard_adapter_spec.rb +183 -0
- data/spec/lutaml/model/yamls_spec.rb +294 -0
- data/spec/spec_helper.rb +1 -0
- metadata +105 -9
- data/lib/lutaml/model/schema/templates/simple_type.rb +0 -247
- /data/lib/lutaml/model/{hash → hash_adapter}/document.rb +0 -0
- /data/lib/lutaml/model/{hash → hash_adapter}/mapping.rb +0 -0
- /data/lib/lutaml/model/{hash → hash_adapter}/mapping_rule.rb +0 -0
- /data/lib/lutaml/model/{hash → hash_adapter}/standard_adapter.rb +0 -0
- /data/lib/lutaml/model/{hash → hash_adapter}/transform.rb +0 -0
@@ -0,0 +1,830 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "lutaml/model"
|
3
|
+
|
4
|
+
module CustomCollection
|
5
|
+
# Custom type classes for testing collections with Register
|
6
|
+
|
7
|
+
class Text < Lutaml::Model::Type::String
|
8
|
+
def to_xml
|
9
|
+
"Text class: #{value}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Basic model for testing collections
|
14
|
+
|
15
|
+
class Publication < Lutaml::Model::Serializable
|
16
|
+
attribute :title, :string
|
17
|
+
attribute :year, :integer
|
18
|
+
attribute :author, :string
|
19
|
+
|
20
|
+
xml do
|
21
|
+
root "publication"
|
22
|
+
|
23
|
+
map_attribute "title", to: :title
|
24
|
+
map_attribute "year", to: :year
|
25
|
+
map_attribute "author", to: :author
|
26
|
+
end
|
27
|
+
|
28
|
+
key_value do
|
29
|
+
map "title", to: :title
|
30
|
+
map "year", to: :year
|
31
|
+
map "author", to: :author
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Item < Lutaml::Model::Serializable
|
36
|
+
attribute :id, :string
|
37
|
+
attribute :name, :string
|
38
|
+
attribute :description, :text
|
39
|
+
|
40
|
+
xml do
|
41
|
+
map_attribute "id", to: :id
|
42
|
+
map_element "name", to: :name
|
43
|
+
map_element "description", to: :description
|
44
|
+
end
|
45
|
+
|
46
|
+
key_value do
|
47
|
+
map "id", to: :id
|
48
|
+
map "name", to: :name
|
49
|
+
map "description", to: :description
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Custom collection class that extends Lutaml::Model::Collection
|
54
|
+
class ItemCollection < Lutaml::Model::Collection
|
55
|
+
instances :items, Item
|
56
|
+
|
57
|
+
xml do
|
58
|
+
root "items"
|
59
|
+
map_element "item", to: :items
|
60
|
+
end
|
61
|
+
|
62
|
+
key_value do
|
63
|
+
root "items"
|
64
|
+
map_instances to: :items
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class OrderedItemCollection < Lutaml::Model::Collection
|
69
|
+
instances :items, Item
|
70
|
+
ordered by: :id, order: :desc
|
71
|
+
end
|
72
|
+
|
73
|
+
# Custom collection class that extends Lutaml::Model::Collection
|
74
|
+
class ItemNoRootCollection < Lutaml::Model::Collection
|
75
|
+
instances :items, Item
|
76
|
+
|
77
|
+
xml do
|
78
|
+
no_root
|
79
|
+
|
80
|
+
map_element "item", to: :items
|
81
|
+
end
|
82
|
+
|
83
|
+
key_value do
|
84
|
+
map_instances to: :items
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Collection with keyed elements
|
89
|
+
class KeyedItemCollection < Lutaml::Model::Collection
|
90
|
+
instances :items, Item
|
91
|
+
|
92
|
+
key_value do
|
93
|
+
map_key to_instance: :id
|
94
|
+
map_instances to: :items
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Collection with keyed elements and value mapping
|
99
|
+
class KeyedValueItemCollection < Lutaml::Model::Collection
|
100
|
+
instances :items, Item
|
101
|
+
|
102
|
+
key_value do
|
103
|
+
map_key to_instance: :id
|
104
|
+
map_value as_attribute: :name
|
105
|
+
map_instances to: :items
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Collection with child mappings
|
110
|
+
class ChildMappedItemCollection < Lutaml::Model::Collection
|
111
|
+
instances :items, Item
|
112
|
+
|
113
|
+
key_value do
|
114
|
+
map "items", to: :items,
|
115
|
+
child_mappings: {
|
116
|
+
id: :key,
|
117
|
+
name: ["details", "name"],
|
118
|
+
description: ["details", "description"],
|
119
|
+
}
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Collection with value map
|
124
|
+
class ValueMappedItemCollection < Lutaml::Model::Collection
|
125
|
+
instances :items, Item
|
126
|
+
|
127
|
+
xml do
|
128
|
+
root "items"
|
129
|
+
map_element "item", to: :items, value_map: {
|
130
|
+
from: { empty: :empty, omitted: :omitted, nil: :nil },
|
131
|
+
to: { empty: :empty, omitted: :omitted, nil: :nil },
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
key_value do
|
136
|
+
root "items"
|
137
|
+
map "items", to: :items, value_map: {
|
138
|
+
from: { empty: :empty, omitted: :omitted, nil: :nil },
|
139
|
+
to: { empty: :empty, omitted: :omitted, nil: :nil },
|
140
|
+
}
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Collection with polymorphic items
|
145
|
+
class BaseItem < Lutaml::Model::Serializable
|
146
|
+
attribute :_class, :string, polymorphic_class: true
|
147
|
+
attribute :name, :string
|
148
|
+
|
149
|
+
xml do
|
150
|
+
map_attribute "item-type", to: :_class, polymorphic_map: {
|
151
|
+
"basic" => "CustomCollection::BasicItem",
|
152
|
+
"advanced" => "CustomCollection::AdvancedItem",
|
153
|
+
}
|
154
|
+
map_element "name", to: :name
|
155
|
+
end
|
156
|
+
|
157
|
+
key_value do
|
158
|
+
map "_class", to: :_class, polymorphic_map: {
|
159
|
+
"Basic" => "CustomCollection::BasicItem",
|
160
|
+
"Advanced" => "CustomCollection::AdvancedItem",
|
161
|
+
}
|
162
|
+
map "name", to: :name
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
class BasicItem < BaseItem
|
167
|
+
attribute :description, :string
|
168
|
+
|
169
|
+
xml do
|
170
|
+
map_element "description", to: :description
|
171
|
+
end
|
172
|
+
|
173
|
+
key_value do
|
174
|
+
map "description", to: :description
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
class AdvancedItem < BaseItem
|
179
|
+
attribute :details, :string
|
180
|
+
attribute :priority, :integer
|
181
|
+
|
182
|
+
xml do
|
183
|
+
map_element "details", to: :details
|
184
|
+
map_element "priority", to: :priority
|
185
|
+
end
|
186
|
+
|
187
|
+
key_value do
|
188
|
+
map "details", to: :details
|
189
|
+
map "priority", to: :priority
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
class PolymorphicItemCollection < Lutaml::Model::Collection
|
194
|
+
instances :items, BaseItem
|
195
|
+
|
196
|
+
xml do
|
197
|
+
root "items"
|
198
|
+
|
199
|
+
map_element "item", to: :items
|
200
|
+
end
|
201
|
+
|
202
|
+
key_value do
|
203
|
+
root "items"
|
204
|
+
|
205
|
+
map_instances to: :items
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
RSpec.describe CustomCollection do
|
211
|
+
let(:items) do
|
212
|
+
[
|
213
|
+
{ id: "1", name: "Item 1", description: "Description 1" },
|
214
|
+
{ id: "2", name: "Item 2", description: "Description 2" },
|
215
|
+
]
|
216
|
+
end
|
217
|
+
|
218
|
+
describe "ItemCollection" do
|
219
|
+
before do
|
220
|
+
Lutaml::Model::GlobalRegister.register(register)
|
221
|
+
Lutaml::Model::Config.default_register = register.id
|
222
|
+
register.register_model(CustomCollection::Text, id: :text)
|
223
|
+
end
|
224
|
+
|
225
|
+
let(:collection) { CustomCollection::ItemCollection.new(items) }
|
226
|
+
let(:register) { Lutaml::Model::Register.new(:collections) }
|
227
|
+
|
228
|
+
let(:xml) do
|
229
|
+
<<~XML
|
230
|
+
<items>
|
231
|
+
<item id="1">
|
232
|
+
<name>Item 1</name>
|
233
|
+
<description>Description 1</description>
|
234
|
+
</item>
|
235
|
+
<item id="2">
|
236
|
+
<name>Item 2</name>
|
237
|
+
<description>Description 2</description>
|
238
|
+
</item>
|
239
|
+
</items>
|
240
|
+
XML
|
241
|
+
end
|
242
|
+
|
243
|
+
let(:yaml) do
|
244
|
+
<<~YAML.strip
|
245
|
+
---
|
246
|
+
items:
|
247
|
+
- id: '1'
|
248
|
+
name: Item 1
|
249
|
+
description: Description 1
|
250
|
+
- id: '2'
|
251
|
+
name: Item 2
|
252
|
+
description: Description 2
|
253
|
+
YAML
|
254
|
+
end
|
255
|
+
|
256
|
+
it { expect(collection.items.size).to eq(2) }
|
257
|
+
it { expect(collection.items.first.id).to eq("1") }
|
258
|
+
it { expect(collection.items.first.name).to eq("Item 1") }
|
259
|
+
it { expect(collection.items.first.description).to eq("Description 1") }
|
260
|
+
it { expect(collection.items.last.id).to eq("2") }
|
261
|
+
it { expect(collection.items.last.name).to eq("Item 2") }
|
262
|
+
it { expect(collection.items.last.description).to eq("Description 2") }
|
263
|
+
|
264
|
+
it "serializes to XML" do
|
265
|
+
register.register_global_type_substitution(
|
266
|
+
from_type: CustomCollection::Text,
|
267
|
+
to_type: Lutaml::Model::Type::String,
|
268
|
+
)
|
269
|
+
expect(collection.to_xml.strip).to eq(xml.strip)
|
270
|
+
end
|
271
|
+
|
272
|
+
it "deserializes from XML" do
|
273
|
+
expect(CustomCollection::ItemCollection.from_xml(xml)).to eq(collection)
|
274
|
+
end
|
275
|
+
|
276
|
+
it "serializes to YAML" do
|
277
|
+
expect(collection.to_yaml.strip).to eq(yaml)
|
278
|
+
end
|
279
|
+
|
280
|
+
it "deserializes from YAML" do
|
281
|
+
expect(CustomCollection::ItemCollection.from_yaml(yaml)).to eq(collection)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
describe "ItemNoRootCollection" do
|
286
|
+
before do
|
287
|
+
Lutaml::Model::GlobalRegister.register(register)
|
288
|
+
Lutaml::Model::Config.default_register = register
|
289
|
+
register.register_model(CustomCollection::Text, id: :text)
|
290
|
+
end
|
291
|
+
|
292
|
+
let(:register) { Lutaml::Model::Register.new(:no_collections) }
|
293
|
+
|
294
|
+
let(:no_root_collection) do
|
295
|
+
CustomCollection::ItemNoRootCollection.new(items)
|
296
|
+
end
|
297
|
+
|
298
|
+
let(:xml_no_root) do
|
299
|
+
<<~XML.strip
|
300
|
+
<item id="1">
|
301
|
+
<name>Item 1</name>
|
302
|
+
<description>Description 1</description>
|
303
|
+
</item>
|
304
|
+
<item id="2">
|
305
|
+
<name>Item 2</name>
|
306
|
+
<description>Description 2</description>
|
307
|
+
</item>
|
308
|
+
XML
|
309
|
+
end
|
310
|
+
|
311
|
+
let(:expected_xml_no_root) do
|
312
|
+
<<~XML.strip
|
313
|
+
<item id="1">
|
314
|
+
<name>Item 1</name>
|
315
|
+
<description>Text class: Description 1</description>
|
316
|
+
</item>
|
317
|
+
<item id="2">
|
318
|
+
<name>Item 2</name>
|
319
|
+
<description>Text class: Description 2</description>
|
320
|
+
</item>
|
321
|
+
XML
|
322
|
+
end
|
323
|
+
|
324
|
+
let(:yaml_no_root) do
|
325
|
+
<<~YAML.strip
|
326
|
+
---
|
327
|
+
- id: '1'
|
328
|
+
name: Item 1
|
329
|
+
description: Description 1
|
330
|
+
- id: '2'
|
331
|
+
name: Item 2
|
332
|
+
description: Description 2
|
333
|
+
YAML
|
334
|
+
end
|
335
|
+
|
336
|
+
it "serializes to XML" do
|
337
|
+
expect(no_root_collection.to_xml.strip).to eq(expected_xml_no_root)
|
338
|
+
end
|
339
|
+
|
340
|
+
it "deserializes from XML" do
|
341
|
+
expect(CustomCollection::ItemNoRootCollection.from_xml(xml_no_root))
|
342
|
+
.to eq(no_root_collection)
|
343
|
+
end
|
344
|
+
|
345
|
+
it "serializes to YAML" do
|
346
|
+
expect(no_root_collection.to_yaml.strip).to eq(yaml_no_root)
|
347
|
+
end
|
348
|
+
|
349
|
+
it "deserializes from YAML" do
|
350
|
+
expect(CustomCollection::ItemNoRootCollection.from_yaml(yaml_no_root))
|
351
|
+
.to eq(no_root_collection)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
describe "KeyedItemCollection" do
|
356
|
+
let(:yaml_keyed) do
|
357
|
+
<<~YAML
|
358
|
+
---
|
359
|
+
item1:
|
360
|
+
name: Item 1
|
361
|
+
description: Description 1
|
362
|
+
item2:
|
363
|
+
name: Item 2
|
364
|
+
description: Description 2
|
365
|
+
YAML
|
366
|
+
end
|
367
|
+
|
368
|
+
let(:expected_object) do
|
369
|
+
CustomCollection::KeyedItemCollection.new(
|
370
|
+
[
|
371
|
+
CustomCollection::Item.new(
|
372
|
+
id: "item1",
|
373
|
+
name: "Item 1",
|
374
|
+
description: "Description 1",
|
375
|
+
),
|
376
|
+
CustomCollection::Item.new(
|
377
|
+
id: "item2",
|
378
|
+
name: "Item 2",
|
379
|
+
description: "Description 2",
|
380
|
+
),
|
381
|
+
],
|
382
|
+
)
|
383
|
+
end
|
384
|
+
|
385
|
+
it "deserializes from YAML with keyed elements" do
|
386
|
+
parsed = CustomCollection::KeyedItemCollection.from_yaml(yaml_keyed)
|
387
|
+
|
388
|
+
expect(parsed).to eq(expected_object)
|
389
|
+
end
|
390
|
+
|
391
|
+
it "serializes to YAML with keyed elements" do
|
392
|
+
collection = CustomCollection::KeyedItemCollection.new(
|
393
|
+
[
|
394
|
+
{ id: "item1", name: "Item 1", description: "Description 1" },
|
395
|
+
{ id: "item2", name: "Item 2", description: "Description 2" },
|
396
|
+
],
|
397
|
+
)
|
398
|
+
expect(collection.to_yaml.strip).to eq(yaml_keyed.strip)
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
describe "KeyedValueItemCollection" do
|
403
|
+
let(:yaml) do
|
404
|
+
<<~YAML
|
405
|
+
---
|
406
|
+
item1: Item 1
|
407
|
+
item2: Item 2
|
408
|
+
YAML
|
409
|
+
end
|
410
|
+
|
411
|
+
it "deserializes from YAML with keyed elements and value mapping" do
|
412
|
+
parsed = CustomCollection::KeyedValueItemCollection.from_yaml(yaml)
|
413
|
+
expect(parsed.items.size).to eq(2)
|
414
|
+
expect(parsed.items.first.id).to eq("item1")
|
415
|
+
expect(parsed.items.first.name).to eq("Item 1")
|
416
|
+
expect(parsed.items.last.id).to eq("item2")
|
417
|
+
expect(parsed.items.last.name).to eq("Item 2")
|
418
|
+
end
|
419
|
+
|
420
|
+
it "serializes to YAML with keyed elements and value mapping" do
|
421
|
+
collection = CustomCollection::KeyedValueItemCollection.new(
|
422
|
+
[
|
423
|
+
{ id: "item1", name: "Item 1" },
|
424
|
+
{ id: "item2", name: "Item 2" },
|
425
|
+
],
|
426
|
+
)
|
427
|
+
expect(collection.to_yaml.strip).to eq(yaml.strip)
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
describe "ChildMappedItemCollection" do
|
432
|
+
let(:yaml) do
|
433
|
+
<<~YAML
|
434
|
+
---
|
435
|
+
"1":
|
436
|
+
details:
|
437
|
+
name: Item 1
|
438
|
+
description: Description 1
|
439
|
+
"2":
|
440
|
+
details:
|
441
|
+
name: Item 2
|
442
|
+
description: Description 2
|
443
|
+
YAML
|
444
|
+
end
|
445
|
+
|
446
|
+
it "deserializes from YAML with child mappings" do
|
447
|
+
parsed = CustomCollection::ChildMappedItemCollection.from_yaml(yaml)
|
448
|
+
|
449
|
+
expect(parsed.items.size).to eq(2)
|
450
|
+
expect(parsed.items.first.id).to eq("1")
|
451
|
+
expect(parsed.items.first.name).to eq("Item 1")
|
452
|
+
expect(parsed.items.first.description).to eq("Description 1")
|
453
|
+
expect(parsed.items.last.id).to eq("2")
|
454
|
+
expect(parsed.items.last.name).to eq("Item 2")
|
455
|
+
expect(parsed.items.last.description).to eq("Description 2")
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
describe "ValueMappedItemCollection" do
|
460
|
+
let(:empty_collection) do
|
461
|
+
CustomCollection::ValueMappedItemCollection.from_yaml("items: []")
|
462
|
+
end
|
463
|
+
|
464
|
+
let(:nil_collection) do
|
465
|
+
CustomCollection::ValueMappedItemCollection.from_yaml("items:")
|
466
|
+
end
|
467
|
+
|
468
|
+
it "returns empty collection" do
|
469
|
+
expect(empty_collection.items).to eq([])
|
470
|
+
end
|
471
|
+
|
472
|
+
it "empty collection serialized to XML is <items/>" do
|
473
|
+
expect(empty_collection.to_xml).to include("<items/>")
|
474
|
+
end
|
475
|
+
|
476
|
+
it "empty collection serialized to YAML is items: []" do
|
477
|
+
expect(empty_collection.to_yaml).to include("items: []")
|
478
|
+
end
|
479
|
+
|
480
|
+
it "returns nil collection" do
|
481
|
+
expect(nil_collection.items).to be_nil
|
482
|
+
end
|
483
|
+
|
484
|
+
it "nil collection serialized to XML is <item xsi:nil=\"true\"/>" do
|
485
|
+
expect(nil_collection.to_xml).to include("<item xsi:nil=\"true\"/>")
|
486
|
+
end
|
487
|
+
|
488
|
+
it "nil collection serialized to YAML is items:" do
|
489
|
+
expect(nil_collection.to_yaml.strip).to eq("---\nitems:")
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
describe "PolymorphicItemCollection" do
|
494
|
+
let(:xml) do
|
495
|
+
<<~XML
|
496
|
+
<items>
|
497
|
+
<item item-type="basic">
|
498
|
+
<name>Basic Item</name>
|
499
|
+
<description>Basic Description</description>
|
500
|
+
</item>
|
501
|
+
<item item-type="advanced">
|
502
|
+
<name>Advanced Item</name>
|
503
|
+
<details>Advanced Details</details>
|
504
|
+
<priority>1</priority>
|
505
|
+
</item>
|
506
|
+
</items>
|
507
|
+
XML
|
508
|
+
end
|
509
|
+
|
510
|
+
let(:yaml) do
|
511
|
+
<<~YAML
|
512
|
+
---
|
513
|
+
items:
|
514
|
+
- _class: Basic
|
515
|
+
name: Basic Item
|
516
|
+
description: Basic Description
|
517
|
+
- _class: Advanced
|
518
|
+
name: Advanced Item
|
519
|
+
details: Advanced Details
|
520
|
+
priority: 1
|
521
|
+
YAML
|
522
|
+
end
|
523
|
+
|
524
|
+
it "deserializes from XML with polymorphic items" do
|
525
|
+
parsed = CustomCollection::PolymorphicItemCollection.from_xml(xml)
|
526
|
+
expect(parsed.items.size).to eq(2)
|
527
|
+
expect(parsed.items.first).to be_a(CustomCollection::BasicItem)
|
528
|
+
expect(parsed.items.first.name).to eq("Basic Item")
|
529
|
+
expect(parsed.items.first.description).to eq("Basic Description")
|
530
|
+
expect(parsed.items.last).to be_a(CustomCollection::AdvancedItem)
|
531
|
+
expect(parsed.items.last.name).to eq("Advanced Item")
|
532
|
+
expect(parsed.items.last.details).to eq("Advanced Details")
|
533
|
+
expect(parsed.items.last.priority).to eq(1)
|
534
|
+
end
|
535
|
+
|
536
|
+
it "deserializes from YAML with polymorphic items" do
|
537
|
+
parsed = CustomCollection::PolymorphicItemCollection.from_yaml(yaml)
|
538
|
+
expect(parsed.items.size).to eq(2)
|
539
|
+
expect(parsed.items.first).to be_a(CustomCollection::BasicItem)
|
540
|
+
expect(parsed.items.first.name).to eq("Basic Item")
|
541
|
+
expect(parsed.items.first.description).to eq("Basic Description")
|
542
|
+
expect(parsed.items.last).to be_a(CustomCollection::AdvancedItem)
|
543
|
+
expect(parsed.items.last.name).to eq("Advanced Item")
|
544
|
+
expect(parsed.items.last.details).to eq("Advanced Details")
|
545
|
+
expect(parsed.items.last.priority).to eq(1)
|
546
|
+
end
|
547
|
+
|
548
|
+
it "serializes to XML with polymorphic items" do
|
549
|
+
collection = CustomCollection::PolymorphicItemCollection.new(
|
550
|
+
[
|
551
|
+
CustomCollection::BasicItem.new(
|
552
|
+
_class: "basic",
|
553
|
+
name: "Basic Item",
|
554
|
+
description: "Basic Description",
|
555
|
+
),
|
556
|
+
CustomCollection::AdvancedItem.new(
|
557
|
+
_class: "advanced",
|
558
|
+
name: "Advanced Item",
|
559
|
+
details: "Advanced Details",
|
560
|
+
priority: 1,
|
561
|
+
),
|
562
|
+
],
|
563
|
+
)
|
564
|
+
|
565
|
+
expect(collection.to_xml.strip).to eq(xml.strip)
|
566
|
+
end
|
567
|
+
|
568
|
+
it "serializes to YAML with polymorphic items" do
|
569
|
+
collection = CustomCollection::PolymorphicItemCollection.new(
|
570
|
+
[
|
571
|
+
CustomCollection::BasicItem.new(
|
572
|
+
_class: "Basic",
|
573
|
+
name: "Basic Item",
|
574
|
+
description: "Basic Description",
|
575
|
+
),
|
576
|
+
CustomCollection::AdvancedItem.new(
|
577
|
+
_class: "Advanced",
|
578
|
+
name: "Advanced Item",
|
579
|
+
details: "Advanced Details",
|
580
|
+
priority: 1,
|
581
|
+
),
|
582
|
+
],
|
583
|
+
)
|
584
|
+
expect(collection.to_yaml.strip).to eq(yaml.strip)
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
describe "Sort Functionality" do
|
589
|
+
let(:items) do
|
590
|
+
[
|
591
|
+
{ id: "3", name: "Item 3", description: "Description 3" },
|
592
|
+
{ id: "1", name: "Item 1", description: "Description 1" },
|
593
|
+
{ id: "2", name: "Item 2", description: "Description 2" },
|
594
|
+
]
|
595
|
+
end
|
596
|
+
|
597
|
+
describe "with order option" do
|
598
|
+
let(:asc_collection_class) do
|
599
|
+
Class.new(Lutaml::Model::Collection) do
|
600
|
+
instances :items, CustomCollection::Item
|
601
|
+
ordered by: :id, order: :asc
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
605
|
+
let(:desc_collection_class) do
|
606
|
+
Class.new(Lutaml::Model::Collection) do
|
607
|
+
instances :items, CustomCollection::Item
|
608
|
+
ordered by: :id, order: :desc
|
609
|
+
end
|
610
|
+
end
|
611
|
+
|
612
|
+
it "sorts items in ascending order when order: :asc is specified" do
|
613
|
+
collection = asc_collection_class.new(items)
|
614
|
+
expect(collection.items.map(&:id)).to eq(["1", "2", "3"])
|
615
|
+
end
|
616
|
+
|
617
|
+
it "sorts items in descending order when order: :desc is specified" do
|
618
|
+
collection = desc_collection_class.new(items)
|
619
|
+
expect(collection.items.map(&:id)).to eq(["3", "2", "1"])
|
620
|
+
end
|
621
|
+
|
622
|
+
it "maintains ascending order after adding new items" do
|
623
|
+
collection = asc_collection_class.new(items)
|
624
|
+
new_item = CustomCollection::Item.new(id: "0", name: "Item 0", description: "Description 0")
|
625
|
+
collection << new_item
|
626
|
+
expect(collection.items.map(&:id)).to eq(["0", "1", "2", "3"])
|
627
|
+
end
|
628
|
+
|
629
|
+
it "maintains descending order after adding new items" do
|
630
|
+
collection = desc_collection_class.new(items)
|
631
|
+
new_item = CustomCollection::Item.new(id: "4", name: "Item 4", description: "Description 4")
|
632
|
+
collection << new_item
|
633
|
+
expect(collection.items.map(&:id)).to eq(["4", "3", "2", "1"])
|
634
|
+
end
|
635
|
+
end
|
636
|
+
end
|
637
|
+
|
638
|
+
describe "Collection methods" do
|
639
|
+
let(:items) do
|
640
|
+
[
|
641
|
+
{ id: "1", name: "Item 1", description: "Description 1" },
|
642
|
+
{ id: "2", name: "Item 2", description: "Description 2" },
|
643
|
+
]
|
644
|
+
end
|
645
|
+
|
646
|
+
let(:collection) { CustomCollection::ItemCollection.new(items) }
|
647
|
+
|
648
|
+
describe "#filter" do
|
649
|
+
it "returns a new collection with filtered items" do
|
650
|
+
filtered = collection.filter { |item| item.id == "1" }
|
651
|
+
expect(filtered).to eq([collection[0]])
|
652
|
+
end
|
653
|
+
end
|
654
|
+
|
655
|
+
describe "#reject" do
|
656
|
+
it "returns a new collection with rejected items" do
|
657
|
+
rejected = collection.reject { |item| item.id == "1" }
|
658
|
+
expect(rejected).to eq([collection[1]])
|
659
|
+
end
|
660
|
+
end
|
661
|
+
|
662
|
+
describe "#select" do
|
663
|
+
it "returns a new collection with selected items" do
|
664
|
+
selected = collection.select { |item| item.id == "1" }
|
665
|
+
expect(selected).to eq([collection[0]])
|
666
|
+
end
|
667
|
+
end
|
668
|
+
|
669
|
+
describe "#map" do
|
670
|
+
it "returns a new collection with mapped items" do
|
671
|
+
mapped = collection.map(&:name)
|
672
|
+
expect(mapped).to eq(["Item 1", "Item 2"])
|
673
|
+
end
|
674
|
+
end
|
675
|
+
|
676
|
+
describe "#find" do
|
677
|
+
it "returns the first item that matches the condition" do
|
678
|
+
found = collection.find { |item| item.id == "1" }
|
679
|
+
expect(found).to eq(collection[0])
|
680
|
+
end
|
681
|
+
end
|
682
|
+
|
683
|
+
describe "#find_all" do
|
684
|
+
it "returns all items that match the condition" do
|
685
|
+
found = collection.find_all { |item| item.id == "1" }
|
686
|
+
expect(found).to eq([collection[0]])
|
687
|
+
end
|
688
|
+
end
|
689
|
+
|
690
|
+
describe "#count" do
|
691
|
+
it "returns the number of items in the collection" do
|
692
|
+
expect(collection.count).to eq(2)
|
693
|
+
end
|
694
|
+
end
|
695
|
+
|
696
|
+
describe "#empty?" do
|
697
|
+
it "returns true if the collection is empty" do
|
698
|
+
empty_collection = CustomCollection::ItemCollection.new([])
|
699
|
+
expect(empty_collection.empty?).to be true
|
700
|
+
end
|
701
|
+
|
702
|
+
it "returns false if the collection is not empty" do
|
703
|
+
expect(collection.empty?).to be false
|
704
|
+
end
|
705
|
+
end
|
706
|
+
end
|
707
|
+
|
708
|
+
describe "Numeric Validations" do
|
709
|
+
before do
|
710
|
+
publication_collection = Class.new(Lutaml::Model::Collection) do
|
711
|
+
instances(:publications, CustomCollection::Publication) do
|
712
|
+
validates :year, numericality: { greater_than: 1900 }
|
713
|
+
end
|
714
|
+
end
|
715
|
+
stub_const("PublicationCollection", publication_collection)
|
716
|
+
end
|
717
|
+
|
718
|
+
let(:valid_publication) do
|
719
|
+
CustomCollection::Publication.new(
|
720
|
+
title: "Publication 1",
|
721
|
+
year: 2000,
|
722
|
+
author: "Author 1",
|
723
|
+
)
|
724
|
+
end
|
725
|
+
|
726
|
+
let(:invalid_publication) do
|
727
|
+
CustomCollection::Publication.new(
|
728
|
+
title: "Publication 1",
|
729
|
+
year: 1800,
|
730
|
+
author: "Author 1",
|
731
|
+
)
|
732
|
+
end
|
733
|
+
|
734
|
+
it "raises error if numeric values are not valid" do
|
735
|
+
collection = PublicationCollection.new(
|
736
|
+
[valid_publication, invalid_publication],
|
737
|
+
)
|
738
|
+
|
739
|
+
expect do
|
740
|
+
collection.validate!
|
741
|
+
end.to raise_error(
|
742
|
+
Lutaml::Model::ValidationError,
|
743
|
+
/`year value is `1800`, which is not greater than 1900`/,
|
744
|
+
)
|
745
|
+
end
|
746
|
+
|
747
|
+
it "does not raise error if numeric values are valid" do
|
748
|
+
collection = PublicationCollection.new([valid_publication])
|
749
|
+
expect { collection.validate! }.not_to raise_error
|
750
|
+
end
|
751
|
+
end
|
752
|
+
|
753
|
+
describe "Presence Validations" do
|
754
|
+
before do
|
755
|
+
publication_collection = Class.new(Lutaml::Model::Collection) do
|
756
|
+
instances(:publications, CustomCollection::Publication) do
|
757
|
+
validates :title, presence: true
|
758
|
+
end
|
759
|
+
end
|
760
|
+
stub_const("PublicationCollection", publication_collection)
|
761
|
+
end
|
762
|
+
|
763
|
+
it "raises error if title is not present" do
|
764
|
+
collection = PublicationCollection.new(
|
765
|
+
[CustomCollection::Publication.new(
|
766
|
+
year: 2000,
|
767
|
+
author: "Author 1",
|
768
|
+
)],
|
769
|
+
)
|
770
|
+
|
771
|
+
expect do
|
772
|
+
collection.validate!
|
773
|
+
end.to raise_error(
|
774
|
+
Lutaml::Model::ValidationError,
|
775
|
+
/`title` is required/,
|
776
|
+
)
|
777
|
+
end
|
778
|
+
|
779
|
+
it "does not raise error if title is present" do
|
780
|
+
collection = PublicationCollection.new(
|
781
|
+
[
|
782
|
+
CustomCollection::Publication.new(
|
783
|
+
title: "Title",
|
784
|
+
),
|
785
|
+
],
|
786
|
+
)
|
787
|
+
expect { collection.validate! }.not_to raise_error
|
788
|
+
end
|
789
|
+
end
|
790
|
+
|
791
|
+
describe "Custom Validations" do
|
792
|
+
before do
|
793
|
+
publication_collection = Class.new(Lutaml::Model::Collection) do
|
794
|
+
instances(:publications, CustomCollection::Publication) do
|
795
|
+
validate :must_have_author
|
796
|
+
|
797
|
+
def must_have_author(publications)
|
798
|
+
publications.each do |publication|
|
799
|
+
next unless publication.author.nil?
|
800
|
+
|
801
|
+
errors.add(:author, "`#{publication.title}` must have an author")
|
802
|
+
end
|
803
|
+
end
|
804
|
+
end
|
805
|
+
end
|
806
|
+
stub_const("PublicationCollection", publication_collection)
|
807
|
+
end
|
808
|
+
|
809
|
+
it "validates custom values" do
|
810
|
+
collection = PublicationCollection.new(
|
811
|
+
[
|
812
|
+
CustomCollection::Publication.new(
|
813
|
+
title: "Publication 1",
|
814
|
+
author: "Author 1",
|
815
|
+
),
|
816
|
+
CustomCollection::Publication.new(
|
817
|
+
title: "Publication 2",
|
818
|
+
),
|
819
|
+
],
|
820
|
+
)
|
821
|
+
|
822
|
+
expect do
|
823
|
+
collection.validate!
|
824
|
+
end.to raise_error(
|
825
|
+
Lutaml::Model::ValidationError,
|
826
|
+
/`Publication 2` must have an author/,
|
827
|
+
)
|
828
|
+
end
|
829
|
+
end
|
830
|
+
end
|