lutaml-store 0.2.0 → 0.2.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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +11 -175
- data/README.adoc +233 -1124
- data/lib/lutaml/store/adapter/base.rb +4 -0
- data/lib/lutaml/store/adapter/memory.rb +8 -0
- data/lib/lutaml/store/cache_store.rb +9 -6
- data/lib/lutaml/store/format.rb +19 -0
- data/lib/lutaml/store/http_cache.rb +3 -13
- data/lib/lutaml/store/model_registration.rb +5 -2
- data/lib/lutaml/store/model_registry.rb +22 -20
- data/lib/lutaml/store/package_store.rb +2 -18
- data/lib/lutaml/store/package_transport/base.rb +48 -0
- data/lib/lutaml/store/package_transport/directory_transport.rb +196 -0
- data/lib/lutaml/store/package_transport/zip_transport.rb +178 -0
- data/lib/lutaml/store/package_transport.rb +11 -438
- data/lib/lutaml/store/version.rb +1 -1
- metadata +12 -77
- data/.github/workflows/main.yml +0 -27
- data/.gitignore +0 -12
- data/CORRECTED_HTTP_CACHE_IMPLEMENTATION.md +0 -209
- data/CORRECTED_HTTP_CACHE_PLAN.md +0 -164
- data/Gemfile +0 -15
- data/Gemfile.lock +0 -227
- data/TODO.impl/0-lutaml-store-self-quality.md +0 -112
- data/TODO.impl/1-lutaml-hal-migration.md +0 -96
- data/TODO.impl/2-glossarist-migration.md +0 -359
- data/TODO.impl/3-lutaml-jsonschema-migration.md +0 -273
- data/bin/console +0 -11
- data/bin/setup +0 -8
- data/demo/Gemfile +0 -15
- data/demo/Gemfile.lock +0 -61
- data/demo/README.adoc +0 -301
- data/demo/data/vcards/co/contact_10_thompson.data +0 -1
- data/demo/data/vcards/co/contact_10_thompson.meta +0 -1
- data/demo/data/vcards/co/contact_1_doe.data +0 -1
- data/demo/data/vcards/co/contact_1_doe.meta +0 -1
- data/demo/data/vcards/co/contact_2_smith.data +0 -1
- data/demo/data/vcards/co/contact_2_smith.meta +0 -1
- data/demo/data/vcards/co/contact_3_johnson.data +0 -1
- data/demo/data/vcards/co/contact_3_johnson.meta +0 -1
- data/demo/data/vcards/co/contact_4_garcia.data +0 -1
- data/demo/data/vcards/co/contact_4_garcia.meta +0 -1
- data/demo/data/vcards/co/contact_5_wilson.data +0 -1
- data/demo/data/vcards/co/contact_5_wilson.meta +0 -1
- data/demo/data/vcards/co/contact_6_brown.data +0 -1
- data/demo/data/vcards/co/contact_6_brown.meta +0 -1
- data/demo/data/vcards/co/contact_7_davis.data +0 -1
- data/demo/data/vcards/co/contact_7_davis.meta +0 -1
- data/demo/data/vcards/co/contact_8_anderson.data +0 -1
- data/demo/data/vcards/co/contact_8_anderson.meta +0 -1
- data/demo/data/vcards/co/contact_9_taylor.data +0 -1
- data/demo/data/vcards/co/contact_9_taylor.meta +0 -1
- data/demo/data/vcards.db +0 -0
- data/demo/pottery_class_demo.rb +0 -164
- data/demo/vcard_models.rb +0 -140
- data/demo/vcard_store_demo.rb +0 -526
- data/lutaml-store.gemspec +0 -36
- data/plan.adoc +0 -606
- data/spec/lutaml/store/adapter_interface_spec.rb +0 -89
- data/spec/lutaml/store/anti_pattern_guard_spec.rb +0 -35
- data/spec/lutaml/store/anti_pattern_spec.rb +0 -78
- data/spec/lutaml/store/autoload_spec.rb +0 -34
- data/spec/lutaml/store/cache_store_spec.rb +0 -271
- data/spec/lutaml/store/compression_spec.rb +0 -78
- data/spec/lutaml/store/config_enhanced_spec.rb +0 -158
- data/spec/lutaml/store/corrected_http_cache_integration_spec.rb +0 -336
- data/spec/lutaml/store/custom_serializer_spec.rb +0 -108
- data/spec/lutaml/store/database_store_spec.rb +0 -279
- data/spec/lutaml/store/file_io_spec.rb +0 -220
- data/spec/lutaml/store/format/yamls_spec.rb +0 -80
- data/spec/lutaml/store/format_round_trip_spec.rb +0 -110
- data/spec/lutaml/store/format_spec.rb +0 -70
- data/spec/lutaml/store/http_cache_entry_spec.rb +0 -203
- data/spec/lutaml/store/http_cache_hal_integration_spec.rb +0 -404
- data/spec/lutaml/store/http_cache_spec.rb +0 -422
- data/spec/lutaml/store/http_header_processor_spec.rb +0 -290
- data/spec/lutaml/store/import_spec.rb +0 -90
- data/spec/lutaml/store/integrity_spec.rb +0 -157
- data/spec/lutaml/store/key_collision_serializer_spec.rb +0 -98
- data/spec/lutaml/store/load_save_spec.rb +0 -107
- data/spec/lutaml/store/lutaml_model_integration_spec.rb +0 -291
- data/spec/lutaml/store/model_serializer_spec.rb +0 -140
- data/spec/lutaml/store/package_definition_spec.rb +0 -89
- data/spec/lutaml/store/package_store_spec.rb +0 -153
- data/spec/lutaml/store/package_transport/directory_transport_spec.rb +0 -139
- data/spec/lutaml/store/package_transport/zip_transport_spec.rb +0 -85
- data/spec/lutaml/store/store_spec.rb +0 -182
- data/spec/lutaml/store_spec.rb +0 -21
- data/spec/spec_helper.rb +0 -16
- data/spec/support/simple_test_model.rb +0 -15
- data/spec/support/yamls_test_model.rb +0 -35
data/README.adoc
CHANGED
|
@@ -3,48 +3,32 @@
|
|
|
3
3
|
https://github.com/lutaml/lutaml-store[image:https://img.shields.io/github/stars/lutaml/lutaml-store.svg?style=social[GitHub Stars]]
|
|
4
4
|
https://github.com/lutaml/lutaml-store[image:https://img.shields.io/github/forks/lutaml/lutaml-store.svg?style=social[GitHub Forks]]
|
|
5
5
|
image:https://img.shields.io/github/license/lutaml/lutaml-store.svg[License]
|
|
6
|
-
image:https://img.shields.io/github/actions/
|
|
6
|
+
image:https://img.shields.io/github/actions/workflow_status/lutaml/lutaml-store/rake.yml?branch=main[Build Status]
|
|
7
7
|
image:https://img.shields.io/gem/v/lutaml-store.svg[RubyGems Version]
|
|
8
8
|
|
|
9
9
|
== Purpose
|
|
10
10
|
|
|
11
|
-
Lutaml::Store provides a
|
|
12
|
-
|
|
13
|
-
relationships
|
|
11
|
+
Lutaml::Store provides a store-centric database-style API for
|
|
12
|
+
link:https://github.com/lutaml/lutaml-model[Lutaml::Model] objects with model
|
|
13
|
+
registry, polymorphic support, composite relationships, and multiple storage
|
|
14
|
+
backends.
|
|
14
15
|
|
|
15
16
|
It offers a unified interface for storing and retrieving complex model
|
|
16
|
-
hierarchies
|
|
17
|
-
|
|
17
|
+
hierarchies, batch file I/O with multiple serialization formats, HTTP-aware
|
|
18
|
+
caching, and package-based persistence with ZIP and directory transports.
|
|
18
19
|
|
|
19
20
|
== Features
|
|
20
21
|
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
* Format-aware file I/O (YAML, JSON, JSONL, Marshal) with layout strategies
|
|
32
|
-
|
|
33
|
-
* Directory import with queryable backend (import_all)
|
|
34
|
-
|
|
35
|
-
* Custom serializer support for key collision workarounds
|
|
36
|
-
|
|
37
|
-
* Dot notation for nested updates ("studio.location")
|
|
38
|
-
|
|
39
|
-
* Both block-based and hash-based update patterns
|
|
40
|
-
|
|
41
|
-
* Multiple storage backends: Memory, FileSystem, and SQLite
|
|
42
|
-
|
|
43
|
-
* Thread-safe operations across all backends
|
|
44
|
-
|
|
45
|
-
* Event system with synchronous and asynchronous handling
|
|
46
|
-
|
|
47
|
-
* Performance monitoring and error tracking
|
|
22
|
+
* **DatabaseStore** — high-level CRUD with model registry, polymorphism, composites
|
|
23
|
+
* **PackageStore** — structured multi-model packages with ZIP and directory transport
|
|
24
|
+
* **BasicStore** — low-level key-value store with optional cache, events, monitoring
|
|
25
|
+
* **CacheStore** — TTL-aware cache with LRU eviction extending BasicStore
|
|
26
|
+
* **HttpCache** — HTTP-aware caching with ETags, conditional requests, Cache-Control
|
|
27
|
+
* **Format-aware file I/O** — YAML, YAMLS, JSON, JSONL, Marshal with layout strategies
|
|
28
|
+
* **Multiple backends** — Memory, FileSystem, SQLite (all thread-safe)
|
|
29
|
+
* **Model registry** — configurable key fields, polymorphic inheritance, composite relationships
|
|
30
|
+
* **Dot-notation updates** — nested attribute paths with block-based updates
|
|
31
|
+
* **Ruby autoload** — lazy constant loading, only loads what you use
|
|
48
32
|
|
|
49
33
|
== Installation
|
|
50
34
|
|
|
@@ -78,8 +62,6 @@ gem 'sqlite3'
|
|
|
78
62
|
|
|
79
63
|
== Quick start
|
|
80
64
|
|
|
81
|
-
Here's a minimal example to get you started:
|
|
82
|
-
|
|
83
65
|
[source,ruby]
|
|
84
66
|
----
|
|
85
67
|
require 'lutaml/model'
|
|
@@ -107,20 +89,19 @@ store = Lutaml::Store.new(
|
|
|
107
89
|
]
|
|
108
90
|
)
|
|
109
91
|
|
|
110
|
-
#
|
|
111
|
-
|
|
92
|
+
# Save with composite relationships (both stored independently)
|
|
93
|
+
pottery = PotteryClass.new(
|
|
112
94
|
class_id: "pottery_101",
|
|
113
95
|
studio: Studio.new(studio_key: "main_studio", name: "Main Studio"),
|
|
114
96
|
description: "Beginner pottery class"
|
|
115
97
|
)
|
|
98
|
+
store.save(pottery)
|
|
116
99
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
# Fetch models
|
|
100
|
+
# Fetch by model and key
|
|
120
101
|
retrieved = store.fetch(model: PotteryClass, class_id: "pottery_101")
|
|
121
102
|
puts retrieved.studio.name # => "Main Studio"
|
|
122
103
|
|
|
123
|
-
#
|
|
104
|
+
# Nested update with dot notation
|
|
124
105
|
store.update(
|
|
125
106
|
model: PotteryClass,
|
|
126
107
|
class_id: "pottery_101",
|
|
@@ -131,108 +112,105 @@ store.update(
|
|
|
131
112
|
)
|
|
132
113
|
----
|
|
133
114
|
|
|
134
|
-
== Architecture
|
|
115
|
+
== Architecture
|
|
135
116
|
|
|
136
|
-
Lutaml::Store
|
|
137
|
-
operations flow through a central store that manages model registrations,
|
|
138
|
-
relationships, and storage backends.
|
|
117
|
+
Lutaml::Store has two layers:
|
|
139
118
|
|
|
140
|
-
|
|
119
|
+
**DatabaseStore** (via `Lutaml::Store.new`)::
|
|
120
|
+
High-level CRUD with model registry.
|
|
121
|
+
Handles polymorphic dispatch, composite model decomposition,
|
|
122
|
+
dot-notation updates, file I/O (`save_all`, `load_all`, `import_all`, `export`).
|
|
141
123
|
|
|
142
|
-
|
|
124
|
+
**BasicStore**::
|
|
125
|
+
Low-level key-value store wrapping an adapter.
|
|
126
|
+
Provides `get`, `set`, `delete`, `exists?`, `all`, `keys`, bulk operations,
|
|
127
|
+
with optional caching, monitoring, and event emission.
|
|
143
128
|
|
|
144
|
-
|
|
145
|
-
Main entry point providing the store-centric API with model registry support.
|
|
146
|
-
Handles model registration, polymorphic relationships, and composite model
|
|
147
|
-
management.
|
|
129
|
+
=== Key classes
|
|
148
130
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
131
|
+
[cols="1,3"]
|
|
132
|
+
|===
|
|
133
|
+
| Class | Role
|
|
152
134
|
|
|
153
|
-
`
|
|
154
|
-
|
|
155
|
-
independently while maintaining references and ensuring referential integrity.
|
|
135
|
+
| `DatabaseStore`
|
|
136
|
+
| High-level CRUD with model registry, composites, polymorphism
|
|
156
137
|
|
|
157
|
-
`
|
|
158
|
-
|
|
159
|
-
updates, and polymorphic model changes.
|
|
138
|
+
| `PackageStore`
|
|
139
|
+
| Multi-model packages with directory/ZIP transport
|
|
160
140
|
|
|
161
|
-
`
|
|
162
|
-
|
|
163
|
-
Provides `load_all`, `save_all`, `import_all`, and `export` for batch persistence
|
|
164
|
-
with configurable layout strategies (separate, grouped, flat).
|
|
141
|
+
| `PackageDefinition`
|
|
142
|
+
| Declarative schema for package structure (models, assets, metadata)
|
|
165
143
|
|
|
166
|
-
`
|
|
167
|
-
|
|
168
|
-
`to_hash`/`from_hash` for standard `Lutaml::Model::Serializable` instances.
|
|
144
|
+
| `BasicStore`
|
|
145
|
+
| Low-level key-value store with optional cache/events/monitoring
|
|
169
146
|
|
|
170
|
-
`
|
|
171
|
-
|
|
172
|
-
backends with caching, events, and monitoring integration.
|
|
147
|
+
| `CacheStore`
|
|
148
|
+
| TTL-aware cache store extending BasicStore
|
|
173
149
|
|
|
174
|
-
|
|
150
|
+
| `HttpCache`
|
|
151
|
+
| HTTP-aware caching with ETags, conditional requests, Cache-Control
|
|
175
152
|
|
|
176
|
-
`
|
|
177
|
-
|
|
178
|
-
volatile persistence characteristics.
|
|
153
|
+
| `ModelRegistry` / `ModelRegistration`
|
|
154
|
+
| Register models with key fields and polymorphic config
|
|
179
155
|
|
|
180
|
-
`
|
|
181
|
-
|
|
182
|
-
moderate data volumes and development environments.
|
|
156
|
+
| `CompositeModelHandler`
|
|
157
|
+
| Stores nested registered models independently, restores references
|
|
183
158
|
|
|
184
|
-
`
|
|
185
|
-
|
|
186
|
-
for production applications requiring data integrity.
|
|
159
|
+
| `AttributeUpdater`
|
|
160
|
+
| Processes dot-notation paths and block-based updates
|
|
187
161
|
|
|
188
|
-
|
|
162
|
+
| `ModelSerializer`
|
|
163
|
+
| Serialization/deserialization with custom serializer support
|
|
189
164
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
key fields and configure polymorphic relationships.
|
|
165
|
+
| `Format`
|
|
166
|
+
| Multi-format file I/O (YAML, JSON, JSONL, Marshal) with layout strategies
|
|
193
167
|
|
|
194
|
-
|
|
168
|
+
| `Config`
|
|
169
|
+
| Parses and validates store configuration
|
|
170
|
+
|===
|
|
195
171
|
|
|
196
|
-
|
|
172
|
+
=== Storage adapters
|
|
197
173
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
store = Lutaml::Store.new(
|
|
201
|
-
adapter: :memory,
|
|
202
|
-
models: [
|
|
203
|
-
{ model: User, key: :user_id },
|
|
204
|
-
{ model: Post, key: :post_id },
|
|
205
|
-
{ model: Comment, key: :comment_id }
|
|
206
|
-
]
|
|
207
|
-
)
|
|
208
|
-
----
|
|
174
|
+
All adapters inherit from `Adapter::Base` and provide `get`, `set`, `delete`,
|
|
175
|
+
`exists?`, `keys`, `all`, `clear`, `size`, `each_key`, and bulk operations.
|
|
209
176
|
|
|
210
|
-
[
|
|
211
|
-
|
|
212
|
-
|
|
177
|
+
[cols="1,1,3"]
|
|
178
|
+
|===
|
|
179
|
+
| Adapter | Type symbol | Use case
|
|
180
|
+
|
|
181
|
+
| `Adapter::Memory`
|
|
182
|
+
| `:memory`
|
|
183
|
+
| Fast in-memory storage for testing, caching, temporary data
|
|
184
|
+
|
|
185
|
+
| `Adapter::FileSystem`
|
|
186
|
+
| `:filesystem`
|
|
187
|
+
| Persistent file-based storage with integrity checks
|
|
188
|
+
|
|
189
|
+
| `Adapter::Sqlite`
|
|
190
|
+
| `:sqlite`
|
|
191
|
+
| ACID-compliant database storage for production use
|
|
192
|
+
|===
|
|
193
|
+
|
|
194
|
+
== Model registry
|
|
195
|
+
|
|
196
|
+
=== Registration
|
|
197
|
+
|
|
198
|
+
Register models with their unique key fields:
|
|
213
199
|
|
|
214
200
|
[source,ruby]
|
|
215
201
|
----
|
|
216
|
-
class User < Lutaml::Model::Serializable
|
|
217
|
-
attribute :user_id, :string
|
|
218
|
-
attribute :name, :string
|
|
219
|
-
attribute :email, :string
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
# The key field must exist as an attribute
|
|
223
202
|
store = Lutaml::Store.new(
|
|
224
203
|
adapter: :memory,
|
|
225
204
|
models: [
|
|
226
|
-
{ model: User, key: :user_id }
|
|
227
|
-
|
|
205
|
+
{ model: User, key: :user_id },
|
|
206
|
+
{ model: Post, key: :post_id }
|
|
228
207
|
]
|
|
229
208
|
)
|
|
230
209
|
----
|
|
231
|
-
====
|
|
232
210
|
|
|
233
|
-
=== Polymorphic
|
|
211
|
+
=== Polymorphic models
|
|
234
212
|
|
|
235
|
-
For
|
|
213
|
+
For inheritance hierarchies, register the base class with a polymorphic class key:
|
|
236
214
|
|
|
237
215
|
[source,ruby]
|
|
238
216
|
----
|
|
@@ -250,52 +228,22 @@ end
|
|
|
250
228
|
store = Lutaml::Store.new(
|
|
251
229
|
adapter: :memory,
|
|
252
230
|
models: [
|
|
253
|
-
{
|
|
254
|
-
model: Studio,
|
|
255
|
-
key: :studio_key,
|
|
256
|
-
polymorphic_class_key: :_class
|
|
257
|
-
}
|
|
258
|
-
# CeramicStudio inherits from Studio, so no separate registration needed
|
|
231
|
+
{ model: Studio, key: :studio_key, polymorphic_class_key: :_class }
|
|
259
232
|
]
|
|
260
233
|
)
|
|
261
|
-
----
|
|
262
|
-
|
|
263
|
-
[example]
|
|
264
|
-
====
|
|
265
|
-
Polymorphic model usage:
|
|
266
|
-
|
|
267
|
-
[source,ruby]
|
|
268
|
-
----
|
|
269
|
-
# Save different types of studios
|
|
270
|
-
regular_studio = Studio.new(studio_key: "studio1", name: "Regular Studio")
|
|
271
|
-
ceramic_studio = CeramicStudio.new(
|
|
272
|
-
studio_key: "studio2",
|
|
273
|
-
name: "Ceramic Studio",
|
|
274
|
-
clay_type: "Porcelain"
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
store.save([regular_studio, ceramic_studio])
|
|
278
234
|
|
|
279
|
-
|
|
280
|
-
retrieved = store.fetch(model: Studio, studio_key: "
|
|
235
|
+
store.save(CeramicStudio.new(studio_key: "cs1", name: "Clay Haus", clay_type: "Porcelain"))
|
|
236
|
+
retrieved = store.fetch(model: Studio, studio_key: "cs1")
|
|
281
237
|
puts retrieved.class.name # => "CeramicStudio"
|
|
282
|
-
puts retrieved.clay_type # => "Porcelain"
|
|
283
238
|
----
|
|
284
|
-
====
|
|
285
239
|
|
|
286
|
-
=== Composite
|
|
240
|
+
=== Composite models
|
|
287
241
|
|
|
288
|
-
When registered models are nested within other registered models, they are
|
|
289
|
-
|
|
242
|
+
When registered models are nested within other registered models, they are stored
|
|
243
|
+
independently while maintaining references:
|
|
290
244
|
|
|
291
245
|
[source,ruby]
|
|
292
246
|
----
|
|
293
|
-
class PotteryClass < Lutaml::Model::Serializable
|
|
294
|
-
attribute :studio, Studio # Studio is also registered
|
|
295
|
-
attribute :class_id, :string
|
|
296
|
-
attribute :description, :string
|
|
297
|
-
end
|
|
298
|
-
|
|
299
247
|
store = Lutaml::Store.new(
|
|
300
248
|
adapter: :memory,
|
|
301
249
|
models: [
|
|
@@ -304,1127 +252,288 @@ store = Lutaml::Store.new(
|
|
|
304
252
|
]
|
|
305
253
|
)
|
|
306
254
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
studio: Studio.new(studio_key: "main_studio", name: "Main Studio"),
|
|
311
|
-
description: "Pottery class"
|
|
255
|
+
pottery = PotteryClass.new(
|
|
256
|
+
class_id: "p101",
|
|
257
|
+
studio: Studio.new(studio_key: "s1", name: "Main Studio")
|
|
312
258
|
)
|
|
259
|
+
store.save(pottery)
|
|
313
260
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
studio = store.fetch(model: Studio, studio_key: "main_studio")
|
|
318
|
-
puts studio.name # => "Main Studio"
|
|
319
|
-
|
|
320
|
-
# PotteryClass maintains reference to Studio
|
|
321
|
-
pottery = store.fetch(model: PotteryClass, class_id: "pottery_101")
|
|
322
|
-
puts pottery.studio.name # => "Main Studio"
|
|
261
|
+
# Both accessible independently
|
|
262
|
+
store.fetch(model: Studio, studio_key: "s1").name # => "Main Studio"
|
|
263
|
+
store.fetch(model: PotteryClass, class_id: "p101").studio.name # => "Main Studio"
|
|
323
264
|
----
|
|
324
265
|
|
|
325
266
|
== CRUD operations
|
|
326
267
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
=== Save operations
|
|
330
|
-
|
|
331
|
-
Save single models or arrays of models:
|
|
332
|
-
|
|
333
|
-
[source,ruby]
|
|
334
|
-
----
|
|
335
|
-
# Save single model
|
|
336
|
-
user = User.new(user_id: "user1", name: "John Doe")
|
|
337
|
-
store.save(user)
|
|
338
|
-
|
|
339
|
-
# Save array of models
|
|
340
|
-
users = [
|
|
341
|
-
User.new(user_id: "user2", name: "Jane Smith"),
|
|
342
|
-
User.new(user_id: "user3", name: "Bob Johnson")
|
|
343
|
-
]
|
|
344
|
-
store.save(users)
|
|
345
|
-
----
|
|
346
|
-
|
|
347
|
-
[example]
|
|
348
|
-
====
|
|
349
|
-
Saving models with composite relationships:
|
|
350
|
-
|
|
351
|
-
[source,ruby]
|
|
352
|
-
----
|
|
353
|
-
pottery_classes = [
|
|
354
|
-
PotteryClass.new(
|
|
355
|
-
class_id: "pottery_101",
|
|
356
|
-
studio: Studio.new(studio_key: "studio1", name: "Main Studio"),
|
|
357
|
-
description: "Beginner class"
|
|
358
|
-
),
|
|
359
|
-
PotteryClass.new(
|
|
360
|
-
class_id: "pottery_201",
|
|
361
|
-
studio: Studio.new(studio_key: "studio2", name: "Advanced Studio"),
|
|
362
|
-
description: "Advanced class"
|
|
363
|
-
)
|
|
364
|
-
]
|
|
365
|
-
|
|
366
|
-
# Saves both PotteryClass instances and their nested Studio instances
|
|
367
|
-
store.save(pottery_classes)
|
|
368
|
-
|
|
369
|
-
# Studios are now available independently
|
|
370
|
-
studio1 = store.fetch(model: Studio, studio_key: "studio1")
|
|
371
|
-
studio2 = store.fetch(model: Studio, studio_key: "studio2")
|
|
372
|
-
----
|
|
373
|
-
====
|
|
374
|
-
|
|
375
|
-
=== Fetch operations
|
|
376
|
-
|
|
377
|
-
Retrieve models by their registered key fields:
|
|
268
|
+
=== Save
|
|
378
269
|
|
|
379
270
|
[source,ruby]
|
|
380
271
|
----
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
pottery_class = store.fetch(model: PotteryClass, class_id: "pottery_101")
|
|
384
|
-
studio = store.fetch(model: Studio, studio_key: "main_studio")
|
|
272
|
+
store.save(User.new(user_id: "u1", name: "Ada"))
|
|
273
|
+
store.save([user1, user2, user3]) # bulk save
|
|
385
274
|
----
|
|
386
275
|
|
|
387
|
-
|
|
388
|
-
====
|
|
389
|
-
Fetching polymorphic models:
|
|
276
|
+
=== Fetch
|
|
390
277
|
|
|
391
278
|
[source,ruby]
|
|
392
279
|
----
|
|
393
|
-
|
|
394
|
-
regular_studio = Studio.new(studio_key: "studio1", name: "Regular")
|
|
395
|
-
ceramic_studio = CeramicStudio.new(
|
|
396
|
-
studio_key: "studio2",
|
|
397
|
-
name: "Ceramic",
|
|
398
|
-
clay_type: "Stoneware"
|
|
399
|
-
)
|
|
400
|
-
|
|
401
|
-
store.save([regular_studio, ceramic_studio])
|
|
402
|
-
|
|
403
|
-
# Fetch returns correct polymorphic type
|
|
404
|
-
studio1 = store.fetch(model: Studio, studio_key: "studio1")
|
|
405
|
-
puts studio1.class.name # => "Studio"
|
|
406
|
-
|
|
407
|
-
studio2 = store.fetch(model: Studio, studio_key: "studio2")
|
|
408
|
-
puts studio2.class.name # => "CeramicStudio"
|
|
409
|
-
puts studio2.clay_type # => "Stoneware"
|
|
280
|
+
user = store.fetch(model: User, user_id: "u1")
|
|
410
281
|
----
|
|
411
|
-
====
|
|
412
|
-
|
|
413
|
-
=== Update operations
|
|
414
282
|
|
|
415
|
-
Update
|
|
416
|
-
|
|
417
|
-
==== Attribute array updates
|
|
283
|
+
=== Update
|
|
418
284
|
|
|
419
285
|
[source,ruby]
|
|
420
286
|
----
|
|
287
|
+
# Hash-based attributes
|
|
421
288
|
store.update(
|
|
422
|
-
model: User,
|
|
423
|
-
user_id: "user1",
|
|
289
|
+
model: User, user_id: "u1",
|
|
424
290
|
attributes: [
|
|
425
|
-
{ key: :name, value: "
|
|
426
|
-
{ key:
|
|
291
|
+
{ key: :name, value: "Grace" },
|
|
292
|
+
{ key: "studio.location", value: "Building A" } # dot notation for nesting
|
|
427
293
|
]
|
|
428
294
|
)
|
|
295
|
+
|
|
296
|
+
# Block-based
|
|
297
|
+
store.update(model: User, user_id: "u1") do |user|
|
|
298
|
+
user.name = "Grace"
|
|
299
|
+
end
|
|
429
300
|
----
|
|
430
301
|
|
|
431
|
-
|
|
302
|
+
=== Destroy
|
|
432
303
|
|
|
433
304
|
[source,ruby]
|
|
434
305
|
----
|
|
435
|
-
store.
|
|
436
|
-
model: PotteryClass,
|
|
437
|
-
class_id: "pottery_101",
|
|
438
|
-
attributes: [
|
|
439
|
-
{ key: :description, value: "Updated description" },
|
|
440
|
-
{ key: "studio.location", value: "Building A, Room 101" },
|
|
441
|
-
{ key: "studio.name", value: "Updated Studio Name" }
|
|
442
|
-
]
|
|
443
|
-
)
|
|
306
|
+
store.destroy(model: User, user_id: "u1")
|
|
444
307
|
----
|
|
445
308
|
|
|
446
|
-
|
|
447
|
-
====
|
|
448
|
-
Complex nested updates:
|
|
309
|
+
== PackageStore
|
|
449
310
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
store.update(
|
|
454
|
-
model: PotteryClass,
|
|
455
|
-
class_id: "pottery_101",
|
|
456
|
-
attributes: [
|
|
457
|
-
{ key: :description, value: "Advanced pottery techniques" },
|
|
458
|
-
{ key: "studio.name", value: "Master Pottery Studio" },
|
|
459
|
-
{ key: "studio.location", value: "Downtown Arts District" }
|
|
460
|
-
]
|
|
461
|
-
)
|
|
462
|
-
|
|
463
|
-
# Verify updates
|
|
464
|
-
pottery_class = store.fetch(model: PotteryClass, class_id: "pottery_101")
|
|
465
|
-
puts pottery_class.description # => "Advanced pottery techniques"
|
|
466
|
-
puts pottery_class.studio.name # => "Master Pottery Studio"
|
|
467
|
-
puts pottery_class.studio.location # => "Downtown Arts District"
|
|
468
|
-
----
|
|
469
|
-
====
|
|
311
|
+
`PackageStore` provides structured multi-model persistence with directory and ZIP
|
|
312
|
+
transports. It is built on `PackageDefinition`, which declares the package schema
|
|
313
|
+
declaratively.
|
|
470
314
|
|
|
471
|
-
|
|
315
|
+
=== Define a package
|
|
472
316
|
|
|
473
317
|
[source,ruby]
|
|
474
318
|
----
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
319
|
+
glossary = Lutaml::Store::PackageDefinition.new(name: "glossary") do |pkg|
|
|
320
|
+
pkg.model(model: Concept, key: :term, dir: "concepts", default_format: :yaml)
|
|
321
|
+
pkg.model(model: Author, key: :name, dir: "authors", default_format: :json)
|
|
322
|
+
pkg.asset("glossary.yaml", type: :file)
|
|
323
|
+
pkg.metadata_model = GlossaryInfo
|
|
324
|
+
pkg.metadata_file = "glossary.yaml"
|
|
478
325
|
end
|
|
479
326
|
----
|
|
480
327
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
Change model types by updating with different polymorphic instances:
|
|
328
|
+
=== Load and save packages
|
|
484
329
|
|
|
485
330
|
[source,ruby]
|
|
486
331
|
----
|
|
487
|
-
#
|
|
488
|
-
store.
|
|
489
|
-
model: PotteryClass,
|
|
490
|
-
class_id: "pottery_101",
|
|
491
|
-
attributes: [
|
|
492
|
-
{
|
|
493
|
-
key: :studio,
|
|
494
|
-
value: CeramicStudio.new(
|
|
495
|
-
studio_key: "main_studio", # Same key, different type
|
|
496
|
-
name: "Ceramic Arts Studio",
|
|
497
|
-
clay_type: "Porcelain"
|
|
498
|
-
)
|
|
499
|
-
}
|
|
500
|
-
]
|
|
501
|
-
)
|
|
502
|
-
|
|
503
|
-
# Fetch returns updated polymorphic type
|
|
504
|
-
pottery_class = store.fetch(model: PotteryClass, class_id: "pottery_101")
|
|
505
|
-
puts pottery_class.studio.class.name # => "CeramicStudio"
|
|
506
|
-
puts pottery_class.studio.clay_type # => "Porcelain"
|
|
507
|
-
----
|
|
332
|
+
# Load from directory
|
|
333
|
+
store = Lutaml::Store::PackageStore.load(glossary, "./my_glossary", transport: :directory)
|
|
508
334
|
|
|
509
|
-
|
|
335
|
+
# Load from ZIP
|
|
336
|
+
store = Lutaml::Store::PackageStore.load(glossary, "./glossary.zip", transport: :zip)
|
|
510
337
|
|
|
511
|
-
|
|
338
|
+
# Query models
|
|
339
|
+
concepts = store.models_for(Concept)
|
|
340
|
+
store.model_count(Concept) # => 42
|
|
341
|
+
store.fetch_model(Concept, "API")
|
|
512
342
|
|
|
513
|
-
|
|
343
|
+
# Modify and save
|
|
344
|
+
store.add_model(Concept.new(term: "REST", definition: "..."))
|
|
345
|
+
store.save("./output", transport: :zip)
|
|
514
346
|
----
|
|
515
|
-
# Delete single model
|
|
516
|
-
store.destroy(model: User, user_id: "user1")
|
|
517
347
|
|
|
518
|
-
|
|
519
|
-
store.destroy(model: PotteryClass, class_id: "pottery_101")
|
|
520
|
-
# Note: Nested Studio remains unless explicitly deleted
|
|
521
|
-
----
|
|
348
|
+
=== Package transports
|
|
522
349
|
|
|
523
|
-
[
|
|
524
|
-
|
|
525
|
-
|
|
350
|
+
[cols="1,1,3"]
|
|
351
|
+
|===
|
|
352
|
+
| Transport | Symbol | Description
|
|
526
353
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
store.destroy(model: PotteryClass, class_id: "pottery_101")
|
|
354
|
+
| `DirectoryTransport`
|
|
355
|
+
| `:directory`
|
|
356
|
+
| Filesystem directory with subdirectories per model type
|
|
531
357
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
358
|
+
| `ZipTransport`
|
|
359
|
+
| `:zip`
|
|
360
|
+
| ZIP archive containing the same directory structure
|
|
361
|
+
|===
|
|
535
362
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
----
|
|
539
|
-
====
|
|
363
|
+
Transports are resolved via registry (`PackageTransport.resolve(:zip)`),
|
|
364
|
+
extensible without modifying existing code.
|
|
540
365
|
|
|
541
|
-
== File I/O
|
|
366
|
+
== File I/O
|
|
542
367
|
|
|
543
|
-
|
|
544
|
-
be written to and read from directories using multiple serialization formats and
|
|
545
|
-
layout strategies.
|
|
368
|
+
DatabaseStore provides batch file I/O through `Format` handlers.
|
|
546
369
|
|
|
547
370
|
=== Format handlers
|
|
548
371
|
|
|
549
|
-
Five built-in format handlers serialize and deserialize `Lutaml::Model::Serializable` instances:
|
|
550
|
-
|
|
551
372
|
[cols="1,1,1,1"]
|
|
552
373
|
|===
|
|
553
374
|
| Format | Symbol | Extension | Description
|
|
554
375
|
|
|
555
376
|
| YAML | `:yaml` | `.yaml` | Single-document YAML files
|
|
556
|
-
| YAMLS | `:yamls` | `.yaml` | Multi-document YAML streams
|
|
377
|
+
| YAMLS | `:yamls` | `.yaml` | Multi-document YAML streams
|
|
557
378
|
| JSON | `:json` | `.json` | Single JSON objects
|
|
558
|
-
| JSONL | `:jsonl` | `.jsonl` | Line-delimited JSON
|
|
379
|
+
| JSONL | `:jsonl` | `.jsonl` | Line-delimited JSON
|
|
559
380
|
| Marshal | `:marshal` | `.bin` | Ruby Marshal binary format
|
|
560
381
|
|===
|
|
561
382
|
|
|
562
|
-
|
|
563
|
-
`deserialize_many`.
|
|
564
|
-
|
|
565
|
-
=== Layout strategies
|
|
566
|
-
|
|
567
|
-
Three layout strategies control how files are organized on disk:
|
|
568
|
-
|
|
569
|
-
[cols="1,1,3"]
|
|
570
|
-
|===
|
|
571
|
-
| Layout | Symbol | Structure
|
|
572
|
-
|
|
573
|
-
| Separate | `:separate` | One file per model, named by key field
|
|
574
|
-
| Grouped | `:grouped` | Multiple models per file, grouped by key
|
|
575
|
-
| Flat | `:flat` | One file per model (no subdirectory grouping)
|
|
576
|
-
|===
|
|
577
|
-
|
|
578
|
-
=== save_all: batch write to directory
|
|
579
|
-
|
|
580
|
-
Write a collection of models to a directory:
|
|
383
|
+
=== save_all / load_all / import_all / export
|
|
581
384
|
|
|
582
385
|
[source,ruby]
|
|
583
386
|
----
|
|
584
|
-
#
|
|
387
|
+
# Write models to directory
|
|
585
388
|
store.save_all(concepts, path: "./data", format: :yaml, layout: :separate)
|
|
586
|
-
# Creates: ./data/concept/key1.yaml, ./data/concept/key2.yaml, ...
|
|
587
|
-
|
|
588
|
-
# Save as grouped (all models in one multi-document YAML)
|
|
589
|
-
store.save_all(concepts, path: "./data", format: :yamls, layout: :grouped)
|
|
590
389
|
|
|
591
|
-
#
|
|
592
|
-
store.save_all(items, path: "./data", format: :jsonl, layout: :separate)
|
|
593
|
-
----
|
|
594
|
-
|
|
595
|
-
The subdirectory name comes from the `dir` option in model registration:
|
|
596
|
-
|
|
597
|
-
[source,ruby]
|
|
598
|
-
----
|
|
599
|
-
store = Lutaml::Store.new(
|
|
600
|
-
adapter: :memory,
|
|
601
|
-
models: [
|
|
602
|
-
{ model: Concept, key: :uuid, dir: "concepts" },
|
|
603
|
-
{ model: Author, key: :name, dir: "authors" }
|
|
604
|
-
]
|
|
605
|
-
)
|
|
606
|
-
# Concepts write to: <path>/concepts/<uuid>.yaml
|
|
607
|
-
# Authors write to: <path>/authors/<name>.yaml
|
|
608
|
-
----
|
|
609
|
-
|
|
610
|
-
=== load_all: batch read from directory
|
|
611
|
-
|
|
612
|
-
Read models from a directory without storing them in the backend:
|
|
613
|
-
|
|
614
|
-
[source,ruby]
|
|
615
|
-
----
|
|
390
|
+
# Read models (returns array, does NOT store in backend)
|
|
616
391
|
models = store.load_all(Concept, path: "./data", format: :yaml, layout: :separate)
|
|
617
|
-
# Returns an array of Concept instances, does NOT store in backend
|
|
618
|
-
----
|
|
619
392
|
|
|
620
|
-
|
|
393
|
+
# Read AND store in backend (makes them queryable)
|
|
394
|
+
store.import_all(Concept, path: "./data", format: :yaml, layout: :separate)
|
|
395
|
+
store.fetch(model: Concept, term: "API") # now available
|
|
621
396
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
[source,ruby]
|
|
397
|
+
# Export to a single file
|
|
398
|
+
store.export(all_concepts, path: "output.yaml", format: :yaml)
|
|
626
399
|
----
|
|
627
|
-
# Import all concepts from directory
|
|
628
|
-
loaded = store.import_all(Concept, path: "./data", format: :yaml, layout: :separate)
|
|
629
400
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
matching = store.where(model: Concept, status: "valid")
|
|
633
|
-
count = store.count(model: Concept)
|
|
634
|
-
----
|
|
401
|
+
Layout strategies: `:separate` (one file per model), `:grouped` (models grouped
|
|
402
|
+
by key), `:flat` (one file per model, no subdirectory).
|
|
635
403
|
|
|
636
|
-
|
|
404
|
+
== HTTP caching
|
|
637
405
|
|
|
638
|
-
|
|
406
|
+
`HttpCache` provides HTTP-aware caching with ETags, conditional requests (304),
|
|
407
|
+
Cache-Control directives, and Vary header support. It uses a storage adapter
|
|
408
|
+
internally and serializes cache entries as `Lutaml::Model` objects via JSON.
|
|
639
409
|
|
|
640
410
|
[source,ruby]
|
|
641
411
|
----
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
When a model's `key_value` DSL maps multiple attributes to the same serialized
|
|
649
|
-
key (e.g., `uuid` and `identifier` both mapping to `"id"`), a custom serializer
|
|
650
|
-
preserves both values through the store round-trip:
|
|
651
|
-
|
|
652
|
-
[source,ruby]
|
|
653
|
-
----
|
|
654
|
-
class ConceptStore
|
|
655
|
-
class Serializer
|
|
656
|
-
def serialize(model)
|
|
657
|
-
{
|
|
658
|
-
"_yaml" => model.to_yaml,
|
|
659
|
-
"_uuid" => model.uuid,
|
|
660
|
-
"_identifier" => model.identifier
|
|
661
|
-
}
|
|
662
|
-
end
|
|
663
|
-
|
|
664
|
-
def deserialize(data, model_class)
|
|
665
|
-
model = model_class.from_yaml(data["_yaml"])
|
|
666
|
-
model.assign_uuid(data["_uuid"]) if data["_uuid"]
|
|
667
|
-
model.identifier = data["_identifier"] if data["_identifier"]
|
|
668
|
-
model
|
|
669
|
-
end
|
|
670
|
-
end
|
|
671
|
-
end
|
|
672
|
-
|
|
673
|
-
store = Lutaml::Store.new(
|
|
674
|
-
adapter: :memory,
|
|
675
|
-
models: [
|
|
676
|
-
{
|
|
677
|
-
model: Concept,
|
|
678
|
-
key: :uuid,
|
|
679
|
-
dir: "concepts",
|
|
680
|
-
serializer: Serializer.new
|
|
681
|
-
}
|
|
682
|
-
]
|
|
412
|
+
cache = Lutaml::Store::HttpCache.new(
|
|
413
|
+
adapter_type: "memory",
|
|
414
|
+
default_ttl: 3600,
|
|
415
|
+
respect_http_headers: true,
|
|
416
|
+
enable_conditional_requests: true
|
|
683
417
|
)
|
|
684
|
-
----
|
|
685
|
-
|
|
686
|
-
[example]
|
|
687
|
-
====
|
|
688
|
-
Custom serializer with key collision workaround:
|
|
689
|
-
|
|
690
|
-
[source,ruby]
|
|
691
|
-
----
|
|
692
|
-
# Without custom serializer: to_hash loses one of uuid/identifier
|
|
693
|
-
# because both map to "id" key. The custom serializer stores the
|
|
694
|
-
# full YAML string plus explicit metadata fields, preserving both.
|
|
695
|
-
----
|
|
696
|
-
====
|
|
697
|
-
|
|
698
|
-
== Advanced features
|
|
699
|
-
|
|
700
|
-
=== Polymorphic inheritance handling
|
|
701
|
-
|
|
702
|
-
Lutaml::Store automatically handles polymorphic inheritance chains, storing
|
|
703
|
-
and retrieving the correct subclass instances:
|
|
704
|
-
|
|
705
|
-
[source,ruby]
|
|
706
|
-
----
|
|
707
|
-
class Vehicle < Lutaml::Model::Serializable
|
|
708
|
-
attribute :vehicle_id, :string
|
|
709
|
-
attribute :make, :string
|
|
710
|
-
attribute :_type, :string, default: -> { "Vehicle" }, polymorphic_class: true
|
|
711
|
-
end
|
|
712
418
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
419
|
+
# Fetch with automatic caching
|
|
420
|
+
response = cache.fetch("GET", "https://api.example.com/resource", {}) do |headers|
|
|
421
|
+
http_client.get("https://api.example.com/resource", headers)
|
|
716
422
|
end
|
|
717
423
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
attribute :_type, :string, default: -> { "Truck" }
|
|
721
|
-
end
|
|
722
|
-
|
|
723
|
-
store = Lutaml::Store.new(
|
|
724
|
-
adapter: :memory,
|
|
725
|
-
models: [
|
|
726
|
-
{ model: Vehicle, key: :vehicle_id, polymorphic_class_key: :_type }
|
|
727
|
-
]
|
|
728
|
-
)
|
|
424
|
+
# Second call returns cached response (no HTTP request made)
|
|
425
|
+
cached = cache.fetch("GET", "https://api.example.com/resource", {}) { raise "shouldn't be called" }
|
|
729
426
|
----
|
|
730
427
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
428
|
+
Supports `no-store`, `no-cache`, `must-revalidate`, `max-age`, ETag-based
|
|
429
|
+
conditional requests, query parameter normalization, and filesystem/sqlite
|
|
430
|
+
adapters for persistent cache storage.
|
|
734
431
|
|
|
735
|
-
|
|
736
|
-
----
|
|
737
|
-
# Save different vehicle types
|
|
738
|
-
vehicles = [
|
|
739
|
-
Car.new(vehicle_id: "car1", make: "Toyota", doors: 4),
|
|
740
|
-
Truck.new(vehicle_id: "truck1", make: "Ford", payload: 2000),
|
|
741
|
-
Vehicle.new(vehicle_id: "vehicle1", make: "Generic")
|
|
742
|
-
]
|
|
743
|
-
|
|
744
|
-
store.save(vehicles)
|
|
745
|
-
|
|
746
|
-
# Fetch returns correct subclass
|
|
747
|
-
car = store.fetch(model: Vehicle, vehicle_id: "car1")
|
|
748
|
-
puts car.class.name # => "Car"
|
|
749
|
-
puts car.doors # => 4
|
|
750
|
-
|
|
751
|
-
truck = store.fetch(model: Vehicle, vehicle_id: "truck1")
|
|
752
|
-
puts truck.class.name # => "Truck"
|
|
753
|
-
puts truck.payload # => 2000
|
|
754
|
-
----
|
|
755
|
-
====
|
|
432
|
+
== CacheStore
|
|
756
433
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
When registered models contain other registered models, Lutaml::Store manages
|
|
760
|
-
the relationships automatically:
|
|
434
|
+
`CacheStore` extends `BasicStore` with TTL-aware caching and LRU eviction:
|
|
761
435
|
|
|
762
436
|
[source,ruby]
|
|
763
437
|
----
|
|
764
|
-
|
|
765
|
-
attribute :order_id, :string
|
|
766
|
-
attribute :customer, User # User is registered
|
|
767
|
-
attribute :items, [Product] # Product is registered
|
|
768
|
-
attribute :total, :decimal
|
|
769
|
-
end
|
|
770
|
-
|
|
771
|
-
store = Lutaml::Store.new(
|
|
438
|
+
cache = Lutaml::Store::CacheStore.new(
|
|
772
439
|
adapter: :memory,
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
{ model: User, key: :user_id },
|
|
776
|
-
{ model: Product, key: :product_id }
|
|
777
|
-
]
|
|
440
|
+
max_size: 1000,
|
|
441
|
+
default_ttl: 3600
|
|
778
442
|
)
|
|
779
|
-
----
|
|
780
|
-
|
|
781
|
-
[example]
|
|
782
|
-
====
|
|
783
|
-
Composite model relationships:
|
|
784
443
|
|
|
785
|
-
|
|
444
|
+
cache.set("key1", "value1", ttl: 600)
|
|
445
|
+
cache.get("key1") # => "value1"
|
|
446
|
+
cache.fetch("key2", "default_value") # => "default_value" (stored)
|
|
447
|
+
cache.fetch("key3") { expensive_compute } # computes and caches
|
|
786
448
|
----
|
|
787
|
-
# Create order with nested registered models
|
|
788
|
-
order = Order.new(
|
|
789
|
-
order_id: "order1",
|
|
790
|
-
customer: User.new(user_id: "user1", name: "John Doe"),
|
|
791
|
-
items: [
|
|
792
|
-
Product.new(product_id: "prod1", name: "Widget", price: 10.00),
|
|
793
|
-
Product.new(product_id: "prod2", name: "Gadget", price: 15.00)
|
|
794
|
-
],
|
|
795
|
-
total: 25.00
|
|
796
|
-
)
|
|
797
|
-
|
|
798
|
-
store.save(order)
|
|
799
|
-
|
|
800
|
-
# All models are stored independently
|
|
801
|
-
customer = store.fetch(model: User, user_id: "user1")
|
|
802
|
-
product1 = store.fetch(model: Product, product_id: "prod1")
|
|
803
|
-
product2 = store.fetch(model: Product, product_id: "prod2")
|
|
804
|
-
|
|
805
|
-
# Order maintains references to all nested models
|
|
806
|
-
retrieved_order = store.fetch(model: Order, order_id: "order1")
|
|
807
|
-
puts retrieved_order.customer.name # => "John Doe"
|
|
808
|
-
puts retrieved_order.items.first.name # => "Widget"
|
|
809
|
-
----
|
|
810
|
-
====
|
|
811
|
-
|
|
812
|
-
=== Nested attribute updates with dot notation
|
|
813
|
-
|
|
814
|
-
Update deeply nested attributes using dot notation:
|
|
815
|
-
|
|
816
|
-
[source,ruby]
|
|
817
|
-
----
|
|
818
|
-
# Update nested attributes
|
|
819
|
-
store.update(
|
|
820
|
-
model: Order,
|
|
821
|
-
order_id: "order1",
|
|
822
|
-
attributes: [
|
|
823
|
-
{ key: "customer.name", value: "Jane Doe" },
|
|
824
|
-
{ key: "customer.email", value: "jane@example.com" },
|
|
825
|
-
{ key: "items.0.price", value: 12.00 }, # Update first item price
|
|
826
|
-
{ key: :total, value: 27.00 }
|
|
827
|
-
]
|
|
828
|
-
)
|
|
829
|
-
----
|
|
830
|
-
|
|
831
|
-
[example]
|
|
832
|
-
====
|
|
833
|
-
Complex nested updates:
|
|
834
|
-
|
|
835
|
-
[source,ruby]
|
|
836
|
-
----
|
|
837
|
-
# Multi-level nested updates
|
|
838
|
-
store.update(
|
|
839
|
-
model: PotteryClass,
|
|
840
|
-
class_id: "pottery_101",
|
|
841
|
-
attributes: [
|
|
842
|
-
{ key: "studio.name", value: "New Studio Name" },
|
|
843
|
-
{ key: "studio.location", value: "New Location" }
|
|
844
|
-
]
|
|
845
|
-
)
|
|
846
|
-
|
|
847
|
-
# Updates are reflected in both the parent and the independently stored model
|
|
848
|
-
pottery_class = store.fetch(model: PotteryClass, class_id: "pottery_101")
|
|
849
|
-
studio = store.fetch(model: Studio, studio_key: pottery_class.studio.studio_key)
|
|
850
|
-
|
|
851
|
-
puts pottery_class.studio.name # => "New Studio Name"
|
|
852
|
-
puts studio.name # => "New Studio Name" (same instance)
|
|
853
|
-
----
|
|
854
|
-
====
|
|
855
449
|
|
|
856
450
|
== Storage backends
|
|
857
451
|
|
|
858
|
-
|
|
859
|
-
use cases and requirements.
|
|
860
|
-
|
|
861
|
-
=== Memory backend
|
|
862
|
-
|
|
863
|
-
Fast in-memory storage ideal for testing, caching, and temporary data:
|
|
864
|
-
|
|
865
|
-
[source,ruby]
|
|
866
|
-
----
|
|
867
|
-
store = Lutaml::Store.new(
|
|
868
|
-
adapter: :memory,
|
|
869
|
-
models: [
|
|
870
|
-
{ model: User, key: :user_id }
|
|
871
|
-
]
|
|
872
|
-
)
|
|
873
|
-
----
|
|
874
|
-
|
|
875
|
-
**Characteristics:**
|
|
876
|
-
|
|
877
|
-
* Fastest performance for all operations
|
|
878
|
-
|
|
879
|
-
* Volatile storage (data lost when process ends)
|
|
880
|
-
|
|
881
|
-
* No persistence across application restarts
|
|
882
|
-
|
|
883
|
-
* Ideal for testing and caching scenarios
|
|
884
|
-
|
|
885
|
-
=== FileSystem backend
|
|
886
|
-
|
|
887
|
-
Persistent file-based storage with directory organization:
|
|
888
|
-
|
|
889
|
-
[source,ruby]
|
|
890
|
-
----
|
|
891
|
-
store = Lutaml::Store.new(
|
|
892
|
-
adapter: {
|
|
893
|
-
type: :filesystem,
|
|
894
|
-
path: "./data/store",
|
|
895
|
-
extension: "json"
|
|
896
|
-
},
|
|
897
|
-
models: [
|
|
898
|
-
{ model: User, key: :user_id }
|
|
899
|
-
]
|
|
900
|
-
)
|
|
901
|
-
----
|
|
902
|
-
|
|
903
|
-
**Characteristics:**
|
|
904
|
-
|
|
905
|
-
* Persistent storage across application restarts
|
|
906
|
-
|
|
907
|
-
* Human-readable file format (JSON by default)
|
|
908
|
-
|
|
909
|
-
* Good for development and moderate data volumes
|
|
910
|
-
|
|
911
|
-
* Directory-based organization for easy browsing
|
|
912
|
-
|
|
913
|
-
[example]
|
|
914
|
-
====
|
|
915
|
-
FileSystem backend configuration:
|
|
916
|
-
|
|
917
|
-
[source,ruby]
|
|
918
|
-
----
|
|
919
|
-
store = Lutaml::Store.new(
|
|
920
|
-
adapter: {
|
|
921
|
-
type: :filesystem,
|
|
922
|
-
path: "/var/app/data",
|
|
923
|
-
extension: "dat",
|
|
924
|
-
create_directories: true
|
|
925
|
-
},
|
|
926
|
-
models: [
|
|
927
|
-
{ model: User, key: :user_id },
|
|
928
|
-
{ model: Post, key: :post_id }
|
|
929
|
-
]
|
|
930
|
-
)
|
|
931
|
-
|
|
932
|
-
# Files are organized by model type:
|
|
933
|
-
# /var/app/data/User/user1.dat
|
|
934
|
-
# /var/app/data/User/user2.dat
|
|
935
|
-
# /var/app/data/Post/post1.dat
|
|
936
|
-
----
|
|
937
|
-
====
|
|
938
|
-
|
|
939
|
-
=== SQLite backend
|
|
940
|
-
|
|
941
|
-
Database storage with ACID compliance and transaction support:
|
|
452
|
+
=== Memory
|
|
942
453
|
|
|
943
454
|
[source,ruby]
|
|
944
455
|
----
|
|
945
|
-
store = Lutaml::Store.new(
|
|
946
|
-
adapter: {
|
|
947
|
-
type: :sqlite,
|
|
948
|
-
path: "./data/store.db"
|
|
949
|
-
},
|
|
950
|
-
models: [
|
|
951
|
-
{ model: User, key: :user_id }
|
|
952
|
-
]
|
|
953
|
-
)
|
|
456
|
+
store = Lutaml::Store.new(adapter: :memory, models: [...])
|
|
954
457
|
----
|
|
955
458
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
* ACID compliance with transaction support
|
|
959
|
-
|
|
960
|
-
* Excellent durability and data integrity
|
|
459
|
+
Fast in-memory storage. Data lost on process exit. Thread-safe via mutex.
|
|
961
460
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
* SQL query capabilities (future enhancement)
|
|
965
|
-
|
|
966
|
-
[example]
|
|
967
|
-
====
|
|
968
|
-
SQLite backend with advanced options:
|
|
461
|
+
=== FileSystem
|
|
969
462
|
|
|
970
463
|
[source,ruby]
|
|
971
464
|
----
|
|
972
465
|
store = Lutaml::Store.new(
|
|
973
|
-
adapter: {
|
|
974
|
-
|
|
975
|
-
path: "/var/app/data/production.db",
|
|
976
|
-
options: {
|
|
977
|
-
journal_mode: "WAL",
|
|
978
|
-
synchronous: "NORMAL",
|
|
979
|
-
cache_size: 10000
|
|
980
|
-
}
|
|
981
|
-
},
|
|
982
|
-
models: [
|
|
983
|
-
{ model: User, key: :user_id },
|
|
984
|
-
{ model: Order, key: :order_id }
|
|
985
|
-
]
|
|
466
|
+
adapter: { type: :filesystem, path: "./data", extension: ".json" },
|
|
467
|
+
models: [...]
|
|
986
468
|
)
|
|
987
469
|
----
|
|
988
|
-
====
|
|
989
470
|
|
|
990
|
-
|
|
471
|
+
Persistent file-based storage with SHA-256 integrity checks. Files organized
|
|
472
|
+
by key in subdirectories.
|
|
991
473
|
|
|
992
|
-
===
|
|
993
|
-
|
|
994
|
-
Configure stores programmatically with full control over all options:
|
|
474
|
+
=== SQLite
|
|
995
475
|
|
|
996
476
|
[source,ruby]
|
|
997
477
|
----
|
|
998
478
|
store = Lutaml::Store.new(
|
|
999
|
-
adapter: {
|
|
1000
|
-
|
|
1001
|
-
path: "./data",
|
|
1002
|
-
extension: "json"
|
|
1003
|
-
},
|
|
1004
|
-
models: [
|
|
1005
|
-
{ model: User, key: :user_id },
|
|
1006
|
-
{
|
|
1007
|
-
model: Studio,
|
|
1008
|
-
key: :studio_key,
|
|
1009
|
-
polymorphic_class_key: :_class
|
|
1010
|
-
}
|
|
1011
|
-
],
|
|
1012
|
-
cache: {
|
|
1013
|
-
enabled: true,
|
|
1014
|
-
max_size: 1000,
|
|
1015
|
-
ttl: 3600
|
|
1016
|
-
},
|
|
1017
|
-
monitoring: {
|
|
1018
|
-
enabled: true
|
|
1019
|
-
},
|
|
1020
|
-
events: {
|
|
1021
|
-
async: false
|
|
1022
|
-
}
|
|
479
|
+
adapter: { type: :sqlite, path: "./store.db" },
|
|
480
|
+
models: [...]
|
|
1023
481
|
)
|
|
1024
482
|
----
|
|
1025
483
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
Use YAML files for environment-specific configurations:
|
|
1029
|
-
|
|
1030
|
-
[source,yaml]
|
|
1031
|
-
----
|
|
1032
|
-
# config/store.yml
|
|
1033
|
-
development:
|
|
1034
|
-
adapter:
|
|
1035
|
-
type: filesystem
|
|
1036
|
-
path: ./tmp/store
|
|
1037
|
-
extension: json
|
|
1038
|
-
models:
|
|
1039
|
-
- model: User
|
|
1040
|
-
key: user_id
|
|
1041
|
-
- model: Studio
|
|
1042
|
-
key: studio_key
|
|
1043
|
-
polymorphic_class_key: _class
|
|
1044
|
-
cache:
|
|
1045
|
-
enabled: true
|
|
1046
|
-
max_size: 100
|
|
1047
|
-
ttl: 1800
|
|
1048
|
-
|
|
1049
|
-
production:
|
|
1050
|
-
adapter:
|
|
1051
|
-
type: sqlite
|
|
1052
|
-
path: /var/app/data/store.db
|
|
1053
|
-
models:
|
|
1054
|
-
- model: User
|
|
1055
|
-
key: user_id
|
|
1056
|
-
- model: Studio
|
|
1057
|
-
key: studio_key
|
|
1058
|
-
polymorphic_class_key: _class
|
|
1059
|
-
cache:
|
|
1060
|
-
enabled: true
|
|
1061
|
-
max_size: 10000
|
|
1062
|
-
ttl: 3600
|
|
1063
|
-
monitoring:
|
|
1064
|
-
enabled: true
|
|
1065
|
-
----
|
|
1066
|
-
|
|
1067
|
-
[example]
|
|
1068
|
-
====
|
|
1069
|
-
Loading YAML configuration:
|
|
1070
|
-
|
|
1071
|
-
[source,ruby]
|
|
1072
|
-
----
|
|
1073
|
-
# Load environment-specific configuration
|
|
1074
|
-
config = YAML.load_file("config/store.yml")[Rails.env]
|
|
1075
|
-
|
|
1076
|
-
# Convert model configurations to proper format
|
|
1077
|
-
models = config["models"].map do |model_config|
|
|
1078
|
-
{
|
|
1079
|
-
model: model_config["model"].constantize,
|
|
1080
|
-
key: model_config["key"].to_sym,
|
|
1081
|
-
polymorphic_class_key: model_config["polymorphic_class_key"]&.to_sym
|
|
1082
|
-
}.compact
|
|
1083
|
-
end
|
|
1084
|
-
|
|
1085
|
-
store = Lutaml::Store.new(
|
|
1086
|
-
adapter: config["adapter"],
|
|
1087
|
-
models: models,
|
|
1088
|
-
cache: config["cache"],
|
|
1089
|
-
monitoring: config["monitoring"]
|
|
1090
|
-
)
|
|
1091
|
-
----
|
|
1092
|
-
====
|
|
484
|
+
ACID-compliant database storage. Requires `sqlite3` gem. Thread-safe with
|
|
485
|
+
connection pooling.
|
|
1093
486
|
|
|
1094
487
|
== Event system
|
|
1095
488
|
|
|
1096
|
-
Lutaml::Store provides a comprehensive event system for monitoring and reacting
|
|
1097
|
-
to store operations.
|
|
1098
|
-
|
|
1099
|
-
=== Available events
|
|
1100
|
-
|
|
1101
|
-
The store emits events for all major operations:
|
|
1102
|
-
|
|
1103
|
-
* `:model_save` - When models are saved
|
|
1104
|
-
|
|
1105
|
-
* `:model_fetch` - When models are fetched
|
|
1106
|
-
|
|
1107
|
-
* `:model_update` - When models are updated
|
|
1108
|
-
|
|
1109
|
-
* `:model_destroy` - When models are destroyed
|
|
1110
|
-
|
|
1111
|
-
* `:model_save_all` - When models are batch-saved to directory
|
|
1112
|
-
|
|
1113
|
-
* `:model_import` - When models are imported from directory into backend
|
|
1114
|
-
|
|
1115
|
-
* `:model_export` - When models are exported to a single file
|
|
1116
|
-
|
|
1117
|
-
* `:model_load_error` - When a file fails to load during load_all/import_all
|
|
1118
|
-
|
|
1119
|
-
* `:composite_model_stored` - When composite models are stored independently
|
|
1120
|
-
|
|
1121
|
-
* `:polymorphic_model_resolved` - When polymorphic models are resolved
|
|
1122
|
-
|
|
1123
|
-
=== Event listeners
|
|
1124
|
-
|
|
1125
|
-
Register event listeners to react to store operations:
|
|
1126
|
-
|
|
1127
489
|
[source,ruby]
|
|
1128
490
|
----
|
|
1129
|
-
|
|
1130
|
-
store.on(:
|
|
1131
|
-
|
|
1132
|
-
end
|
|
1133
|
-
|
|
1134
|
-
store.on(:model_update) do |event_data|
|
|
1135
|
-
puts "Updated #{event_data[:model].class.name}: #{event_data[:changes]}"
|
|
1136
|
-
end
|
|
1137
|
-
|
|
1138
|
-
store.on(:model_fetch) do |event_data|
|
|
1139
|
-
puts "Fetched #{event_data[:model].class.name} from #{event_data[:source]}"
|
|
1140
|
-
end
|
|
491
|
+
store.on(:model_save) { |data| logger.info("Saved #{data[:model].class}") }
|
|
492
|
+
store.on(:model_fetch) { |data| logger.debug("Fetched #{data[:key]}") }
|
|
493
|
+
store.on(:model_destroy) { |data| audit_log << data }
|
|
1141
494
|
----
|
|
1142
495
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
[source,ruby]
|
|
1148
|
-
----
|
|
1149
|
-
# Audit trail implementation
|
|
1150
|
-
audit_log = []
|
|
1151
|
-
|
|
1152
|
-
store.on(:model_save) do |data|
|
|
1153
|
-
audit_log << {
|
|
1154
|
-
action: :save,
|
|
1155
|
-
model: data[:model].class.name,
|
|
1156
|
-
key: data[:key],
|
|
1157
|
-
timestamp: Time.now
|
|
1158
|
-
}
|
|
1159
|
-
end
|
|
1160
|
-
|
|
1161
|
-
store.on(:model_update) do |data|
|
|
1162
|
-
audit_log << {
|
|
1163
|
-
action: :update,
|
|
1164
|
-
model: data[:model].class.name,
|
|
1165
|
-
key: data[:key],
|
|
1166
|
-
changes: data[:changes],
|
|
1167
|
-
timestamp: Time.now
|
|
1168
|
-
}
|
|
1169
|
-
end
|
|
1170
|
-
|
|
1171
|
-
# Use the store
|
|
1172
|
-
user = User.new(user_id: "user1", name: "John")
|
|
1173
|
-
store.save(user)
|
|
1174
|
-
|
|
1175
|
-
store.update(model: User, user_id: "user1") do |u|
|
|
1176
|
-
u.name = "Jane"
|
|
1177
|
-
end
|
|
1178
|
-
|
|
1179
|
-
puts audit_log
|
|
1180
|
-
# => [
|
|
1181
|
-
# { action: :save, model: "User", key: "user1", timestamp: ... },
|
|
1182
|
-
# { action: :update, model: "User", key: "user1", changes: {...}, timestamp: ... }
|
|
1183
|
-
# ]
|
|
1184
|
-
----
|
|
1185
|
-
====
|
|
1186
|
-
|
|
1187
|
-
== Performance and monitoring
|
|
1188
|
-
|
|
1189
|
-
=== Performance characteristics
|
|
1190
|
-
|
|
1191
|
-
Different backends have different performance profiles:
|
|
1192
|
-
|
|
1193
|
-
**Memory Backend:**
|
|
1194
|
-
|
|
1195
|
-
* Read: O(1) - Hash lookup
|
|
1196
|
-
|
|
1197
|
-
* Write: O(1) - Hash assignment
|
|
1198
|
-
|
|
1199
|
-
* Memory usage: All data in RAM
|
|
1200
|
-
|
|
1201
|
-
**FileSystem Backend:**
|
|
1202
|
-
|
|
1203
|
-
* Read: O(1) + file I/O
|
|
1204
|
-
|
|
1205
|
-
* Write: O(1) + file I/O + serialization
|
|
1206
|
-
|
|
1207
|
-
* Memory usage: Minimal (data on disk)
|
|
1208
|
-
|
|
1209
|
-
**SQLite Backend:**
|
|
1210
|
-
|
|
1211
|
-
* Read: O(log n) - B-tree lookup
|
|
1212
|
-
|
|
1213
|
-
* Write: O(log n) + transaction overhead
|
|
1214
|
-
|
|
1215
|
-
* Memory usage: Configurable cache + minimal
|
|
1216
|
-
|
|
1217
|
-
=== Monitoring and statistics
|
|
1218
|
-
|
|
1219
|
-
Enable monitoring to track performance and usage:
|
|
1220
|
-
|
|
1221
|
-
[source,ruby]
|
|
1222
|
-
----
|
|
1223
|
-
store = Lutaml::Store.new(
|
|
1224
|
-
adapter: :memory,
|
|
1225
|
-
models: [{ model: User, key: :user_id }],
|
|
1226
|
-
monitoring: { enabled: true }
|
|
1227
|
-
)
|
|
1228
|
-
|
|
1229
|
-
# Get comprehensive statistics
|
|
1230
|
-
stats = store.stats
|
|
1231
|
-
puts stats
|
|
1232
|
-
# => {
|
|
1233
|
-
# models_registered: 1,
|
|
1234
|
-
# total_operations: 150,
|
|
1235
|
-
# operations: {
|
|
1236
|
-
# save: 50,
|
|
1237
|
-
# fetch: 80,
|
|
1238
|
-
# update: 15,
|
|
1239
|
-
# destroy: 5
|
|
1240
|
-
# },
|
|
1241
|
-
# performance: {
|
|
1242
|
-
# save: { avg: 0.001, min: 0.0005, max: 0.002 },
|
|
1243
|
-
# fetch: { avg: 0.0008, min: 0.0003, max: 0.0015 }
|
|
1244
|
-
# },
|
|
1245
|
-
# backend: "Memory",
|
|
1246
|
-
# cache_hit_rate: 0.75
|
|
1247
|
-
# }
|
|
1248
|
-
----
|
|
1249
|
-
|
|
1250
|
-
[example]
|
|
1251
|
-
====
|
|
1252
|
-
Performance monitoring in production:
|
|
1253
|
-
|
|
1254
|
-
[source,ruby]
|
|
1255
|
-
----
|
|
1256
|
-
# Monitor cache performance
|
|
1257
|
-
store.on(:cache_miss) do |data|
|
|
1258
|
-
metrics.increment("store.cache.miss", tags: ["model:#{data[:model]}"])
|
|
1259
|
-
end
|
|
1260
|
-
|
|
1261
|
-
store.on(:cache_hit) do |data|
|
|
1262
|
-
metrics.increment("store.cache.hit", tags: ["model:#{data[:model]}"])
|
|
1263
|
-
end
|
|
1264
|
-
----
|
|
1265
|
-
====
|
|
496
|
+
Events: `:model_save`, `:model_fetch`, `:model_update`, `:model_destroy`,
|
|
497
|
+
`:model_save_all`, `:model_import`, `:model_export`, `:model_load_error`,
|
|
498
|
+
`:composite_model_stored`, `:polymorphic_model_resolved`.
|
|
1266
499
|
|
|
1267
500
|
== Error handling
|
|
1268
501
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
=== Error types
|
|
502
|
+
Error hierarchy:
|
|
1272
503
|
|
|
1273
|
-
`Lutaml::Store::
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
`
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
`
|
|
1280
|
-
Raised when polymorphic model updates fail due to type conflicts.
|
|
1281
|
-
|
|
1282
|
-
`Lutaml::Store::CompositeModelError`::
|
|
1283
|
-
Raised when composite model handling encounters issues.
|
|
1284
|
-
|
|
1285
|
-
`Lutaml::Store::ConfigurationError`::
|
|
1286
|
-
Raised for invalid store or adapter configurations.
|
|
1287
|
-
|
|
1288
|
-
=== Error handling patterns
|
|
1289
|
-
|
|
1290
|
-
[source,ruby]
|
|
1291
|
-
----
|
|
1292
|
-
begin
|
|
1293
|
-
store = Lutaml::Store.new(
|
|
1294
|
-
adapter: :memory,
|
|
1295
|
-
models: [
|
|
1296
|
-
{ model: User, key: :invalid_field } # Field doesn't exist
|
|
1297
|
-
]
|
|
1298
|
-
)
|
|
1299
|
-
rescue Lutaml::Store::ConfigurationError => e
|
|
1300
|
-
puts "Configuration error: #{e.message}"
|
|
1301
|
-
end
|
|
1302
|
-
|
|
1303
|
-
begin
|
|
1304
|
-
# Try to fetch unregistered model
|
|
1305
|
-
result = store.fetch(model: UnregisteredModel, id: "test")
|
|
1306
|
-
rescue Lutaml::Store::ModelNotRegisteredError => e
|
|
1307
|
-
puts "Model not registered: #{e.message}"
|
|
1308
|
-
end
|
|
1309
|
-
----
|
|
1310
|
-
|
|
1311
|
-
[example]
|
|
1312
|
-
====
|
|
1313
|
-
Comprehensive error handling:
|
|
1314
|
-
|
|
1315
|
-
[source,ruby]
|
|
1316
|
-
----
|
|
1317
|
-
def safe_store_operation
|
|
1318
|
-
yield
|
|
1319
|
-
rescue Lutaml::Store::ModelNotRegisteredError => e
|
|
1320
|
-
logger.error "Unregistered model: #{e.message}"
|
|
1321
|
-
{ error: "Model not found", details: e.message }
|
|
1322
|
-
rescue Lutaml::Store::InvalidKeyError => e
|
|
1323
|
-
logger.error "Invalid key: #{e.message}"
|
|
1324
|
-
{ error: "Invalid key", details: e.message }
|
|
1325
|
-
rescue Lutaml::Store::ConfigurationError => e
|
|
1326
|
-
logger.error "Configuration error: #{e.message}"
|
|
1327
|
-
{ error: "Configuration issue", details: e.message }
|
|
1328
|
-
rescue => e
|
|
1329
|
-
logger.error "Unexpected error: #{e.message}"
|
|
1330
|
-
{ error: "Internal error", details: e.message }
|
|
1331
|
-
end
|
|
1332
|
-
----
|
|
1333
|
-
====
|
|
504
|
+
* `Lutaml::Store::Error`
|
|
505
|
+
** `ConfigurationError` — invalid store or adapter config
|
|
506
|
+
** `BackendError` — adapter-level failures
|
|
507
|
+
** `ModelNotRegisteredError` — operations on unregistered models
|
|
508
|
+
** `InvalidKeyError` — missing or invalid key fields
|
|
509
|
+
** `PolymorphicUpdateError` — polymorphic type conflicts
|
|
510
|
+
** `CompositeModelError` — composite model handling failures
|
|
1334
511
|
|
|
1335
512
|
== Thread safety
|
|
1336
513
|
|
|
1337
|
-
All
|
|
1338
|
-
|
|
1339
|
-
[source,ruby]
|
|
1340
|
-
----
|
|
1341
|
-
store = Lutaml::Store.new(
|
|
1342
|
-
adapter: :memory,
|
|
1343
|
-
models: [{ model: User, key: :user_id }]
|
|
1344
|
-
)
|
|
1345
|
-
|
|
1346
|
-
# Safe to use from multiple threads
|
|
1347
|
-
threads = 10.times.map do |i|
|
|
1348
|
-
Thread.new do
|
|
1349
|
-
user = User.new(user_id: "user#{i}", name: "User #{i}")
|
|
1350
|
-
store.save(user)
|
|
1351
|
-
retrieved = store.fetch(model: User, user_id: "user#{i}")
|
|
1352
|
-
puts "Thread #{i}: #{retrieved.name}"
|
|
1353
|
-
end
|
|
1354
|
-
end
|
|
1355
|
-
|
|
1356
|
-
threads.each(&:join)
|
|
1357
|
-
----
|
|
1358
|
-
|
|
1359
|
-
[example]
|
|
1360
|
-
Concurrent operations with different models:
|
|
1361
|
-
|
|
1362
|
-
[source,ruby]
|
|
1363
|
-
----
|
|
1364
|
-
store = Lutaml::Store.new(
|
|
1365
|
-
adapter: :sqlite,
|
|
1366
|
-
models: [
|
|
1367
|
-
{ model: User, key: :user_id },
|
|
1368
|
-
{ model: Post, key: :post_id }
|
|
1369
|
-
]
|
|
1370
|
-
)
|
|
1371
|
-
|
|
1372
|
-
# Multiple threads can safely operate on different models
|
|
1373
|
-
user_thread = Thread.new do
|
|
1374
|
-
100.times do |i|
|
|
1375
|
-
user = User.new(user_id: "user#{i}", name: "User #{i}")
|
|
1376
|
-
store.save(user)
|
|
1377
|
-
end
|
|
1378
|
-
end
|
|
1379
|
-
|
|
1380
|
-
post_thread = Thread.new do
|
|
1381
|
-
100.times do |i|
|
|
1382
|
-
post = Post.new(post_id: "post#{i}", title: "Post #{i}")
|
|
1383
|
-
store.save(post)
|
|
1384
|
-
end
|
|
1385
|
-
end
|
|
1386
|
-
|
|
1387
|
-
[user_thread, post_thread].each(&:join)
|
|
1388
|
-
----
|
|
1389
|
-
|
|
514
|
+
All adapters use mutex-based synchronization. Safe for concurrent use across
|
|
515
|
+
threads.
|
|
1390
516
|
|
|
1391
517
|
== Development
|
|
1392
518
|
|
|
1393
|
-
After checking out the repo, run:
|
|
1394
|
-
|
|
1395
|
-
[source,sh]
|
|
1396
|
-
----
|
|
1397
|
-
bin/setup # Install dependencies
|
|
1398
|
-
bundle exec rspec # Run tests
|
|
1399
|
-
bundle exec rubocop # Run linting
|
|
1400
|
-
----
|
|
1401
|
-
|
|
1402
|
-
To install this gem onto your local machine, run:
|
|
1403
|
-
|
|
1404
519
|
[source,sh]
|
|
1405
520
|
----
|
|
1406
|
-
|
|
521
|
+
bin/setup # Install dependencies
|
|
522
|
+
bundle exec rake # Run specs + rubocop
|
|
523
|
+
bundle exec rspec # Run all specs
|
|
524
|
+
bundle exec rspec spec/lutaml/store/database_store_spec.rb # Single spec file
|
|
525
|
+
bundle exec rubocop # Lint
|
|
1407
526
|
----
|
|
1408
527
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
[source,sh]
|
|
1412
|
-
----
|
|
1413
|
-
bundle exec rake release
|
|
1414
|
-
----
|
|
528
|
+
Ruby >= 3.1 required.
|
|
1415
529
|
|
|
1416
530
|
== Contributing
|
|
1417
531
|
|
|
1418
532
|
Bug reports and pull requests are welcome on GitHub at
|
|
1419
533
|
https://github.com/lutaml/lutaml-store.
|
|
1420
534
|
|
|
1421
|
-
|
|
1422
|
-
contributors are expected to adhere to the
|
|
1423
|
-
https://github.com/lutaml/lutaml-store/blob/main/CODE_OF_CONDUCT.md[code of conduct].
|
|
1424
|
-
|
|
1425
|
-
== License and copyright
|
|
535
|
+
== License
|
|
1426
536
|
|
|
1427
|
-
|
|
1428
|
-
See the link:LICENSE[] file for details.
|
|
537
|
+
BSD-2-Clause. See the link:LICENSE[] file for details.
|
|
1429
538
|
|
|
1430
539
|
Copyright Ribose.
|