lutaml-store 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +13 -177
  3. data/CLAUDE.md +22 -9
  4. data/README.adoc +326 -1103
  5. data/lib/lutaml/store/adapter/base.rb +4 -0
  6. data/lib/lutaml/store/adapter/memory.rb +8 -0
  7. data/lib/lutaml/store/adapter.rb +17 -0
  8. data/lib/lutaml/store/basic_store.rb +1 -10
  9. data/lib/lutaml/store/cache_store.rb +11 -16
  10. data/lib/lutaml/store/database_store.rb +19 -9
  11. data/lib/lutaml/store/format/base.rb +4 -0
  12. data/lib/lutaml/store/format/marshal_format.rb +4 -0
  13. data/lib/lutaml/store/format/xml.rb +37 -0
  14. data/lib/lutaml/store/format/yamls.rb +1 -1
  15. data/lib/lutaml/store/format.rb +22 -1
  16. data/lib/lutaml/store/format_serializer.rb +44 -0
  17. data/lib/lutaml/store/http_cache.rb +3 -13
  18. data/lib/lutaml/store/model_registration.rb +5 -2
  19. data/lib/lutaml/store/model_registry.rb +22 -20
  20. data/lib/lutaml/store/package_store.rb +2 -18
  21. data/lib/lutaml/store/package_transport/base.rb +60 -0
  22. data/lib/lutaml/store/package_transport/directory_transport.rb +196 -0
  23. data/lib/lutaml/store/package_transport/zip_transport.rb +183 -0
  24. data/lib/lutaml/store/package_transport.rb +11 -438
  25. data/lib/lutaml/store/storage_key.rb +1 -1
  26. data/lib/lutaml/store/version.rb +1 -1
  27. data/lib/lutaml/store.rb +1 -0
  28. metadata +14 -77
  29. data/.github/workflows/main.yml +0 -27
  30. data/.gitignore +0 -12
  31. data/CORRECTED_HTTP_CACHE_IMPLEMENTATION.md +0 -209
  32. data/CORRECTED_HTTP_CACHE_PLAN.md +0 -164
  33. data/Gemfile +0 -15
  34. data/Gemfile.lock +0 -227
  35. data/TODO.impl/0-lutaml-store-self-quality.md +0 -112
  36. data/TODO.impl/1-lutaml-hal-migration.md +0 -96
  37. data/TODO.impl/2-glossarist-migration.md +0 -359
  38. data/TODO.impl/3-lutaml-jsonschema-migration.md +0 -273
  39. data/bin/console +0 -11
  40. data/bin/setup +0 -8
  41. data/demo/Gemfile +0 -15
  42. data/demo/Gemfile.lock +0 -61
  43. data/demo/README.adoc +0 -301
  44. data/demo/data/vcards/co/contact_10_thompson.data +0 -1
  45. data/demo/data/vcards/co/contact_10_thompson.meta +0 -1
  46. data/demo/data/vcards/co/contact_1_doe.data +0 -1
  47. data/demo/data/vcards/co/contact_1_doe.meta +0 -1
  48. data/demo/data/vcards/co/contact_2_smith.data +0 -1
  49. data/demo/data/vcards/co/contact_2_smith.meta +0 -1
  50. data/demo/data/vcards/co/contact_3_johnson.data +0 -1
  51. data/demo/data/vcards/co/contact_3_johnson.meta +0 -1
  52. data/demo/data/vcards/co/contact_4_garcia.data +0 -1
  53. data/demo/data/vcards/co/contact_4_garcia.meta +0 -1
  54. data/demo/data/vcards/co/contact_5_wilson.data +0 -1
  55. data/demo/data/vcards/co/contact_5_wilson.meta +0 -1
  56. data/demo/data/vcards/co/contact_6_brown.data +0 -1
  57. data/demo/data/vcards/co/contact_6_brown.meta +0 -1
  58. data/demo/data/vcards/co/contact_7_davis.data +0 -1
  59. data/demo/data/vcards/co/contact_7_davis.meta +0 -1
  60. data/demo/data/vcards/co/contact_8_anderson.data +0 -1
  61. data/demo/data/vcards/co/contact_8_anderson.meta +0 -1
  62. data/demo/data/vcards/co/contact_9_taylor.data +0 -1
  63. data/demo/data/vcards/co/contact_9_taylor.meta +0 -1
  64. data/demo/data/vcards.db +0 -0
  65. data/demo/pottery_class_demo.rb +0 -164
  66. data/demo/vcard_models.rb +0 -140
  67. data/demo/vcard_store_demo.rb +0 -526
  68. data/lutaml-store.gemspec +0 -36
  69. data/plan.adoc +0 -606
  70. data/spec/lutaml/store/adapter_interface_spec.rb +0 -89
  71. data/spec/lutaml/store/anti_pattern_guard_spec.rb +0 -35
  72. data/spec/lutaml/store/anti_pattern_spec.rb +0 -78
  73. data/spec/lutaml/store/autoload_spec.rb +0 -34
  74. data/spec/lutaml/store/cache_store_spec.rb +0 -271
  75. data/spec/lutaml/store/compression_spec.rb +0 -78
  76. data/spec/lutaml/store/config_enhanced_spec.rb +0 -158
  77. data/spec/lutaml/store/corrected_http_cache_integration_spec.rb +0 -336
  78. data/spec/lutaml/store/custom_serializer_spec.rb +0 -108
  79. data/spec/lutaml/store/database_store_spec.rb +0 -279
  80. data/spec/lutaml/store/file_io_spec.rb +0 -220
  81. data/spec/lutaml/store/format/yamls_spec.rb +0 -80
  82. data/spec/lutaml/store/format_round_trip_spec.rb +0 -110
  83. data/spec/lutaml/store/format_spec.rb +0 -70
  84. data/spec/lutaml/store/http_cache_entry_spec.rb +0 -203
  85. data/spec/lutaml/store/http_cache_hal_integration_spec.rb +0 -404
  86. data/spec/lutaml/store/http_cache_spec.rb +0 -422
  87. data/spec/lutaml/store/http_header_processor_spec.rb +0 -290
  88. data/spec/lutaml/store/import_spec.rb +0 -90
  89. data/spec/lutaml/store/integrity_spec.rb +0 -157
  90. data/spec/lutaml/store/key_collision_serializer_spec.rb +0 -98
  91. data/spec/lutaml/store/load_save_spec.rb +0 -107
  92. data/spec/lutaml/store/lutaml_model_integration_spec.rb +0 -291
  93. data/spec/lutaml/store/model_serializer_spec.rb +0 -140
  94. data/spec/lutaml/store/package_definition_spec.rb +0 -89
  95. data/spec/lutaml/store/package_store_spec.rb +0 -153
  96. data/spec/lutaml/store/package_transport/directory_transport_spec.rb +0 -139
  97. data/spec/lutaml/store/package_transport/zip_transport_spec.rb +0 -85
  98. data/spec/lutaml/store/store_spec.rb +0 -182
  99. data/spec/lutaml/store_spec.rb +0 -21
  100. data/spec/spec_helper.rb +0 -16
  101. data/spec/support/simple_test_model.rb +0 -15
  102. 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,402 @@ 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:
268
+ === Save
350
269
 
351
270
  [source,ruby]
352
271
  ----
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")
272
+ store.save(User.new(user_id: "u1", name: "Ada"))
273
+ store.save([user1, user2, user3]) # bulk save
372
274
  ----
373
- ====
374
275
 
375
- === Fetch operations
376
-
377
- Retrieve models by their registered key fields:
276
+ === Fetch
378
277
 
379
278
  [source,ruby]
380
279
  ----
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")
280
+ user = store.fetch(model: User, user_id: "u1")
385
281
  ----
386
282
 
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
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
  )
429
- ----
430
295
 
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"
296
+ # Block-based
297
+ store.update(model: User, user_id: "u1") do |user|
298
+ user.name = "Grace"
478
299
  end
479
300
  ----
480
301
 
481
- ==== Polymorphic model updates
482
-
483
- Change model types by updating with different polymorphic instances:
302
+ === Destroy
484
303
 
485
304
  [source,ruby]
486
305
  ----
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"
306
+ store.destroy(model: User, user_id: "u1")
507
307
  ----
508
308
 
509
- === Destroy operations
309
+ == PackageStore
510
310
 
511
- Delete models by their key fields:
311
+ `PackageStore` provides structured multi-model persistence with directory and ZIP
312
+ transports. Use it when you need to bundle multiple model types, binary assets,
313
+ and metadata into a single loadable package.
512
314
 
513
- [source,ruby]
514
- ----
515
- # Delete single model
516
- store.destroy(model: User, user_id: "user1")
315
+ The workflow is: define the package schema with `PackageDefinition`, then load,
316
+ query, modify, and save using `PackageStore`.
517
317
 
518
- # Delete model with composite relationships
519
- store.destroy(model: PotteryClass, class_id: "pottery_101")
520
- # Note: Nested Studio remains unless explicitly deleted
521
- ----
318
+ === Define a package
522
319
 
523
- [example]
524
- ====
525
- Managing composite model deletion:
320
+ `PackageDefinition` declares the package structure — which models, assets, and
321
+ metadata the package contains:
526
322
 
527
323
  [source,ruby]
528
324
  ----
529
- # 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:
325
+ require "lutaml/model"
326
+ require "lutaml/store"
550
327
 
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
- |===
328
+ # Define your models
329
+ class Concept < Lutaml::Model::Serializable
330
+ attribute :term, :string
331
+ attribute :definition, :string
332
+ end
561
333
 
562
- All format handlers implement `serialize`, `deserialize`, `serialize_many`, and
563
- `deserialize_many`.
334
+ class Author < Lutaml::Model::Serializable
335
+ attribute :name, :string
336
+ attribute :email, :string
337
+ end
564
338
 
565
- === Layout strategies
339
+ class GlossaryInfo < Lutaml::Model::Serializable
340
+ attribute :title, :string
341
+ attribute :version, :string
342
+ end
566
343
 
567
- Three layout strategies control how files are organized on disk:
344
+ # Declare the package schema
345
+ glossary = Lutaml::Store::PackageDefinition.new(
346
+ name: "glossary",
347
+ metadata_model: GlossaryInfo,
348
+ metadata_file: "glossary.yaml",
349
+ metadata_key: :title
350
+ ) do |pkg|
351
+ # Models stored in subdirectories, one file per instance
352
+ pkg.model(model: Concept, key: :term, dir: "concepts", default_format: :yaml)
353
+ pkg.model(model: Author, key: :name, dir: "authors", default_format: :json)
354
+
355
+ # Binary assets bundled in the package
356
+ pkg.asset("logo.png", type: :file)
357
+ pkg.asset("attachments", type: :directory)
358
+ end
359
+ ----
568
360
 
569
- [cols="1,1,3"]
570
- |===
571
- | Layout | Symbol | Structure
361
+ Each `pkg.model` call registers a model type with:
572
362
 
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
- |===
363
+ * `model:` the `Lutaml::Model::Serializable` class
364
+ * `key:` attribute used as the unique identifier (becomes the filename)
365
+ * `dir:` subdirectory for this model type (use `file:` instead for a single-file model)
366
+ * `layout:` — `:separate` (one file per instance, default) or `:grouped` (multiple instances per file)
367
+ * `default_format:` — `:yaml`, `:json`, `:jsonl`, `:yamls`, or `:marshal`
577
368
 
578
- === save_all: batch write to directory
369
+ === Directory structure
579
370
 
580
- Write a collection of models to a directory:
371
+ When saved to disk, the glossary package produces:
581
372
 
582
- [source,ruby]
583
373
  ----
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)
374
+ my_glossary/
375
+ ├── glossary.yaml # metadata (GlossaryInfo)
376
+ ├── concepts/ # Concept model instances
377
+ │ ├── API.yaml
378
+ │ ├── REST.yaml
379
+ │ └── YAML.yaml
380
+ ├── authors/ # Author model instances
381
+ │ ├── john_doe.json
382
+ │ └── jane_smith.json
383
+ ├── logo.png # asset file
384
+ └── attachments/ # asset directory
385
+ └── spec.pdf
593
386
  ----
594
387
 
595
- The subdirectory name comes from the `dir` option in model registration:
388
+ === Load and query
596
389
 
597
390
  [source,ruby]
598
391
  ----
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
- ----
392
+ # Load from directory
393
+ store = Lutaml::Store::PackageStore.load(glossary, "./my_glossary")
609
394
 
610
- === load_all: batch read from directory
395
+ # Load from ZIP file
396
+ store = Lutaml::Store::PackageStore.load(glossary, "./glossary.zip", transport: :zip)
611
397
 
612
- Read models from a directory without storing them in the backend:
398
+ # Access metadata
399
+ store.metadata # => GlossaryInfo instance
613
400
 
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
- ----
401
+ # Query models
402
+ concepts = store.models_for(Concept) # => array of Concept instances
403
+ concept = store.fetch_model(Concept, "API")
404
+ store.model_count(Concept) # => 3
405
+ store.model_exists?(Concept, "REST") # => true
619
406
 
620
- === import_all: load and index for querying
407
+ # Access assets
408
+ store.asset("logo.png") # => binary content
409
+ store.asset_paths # => ["logo.png", "attachments/spec.pdf"]
621
410
 
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]
411
+ # Package statistics
412
+ store.stats
413
+ # => { package: "glossary", models: { "Concept" => 3, "Author" => 2 },
414
+ # assets: 2, metadata: true }
626
415
  ----
627
- # Import all concepts from directory
628
- loaded = store.import_all(Concept, path: "./data", format: :yaml, layout: :separate)
629
-
630
- # Now queryable via the store
631
- concept = store.fetch(model: Concept, uuid: "abc-123")
632
- matching = store.where(model: Concept, status: "valid")
633
- count = store.count(model: Concept)
634
- ----
635
-
636
- === export: write to a single file
637
416
 
638
- Serialize models to a single output file:
417
+ === Modify and save
639
418
 
640
419
  [source,ruby]
641
420
  ----
642
- all_concepts = store.all(model: Concept)
643
- store.export(all_concepts, path: "output/concepts.yaml", format: :yaml)
644
- ----
421
+ # Add models
422
+ store.add_model(Concept.new(term: "JSON", definition: "..."))
423
+ store.add_models([concept1, concept2])
645
424
 
646
- === Custom serializers
425
+ # Remove models
426
+ store.remove_model(Concept, "deprecated_term")
647
427
 
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:
428
+ # Add/remove assets
429
+ store.add_asset("diagram.svg", svg_content)
430
+ store.remove_asset("old_diagram.svg")
651
431
 
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
432
+ # Save to directory (default format per model)
433
+ store.save("./output")
672
434
 
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
- ----
435
+ # Save to ZIP with per-model format overrides
436
+ store.save("./glossary.zip", transport: :zip, formats: { Concept => :json })
685
437
 
686
- [example]
687
- ====
688
- Custom serializer with key collision workaround:
438
+ # Save with global format override
439
+ store.save("./output", format: :yaml)
689
440
 
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.
441
+ # Bulk operations
442
+ store.clear_models(Concept) # remove all Concept instances
443
+ store.clear_all # remove everything
695
444
  ----
696
- ====
697
445
 
698
- == Advanced features
446
+ === Per-model format override
699
447
 
700
- === Polymorphic inheritance handling
701
-
702
- Lutaml::Store automatically handles polymorphic inheritance chains, storing
703
- and retrieving the correct subclass instances:
448
+ The `save` method accepts format overrides:
704
449
 
705
450
  [source,ruby]
706
451
  ----
707
- 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
- ----
452
+ # Global format for all models
453
+ store.save("./out", format: :json)
730
454
 
731
- [example]
732
- ====
733
- Polymorphic inheritance in action:
455
+ # Per-model format
456
+ store.save("./out", formats: { Concept => :yaml, Author => :jsonl })
734
457
 
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
458
+ # Default: each model uses its default_format from the definition
459
+ store.save("./out")
754
460
  ----
755
- ====
756
461
 
757
- === Composite model reference management
462
+ === Package transports
758
463
 
759
- When registered models contain other registered models, Lutaml::Store manages
760
- the relationships automatically:
464
+ [cols="1,1,3"]
465
+ |===
466
+ | Transport | Symbol | Description
761
467
 
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
468
+ | `DirectoryTransport`
469
+ | `:directory`
470
+ | Filesystem directory with subdirectories per model type
770
471
 
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
- ----
472
+ | `ZipTransport`
473
+ | `:zip`
474
+ | ZIP archive containing the same directory structure
475
+ |===
780
476
 
781
- [example]
782
- ====
783
- Composite model relationships:
477
+ Transports are resolved via registry (`PackageTransport.resolve(:zip)`),
478
+ extensible without modifying existing code.
784
479
 
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
- )
480
+ == File I/O
797
481
 
798
- store.save(order)
482
+ DatabaseStore provides batch file I/O through `Format` handlers.
799
483
 
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")
484
+ === Format handlers
804
485
 
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
- ====
486
+ [cols="1,1,1,1"]
487
+ |===
488
+ | Format | Symbol | Extension | Description
811
489
 
812
- === Nested attribute updates with dot notation
490
+ | YAML | `:yaml` | `.yaml` | Single-document YAML files
491
+ | YAMLS | `:yamls` | `.yaml` | Multi-document YAML streams
492
+ | JSON | `:json` | `.json` | Single JSON objects
493
+ | JSONL | `:jsonl` | `.jsonl` | Line-delimited JSON
494
+ | Marshal | `:marshal` | `.bin` | Ruby Marshal binary format
495
+ |===
813
496
 
814
- Update deeply nested attributes using dot notation:
497
+ === save_all / load_all / import_all / export
815
498
 
816
499
  [source,ruby]
817
500
  ----
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:
501
+ # Write models to directory
502
+ store.save_all(concepts, path: "./data", format: :yaml, layout: :separate)
834
503
 
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
- )
504
+ # Read models (returns array, does NOT store in backend)
505
+ models = store.load_all(Concept, path: "./data", format: :yaml, layout: :separate)
846
506
 
847
- # 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)
507
+ # Read AND store in backend (makes them queryable)
508
+ store.import_all(Concept, path: "./data", format: :yaml, layout: :separate)
509
+ store.fetch(model: Concept, term: "API") # now available
850
510
 
851
- puts pottery_class.studio.name # => "New Studio Name"
852
- puts studio.name # => "New Studio Name" (same instance)
511
+ # Export to a single file
512
+ store.export(all_concepts, path: "output.yaml", format: :yaml)
853
513
  ----
854
- ====
855
514
 
856
- == Storage backends
515
+ Layout strategies: `:separate` (one file per model), `:grouped` (models grouped
516
+ by key), `:flat` (one file per model, no subdirectory).
857
517
 
858
- Lutaml::Store supports multiple storage backends, each optimized for different
859
- use cases and requirements.
518
+ == HTTP caching
860
519
 
861
- === Memory backend
862
-
863
- Fast in-memory storage ideal for testing, caching, and temporary data:
520
+ `HttpCache` provides HTTP-aware caching with ETags, conditional requests (304),
521
+ Cache-Control directives, and Vary header support. It uses a storage adapter
522
+ internally and serializes cache entries as `Lutaml::Model` objects via JSON.
864
523
 
865
524
  [source,ruby]
866
525
  ----
867
- store = Lutaml::Store.new(
868
- adapter: :memory,
869
- models: [
870
- { model: User, key: :user_id }
871
- ]
526
+ cache = Lutaml::Store::HttpCache.new(
527
+ adapter_type: "memory",
528
+ default_ttl: 3600,
529
+ respect_http_headers: true,
530
+ enable_conditional_requests: true
872
531
  )
873
- ----
874
-
875
- **Characteristics:**
876
532
 
877
- * 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:
533
+ # Fetch with automatic caching
534
+ response = cache.fetch("GET", "https://api.example.com/resource", {}) do |headers|
535
+ http_client.get("https://api.example.com/resource", headers)
536
+ end
888
537
 
889
- [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
- )
538
+ # Second call returns cached response (no HTTP request made)
539
+ cached = cache.fetch("GET", "https://api.example.com/resource", {}) { raise "shouldn't be called" }
901
540
  ----
902
541
 
903
- **Characteristics:**
542
+ Supports `no-store`, `no-cache`, `must-revalidate`, `max-age`, ETag-based
543
+ conditional requests, query parameter normalization, and filesystem/sqlite
544
+ adapters for persistent cache storage.
904
545
 
905
- * Persistent storage across application restarts
546
+ == CacheStore
906
547
 
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:
548
+ `CacheStore` extends `BasicStore` with TTL-aware caching and LRU eviction:
916
549
 
917
550
  [source,ruby]
918
551
  ----
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
- ]
552
+ cache = Lutaml::Store::CacheStore.new(
553
+ adapter: :memory,
554
+ max_size: 1000,
555
+ default_ttl: 3600
930
556
  )
931
557
 
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
558
+ cache.set("key1", "value1", ttl: 600)
559
+ cache.get("key1") # => "value1"
560
+ cache.fetch("key2", "default_value") # => "default_value" (stored)
561
+ cache.fetch("key3") { expensive_compute } # computes and caches
936
562
  ----
937
- ====
938
563
 
939
- === SQLite backend
564
+ == Storage backends
940
565
 
941
- Database storage with ACID compliance and transaction support:
566
+ === Memory
942
567
 
943
568
  [source,ruby]
944
569
  ----
945
- store = Lutaml::Store.new(
946
- adapter: {
947
- type: :sqlite,
948
- path: "./data/store.db"
949
- },
950
- models: [
951
- { model: User, key: :user_id }
952
- ]
953
- )
570
+ store = Lutaml::Store.new(adapter: :memory, models: [...])
954
571
  ----
955
572
 
956
- **Characteristics:**
957
-
958
- * ACID compliance with transaction support
959
-
960
- * Excellent durability and data integrity
573
+ Fast in-memory storage. Data lost on process exit. Thread-safe via mutex.
961
574
 
962
- * Suitable for production applications
963
-
964
- * SQL query capabilities (future enhancement)
965
-
966
- [example]
967
- ====
968
- SQLite backend with advanced options:
575
+ === FileSystem
969
576
 
970
577
  [source,ruby]
971
578
  ----
972
579
  store = Lutaml::Store.new(
973
- adapter: {
974
- 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
- ]
580
+ adapter: { type: :filesystem, path: "./data", extension: ".json" },
581
+ models: [...]
986
582
  )
987
583
  ----
988
- ====
989
584
 
990
- == Configuration and customization
585
+ Persistent file-based storage with SHA-256 integrity checks. Files organized
586
+ by key in subdirectories.
991
587
 
992
- === Programmatic configuration
993
-
994
- Configure stores programmatically with full control over all options:
588
+ === SQLite
995
589
 
996
590
  [source,ruby]
997
591
  ----
998
592
  store = Lutaml::Store.new(
999
- adapter: {
1000
- 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
- }
593
+ adapter: { type: :sqlite, path: "./store.db" },
594
+ models: [...]
1023
595
  )
1024
596
  ----
1025
597
 
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
- ====
598
+ ACID-compliant database storage. Requires `sqlite3` gem. Thread-safe with
599
+ connection pooling.
1093
600
 
1094
601
  == Event system
1095
602
 
1096
- Lutaml::Store provides a comprehensive event system for monitoring and reacting
1097
- to store operations.
1098
-
1099
- === Available events
1100
-
1101
- The store emits events for all major operations:
1102
-
1103
- * `:model_save` - When models are saved
1104
-
1105
- * `:model_fetch` - When models are fetched
1106
-
1107
- * `:model_update` - When models are updated
1108
-
1109
- * `:model_destroy` - When models are destroyed
1110
-
1111
- * `:model_save_all` - When models are batch-saved to directory
1112
-
1113
- * `:model_import` - When models are imported from directory into backend
1114
-
1115
- * `:model_export` - When models are exported to a single file
1116
-
1117
- * `:model_load_error` - When a file fails to load during load_all/import_all
1118
-
1119
- * `:composite_model_stored` - When composite models are stored independently
1120
-
1121
- * `:polymorphic_model_resolved` - When polymorphic models are resolved
1122
-
1123
- === Event listeners
1124
-
1125
- Register event listeners to react to store operations:
1126
-
1127
603
  [source,ruby]
1128
604
  ----
1129
- # 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
605
+ store.on(:model_save) { |data| logger.info("Saved #{data[:model].class}") }
606
+ store.on(:model_fetch) { |data| logger.debug("Fetched #{data[:key]}") }
607
+ store.on(:model_destroy) { |data| audit_log << data }
1141
608
  ----
1142
609
 
1143
- [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
- ====
610
+ Events: `:model_save`, `:model_fetch`, `:model_update`, `:model_destroy`,
611
+ `:model_save_all`, `:model_import`, `:model_export`, `:model_load_error`,
612
+ `:composite_model_stored`, `:polymorphic_model_resolved`.
1266
613
 
1267
614
  == Error handling
1268
615
 
1269
- Lutaml::Store defines specific error types for different failure scenarios:
1270
-
1271
- === Error types
616
+ Error hierarchy:
1272
617
 
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
- ====
618
+ * `Lutaml::Store::Error`
619
+ ** `ConfigurationError` invalid store or adapter config
620
+ ** `BackendError` — adapter-level failures
621
+ ** `ModelNotRegisteredError` — operations on unregistered models
622
+ ** `InvalidKeyError` missing or invalid key fields
623
+ ** `PolymorphicUpdateError` — polymorphic type conflicts
624
+ ** `CompositeModelError` — composite model handling failures
1334
625
 
1335
626
  == Thread safety
1336
627
 
1337
- All 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
-
628
+ All adapters use mutex-based synchronization. Safe for concurrent use across
629
+ threads.
1390
630
 
1391
631
  == Development
1392
632
 
1393
- After checking out the repo, run:
1394
-
1395
- [source,sh]
1396
- ----
1397
- bin/setup # Install dependencies
1398
- bundle exec rspec # Run tests
1399
- bundle exec rubocop # Run linting
1400
- ----
1401
-
1402
- To install this gem onto your local machine, run:
1403
-
1404
633
  [source,sh]
1405
634
  ----
1406
- bundle exec rake install
635
+ bin/setup # Install dependencies
636
+ bundle exec rake # Run specs + rubocop
637
+ bundle exec rspec # Run all specs
638
+ bundle exec rspec spec/lutaml/store/database_store_spec.rb # Single spec file
639
+ bundle exec rubocop # Lint
1407
640
  ----
1408
641
 
1409
- 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
- ----
642
+ Ruby >= 3.1 required.
1415
643
 
1416
644
  == Contributing
1417
645
 
1418
646
  Bug reports and pull requests are welcome on GitHub at
1419
647
  https://github.com/lutaml/lutaml-store.
1420
648
 
1421
- 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
649
+ == License
1426
650
 
1427
- This project is licensed under the MIT License.
1428
- See the link:LICENSE[] file for details.
651
+ BSD-2-Clause. See the link:LICENSE[] file for details.
1429
652
 
1430
653
  Copyright Ribose.