lutaml-store 0.1.1

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