textus 0.15.0 → 0.18.0
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/ARCHITECTURE.md +14 -14
- data/CHANGELOG.md +313 -0
- data/README.md +13 -9
- data/SPEC.md +13 -10
- data/docs/conventions.md +2 -2
- data/lib/textus/application/context.rb +24 -0
- data/lib/textus/application/reads/audit.rb +1 -1
- data/lib/textus/application/reads/blame.rb +3 -1
- data/lib/textus/application/reads/deps.rb +1 -1
- data/lib/textus/application/reads/freshness.rb +12 -3
- data/lib/textus/application/reads/get.rb +32 -8
- data/lib/textus/application/reads/get_or_refresh.rb +5 -5
- data/lib/textus/application/reads/list.rb +3 -1
- data/lib/textus/application/reads/published.rb +1 -1
- data/lib/textus/application/reads/rdeps.rb +1 -1
- data/lib/textus/application/reads/schema_envelope.rb +3 -1
- data/lib/textus/application/reads/stale.rb +1 -1
- data/lib/textus/application/reads/uid.rb +1 -1
- data/lib/textus/application/reads/validate_all.rb +6 -1
- data/lib/textus/application/reads/validator.rb +84 -0
- data/lib/textus/application/reads/where.rb +4 -1
- data/lib/textus/application/refresh/all.rb +8 -1
- data/lib/textus/application/refresh/orchestrator.rb +2 -3
- data/lib/textus/application/refresh/worker.rb +18 -15
- data/lib/textus/application/writes/accept.rb +12 -12
- data/lib/textus/application/writes/build.rb +3 -4
- data/lib/textus/application/writes/delete.rb +10 -15
- data/lib/textus/application/writes/envelope_io.rb +106 -0
- data/lib/textus/application/writes/mv.rb +25 -27
- data/lib/textus/application/writes/publish.rb +8 -9
- data/lib/textus/application/writes/put.rb +12 -16
- data/lib/textus/application/writes/reject.rb +10 -10
- data/lib/textus/builder/pipeline.rb +2 -2
- data/lib/textus/cli/group/hook.rb +1 -3
- data/lib/textus/cli/group/key.rb +1 -4
- data/lib/textus/cli/group/refresh.rb +1 -2
- data/lib/textus/cli/group/rule.rb +1 -3
- data/lib/textus/cli/group/schema.rb +1 -5
- data/lib/textus/cli/group.rb +12 -16
- data/lib/textus/cli/verb/accept.rb +3 -1
- data/lib/textus/cli/verb/audit.rb +3 -1
- data/lib/textus/cli/verb/blame.rb +3 -1
- data/lib/textus/cli/verb/build.rb +4 -2
- data/lib/textus/cli/verb/delete.rb +3 -1
- data/lib/textus/cli/verb/deps.rb +3 -1
- data/lib/textus/cli/verb/doctor.rb +2 -0
- data/lib/textus/cli/verb/freshness.rb +3 -1
- data/lib/textus/cli/verb/get.rb +3 -1
- data/lib/textus/cli/verb/hook_run.rb +3 -0
- data/lib/textus/cli/verb/hooks.rb +3 -0
- data/lib/textus/cli/verb/init.rb +2 -0
- data/lib/textus/cli/verb/intro.rb +2 -0
- data/lib/textus/cli/verb/key_normalize.rb +3 -0
- data/lib/textus/cli/verb/list.rb +3 -1
- data/lib/textus/cli/verb/mv.rb +4 -1
- data/lib/textus/cli/verb/published.rb +3 -1
- data/lib/textus/cli/verb/put.rb +3 -1
- data/lib/textus/cli/verb/rdeps.rb +3 -1
- data/lib/textus/cli/verb/refresh.rb +1 -1
- data/lib/textus/cli/verb/refresh_stale.rb +3 -0
- data/lib/textus/cli/verb/reject.rb +3 -1
- data/lib/textus/cli/verb/rule_explain.rb +4 -1
- data/lib/textus/cli/verb/rule_list.rb +3 -0
- data/lib/textus/cli/verb/schema.rb +4 -1
- data/lib/textus/cli/verb/schema_diff.rb +3 -0
- data/lib/textus/cli/verb/schema_init.rb +3 -0
- data/lib/textus/cli/verb/schema_migrate.rb +3 -0
- data/lib/textus/cli/verb/uid.rb +4 -1
- data/lib/textus/cli/verb/where.rb +3 -1
- data/lib/textus/cli/verb.rb +30 -0
- data/lib/textus/cli.rb +18 -27
- data/lib/textus/doctor/check/audit_log.rb +1 -1
- data/lib/textus/doctor/check/hooks.rb +3 -1
- data/lib/textus/doctor/check/intake_registration.rb +3 -3
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor/check/sentinels.rb +2 -2
- data/lib/textus/domain/freshness/evaluator.rb +1 -1
- data/lib/textus/domain/freshness/policy.rb +1 -1
- data/lib/textus/domain/freshness/verdict.rb +1 -1
- data/lib/textus/domain/freshness.rb +40 -0
- data/lib/textus/domain/policy/predicates/schema_valid.rb +2 -2
- data/lib/textus/{store → domain}/sentinel.rb +1 -1
- data/lib/textus/{store → domain}/staleness/generator_check.rb +1 -1
- data/lib/textus/{store → domain}/staleness/intake_check.rb +1 -1
- data/lib/textus/{store → domain}/staleness.rb +1 -1
- data/lib/textus/entry/json.rb +1 -1
- data/lib/textus/entry/markdown.rb +1 -1
- data/lib/textus/entry/yaml.rb +1 -1
- data/lib/textus/envelope.rb +7 -3
- data/lib/textus/errors.rb +19 -0
- data/lib/textus/hooks/builtin.rb +6 -6
- data/lib/textus/hooks/dispatcher.rb +17 -9
- data/lib/textus/hooks/loader.rb +20 -17
- data/lib/textus/hooks/registry.rb +4 -0
- data/lib/textus/{store → infra}/audit_log.rb +1 -1
- data/lib/textus/infra/audit_subscriber.rb +43 -0
- data/lib/textus/infra/publisher.rb +3 -3
- data/lib/textus/infra/storage/file_store.rb +26 -0
- data/lib/textus/init.rb +11 -9
- data/lib/textus/manifest/resolution.rb +5 -0
- data/lib/textus/manifest.rb +4 -3
- data/lib/textus/migrate_keys.rb +1 -1
- data/lib/textus/operations.rb +83 -16
- data/lib/textus/projection.rb +2 -2
- data/lib/textus/refresh.rb +1 -1
- data/lib/textus/schema/tools.rb +5 -5
- data/lib/textus/schemas.rb +46 -0
- data/lib/textus/store.rb +12 -49
- data/lib/textus/uid.rb +18 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +17 -1
- metadata +14 -13
- data/lib/textus/hooks/dsl.rb +0 -11
- data/lib/textus/operations/reads.rb +0 -56
- data/lib/textus/operations/refresh.rb +0 -27
- data/lib/textus/operations/writes.rb +0 -21
- data/lib/textus/store/reader.rb +0 -69
- data/lib/textus/store/validator.rb +0 -82
- data/lib/textus/store/writer.rb +0 -102
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Infra
|
|
5
|
+
# Writes an "event_error" audit row when a user hook raises during
|
|
6
|
+
# Hooks::Dispatcher publish. Attached at Store boot.
|
|
7
|
+
#
|
|
8
|
+
# Integration: uses Hooks::Dispatcher#on_error callback (chosen over a
|
|
9
|
+
# synthetic :hook_error event because the dispatcher already owns the
|
|
10
|
+
# rescue and the failure is a dispatcher-internal concern, not a domain
|
|
11
|
+
# event subscribers should be able to filter by key glob).
|
|
12
|
+
#
|
|
13
|
+
# NOTE (0.16 scope): lifecycle audit rows for verb: "put" / "delete" /
|
|
14
|
+
# "rename" are still written directly by Store::Writer and
|
|
15
|
+
# Application::Writes::Mv. Moving those into this subscriber requires
|
|
16
|
+
# event payloads to carry etag_before/etag_after across many write paths;
|
|
17
|
+
# that is properly a 0.18 port-extraction concern.
|
|
18
|
+
class AuditSubscriber
|
|
19
|
+
def initialize(audit_log)
|
|
20
|
+
@audit_log = audit_log
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def attach(bus)
|
|
24
|
+
bus.on_error do |event:, hook:, key:, kwargs:, error:|
|
|
25
|
+
record_error(event: event, hook: hook, key: key, kwargs: kwargs, error: error)
|
|
26
|
+
end
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def record_error(event:, hook:, key:, kwargs:, error:)
|
|
33
|
+
extras = { "event" => event.to_s, "hook" => hook.to_s, "error" => "#{error.class}: #{error.message}" }
|
|
34
|
+
extras["target_key"] = kwargs[:target_key] if kwargs.key?(:target_key)
|
|
35
|
+
extras["pending_key"] = kwargs[:pending_key] if kwargs.key?(:pending_key)
|
|
36
|
+
@audit_log.append(
|
|
37
|
+
role: "runner", verb: "event_error", key: key,
|
|
38
|
+
etag_before: nil, etag_after: nil, extras: extras
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -6,7 +6,7 @@ module Textus
|
|
|
6
6
|
# Publish = copy + sentinel. The in-store file is already the consumer-shaped
|
|
7
7
|
# artifact; no parsing or stripping.
|
|
8
8
|
#
|
|
9
|
-
# Sentinel I/O is delegated to
|
|
9
|
+
# Sentinel I/O is delegated to Textus::Domain::Sentinel. Sentinels live under
|
|
10
10
|
# `<store_root>/sentinels/` and mirror the target's repo-relative layout so
|
|
11
11
|
# consumer directories aren't polluted with `.textus-managed.json` siblings.
|
|
12
12
|
module Publisher
|
|
@@ -15,7 +15,7 @@ module Textus
|
|
|
15
15
|
refuse_if_unmanaged(target, store_root)
|
|
16
16
|
File.delete(target) if File.symlink?(target)
|
|
17
17
|
FileUtils.cp(source, target)
|
|
18
|
-
|
|
18
|
+
Textus::Domain::Sentinel.write!(target: target, source: source, store_root: store_root)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def self.refuse_if_unmanaged(target, store_root)
|
|
@@ -26,7 +26,7 @@ module Textus
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def self.managed?(target, store_root)
|
|
29
|
-
File.exist?(
|
|
29
|
+
File.exist?(Textus::Domain::Sentinel.sentinel_path(target, store_root))
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Infra
|
|
5
|
+
module Storage
|
|
6
|
+
# Pure filesystem I/O port. Wraps File/FileUtils/Etag with no knowledge
|
|
7
|
+
# of envelopes, entries, schemas, or audit.
|
|
8
|
+
class FileStore
|
|
9
|
+
def read(path) = File.binread(path)
|
|
10
|
+
|
|
11
|
+
def write(path, bytes)
|
|
12
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
13
|
+
File.binwrite(path, bytes)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Raises Errno::ENOENT if absent — mirrors File.delete and matches the
|
|
17
|
+
# semantics used by Store::Writer (which guards with File.exist? first).
|
|
18
|
+
def delete(path) = File.delete(path)
|
|
19
|
+
|
|
20
|
+
def exists?(path) = File.exist?(path)
|
|
21
|
+
|
|
22
|
+
def etag(path) = Etag.for_file(path)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/textus/init.rb
CHANGED
|
@@ -28,17 +28,19 @@ module Textus
|
|
|
28
28
|
## DSL
|
|
29
29
|
|
|
30
30
|
```ruby
|
|
31
|
-
Textus.
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
Textus.hook do |reg|
|
|
32
|
+
reg.on(:resolve_intake, :my_source) do |config:, args:, **|
|
|
33
|
+
{ _meta: { "last_refreshed_at" => Time.now.utc.iso8601 }, body: "…" }
|
|
34
|
+
end
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
reg.on(:transform_rows, :my_source) { |rows:, **| rows.map { |r| r.merge(processed: true) } }
|
|
37
|
+
reg.on(:validate, :my_check) { |store:, **| [] }
|
|
38
|
+
reg.on(:entry_put, :my_listener, keys: ["working.*"]) { |key:, envelope:, **| }
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
# Run a side-effect every time textus writes a file to your repo:
|
|
41
|
+
reg.on(:file_published, :notify) do |key:, target:, **|
|
|
42
|
+
warn "wrote \#{target} (from \#{key})"
|
|
43
|
+
end
|
|
42
44
|
end
|
|
43
45
|
```
|
|
44
46
|
|
data/lib/textus/manifest.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "yaml"
|
|
2
2
|
require_relative "manifest/schema"
|
|
3
|
+
require_relative "manifest/resolution"
|
|
3
4
|
|
|
4
5
|
module Textus
|
|
5
6
|
class Manifest
|
|
@@ -86,7 +87,7 @@ module Textus
|
|
|
86
87
|
rules.for(key)
|
|
87
88
|
end
|
|
88
89
|
|
|
89
|
-
# Returns
|
|
90
|
+
# Returns a Resolution(entry:, path:, remaining:) value object.
|
|
90
91
|
def resolve(key)
|
|
91
92
|
validate_key!(key)
|
|
92
93
|
segments = key.split(".")
|
|
@@ -101,7 +102,7 @@ module Textus
|
|
|
101
102
|
remaining = segments[esegs.length..]
|
|
102
103
|
if remaining.empty?
|
|
103
104
|
path = resolve_leaf_path(entry)
|
|
104
|
-
|
|
105
|
+
Resolution.new(entry: entry, path: path, remaining: [])
|
|
105
106
|
else
|
|
106
107
|
raise UnknownKey.new(key, suggestions: suggestions_for(key)) unless entry.nested
|
|
107
108
|
|
|
@@ -111,7 +112,7 @@ module Textus
|
|
|
111
112
|
primary_ext = Textus::Entry.for_format(entry.format).extensions.first
|
|
112
113
|
File.join(@root, "zones", entry.path, *remaining) + primary_ext
|
|
113
114
|
end
|
|
114
|
-
|
|
115
|
+
Resolution.new(entry: entry, path: path, remaining: remaining)
|
|
115
116
|
end
|
|
116
117
|
end
|
|
117
118
|
|
data/lib/textus/migrate_keys.rb
CHANGED
|
@@ -112,7 +112,7 @@ module Textus
|
|
|
112
112
|
# ------------------------------------------------------------------
|
|
113
113
|
|
|
114
114
|
def apply!(store, renames)
|
|
115
|
-
audit =
|
|
115
|
+
audit = Textus::Infra::AuditLog.new(store.root)
|
|
116
116
|
renames.each do |r|
|
|
117
117
|
# Bottom-up order means a child's ancestors haven't moved yet, so
|
|
118
118
|
# `from`/`to` are valid as-recorded. The audit `key` reflects the
|
data/lib/textus/operations.rb
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
# Single canonical entrypoint for invoking application use-cases against a
|
|
3
|
-
# store.
|
|
3
|
+
# store. Public surface is flat — one method per use case:
|
|
4
4
|
#
|
|
5
5
|
# ops = Textus::Operations.for(store, role: "agent")
|
|
6
|
-
# ops.
|
|
7
|
-
# ops.
|
|
8
|
-
# ops.
|
|
9
|
-
# ops.refresh
|
|
10
|
-
#
|
|
11
|
-
# Replaces the prior `Textus::Composition` module (deleted in v0.12.2).
|
|
6
|
+
# ops.put(key, body: "...")
|
|
7
|
+
# ops.get(key) # pure read
|
|
8
|
+
# ops.get_or_refresh(key) # read + refresh-on-stale
|
|
9
|
+
# ops.refresh(key) # synchronous worker refresh
|
|
10
|
+
# ops.refresh_all(prefix: ..., zone: ...)
|
|
12
11
|
class Operations
|
|
13
12
|
def self.for(store, role: Role::DEFAULT, correlation_id: nil, dry_run: false)
|
|
14
13
|
ctx = Application::Context.new(
|
|
@@ -26,20 +25,88 @@ module Textus
|
|
|
26
25
|
@ctx = ctx
|
|
27
26
|
end
|
|
28
27
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
def with_role(role) = self.class.new(@ctx.with_role(role))
|
|
29
|
+
|
|
30
|
+
# writes
|
|
31
|
+
def put(...) = put_op.call(...)
|
|
32
|
+
def delete(...) = delete_op.call(...)
|
|
33
|
+
def mv(...) = mv_op.call(...)
|
|
34
|
+
def accept(...) = accept_op.call(...)
|
|
35
|
+
def reject(...) = reject_op.call(...)
|
|
36
|
+
def build(...) = build_op.call(...)
|
|
37
|
+
def publish(...) = publish_op.call(...)
|
|
38
|
+
|
|
39
|
+
# reads
|
|
40
|
+
def get(...) = get_op.call(...)
|
|
41
|
+
def get_or_refresh(...) = get_or_refresh_op.call(...)
|
|
42
|
+
def list(...) = list_op.call(...)
|
|
43
|
+
def where(...) = where_op.call(...)
|
|
44
|
+
def uid(...) = uid_op.call(...)
|
|
45
|
+
def schema_envelope(...) = schema_envelope_op.call(...)
|
|
46
|
+
def deps(...) = deps_op.call(...)
|
|
47
|
+
def rdeps(...) = rdeps_op.call(...)
|
|
48
|
+
def published(...) = published_op.call(...)
|
|
49
|
+
def stale(...) = stale_op.call(...)
|
|
50
|
+
def audit(...) = audit_op.call(...)
|
|
51
|
+
def blame(...) = blame_op.call(...)
|
|
52
|
+
def policy_explain(...) = policy_explain_op.call(...)
|
|
53
|
+
def freshness(...) = freshness_op.call(...)
|
|
54
|
+
def validate_all(...) = validate_all_op.call(...)
|
|
55
|
+
|
|
56
|
+
# refresh
|
|
57
|
+
def refresh(key) = refresh_worker_op.run(key)
|
|
58
|
+
def refresh_all(**) = Application::Refresh::All.call(@ctx, **)
|
|
59
|
+
|
|
60
|
+
private
|
|
32
61
|
|
|
33
|
-
def
|
|
34
|
-
@
|
|
62
|
+
def envelope_io
|
|
63
|
+
@envelope_io ||= Application::Writes::EnvelopeIO.new(
|
|
64
|
+
file_store: @ctx.file_store,
|
|
65
|
+
manifest: @ctx.manifest,
|
|
66
|
+
schemas: @ctx.schemas,
|
|
67
|
+
audit_log: @ctx.audit_log,
|
|
68
|
+
ctx: @ctx,
|
|
69
|
+
)
|
|
35
70
|
end
|
|
36
71
|
|
|
37
|
-
def
|
|
38
|
-
|
|
72
|
+
def put_op = @put_op ||= Application::Writes::Put.new(ctx: @ctx, envelope_io: envelope_io)
|
|
73
|
+
def delete_op = @delete_op ||= Application::Writes::Delete.new(ctx: @ctx, envelope_io: envelope_io)
|
|
74
|
+
def mv_op = @mv_op ||= Application::Writes::Mv.new(ctx: @ctx, envelope_io: envelope_io)
|
|
75
|
+
def accept_op = @accept_op ||= Application::Writes::Accept.new(ctx: @ctx, envelope_io: envelope_io)
|
|
76
|
+
def reject_op = @reject_op ||= Application::Writes::Reject.new(ctx: @ctx, envelope_io: envelope_io)
|
|
77
|
+
def build_op = @build_op ||= Application::Writes::Build.new(ctx: @ctx)
|
|
78
|
+
def publish_op = @publish_op ||= Application::Writes::Publish.new(ctx: @ctx)
|
|
79
|
+
|
|
80
|
+
def get_op = @get_op ||= Application::Reads::Get.new(ctx: @ctx) # rubocop:disable Naming/AccessorMethodName
|
|
81
|
+
|
|
82
|
+
def get_or_refresh_op # rubocop:disable Naming/AccessorMethodName
|
|
83
|
+
@get_or_refresh_op ||= Application::Reads::GetOrRefresh.new(ctx: @ctx, get: get_op,
|
|
84
|
+
orchestrator: orchestrator_op)
|
|
39
85
|
end
|
|
40
86
|
|
|
41
|
-
def
|
|
42
|
-
|
|
87
|
+
def list_op = @list_op ||= Application::Reads::List.new(ctx: @ctx)
|
|
88
|
+
def where_op = @where_op ||= Application::Reads::Where.new(ctx: @ctx)
|
|
89
|
+
def uid_op = @uid_op ||= Application::Reads::Uid.new(ctx: @ctx)
|
|
90
|
+
def schema_envelope_op = @schema_envelope_op ||= Application::Reads::SchemaEnvelope.new(ctx: @ctx)
|
|
91
|
+
def deps_op = @deps_op ||= Application::Reads::Deps.new(ctx: @ctx)
|
|
92
|
+
def rdeps_op = @rdeps_op ||= Application::Reads::Rdeps.new(ctx: @ctx)
|
|
93
|
+
def published_op = @published_op ||= Application::Reads::Published.new(ctx: @ctx)
|
|
94
|
+
def stale_op = @stale_op ||= Application::Reads::Stale.new(ctx: @ctx)
|
|
95
|
+
def audit_op = @audit_op ||= Application::Reads::Audit.new(ctx: @ctx)
|
|
96
|
+
def blame_op = @blame_op ||= Application::Reads::Blame.new(ctx: @ctx)
|
|
97
|
+
def policy_explain_op = @policy_explain_op ||= Application::Reads::PolicyExplain.new(ctx: @ctx)
|
|
98
|
+
def freshness_op = @freshness_op ||= Application::Reads::Freshness.new(ctx: @ctx)
|
|
99
|
+
def validate_all_op = @validate_all_op ||= Application::Reads::ValidateAll.new(ctx: @ctx)
|
|
100
|
+
|
|
101
|
+
def refresh_worker_op = @refresh_worker_op ||= Application::Refresh::Worker.new(ctx: @ctx, envelope_io: envelope_io)
|
|
102
|
+
|
|
103
|
+
def orchestrator_op
|
|
104
|
+
@orchestrator_op ||= Application::Refresh::Orchestrator.new(
|
|
105
|
+
worker: refresh_worker_op,
|
|
106
|
+
store_root: @ctx.store.root,
|
|
107
|
+
store: @ctx.store,
|
|
108
|
+
role: @ctx.role,
|
|
109
|
+
)
|
|
43
110
|
end
|
|
44
111
|
end
|
|
45
112
|
end
|
data/lib/textus/projection.rb
CHANGED
|
@@ -7,8 +7,8 @@ module Textus
|
|
|
7
7
|
REDUCER_TIMEOUT_SECONDS = 2
|
|
8
8
|
|
|
9
9
|
# `reader` — a callable `->(key) { envelope_or_nil }`. Caller picks
|
|
10
|
-
# semantics: pure read (`ops.
|
|
11
|
-
# `ops.
|
|
10
|
+
# semantics: pure read (`ops.get`) for materialization paths;
|
|
11
|
+
# `ops.get_or_refresh` if you want refresh-on-stale.
|
|
12
12
|
# `lister` — a callable `->(prefix:) { [ { "key" => ... }, ... ] }`.
|
|
13
13
|
# `transform_resolver` — a callable `->(name) { callable_or_raise }`.
|
|
14
14
|
# `transform_context` — `Application::Context` handed to the transform reducer.
|
data/lib/textus/refresh.rb
CHANGED
data/lib/textus/schema/tools.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Textus
|
|
|
6
6
|
module Tools
|
|
7
7
|
# textus schema init NAME --from=KEY → infer YAML schema from an entry's frontmatter
|
|
8
8
|
def self.init(store, name:, from:)
|
|
9
|
-
env = Textus::Operations.for(store).
|
|
9
|
+
env = Textus::Operations.for(store).get(from)
|
|
10
10
|
meta = env.meta
|
|
11
11
|
schema = {
|
|
12
12
|
"name" => name,
|
|
@@ -25,7 +25,7 @@ module Textus
|
|
|
25
25
|
schema = load_schema(store, name)
|
|
26
26
|
drift = []
|
|
27
27
|
store.manifest.enumerate.each do |row|
|
|
28
|
-
env = Textus::Operations.for(store).
|
|
28
|
+
env = Textus::Operations.for(store).get(row[:key])
|
|
29
29
|
begin
|
|
30
30
|
schema.validate!(env.meta)
|
|
31
31
|
rescue SchemaViolation => e
|
|
@@ -52,7 +52,7 @@ module Textus
|
|
|
52
52
|
ops = Textus::Operations.for(store, role: "human")
|
|
53
53
|
touched = []
|
|
54
54
|
store.manifest.enumerate.each do |row|
|
|
55
|
-
env = ops.
|
|
55
|
+
env = ops.get(row[:key])
|
|
56
56
|
meta = env.meta.dup
|
|
57
57
|
changed = false
|
|
58
58
|
renames.each do |old, new|
|
|
@@ -63,7 +63,7 @@ module Textus
|
|
|
63
63
|
end
|
|
64
64
|
next unless changed
|
|
65
65
|
|
|
66
|
-
ops.
|
|
66
|
+
ops.put(row[:key], meta: meta, body: env.body)
|
|
67
67
|
touched << row[:key]
|
|
68
68
|
end
|
|
69
69
|
{ "protocol" => PROTOCOL, "migrated" => touched, "renames" => renames }
|
|
@@ -81,7 +81,7 @@ module Textus
|
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
def self.load_schema(store, name)
|
|
84
|
-
store.
|
|
84
|
+
store.schemas.fetch(name)
|
|
85
85
|
rescue IoError
|
|
86
86
|
raise UsageError.new("schema not found: #{name}")
|
|
87
87
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
# Eager-loading schema cache. Loads every *.yaml under +dir+ at construction.
|
|
3
|
+
# A missing directory is treated as "no schemas" (does not raise) to mirror
|
|
4
|
+
# the lazy behavior previously embedded in Store#schema_for.
|
|
5
|
+
class Schemas
|
|
6
|
+
def initialize(dir)
|
|
7
|
+
@dir = dir
|
|
8
|
+
@schemas = {}
|
|
9
|
+
load_all
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def fetch(name)
|
|
13
|
+
@schemas[name] || raise(IoError.new("schema not found: #{File.join(@dir, "#{name}.yaml")}"))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Only nil short-circuits. A missing-but-named schema still raises IoError.
|
|
17
|
+
def fetch_or_nil(name)
|
|
18
|
+
return nil if name.nil?
|
|
19
|
+
|
|
20
|
+
fetch(name)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def all
|
|
24
|
+
@schemas.values
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def load_all
|
|
30
|
+
return unless File.directory?(@dir)
|
|
31
|
+
|
|
32
|
+
Dir.glob(File.join(@dir, "*.yaml")).each do |path|
|
|
33
|
+
name = File.basename(path, ".yaml")
|
|
34
|
+
begin
|
|
35
|
+
@schemas[name] = Schema.load(path)
|
|
36
|
+
rescue StandardError
|
|
37
|
+
# Tolerate broken schema files at construction time so the rest of
|
|
38
|
+
# the store remains loadable. Surfacing the failure is the job of
|
|
39
|
+
# Doctor::Check::SchemaParseError. Lookups via #fetch still raise
|
|
40
|
+
# IoError for the missing-but-named schema.
|
|
41
|
+
next
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/textus/store.rb
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
require "fileutils"
|
|
2
|
-
require "securerandom"
|
|
3
2
|
|
|
4
3
|
module Textus
|
|
5
4
|
class Store
|
|
6
|
-
attr_reader :root, :manifest, :
|
|
7
|
-
|
|
8
|
-
# A Textus UID: 16 lowercase hex chars (SecureRandom.hex(8)). Not a UUID —
|
|
9
|
-
# short on purpose. Random enough for collision-never-in-practice within a
|
|
10
|
-
# single store.
|
|
11
|
-
def self.mint_uid
|
|
12
|
-
SecureRandom.hex(8)
|
|
13
|
-
end
|
|
5
|
+
attr_reader :root, :manifest, :schemas, :file_store, :audit_log, :bus, :registry
|
|
14
6
|
|
|
15
7
|
def self.discover(start_dir = Dir.pwd, root: nil)
|
|
16
8
|
explicit = root || ENV.fetch("TEXTUS_ROOT", nil)
|
|
@@ -37,46 +29,17 @@ module Textus
|
|
|
37
29
|
end
|
|
38
30
|
|
|
39
31
|
def initialize(root)
|
|
40
|
-
@root
|
|
41
|
-
@manifest
|
|
42
|
-
@
|
|
43
|
-
@
|
|
44
|
-
@
|
|
45
|
-
|
|
46
|
-
@
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def load_hooks
|
|
52
|
-
Textus.with_registry(@registry) do
|
|
53
|
-
Hooks::Builtin.register_all
|
|
54
|
-
dir = File.join(@root, "hooks")
|
|
55
|
-
return unless File.directory?(dir)
|
|
56
|
-
|
|
57
|
-
Dir.glob(File.join(dir, "**/*.rb")).sort.each do |f| # rubocop:disable Lint/RedundantDirGlobSort
|
|
58
|
-
begin
|
|
59
|
-
load(f)
|
|
60
|
-
rescue StandardError, ScriptError => e
|
|
61
|
-
raise UsageError.new("failed loading hook #{File.basename(f)}: #{e.class}: #{e.message}")
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def schema_for(name)
|
|
68
|
-
return nil if name.nil?
|
|
69
|
-
|
|
70
|
-
@schemas[name] ||= begin
|
|
71
|
-
sp = File.join(@root, "schemas", "#{name}.yaml")
|
|
72
|
-
raise IoError.new("schema not found: #{sp}") unless File.exist?(sp)
|
|
73
|
-
|
|
74
|
-
Schema.load(sp)
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def audit_log
|
|
79
|
-
@audit_log ||= Store::AuditLog.new(@root)
|
|
32
|
+
@root = File.expand_path(root)
|
|
33
|
+
@manifest = Manifest.load(@root)
|
|
34
|
+
@schemas = Schemas.new(File.join(@root, "schemas"))
|
|
35
|
+
@file_store = Infra::Storage::FileStore.new
|
|
36
|
+
@audit_log = Infra::AuditLog.new(@root)
|
|
37
|
+
@bus = Hooks::Dispatcher.new
|
|
38
|
+
@registry = Hooks::Registry.new(dispatcher: @bus)
|
|
39
|
+
Infra::AuditSubscriber.new(@audit_log).attach(@bus)
|
|
40
|
+
Hooks::Builtin.register_all(@registry)
|
|
41
|
+
Hooks::Loader.new(registry: @registry).load_dir(File.join(@root, "hooks"))
|
|
42
|
+
@bus.publish(:store_loaded, store: Application::Context.system(self))
|
|
80
43
|
end
|
|
81
44
|
end
|
|
82
45
|
end
|
data/lib/textus/uid.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
# A Textus UID: 16 lowercase hex chars (SecureRandom.hex(8)). Not a UUID —
|
|
5
|
+
# short on purpose. Random enough for collision-never-in-practice within a
|
|
6
|
+
# single store.
|
|
7
|
+
module Uid
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def mint
|
|
11
|
+
SecureRandom.hex(8)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def valid?(str)
|
|
15
|
+
str.is_a?(String) && str.match?(/\A[0-9a-f]{16}\z/)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/textus/version.rb
CHANGED
data/lib/textus.rb
CHANGED
|
@@ -8,11 +8,27 @@ loader.inflector.inflect(
|
|
|
8
8
|
"json" => "Json",
|
|
9
9
|
"yaml" => "Yaml",
|
|
10
10
|
"hook_dsl_scanner" => "HookDSLScanner",
|
|
11
|
+
"envelope_io" => "EnvelopeIO",
|
|
11
12
|
)
|
|
12
13
|
loader.ignore(File.expand_path("textus/errors.rb", __dir__))
|
|
13
14
|
loader.setup
|
|
14
15
|
loader.eager_load
|
|
15
16
|
|
|
16
17
|
module Textus
|
|
17
|
-
|
|
18
|
+
@hook_mutex = Mutex.new
|
|
19
|
+
@hook_blocks = []
|
|
20
|
+
|
|
21
|
+
def self.hook(&blk)
|
|
22
|
+
raise UsageError.new("hook block required") unless blk
|
|
23
|
+
|
|
24
|
+
@hook_mutex.synchronize { @hook_blocks << blk }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.drain_hook_blocks
|
|
28
|
+
@hook_mutex.synchronize do
|
|
29
|
+
blocks = @hook_blocks
|
|
30
|
+
@hook_blocks = []
|
|
31
|
+
blocks
|
|
32
|
+
end
|
|
33
|
+
end
|
|
18
34
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: textus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.18.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -124,6 +124,7 @@ files:
|
|
|
124
124
|
- lib/textus/application/reads/stale.rb
|
|
125
125
|
- lib/textus/application/reads/uid.rb
|
|
126
126
|
- lib/textus/application/reads/validate_all.rb
|
|
127
|
+
- lib/textus/application/reads/validator.rb
|
|
127
128
|
- lib/textus/application/reads/where.rb
|
|
128
129
|
- lib/textus/application/refresh/all.rb
|
|
129
130
|
- lib/textus/application/refresh/orchestrator.rb
|
|
@@ -131,6 +132,7 @@ files:
|
|
|
131
132
|
- lib/textus/application/writes/accept.rb
|
|
132
133
|
- lib/textus/application/writes/build.rb
|
|
133
134
|
- lib/textus/application/writes/delete.rb
|
|
135
|
+
- lib/textus/application/writes/envelope_io.rb
|
|
134
136
|
- lib/textus/application/writes/mv.rb
|
|
135
137
|
- lib/textus/application/writes/publish.rb
|
|
136
138
|
- lib/textus/application/writes/put.rb
|
|
@@ -198,6 +200,7 @@ files:
|
|
|
198
200
|
- lib/textus/doctor/check/templates.rb
|
|
199
201
|
- lib/textus/doctor/check/unowned_schema_fields.rb
|
|
200
202
|
- lib/textus/domain/action.rb
|
|
203
|
+
- lib/textus/domain/freshness.rb
|
|
201
204
|
- lib/textus/domain/freshness/evaluator.rb
|
|
202
205
|
- lib/textus/domain/freshness/policy.rb
|
|
203
206
|
- lib/textus/domain/freshness/verdict.rb
|
|
@@ -211,6 +214,10 @@ files:
|
|
|
211
214
|
- lib/textus/domain/policy/promote.rb
|
|
212
215
|
- lib/textus/domain/policy/promotion.rb
|
|
213
216
|
- lib/textus/domain/policy/refresh.rb
|
|
217
|
+
- lib/textus/domain/sentinel.rb
|
|
218
|
+
- lib/textus/domain/staleness.rb
|
|
219
|
+
- lib/textus/domain/staleness/generator_check.rb
|
|
220
|
+
- lib/textus/domain/staleness/intake_check.rb
|
|
214
221
|
- lib/textus/entry.rb
|
|
215
222
|
- lib/textus/entry/base.rb
|
|
216
223
|
- lib/textus/entry/json.rb
|
|
@@ -222,15 +229,17 @@ files:
|
|
|
222
229
|
- lib/textus/etag.rb
|
|
223
230
|
- lib/textus/hooks/builtin.rb
|
|
224
231
|
- lib/textus/hooks/dispatcher.rb
|
|
225
|
-
- lib/textus/hooks/dsl.rb
|
|
226
232
|
- lib/textus/hooks/loader.rb
|
|
227
233
|
- lib/textus/hooks/registry.rb
|
|
234
|
+
- lib/textus/infra/audit_log.rb
|
|
235
|
+
- lib/textus/infra/audit_subscriber.rb
|
|
228
236
|
- lib/textus/infra/build_lock.rb
|
|
229
237
|
- lib/textus/infra/clock.rb
|
|
230
238
|
- lib/textus/infra/event_bus.rb
|
|
231
239
|
- lib/textus/infra/publisher.rb
|
|
232
240
|
- lib/textus/infra/refresh/detached.rb
|
|
233
241
|
- lib/textus/infra/refresh/lock.rb
|
|
242
|
+
- lib/textus/infra/storage/file_store.rb
|
|
234
243
|
- lib/textus/init.rb
|
|
235
244
|
- lib/textus/intro.rb
|
|
236
245
|
- lib/textus/key/distance.rb
|
|
@@ -245,28 +254,20 @@ files:
|
|
|
245
254
|
- lib/textus/manifest/entry/validators/index_filename.rb
|
|
246
255
|
- lib/textus/manifest/entry/validators/inject_intro.rb
|
|
247
256
|
- lib/textus/manifest/entry/validators/publish_each.rb
|
|
257
|
+
- lib/textus/manifest/resolution.rb
|
|
248
258
|
- lib/textus/manifest/rules.rb
|
|
249
259
|
- lib/textus/manifest/schema.rb
|
|
250
260
|
- lib/textus/migrate_keys.rb
|
|
251
261
|
- lib/textus/mustache.rb
|
|
252
262
|
- lib/textus/operations.rb
|
|
253
|
-
- lib/textus/operations/reads.rb
|
|
254
|
-
- lib/textus/operations/refresh.rb
|
|
255
|
-
- lib/textus/operations/writes.rb
|
|
256
263
|
- lib/textus/projection.rb
|
|
257
264
|
- lib/textus/refresh.rb
|
|
258
265
|
- lib/textus/role.rb
|
|
259
266
|
- lib/textus/schema.rb
|
|
260
267
|
- lib/textus/schema/tools.rb
|
|
268
|
+
- lib/textus/schemas.rb
|
|
261
269
|
- lib/textus/store.rb
|
|
262
|
-
- lib/textus/
|
|
263
|
-
- lib/textus/store/reader.rb
|
|
264
|
-
- lib/textus/store/sentinel.rb
|
|
265
|
-
- lib/textus/store/staleness.rb
|
|
266
|
-
- lib/textus/store/staleness/generator_check.rb
|
|
267
|
-
- lib/textus/store/staleness/intake_check.rb
|
|
268
|
-
- lib/textus/store/validator.rb
|
|
269
|
-
- lib/textus/store/writer.rb
|
|
270
|
+
- lib/textus/uid.rb
|
|
270
271
|
- lib/textus/version.rb
|
|
271
272
|
homepage: https://github.com/patrick204nqh/textus
|
|
272
273
|
licenses:
|