lutaml-store 0.2.0 → 0.2.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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +13 -177
- data/CLAUDE.md +22 -9
- data/README.adoc +326 -1103
- data/lib/lutaml/store/adapter/base.rb +4 -0
- data/lib/lutaml/store/adapter/memory.rb +8 -0
- data/lib/lutaml/store/adapter.rb +17 -0
- data/lib/lutaml/store/basic_store.rb +1 -10
- data/lib/lutaml/store/cache_store.rb +11 -16
- data/lib/lutaml/store/database_store.rb +19 -9
- data/lib/lutaml/store/format/base.rb +4 -0
- data/lib/lutaml/store/format/marshal_format.rb +4 -0
- data/lib/lutaml/store/format/xml.rb +37 -0
- data/lib/lutaml/store/format/yamls.rb +1 -1
- data/lib/lutaml/store/format.rb +22 -1
- data/lib/lutaml/store/format_serializer.rb +44 -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 +60 -0
- data/lib/lutaml/store/package_transport/directory_transport.rb +196 -0
- data/lib/lutaml/store/package_transport/zip_transport.rb +183 -0
- data/lib/lutaml/store/package_transport.rb +11 -438
- data/lib/lutaml/store/storage_key.rb +1 -1
- data/lib/lutaml/store/version.rb +1 -1
- data/lib/lutaml/store.rb +1 -0
- metadata +14 -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,402 @@ 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:
|
|
268
|
+
=== Save
|
|
350
269
|
|
|
351
270
|
[source,ruby]
|
|
352
271
|
----
|
|
353
|
-
|
|
354
|
-
|
|
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")
|
|
272
|
+
store.save(User.new(user_id: "u1", name: "Ada"))
|
|
273
|
+
store.save([user1, user2, user3]) # bulk save
|
|
372
274
|
----
|
|
373
|
-
====
|
|
374
275
|
|
|
375
|
-
=== Fetch
|
|
376
|
-
|
|
377
|
-
Retrieve models by their registered key fields:
|
|
276
|
+
=== Fetch
|
|
378
277
|
|
|
379
278
|
[source,ruby]
|
|
380
279
|
----
|
|
381
|
-
|
|
382
|
-
user = store.fetch(model: User, user_id: "user1")
|
|
383
|
-
pottery_class = store.fetch(model: PotteryClass, class_id: "pottery_101")
|
|
384
|
-
studio = store.fetch(model: Studio, studio_key: "main_studio")
|
|
280
|
+
user = store.fetch(model: User, user_id: "u1")
|
|
385
281
|
----
|
|
386
282
|
|
|
387
|
-
|
|
388
|
-
====
|
|
389
|
-
Fetching polymorphic models:
|
|
390
|
-
|
|
391
|
-
[source,ruby]
|
|
392
|
-
----
|
|
393
|
-
# Save different studio types
|
|
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"
|
|
410
|
-
----
|
|
411
|
-
====
|
|
412
|
-
|
|
413
|
-
=== Update operations
|
|
414
|
-
|
|
415
|
-
Update models using attribute arrays, dot notation, or blocks:
|
|
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
|
)
|
|
429
|
-
----
|
|
430
295
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
----
|
|
435
|
-
store.update(
|
|
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
|
-
)
|
|
444
|
-
----
|
|
445
|
-
|
|
446
|
-
[example]
|
|
447
|
-
====
|
|
448
|
-
Complex nested updates:
|
|
449
|
-
|
|
450
|
-
[source,ruby]
|
|
451
|
-
----
|
|
452
|
-
# Update multiple nested attributes
|
|
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
|
-
====
|
|
470
|
-
|
|
471
|
-
==== Block-based updates
|
|
472
|
-
|
|
473
|
-
[source,ruby]
|
|
474
|
-
----
|
|
475
|
-
store.update(model: User, user_id: "user1") do |user|
|
|
476
|
-
user.name = "Updated Name"
|
|
477
|
-
user.email = "updated@example.com"
|
|
296
|
+
# Block-based
|
|
297
|
+
store.update(model: User, user_id: "u1") do |user|
|
|
298
|
+
user.name = "Grace"
|
|
478
299
|
end
|
|
479
300
|
----
|
|
480
301
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
Change model types by updating with different polymorphic instances:
|
|
302
|
+
=== Destroy
|
|
484
303
|
|
|
485
304
|
[source,ruby]
|
|
486
305
|
----
|
|
487
|
-
|
|
488
|
-
store.update(
|
|
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"
|
|
306
|
+
store.destroy(model: User, user_id: "u1")
|
|
507
307
|
----
|
|
508
308
|
|
|
509
|
-
|
|
309
|
+
== PackageStore
|
|
510
310
|
|
|
511
|
-
|
|
311
|
+
`PackageStore` provides structured multi-model persistence with directory and ZIP
|
|
312
|
+
transports. Use it when you need to bundle multiple model types, binary assets,
|
|
313
|
+
and metadata into a single loadable package.
|
|
512
314
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
# Delete single model
|
|
516
|
-
store.destroy(model: User, user_id: "user1")
|
|
315
|
+
The workflow is: define the package schema with `PackageDefinition`, then load,
|
|
316
|
+
query, modify, and save using `PackageStore`.
|
|
517
317
|
|
|
518
|
-
|
|
519
|
-
store.destroy(model: PotteryClass, class_id: "pottery_101")
|
|
520
|
-
# Note: Nested Studio remains unless explicitly deleted
|
|
521
|
-
----
|
|
318
|
+
=== Define a package
|
|
522
319
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
Managing composite model deletion:
|
|
320
|
+
`PackageDefinition` declares the package structure — which models, assets, and
|
|
321
|
+
metadata the package contains:
|
|
526
322
|
|
|
527
323
|
[source,ruby]
|
|
528
324
|
----
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
# Studio still exists independently
|
|
533
|
-
studio = store.fetch(model: Studio, studio_key: "main_studio")
|
|
534
|
-
puts studio.name # => Still accessible
|
|
535
|
-
|
|
536
|
-
# Delete studio separately if needed
|
|
537
|
-
store.destroy(model: Studio, studio_key: "main_studio")
|
|
538
|
-
----
|
|
539
|
-
====
|
|
540
|
-
|
|
541
|
-
== File I/O and format handling
|
|
542
|
-
|
|
543
|
-
Lutaml::Store provides format-aware file I/O for batch persistence. Models can
|
|
544
|
-
be written to and read from directories using multiple serialization formats and
|
|
545
|
-
layout strategies.
|
|
546
|
-
|
|
547
|
-
=== Format handlers
|
|
548
|
-
|
|
549
|
-
Five built-in format handlers serialize and deserialize `Lutaml::Model::Serializable` instances:
|
|
325
|
+
require "lutaml/model"
|
|
326
|
+
require "lutaml/store"
|
|
550
327
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
| YAMLS | `:yamls` | `.yaml` | Multi-document YAML streams (many models per file)
|
|
557
|
-
| JSON | `:json` | `.json` | Single JSON objects
|
|
558
|
-
| JSONL | `:jsonl` | `.jsonl` | Line-delimited JSON (one object per line)
|
|
559
|
-
| Marshal | `:marshal` | `.bin` | Ruby Marshal binary format
|
|
560
|
-
|===
|
|
328
|
+
# Define your models
|
|
329
|
+
class Concept < Lutaml::Model::Serializable
|
|
330
|
+
attribute :term, :string
|
|
331
|
+
attribute :definition, :string
|
|
332
|
+
end
|
|
561
333
|
|
|
562
|
-
|
|
563
|
-
|
|
334
|
+
class Author < Lutaml::Model::Serializable
|
|
335
|
+
attribute :name, :string
|
|
336
|
+
attribute :email, :string
|
|
337
|
+
end
|
|
564
338
|
|
|
565
|
-
|
|
339
|
+
class GlossaryInfo < Lutaml::Model::Serializable
|
|
340
|
+
attribute :title, :string
|
|
341
|
+
attribute :version, :string
|
|
342
|
+
end
|
|
566
343
|
|
|
567
|
-
|
|
344
|
+
# Declare the package schema
|
|
345
|
+
glossary = Lutaml::Store::PackageDefinition.new(
|
|
346
|
+
name: "glossary",
|
|
347
|
+
metadata_model: GlossaryInfo,
|
|
348
|
+
metadata_file: "glossary.yaml",
|
|
349
|
+
metadata_key: :title
|
|
350
|
+
) do |pkg|
|
|
351
|
+
# Models stored in subdirectories, one file per instance
|
|
352
|
+
pkg.model(model: Concept, key: :term, dir: "concepts", default_format: :yaml)
|
|
353
|
+
pkg.model(model: Author, key: :name, dir: "authors", default_format: :json)
|
|
354
|
+
|
|
355
|
+
# Binary assets bundled in the package
|
|
356
|
+
pkg.asset("logo.png", type: :file)
|
|
357
|
+
pkg.asset("attachments", type: :directory)
|
|
358
|
+
end
|
|
359
|
+
----
|
|
568
360
|
|
|
569
|
-
|
|
570
|
-
|===
|
|
571
|
-
| Layout | Symbol | Structure
|
|
361
|
+
Each `pkg.model` call registers a model type with:
|
|
572
362
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
363
|
+
* `model:` — the `Lutaml::Model::Serializable` class
|
|
364
|
+
* `key:` — attribute used as the unique identifier (becomes the filename)
|
|
365
|
+
* `dir:` — subdirectory for this model type (use `file:` instead for a single-file model)
|
|
366
|
+
* `layout:` — `:separate` (one file per instance, default) or `:grouped` (multiple instances per file)
|
|
367
|
+
* `default_format:` — `:yaml`, `:json`, `:jsonl`, `:yamls`, or `:marshal`
|
|
577
368
|
|
|
578
|
-
===
|
|
369
|
+
=== Directory structure
|
|
579
370
|
|
|
580
|
-
|
|
371
|
+
When saved to disk, the glossary package produces:
|
|
581
372
|
|
|
582
|
-
[source,ruby]
|
|
583
373
|
----
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
#
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
374
|
+
my_glossary/
|
|
375
|
+
├── glossary.yaml # metadata (GlossaryInfo)
|
|
376
|
+
├── concepts/ # Concept model instances
|
|
377
|
+
│ ├── API.yaml
|
|
378
|
+
│ ├── REST.yaml
|
|
379
|
+
│ └── YAML.yaml
|
|
380
|
+
├── authors/ # Author model instances
|
|
381
|
+
│ ├── john_doe.json
|
|
382
|
+
│ └── jane_smith.json
|
|
383
|
+
├── logo.png # asset file
|
|
384
|
+
└── attachments/ # asset directory
|
|
385
|
+
└── spec.pdf
|
|
593
386
|
----
|
|
594
387
|
|
|
595
|
-
|
|
388
|
+
=== Load and query
|
|
596
389
|
|
|
597
390
|
[source,ruby]
|
|
598
391
|
----
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
----
|
|
392
|
+
# Load from directory
|
|
393
|
+
store = Lutaml::Store::PackageStore.load(glossary, "./my_glossary")
|
|
609
394
|
|
|
610
|
-
|
|
395
|
+
# Load from ZIP file
|
|
396
|
+
store = Lutaml::Store::PackageStore.load(glossary, "./glossary.zip", transport: :zip)
|
|
611
397
|
|
|
612
|
-
|
|
398
|
+
# Access metadata
|
|
399
|
+
store.metadata # => GlossaryInfo instance
|
|
613
400
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
#
|
|
618
|
-
|
|
401
|
+
# Query models
|
|
402
|
+
concepts = store.models_for(Concept) # => array of Concept instances
|
|
403
|
+
concept = store.fetch_model(Concept, "API")
|
|
404
|
+
store.model_count(Concept) # => 3
|
|
405
|
+
store.model_exists?(Concept, "REST") # => true
|
|
619
406
|
|
|
620
|
-
|
|
407
|
+
# Access assets
|
|
408
|
+
store.asset("logo.png") # => binary content
|
|
409
|
+
store.asset_paths # => ["logo.png", "attachments/spec.pdf"]
|
|
621
410
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
411
|
+
# Package statistics
|
|
412
|
+
store.stats
|
|
413
|
+
# => { package: "glossary", models: { "Concept" => 3, "Author" => 2 },
|
|
414
|
+
# assets: 2, metadata: true }
|
|
626
415
|
----
|
|
627
|
-
# Import all concepts from directory
|
|
628
|
-
loaded = store.import_all(Concept, path: "./data", format: :yaml, layout: :separate)
|
|
629
|
-
|
|
630
|
-
# Now queryable via the store
|
|
631
|
-
concept = store.fetch(model: Concept, uuid: "abc-123")
|
|
632
|
-
matching = store.where(model: Concept, status: "valid")
|
|
633
|
-
count = store.count(model: Concept)
|
|
634
|
-
----
|
|
635
|
-
|
|
636
|
-
=== export: write to a single file
|
|
637
416
|
|
|
638
|
-
|
|
417
|
+
=== Modify and save
|
|
639
418
|
|
|
640
419
|
[source,ruby]
|
|
641
420
|
----
|
|
642
|
-
|
|
643
|
-
store.
|
|
644
|
-
|
|
421
|
+
# Add models
|
|
422
|
+
store.add_model(Concept.new(term: "JSON", definition: "..."))
|
|
423
|
+
store.add_models([concept1, concept2])
|
|
645
424
|
|
|
646
|
-
|
|
425
|
+
# Remove models
|
|
426
|
+
store.remove_model(Concept, "deprecated_term")
|
|
647
427
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
428
|
+
# Add/remove assets
|
|
429
|
+
store.add_asset("diagram.svg", svg_content)
|
|
430
|
+
store.remove_asset("old_diagram.svg")
|
|
651
431
|
|
|
652
|
-
|
|
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
|
|
432
|
+
# Save to directory (default format per model)
|
|
433
|
+
store.save("./output")
|
|
672
434
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
models: [
|
|
676
|
-
{
|
|
677
|
-
model: Concept,
|
|
678
|
-
key: :uuid,
|
|
679
|
-
dir: "concepts",
|
|
680
|
-
serializer: Serializer.new
|
|
681
|
-
}
|
|
682
|
-
]
|
|
683
|
-
)
|
|
684
|
-
----
|
|
435
|
+
# Save to ZIP with per-model format overrides
|
|
436
|
+
store.save("./glossary.zip", transport: :zip, formats: { Concept => :json })
|
|
685
437
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
Custom serializer with key collision workaround:
|
|
438
|
+
# Save with global format override
|
|
439
|
+
store.save("./output", format: :yaml)
|
|
689
440
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
#
|
|
693
|
-
# because both map to "id" key. The custom serializer stores the
|
|
694
|
-
# full YAML string plus explicit metadata fields, preserving both.
|
|
441
|
+
# Bulk operations
|
|
442
|
+
store.clear_models(Concept) # remove all Concept instances
|
|
443
|
+
store.clear_all # remove everything
|
|
695
444
|
----
|
|
696
|
-
====
|
|
697
445
|
|
|
698
|
-
|
|
446
|
+
=== Per-model format override
|
|
699
447
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
Lutaml::Store automatically handles polymorphic inheritance chains, storing
|
|
703
|
-
and retrieving the correct subclass instances:
|
|
448
|
+
The `save` method accepts format overrides:
|
|
704
449
|
|
|
705
450
|
[source,ruby]
|
|
706
451
|
----
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
attribute :make, :string
|
|
710
|
-
attribute :_type, :string, default: -> { "Vehicle" }, polymorphic_class: true
|
|
711
|
-
end
|
|
712
|
-
|
|
713
|
-
class Car < Vehicle
|
|
714
|
-
attribute :doors, :integer
|
|
715
|
-
attribute :_type, :string, default: -> { "Car" }
|
|
716
|
-
end
|
|
717
|
-
|
|
718
|
-
class Truck < Vehicle
|
|
719
|
-
attribute :payload, :integer
|
|
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
|
-
)
|
|
729
|
-
----
|
|
452
|
+
# Global format for all models
|
|
453
|
+
store.save("./out", format: :json)
|
|
730
454
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
Polymorphic inheritance in action:
|
|
455
|
+
# Per-model format
|
|
456
|
+
store.save("./out", formats: { Concept => :yaml, Author => :jsonl })
|
|
734
457
|
|
|
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
|
|
458
|
+
# Default: each model uses its default_format from the definition
|
|
459
|
+
store.save("./out")
|
|
754
460
|
----
|
|
755
|
-
====
|
|
756
461
|
|
|
757
|
-
===
|
|
462
|
+
=== Package transports
|
|
758
463
|
|
|
759
|
-
|
|
760
|
-
|
|
464
|
+
[cols="1,1,3"]
|
|
465
|
+
|===
|
|
466
|
+
| Transport | Symbol | Description
|
|
761
467
|
|
|
762
|
-
|
|
763
|
-
|
|
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
|
|
468
|
+
| `DirectoryTransport`
|
|
469
|
+
| `:directory`
|
|
470
|
+
| Filesystem directory with subdirectories per model type
|
|
770
471
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
{ model: User, key: :user_id },
|
|
776
|
-
{ model: Product, key: :product_id }
|
|
777
|
-
]
|
|
778
|
-
)
|
|
779
|
-
----
|
|
472
|
+
| `ZipTransport`
|
|
473
|
+
| `:zip`
|
|
474
|
+
| ZIP archive containing the same directory structure
|
|
475
|
+
|===
|
|
780
476
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
Composite model relationships:
|
|
477
|
+
Transports are resolved via registry (`PackageTransport.resolve(:zip)`),
|
|
478
|
+
extensible without modifying existing code.
|
|
784
479
|
|
|
785
|
-
|
|
786
|
-
----
|
|
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
|
-
)
|
|
480
|
+
== File I/O
|
|
797
481
|
|
|
798
|
-
|
|
482
|
+
DatabaseStore provides batch file I/O through `Format` handlers.
|
|
799
483
|
|
|
800
|
-
|
|
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")
|
|
484
|
+
=== Format handlers
|
|
804
485
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
puts retrieved_order.items.first.name # => "Widget"
|
|
809
|
-
----
|
|
810
|
-
====
|
|
486
|
+
[cols="1,1,1,1"]
|
|
487
|
+
|===
|
|
488
|
+
| Format | Symbol | Extension | Description
|
|
811
489
|
|
|
812
|
-
|
|
490
|
+
| YAML | `:yaml` | `.yaml` | Single-document YAML files
|
|
491
|
+
| YAMLS | `:yamls` | `.yaml` | Multi-document YAML streams
|
|
492
|
+
| JSON | `:json` | `.json` | Single JSON objects
|
|
493
|
+
| JSONL | `:jsonl` | `.jsonl` | Line-delimited JSON
|
|
494
|
+
| Marshal | `:marshal` | `.bin` | Ruby Marshal binary format
|
|
495
|
+
|===
|
|
813
496
|
|
|
814
|
-
|
|
497
|
+
=== save_all / load_all / import_all / export
|
|
815
498
|
|
|
816
499
|
[source,ruby]
|
|
817
500
|
----
|
|
818
|
-
#
|
|
819
|
-
store.
|
|
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:
|
|
501
|
+
# Write models to directory
|
|
502
|
+
store.save_all(concepts, path: "./data", format: :yaml, layout: :separate)
|
|
834
503
|
|
|
835
|
-
|
|
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
|
-
)
|
|
504
|
+
# Read models (returns array, does NOT store in backend)
|
|
505
|
+
models = store.load_all(Concept, path: "./data", format: :yaml, layout: :separate)
|
|
846
506
|
|
|
847
|
-
#
|
|
848
|
-
|
|
849
|
-
|
|
507
|
+
# Read AND store in backend (makes them queryable)
|
|
508
|
+
store.import_all(Concept, path: "./data", format: :yaml, layout: :separate)
|
|
509
|
+
store.fetch(model: Concept, term: "API") # now available
|
|
850
510
|
|
|
851
|
-
|
|
852
|
-
|
|
511
|
+
# Export to a single file
|
|
512
|
+
store.export(all_concepts, path: "output.yaml", format: :yaml)
|
|
853
513
|
----
|
|
854
|
-
====
|
|
855
514
|
|
|
856
|
-
|
|
515
|
+
Layout strategies: `:separate` (one file per model), `:grouped` (models grouped
|
|
516
|
+
by key), `:flat` (one file per model, no subdirectory).
|
|
857
517
|
|
|
858
|
-
|
|
859
|
-
use cases and requirements.
|
|
518
|
+
== HTTP caching
|
|
860
519
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
520
|
+
`HttpCache` provides HTTP-aware caching with ETags, conditional requests (304),
|
|
521
|
+
Cache-Control directives, and Vary header support. It uses a storage adapter
|
|
522
|
+
internally and serializes cache entries as `Lutaml::Model` objects via JSON.
|
|
864
523
|
|
|
865
524
|
[source,ruby]
|
|
866
525
|
----
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
526
|
+
cache = Lutaml::Store::HttpCache.new(
|
|
527
|
+
adapter_type: "memory",
|
|
528
|
+
default_ttl: 3600,
|
|
529
|
+
respect_http_headers: true,
|
|
530
|
+
enable_conditional_requests: true
|
|
872
531
|
)
|
|
873
|
-
----
|
|
874
|
-
|
|
875
|
-
**Characteristics:**
|
|
876
532
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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:
|
|
533
|
+
# Fetch with automatic caching
|
|
534
|
+
response = cache.fetch("GET", "https://api.example.com/resource", {}) do |headers|
|
|
535
|
+
http_client.get("https://api.example.com/resource", headers)
|
|
536
|
+
end
|
|
888
537
|
|
|
889
|
-
|
|
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
|
-
)
|
|
538
|
+
# Second call returns cached response (no HTTP request made)
|
|
539
|
+
cached = cache.fetch("GET", "https://api.example.com/resource", {}) { raise "shouldn't be called" }
|
|
901
540
|
----
|
|
902
541
|
|
|
903
|
-
|
|
542
|
+
Supports `no-store`, `no-cache`, `must-revalidate`, `max-age`, ETag-based
|
|
543
|
+
conditional requests, query parameter normalization, and filesystem/sqlite
|
|
544
|
+
adapters for persistent cache storage.
|
|
904
545
|
|
|
905
|
-
|
|
546
|
+
== CacheStore
|
|
906
547
|
|
|
907
|
-
|
|
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:
|
|
548
|
+
`CacheStore` extends `BasicStore` with TTL-aware caching and LRU eviction:
|
|
916
549
|
|
|
917
550
|
[source,ruby]
|
|
918
551
|
----
|
|
919
|
-
|
|
920
|
-
adapter:
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
extension: "dat",
|
|
924
|
-
create_directories: true
|
|
925
|
-
},
|
|
926
|
-
models: [
|
|
927
|
-
{ model: User, key: :user_id },
|
|
928
|
-
{ model: Post, key: :post_id }
|
|
929
|
-
]
|
|
552
|
+
cache = Lutaml::Store::CacheStore.new(
|
|
553
|
+
adapter: :memory,
|
|
554
|
+
max_size: 1000,
|
|
555
|
+
default_ttl: 3600
|
|
930
556
|
)
|
|
931
557
|
|
|
932
|
-
|
|
933
|
-
#
|
|
934
|
-
#
|
|
935
|
-
#
|
|
558
|
+
cache.set("key1", "value1", ttl: 600)
|
|
559
|
+
cache.get("key1") # => "value1"
|
|
560
|
+
cache.fetch("key2", "default_value") # => "default_value" (stored)
|
|
561
|
+
cache.fetch("key3") { expensive_compute } # computes and caches
|
|
936
562
|
----
|
|
937
|
-
====
|
|
938
563
|
|
|
939
|
-
|
|
564
|
+
== Storage backends
|
|
940
565
|
|
|
941
|
-
|
|
566
|
+
=== Memory
|
|
942
567
|
|
|
943
568
|
[source,ruby]
|
|
944
569
|
----
|
|
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
|
-
)
|
|
570
|
+
store = Lutaml::Store.new(adapter: :memory, models: [...])
|
|
954
571
|
----
|
|
955
572
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
* ACID compliance with transaction support
|
|
959
|
-
|
|
960
|
-
* Excellent durability and data integrity
|
|
573
|
+
Fast in-memory storage. Data lost on process exit. Thread-safe via mutex.
|
|
961
574
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
* SQL query capabilities (future enhancement)
|
|
965
|
-
|
|
966
|
-
[example]
|
|
967
|
-
====
|
|
968
|
-
SQLite backend with advanced options:
|
|
575
|
+
=== FileSystem
|
|
969
576
|
|
|
970
577
|
[source,ruby]
|
|
971
578
|
----
|
|
972
579
|
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
|
-
]
|
|
580
|
+
adapter: { type: :filesystem, path: "./data", extension: ".json" },
|
|
581
|
+
models: [...]
|
|
986
582
|
)
|
|
987
583
|
----
|
|
988
|
-
====
|
|
989
584
|
|
|
990
|
-
|
|
585
|
+
Persistent file-based storage with SHA-256 integrity checks. Files organized
|
|
586
|
+
by key in subdirectories.
|
|
991
587
|
|
|
992
|
-
===
|
|
993
|
-
|
|
994
|
-
Configure stores programmatically with full control over all options:
|
|
588
|
+
=== SQLite
|
|
995
589
|
|
|
996
590
|
[source,ruby]
|
|
997
591
|
----
|
|
998
592
|
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
|
-
}
|
|
593
|
+
adapter: { type: :sqlite, path: "./store.db" },
|
|
594
|
+
models: [...]
|
|
1023
595
|
)
|
|
1024
596
|
----
|
|
1025
597
|
|
|
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
|
-
====
|
|
598
|
+
ACID-compliant database storage. Requires `sqlite3` gem. Thread-safe with
|
|
599
|
+
connection pooling.
|
|
1093
600
|
|
|
1094
601
|
== Event system
|
|
1095
602
|
|
|
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
603
|
[source,ruby]
|
|
1128
604
|
----
|
|
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
|
|
605
|
+
store.on(:model_save) { |data| logger.info("Saved #{data[:model].class}") }
|
|
606
|
+
store.on(:model_fetch) { |data| logger.debug("Fetched #{data[:key]}") }
|
|
607
|
+
store.on(:model_destroy) { |data| audit_log << data }
|
|
1141
608
|
----
|
|
1142
609
|
|
|
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
|
-
====
|
|
610
|
+
Events: `:model_save`, `:model_fetch`, `:model_update`, `:model_destroy`,
|
|
611
|
+
`:model_save_all`, `:model_import`, `:model_export`, `:model_load_error`,
|
|
612
|
+
`:composite_model_stored`, `:polymorphic_model_resolved`.
|
|
1266
613
|
|
|
1267
614
|
== Error handling
|
|
1268
615
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
=== Error types
|
|
616
|
+
Error hierarchy:
|
|
1272
617
|
|
|
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
|
-
====
|
|
618
|
+
* `Lutaml::Store::Error`
|
|
619
|
+
** `ConfigurationError` — invalid store or adapter config
|
|
620
|
+
** `BackendError` — adapter-level failures
|
|
621
|
+
** `ModelNotRegisteredError` — operations on unregistered models
|
|
622
|
+
** `InvalidKeyError` — missing or invalid key fields
|
|
623
|
+
** `PolymorphicUpdateError` — polymorphic type conflicts
|
|
624
|
+
** `CompositeModelError` — composite model handling failures
|
|
1334
625
|
|
|
1335
626
|
== Thread safety
|
|
1336
627
|
|
|
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
|
-
|
|
628
|
+
All adapters use mutex-based synchronization. Safe for concurrent use across
|
|
629
|
+
threads.
|
|
1390
630
|
|
|
1391
631
|
== Development
|
|
1392
632
|
|
|
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
633
|
[source,sh]
|
|
1405
634
|
----
|
|
1406
|
-
|
|
635
|
+
bin/setup # Install dependencies
|
|
636
|
+
bundle exec rake # Run specs + rubocop
|
|
637
|
+
bundle exec rspec # Run all specs
|
|
638
|
+
bundle exec rspec spec/lutaml/store/database_store_spec.rb # Single spec file
|
|
639
|
+
bundle exec rubocop # Lint
|
|
1407
640
|
----
|
|
1408
641
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
[source,sh]
|
|
1412
|
-
----
|
|
1413
|
-
bundle exec rake release
|
|
1414
|
-
----
|
|
642
|
+
Ruby >= 3.1 required.
|
|
1415
643
|
|
|
1416
644
|
== Contributing
|
|
1417
645
|
|
|
1418
646
|
Bug reports and pull requests are welcome on GitHub at
|
|
1419
647
|
https://github.com/lutaml/lutaml-store.
|
|
1420
648
|
|
|
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
|
|
649
|
+
== License
|
|
1426
650
|
|
|
1427
|
-
|
|
1428
|
-
See the link:LICENSE[] file for details.
|
|
651
|
+
BSD-2-Clause. See the link:LICENSE[] file for details.
|
|
1429
652
|
|
|
1430
653
|
Copyright Ribose.
|