textus 0.14.4 → 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 +378 -0
- data/README.md +13 -9
- data/SPEC.md +13 -10
- data/docs/conventions.md +11 -0
- data/lib/textus/application/context.rb +25 -7
- 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 +38 -33
- data/lib/textus/application/reads/get_or_refresh.rb +51 -0
- 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 +11 -3
- data/lib/textus/application/refresh/worker.rb +27 -20
- 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 +8 -1
- 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 +4 -1
- 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 +40 -35
- 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 +84 -17
- data/lib/textus/projection.rb +16 -11
- 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 +15 -13
- data/lib/textus/hooks/dsl.rb +0 -11
- data/lib/textus/operations/reads.rb +0 -39
- 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,106 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Application
|
|
3
|
+
module Writes
|
|
4
|
+
# Owns the write pipeline (validate, serialize, etag-check, write, audit)
|
|
5
|
+
# extracted from Store::Writer. Talks to ports (FileStore, Schemas,
|
|
6
|
+
# AuditLog, Manifest) instead of File/FileUtils and Store directly.
|
|
7
|
+
#
|
|
8
|
+
# No permission check, no event firing — those belong to the caller
|
|
9
|
+
# (Application::Writes::Put / ::Delete).
|
|
10
|
+
class EnvelopeIO
|
|
11
|
+
Payload = Data.define(:meta, :body, :content)
|
|
12
|
+
|
|
13
|
+
def initialize(file_store:, manifest:, schemas:, audit_log:, ctx:)
|
|
14
|
+
@file_store = file_store
|
|
15
|
+
@manifest = manifest
|
|
16
|
+
@schemas = schemas
|
|
17
|
+
@audit_log = audit_log
|
|
18
|
+
@ctx = ctx
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def write(key, mentry:, payload:, if_etag: nil)
|
|
22
|
+
path = @manifest.resolve(key).path
|
|
23
|
+
|
|
24
|
+
meta = payload.meta || {}
|
|
25
|
+
strategy = Entry.for_format(mentry.format)
|
|
26
|
+
|
|
27
|
+
existing_uid = existing_uid_for(mentry, path)
|
|
28
|
+
meta, content = ensure_uid(mentry.format, meta, payload.content, existing_uid)
|
|
29
|
+
|
|
30
|
+
bytes, eff_meta, eff_body, eff_content = serialize_for_put(
|
|
31
|
+
mentry: mentry, path: path, strategy: strategy,
|
|
32
|
+
meta: meta, body: payload.body, content: content
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
enforce_name_match!(path, eff_meta, mentry.format)
|
|
36
|
+
|
|
37
|
+
schema = @schemas.fetch_or_nil(mentry.schema)
|
|
38
|
+
if schema
|
|
39
|
+
Entry.for_format(mentry.format).validate_against(
|
|
40
|
+
schema,
|
|
41
|
+
{ "_meta" => eff_meta, "content" => eff_content },
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
etag_before = @file_store.exists?(path) ? @file_store.etag(path) : nil
|
|
46
|
+
raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && (etag_before != if_etag)
|
|
47
|
+
|
|
48
|
+
@file_store.write(path, bytes)
|
|
49
|
+
etag_after = Etag.for_bytes(bytes)
|
|
50
|
+
@audit_log.append(
|
|
51
|
+
role: @ctx.role, verb: "put", key: key,
|
|
52
|
+
etag_before: etag_before, etag_after: etag_after,
|
|
53
|
+
extras: @ctx.correlation_id ? { "correlation_id" => @ctx.correlation_id } : nil
|
|
54
|
+
)
|
|
55
|
+
Envelope.build(
|
|
56
|
+
key: key, mentry: mentry, path: path,
|
|
57
|
+
meta: eff_meta, body: eff_body, etag: etag_after, content: eff_content
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def delete(key, mentry:, if_etag: nil)
|
|
62
|
+
_ = mentry
|
|
63
|
+
path = @manifest.resolve(key).path
|
|
64
|
+
raise UnknownKey.new(key, suggestions: @manifest.suggestions_for(key)) unless @file_store.exists?(path)
|
|
65
|
+
|
|
66
|
+
etag_before = @file_store.etag(path)
|
|
67
|
+
raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && if_etag != etag_before
|
|
68
|
+
|
|
69
|
+
@file_store.delete(path)
|
|
70
|
+
@audit_log.append(
|
|
71
|
+
role: @ctx.role, verb: "delete", key: key,
|
|
72
|
+
etag_before: etag_before, etag_after: nil,
|
|
73
|
+
extras: @ctx.correlation_id ? { "correlation_id" => @ctx.correlation_id } : nil
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def existing_uid_for(mentry, path)
|
|
80
|
+
return nil unless @file_store.exists?(path)
|
|
81
|
+
|
|
82
|
+
raw = @file_store.read(path)
|
|
83
|
+
parsed = Entry.for_format(mentry.format).parse(raw, path: path)
|
|
84
|
+
Envelope.extract_uid(parsed["_meta"])
|
|
85
|
+
rescue StandardError
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def ensure_uid(format, meta, content, existing_uid)
|
|
90
|
+
Textus::Entry.for_format(format).inject_uid(meta, content, existing_uid)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def enforce_name_match!(path, meta, format)
|
|
94
|
+
Textus::Entry.for_format(format).enforce_name_match!(path, meta)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def serialize_for_put(mentry:, path:, strategy:, meta:, body:, content:)
|
|
98
|
+
_ = strategy
|
|
99
|
+
Textus::Entry.for_format(mentry.format).serialize_for_put(
|
|
100
|
+
meta: meta, body: body, content: content, path: path,
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -9,9 +9,9 @@ module Textus
|
|
|
9
9
|
:new_mentry, :uid, :etag_before
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
-
def initialize(ctx:,
|
|
12
|
+
def initialize(ctx:, envelope_io:)
|
|
13
13
|
@ctx = ctx
|
|
14
|
-
@
|
|
14
|
+
@envelope_io = envelope_io
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def call(old_key, new_key, dry_run: false)
|
|
@@ -26,23 +26,28 @@ module Textus
|
|
|
26
26
|
|
|
27
27
|
private
|
|
28
28
|
|
|
29
|
-
def manifest = @ctx.
|
|
30
|
-
def
|
|
29
|
+
def manifest = @ctx.manifest
|
|
30
|
+
def reader_get(key) = (@reader_get ||= Textus::Application::Reads::Get.new(ctx: @ctx)).call(key)
|
|
31
31
|
|
|
32
32
|
def prepare_plan(old_key, new_key)
|
|
33
33
|
manifest.validate_key!(old_key)
|
|
34
34
|
manifest.validate_key!(new_key)
|
|
35
35
|
raise UsageError.new("mv: old and new keys are identical") if old_key == new_key
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
old_res = manifest.resolve(old_key)
|
|
38
|
+
old_mentry = old_res.entry
|
|
39
|
+
old_path = old_res.path
|
|
40
|
+
raise UnknownKey.new(old_key) unless @ctx.file_store.exists?(old_path)
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
new_res = manifest.resolve(new_key)
|
|
43
|
+
new_mentry = new_res.entry
|
|
44
|
+
new_path = new_res.path
|
|
41
45
|
validate_zone_and_format!(old_mentry, new_mentry)
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
@ctx.authorize_write!(old_mentry)
|
|
47
|
+
@ctx.authorize_write!(new_mentry)
|
|
48
|
+
raise UsageError.new("mv: target '#{new_key}' already exists at #{new_path}") if @ctx.file_store.exists?(new_path)
|
|
44
49
|
|
|
45
|
-
pre_env =
|
|
50
|
+
pre_env = reader_get(old_key)
|
|
46
51
|
plan = MovePlan.new(
|
|
47
52
|
old_key: old_key, new_key: new_key,
|
|
48
53
|
old_path: old_path, new_path: new_path,
|
|
@@ -64,17 +69,10 @@ module Textus
|
|
|
64
69
|
raise UsageError.new("mv: format mismatch (#{old_mentry.format} → #{new_mentry.format}); refusing.")
|
|
65
70
|
end
|
|
66
71
|
|
|
67
|
-
def validate_writer!(mentry, key)
|
|
68
|
-
writers = manifest.zone_writers(mentry.zone)
|
|
69
|
-
return if writers.include?(@ctx.role)
|
|
70
|
-
|
|
71
|
-
raise WriteForbidden.new(key, mentry.zone, writers: writers)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
72
|
def ensure_uid!(plan, pre_env:)
|
|
75
73
|
return plan if plan.uid
|
|
76
74
|
|
|
77
|
-
env = Textus::Application::Writes::Put.new(ctx: @ctx,
|
|
75
|
+
env = Textus::Application::Writes::Put.new(ctx: @ctx, envelope_io: @envelope_io).call(
|
|
78
76
|
plan.old_key,
|
|
79
77
|
meta: pre_env.meta,
|
|
80
78
|
body: pre_env.body,
|
|
@@ -99,19 +97,19 @@ module Textus
|
|
|
99
97
|
}
|
|
100
98
|
extras["correlation_id"] = @ctx.correlation_id if @ctx.correlation_id
|
|
101
99
|
|
|
102
|
-
@ctx.
|
|
100
|
+
@ctx.audit_log.append(
|
|
103
101
|
role: @ctx.role, verb: "mv", key: plan.new_key,
|
|
104
102
|
etag_before: plan.etag_before, etag_after: etag_after,
|
|
105
103
|
extras: extras
|
|
106
104
|
)
|
|
107
|
-
new_envelope =
|
|
108
|
-
@bus.publish(:entry_renamed,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
105
|
+
new_envelope = reader_get(plan.new_key)
|
|
106
|
+
@ctx.bus.publish(:entry_renamed,
|
|
107
|
+
store: @ctx.with_role(@ctx.role),
|
|
108
|
+
key: plan.new_key,
|
|
109
|
+
from_key: plan.old_key,
|
|
110
|
+
to_key: plan.new_key,
|
|
111
|
+
envelope: new_envelope,
|
|
112
|
+
correlation_id: @ctx.correlation_id)
|
|
115
113
|
new_envelope
|
|
116
114
|
end
|
|
117
115
|
|
|
@@ -5,9 +5,8 @@ module Textus
|
|
|
5
5
|
# `:file_published` for each copy. Mirror of `Build` for the publish
|
|
6
6
|
# half — split out from the old Build per ADR 0007.
|
|
7
7
|
class Publish
|
|
8
|
-
def initialize(ctx
|
|
8
|
+
def initialize(ctx:)
|
|
9
9
|
@ctx = ctx
|
|
10
|
-
@bus = bus
|
|
11
10
|
end
|
|
12
11
|
|
|
13
12
|
def call(prefix: nil)
|
|
@@ -42,13 +41,13 @@ module Textus
|
|
|
42
41
|
end
|
|
43
42
|
|
|
44
43
|
Textus::Infra::Publisher.publish(source: row[:path], target: target_abs, store_root: store.root)
|
|
45
|
-
@bus.publish(:file_published,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
44
|
+
@ctx.bus.publish(:file_published,
|
|
45
|
+
store: @ctx.with_role(@ctx.role),
|
|
46
|
+
key: row[:key],
|
|
47
|
+
envelope: Textus::Application::Reads::Get.new(ctx: @ctx).call(row[:key]),
|
|
48
|
+
source: row[:path],
|
|
49
|
+
target: target_abs,
|
|
50
|
+
correlation_id: @ctx.correlation_id)
|
|
52
51
|
{ "key" => row[:key], "source" => row[:path], "target" => target_abs }
|
|
53
52
|
end
|
|
54
53
|
end
|
|
@@ -2,34 +2,30 @@ module Textus
|
|
|
2
2
|
module Application
|
|
3
3
|
module Writes
|
|
4
4
|
class Put
|
|
5
|
-
def initialize(ctx:,
|
|
5
|
+
def initialize(ctx:, envelope_io:)
|
|
6
6
|
@ctx = ctx
|
|
7
|
-
@
|
|
7
|
+
@envelope_io = envelope_io
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def call(key, meta: nil, body: nil, content: nil, if_etag: nil, suppress_events: false)
|
|
11
|
-
@ctx.
|
|
12
|
-
mentry
|
|
11
|
+
@ctx.manifest.validate_key!(key)
|
|
12
|
+
mentry = @ctx.manifest.resolve(key).entry
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
raise WriteForbidden.new(key, mentry.zone,
|
|
16
|
-
writers: @ctx.store.manifest.zone_writers(mentry.zone))
|
|
17
|
-
end
|
|
14
|
+
@ctx.authorize_write!(mentry)
|
|
18
15
|
|
|
19
|
-
envelope = @
|
|
16
|
+
envelope = @envelope_io.write(
|
|
20
17
|
key,
|
|
21
18
|
mentry: mentry,
|
|
22
|
-
payload: Textus::
|
|
23
|
-
ctx: @ctx,
|
|
19
|
+
payload: Textus::Application::Writes::EnvelopeIO::Payload.new(meta: meta, body: body, content: content),
|
|
24
20
|
if_etag: if_etag,
|
|
25
21
|
)
|
|
26
22
|
|
|
27
23
|
unless suppress_events
|
|
28
|
-
@bus.publish(:entry_put,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
@ctx.bus.publish(:entry_put,
|
|
25
|
+
store: @ctx.with_role(@ctx.role),
|
|
26
|
+
key: key,
|
|
27
|
+
envelope: envelope,
|
|
28
|
+
correlation_id: @ctx.correlation_id)
|
|
33
29
|
end
|
|
34
30
|
|
|
35
31
|
envelope
|
|
@@ -2,32 +2,32 @@ module Textus
|
|
|
2
2
|
module Application
|
|
3
3
|
module Writes
|
|
4
4
|
class Reject
|
|
5
|
-
def initialize(ctx:,
|
|
5
|
+
def initialize(ctx:, envelope_io:)
|
|
6
6
|
@ctx = ctx
|
|
7
|
-
@
|
|
7
|
+
@envelope_io = envelope_io
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def call(pending_key)
|
|
11
11
|
raise ProposalError.new("only human role can reject proposals; got '#{@ctx.role}'") unless @ctx.role == "human"
|
|
12
12
|
|
|
13
|
-
mentry
|
|
13
|
+
mentry = @ctx.manifest.resolve(pending_key).entry
|
|
14
14
|
unless mentry.in_proposal_zone?
|
|
15
15
|
raise ProposalError.new("reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.zone})")
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
env = @ctx.
|
|
18
|
+
env = Textus::Application::Reads::Get.new(ctx: @ctx).call(pending_key)
|
|
19
19
|
proposal = env.meta&.dig("proposal") or
|
|
20
20
|
raise ProposalError.new("entry has no proposal block: #{pending_key}")
|
|
21
21
|
target_key = proposal["target_key"] or
|
|
22
22
|
raise ProposalError.new("proposal missing target_key")
|
|
23
23
|
|
|
24
|
-
Textus::Application::Writes::Delete.new(ctx: @ctx,
|
|
24
|
+
Textus::Application::Writes::Delete.new(ctx: @ctx, envelope_io: @envelope_io).call(pending_key, suppress_events: true)
|
|
25
25
|
|
|
26
|
-
@bus.publish(:proposal_rejected,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
@ctx.bus.publish(:proposal_rejected,
|
|
27
|
+
store: @ctx.with_role(@ctx.role),
|
|
28
|
+
key: pending_key,
|
|
29
|
+
target_key: target_key,
|
|
30
|
+
correlation_id: @ctx.correlation_id)
|
|
31
31
|
|
|
32
32
|
{ "protocol" => PROTOCOL, "rejected" => pending_key, "target_key" => target_key }
|
|
33
33
|
end
|
|
@@ -62,7 +62,14 @@ module Textus
|
|
|
62
62
|
# 1. Load sources + project + reduce
|
|
63
63
|
data =
|
|
64
64
|
if mentry.projection
|
|
65
|
-
|
|
65
|
+
ops = Operations.for(store)
|
|
66
|
+
Projection.new(
|
|
67
|
+
reader: ops.method(:get),
|
|
68
|
+
spec: mentry.projection,
|
|
69
|
+
lister: ops.method(:list),
|
|
70
|
+
transform_resolver: ->(name) { store.registry.rpc_callable(:transform_rows, name) },
|
|
71
|
+
transform_context: Application::Context.system(store),
|
|
72
|
+
).run
|
|
66
73
|
else
|
|
67
74
|
{ "entries" => [], "count" => 0, "generated_at" => Time.now.utc.iso8601 }
|
|
68
75
|
end
|
data/lib/textus/cli/group/key.rb
CHANGED
|
@@ -2,11 +2,7 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Group
|
|
4
4
|
class Schema < Group
|
|
5
|
-
|
|
6
|
-
subcommands["show"] = Verb::Schema
|
|
7
|
-
subcommands["init"] = Verb::SchemaInit
|
|
8
|
-
subcommands["diff"] = Verb::SchemaDiff
|
|
9
|
-
subcommands["migrate"] = Verb::SchemaMigrate
|
|
5
|
+
command_name "schema"
|
|
10
6
|
end
|
|
11
7
|
end
|
|
12
8
|
end
|
data/lib/textus/cli/group.rb
CHANGED
|
@@ -2,19 +2,14 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Group < Verb
|
|
4
4
|
class << self
|
|
5
|
+
# Subcommands are auto-derived: any Verb descendant whose
|
|
6
|
+
# `parent_group` is this group counts as a subcommand. Sorted
|
|
7
|
+
# alphabetically by command_name for stable help output.
|
|
5
8
|
def subcommands
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@cli_name || raise("subclass must define cli_name")
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
attr_writer :cli_name
|
|
14
|
-
|
|
15
|
-
def inherited(subclass)
|
|
16
|
-
super
|
|
17
|
-
subclass.instance_variable_set(:@subcommands, {})
|
|
9
|
+
Verb.descendants
|
|
10
|
+
.select { |k| k.parent_group == self && k.command_name }
|
|
11
|
+
.sort_by(&:command_name)
|
|
12
|
+
.to_h { |k| [k.command_name, k] }
|
|
18
13
|
end
|
|
19
14
|
|
|
20
15
|
def needs_store?
|
|
@@ -24,18 +19,19 @@ module Textus
|
|
|
24
19
|
end
|
|
25
20
|
|
|
26
21
|
def parse(argv)
|
|
22
|
+
subs = self.class.subcommands
|
|
27
23
|
subname = argv.shift
|
|
28
24
|
if subname.nil?
|
|
29
25
|
raise UsageError.new(
|
|
30
|
-
"#{self.class.
|
|
26
|
+
"#{self.class.command_name} requires a subcommand: #{subs.keys.join(", ")}",
|
|
31
27
|
)
|
|
32
28
|
end
|
|
33
29
|
|
|
34
|
-
@sub_klass =
|
|
30
|
+
@sub_klass = subs[subname]
|
|
35
31
|
unless @sub_klass
|
|
36
32
|
raise UsageError.new(
|
|
37
|
-
"unknown #{self.class.
|
|
38
|
-
"Valid: #{
|
|
33
|
+
"unknown #{self.class.command_name} subcommand '#{subname}'. " \
|
|
34
|
+
"Valid: #{subs.keys.join(", ")}",
|
|
39
35
|
)
|
|
40
36
|
end
|
|
41
37
|
|
|
@@ -2,11 +2,13 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Accept < Verb
|
|
5
|
+
command_name "accept"
|
|
6
|
+
|
|
5
7
|
option :as_flag, "--as=ROLE"
|
|
6
8
|
|
|
7
9
|
def call(store)
|
|
8
10
|
key = positional.shift or raise UsageError.new("accept requires a key")
|
|
9
|
-
emit(operations_for(store).
|
|
11
|
+
emit(operations_for(store).accept(key))
|
|
10
12
|
end
|
|
11
13
|
end
|
|
12
14
|
end
|
|
@@ -2,6 +2,8 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Audit < Verb
|
|
5
|
+
command_name "audit"
|
|
6
|
+
|
|
5
7
|
option :key_filter, "--key=KEY"
|
|
6
8
|
option :zone, "--zone=Z"
|
|
7
9
|
option :role_filter, "--role=ROLE"
|
|
@@ -13,7 +15,7 @@ module Textus
|
|
|
13
15
|
def call(store)
|
|
14
16
|
ops = operations_for(store)
|
|
15
17
|
since_time = since && Textus::Application::Reads::Audit.parse_since(since, now: ops.ctx.now)
|
|
16
|
-
rows = ops.
|
|
18
|
+
rows = ops.audit(
|
|
17
19
|
key: key_filter,
|
|
18
20
|
zone: zone,
|
|
19
21
|
role: role_filter,
|
|
@@ -2,11 +2,13 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Blame < Verb
|
|
5
|
+
command_name "blame"
|
|
6
|
+
|
|
5
7
|
option :limit, "--limit=N"
|
|
6
8
|
|
|
7
9
|
def call(store)
|
|
8
10
|
key = positional.shift or raise UsageError.new("blame requires a key")
|
|
9
|
-
rows = operations_for(store).
|
|
11
|
+
rows = operations_for(store).blame(key: key, limit: limit&.to_i)
|
|
10
12
|
emit({ "verb" => "blame", "key" => key, "rows" => rows })
|
|
11
13
|
end
|
|
12
14
|
end
|
|
@@ -2,13 +2,15 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Build < Verb
|
|
5
|
+
command_name "build"
|
|
6
|
+
|
|
5
7
|
option :prefix, "--prefix=K"
|
|
6
8
|
|
|
7
9
|
def call(store)
|
|
8
10
|
Textus::Infra::BuildLock.with(root: store.root) do
|
|
9
11
|
ops = Textus::Operations.for(store, role: "builder")
|
|
10
|
-
build_res = ops.
|
|
11
|
-
publish_res = ops.
|
|
12
|
+
build_res = ops.build(prefix: prefix)
|
|
13
|
+
publish_res = ops.publish(prefix: prefix)
|
|
12
14
|
emit({ "protocol" => Textus::PROTOCOL,
|
|
13
15
|
"built" => build_res["built"],
|
|
14
16
|
"published_leaves" => publish_res["published_leaves"] })
|
|
@@ -2,12 +2,14 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Delete < Verb
|
|
5
|
+
command_name "delete"
|
|
6
|
+
|
|
5
7
|
option :as_flag, "--as=ROLE"
|
|
6
8
|
option :if_etag, "--if-etag=E"
|
|
7
9
|
|
|
8
10
|
def call(store)
|
|
9
11
|
key = positional.shift or raise UsageError.new("delete requires a key")
|
|
10
|
-
emit(operations_for(store).
|
|
12
|
+
emit(operations_for(store).delete(key, if_etag: if_etag))
|
|
11
13
|
end
|
|
12
14
|
end
|
|
13
15
|
end
|
data/lib/textus/cli/verb/deps.rb
CHANGED
|
@@ -2,9 +2,11 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Deps < Verb
|
|
5
|
+
command_name "deps"
|
|
6
|
+
|
|
5
7
|
def call(store)
|
|
6
8
|
key = positional.shift or raise UsageError.new("deps requires a key")
|
|
7
|
-
emit({ "key" => key, "deps" => operations_for(store).
|
|
9
|
+
emit({ "key" => key, "deps" => operations_for(store).deps(key) })
|
|
8
10
|
end
|
|
9
11
|
end
|
|
10
12
|
end
|
|
@@ -2,11 +2,13 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Freshness < Verb
|
|
5
|
+
command_name "freshness"
|
|
6
|
+
|
|
5
7
|
option :prefix, "--prefix=KEY"
|
|
6
8
|
option :zone, "--zone=Z"
|
|
7
9
|
|
|
8
10
|
def call(store)
|
|
9
|
-
rows = operations_for(store).
|
|
11
|
+
rows = operations_for(store).freshness(prefix: prefix, zone: zone)
|
|
10
12
|
emit({ "verb" => "freshness", "rows" => rows })
|
|
11
13
|
end
|
|
12
14
|
end
|
data/lib/textus/cli/verb/get.rb
CHANGED
|
@@ -2,11 +2,13 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Get < Verb
|
|
5
|
+
command_name "get"
|
|
6
|
+
|
|
5
7
|
option :as_flag, "--as=ROLE"
|
|
6
8
|
|
|
7
9
|
def call(store)
|
|
8
10
|
key = positional.shift or raise UsageError.new("get requires a key")
|
|
9
|
-
result = operations_for(store).
|
|
11
|
+
result = operations_for(store).get_or_refresh(key)
|
|
10
12
|
raise Textus::UnknownKey.new(key, suggestions: store.manifest.suggestions_for(key)) if result.nil?
|
|
11
13
|
|
|
12
14
|
emit(result.to_h_for_wire)
|
data/lib/textus/cli/verb/init.rb
CHANGED