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.
- checksums.yaml +4 -4
- data/CLAUDE.md +6 -0
- data/README.adoc +100 -3
- data/docs/astro.config.mjs +12 -0
- data/docs/package-lock.json +6098 -0
- data/docs/package.json +18 -0
- data/docs/public/favicon.svg +1 -0
- data/docs/public/lutaml-logo_logo-full-dark.svg +1 -0
- data/docs/public/lutaml-logo_logo-full-light.svg +1 -0
- data/docs/src/layouts/Base.astro +44 -0
- data/docs/src/layouts/Docs.astro +41 -0
- data/docs/src/pages/adapters.md +46 -0
- data/docs/src/pages/architecture.md +68 -0
- data/docs/src/pages/cloud-contract.md +48 -0
- data/docs/src/pages/formats.md +54 -0
- data/docs/src/pages/http-cache.md +43 -0
- data/docs/src/pages/index.md +82 -0
- data/docs/src/pages/quick-start.md +109 -0
- data/docs/src/pages/sources.md +83 -0
- data/docs/src/pages/stores.md +110 -0
- data/docs/src/styles/global.css +10 -0
- data/lib/lutaml/store/adapter/base.rb +14 -1
- data/lib/lutaml/store/adapter/filesystem.rb +283 -108
- data/lib/lutaml/store/adapter/memory.rb +16 -1
- data/lib/lutaml/store/adapter/sqlite.rb +26 -1
- data/lib/lutaml/store/basic_store.rb +11 -0
- data/lib/lutaml/store/cache_store.rb +104 -71
- data/lib/lutaml/store/config.rb +13 -1
- data/lib/lutaml/store/format.rb +13 -0
- data/lib/lutaml/store/manifest.rb +85 -0
- data/lib/lutaml/store/mirror.rb +94 -0
- data/lib/lutaml/store/repository.rb +178 -0
- data/lib/lutaml/store/source/base.rb +137 -0
- data/lib/lutaml/store/source/directory.rb +49 -0
- data/lib/lutaml/store/source/https.rb +109 -0
- data/lib/lutaml/store/source/rest.rb +61 -0
- data/lib/lutaml/store/source/zip.rb +52 -0
- data/lib/lutaml/store/source.rb +55 -0
- data/lib/lutaml/store/version.rb +1 -1
- data/lib/lutaml/store.rb +5 -0
- metadata +29 -2
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Store
|
|
5
|
+
module Source
|
|
6
|
+
# Contract shared by every source.
|
|
7
|
+
#
|
|
8
|
+
# Subclasses implement {#read} (bytes for one key, raising
|
|
9
|
+
# NotFoundError for a definitive miss and BackendError for transport
|
|
10
|
+
# trouble) and {#manifest_raw} (the manifest document). Everything
|
|
11
|
+
# else — keys, existence, typed reads — composes those two.
|
|
12
|
+
class Base
|
|
13
|
+
OPTIONS = [].freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :options
|
|
16
|
+
|
|
17
|
+
def initialize(**options)
|
|
18
|
+
@options = options
|
|
19
|
+
missing = (self.class::OPTIONS - options.keys)
|
|
20
|
+
unless missing.empty?
|
|
21
|
+
raise ConfigurationError,
|
|
22
|
+
"#{self.class.name.split("::").last.downcase} source requires: " \
|
|
23
|
+
"#{missing.map(&:inspect).join(", ")}"
|
|
24
|
+
end
|
|
25
|
+
configure
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Bytes of one record. Raises NotFoundError (definitive) or
|
|
29
|
+
# BackendError (transport). Never returns nil.
|
|
30
|
+
#
|
|
31
|
+
# @param key [String]
|
|
32
|
+
# @return [String]
|
|
33
|
+
def read(_key)
|
|
34
|
+
raise NotImplementedError, "#{self.class}#read"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The manifest document as a string.
|
|
38
|
+
#
|
|
39
|
+
# @return [String]
|
|
40
|
+
def manifest_raw
|
|
41
|
+
raise NotImplementedError, "#{self.class}#manifest_raw"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The parsed manifest, memoized.
|
|
45
|
+
#
|
|
46
|
+
# @return [Manifest]
|
|
47
|
+
def manifest
|
|
48
|
+
@manifest ||= Manifest.parse(manifest_raw, format: manifest_format)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# All keys declared by the manifest. There is deliberately no
|
|
52
|
+
# fallback listing: an unenumerable source raises ConfigurationError
|
|
53
|
+
# instead of guessing.
|
|
54
|
+
#
|
|
55
|
+
# @return [Array<String>]
|
|
56
|
+
def keys
|
|
57
|
+
manifest.keys
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Manifest entry for one key.
|
|
61
|
+
#
|
|
62
|
+
# @param key [String]
|
|
63
|
+
# @return [Manifest::Entry, nil]
|
|
64
|
+
def entry_for(key)
|
|
65
|
+
manifest.entry_for(key)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def each_key(&block)
|
|
69
|
+
return to_enum(:each_key) unless block
|
|
70
|
+
|
|
71
|
+
keys.each(&block)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Existence via a full read. A HEAD-based optimization may be
|
|
75
|
+
# provided per source; the default is honest and uniform.
|
|
76
|
+
#
|
|
77
|
+
# @param key [String]
|
|
78
|
+
# @return [Boolean]
|
|
79
|
+
def exist?(key)
|
|
80
|
+
read(key)
|
|
81
|
+
true
|
|
82
|
+
rescue NotFoundError
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Deserialized record using the consumer's model class — the model
|
|
87
|
+
# is declared by the caller, never inferred.
|
|
88
|
+
#
|
|
89
|
+
# @param key [String]
|
|
90
|
+
# @param model_class [Class] a Lutaml::Model::Serializable subclass
|
|
91
|
+
# @return the model instance
|
|
92
|
+
def get(key, model_class)
|
|
93
|
+
format_for(key).deserialize(read(key), model_class)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Location of one key relative to the source. Manifest-declared
|
|
97
|
+
# locations win; otherwise the single-segment convention
|
|
98
|
+
# "entries/<percent-encoded key>" applies.
|
|
99
|
+
#
|
|
100
|
+
# @param key [String]
|
|
101
|
+
# @return [String]
|
|
102
|
+
def path_for(key)
|
|
103
|
+
entry = manifest.entry_for(key) if manifestable?
|
|
104
|
+
return entry.location if entry&.location
|
|
105
|
+
|
|
106
|
+
"entries/#{Source.encode_key(key)}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def manifestable?
|
|
112
|
+
manifest
|
|
113
|
+
true
|
|
114
|
+
rescue NotImplementedError, BackendError, NotFoundError
|
|
115
|
+
false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def manifest_format
|
|
119
|
+
@options[:manifest_format] || :json
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def format_for(key)
|
|
123
|
+
fmt = Format.for_extension(::File.extname(path_for(key).to_s))
|
|
124
|
+
fmt ||= Format.for_extension(::File.extname(key.to_s))
|
|
125
|
+
fmt ||= @options[:default_format] &&
|
|
126
|
+
Format.resolve(@options[:default_format])
|
|
127
|
+
fmt || raise(ConfigurationError,
|
|
128
|
+
"cannot determine the format of #{key.inspect}: pass default_format:")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def require_option(name)
|
|
132
|
+
@options[name] || raise(ConfigurationError, "#{name} is required")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Lutaml
|
|
7
|
+
module Store
|
|
8
|
+
module Source
|
|
9
|
+
# A local package directory: `manifest.json` + `entries/`. This is the
|
|
10
|
+
# layout {Mirror}#pull writes and what a downloaded official package
|
|
11
|
+
# (GCR-style) ships — one reader for caches, distributions and
|
|
12
|
+
# checkouts.
|
|
13
|
+
class Directory < Base
|
|
14
|
+
OPTIONS = %i[path].freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :package_root
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def configure
|
|
21
|
+
@path = require_option(:path)
|
|
22
|
+
@package_root = @path
|
|
23
|
+
# A read-only source never writes - not even directory creation.
|
|
24
|
+
# A missing path simply holds no entries and no manifest.
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def entry_path(key)
|
|
28
|
+
File.join(@path, path_for(key))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
public
|
|
32
|
+
|
|
33
|
+
def read(key)
|
|
34
|
+
File.binread(entry_path(key))
|
|
35
|
+
rescue Errno::ENOENT
|
|
36
|
+
raise NotFoundError, "no entry #{key.inspect} in #{@path}"
|
|
37
|
+
rescue SystemCallError => e
|
|
38
|
+
raise BackendError, "cannot read #{key.inspect} in #{@path}: #{e.message}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def manifest_raw
|
|
42
|
+
File.binread(File.join(@path, "manifest.json"))
|
|
43
|
+
rescue Errno::ENOENT
|
|
44
|
+
raise NotFoundError, "no manifest.json in #{@path}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module Lutaml
|
|
9
|
+
module Store
|
|
10
|
+
module Source
|
|
11
|
+
# Any static HTTP host — GitHub Pages, raw.githubusercontent, an
|
|
12
|
+
# object-storage bucket. The consumer states the base URL; the source
|
|
13
|
+
# appends one percent-encoded path segment per key.
|
|
14
|
+
#
|
|
15
|
+
# Every read may be wrapped in an explicit `cache:` (an
|
|
16
|
+
# HttpCacheConfig hash) so ETag/304 revalidation is handled by the
|
|
17
|
+
# store's HttpCache. With no cache configured, reads go straight to
|
|
18
|
+
# the network — nothing is implied.
|
|
19
|
+
#
|
|
20
|
+
# `transport:` accepts a callable for tests and alternative HTTP
|
|
21
|
+
# stacks: `->(uri, headers) { {status_code:, headers:, body:} }`.
|
|
22
|
+
class Https < Base
|
|
23
|
+
OPTIONS = %i[base_url].freeze
|
|
24
|
+
|
|
25
|
+
RETRIED_ERRORS = [
|
|
26
|
+
SocketError, Timeout::Error, IOError, SystemCallError,
|
|
27
|
+
OpenSSL::SSL::SSLError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
|
|
28
|
+
Net::ProtocolError
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def configure
|
|
34
|
+
base = require_option(:base_url)
|
|
35
|
+
@base = URI.parse(base.to_s)
|
|
36
|
+
raise ConfigurationError, "base_url must be http(s)" unless @base.is_a?(URI::HTTP) ||
|
|
37
|
+
@base.is_a?(URI::HTTPS)
|
|
38
|
+
|
|
39
|
+
@headers = @options.fetch(:headers, {})
|
|
40
|
+
@timeout = @options.fetch(:timeout, 60)
|
|
41
|
+
@open_timeout = @options.fetch(:open_timeout, 60)
|
|
42
|
+
@max_redirects = @options.fetch(:max_redirects, 3)
|
|
43
|
+
@transport = @options[:transport]
|
|
44
|
+
@cache = @options[:cache] && HttpCache.new(@options[:cache])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def fetch(uri, headers = {}, redirects = @max_redirects)
|
|
48
|
+
response = raw_fetch(uri, headers)
|
|
49
|
+
case response[:status_code]
|
|
50
|
+
when 200..299 then response
|
|
51
|
+
when 301, 302, 307, 308
|
|
52
|
+
raise BackendError, "too many redirects fetching #{uri}" if redirects.zero?
|
|
53
|
+
|
|
54
|
+
fetch(URI.parse(response[:headers]["location"]), headers, redirects - 1)
|
|
55
|
+
when 404 then raise NotFoundError, "no entry at #{uri}"
|
|
56
|
+
else raise BackendError, "HTTP #{response[:status_code]} fetching #{uri}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def raw_fetch(uri, headers)
|
|
61
|
+
if @transport
|
|
62
|
+
begin
|
|
63
|
+
return @transport.call(uri, headers)
|
|
64
|
+
rescue *RETRIED_ERRORS => e
|
|
65
|
+
raise BackendError, "cannot fetch #{uri}: #{e.class}: #{e.message}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
if @cache
|
|
70
|
+
return @cache.fetch(:get, uri.to_s, headers) do |h|
|
|
71
|
+
http_get(uri, h)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
http_get(uri, headers)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def http_get(uri, headers)
|
|
79
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
80
|
+
http.use_ssl = uri.is_a?(URI::HTTPS)
|
|
81
|
+
http.open_timeout = @open_timeout
|
|
82
|
+
http.read_timeout = @timeout
|
|
83
|
+
begin
|
|
84
|
+
resp = http.get(uri, @headers.merge(headers))
|
|
85
|
+
{ status_code: resp.code.to_i, headers: resp.each_header.to_h, body: resp.body }
|
|
86
|
+
rescue *RETRIED_ERRORS => e
|
|
87
|
+
raise BackendError, "cannot fetch #{uri}: #{e.class}: #{e.message}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def url_for(relative_path)
|
|
92
|
+
base = @base.to_s.sub(%r{/+\z}, "")
|
|
93
|
+
path = relative_path.sub(%r{\A/+}, "")
|
|
94
|
+
URI.parse("#{base}/#{path}")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
public
|
|
98
|
+
|
|
99
|
+
def read(key)
|
|
100
|
+
fetch(url_for(Source.encode_key(key)))[:body]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def manifest_raw
|
|
104
|
+
fetch(url_for(@options.fetch(:manifest_path, "manifest.json")))[:body]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Store
|
|
7
|
+
module Source
|
|
8
|
+
# A repository served by the lutaml cloud store API (the contract
|
|
9
|
+
# distilled from api.relaton.org):
|
|
10
|
+
#
|
|
11
|
+
# GET {base}/collections → collection list
|
|
12
|
+
# GET {base}/collections/{c}/manifest → Manifest
|
|
13
|
+
# GET {base}/collections/{c}/entries/{key} → record
|
|
14
|
+
#
|
|
15
|
+
# 404 is a definitive "no such key/collection" (NotFoundError); every
|
|
16
|
+
# other non-2xx is a BackendError. Path shapes are fixed here so
|
|
17
|
+
# server implementers have one contract — not per-client guesswork.
|
|
18
|
+
class Rest < Https
|
|
19
|
+
OPTIONS = %i[base_url collection].freeze
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def configure
|
|
24
|
+
super
|
|
25
|
+
@collection = require_option(:collection)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def collection_path(suffix)
|
|
29
|
+
"collections/#{Source.encode_key(@collection)}#{suffix}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
public
|
|
33
|
+
|
|
34
|
+
def read(key)
|
|
35
|
+
fetch(url_for(collection_path("/entries/#{Source.encode_key(key)}")))[:body]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def manifest_raw
|
|
39
|
+
fetch(url_for(collection_path("/manifest")))[:body]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# All collections the API publishes.
|
|
43
|
+
#
|
|
44
|
+
# @return [Array<Hash>]
|
|
45
|
+
def collections
|
|
46
|
+
JSON.parse(fetch(url_for("collections"))[:body])["collections"]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The keys of one declared shard, when the collection's manifest
|
|
50
|
+
# advertises sharding. Shard-number semantics belong to the domain;
|
|
51
|
+
# this only fetches the named part.
|
|
52
|
+
#
|
|
53
|
+
# @param number [Integer]
|
|
54
|
+
# @return [Array<String>]
|
|
55
|
+
def shard(number)
|
|
56
|
+
JSON.parse(fetch(url_for(collection_path("/shards/#{Integer(number)}")))[:body])["keys"]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zip"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Store
|
|
7
|
+
module Source
|
|
8
|
+
# A packaged repository read in place from a .zip — the shape of a
|
|
9
|
+
# downloaded official package. Same internal layout as
|
|
10
|
+
# {Directory}: manifest.json + entries/.
|
|
11
|
+
class Zip < Base
|
|
12
|
+
OPTIONS = %i[path].freeze
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def configure
|
|
17
|
+
@path = require_option(:path)
|
|
18
|
+
return if File.file?(@path)
|
|
19
|
+
|
|
20
|
+
raise ConfigurationError, "no such package file: #{@path}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def with_zip(&block)
|
|
24
|
+
::Zip::File.open(@path, &block)
|
|
25
|
+
rescue ::Zip::Error => e
|
|
26
|
+
raise BackendError, "cannot open package #{@path}: #{e.message}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
public
|
|
30
|
+
|
|
31
|
+
def read(key)
|
|
32
|
+
inner = path_for(key)
|
|
33
|
+
with_zip do |zip|
|
|
34
|
+
entry = zip.find_entry(inner)
|
|
35
|
+
raise NotFoundError, "no entry #{key.inspect} in #{::File.basename(@path)}" unless entry
|
|
36
|
+
|
|
37
|
+
entry.get_input_stream.read
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def manifest_raw
|
|
42
|
+
with_zip do |zip|
|
|
43
|
+
entry = zip.find_entry("manifest.json")
|
|
44
|
+
raise NotFoundError, "no manifest.json in #{::File.basename(@path)}" unless entry
|
|
45
|
+
|
|
46
|
+
entry.get_input_stream.read
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Store
|
|
5
|
+
# Read-only views over a LutaML data repository.
|
|
6
|
+
#
|
|
7
|
+
# A source answers four questions about a remote or local collection of
|
|
8
|
+
# LutaML records: does a key exist, what are its bytes, which keys exist,
|
|
9
|
+
# and what does the repository's manifest declare. Sources never write.
|
|
10
|
+
#
|
|
11
|
+
# Selection is explicit: `Source.for(type, options)` raises
|
|
12
|
+
# `ConfigurationError` for a missing option — there is no discovery,
|
|
13
|
+
# no environment guessing and no fallback between sources. Consumers
|
|
14
|
+
# compose sources with {Repository} for caching policies.
|
|
15
|
+
module Source
|
|
16
|
+
autoload :Base, "lutaml/store/source/base"
|
|
17
|
+
autoload :Directory, "lutaml/store/source/directory"
|
|
18
|
+
autoload :Zip, "lutaml/store/source/zip"
|
|
19
|
+
autoload :Https, "lutaml/store/source/https"
|
|
20
|
+
autoload :Rest, "lutaml/store/source/rest"
|
|
21
|
+
|
|
22
|
+
TYPES = {
|
|
23
|
+
directory: "Directory",
|
|
24
|
+
zip: "Zip",
|
|
25
|
+
https: "Https",
|
|
26
|
+
rest: "Rest"
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
# Builds an explicitly configured source.
|
|
31
|
+
#
|
|
32
|
+
# @param type [Symbol] one of {TYPES}
|
|
33
|
+
# @param options [Hash] passed to the source class
|
|
34
|
+
# @return [Source::Base]
|
|
35
|
+
def for(type, **options)
|
|
36
|
+
name = TYPES[type.to_sym]
|
|
37
|
+
raise ConfigurationError, "unknown source type: #{type.inspect}" unless name
|
|
38
|
+
|
|
39
|
+
Source.const_get(name).new(**options)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# A key is percent-encoded into exactly one path segment, so storage
|
|
43
|
+
# keys like "RFC 3986" or "ISO/IEC DIR 1" map to one file each on
|
|
44
|
+
# every platform. The manifest keeps the real key.
|
|
45
|
+
def encode_key(key)
|
|
46
|
+
CGI.escape(key.to_s)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def decode_key(segment)
|
|
50
|
+
CGI.unescape(segment.to_s)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/lutaml/store/version.rb
CHANGED
data/lib/lutaml/store.rb
CHANGED
|
@@ -5,6 +5,7 @@ module Lutaml
|
|
|
5
5
|
class Error < StandardError; end
|
|
6
6
|
class ConfigurationError < Error; end
|
|
7
7
|
class BackendError < Error; end
|
|
8
|
+
class NotFoundError < Error; end
|
|
8
9
|
class ModelNotRegisteredError < Error; end
|
|
9
10
|
class InvalidKeyError < Error; end
|
|
10
11
|
class PolymorphicUpdateError < Error; end
|
|
@@ -39,6 +40,10 @@ module Lutaml
|
|
|
39
40
|
autoload :HttpCacheConfig, "lutaml/store/http_cache_config"
|
|
40
41
|
autoload :HttpCacheEntry, "lutaml/store/http_cache_entry"
|
|
41
42
|
autoload :HttpHeaderProcessor, "lutaml/store/http_header_processor"
|
|
43
|
+
autoload :Manifest, "lutaml/store/manifest"
|
|
44
|
+
autoload :Mirror, "lutaml/store/mirror"
|
|
45
|
+
autoload :Repository, "lutaml/store/repository"
|
|
46
|
+
autoload :Source, "lutaml/store/source"
|
|
42
47
|
|
|
43
48
|
def self.new(adapter:, models: [], **options)
|
|
44
49
|
DatabaseStore.new(adapter: adapter, models: models, **options)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-store
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ronald Tse
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -61,6 +61,24 @@ files:
|
|
|
61
61
|
- CODE_OF_CONDUCT.md
|
|
62
62
|
- README.adoc
|
|
63
63
|
- Rakefile
|
|
64
|
+
- docs/astro.config.mjs
|
|
65
|
+
- docs/package-lock.json
|
|
66
|
+
- docs/package.json
|
|
67
|
+
- docs/public/favicon.svg
|
|
68
|
+
- docs/public/lutaml-logo_logo-full-dark.svg
|
|
69
|
+
- docs/public/lutaml-logo_logo-full-light.svg
|
|
70
|
+
- docs/src/layouts/Base.astro
|
|
71
|
+
- docs/src/layouts/Docs.astro
|
|
72
|
+
- docs/src/pages/adapters.md
|
|
73
|
+
- docs/src/pages/architecture.md
|
|
74
|
+
- docs/src/pages/cloud-contract.md
|
|
75
|
+
- docs/src/pages/formats.md
|
|
76
|
+
- docs/src/pages/http-cache.md
|
|
77
|
+
- docs/src/pages/index.md
|
|
78
|
+
- docs/src/pages/quick-start.md
|
|
79
|
+
- docs/src/pages/sources.md
|
|
80
|
+
- docs/src/pages/stores.md
|
|
81
|
+
- docs/src/styles/global.css
|
|
64
82
|
- lib/lutaml/store.rb
|
|
65
83
|
- lib/lutaml/store/adapter.rb
|
|
66
84
|
- lib/lutaml/store/adapter/base.rb
|
|
@@ -90,6 +108,8 @@ files:
|
|
|
90
108
|
- lib/lutaml/store/http_cache_entry.rb
|
|
91
109
|
- lib/lutaml/store/http_header_processor.rb
|
|
92
110
|
- lib/lutaml/store/integrity.rb
|
|
111
|
+
- lib/lutaml/store/manifest.rb
|
|
112
|
+
- lib/lutaml/store/mirror.rb
|
|
93
113
|
- lib/lutaml/store/model_registration.rb
|
|
94
114
|
- lib/lutaml/store/model_registry.rb
|
|
95
115
|
- lib/lutaml/store/model_serializer.rb
|
|
@@ -102,6 +122,13 @@ files:
|
|
|
102
122
|
- lib/lutaml/store/package_transport/zip_transport.rb
|
|
103
123
|
- lib/lutaml/store/predicate.rb
|
|
104
124
|
- lib/lutaml/store/query.rb
|
|
125
|
+
- lib/lutaml/store/repository.rb
|
|
126
|
+
- lib/lutaml/store/source.rb
|
|
127
|
+
- lib/lutaml/store/source/base.rb
|
|
128
|
+
- lib/lutaml/store/source/directory.rb
|
|
129
|
+
- lib/lutaml/store/source/https.rb
|
|
130
|
+
- lib/lutaml/store/source/rest.rb
|
|
131
|
+
- lib/lutaml/store/source/zip.rb
|
|
105
132
|
- lib/lutaml/store/storage_key.rb
|
|
106
133
|
- lib/lutaml/store/version.rb
|
|
107
134
|
- sig/lutaml/store.rbs
|