lutaml-model 0.8.0 → 0.8.2

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 (48) 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 +18 -186
  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/adapter/multi_json_adapter.rb +4 -2
  14. data/lib/lutaml/json/adapter/oj_adapter.rb +4 -2
  15. data/lib/lutaml/json.rb +4 -0
  16. data/lib/lutaml/key_value/adapter/json/multi_json_adapter.rb +4 -2
  17. data/lib/lutaml/key_value/adapter/json/oj_adapter.rb +4 -2
  18. data/lib/lutaml/model/adapter_resolver.rb +410 -0
  19. data/lib/lutaml/model/adapter_scope.rb +64 -0
  20. data/lib/lutaml/model/config.rb +84 -21
  21. data/lib/lutaml/model/configuration.rb +17 -249
  22. data/lib/lutaml/model/format_registry.rb +44 -117
  23. data/lib/lutaml/model/mapping/listener.rb +4 -2
  24. data/lib/lutaml/model/serialize/format_conversion.rb +42 -3
  25. data/lib/lutaml/model/serialize.rb +4 -2
  26. data/lib/lutaml/model/services/base.rb +4 -2
  27. data/lib/lutaml/model/version.rb +1 -1
  28. data/lib/lutaml/model.rb +2 -0
  29. data/lib/lutaml/toml.rb +10 -3
  30. data/lib/lutaml/xml/serialization/instance_methods.rb +6 -0
  31. data/lib/lutaml/xml.rb +3 -4
  32. data/lib/lutaml/yaml.rb +4 -0
  33. data/lib/lutaml/yamls/adapter/mapping.rb +7 -0
  34. data/lib/lutaml/yamls/adapter/standard_adapter.rb +23 -2
  35. data/lib/lutaml/yamls/adapter/transform.rb +105 -7
  36. data/lib/lutaml/yamls/adapter/yamls_sequence.rb +20 -0
  37. data/lib/lutaml/yamls/adapter/yamls_sequence_rule.rb +48 -0
  38. data/lib/lutaml/yamls/adapter.rb +2 -0
  39. data/spec/fixtures/geolexica_v2_concept.rb +136 -0
  40. data/spec/fixtures/geolexica_v2_sample.yaml +36 -0
  41. data/spec/fixtures/geolexica_v2_sample2.yaml +38 -0
  42. data/spec/fixtures/yamls_range_concept.rb +139 -0
  43. data/spec/lutaml/model/xml_decoupling_spec.rb +5 -4
  44. data/spec/lutaml/model/yamls_range_spec.rb +393 -0
  45. data/spec/lutaml/model/yamls_sequence_spec.rb +245 -0
  46. data/spec/spec_helper.rb +5 -0
  47. metadata +13 -3
  48. data/bench/bench_uniword.rb +0 -69
@@ -0,0 +1,335 @@
1
+ = YAMLS Sequence -- Heterogeneous YAML Stream Support
2
+ :toc:
3
+ :toclevels: 3
4
+
5
+ == Overview
6
+
7
+ A **YAML Stream** (file extension `.yaml`, format symbol `:yamls`) is a
8
+ multi-document YAML file where documents are separated by `---`. The
9
+ *YAMLS Sequence* feature allows different documents in the stream to map to
10
+ *different* model types at specific positions -- analogous to XML Schema's
11
+ `<sequence>` element.
12
+
13
+ == The Problem
14
+
15
+ Many real-world YAML streams contain heterogeneous document types:
16
+
17
+ [source,yaml]
18
+ ----
19
+ --- # Doc 0 → ConceptIndex
20
+ data:
21
+ identifier: 3.5.8.8
22
+ localized_concepts:
23
+ eng: fbe1444a-7c11-555e-bb1b-680a4e6f2502
24
+ id: 0171b198-d068-53d9-8741-fb87e6755d62
25
+
26
+ --- # Doc 1 → LocalizedConcept
27
+ data:
28
+ definition:
29
+ - content: characteristic of a financial model
30
+ terms:
31
+ - type: expression
32
+ designation: membership-based
33
+ language_code: eng
34
+ id: fbe1444a-7c11-555e-bb1b-680a4e6f2502
35
+ ----
36
+
37
+ Doc 0 is a `ConceptIndex`, docs 1+ are `LocalizedConcept` entries. The
38
+ existing `yamls` format with `map_instances` only supports *homogeneous*
39
+ streams where all documents share the same type.
40
+
41
+ == The Solution
42
+
43
+ The `sequence` block inside the `yamls` mapping DSL provides position-based
44
+ document mapping:
45
+
46
+ [source,ruby]
47
+ ----
48
+ class ManagedConcept < Lutaml::Model::Serializable
49
+ attribute :index, ConceptIndex
50
+ attribute :localized, LocalizedConcept, collection: true
51
+
52
+ yamls do
53
+ sequence do
54
+ map_document 0, to: :index, type: ConceptIndex
55
+ map_document 1.., to: :localized, type: LocalizedConcept, collection: true
56
+ end
57
+ end
58
+ end
59
+ ----
60
+
61
+ === `map_document` Parameters
62
+
63
+ |===
64
+ | Parameter | Type | Description
65
+
66
+ | `position`
67
+ | Integer or Range
68
+ | Document position in the stream. See <<position-semantics>>.
69
+
70
+ | `to`
71
+ | Symbol
72
+ | Attribute name on the parent model.
73
+
74
+ | `type`
75
+ | Class
76
+ | Model class for deserialization of matching documents.
77
+
78
+ | `collection`
79
+ | Boolean (default: `false`)
80
+ | Set to `true` if multiple documents map to this attribute.
81
+ |===
82
+
83
+ [[position-semantics]]
84
+ == Position Semantics
85
+
86
+ The `position` parameter supports Integer, positive Range, negative indices,
87
+ and mixed Range expressions:
88
+
89
+ [cols="1,4"]
90
+ |===
91
+ | Position | Meaning
92
+
93
+ | `0` (Integer)
94
+ | Document at index 0 only. Singular (`collection: false`).
95
+
96
+ | `-1` (negative Integer)
97
+ | Last document in the stream.
98
+
99
+ | `-2` (negative Integer)
100
+ | Second-to-last document.
101
+
102
+ | `1..` (open Range)
103
+ | All documents from index 1 to the end. Collection (`collection: true`).
104
+
105
+ | `0..1` (bounded Range)
106
+ | Documents at indices 0 and 1. Collection.
107
+
108
+ | `2..4` (bounded Range)
109
+ | Documents at indices 2, 3, 4. Collection.
110
+
111
+ | `-2..-1` (negative Range)
112
+ | Last 2 documents in the stream. Collection.
113
+
114
+ | `1..-1` (mixed Range)
115
+ | Documents from index 1 to the end. Collection.
116
+
117
+ | `2..-1` (mixed Range)
118
+ | Documents from index 2 to the end. Collection.
119
+ |===
120
+
121
+ === Negative Index Resolution
122
+
123
+ Negative indices are resolved relative to the total document count:
124
+
125
+ - `-1` resolves to `doc_count - 1` (last document)
126
+ - `-2` resolves to `doc_count - 2` (second-to-last)
127
+ - `-2..-1` resolves to `(doc_count - 2)..(doc_count - 1)` (last 2 documents)
128
+
129
+ Out-of-bounds indices are clamped: `-10..-1` on a 5-document stream resolves
130
+ to `0..4` (all documents).
131
+
132
+ == Examples
133
+
134
+ === Two-Model Stream (Index + Localized Concepts)
135
+
136
+ [source,ruby]
137
+ ----
138
+ class ManagedConcept < Lutaml::Model::Serializable
139
+ attribute :index, ConceptIndex
140
+ attribute :localized, LocalizedConcept, collection: true
141
+
142
+ yamls do
143
+ sequence do
144
+ map_document 0, to: :index, type: ConceptIndex
145
+ map_document 1.., to: :localized, type: LocalizedConcept, collection: true
146
+ end
147
+ end
148
+ end
149
+
150
+ managed = ManagedConcept.from_yamls(yaml_stream)
151
+ managed.index.data.identifier #=> "3.5.8.8"
152
+ managed.localized.first.data.language_code #=> "eng"
153
+ ----
154
+
155
+ === Three-Model Stream with Bounded Ranges
156
+
157
+ [source,ruby]
158
+ ----
159
+ class Document < Lutaml::Model::Serializable
160
+ attribute :headers, Header, collection: true
161
+ attribute :entries, Entry, collection: true
162
+ attribute :footer, Footer
163
+
164
+ yamls do
165
+ sequence do
166
+ map_document 0..1, to: :headers, type: Header, collection: true
167
+ map_document 2..3, to: :entries, type: Entry, collection: true
168
+ map_document -1, to: :footer, type: Footer
169
+ end
170
+ end
171
+ end
172
+ ----
173
+
174
+ === Negative Ranges for Fixed-From-End Positioning
175
+
176
+ Useful when the front of the stream varies but the tail structure is fixed:
177
+
178
+ [source,ruby]
179
+ ----
180
+ class Report < Lutaml::Model::Serializable
181
+ attribute :headers, Header, collection: true
182
+ attribute :trailers, Entry, collection: true
183
+
184
+ yamls do
185
+ sequence do
186
+ map_document 0..1, to: :headers, type: Header, collection: true
187
+ map_document -2..-1, to: :trailers, type: Entry, collection: true
188
+ end
189
+ end
190
+ end
191
+ ----
192
+
193
+ === Mixed Ranges (Single + Range + Negative)
194
+
195
+ Three different types across a 7-document stream:
196
+
197
+ [source,ruby]
198
+ ----
199
+ class ThreeRanges < Lutaml::Model::Serializable
200
+ attribute :headers, Header, collection: true
201
+ attribute :entries, Entry, collection: true
202
+ attribute :footer, Footer
203
+
204
+ yamls do
205
+ sequence do
206
+ map_document 0..1, to: :headers, type: Header, collection: true
207
+ map_document -3..-2, to: :entries, type: Entry, collection: true
208
+ map_document -1, to: :footer, type: Footer
209
+ end
210
+ end
211
+ end
212
+ ----
213
+
214
+ == Serialization (Round-Trip)
215
+
216
+ Calling `to_yamls` on a model with a sequence definition produces a valid
217
+ YAML stream. Each rule's values are serialized in rule order:
218
+
219
+ [source,ruby]
220
+ ----
221
+ managed = ManagedConcept.from_yamls(yaml_stream)
222
+ output = managed.to_yamls
223
+ managed2 = ManagedConcept.from_yamls(output)
224
+
225
+ managed2.index.id == managed.index.id # true
226
+ managed2.localized.first.id == managed.localized.first.id # true
227
+ ----
228
+
229
+ == Loading a Directory of Sequence-Based Files
230
+
231
+ Each file is a complete YAML stream (one model instance). Load them
232
+ individually and assemble into a collection:
233
+
234
+ [source,ruby]
235
+ ----
236
+ concepts = Dir["glossary/*.yaml"].map do |f|
237
+ ManagedConcept.from_yamls(File.read(f))
238
+ end
239
+ collection = ManagedConceptCollection.new(concepts)
240
+ ----
241
+
242
+ == Architecture
243
+
244
+ === New Classes
245
+
246
+ |===
247
+ | Class | Responsibility
248
+
249
+ | `Lutaml::Yamls::Adapter::YamlsSequence`
250
+ | Ordered collection of sequence rules
251
+
252
+ | `Lutaml::Yamls::Adapter::YamlsSequenceRule`
253
+ | Maps doc position to model type, handles value assignment
254
+
255
+ | `Lutaml::Yamls::Adapter::Mapping`
256
+ | DSL surface (`sequence {}` block)
257
+
258
+ | `Lutaml::Yamls::Adapter::Transform`
259
+ | Sequence-aware deserialization/serialization
260
+ |===
261
+
262
+ === Flow
263
+
264
+ .Deserialization
265
+ [source]
266
+ ----
267
+ YAML Stream String
268
+
269
+ ▼ StandardAdapter.parse (YAML.load_stream)
270
+ Array<Hash> (one per document)
271
+
272
+ ▼ Transform.data_to_model_with_sequence
273
+ │ for each YamlsSequenceRule:
274
+ │ extract docs for position (Integer or Range)
275
+ │ deserialize each doc via YAML transformer
276
+ │ assign to model attribute
277
+
278
+ Model Instance
279
+ ----
280
+
281
+ .Serialization
282
+ [source]
283
+ ----
284
+ Model Instance
285
+
286
+ ▼ Transform.model_to_data_with_sequence
287
+ │ for each YamlsSequenceRule:
288
+ │ read attribute value from instance
289
+ │ serialize each item via YAML transformer
290
+ │ collect into ordered array
291
+
292
+ Array<Hash>
293
+
294
+ ▼ StandardAdapter.to_yamls (YAML.dump per doc)
295
+ YAML Stream String
296
+ ----
297
+
298
+ == Files
299
+
300
+ |===
301
+ | File | Description
302
+
303
+ | `lib/lutaml/yamls/adapter.rb`
304
+ | Autoloads new classes
305
+
306
+ | `lib/lutaml/yamls/adapter/yamls_sequence.rb`
307
+ | `YamlsSequence` class
308
+
309
+ | `lib/lutaml/yamls/adapter/yamls_sequence_rule.rb`
310
+ | `YamlsSequenceRule` class with `resolve_range`
311
+
312
+ | `lib/lutaml/yamls/adapter/mapping.rb`
313
+ | `Mapping` with `sequence` DSL
314
+
315
+ | `lib/lutaml/yamls/adapter/transform.rb`
316
+ | Sequence-aware de/serialization
317
+
318
+ | `lib/lutaml/yamls/adapter/standard_adapter.rb`
319
+ | `YAML.load_stream` based parser
320
+
321
+ | `lib/lutaml/model/serialize/format_conversion.rb`
322
+ | `array_passthrough_format?` hook
323
+
324
+ | `spec/lutaml/model/yamls_sequence_spec.rb`
325
+ | Geolexica v2 integration tests
326
+
327
+ | `spec/lutaml/model/yamls_range_spec.rb`
328
+ | Range position tests (negative, bounded, mixed)
329
+
330
+ | `spec/fixtures/geolexica_v2_concept.rb`
331
+ | Geolexica v2 model fixture
332
+
333
+ | `spec/fixtures/yamls_range_concept.rb`
334
+ | Range test model fixture
335
+ |===
@@ -18,6 +18,10 @@ Lutaml::Model::FormatRegistry.register(
18
18
  adapter_class: Lutaml::HashFormat::Adapter::StandardAdapter,
19
19
  transformer: Lutaml::HashFormat::Adapter::Transform,
20
20
  key_value: true,
21
+ adapter_options: {
22
+ available: %i[standard standard_hash],
23
+ default: :standard,
24
+ },
21
25
  )
22
26
 
23
27
  # Register Hash type serializers
@@ -16,7 +16,8 @@ module Lutaml
16
16
  "multi_json gem is not available. Please add 'multi_json' to your Gemfile."
17
17
  end
18
18
 
19
- def to_json(*)
19
+ # rubocop:disable Style/ArgumentsForwarding -- anonymous * requires Ruby 3.2+
20
+ def to_json(*args)
20
21
  require "multi_json"
21
22
  # Handle KeyValueElement input (new symmetric architecture)
22
23
  attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
@@ -27,7 +28,8 @@ module Lutaml
27
28
  @attributes
28
29
  end
29
30
 
30
- MultiJson.dump(attributes_to_serialize, *)
31
+ MultiJson.dump(attributes_to_serialize, *args)
32
+ # rubocop:enable Style/ArgumentsForwarding
31
33
  rescue LoadError
32
34
  raise LoadError,
33
35
  "multi_json gem is not available. Please add 'multi_json' to your Gemfile."
@@ -16,7 +16,8 @@ module Lutaml
16
16
  "oj gem is not available. Please add 'oj' to your Gemfile or use the StandardAdapter."
17
17
  end
18
18
 
19
- def to_json(*)
19
+ # rubocop:disable Style/ArgumentsForwarding -- anonymous * requires Ruby 3.2+
20
+ def to_json(*args)
20
21
  require "oj"
21
22
  # Handle KeyValueElement input (new symmetric architecture)
22
23
  attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
@@ -27,7 +28,8 @@ module Lutaml
27
28
  @attributes
28
29
  end
29
30
 
30
- Oj.dump(attributes_to_serialize, *)
31
+ Oj.dump(attributes_to_serialize, *args)
32
+ # rubocop:enable Style/ArgumentsForwarding
31
33
  rescue LoadError
32
34
  raise LoadError,
33
35
  "oj gem is not available. Please add 'oj' to your Gemfile or use the StandardAdapter."
data/lib/lutaml/json.rb CHANGED
@@ -51,6 +51,10 @@ Lutaml::Model::FormatRegistry.register(
51
51
  adapter_class: Lutaml::Json::Adapter::StandardAdapter,
52
52
  transformer: Lutaml::Json::Adapter::Transform,
53
53
  key_value: true,
54
+ adapter_options: {
55
+ available: %i[standard standard_json multi_json oj],
56
+ default: :standard,
57
+ },
54
58
  )
55
59
 
56
60
  # Register JSON type serializers
@@ -20,7 +20,8 @@ module Lutaml
20
20
  "multi_json gem is not available. Please add 'multi_json' to your Gemfile."
21
21
  end
22
22
 
23
- def to_json(*)
23
+ # rubocop:disable Style/ArgumentsForwarding -- anonymous * requires Ruby 3.2+
24
+ def to_json(*args)
24
25
  require "multi_json"
25
26
  # Handle KeyValueElement input (new symmetric architecture)
26
27
  attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
@@ -31,7 +32,8 @@ module Lutaml
31
32
  @attributes
32
33
  end
33
34
 
34
- MultiJson.dump(attributes_to_serialize, *)
35
+ MultiJson.dump(attributes_to_serialize, *args)
36
+ # rubocop:enable Style/ArgumentsForwarding
35
37
  rescue LoadError
36
38
  raise LoadError,
37
39
  "multi_json gem is not available. Please add 'multi_json' to your Gemfile."
@@ -20,7 +20,8 @@ module Lutaml
20
20
  "oj gem is not available. Please add 'oj' to your Gemfile or use the StandardAdapter."
21
21
  end
22
22
 
23
- def to_json(*)
23
+ # rubocop:disable Style/ArgumentsForwarding -- anonymous * requires Ruby 3.2+
24
+ def to_json(*args)
24
25
  require "oj"
25
26
  # Handle KeyValueElement input (new symmetric architecture)
26
27
  attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
@@ -31,7 +32,8 @@ module Lutaml
31
32
  @attributes
32
33
  end
33
34
 
34
- Oj.dump(attributes_to_serialize, *)
35
+ Oj.dump(attributes_to_serialize, *args)
36
+ # rubocop:enable Style/ArgumentsForwarding
35
37
  rescue LoadError
36
38
  raise LoadError,
37
39
  "oj gem is not available. Please add 'oj' to your Gemfile or use the StandardAdapter."