lutaml-model 0.7.3 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +1 -0
  3. data/.github/workflows/dependent-tests.yml +4 -0
  4. data/.github/workflows/rake.yml +12 -0
  5. data/.github/workflows/release.yml +3 -0
  6. data/.gitignore +6 -1
  7. data/.irbrc +1 -0
  8. data/.pryrc +1 -0
  9. data/.rubocop_todo.yml +25 -52
  10. data/README.adoc +2294 -192
  11. data/docs/custom_registers.adoc +228 -0
  12. data/docs/schema_generation.adoc +898 -0
  13. data/docs/schema_import.adoc +364 -0
  14. data/flake.lock +114 -0
  15. data/flake.nix +103 -0
  16. data/lib/lutaml/model/attribute.rb +230 -94
  17. data/lib/lutaml/model/choice.rb +30 -0
  18. data/lib/lutaml/model/collection.rb +195 -0
  19. data/lib/lutaml/model/comparable_model.rb +3 -3
  20. data/lib/lutaml/model/config.rb +26 -3
  21. data/lib/lutaml/model/constants.rb +2 -0
  22. data/lib/lutaml/model/error/element_count_out_of_range_error.rb +29 -0
  23. data/lib/lutaml/model/error/invalid_attribute_name_error.rb +15 -0
  24. data/lib/lutaml/model/error/invalid_attribute_options_error.rb +16 -0
  25. data/lib/lutaml/model/error/invalid_choice_range_error.rb +3 -5
  26. data/lib/lutaml/model/error/register/not_registrable_class_error.rb +11 -0
  27. data/lib/lutaml/model/error/type/invalid_value_error.rb +5 -3
  28. data/lib/lutaml/model/error/type/max_bound_error.rb +20 -0
  29. data/lib/lutaml/model/error/type/max_length_error.rb +20 -0
  30. data/lib/lutaml/model/error/type/min_bound_error.rb +20 -0
  31. data/lib/lutaml/model/error/type/min_length_error.rb +20 -0
  32. data/lib/lutaml/model/error/type/pattern_not_matched_error.rb +18 -0
  33. data/lib/lutaml/model/error/validation_failed_error.rb +9 -0
  34. data/lib/lutaml/model/error.rb +10 -0
  35. data/lib/lutaml/model/errors.rb +36 -0
  36. data/lib/lutaml/model/format_registry.rb +5 -2
  37. data/lib/lutaml/model/global_register.rb +41 -0
  38. data/lib/lutaml/model/{hash.rb → hash_adapter.rb} +5 -5
  39. data/lib/lutaml/model/jsonl/document.rb +14 -0
  40. data/lib/lutaml/model/jsonl/mapping.rb +19 -0
  41. data/lib/lutaml/model/jsonl/mapping_rule.rb +9 -0
  42. data/lib/lutaml/model/jsonl/standard_adapter.rb +33 -0
  43. data/lib/lutaml/model/jsonl/transform.rb +19 -0
  44. data/lib/lutaml/model/jsonl.rb +21 -0
  45. data/lib/lutaml/model/key_value_document.rb +3 -2
  46. data/lib/lutaml/model/mapping/key_value_mapping.rb +64 -4
  47. data/lib/lutaml/model/mapping/key_value_mapping_rule.rb +4 -0
  48. data/lib/lutaml/model/mapping/mapping_rule.rb +8 -3
  49. data/lib/lutaml/model/register.rb +105 -0
  50. data/lib/lutaml/model/registrable.rb +6 -0
  51. data/lib/lutaml/model/schema/base_schema.rb +64 -0
  52. data/lib/lutaml/model/schema/decorators/attribute.rb +114 -0
  53. data/lib/lutaml/model/schema/decorators/choices.rb +31 -0
  54. data/lib/lutaml/model/schema/decorators/class_definition.rb +85 -0
  55. data/lib/lutaml/model/schema/decorators/definition_collection.rb +97 -0
  56. data/lib/lutaml/model/schema/generator/definition.rb +53 -0
  57. data/lib/lutaml/model/schema/generator/definitions_collection.rb +81 -0
  58. data/lib/lutaml/model/schema/generator/properties_collection.rb +63 -0
  59. data/lib/lutaml/model/schema/generator/property.rb +110 -0
  60. data/lib/lutaml/model/schema/generator/ref.rb +24 -0
  61. data/lib/lutaml/model/schema/helpers/template_helper.rb +49 -0
  62. data/lib/lutaml/model/schema/json_schema.rb +42 -49
  63. data/lib/lutaml/model/schema/relaxng_schema.rb +14 -10
  64. data/lib/lutaml/model/schema/renderer.rb +36 -0
  65. data/lib/lutaml/model/schema/shared_methods.rb +24 -0
  66. data/lib/lutaml/model/schema/templates/model.erb +9 -0
  67. data/lib/lutaml/model/schema/xml_compiler/attribute.rb +85 -0
  68. data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +45 -0
  69. data/lib/lutaml/model/schema/xml_compiler/choice.rb +65 -0
  70. data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +27 -0
  71. data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +34 -0
  72. data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +136 -0
  73. data/lib/lutaml/model/schema/xml_compiler/element.rb +104 -0
  74. data/lib/lutaml/model/schema/xml_compiler/group.rb +97 -0
  75. data/lib/lutaml/model/schema/xml_compiler/restriction.rb +101 -0
  76. data/lib/lutaml/model/schema/xml_compiler/sequence.rb +50 -0
  77. data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +36 -0
  78. data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +189 -0
  79. data/lib/lutaml/model/schema/xml_compiler.rb +231 -587
  80. data/lib/lutaml/model/schema/xsd_schema.rb +12 -8
  81. data/lib/lutaml/model/schema/yaml_schema.rb +41 -35
  82. data/lib/lutaml/model/schema.rb +1 -0
  83. data/lib/lutaml/model/sequence.rb +60 -30
  84. data/lib/lutaml/model/serialize.rb +177 -54
  85. data/lib/lutaml/model/services/base.rb +11 -0
  86. data/lib/lutaml/model/services/logger.rb +2 -2
  87. data/lib/lutaml/model/services/rule_value_extractor.rb +92 -0
  88. data/lib/lutaml/model/services/type/validator/number.rb +25 -0
  89. data/lib/lutaml/model/services/type/validator/string.rb +52 -0
  90. data/lib/lutaml/model/services/type/validator.rb +43 -0
  91. data/lib/lutaml/model/services/validator.rb +145 -0
  92. data/lib/lutaml/model/services.rb +3 -0
  93. data/lib/lutaml/model/transform/key_value_transform.rb +68 -62
  94. data/lib/lutaml/model/transform/xml_transform.rb +46 -57
  95. data/lib/lutaml/model/transform.rb +23 -8
  96. data/lib/lutaml/model/type/boolean.rb +1 -1
  97. data/lib/lutaml/model/type/date.rb +1 -1
  98. data/lib/lutaml/model/type/date_time.rb +1 -1
  99. data/lib/lutaml/model/type/decimal.rb +11 -9
  100. data/lib/lutaml/model/type/float.rb +2 -1
  101. data/lib/lutaml/model/type/integer.rb +24 -21
  102. data/lib/lutaml/model/type/string.rb +4 -2
  103. data/lib/lutaml/model/type/time.rb +1 -1
  104. data/lib/lutaml/model/type/time_without_date.rb +1 -1
  105. data/lib/lutaml/model/type/value.rb +5 -1
  106. data/lib/lutaml/model/type.rb +5 -2
  107. data/lib/lutaml/model/utils.rb +30 -8
  108. data/lib/lutaml/model/validation.rb +6 -4
  109. data/lib/lutaml/model/version.rb +1 -1
  110. data/lib/lutaml/model/xml/document.rb +37 -19
  111. data/lib/lutaml/model/xml/mapping.rb +74 -13
  112. data/lib/lutaml/model/xml/mapping_rule.rb +10 -2
  113. data/lib/lutaml/model/xml/nokogiri_adapter.rb +5 -3
  114. data/lib/lutaml/model/xml/oga/element.rb +4 -1
  115. data/lib/lutaml/model/xml/oga_adapter.rb +4 -3
  116. data/lib/lutaml/model/xml/ox_adapter.rb +20 -6
  117. data/lib/lutaml/model/xml/xml_element.rb +3 -28
  118. data/lib/lutaml/model/xml_adapter/element.rb +1 -1
  119. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +1 -1
  120. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +1 -1
  121. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +1 -1
  122. data/lib/lutaml/model/yamls/document.rb +14 -0
  123. data/lib/lutaml/model/yamls/mapping.rb +19 -0
  124. data/lib/lutaml/model/yamls/mapping_rule.rb +9 -0
  125. data/lib/lutaml/model/yamls/standard_adapter.rb +34 -0
  126. data/lib/lutaml/model/yamls/transform.rb +19 -0
  127. data/lib/lutaml/model/yamls.rb +21 -0
  128. data/lib/lutaml/model.rb +7 -31
  129. data/spec/benchmarks/xml_parsing_benchmark_spec.rb +4 -5
  130. data/spec/fixtures/xml/advanced_test_schema.xsd +134 -0
  131. data/spec/fixtures/xml/examples/nested_categories.xml +55 -0
  132. data/spec/fixtures/xml/examples/valid_catalog.xml +43 -0
  133. data/spec/fixtures/xml/product_catalog.xsd +151 -0
  134. data/spec/fixtures/xml/specifications_schema.xsd +38 -0
  135. data/spec/lutaml/model/attribute_collection_spec.rb +101 -0
  136. data/spec/lutaml/model/attribute_spec.rb +41 -44
  137. data/spec/lutaml/model/choice_spec.rb +44 -0
  138. data/spec/lutaml/model/custom_collection_spec.rb +830 -0
  139. data/spec/lutaml/model/global_register_spec.rb +108 -0
  140. data/spec/lutaml/model/group_spec.rb +9 -3
  141. data/spec/lutaml/model/jsonl/standard_adapter_spec.rb +91 -0
  142. data/spec/lutaml/model/jsonl_spec.rb +229 -0
  143. data/spec/lutaml/model/multiple_mapping_spec.rb +1 -1
  144. data/spec/lutaml/model/register/key_value_spec.rb +275 -0
  145. data/spec/lutaml/model/register/xml_spec.rb +185 -0
  146. data/spec/lutaml/model/register_spec.rb +147 -0
  147. data/spec/lutaml/model/rule_value_extractor_spec.rb +162 -0
  148. data/spec/lutaml/model/schema/generator/definitions_collection_spec.rb +120 -0
  149. data/spec/lutaml/model/schema/json_schema_spec.rb +412 -51
  150. data/spec/lutaml/model/schema/json_schema_to_models_spec.rb +383 -0
  151. data/spec/lutaml/model/schema/xml_compiler/attribute_group_spec.rb +65 -0
  152. data/spec/lutaml/model/schema/xml_compiler/attribute_spec.rb +63 -0
  153. data/spec/lutaml/model/schema/xml_compiler/choice_spec.rb +71 -0
  154. data/spec/lutaml/model/schema/xml_compiler/complex_content_restriction_spec.rb +55 -0
  155. data/spec/lutaml/model/schema/xml_compiler/complex_content_spec.rb +37 -0
  156. data/spec/lutaml/model/schema/xml_compiler/complex_type_spec.rb +173 -0
  157. data/spec/lutaml/model/schema/xml_compiler/element_spec.rb +63 -0
  158. data/spec/lutaml/model/schema/xml_compiler/group_spec.rb +86 -0
  159. data/spec/lutaml/model/schema/xml_compiler/restriction_spec.rb +76 -0
  160. data/spec/lutaml/model/schema/xml_compiler/sequence_spec.rb +59 -0
  161. data/spec/lutaml/model/schema/xml_compiler/simple_content_spec.rb +55 -0
  162. data/spec/lutaml/model/schema/xml_compiler/simple_type_spec.rb +181 -0
  163. data/spec/lutaml/model/schema/xml_compiler_spec.rb +503 -1804
  164. data/spec/lutaml/model/schema/yaml_schema_spec.rb +249 -26
  165. data/spec/lutaml/model/sequence_spec.rb +36 -0
  166. data/spec/lutaml/model/serializable_spec.rb +31 -0
  167. data/spec/lutaml/model/type_spec.rb +8 -4
  168. data/spec/lutaml/model/utils_spec.rb +3 -3
  169. data/spec/lutaml/model/xml/derived_attributes_spec.rb +1 -1
  170. data/spec/lutaml/model/xml/root_mappings/nested_child_mappings_spec.rb +164 -0
  171. data/spec/lutaml/model/xml/xml_element_spec.rb +7 -1
  172. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +6 -6
  173. data/spec/lutaml/model/xml_adapter_spec.rb +24 -0
  174. data/spec/lutaml/model/xml_mapping_rule_spec.rb +11 -4
  175. data/spec/lutaml/model/xml_mapping_spec.rb +1 -1
  176. data/spec/lutaml/model/yamls/standard_adapter_spec.rb +183 -0
  177. data/spec/lutaml/model/yamls_spec.rb +294 -0
  178. data/spec/spec_helper.rb +1 -0
  179. metadata +106 -9
  180. data/lib/lutaml/model/schema/templates/simple_type.rb +0 -247
  181. /data/lib/lutaml/model/{hash → hash_adapter}/document.rb +0 -0
  182. /data/lib/lutaml/model/{hash → hash_adapter}/mapping.rb +0 -0
  183. /data/lib/lutaml/model/{hash → hash_adapter}/mapping_rule.rb +0 -0
  184. /data/lib/lutaml/model/{hash → hash_adapter}/standard_adapter.rb +0 -0
  185. /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