lutaml-store 0.2.4 → 0.3.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +6 -0
  3. data/README.adoc +100 -3
  4. data/docs/astro.config.mjs +12 -0
  5. data/docs/package-lock.json +6098 -0
  6. data/docs/package.json +18 -0
  7. data/docs/public/favicon.svg +1 -0
  8. data/docs/public/lutaml-logo_logo-full-dark.svg +1 -0
  9. data/docs/public/lutaml-logo_logo-full-light.svg +1 -0
  10. data/docs/src/layouts/Base.astro +44 -0
  11. data/docs/src/layouts/Docs.astro +41 -0
  12. data/docs/src/pages/adapters.md +46 -0
  13. data/docs/src/pages/architecture.md +68 -0
  14. data/docs/src/pages/cloud-contract.md +48 -0
  15. data/docs/src/pages/formats.md +54 -0
  16. data/docs/src/pages/http-cache.md +43 -0
  17. data/docs/src/pages/index.md +82 -0
  18. data/docs/src/pages/quick-start.md +109 -0
  19. data/docs/src/pages/sources.md +83 -0
  20. data/docs/src/pages/stores.md +110 -0
  21. data/docs/src/styles/global.css +10 -0
  22. data/lib/lutaml/store/adapter/base.rb +14 -1
  23. data/lib/lutaml/store/adapter/filesystem.rb +283 -108
  24. data/lib/lutaml/store/adapter/memory.rb +16 -1
  25. data/lib/lutaml/store/adapter/sqlite.rb +26 -1
  26. data/lib/lutaml/store/basic_store.rb +11 -0
  27. data/lib/lutaml/store/cache_store.rb +104 -71
  28. data/lib/lutaml/store/config.rb +13 -1
  29. data/lib/lutaml/store/format.rb +13 -0
  30. data/lib/lutaml/store/manifest.rb +85 -0
  31. data/lib/lutaml/store/mirror.rb +94 -0
  32. data/lib/lutaml/store/repository.rb +178 -0
  33. data/lib/lutaml/store/source/base.rb +137 -0
  34. data/lib/lutaml/store/source/directory.rb +49 -0
  35. data/lib/lutaml/store/source/https.rb +109 -0
  36. data/lib/lutaml/store/source/rest.rb +61 -0
  37. data/lib/lutaml/store/source/zip.rb +52 -0
  38. data/lib/lutaml/store/source.rb +55 -0
  39. data/lib/lutaml/store/version.rb +1 -1
  40. data/lib/lutaml/store.rb +5 -0
  41. metadata +29 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6093f17ea0bca9240b608835a29b8f060a0c8c41be950e93fc0713e8c1b9444
4
- data.tar.gz: f0f9ee8ace22fa22ec62408425ba7fa479648c57158c92967bc8e235fd037285
3
+ metadata.gz: 4481af10b7c80db97bffbe2e1ad5119d615370596c46c88c84e536411d1ffdda
4
+ data.tar.gz: 618ad367561533d0aa576ef713dfb384173e34ec1ec0047e3cbab400af9c4dce
5
5
  SHA512:
6
- metadata.gz: 97a8aa5dc0d52617b1f4d97bd8378664eead01e299410d8dd506b775be23d69616008cde2e053b6a649819726048afdd2b227a455979630b2f07d04e76deed6d
7
- data.tar.gz: b80834cd232f87d378c0a3a5f7ea17ef2c7ea2d68103d890756eabb2f2c85e20e1b1f193ec64cd0198fdb4235c0ff68ca6de87e788b7aabaa26b49a081e2023e
6
+ metadata.gz: 2e22cb07d0781f6aba2c2ebc043322e5a756519284740b74661906b6f00cbce8536291967868b86fd2a1908de46e6c1d4450f35c517d4919f73020d65890dc63
7
+ data.tar.gz: 26f2ef6b87a85e2c020571319640ceed71955ace41c97c2052857b97bd8d68e599cf466cd349f31c802a0fda7172cd857ce1bfbed952076b7425aea07c7d2de0
data/CLAUDE.md CHANGED
@@ -49,6 +49,12 @@ All inherit from `Format::Base`. Six formats: `Yaml`, `Yamls`, `Json`, `Jsonl`,
49
49
 
50
50
  All inherit from `Adapter::Base`. Three backends: `Memory`, `FileSystem`, `SQLite`. Registered in `Adapter` module and resolved via `Adapter.resolve(:type, options)`. New adapters can be added with `Adapter.register(:custom, CustomClass)` without modifying existing code (OCP).
51
51
 
52
+ `Base#each_key` defaults to `keys.each`; `Base#update(key) { |old| new }` defaults to get + set inside `transaction`. Memory, FileSystem and Sqlite override `update` to be atomic. `FileSystem` percent-encodes file names (digest + key in `.meta` for long names), writes non-String values as JSON (`format: "json"` in `.meta`), and locks `<root>/.lock` (`with_lock(:ex/:sh)`, re-entrant per thread; a shared lock cannot be upgraded).
53
+
54
+ Inside `Lutaml::Store`, a bare `Monitor` resolves to `Lutaml::Store::Monitor` (the stats collector) — write `::Monitor` for Ruby's re-entrant lock.
55
+
56
+ Specs that load `sqlite3` must sort after `autoload_spec.rb` (see `sqlite_scale_spec.rb`). Run specs with a UTF-8 locale (`LANG=C.UTF-8`); the anti-pattern guard fails under US-ASCII.
57
+
52
58
  ### PackageStore and transports
53
59
 
54
60
  `PackageStore` provides structured multi-model persistence. `PackageDefinition` declares which models, assets, and metadata the package contains. Transports (`DirectoryTransport`, `ZipTransport`) handle reading/writing to disk. Format handlers determine serialization per model entry.
data/README.adoc CHANGED
@@ -17,8 +17,64 @@ It offers a unified interface for storing and retrieving complex model
17
17
  hierarchies, batch file I/O with multiple serialization formats, HTTP-aware
18
18
  caching, and package-based persistence with ZIP and directory transports.
19
19
 
20
+ It also provides **read-only Sources** for working with data repositories that
21
+ live outside your process — GitHub Pages, any static host, a REST API, a local
22
+ package directory, or a `.zip` distribution — through one facade that never
23
+ guesses which layer answered.
24
+
25
+ == Quick start: read a LutaML data repository
26
+
27
+ The same read API whether the data lives in the cloud, a local package, or a
28
+ downloaded `.zip` — backends are configured, never guessed:
29
+
30
+ [source,ruby]
31
+ ----
32
+ require "lutaml/store"
33
+
34
+ # From a cloud API (api.relaton.org is the reference implementation)
35
+ repo = Lutaml::Store::Repository.new(
36
+ source: Lutaml::Store::Source.for(:rest,
37
+ base_url: "https://api.relaton.org", collection: "ietf"),
38
+ cache: Lutaml::Store::Source.for(:directory, path: "~/.cache/relaton/ietf"),
39
+ )
40
+
41
+ # From a local GCR-style package — identical read API
42
+ repo = Lutaml::Store::Repository.new(
43
+ source: Lutaml::Store::Source.for(:directory, path: "~/gcr/relaton/ietf"),
44
+ )
45
+
46
+ # From a downloaded .zip distribution — same API
47
+ repo = Lutaml::Store::Repository.new(
48
+ source: Lutaml::Store::Source.for(:zip, path: "ietf.zip"),
49
+ )
50
+
51
+ # Read
52
+ repo.read("RFC 7231") # => bytes
53
+ repo.exist?("RFC 7231") # => true
54
+ repo.keys # => ["RFC 7231", ...]
55
+ repo.manifest # => Lutaml::Store::Manifest
56
+
57
+ # Parse into your model — the model class is declared, never inferred
58
+ repo.get("RFC 7231", MyModel)
59
+
60
+ # Search by metadata fields
61
+ repo.search(docid: "RFC 7231")
62
+
63
+ # Pull the whole collection into a local GCR-style package
64
+ repo.pull!(into: "~/gcr/relaton", collection: "ietf")
65
+
66
+ # Offline mode: never touch the source, serve from the local package only
67
+ offline = Lutaml::Store::Repository.new(source: repo.source, cache: repo.cache, mode: :offline)
68
+ offline.read("RFC 7231") # served from the cache, no network
69
+
70
+
71
+
20
72
  == Features
21
73
 
74
+ * **Repository** — the uniform facade: one read API over any source (cloud, local, zip), explicit cache, online/offline modes
75
+ * **Source** (read-only) — Directory, Zip, Https (ETag/304), Rest (the lutaml cloud store API); 404 = NotFoundError, transport = BackendError
76
+ * **Manifest** — the enumeration SSOT: keys, sha256 digests, opaque shard metadata
77
+ * **Mirror** — pull any source into a GCR-style package (incremental, digest-verified); pack to a distributable .zip
22
78
  * **DatabaseStore** — high-level CRUD with model registry, polymorphism, composites
23
79
  * **PackageStore** — structured multi-model packages with ZIP and directory transport
24
80
  * **BasicStore** — low-level key-value store with optional cache, events, monitoring
@@ -172,7 +228,21 @@ with optional caching, monitoring, and event emission.
172
228
  === Storage adapters
173
229
 
174
230
  All adapters inherit from `Adapter::Base` and provide `get`, `set`, `delete`,
175
- `exists?`, `keys`, `all`, `clear`, `size`, `each_key`, and bulk operations.
231
+ `exists?`, `keys`, `all`, `clear`, `size`, `each_key`, `update`, and bulk
232
+ operations. `each_key` defaults to `keys.each`, so a custom adapter only needs
233
+ `keys`.
234
+
235
+ `update(key) { |old| new }` is a read-modify-write: the block gets the current
236
+ value (`nil` when the key is missing) and its result is stored. It is atomic
237
+ across threads for Memory, and across threads and processes for FileSystem and
238
+ SQLite. `BasicStore#update` calls it and refreshes the read cache.
239
+
240
+ [source,ruby]
241
+ ----
242
+ store = Lutaml::Store::BasicStore.new(adapter_type: :filesystem,
243
+ adapter_options: { path: "./data" })
244
+ store.update("counter") { |n| (n.to_i + 1).to_s }
245
+ ----
176
246
 
177
247
  [cols="1,1,3"]
178
248
  |===
@@ -585,6 +655,21 @@ store = Lutaml::Store.new(
585
655
  Persistent file-based storage with SHA-256 integrity checks. Files organized
586
656
  by key in subdirectories.
587
657
 
658
+ * *File names.* Every byte outside `[A-Za-z0-9._-]` is percent-encoded, so
659
+ `ISO/19115` and `ISO:19115` get different files and `keys` returns the
660
+ original keys. A leading `.` is encoded too, so no file is hidden or outside
661
+ the root. A name longer than 200 bytes becomes `~` plus the SHA-256 of the
662
+ key; the `.meta` file beside it keeps the key. Files written by 0.3.0 (with
663
+ `_` in place of unsafe characters) are still read, and move to the new name
664
+ on the next write of that key.
665
+ * *Values.* A `String` is written as is. Any other value (for example the
666
+ Hash that `DatabaseStore` saves) is written as JSON and read back as JSON.
667
+ * *Locks.* Writes hold an exclusive `flock` on `<path>/.lock` and reads hold a
668
+ shared one. Data and `.meta` files are written to a unique temp file and
669
+ renamed into place, so readers never see a partial write.
670
+ * *Case-insensitive file systems* (the macOS default) can still merge keys that
671
+ differ only in letter case.
672
+
588
673
  === SQLite
589
674
 
590
675
  [source,ruby]
@@ -625,8 +710,20 @@ Error hierarchy:
625
710
 
626
711
  == Thread safety
627
712
 
628
- All adapters use mutex-based synchronization. Safe for concurrent use across
629
- threads.
713
+ * `Adapter::Memory`: writes are synchronized with a Monitor; reads use a
714
+ frozen snapshot.
715
+ * `Adapter::FileSystem`: safe across threads and processes that share one
716
+ directory (see the FileSystem section above). `transaction { ... }` holds the exclusive
717
+ lock for the whole block.
718
+ * `Adapter::Sqlite`: SQLite locking with a busy timeout; `update` uses
719
+ `BEGIN IMMEDIATE`.
720
+ * `CacheStore`: all operations run under one lock per instance. The TTL and
721
+ LRU bookkeeping is per process.
722
+ * `BasicStore`: the optional read cache is per process. Disable it
723
+ (`cache: { enabled: false }`) when other processes write the same store.
724
+
725
+ A `get` followed by a `set` is never atomic. Use `update` for a
726
+ read-modify-write.
630
727
 
631
728
  == Development
632
729
 
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "astro/config";
2
+ import vue from "@astrojs/vue";
3
+ import tailwindcss from "@tailwindcss/vite";
4
+
5
+ export default defineConfig({
6
+ site: "https://lutaml.github.io",
7
+ base: "/lutaml-store",
8
+ integrations: [vue()],
9
+ vite: {
10
+ plugins: [tailwindcss()],
11
+ },
12
+ });