lutaml-model 0.8.0 → 0.8.1

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-repos.json +9 -0
  3. data/.github/workflows/downstream-performance.yml +0 -3
  4. data/.rubocop_todo.yml +21 -187
  5. data/README.adoc +212 -15
  6. data/bench/bench_xmi.rb +6 -6
  7. data/bench/gate_config.rb +2 -9
  8. data/docs/_pages/configuration.adoc +155 -41
  9. data/docs/_pages/serialization_adapters.adoc +65 -14
  10. data/docs/index.adoc +3 -1
  11. data/docs/yamls_sequence.adoc +335 -0
  12. data/lib/lutaml/hash_format.rb +4 -0
  13. data/lib/lutaml/json.rb +4 -0
  14. data/lib/lutaml/model/adapter_resolver.rb +410 -0
  15. data/lib/lutaml/model/adapter_scope.rb +64 -0
  16. data/lib/lutaml/model/config.rb +84 -21
  17. data/lib/lutaml/model/configuration.rb +17 -249
  18. data/lib/lutaml/model/format_registry.rb +44 -117
  19. data/lib/lutaml/model/serialize/format_conversion.rb +42 -3
  20. data/lib/lutaml/model/serialize.rb +4 -2
  21. data/lib/lutaml/model/version.rb +1 -1
  22. data/lib/lutaml/model.rb +2 -0
  23. data/lib/lutaml/toml.rb +10 -3
  24. data/lib/lutaml/xml/serialization/instance_methods.rb +6 -0
  25. data/lib/lutaml/xml.rb +3 -4
  26. data/lib/lutaml/yaml.rb +4 -0
  27. data/lib/lutaml/yamls/adapter/mapping.rb +7 -0
  28. data/lib/lutaml/yamls/adapter/standard_adapter.rb +23 -2
  29. data/lib/lutaml/yamls/adapter/transform.rb +105 -7
  30. data/lib/lutaml/yamls/adapter/yamls_sequence.rb +20 -0
  31. data/lib/lutaml/yamls/adapter/yamls_sequence_rule.rb +48 -0
  32. data/lib/lutaml/yamls/adapter.rb +2 -0
  33. data/spec/fixtures/geolexica_v2_concept.rb +136 -0
  34. data/spec/fixtures/geolexica_v2_sample.yaml +36 -0
  35. data/spec/fixtures/geolexica_v2_sample2.yaml +38 -0
  36. data/spec/fixtures/yamls_range_concept.rb +139 -0
  37. data/spec/lutaml/model/xml_decoupling_spec.rb +5 -4
  38. data/spec/lutaml/model/yamls_range_spec.rb +393 -0
  39. data/spec/lutaml/model/yamls_sequence_spec.rb +245 -0
  40. data/spec/spec_helper.rb +5 -0
  41. metadata +13 -3
  42. data/bench/bench_uniword.rb +0 -69
@@ -0,0 +1,139 @@
1
+ require_relative "../../lib/lutaml/model"
2
+
3
+ module YamlsRangeTest
4
+ class Header < Lutaml::Model::Serializable
5
+ attribute :title, :string
6
+ attribute :version, :integer
7
+
8
+ yaml do
9
+ map "title", to: :title
10
+ map "version", to: :version
11
+ end
12
+ end
13
+
14
+ class Metadata < Lutaml::Model::Serializable
15
+ attribute :author, :string
16
+ attribute :date, :string
17
+
18
+ yaml do
19
+ map "author", to: :author
20
+ map "date", to: :date
21
+ end
22
+ end
23
+
24
+ class Entry < Lutaml::Model::Serializable
25
+ attribute :name, :string
26
+ attribute :value, :string
27
+
28
+ yaml do
29
+ map "name", to: :name
30
+ map "value", to: :value
31
+ end
32
+ end
33
+
34
+ class Footer < Lutaml::Model::Serializable
35
+ attribute :note, :string
36
+
37
+ yaml do
38
+ map "note", to: :note
39
+ end
40
+ end
41
+
42
+ # Uses various range patterns: 0..1, 2..3, -1, etc.
43
+ class Document < Lutaml::Model::Serializable
44
+ attribute :headers, Header, collection: true
45
+ attribute :entries, Entry, collection: true
46
+ attribute :footer, Footer
47
+
48
+ yamls do
49
+ sequence do
50
+ map_document 0..1, to: :headers, type: Header, collection: true
51
+ map_document 2..3, to: :entries, type: Entry, collection: true
52
+ map_document -1, to: :footer, type: Footer
53
+ end
54
+ end
55
+ end
56
+
57
+ # Uses negative ranges: -2..-1 for the last 2 docs
58
+ class DocumentNegRange < Lutaml::Model::Serializable
59
+ attribute :headers, Header, collection: true
60
+ attribute :trailers, Entry, collection: true
61
+
62
+ yamls do
63
+ sequence do
64
+ map_document 0..1, to: :headers, type: Header, collection: true
65
+ map_document -2..-1, to: :trailers, type: Entry, collection: true
66
+ end
67
+ end
68
+ end
69
+
70
+ # Uses 2..-1 to capture from position 2 to end
71
+ class DocumentOpenRange < Lutaml::Model::Serializable
72
+ attribute :header, Header
73
+ attribute :rest, Entry, collection: true
74
+
75
+ yamls do
76
+ sequence do
77
+ map_document 0, to: :header, type: Header
78
+ map_document 1..-1, to: :rest, type: Entry, collection: true
79
+ end
80
+ end
81
+ end
82
+
83
+ # Uses -1 for last doc as a collection
84
+ class DocumentLastOnly < Lutaml::Model::Serializable
85
+ attribute :last_entry, Entry
86
+
87
+ yamls do
88
+ sequence do
89
+ map_document -1, to: :last_entry, type: Entry
90
+ end
91
+ end
92
+ end
93
+
94
+ # 3 ranges with 3 different types:
95
+ # 0..1 → Headers (flex range in front), 2..3 → Metadata, -2..-1 → Footers (flex range at back)
96
+ class ThreeRangesFrontFlex < Lutaml::Model::Serializable
97
+ attribute :headers, Header, collection: true
98
+ attribute :metas, Metadata, collection: true
99
+ attribute :trailers, Entry, collection: true
100
+
101
+ yamls do
102
+ sequence do
103
+ map_document 0..1, to: :headers, type: Header, collection: true
104
+ map_document 2..3, to: :metas, type: Metadata, collection: true
105
+ map_document -2..-1, to: :trailers, type: Entry, collection: true
106
+ end
107
+ end
108
+ end
109
+
110
+ # 3 ranges: 0 (single Header), 1..3 (Metadata collection), -1 (single Footer)
111
+ class ThreeRangesMixed < Lutaml::Model::Serializable
112
+ attribute :header, Header
113
+ attribute :metas, Metadata, collection: true
114
+ attribute :footer, Footer
115
+
116
+ yamls do
117
+ sequence do
118
+ map_document 0, to: :header, type: Header
119
+ map_document 1..3, to: :metas, type: Metadata, collection: true
120
+ map_document -1, to: :footer, type: Footer
121
+ end
122
+ end
123
+ end
124
+
125
+ # 3 ranges: 0..1 (Headers), -3..-2 (Entries), -1 (Footer)
126
+ class ThreeRangesNegMiddle < Lutaml::Model::Serializable
127
+ attribute :headers, Header, collection: true
128
+ attribute :entries, Entry, collection: true
129
+ attribute :footer, Footer
130
+
131
+ yamls do
132
+ sequence do
133
+ map_document 0..1, to: :headers, type: Header, collection: true
134
+ map_document -3..-2, to: :entries, type: Entry, collection: true
135
+ map_document -1, to: :footer, type: Footer
136
+ end
137
+ end
138
+ end
139
+ end
@@ -97,12 +97,13 @@ RSpec.describe "XML decoupling from core model" do
97
97
  end
98
98
 
99
99
  describe "Config" do
100
- it "does not include :xml in AVAILABLE_FORMATS constant" do
101
- expect(Lutaml::Model::Configuration::AVAILABLE_FORMATS).not_to include(:xml)
100
+ it "does not include :xml in Config::AVAILABLE_FORMATS constant" do
101
+ expect(Lutaml::Model::Config::AVAILABLE_FORMATS).not_to include(:xml)
102
102
  end
103
103
 
104
- it "does not include XML in static ADAPTERS hash" do
105
- expect(Lutaml::Model::Configuration::ADAPTERS).not_to have_key(:xml)
104
+ it "does not include XML in AdapterResolver metadata" do
105
+ # XML is registered dynamically via FormatRegistry, not in Config boot constants
106
+ expect(Lutaml::Model::FormatRegistry.registered?(:xml)).to be true
106
107
  end
107
108
 
108
109
  it "allows setting XML adapter via Config" do
@@ -0,0 +1,393 @@
1
+ require "spec_helper"
2
+ require_relative "../../fixtures/yamls_range_concept"
3
+
4
+ RSpec.describe "YAMLS sequence range positions" do
5
+ # Doc 0-1: Header fields (title/version), Doc 2-3: Entry fields (name/value), Doc 4: Footer fields (note)
6
+ let(:mixed_doc_yaml) do
7
+ <<~YAMLS
8
+ ---
9
+ title: Doc Zero
10
+ version: 0
11
+
12
+ ---
13
+ title: Doc One
14
+ version: 1
15
+
16
+ ---
17
+ name: Doc Two
18
+ value: v2
19
+
20
+ ---
21
+ name: Doc Three
22
+ value: v3
23
+
24
+ ---
25
+ note: end of stream
26
+ YAMLS
27
+ end
28
+
29
+ # All docs use Entry fields (name/value) for Entry-only models
30
+ let(:entry_doc_yaml) do
31
+ <<~YAMLS
32
+ ---
33
+ name: Doc Zero
34
+ value: v0
35
+
36
+ ---
37
+ name: Doc One
38
+ value: v1
39
+
40
+ ---
41
+ name: Doc Two
42
+ value: v2
43
+
44
+ ---
45
+ name: Doc Three
46
+ value: v3
47
+
48
+ ---
49
+ name: Doc Four
50
+ value: v4
51
+ YAMLS
52
+ end
53
+
54
+ # All docs use Header fields (title/version) for Header-only models
55
+ let(:header_doc_yaml) do
56
+ <<~YAMLS
57
+ ---
58
+ title: Doc Zero
59
+ version: 0
60
+
61
+ ---
62
+ title: Doc One
63
+ version: 1
64
+
65
+ ---
66
+ title: Doc Two
67
+ version: 2
68
+
69
+ ---
70
+ title: Doc Three
71
+ version: 3
72
+
73
+ ---
74
+ title: Doc Four
75
+ version: 4
76
+ YAMLS
77
+ end
78
+
79
+ # --- 3-range tests with 3 different class types ---
80
+
81
+ # 7-doc YAML: docs 0-1 = Header, docs 2-3 = Metadata, docs 4-5 = Entry, doc 6 = Footer
82
+ let(:seven_doc_yaml) do
83
+ <<~YAMLS
84
+ ---
85
+ title: Alpha
86
+ version: 1
87
+
88
+ ---
89
+ title: Beta
90
+ version: 2
91
+
92
+ ---
93
+ author: Alice
94
+ date: 2024-01-01
95
+
96
+ ---
97
+ author: Bob
98
+ date: 2024-06-15
99
+
100
+ ---
101
+ name: EntryOne
102
+ value: val1
103
+
104
+ ---
105
+ name: EntryTwo
106
+ value: val2
107
+
108
+ ---
109
+ note: final note
110
+ YAMLS
111
+ end
112
+
113
+ describe "range 0..1, 2..3, and negative -1" do
114
+ subject(:doc) { YamlsRangeTest::Document.from_yamls(mixed_doc_yaml) }
115
+
116
+ it "maps documents 0 and 1 to headers collection" do
117
+ expect(doc.headers.length).to eq(2)
118
+ expect(doc.headers[0].title).to eq("Doc Zero")
119
+ expect(doc.headers[0].version).to eq(0)
120
+ expect(doc.headers[1].title).to eq("Doc One")
121
+ expect(doc.headers[1].version).to eq(1)
122
+ end
123
+
124
+ it "maps documents 2 and 3 to entries collection" do
125
+ expect(doc.entries.length).to eq(2)
126
+ expect(doc.entries[0].name).to eq("Doc Two")
127
+ expect(doc.entries[0].value).to eq("v2")
128
+ expect(doc.entries[1].name).to eq("Doc Three")
129
+ expect(doc.entries[1].value).to eq("v3")
130
+ end
131
+
132
+ it "maps document -1 (last) to footer" do
133
+ expect(doc.footer).to be_a(YamlsRangeTest::Footer)
134
+ expect(doc.footer.note).to eq("end of stream")
135
+ end
136
+
137
+ it "round-trips through serialization" do
138
+ output = doc.to_yamls
139
+ doc2 = YamlsRangeTest::Document.from_yamls(output)
140
+ expect(doc2.headers.length).to eq(2)
141
+ expect(doc2.headers[0].title).to eq("Doc Zero")
142
+ expect(doc2.entries.length).to eq(2)
143
+ expect(doc2.entries[0].name).to eq("Doc Two")
144
+ expect(doc2.footer.note).to eq("end of stream")
145
+ end
146
+ end
147
+
148
+ describe "negative range -2..-1" do
149
+ subject(:doc) { YamlsRangeTest::DocumentNegRange.from_yamls(mixed_doc_yaml) }
150
+
151
+ it "maps documents 0 and 1 to headers" do
152
+ expect(doc.headers.length).to eq(2)
153
+ expect(doc.headers[0].title).to eq("Doc Zero")
154
+ expect(doc.headers[1].title).to eq("Doc One")
155
+ end
156
+
157
+ it "maps documents -2..-1 (docs 3 and 4) to trailers" do
158
+ expect(doc.trailers.length).to eq(2)
159
+ expect(doc.trailers[0].name).to eq("Doc Three")
160
+ expect(doc.trailers[0].value).to eq("v3")
161
+ # Doc 4 has 'note' not 'name'/'value', so Entry fields are nil
162
+ expect(doc.trailers[1].name).to be_nil
163
+ expect(doc.trailers[1].value).to be_nil
164
+ end
165
+
166
+ it "round-trips through serialization" do
167
+ output = doc.to_yamls
168
+ doc2 = YamlsRangeTest::DocumentNegRange.from_yamls(output)
169
+ expect(doc2.headers.length).to eq(2)
170
+ expect(doc2.trailers.length).to eq(2)
171
+ expect(doc2.trailers[0].name).to eq("Doc Three")
172
+ end
173
+ end
174
+
175
+ describe "range 1..-1 (from position 1 to end)" do
176
+ # Doc 0 is Header (title/version), docs 1-4 are Entry (name/value)
177
+ subject(:doc) { YamlsRangeTest::DocumentOpenRange.from_yamls(open_range_yaml) }
178
+
179
+ let(:open_range_yaml) do
180
+ <<~YAMLS
181
+ ---
182
+ title: Doc Zero
183
+ version: 0
184
+
185
+ ---
186
+ name: Doc One
187
+ value: v1
188
+
189
+ ---
190
+ name: Doc Two
191
+ value: v2
192
+
193
+ ---
194
+ name: Doc Three
195
+ value: v3
196
+
197
+ ---
198
+ name: Doc Four
199
+ value: v4
200
+ YAMLS
201
+ end
202
+
203
+ it "maps document 0 to header" do
204
+ expect(doc.header.title).to eq("Doc Zero")
205
+ expect(doc.header.version).to eq(0)
206
+ end
207
+
208
+ it "maps documents 1..-1 (all remaining) to rest collection" do
209
+ expect(doc.rest.length).to eq(4)
210
+ expect(doc.rest[0].name).to eq("Doc One")
211
+ expect(doc.rest[0].value).to eq("v1")
212
+ expect(doc.rest[1].name).to eq("Doc Two")
213
+ expect(doc.rest[2].name).to eq("Doc Three")
214
+ expect(doc.rest[3].name).to eq("Doc Four")
215
+ end
216
+
217
+ it "round-trips through serialization" do
218
+ output = doc.to_yamls
219
+ doc2 = YamlsRangeTest::DocumentOpenRange.from_yamls(output)
220
+ expect(doc2.header.title).to eq("Doc Zero")
221
+ expect(doc2.rest.length).to eq(4)
222
+ expect(doc2.rest[0].name).to eq("Doc One")
223
+ end
224
+ end
225
+
226
+ describe "negative single index -1" do
227
+ subject(:doc) { YamlsRangeTest::DocumentLastOnly.from_yamls(entry_doc_yaml) }
228
+
229
+ it "maps only the last document" do
230
+ expect(doc.last_entry).to be_a(YamlsRangeTest::Entry)
231
+ expect(doc.last_entry.name).to eq("Doc Four")
232
+ expect(doc.last_entry.value).to eq("v4")
233
+ end
234
+
235
+ it "round-trips through serialization" do
236
+ output = doc.to_yamls
237
+ doc2 = YamlsRangeTest::DocumentLastOnly.from_yamls(output)
238
+ expect(doc2.last_entry.name).to eq("Doc Four")
239
+ end
240
+ end
241
+
242
+ describe "YamlsSequenceRule#resolve_range" do
243
+ let(:rule_class) { Lutaml::Yamls::Adapter::YamlsSequenceRule }
244
+
245
+ it "resolves Integer 0 to 0..0" do
246
+ rule = rule_class.new(0, to: :x, type: Object)
247
+ expect(rule.resolve_range(5)).to eq(0..0)
248
+ end
249
+
250
+ it "resolves Integer -1 to 4..4 when doc_count is 5" do
251
+ rule = rule_class.new(-1, to: :x, type: Object)
252
+ expect(rule.resolve_range(5)).to eq(4..4)
253
+ end
254
+
255
+ it "resolves Integer -2 to 3..3 when doc_count is 5" do
256
+ rule = rule_class.new(-2, to: :x, type: Object)
257
+ expect(rule.resolve_range(5)).to eq(3..3)
258
+ end
259
+
260
+ it "resolves Range 0..1 to 0..1" do
261
+ rule = rule_class.new(0..1, to: :x, type: Object)
262
+ expect(rule.resolve_range(5)).to eq(0..1)
263
+ end
264
+
265
+ it "resolves Range -2..-1 to 3..4 when doc_count is 5" do
266
+ rule = rule_class.new(-2..-1, to: :x, type: Object)
267
+ expect(rule.resolve_range(5)).to eq(3..4)
268
+ end
269
+
270
+ it "resolves Range 1.. to 1..4 when doc_count is 5" do
271
+ rule = rule_class.new(1.., to: :x, type: Object)
272
+ expect(rule.resolve_range(5)).to eq(1..4)
273
+ end
274
+
275
+ it "resolves Range 2..-1 to 2..4 when doc_count is 5" do
276
+ rule = rule_class.new(2..-1, to: :x, type: Object)
277
+ expect(rule.resolve_range(5)).to eq(2..4)
278
+ end
279
+
280
+ it "resolves Range -3..-1 to 2..4 when doc_count is 5" do
281
+ rule = rule_class.new(-3..-1, to: :x, type: Object)
282
+ expect(rule.resolve_range(5)).to eq(2..4)
283
+ end
284
+
285
+ it "clamps out-of-bounds end index" do
286
+ rule = rule_class.new(3..10, to: :x, type: Object)
287
+ expect(rule.resolve_range(5)).to eq(3..4)
288
+ end
289
+
290
+ it "clamps negative start that resolves below 0" do
291
+ rule = rule_class.new(-10..-1, to: :x, type: Object)
292
+ expect(rule.resolve_range(5)).to eq(0..4)
293
+ end
294
+
295
+ it "returns nil for zero doc_count" do
296
+ rule = rule_class.new(0, to: :x, type: Object)
297
+ expect(rule.resolve_range(0)).to be_nil
298
+ end
299
+ end
300
+
301
+ describe "3 ranges: 0..1 (Header), 2..3 (Metadata), -2..-1 (Entry) — flex range at back" do
302
+ subject(:doc) { YamlsRangeTest::ThreeRangesFrontFlex.from_yamls(seven_doc_yaml) }
303
+
304
+ it "maps docs 0..1 to headers" do
305
+ expect(doc.headers.length).to eq(2)
306
+ expect(doc.headers[0].title).to eq("Alpha")
307
+ expect(doc.headers[1].title).to eq("Beta")
308
+ end
309
+
310
+ it "maps docs 2..3 to metas" do
311
+ expect(doc.metas.length).to eq(2)
312
+ expect(doc.metas[0].author).to eq("Alice")
313
+ expect(doc.metas[1].author).to eq("Bob")
314
+ end
315
+
316
+ it "maps docs -2..-1 (docs 5 and 6) to trailers" do
317
+ expect(doc.trailers.length).to eq(2)
318
+ expect(doc.trailers[0].name).to eq("EntryTwo")
319
+ expect(doc.trailers[0].value).to eq("val2")
320
+ # doc 6 has 'note' field, not 'name'/'value', so Entry fields are nil
321
+ expect(doc.trailers[1].name).to be_nil
322
+ end
323
+
324
+ it "round-trips through serialization" do
325
+ output = doc.to_yamls
326
+ doc2 = YamlsRangeTest::ThreeRangesFrontFlex.from_yamls(output)
327
+ expect(doc2.headers.length).to eq(2)
328
+ expect(doc2.metas.length).to eq(2)
329
+ expect(doc2.trailers.length).to eq(2)
330
+ expect(doc2.metas[0].author).to eq("Alice")
331
+ end
332
+ end
333
+
334
+ describe "3 ranges: 0 (Header), 1..3 (Metadata), -1 (Footer) — mixed single and range" do
335
+ subject(:doc) { YamlsRangeTest::ThreeRangesMixed.from_yamls(seven_doc_yaml) }
336
+
337
+ it "maps doc 0 to header" do
338
+ expect(doc.header.title).to eq("Alpha")
339
+ expect(doc.header.version).to eq(1)
340
+ end
341
+
342
+ it "maps docs 1..3 to metas" do
343
+ expect(doc.metas.length).to eq(3)
344
+ # doc 1 has Header fields (title/version), mapped as Metadata → author is nil
345
+ expect(doc.metas[0].author).to be_nil
346
+ expect(doc.metas[1].author).to eq("Alice")
347
+ expect(doc.metas[2].author).to eq("Bob")
348
+ end
349
+
350
+ it "maps doc -1 (last) to footer" do
351
+ expect(doc.footer.note).to eq("final note")
352
+ end
353
+
354
+ it "round-trips through serialization" do
355
+ output = doc.to_yamls
356
+ doc2 = YamlsRangeTest::ThreeRangesMixed.from_yamls(output)
357
+ expect(doc2.header.title).to eq("Alpha")
358
+ expect(doc2.metas.length).to eq(3)
359
+ expect(doc2.footer.note).to eq("final note")
360
+ end
361
+ end
362
+
363
+ describe "3 ranges: 0..1 (Header), -3..-2 (Entry), -1 (Footer) — negative middle range" do
364
+ subject(:doc) { YamlsRangeTest::ThreeRangesNegMiddle.from_yamls(seven_doc_yaml) }
365
+
366
+ it "maps docs 0..1 to headers" do
367
+ expect(doc.headers.length).to eq(2)
368
+ expect(doc.headers[0].title).to eq("Alpha")
369
+ expect(doc.headers[1].title).to eq("Beta")
370
+ end
371
+
372
+ it "maps docs -3..-2 (docs 4 and 5) to entries" do
373
+ expect(doc.entries.length).to eq(2)
374
+ expect(doc.entries[0].name).to eq("EntryOne")
375
+ expect(doc.entries[0].value).to eq("val1")
376
+ expect(doc.entries[1].name).to eq("EntryTwo")
377
+ expect(doc.entries[1].value).to eq("val2")
378
+ end
379
+
380
+ it "maps doc -1 (last) to footer" do
381
+ expect(doc.footer.note).to eq("final note")
382
+ end
383
+
384
+ it "round-trips through serialization" do
385
+ output = doc.to_yamls
386
+ doc2 = YamlsRangeTest::ThreeRangesNegMiddle.from_yamls(output)
387
+ expect(doc2.headers.length).to eq(2)
388
+ expect(doc2.entries.length).to eq(2)
389
+ expect(doc2.entries[0].name).to eq("EntryOne")
390
+ expect(doc2.footer.note).to eq("final note")
391
+ end
392
+ end
393
+ end