lutaml-store 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +11 -175
  3. data/README.adoc +233 -1124
  4. data/lib/lutaml/store/adapter/base.rb +4 -0
  5. data/lib/lutaml/store/adapter/memory.rb +8 -0
  6. data/lib/lutaml/store/cache_store.rb +9 -6
  7. data/lib/lutaml/store/format.rb +19 -0
  8. data/lib/lutaml/store/http_cache.rb +3 -13
  9. data/lib/lutaml/store/model_registration.rb +5 -2
  10. data/lib/lutaml/store/model_registry.rb +22 -20
  11. data/lib/lutaml/store/package_store.rb +2 -18
  12. data/lib/lutaml/store/package_transport/base.rb +48 -0
  13. data/lib/lutaml/store/package_transport/directory_transport.rb +196 -0
  14. data/lib/lutaml/store/package_transport/zip_transport.rb +178 -0
  15. data/lib/lutaml/store/package_transport.rb +11 -438
  16. data/lib/lutaml/store/version.rb +1 -1
  17. metadata +12 -77
  18. data/.github/workflows/main.yml +0 -27
  19. data/.gitignore +0 -12
  20. data/CORRECTED_HTTP_CACHE_IMPLEMENTATION.md +0 -209
  21. data/CORRECTED_HTTP_CACHE_PLAN.md +0 -164
  22. data/Gemfile +0 -15
  23. data/Gemfile.lock +0 -227
  24. data/TODO.impl/0-lutaml-store-self-quality.md +0 -112
  25. data/TODO.impl/1-lutaml-hal-migration.md +0 -96
  26. data/TODO.impl/2-glossarist-migration.md +0 -359
  27. data/TODO.impl/3-lutaml-jsonschema-migration.md +0 -273
  28. data/bin/console +0 -11
  29. data/bin/setup +0 -8
  30. data/demo/Gemfile +0 -15
  31. data/demo/Gemfile.lock +0 -61
  32. data/demo/README.adoc +0 -301
  33. data/demo/data/vcards/co/contact_10_thompson.data +0 -1
  34. data/demo/data/vcards/co/contact_10_thompson.meta +0 -1
  35. data/demo/data/vcards/co/contact_1_doe.data +0 -1
  36. data/demo/data/vcards/co/contact_1_doe.meta +0 -1
  37. data/demo/data/vcards/co/contact_2_smith.data +0 -1
  38. data/demo/data/vcards/co/contact_2_smith.meta +0 -1
  39. data/demo/data/vcards/co/contact_3_johnson.data +0 -1
  40. data/demo/data/vcards/co/contact_3_johnson.meta +0 -1
  41. data/demo/data/vcards/co/contact_4_garcia.data +0 -1
  42. data/demo/data/vcards/co/contact_4_garcia.meta +0 -1
  43. data/demo/data/vcards/co/contact_5_wilson.data +0 -1
  44. data/demo/data/vcards/co/contact_5_wilson.meta +0 -1
  45. data/demo/data/vcards/co/contact_6_brown.data +0 -1
  46. data/demo/data/vcards/co/contact_6_brown.meta +0 -1
  47. data/demo/data/vcards/co/contact_7_davis.data +0 -1
  48. data/demo/data/vcards/co/contact_7_davis.meta +0 -1
  49. data/demo/data/vcards/co/contact_8_anderson.data +0 -1
  50. data/demo/data/vcards/co/contact_8_anderson.meta +0 -1
  51. data/demo/data/vcards/co/contact_9_taylor.data +0 -1
  52. data/demo/data/vcards/co/contact_9_taylor.meta +0 -1
  53. data/demo/data/vcards.db +0 -0
  54. data/demo/pottery_class_demo.rb +0 -164
  55. data/demo/vcard_models.rb +0 -140
  56. data/demo/vcard_store_demo.rb +0 -526
  57. data/lutaml-store.gemspec +0 -36
  58. data/plan.adoc +0 -606
  59. data/spec/lutaml/store/adapter_interface_spec.rb +0 -89
  60. data/spec/lutaml/store/anti_pattern_guard_spec.rb +0 -35
  61. data/spec/lutaml/store/anti_pattern_spec.rb +0 -78
  62. data/spec/lutaml/store/autoload_spec.rb +0 -34
  63. data/spec/lutaml/store/cache_store_spec.rb +0 -271
  64. data/spec/lutaml/store/compression_spec.rb +0 -78
  65. data/spec/lutaml/store/config_enhanced_spec.rb +0 -158
  66. data/spec/lutaml/store/corrected_http_cache_integration_spec.rb +0 -336
  67. data/spec/lutaml/store/custom_serializer_spec.rb +0 -108
  68. data/spec/lutaml/store/database_store_spec.rb +0 -279
  69. data/spec/lutaml/store/file_io_spec.rb +0 -220
  70. data/spec/lutaml/store/format/yamls_spec.rb +0 -80
  71. data/spec/lutaml/store/format_round_trip_spec.rb +0 -110
  72. data/spec/lutaml/store/format_spec.rb +0 -70
  73. data/spec/lutaml/store/http_cache_entry_spec.rb +0 -203
  74. data/spec/lutaml/store/http_cache_hal_integration_spec.rb +0 -404
  75. data/spec/lutaml/store/http_cache_spec.rb +0 -422
  76. data/spec/lutaml/store/http_header_processor_spec.rb +0 -290
  77. data/spec/lutaml/store/import_spec.rb +0 -90
  78. data/spec/lutaml/store/integrity_spec.rb +0 -157
  79. data/spec/lutaml/store/key_collision_serializer_spec.rb +0 -98
  80. data/spec/lutaml/store/load_save_spec.rb +0 -107
  81. data/spec/lutaml/store/lutaml_model_integration_spec.rb +0 -291
  82. data/spec/lutaml/store/model_serializer_spec.rb +0 -140
  83. data/spec/lutaml/store/package_definition_spec.rb +0 -89
  84. data/spec/lutaml/store/package_store_spec.rb +0 -153
  85. data/spec/lutaml/store/package_transport/directory_transport_spec.rb +0 -139
  86. data/spec/lutaml/store/package_transport/zip_transport_spec.rb +0 -85
  87. data/spec/lutaml/store/store_spec.rb +0 -182
  88. data/spec/lutaml/store_spec.rb +0 -21
  89. data/spec/spec_helper.rb +0 -16
  90. data/spec/support/simple_test_model.rb +0 -15
  91. 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/workflow/status/lutaml/lutaml-store/test.yml?branch=main[Build Status]
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 sophisticated store-centric database-style API for
12
- LutaML Models with model registry, polymorphic support, and composite model
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 across different storage backends, making it ideal for applications
17
- that need sophisticated object persistence with database-like operations.
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
- * 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
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
- # Create and save models
111
- pottery_class = PotteryClass.new(
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
- store.save(pottery_class)
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
- # Update models
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 overview
115
+ == Architecture
135
116
 
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.
117
+ Lutaml::Store has two layers:
139
118
 
140
- === Core components
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
- The library is organized into these main components:
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
- `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.
129
+ === Key classes
148
130
 
149
- `Lutaml::Store::ModelRegistry`::
150
- Manages registered models with their key fields and polymorphic configurations.
151
- Validates model registrations and provides model lookup capabilities.
131
+ [cols="1,3"]
132
+ |===
133
+ | Class | Role
152
134
 
153
- `Lutaml::Store::CompositeModelHandler`::
154
- Handles relationships between registered models, storing composite models
155
- independently while maintaining references and ensuring referential integrity.
135
+ | `DatabaseStore`
136
+ | High-level CRUD with model registry, composites, polymorphism
156
137
 
157
- `Lutaml::Store::AttributeUpdater`::
158
- Processes model updates including dot notation for nested attributes, block-based
159
- updates, and polymorphic model changes.
138
+ | `PackageStore`
139
+ | Multi-model packages with directory/ZIP transport
160
140
 
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).
141
+ | `PackageDefinition`
142
+ | Declarative schema for package structure (models, assets, metadata)
165
143
 
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.
144
+ | `BasicStore`
145
+ | Low-level key-value store with optional cache/events/monitoring
169
146
 
170
- `Lutaml::Store::Store`::
171
- Low-level storage interface providing unified key-value operations across all
172
- backends with caching, events, and monitoring integration.
147
+ | `CacheStore`
148
+ | TTL-aware cache store extending BasicStore
173
149
 
174
- === Storage backends
150
+ | `HttpCache`
151
+ | HTTP-aware caching with ETags, conditional requests, Cache-Control
175
152
 
176
- `Memory Backend`::
177
- Fast in-memory storage ideal for caching, testing, and temporary data with
178
- volatile persistence characteristics.
153
+ | `ModelRegistry` / `ModelRegistration`
154
+ | Register models with key fields and polymorphic config
179
155
 
180
- `FileSystem Backend`::
181
- Persistent file-based storage with directory organization suitable for
182
- moderate data volumes and development environments.
156
+ | `CompositeModelHandler`
157
+ | Stores nested registered models independently, restores references
183
158
 
184
- `SQLite Backend`::
185
- Database storage with ACID compliance, transaction support, and durability
186
- for production applications requiring data integrity.
159
+ | `AttributeUpdater`
160
+ | Processes dot-notation paths and block-based updates
187
161
 
188
- == Model registry system
162
+ | `ModelSerializer`
163
+ | Serialization/deserialization with custom serializer support
189
164
 
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.
165
+ | `Format`
166
+ | Multi-format file I/O (YAML, JSON, JSONL, Marshal) with layout strategies
193
167
 
194
- === Basic model registration
168
+ | `Config`
169
+ | Parses and validates store configuration
170
+ |===
195
171
 
196
- Register models by specifying the model class and its unique key field:
172
+ === Storage adapters
197
173
 
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
- ----
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
- [example]
211
- ====
212
- Model registration with validation:
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 } # ✓ Valid - user_id exists
227
- # { model: User, key: :invalid } # ✗ Error - invalid doesn't exist
205
+ { model: User, key: :user_id },
206
+ { model: Post, key: :post_id }
228
207
  ]
229
208
  )
230
209
  ----
231
- ====
232
210
 
233
- === Polymorphic model registration
211
+ === Polymorphic models
234
212
 
235
- For models with inheritance hierarchies, use polymorphic registration:
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
- # Fetch returns correct subclass
280
- retrieved = store.fetch(model: Studio, studio_key: "studio2")
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 model relationships
240
+ === Composite models
287
241
 
288
- When registered models are nested within other registered models, they are
289
- stored independently while maintaining references:
242
+ When registered models are nested within other registered models, they are stored
243
+ independently while maintaining references:
290
244
 
291
245
  [source,ruby]
292
246
  ----
293
- class PotteryClass < Lutaml::Model::Serializable
294
- attribute :studio, Studio # Studio is also registered
295
- attribute :class_id, :string
296
- attribute :description, :string
297
- end
298
-
299
247
  store = Lutaml::Store.new(
300
248
  adapter: :memory,
301
249
  models: [
@@ -304,1127 +252,288 @@ store = Lutaml::Store.new(
304
252
  ]
305
253
  )
306
254
 
307
- # 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"
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
- 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"
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
- 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:
268
+ === Save
378
269
 
379
270
  [source,ruby]
380
271
  ----
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")
272
+ store.save(User.new(user_id: "u1", name: "Ada"))
273
+ store.save([user1, user2, user3]) # bulk save
385
274
  ----
386
275
 
387
- [example]
388
- ====
389
- Fetching polymorphic models:
276
+ === Fetch
390
277
 
391
278
  [source,ruby]
392
279
  ----
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"
280
+ user = store.fetch(model: User, user_id: "u1")
410
281
  ----
411
- ====
412
-
413
- === Update operations
414
282
 
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: "John Smith" },
426
- { key: :email, value: "john.smith@example.com" }
291
+ { key: :name, value: "Grace" },
292
+ { key: "studio.location", value: "Building A" } # dot notation for nesting
427
293
  ]
428
294
  )
295
+
296
+ # Block-based
297
+ store.update(model: User, user_id: "u1") do |user|
298
+ user.name = "Grace"
299
+ end
429
300
  ----
430
301
 
431
- ==== Dot notation for nested updates
302
+ === Destroy
432
303
 
433
304
  [source,ruby]
434
305
  ----
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
- )
306
+ store.destroy(model: User, user_id: "u1")
444
307
  ----
445
308
 
446
- [example]
447
- ====
448
- Complex nested updates:
309
+ == PackageStore
449
310
 
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
- ====
311
+ `PackageStore` provides structured multi-model persistence with directory and ZIP
312
+ transports. It is built on `PackageDefinition`, which declares the package schema
313
+ declaratively.
470
314
 
471
- ==== Block-based updates
315
+ === Define a package
472
316
 
473
317
  [source,ruby]
474
318
  ----
475
- store.update(model: User, user_id: "user1") do |user|
476
- user.name = "Updated Name"
477
- user.email = "updated@example.com"
319
+ glossary = Lutaml::Store::PackageDefinition.new(name: "glossary") do |pkg|
320
+ pkg.model(model: Concept, key: :term, dir: "concepts", default_format: :yaml)
321
+ pkg.model(model: Author, key: :name, dir: "authors", default_format: :json)
322
+ pkg.asset("glossary.yaml", type: :file)
323
+ pkg.metadata_model = GlossaryInfo
324
+ pkg.metadata_file = "glossary.yaml"
478
325
  end
479
326
  ----
480
327
 
481
- ==== Polymorphic model updates
482
-
483
- Change model types by updating with different polymorphic instances:
328
+ === Load and save packages
484
329
 
485
330
  [source,ruby]
486
331
  ----
487
- # 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
- ----
332
+ # Load from directory
333
+ store = Lutaml::Store::PackageStore.load(glossary, "./my_glossary", transport: :directory)
508
334
 
509
- === Destroy operations
335
+ # Load from ZIP
336
+ store = Lutaml::Store::PackageStore.load(glossary, "./glossary.zip", transport: :zip)
510
337
 
511
- Delete models by their key fields:
338
+ # Query models
339
+ concepts = store.models_for(Concept)
340
+ store.model_count(Concept) # => 42
341
+ store.fetch_model(Concept, "API")
512
342
 
513
- [source,ruby]
343
+ # Modify and save
344
+ store.add_model(Concept.new(term: "REST", definition: "..."))
345
+ store.save("./output", transport: :zip)
514
346
  ----
515
- # Delete single model
516
- store.destroy(model: User, user_id: "user1")
517
347
 
518
- # Delete model with composite relationships
519
- store.destroy(model: PotteryClass, class_id: "pottery_101")
520
- # Note: Nested Studio remains unless explicitly deleted
521
- ----
348
+ === Package transports
522
349
 
523
- [example]
524
- ====
525
- Managing composite model deletion:
350
+ [cols="1,1,3"]
351
+ |===
352
+ | Transport | Symbol | Description
526
353
 
527
- [source,ruby]
528
- ----
529
- # Delete pottery class but keep studio
530
- store.destroy(model: PotteryClass, class_id: "pottery_101")
354
+ | `DirectoryTransport`
355
+ | `:directory`
356
+ | Filesystem directory with subdirectories per model type
531
357
 
532
- # Studio still exists independently
533
- studio = store.fetch(model: Studio, studio_key: "main_studio")
534
- puts studio.name # => Still accessible
358
+ | `ZipTransport`
359
+ | `:zip`
360
+ | ZIP archive containing the same directory structure
361
+ |===
535
362
 
536
- # Delete studio separately if needed
537
- store.destroy(model: Studio, studio_key: "main_studio")
538
- ----
539
- ====
363
+ Transports are resolved via registry (`PackageTransport.resolve(:zip)`),
364
+ extensible without modifying existing code.
540
365
 
541
- == File I/O and format handling
366
+ == File I/O
542
367
 
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.
368
+ DatabaseStore provides batch file I/O through `Format` handlers.
546
369
 
547
370
  === Format handlers
548
371
 
549
- Five built-in format handlers serialize and deserialize `Lutaml::Model::Serializable` instances:
550
-
551
372
  [cols="1,1,1,1"]
552
373
  |===
553
374
  | Format | Symbol | Extension | Description
554
375
 
555
376
  | YAML | `:yaml` | `.yaml` | Single-document YAML files
556
- | YAMLS | `:yamls` | `.yaml` | Multi-document YAML streams (many models per file)
377
+ | YAMLS | `:yamls` | `.yaml` | Multi-document YAML streams
557
378
  | JSON | `:json` | `.json` | Single JSON objects
558
- | JSONL | `:jsonl` | `.jsonl` | Line-delimited JSON (one object per line)
379
+ | JSONL | `:jsonl` | `.jsonl` | Line-delimited JSON
559
380
  | Marshal | `:marshal` | `.bin` | Ruby Marshal binary format
560
381
  |===
561
382
 
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:
383
+ === save_all / load_all / import_all / export
581
384
 
582
385
  [source,ruby]
583
386
  ----
584
- # Save with separate layout (one YAML file per model)
387
+ # Write models to directory
585
388
  store.save_all(concepts, path: "./data", format: :yaml, layout: :separate)
586
- # Creates: ./data/concept/key1.yaml, ./data/concept/key2.yaml, ...
587
-
588
- # Save as grouped (all models in one multi-document YAML)
589
- store.save_all(concepts, path: "./data", format: :yamls, layout: :grouped)
590
389
 
591
- # 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
- ----
390
+ # Read models (returns array, does NOT store in backend)
616
391
  models = store.load_all(Concept, path: "./data", format: :yaml, layout: :separate)
617
- # Returns an array of Concept instances, does NOT store in backend
618
- ----
619
392
 
620
- === import_all: load and index for querying
393
+ # Read AND store in backend (makes them queryable)
394
+ store.import_all(Concept, path: "./data", format: :yaml, layout: :separate)
395
+ store.fetch(model: Concept, term: "API") # now available
621
396
 
622
- 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]
397
+ # Export to a single file
398
+ store.export(all_concepts, path: "output.yaml", format: :yaml)
626
399
  ----
627
- # Import all concepts from directory
628
- loaded = store.import_all(Concept, path: "./data", format: :yaml, layout: :separate)
629
400
 
630
- # 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
- ----
401
+ Layout strategies: `:separate` (one file per model), `:grouped` (models grouped
402
+ by key), `:flat` (one file per model, no subdirectory).
635
403
 
636
- === export: write to a single file
404
+ == HTTP caching
637
405
 
638
- Serialize models to a single output file:
406
+ `HttpCache` provides HTTP-aware caching with ETags, conditional requests (304),
407
+ Cache-Control directives, and Vary header support. It uses a storage adapter
408
+ internally and serializes cache entries as `Lutaml::Model` objects via JSON.
639
409
 
640
410
  [source,ruby]
641
411
  ----
642
- 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
- ]
412
+ cache = Lutaml::Store::HttpCache.new(
413
+ adapter_type: "memory",
414
+ default_ttl: 3600,
415
+ respect_http_headers: true,
416
+ enable_conditional_requests: true
683
417
  )
684
- ----
685
-
686
- [example]
687
- ====
688
- Custom serializer with key collision workaround:
689
-
690
- [source,ruby]
691
- ----
692
- # Without custom serializer: to_hash loses one of uuid/identifier
693
- # because both map to "id" key. The custom serializer stores the
694
- # full YAML string plus explicit metadata fields, preserving both.
695
- ----
696
- ====
697
-
698
- == Advanced features
699
-
700
- === Polymorphic inheritance handling
701
-
702
- Lutaml::Store automatically handles polymorphic inheritance chains, storing
703
- and retrieving the correct subclass instances:
704
-
705
- [source,ruby]
706
- ----
707
- class Vehicle < Lutaml::Model::Serializable
708
- attribute :vehicle_id, :string
709
- attribute :make, :string
710
- attribute :_type, :string, default: -> { "Vehicle" }, polymorphic_class: true
711
- end
712
418
 
713
- class Car < Vehicle
714
- attribute :doors, :integer
715
- attribute :_type, :string, default: -> { "Car" }
419
+ # Fetch with automatic caching
420
+ response = cache.fetch("GET", "https://api.example.com/resource", {}) do |headers|
421
+ http_client.get("https://api.example.com/resource", headers)
716
422
  end
717
423
 
718
- 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
- )
424
+ # Second call returns cached response (no HTTP request made)
425
+ cached = cache.fetch("GET", "https://api.example.com/resource", {}) { raise "shouldn't be called" }
729
426
  ----
730
427
 
731
- [example]
732
- ====
733
- Polymorphic inheritance in action:
428
+ Supports `no-store`, `no-cache`, `must-revalidate`, `max-age`, ETag-based
429
+ conditional requests, query parameter normalization, and filesystem/sqlite
430
+ adapters for persistent cache storage.
734
431
 
735
- [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
- ====
432
+ == CacheStore
756
433
 
757
- === Composite model reference management
758
-
759
- When registered models contain other registered models, Lutaml::Store manages
760
- the relationships automatically:
434
+ `CacheStore` extends `BasicStore` with TTL-aware caching and LRU eviction:
761
435
 
762
436
  [source,ruby]
763
437
  ----
764
- 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(
438
+ cache = Lutaml::Store::CacheStore.new(
772
439
  adapter: :memory,
773
- models: [
774
- { model: Order, key: :order_id },
775
- { model: User, key: :user_id },
776
- { model: Product, key: :product_id }
777
- ]
440
+ max_size: 1000,
441
+ default_ttl: 3600
778
442
  )
779
- ----
780
-
781
- [example]
782
- ====
783
- Composite model relationships:
784
443
 
785
- [source,ruby]
444
+ cache.set("key1", "value1", ttl: 600)
445
+ cache.get("key1") # => "value1"
446
+ cache.fetch("key2", "default_value") # => "default_value" (stored)
447
+ cache.fetch("key3") { expensive_compute } # computes and caches
786
448
  ----
787
- # Create order with nested registered models
788
- order = Order.new(
789
- order_id: "order1",
790
- customer: User.new(user_id: "user1", name: "John Doe"),
791
- items: [
792
- Product.new(product_id: "prod1", name: "Widget", price: 10.00),
793
- Product.new(product_id: "prod2", name: "Gadget", price: 15.00)
794
- ],
795
- total: 25.00
796
- )
797
-
798
- store.save(order)
799
-
800
- # All models are stored independently
801
- customer = store.fetch(model: User, user_id: "user1")
802
- product1 = store.fetch(model: Product, product_id: "prod1")
803
- product2 = store.fetch(model: Product, product_id: "prod2")
804
-
805
- # Order maintains references to all nested models
806
- retrieved_order = store.fetch(model: Order, order_id: "order1")
807
- puts retrieved_order.customer.name # => "John Doe"
808
- puts retrieved_order.items.first.name # => "Widget"
809
- ----
810
- ====
811
-
812
- === Nested attribute updates with dot notation
813
-
814
- Update deeply nested attributes using dot notation:
815
-
816
- [source,ruby]
817
- ----
818
- # Update nested attributes
819
- store.update(
820
- model: Order,
821
- order_id: "order1",
822
- attributes: [
823
- { key: "customer.name", value: "Jane Doe" },
824
- { key: "customer.email", value: "jane@example.com" },
825
- { key: "items.0.price", value: 12.00 }, # Update first item price
826
- { key: :total, value: 27.00 }
827
- ]
828
- )
829
- ----
830
-
831
- [example]
832
- ====
833
- Complex nested updates:
834
-
835
- [source,ruby]
836
- ----
837
- # Multi-level nested updates
838
- store.update(
839
- model: PotteryClass,
840
- class_id: "pottery_101",
841
- attributes: [
842
- { key: "studio.name", value: "New Studio Name" },
843
- { key: "studio.location", value: "New Location" }
844
- ]
845
- )
846
-
847
- # Updates are reflected in both the parent and the independently stored model
848
- pottery_class = store.fetch(model: PotteryClass, class_id: "pottery_101")
849
- studio = store.fetch(model: Studio, studio_key: pottery_class.studio.studio_key)
850
-
851
- puts pottery_class.studio.name # => "New Studio Name"
852
- puts studio.name # => "New Studio Name" (same instance)
853
- ----
854
- ====
855
449
 
856
450
  == Storage backends
857
451
 
858
- 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:
452
+ === Memory
942
453
 
943
454
  [source,ruby]
944
455
  ----
945
- store = Lutaml::Store.new(
946
- adapter: {
947
- type: :sqlite,
948
- path: "./data/store.db"
949
- },
950
- models: [
951
- { model: User, key: :user_id }
952
- ]
953
- )
456
+ store = Lutaml::Store.new(adapter: :memory, models: [...])
954
457
  ----
955
458
 
956
- **Characteristics:**
957
-
958
- * ACID compliance with transaction support
959
-
960
- * Excellent durability and data integrity
459
+ Fast in-memory storage. Data lost on process exit. Thread-safe via mutex.
961
460
 
962
- * Suitable for production applications
963
-
964
- * SQL query capabilities (future enhancement)
965
-
966
- [example]
967
- ====
968
- SQLite backend with advanced options:
461
+ === FileSystem
969
462
 
970
463
  [source,ruby]
971
464
  ----
972
465
  store = Lutaml::Store.new(
973
- adapter: {
974
- 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
- ]
466
+ adapter: { type: :filesystem, path: "./data", extension: ".json" },
467
+ models: [...]
986
468
  )
987
469
  ----
988
- ====
989
470
 
990
- == Configuration and customization
471
+ Persistent file-based storage with SHA-256 integrity checks. Files organized
472
+ by key in subdirectories.
991
473
 
992
- === Programmatic configuration
993
-
994
- Configure stores programmatically with full control over all options:
474
+ === SQLite
995
475
 
996
476
  [source,ruby]
997
477
  ----
998
478
  store = Lutaml::Store.new(
999
- adapter: {
1000
- 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
- }
479
+ adapter: { type: :sqlite, path: "./store.db" },
480
+ models: [...]
1023
481
  )
1024
482
  ----
1025
483
 
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
- ====
484
+ ACID-compliant database storage. Requires `sqlite3` gem. Thread-safe with
485
+ connection pooling.
1093
486
 
1094
487
  == Event system
1095
488
 
1096
- Lutaml::Store provides a comprehensive event system for monitoring and reacting
1097
- to store operations.
1098
-
1099
- === Available events
1100
-
1101
- The store emits events for all major operations:
1102
-
1103
- * `:model_save` - When models are saved
1104
-
1105
- * `:model_fetch` - When models are fetched
1106
-
1107
- * `:model_update` - When models are updated
1108
-
1109
- * `:model_destroy` - When models are destroyed
1110
-
1111
- * `:model_save_all` - When models are batch-saved to directory
1112
-
1113
- * `:model_import` - When models are imported from directory into backend
1114
-
1115
- * `:model_export` - When models are exported to a single file
1116
-
1117
- * `:model_load_error` - When a file fails to load during load_all/import_all
1118
-
1119
- * `:composite_model_stored` - When composite models are stored independently
1120
-
1121
- * `:polymorphic_model_resolved` - When polymorphic models are resolved
1122
-
1123
- === Event listeners
1124
-
1125
- Register event listeners to react to store operations:
1126
-
1127
489
  [source,ruby]
1128
490
  ----
1129
- # 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
491
+ store.on(:model_save) { |data| logger.info("Saved #{data[:model].class}") }
492
+ store.on(:model_fetch) { |data| logger.debug("Fetched #{data[:key]}") }
493
+ store.on(:model_destroy) { |data| audit_log << data }
1141
494
  ----
1142
495
 
1143
- [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
- ====
496
+ Events: `:model_save`, `:model_fetch`, `:model_update`, `:model_destroy`,
497
+ `:model_save_all`, `:model_import`, `:model_export`, `:model_load_error`,
498
+ `:composite_model_stored`, `:polymorphic_model_resolved`.
1266
499
 
1267
500
  == Error handling
1268
501
 
1269
- Lutaml::Store defines specific error types for different failure scenarios:
1270
-
1271
- === Error types
502
+ Error hierarchy:
1272
503
 
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
- ====
504
+ * `Lutaml::Store::Error`
505
+ ** `ConfigurationError` invalid store or adapter config
506
+ ** `BackendError` — adapter-level failures
507
+ ** `ModelNotRegisteredError` — operations on unregistered models
508
+ ** `InvalidKeyError` missing or invalid key fields
509
+ ** `PolymorphicUpdateError` — polymorphic type conflicts
510
+ ** `CompositeModelError` — composite model handling failures
1334
511
 
1335
512
  == Thread safety
1336
513
 
1337
- All 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
-
514
+ All adapters use mutex-based synchronization. Safe for concurrent use across
515
+ threads.
1390
516
 
1391
517
  == Development
1392
518
 
1393
- After checking out the repo, run:
1394
-
1395
- [source,sh]
1396
- ----
1397
- bin/setup # Install dependencies
1398
- bundle exec rspec # Run tests
1399
- bundle exec rubocop # Run linting
1400
- ----
1401
-
1402
- To install this gem onto your local machine, run:
1403
-
1404
519
  [source,sh]
1405
520
  ----
1406
- bundle exec rake install
521
+ bin/setup # Install dependencies
522
+ bundle exec rake # Run specs + rubocop
523
+ bundle exec rspec # Run all specs
524
+ bundle exec rspec spec/lutaml/store/database_store_spec.rb # Single spec file
525
+ bundle exec rubocop # Lint
1407
526
  ----
1408
527
 
1409
- 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
- ----
528
+ Ruby >= 3.1 required.
1415
529
 
1416
530
  == Contributing
1417
531
 
1418
532
  Bug reports and pull requests are welcome on GitHub at
1419
533
  https://github.com/lutaml/lutaml-store.
1420
534
 
1421
- 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
535
+ == License
1426
536
 
1427
- This project is licensed under the MIT License.
1428
- See the link:LICENSE[] file for details.
537
+ BSD-2-Clause. See the link:LICENSE[] file for details.
1429
538
 
1430
539
  Copyright Ribose.