lutaml-store 0.1.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 +7 -0
- data/.github/workflows/main.yml +27 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +10 -0
- data/.rubocop_todo.yml +450 -0
- data/CLAUDE.md +57 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CORRECTED_HTTP_CACHE_IMPLEMENTATION.md +209 -0
- data/CORRECTED_HTTP_CACHE_PLAN.md +164 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +220 -0
- data/README.adoc +1430 -0
- data/Rakefile +12 -0
- data/TODO.impl/0-lutaml-store-self-quality.md +112 -0
- data/TODO.impl/1-lutaml-hal-migration.md +60 -0
- data/TODO.impl/2-glossarist-migration.md +359 -0
- data/TODO.impl/3-lutaml-jsonschema-migration.md +273 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/demo/Gemfile +15 -0
- data/demo/Gemfile.lock +61 -0
- data/demo/README.adoc +301 -0
- data/demo/data/vcards/co/contact_10_thompson.data +1 -0
- data/demo/data/vcards/co/contact_10_thompson.meta +1 -0
- data/demo/data/vcards/co/contact_1_doe.data +1 -0
- data/demo/data/vcards/co/contact_1_doe.meta +1 -0
- data/demo/data/vcards/co/contact_2_smith.data +1 -0
- data/demo/data/vcards/co/contact_2_smith.meta +1 -0
- data/demo/data/vcards/co/contact_3_johnson.data +1 -0
- data/demo/data/vcards/co/contact_3_johnson.meta +1 -0
- data/demo/data/vcards/co/contact_4_garcia.data +1 -0
- data/demo/data/vcards/co/contact_4_garcia.meta +1 -0
- data/demo/data/vcards/co/contact_5_wilson.data +1 -0
- data/demo/data/vcards/co/contact_5_wilson.meta +1 -0
- data/demo/data/vcards/co/contact_6_brown.data +1 -0
- data/demo/data/vcards/co/contact_6_brown.meta +1 -0
- data/demo/data/vcards/co/contact_7_davis.data +1 -0
- data/demo/data/vcards/co/contact_7_davis.meta +1 -0
- data/demo/data/vcards/co/contact_8_anderson.data +1 -0
- data/demo/data/vcards/co/contact_8_anderson.meta +1 -0
- data/demo/data/vcards/co/contact_9_taylor.data +1 -0
- data/demo/data/vcards/co/contact_9_taylor.meta +1 -0
- data/demo/data/vcards.db +0 -0
- data/demo/pottery_class_demo.rb +164 -0
- data/demo/vcard_models.rb +140 -0
- data/demo/vcard_store_demo.rb +526 -0
- data/lib/lutaml/store/adapter/base.rb +65 -0
- data/lib/lutaml/store/adapter/filesystem.rb +288 -0
- data/lib/lutaml/store/adapter/memory.rb +225 -0
- data/lib/lutaml/store/adapter/sqlite.rb +193 -0
- data/lib/lutaml/store/adapter.rb +12 -0
- data/lib/lutaml/store/attribute_updater.rb +198 -0
- data/lib/lutaml/store/basic_store.rb +190 -0
- data/lib/lutaml/store/cache.rb +108 -0
- data/lib/lutaml/store/cache_store.rb +282 -0
- data/lib/lutaml/store/composite_model_handler.rb +169 -0
- data/lib/lutaml/store/compression.rb +137 -0
- data/lib/lutaml/store/config.rb +178 -0
- data/lib/lutaml/store/database_store.rb +425 -0
- data/lib/lutaml/store/events.rb +92 -0
- data/lib/lutaml/store/format/base.rb +33 -0
- data/lib/lutaml/store/format/json.rb +25 -0
- data/lib/lutaml/store/format/jsonl.rb +37 -0
- data/lib/lutaml/store/format/marshal_format.rb +37 -0
- data/lib/lutaml/store/format/yaml.rb +29 -0
- data/lib/lutaml/store/format/yamls.rb +35 -0
- data/lib/lutaml/store/format.rb +33 -0
- data/lib/lutaml/store/http_cache.rb +279 -0
- data/lib/lutaml/store/http_cache_config.rb +53 -0
- data/lib/lutaml/store/http_cache_entry.rb +69 -0
- data/lib/lutaml/store/http_header_processor.rb +175 -0
- data/lib/lutaml/store/integrity.rb +102 -0
- data/lib/lutaml/store/model_registration.rb +75 -0
- data/lib/lutaml/store/model_registry.rb +123 -0
- data/lib/lutaml/store/model_serializer.rb +69 -0
- data/lib/lutaml/store/monitor.rb +192 -0
- data/lib/lutaml/store/storage_key.rb +40 -0
- data/lib/lutaml/store/version.rb +7 -0
- data/lib/lutaml/store.rb +41 -0
- data/lutaml-store.gemspec +35 -0
- data/plan.adoc +606 -0
- data/sig/lutaml/store.rbs +6 -0
- data/spec/lutaml/store/adapter_interface_spec.rb +89 -0
- data/spec/lutaml/store/anti_pattern_guard_spec.rb +35 -0
- data/spec/lutaml/store/anti_pattern_spec.rb +78 -0
- data/spec/lutaml/store/autoload_spec.rb +34 -0
- data/spec/lutaml/store/cache_store_spec.rb +271 -0
- data/spec/lutaml/store/compression_spec.rb +78 -0
- data/spec/lutaml/store/config_enhanced_spec.rb +158 -0
- data/spec/lutaml/store/corrected_http_cache_integration_spec.rb +336 -0
- data/spec/lutaml/store/custom_serializer_spec.rb +108 -0
- data/spec/lutaml/store/database_store_spec.rb +279 -0
- data/spec/lutaml/store/file_io_spec.rb +219 -0
- data/spec/lutaml/store/format_round_trip_spec.rb +110 -0
- data/spec/lutaml/store/format_spec.rb +70 -0
- data/spec/lutaml/store/http_cache_entry_spec.rb +203 -0
- data/spec/lutaml/store/http_cache_hal_integration_spec.rb +404 -0
- data/spec/lutaml/store/http_cache_spec.rb +422 -0
- data/spec/lutaml/store/http_header_processor_spec.rb +290 -0
- data/spec/lutaml/store/import_spec.rb +90 -0
- data/spec/lutaml/store/integrity_spec.rb +157 -0
- data/spec/lutaml/store/key_collision_serializer_spec.rb +98 -0
- data/spec/lutaml/store/load_save_spec.rb +107 -0
- data/spec/lutaml/store/lutaml_model_integration_spec.rb +291 -0
- data/spec/lutaml/store/model_serializer_spec.rb +140 -0
- data/spec/lutaml/store/store_spec.rb +182 -0
- data/spec/lutaml/store_spec.rb +21 -0
- data/spec/spec_helper.rb +16 -0
- metadata +166 -0
data/README.adoc
ADDED
|
@@ -0,0 +1,1430 @@
|
|
|
1
|
+
= Lutaml::Store
|
|
2
|
+
|
|
3
|
+
https://github.com/lutaml/lutaml-store[image:https://img.shields.io/github/stars/lutaml/lutaml-store.svg?style=social[GitHub Stars]]
|
|
4
|
+
https://github.com/lutaml/lutaml-store[image:https://img.shields.io/github/forks/lutaml/lutaml-store.svg?style=social[GitHub Forks]]
|
|
5
|
+
image:https://img.shields.io/github/license/lutaml/lutaml-store.svg[License]
|
|
6
|
+
image:https://img.shields.io/github/actions/workflow/status/lutaml/lutaml-store/test.yml?branch=main[Build Status]
|
|
7
|
+
image:https://img.shields.io/gem/v/lutaml-store.svg[RubyGems Version]
|
|
8
|
+
|
|
9
|
+
== Purpose
|
|
10
|
+
|
|
11
|
+
Lutaml::Store provides a sophisticated store-centric database-style API for
|
|
12
|
+
LutaML Models with model registry, polymorphic support, and composite model
|
|
13
|
+
relationships.
|
|
14
|
+
|
|
15
|
+
It offers a unified interface for storing and retrieving complex model
|
|
16
|
+
hierarchies across different storage backends, making it ideal for applications
|
|
17
|
+
that need sophisticated object persistence with database-like operations.
|
|
18
|
+
|
|
19
|
+
== Features
|
|
20
|
+
|
|
21
|
+
* Store-centric API design with all persistence operations through the store
|
|
22
|
+
|
|
23
|
+
* Model registry system with configurable key fields
|
|
24
|
+
|
|
25
|
+
* Polymorphic model support with inheritance handling
|
|
26
|
+
|
|
27
|
+
* Composite model relationships (nested registered models stored independently)
|
|
28
|
+
|
|
29
|
+
* Database-style CRUD operations (fetch, save, update, destroy)
|
|
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
|
|
48
|
+
|
|
49
|
+
== Installation
|
|
50
|
+
|
|
51
|
+
Add this line to your application's Gemfile:
|
|
52
|
+
|
|
53
|
+
[source,ruby]
|
|
54
|
+
----
|
|
55
|
+
gem 'lutaml-store'
|
|
56
|
+
----
|
|
57
|
+
|
|
58
|
+
And then execute:
|
|
59
|
+
|
|
60
|
+
[source,sh]
|
|
61
|
+
----
|
|
62
|
+
$ bundle install
|
|
63
|
+
----
|
|
64
|
+
|
|
65
|
+
Or install it yourself as:
|
|
66
|
+
|
|
67
|
+
[source,sh]
|
|
68
|
+
----
|
|
69
|
+
$ gem install lutaml-store
|
|
70
|
+
----
|
|
71
|
+
|
|
72
|
+
For SQLite backend support, also add:
|
|
73
|
+
|
|
74
|
+
[source,ruby]
|
|
75
|
+
----
|
|
76
|
+
gem 'sqlite3'
|
|
77
|
+
----
|
|
78
|
+
|
|
79
|
+
== Quick start
|
|
80
|
+
|
|
81
|
+
Here's a minimal example to get you started:
|
|
82
|
+
|
|
83
|
+
[source,ruby]
|
|
84
|
+
----
|
|
85
|
+
require 'lutaml/model'
|
|
86
|
+
require 'lutaml/store'
|
|
87
|
+
|
|
88
|
+
# Define your models
|
|
89
|
+
class Studio < Lutaml::Model::Serializable
|
|
90
|
+
attribute :studio_key, :string
|
|
91
|
+
attribute :name, :string
|
|
92
|
+
attribute :location, :string
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class PotteryClass < Lutaml::Model::Serializable
|
|
96
|
+
attribute :studio, Studio
|
|
97
|
+
attribute :class_id, :string
|
|
98
|
+
attribute :description, :string
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Create a store with model registry
|
|
102
|
+
store = Lutaml::Store.new(
|
|
103
|
+
adapter: :memory,
|
|
104
|
+
models: [
|
|
105
|
+
{ model: PotteryClass, key: :class_id },
|
|
106
|
+
{ model: Studio, key: :studio_key }
|
|
107
|
+
]
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Create and save models
|
|
111
|
+
pottery_class = PotteryClass.new(
|
|
112
|
+
class_id: "pottery_101",
|
|
113
|
+
studio: Studio.new(studio_key: "main_studio", name: "Main Studio"),
|
|
114
|
+
description: "Beginner pottery class"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
store.save(pottery_class)
|
|
118
|
+
|
|
119
|
+
# Fetch models
|
|
120
|
+
retrieved = store.fetch(model: PotteryClass, class_id: "pottery_101")
|
|
121
|
+
puts retrieved.studio.name # => "Main Studio"
|
|
122
|
+
|
|
123
|
+
# Update models
|
|
124
|
+
store.update(
|
|
125
|
+
model: PotteryClass,
|
|
126
|
+
class_id: "pottery_101",
|
|
127
|
+
attributes: [
|
|
128
|
+
{ key: :description, value: "Advanced pottery class" },
|
|
129
|
+
{ key: "studio.location", value: "Building A" }
|
|
130
|
+
]
|
|
131
|
+
)
|
|
132
|
+
----
|
|
133
|
+
|
|
134
|
+
== Architecture overview
|
|
135
|
+
|
|
136
|
+
Lutaml::Store implements a store-centric architecture where all persistence
|
|
137
|
+
operations flow through a central store that manages model registrations,
|
|
138
|
+
relationships, and storage backends.
|
|
139
|
+
|
|
140
|
+
=== Core components
|
|
141
|
+
|
|
142
|
+
The library is organized into these main components:
|
|
143
|
+
|
|
144
|
+
`Lutaml::Store`::
|
|
145
|
+
Main entry point providing the store-centric API with model registry support.
|
|
146
|
+
Handles model registration, polymorphic relationships, and composite model
|
|
147
|
+
management.
|
|
148
|
+
|
|
149
|
+
`Lutaml::Store::ModelRegistry`::
|
|
150
|
+
Manages registered models with their key fields and polymorphic configurations.
|
|
151
|
+
Validates model registrations and provides model lookup capabilities.
|
|
152
|
+
|
|
153
|
+
`Lutaml::Store::CompositeModelHandler`::
|
|
154
|
+
Handles relationships between registered models, storing composite models
|
|
155
|
+
independently while maintaining references and ensuring referential integrity.
|
|
156
|
+
|
|
157
|
+
`Lutaml::Store::AttributeUpdater`::
|
|
158
|
+
Processes model updates including dot notation for nested attributes, block-based
|
|
159
|
+
updates, and polymorphic model changes.
|
|
160
|
+
|
|
161
|
+
`Lutaml::Store::Format`::
|
|
162
|
+
Format-aware file I/O with handlers for YAML, YAMLS, JSON, JSONL, and Marshal.
|
|
163
|
+
Provides `load_all`, `save_all`, `import_all`, and `export` for batch persistence
|
|
164
|
+
with configurable layout strategies (separate, grouped, flat).
|
|
165
|
+
|
|
166
|
+
`Lutaml::Store::ModelSerializer`::
|
|
167
|
+
Delegates to custom serializers per model registration, or falls back to
|
|
168
|
+
`to_hash`/`from_hash` for standard `Lutaml::Model::Serializable` instances.
|
|
169
|
+
|
|
170
|
+
`Lutaml::Store::Store`::
|
|
171
|
+
Low-level storage interface providing unified key-value operations across all
|
|
172
|
+
backends with caching, events, and monitoring integration.
|
|
173
|
+
|
|
174
|
+
=== Storage backends
|
|
175
|
+
|
|
176
|
+
`Memory Backend`::
|
|
177
|
+
Fast in-memory storage ideal for caching, testing, and temporary data with
|
|
178
|
+
volatile persistence characteristics.
|
|
179
|
+
|
|
180
|
+
`FileSystem Backend`::
|
|
181
|
+
Persistent file-based storage with directory organization suitable for
|
|
182
|
+
moderate data volumes and development environments.
|
|
183
|
+
|
|
184
|
+
`SQLite Backend`::
|
|
185
|
+
Database storage with ACID compliance, transaction support, and durability
|
|
186
|
+
for production applications requiring data integrity.
|
|
187
|
+
|
|
188
|
+
== Model registry system
|
|
189
|
+
|
|
190
|
+
The model registry is the foundation of Lutaml::Store's sophisticated
|
|
191
|
+
persistence capabilities. It allows you to register models with their unique
|
|
192
|
+
key fields and configure polymorphic relationships.
|
|
193
|
+
|
|
194
|
+
=== Basic model registration
|
|
195
|
+
|
|
196
|
+
Register models by specifying the model class and its unique key field:
|
|
197
|
+
|
|
198
|
+
[source,ruby]
|
|
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
|
+
----
|
|
209
|
+
|
|
210
|
+
[example]
|
|
211
|
+
====
|
|
212
|
+
Model registration with validation:
|
|
213
|
+
|
|
214
|
+
[source,ruby]
|
|
215
|
+
----
|
|
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
|
+
store = Lutaml::Store.new(
|
|
224
|
+
adapter: :memory,
|
|
225
|
+
models: [
|
|
226
|
+
{ model: User, key: :user_id } # ✓ Valid - user_id exists
|
|
227
|
+
# { model: User, key: :invalid } # ✗ Error - invalid doesn't exist
|
|
228
|
+
]
|
|
229
|
+
)
|
|
230
|
+
----
|
|
231
|
+
====
|
|
232
|
+
|
|
233
|
+
=== Polymorphic model registration
|
|
234
|
+
|
|
235
|
+
For models with inheritance hierarchies, use polymorphic registration:
|
|
236
|
+
|
|
237
|
+
[source,ruby]
|
|
238
|
+
----
|
|
239
|
+
class Studio < Lutaml::Model::Serializable
|
|
240
|
+
attribute :studio_key, :string
|
|
241
|
+
attribute :name, :string
|
|
242
|
+
attribute :_class, :string, default: -> { "Studio" }, polymorphic_class: true
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
class CeramicStudio < Studio
|
|
246
|
+
attribute :clay_type, :string
|
|
247
|
+
attribute :_class, :string, default: -> { "CeramicStudio" }
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
store = Lutaml::Store.new(
|
|
251
|
+
adapter: :memory,
|
|
252
|
+
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
|
|
259
|
+
]
|
|
260
|
+
)
|
|
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
|
+
|
|
279
|
+
# Fetch returns correct subclass
|
|
280
|
+
retrieved = store.fetch(model: Studio, studio_key: "studio2")
|
|
281
|
+
puts retrieved.class.name # => "CeramicStudio"
|
|
282
|
+
puts retrieved.clay_type # => "Porcelain"
|
|
283
|
+
----
|
|
284
|
+
====
|
|
285
|
+
|
|
286
|
+
=== Composite model relationships
|
|
287
|
+
|
|
288
|
+
When registered models are nested within other registered models, they are
|
|
289
|
+
stored independently while maintaining references:
|
|
290
|
+
|
|
291
|
+
[source,ruby]
|
|
292
|
+
----
|
|
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
|
+
store = Lutaml::Store.new(
|
|
300
|
+
adapter: :memory,
|
|
301
|
+
models: [
|
|
302
|
+
{ model: PotteryClass, key: :class_id },
|
|
303
|
+
{ model: Studio, key: :studio_key }
|
|
304
|
+
]
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Both PotteryClass and its nested Studio are stored independently
|
|
308
|
+
pottery_class = PotteryClass.new(
|
|
309
|
+
class_id: "pottery_101",
|
|
310
|
+
studio: Studio.new(studio_key: "main_studio", name: "Main Studio"),
|
|
311
|
+
description: "Pottery class"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
store.save(pottery_class)
|
|
315
|
+
|
|
316
|
+
# Studio can be fetched independently
|
|
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"
|
|
323
|
+
----
|
|
324
|
+
|
|
325
|
+
== CRUD operations
|
|
326
|
+
|
|
327
|
+
Lutaml::Store provides database-style CRUD operations for registered models.
|
|
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:
|
|
378
|
+
|
|
379
|
+
[source,ruby]
|
|
380
|
+
----
|
|
381
|
+
# Fetch by key field name and value
|
|
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")
|
|
385
|
+
----
|
|
386
|
+
|
|
387
|
+
[example]
|
|
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
|
|
418
|
+
|
|
419
|
+
[source,ruby]
|
|
420
|
+
----
|
|
421
|
+
store.update(
|
|
422
|
+
model: User,
|
|
423
|
+
user_id: "user1",
|
|
424
|
+
attributes: [
|
|
425
|
+
{ key: :name, value: "John Smith" },
|
|
426
|
+
{ key: :email, value: "john.smith@example.com" }
|
|
427
|
+
]
|
|
428
|
+
)
|
|
429
|
+
----
|
|
430
|
+
|
|
431
|
+
==== Dot notation for nested updates
|
|
432
|
+
|
|
433
|
+
[source,ruby]
|
|
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"
|
|
478
|
+
end
|
|
479
|
+
----
|
|
480
|
+
|
|
481
|
+
==== Polymorphic model updates
|
|
482
|
+
|
|
483
|
+
Change model types by updating with different polymorphic instances:
|
|
484
|
+
|
|
485
|
+
[source,ruby]
|
|
486
|
+
----
|
|
487
|
+
# Change Studio to CeramicStudio
|
|
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"
|
|
507
|
+
----
|
|
508
|
+
|
|
509
|
+
=== Destroy operations
|
|
510
|
+
|
|
511
|
+
Delete models by their key fields:
|
|
512
|
+
|
|
513
|
+
[source,ruby]
|
|
514
|
+
----
|
|
515
|
+
# Delete single model
|
|
516
|
+
store.destroy(model: User, user_id: "user1")
|
|
517
|
+
|
|
518
|
+
# Delete model with composite relationships
|
|
519
|
+
store.destroy(model: PotteryClass, class_id: "pottery_101")
|
|
520
|
+
# Note: Nested Studio remains unless explicitly deleted
|
|
521
|
+
----
|
|
522
|
+
|
|
523
|
+
[example]
|
|
524
|
+
====
|
|
525
|
+
Managing composite model deletion:
|
|
526
|
+
|
|
527
|
+
[source,ruby]
|
|
528
|
+
----
|
|
529
|
+
# Delete pottery class but keep studio
|
|
530
|
+
store.destroy(model: PotteryClass, class_id: "pottery_101")
|
|
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:
|
|
550
|
+
|
|
551
|
+
[cols="1,1,1,1"]
|
|
552
|
+
|===
|
|
553
|
+
| Format | Symbol | Extension | Description
|
|
554
|
+
|
|
555
|
+
| YAML | `:yaml` | `.yaml` | Single-document YAML files
|
|
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
|
+
|===
|
|
561
|
+
|
|
562
|
+
All format handlers implement `serialize`, `deserialize`, `serialize_many`, and
|
|
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:
|
|
581
|
+
|
|
582
|
+
[source,ruby]
|
|
583
|
+
----
|
|
584
|
+
# Save with separate layout (one YAML file per model)
|
|
585
|
+
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
|
+
|
|
591
|
+
# Save as JSONL (one JSON object per line)
|
|
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
|
+
----
|
|
616
|
+
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
|
+
|
|
620
|
+
=== import_all: load and index for querying
|
|
621
|
+
|
|
622
|
+
Load models from a directory AND store them in the key-value backend, making
|
|
623
|
+
them available for `fetch`, `where`, `count`, and `exists?` queries:
|
|
624
|
+
|
|
625
|
+
[source,ruby]
|
|
626
|
+
----
|
|
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
|
+
|
|
638
|
+
Serialize models to a single output file:
|
|
639
|
+
|
|
640
|
+
[source,ruby]
|
|
641
|
+
----
|
|
642
|
+
all_concepts = store.all(model: Concept)
|
|
643
|
+
store.export(all_concepts, path: "output/concepts.yaml", format: :yaml)
|
|
644
|
+
----
|
|
645
|
+
|
|
646
|
+
=== Custom serializers
|
|
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
|
+
]
|
|
683
|
+
)
|
|
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
|
+
|
|
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
|
+
----
|
|
730
|
+
|
|
731
|
+
[example]
|
|
732
|
+
====
|
|
733
|
+
Polymorphic inheritance in action:
|
|
734
|
+
|
|
735
|
+
[source,ruby]
|
|
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
|
+
====
|
|
756
|
+
|
|
757
|
+
=== Composite model reference management
|
|
758
|
+
|
|
759
|
+
When registered models contain other registered models, Lutaml::Store manages
|
|
760
|
+
the relationships automatically:
|
|
761
|
+
|
|
762
|
+
[source,ruby]
|
|
763
|
+
----
|
|
764
|
+
class Order < Lutaml::Model::Serializable
|
|
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(
|
|
772
|
+
adapter: :memory,
|
|
773
|
+
models: [
|
|
774
|
+
{ model: Order, key: :order_id },
|
|
775
|
+
{ model: User, key: :user_id },
|
|
776
|
+
{ model: Product, key: :product_id }
|
|
777
|
+
]
|
|
778
|
+
)
|
|
779
|
+
----
|
|
780
|
+
|
|
781
|
+
[example]
|
|
782
|
+
====
|
|
783
|
+
Composite model relationships:
|
|
784
|
+
|
|
785
|
+
[source,ruby]
|
|
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
|
+
)
|
|
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
|
+
|
|
856
|
+
== Storage backends
|
|
857
|
+
|
|
858
|
+
Lutaml::Store supports multiple storage backends, each optimized for different
|
|
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:
|
|
942
|
+
|
|
943
|
+
[source,ruby]
|
|
944
|
+
----
|
|
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
|
+
)
|
|
954
|
+
----
|
|
955
|
+
|
|
956
|
+
**Characteristics:**
|
|
957
|
+
|
|
958
|
+
* ACID compliance with transaction support
|
|
959
|
+
|
|
960
|
+
* Excellent durability and data integrity
|
|
961
|
+
|
|
962
|
+
* Suitable for production applications
|
|
963
|
+
|
|
964
|
+
* SQL query capabilities (future enhancement)
|
|
965
|
+
|
|
966
|
+
[example]
|
|
967
|
+
====
|
|
968
|
+
SQLite backend with advanced options:
|
|
969
|
+
|
|
970
|
+
[source,ruby]
|
|
971
|
+
----
|
|
972
|
+
store = Lutaml::Store.new(
|
|
973
|
+
adapter: {
|
|
974
|
+
type: :sqlite,
|
|
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
|
+
]
|
|
986
|
+
)
|
|
987
|
+
----
|
|
988
|
+
====
|
|
989
|
+
|
|
990
|
+
== Configuration and customization
|
|
991
|
+
|
|
992
|
+
=== Programmatic configuration
|
|
993
|
+
|
|
994
|
+
Configure stores programmatically with full control over all options:
|
|
995
|
+
|
|
996
|
+
[source,ruby]
|
|
997
|
+
----
|
|
998
|
+
store = Lutaml::Store.new(
|
|
999
|
+
adapter: {
|
|
1000
|
+
type: :filesystem,
|
|
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
|
+
}
|
|
1023
|
+
)
|
|
1024
|
+
----
|
|
1025
|
+
|
|
1026
|
+
=== YAML configuration
|
|
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
|
+
====
|
|
1093
|
+
|
|
1094
|
+
== Event system
|
|
1095
|
+
|
|
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
|
+
[source,ruby]
|
|
1128
|
+
----
|
|
1129
|
+
# Register event listeners
|
|
1130
|
+
store.on(:model_save) do |event_data|
|
|
1131
|
+
puts "Saved #{event_data[:model].class.name} with key #{event_data[:key]}"
|
|
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
|
|
1141
|
+
----
|
|
1142
|
+
|
|
1143
|
+
[example]
|
|
1144
|
+
====
|
|
1145
|
+
Comprehensive event monitoring:
|
|
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
|
+
====
|
|
1266
|
+
|
|
1267
|
+
== Error handling
|
|
1268
|
+
|
|
1269
|
+
Lutaml::Store defines specific error types for different failure scenarios:
|
|
1270
|
+
|
|
1271
|
+
=== Error types
|
|
1272
|
+
|
|
1273
|
+
`Lutaml::Store::ModelNotRegisteredError`::
|
|
1274
|
+
Raised when attempting operations on unregistered models.
|
|
1275
|
+
|
|
1276
|
+
`Lutaml::Store::InvalidKeyError`::
|
|
1277
|
+
Raised when key field doesn't exist on model or key value is nil.
|
|
1278
|
+
|
|
1279
|
+
`Lutaml::Store::PolymorphicUpdateError`::
|
|
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
|
+
====
|
|
1334
|
+
|
|
1335
|
+
== Thread safety
|
|
1336
|
+
|
|
1337
|
+
All Lutaml::Store operations are thread-safe across all backends:
|
|
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
|
+
|
|
1390
|
+
|
|
1391
|
+
== Development
|
|
1392
|
+
|
|
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
|
+
[source,sh]
|
|
1405
|
+
----
|
|
1406
|
+
bundle exec rake install
|
|
1407
|
+
----
|
|
1408
|
+
|
|
1409
|
+
To release a new version, update the version number in `version.rb`, and then run:
|
|
1410
|
+
|
|
1411
|
+
[source,sh]
|
|
1412
|
+
----
|
|
1413
|
+
bundle exec rake release
|
|
1414
|
+
----
|
|
1415
|
+
|
|
1416
|
+
== Contributing
|
|
1417
|
+
|
|
1418
|
+
Bug reports and pull requests are welcome on GitHub at
|
|
1419
|
+
https://github.com/lutaml/lutaml-store.
|
|
1420
|
+
|
|
1421
|
+
This project is intended to be a safe, welcoming space for collaboration, and
|
|
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
|
|
1426
|
+
|
|
1427
|
+
This project is licensed under the MIT License.
|
|
1428
|
+
See the link:LICENSE[] file for details.
|
|
1429
|
+
|
|
1430
|
+
Copyright Ribose.
|