textus 0.15.0 → 0.20.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 +50 -55
- data/CHANGELOG.md +486 -0
- data/README.md +13 -9
- data/SPEC.md +13 -10
- data/docs/conventions.md +2 -2
- data/lib/textus/application/context.rb +20 -34
- data/lib/textus/application/policy/predicates/human_accept.rb +30 -0
- data/lib/textus/{domain → application}/policy/predicates/schema_valid.rb +5 -5
- data/lib/textus/{domain → application}/policy/promotion.rb +20 -3
- data/lib/textus/application/projection.rb +91 -0
- data/lib/textus/application/reads/audit.rb +4 -4
- data/lib/textus/application/reads/blame.rb +11 -8
- data/lib/textus/application/reads/deps.rb +14 -3
- data/lib/textus/application/reads/freshness.rb +17 -6
- data/lib/textus/application/reads/get.rb +37 -11
- data/lib/textus/application/reads/get_or_refresh.rb +8 -8
- data/lib/textus/application/reads/list.rb +5 -3
- data/lib/textus/application/reads/policy_explain.rb +3 -3
- data/lib/textus/application/reads/published.rb +5 -3
- data/lib/textus/application/reads/rdeps.rb +15 -3
- data/lib/textus/application/reads/schema_envelope.rb +6 -3
- data/lib/textus/application/reads/stale.rb +3 -3
- data/lib/textus/application/reads/uid.rb +11 -3
- data/lib/textus/application/reads/validate_all.rb +12 -3
- data/lib/textus/application/reads/validator.rb +84 -0
- data/lib/textus/application/reads/where.rb +6 -3
- data/lib/textus/application/refresh/all.rb +16 -5
- data/lib/textus/application/refresh/orchestrator.rb +9 -9
- data/lib/textus/application/refresh/worker.rb +59 -32
- data/lib/textus/application/tools/migrate_keys.rb +191 -0
- data/lib/textus/application/tools/migrate_manifest_to_kinds.rb +31 -0
- data/lib/textus/application/writes/accept.rb +36 -13
- data/lib/textus/application/writes/delete.rb +13 -15
- data/lib/textus/application/writes/envelope_io.rb +166 -0
- data/lib/textus/application/writes/materializer.rb +50 -0
- data/lib/textus/application/writes/mv.rb +56 -95
- data/lib/textus/application/writes/publish.rb +132 -27
- data/lib/textus/application/writes/put.rb +17 -20
- data/lib/textus/application/writes/reject.rb +18 -9
- data/lib/textus/builder/pipeline.rb +21 -15
- data/lib/textus/builder/renderer/json.rb +4 -1
- data/lib/textus/builder/renderer/markdown.rb +7 -1
- data/lib/textus/builder/renderer/yaml.rb +4 -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 -5
- 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 +4 -2
- data/lib/textus/cli/verb/hook_run.rb +6 -4
- data/lib/textus/cli/verb/hooks.rb +8 -5
- 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 +35 -3
- 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 +5 -4
- 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 -2
- 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/handler_allowlist.rb +3 -2
- data/lib/textus/doctor/check/hooks.rb +4 -2
- data/lib/textus/doctor/check/illegal_keys.rb +6 -5
- data/lib/textus/doctor/check/intake_registration.rb +5 -5
- data/lib/textus/doctor/check/manifest_files.rb +1 -1
- data/lib/textus/doctor/check/protocol_version.rb +2 -2
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor/check/sentinels.rb +2 -2
- data/lib/textus/doctor/check/templates.rb +4 -3
- data/lib/textus/doctor.rb +3 -4
- data/lib/textus/domain/authorizer.rb +37 -0
- 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/{store → domain}/sentinel.rb +1 -1
- data/lib/textus/{store → domain}/staleness/generator_check.rb +9 -8
- data/lib/textus/{store → domain}/staleness/intake_check.rb +3 -3
- 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/bus.rb +155 -0
- data/lib/textus/hooks/context.rb +38 -0
- data/lib/textus/hooks/fire_report.rb +23 -0
- data/lib/textus/hooks/loader.rb +20 -17
- data/lib/textus/{store → infra}/audit_log.rb +1 -1
- data/lib/textus/infra/audit_subscriber.rb +43 -0
- data/lib/textus/infra/event_bus.rb +3 -3
- data/lib/textus/infra/publisher.rb +3 -3
- data/lib/textus/infra/refresh/detached.rb +1 -1
- data/lib/textus/infra/storage/file_store.rb +26 -0
- data/lib/textus/init.rb +14 -11
- data/lib/textus/intro.rb +7 -7
- data/lib/textus/manifest/entry/base.rb +38 -0
- data/lib/textus/manifest/entry/derived.rb +25 -0
- data/lib/textus/manifest/entry/intake.rb +19 -0
- data/lib/textus/manifest/entry/leaf.rb +16 -0
- data/lib/textus/manifest/entry/nested.rb +39 -0
- data/lib/textus/manifest/entry/parser.rb +64 -31
- data/lib/textus/manifest/entry/validators/events.rb +3 -2
- data/lib/textus/manifest/entry/validators/format_matrix.rb +5 -3
- data/lib/textus/manifest/entry/validators/index_filename.rb +11 -10
- data/lib/textus/manifest/entry/validators/inject_intro.rb +5 -2
- data/lib/textus/manifest/entry/validators/publish_each.rb +9 -6
- data/lib/textus/manifest/entry.rb +0 -72
- data/lib/textus/manifest/resolution.rb +5 -0
- data/lib/textus/manifest/resolver.rb +109 -0
- data/lib/textus/manifest/schema.rb +1 -1
- data/lib/textus/manifest.rb +4 -100
- data/lib/textus/operations.rb +147 -23
- data/lib/textus/schema/tools.rb +7 -7
- 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 +31 -23
- data/lib/textus/application/writes/build.rb +0 -79
- data/lib/textus/dependencies.rb +0 -23
- data/lib/textus/domain/policy/predicates/human_accept.rb +0 -31
- data/lib/textus/hooks/dispatcher.rb +0 -63
- data/lib/textus/hooks/dsl.rb +0 -11
- data/lib/textus/hooks/registry.rb +0 -81
- data/lib/textus/migrate_keys.rb +0 -187
- 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/projection.rb +0 -89
- data/lib/textus/refresh.rb +0 -39
- data/lib/textus/store/reader.rb +0 -69
- data/lib/textus/store/validator.rb +0 -82
- data/lib/textus/store/writer.rb +0 -102
data/lib/textus/cli/verb/get.rb
CHANGED
|
@@ -2,12 +2,14 @@ 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).
|
|
10
|
-
raise Textus::UnknownKey.new(key, suggestions: store.manifest.suggestions_for(key)) if result.nil?
|
|
11
|
+
result = operations_for(store).get_or_refresh(key)
|
|
12
|
+
raise Textus::UnknownKey.new(key, suggestions: store.manifest.resolver.suggestions_for(key)) if result.nil?
|
|
11
13
|
|
|
12
14
|
emit(result.to_h_for_wire)
|
|
13
15
|
end
|
|
@@ -2,6 +2,9 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class HookRun < Verb
|
|
5
|
+
command_name "run"
|
|
6
|
+
parent_group Group::Hook
|
|
7
|
+
|
|
5
8
|
def parse(argv)
|
|
6
9
|
@raw_argv = argv
|
|
7
10
|
end
|
|
@@ -23,13 +26,12 @@ module Textus
|
|
|
23
26
|
end
|
|
24
27
|
end
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
callable = store.
|
|
28
|
-
view = Application::Context.new(store: store, role: role)
|
|
29
|
+
Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
30
|
+
callable = store.bus.rpc_callable(:resolve_intake, name)
|
|
29
31
|
|
|
30
32
|
begin
|
|
31
33
|
Timeout.timeout(Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS) do
|
|
32
|
-
callable.call(config: {}, store:
|
|
34
|
+
callable.call(config: {}, store: store, args: args)
|
|
33
35
|
end
|
|
34
36
|
rescue Timeout::Error
|
|
35
37
|
raise UsageError.new(
|
|
@@ -2,9 +2,12 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Hooks < Verb
|
|
5
|
+
command_name "list"
|
|
6
|
+
parent_group Group::Hook
|
|
7
|
+
|
|
5
8
|
option :event_filter, "--event=E"
|
|
6
9
|
|
|
7
|
-
def call(store) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
10
|
+
def call(store) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
8
11
|
subcommand = positional.first
|
|
9
12
|
if subcommand
|
|
10
13
|
raise UsageError.new("hook requires 'list'") unless subcommand == "list"
|
|
@@ -13,15 +16,15 @@ module Textus
|
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
rows = []
|
|
16
|
-
Textus::Hooks::
|
|
19
|
+
Textus::Hooks::Bus::EVENTS.each do |event, spec|
|
|
17
20
|
mode = spec[:mode].to_s
|
|
18
21
|
case spec[:mode]
|
|
19
22
|
when :rpc
|
|
20
|
-
store.
|
|
23
|
+
store.bus.rpc_names(event).each do |name|
|
|
21
24
|
rows << { "event" => event.to_s, "mode" => mode, "name" => name.to_s }
|
|
22
25
|
end
|
|
23
26
|
when :pubsub
|
|
24
|
-
store.
|
|
27
|
+
store.bus.pubsub_handlers(event).each do |h|
|
|
25
28
|
row = { "event" => event.to_s, "mode" => mode, "name" => h[:name].to_s }
|
|
26
29
|
row["keys"] = Array(h[:keys]) if h[:keys]
|
|
27
30
|
rows << row
|
|
@@ -29,7 +32,7 @@ module Textus
|
|
|
29
32
|
end
|
|
30
33
|
end
|
|
31
34
|
store.manifest.entries.each do |e|
|
|
32
|
-
e.events.each do |evt, defs|
|
|
35
|
+
(e.respond_to?(:events) ? e.events : {}).each do |evt, defs|
|
|
33
36
|
Array(defs).each do |defn|
|
|
34
37
|
next unless defn["exec"]
|
|
35
38
|
|
data/lib/textus/cli/verb/init.rb
CHANGED
|
@@ -2,13 +2,45 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class KeyNormalize < Verb
|
|
5
|
+
command_name "normalize"
|
|
6
|
+
parent_group Group::Key
|
|
7
|
+
|
|
5
8
|
option :write, "--write"
|
|
6
9
|
option :dry_run, "--dry-run"
|
|
10
|
+
option :upgrade_manifest, "--upgrade-manifest"
|
|
7
11
|
|
|
8
12
|
def call(store)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
if upgrade_manifest
|
|
14
|
+
run_upgrade_manifest(store)
|
|
15
|
+
else
|
|
16
|
+
effective_write = write && !dry_run
|
|
17
|
+
res = Textus::Application::Tools::MigrateKeys.run(store, write: effective_write || false)
|
|
18
|
+
emit(res, exit_code: res["ok"] ? 0 : 1)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def run_upgrade_manifest(store)
|
|
25
|
+
manifest_path = File.join(store.root, "manifest.yaml")
|
|
26
|
+
orig = File.read(manifest_path)
|
|
27
|
+
new_yaml = Textus::Application::Tools::MigrateManifestToKinds.upgrade_yaml(orig)
|
|
28
|
+
|
|
29
|
+
if dry_run
|
|
30
|
+
diff_lines = unified_diff(orig, new_yaml, manifest_path)
|
|
31
|
+
emit({ "protocol" => PROTOCOL, "dry_run" => true, "diff" => diff_lines, "ok" => true }, exit_code: 0)
|
|
32
|
+
else
|
|
33
|
+
File.write(manifest_path, new_yaml)
|
|
34
|
+
puts "upgraded manifest at #{manifest_path}"
|
|
35
|
+
emit({ "protocol" => PROTOCOL, "upgraded" => manifest_path, "ok" => true }, exit_code: 0)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def unified_diff(before, after, _path)
|
|
40
|
+
before.lines.zip(after.lines).each_with_object([]) do |(a, b), acc|
|
|
41
|
+
acc << "- #{a.chomp}" if a && a != b
|
|
42
|
+
acc << "+ #{b.chomp}" if b && a != b
|
|
43
|
+
end
|
|
12
44
|
end
|
|
13
45
|
end
|
|
14
46
|
end
|
data/lib/textus/cli/verb/list.rb
CHANGED
|
@@ -2,11 +2,13 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class List < Verb
|
|
5
|
+
command_name "list"
|
|
6
|
+
|
|
5
7
|
option :prefix, "--prefix=KEY"
|
|
6
8
|
option :zone, "--zone=Z"
|
|
7
9
|
|
|
8
10
|
def call(store)
|
|
9
|
-
emit({ "entries" => operations_for(store).
|
|
11
|
+
emit({ "entries" => operations_for(store).list(prefix: prefix, zone: zone) })
|
|
10
12
|
end
|
|
11
13
|
end
|
|
12
14
|
end
|
data/lib/textus/cli/verb/mv.rb
CHANGED
|
@@ -2,13 +2,16 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Mv < Verb
|
|
5
|
+
command_name "mv"
|
|
6
|
+
parent_group Group::Key
|
|
7
|
+
|
|
5
8
|
option :as_flag, "--as=ROLE"
|
|
6
9
|
option :dry_run, "--dry-run"
|
|
7
10
|
|
|
8
11
|
def call(store)
|
|
9
12
|
old_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
|
|
10
13
|
new_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
|
|
11
|
-
emit(operations_for(store).
|
|
14
|
+
emit(operations_for(store).mv(old_key, new_key, dry_run: dry_run || false))
|
|
12
15
|
end
|
|
13
16
|
end
|
|
14
17
|
end
|
data/lib/textus/cli/verb/put.rb
CHANGED
|
@@ -2,6 +2,8 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Put < Verb
|
|
5
|
+
command_name "put"
|
|
6
|
+
|
|
5
7
|
option :as_flag, "--as=ROLE"
|
|
6
8
|
option :use_stdin, "--stdin"
|
|
7
9
|
option :fetch_name, "--fetch=NAME"
|
|
@@ -15,12 +17,11 @@ module Textus
|
|
|
15
17
|
raw = @stdin.read
|
|
16
18
|
payload =
|
|
17
19
|
if fetch_name
|
|
18
|
-
callable = store.
|
|
20
|
+
callable = store.bus.rpc_callable(:resolve_intake, fetch_name)
|
|
19
21
|
result =
|
|
20
22
|
begin
|
|
21
23
|
Timeout.timeout(Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS) do
|
|
22
|
-
callable.call(config: { "bytes" => raw },
|
|
23
|
-
store: Textus::Application::Context.new(store: store, role: role), args: {})
|
|
24
|
+
callable.call(config: { "bytes" => raw }, store: store, args: {})
|
|
24
25
|
end
|
|
25
26
|
rescue Timeout::Error
|
|
26
27
|
raise UsageError.new(
|
|
@@ -43,7 +44,7 @@ module Textus
|
|
|
43
44
|
meta = payload["_meta"] || {}
|
|
44
45
|
body = payload["body"] || ""
|
|
45
46
|
if_etag = payload["if_etag"]
|
|
46
|
-
result = Textus::Operations.for(store, role: role).
|
|
47
|
+
result = Textus::Operations.for(store, role: role).put(key, meta: meta, body: body, if_etag: if_etag)
|
|
47
48
|
emit(result.to_h_for_wire)
|
|
48
49
|
end
|
|
49
50
|
end
|
|
@@ -2,9 +2,11 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Rdeps < Verb
|
|
5
|
+
command_name "rdeps"
|
|
6
|
+
|
|
5
7
|
def call(store)
|
|
6
8
|
key = positional.shift or raise UsageError.new("rdeps requires a key")
|
|
7
|
-
emit({ "key" => key, "rdeps" => operations_for(store).
|
|
9
|
+
emit({ "key" => key, "rdeps" => operations_for(store).rdeps(key) })
|
|
8
10
|
end
|
|
9
11
|
end
|
|
10
12
|
end
|
|
@@ -2,13 +2,15 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class RefreshStale < Verb
|
|
5
|
+
command_name "stale"
|
|
6
|
+
parent_group Group::Refresh
|
|
7
|
+
|
|
5
8
|
option :prefix, "--prefix=KEY"
|
|
6
9
|
option :zone, "--zone=Z"
|
|
7
10
|
option :as_flag, "--as=ROLE"
|
|
8
11
|
|
|
9
12
|
def call(store)
|
|
10
|
-
|
|
11
|
-
result = Textus::Application::Refresh::All.call(ctx, prefix: prefix, zone: zone)
|
|
13
|
+
result = operations_for(store).refresh_all(prefix: prefix, zone: zone)
|
|
12
14
|
emit(result)
|
|
13
15
|
result["ok"] ? 0 : 1
|
|
14
16
|
end
|
|
@@ -2,11 +2,13 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Reject < Verb
|
|
5
|
+
command_name "reject"
|
|
6
|
+
|
|
5
7
|
option :as_flag, "--as=ROLE"
|
|
6
8
|
|
|
7
9
|
def call(store)
|
|
8
10
|
key = positional.shift or raise UsageError.new("reject requires a key")
|
|
9
|
-
emit(operations_for(store).
|
|
11
|
+
emit(operations_for(store).reject(key))
|
|
10
12
|
end
|
|
11
13
|
end
|
|
12
14
|
end
|
|
@@ -2,9 +2,12 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class RuleExplain < Verb
|
|
5
|
+
command_name "explain"
|
|
6
|
+
parent_group Group::Rule
|
|
7
|
+
|
|
5
8
|
def call(store)
|
|
6
9
|
key = positional.shift or raise UsageError.new("policy explain requires a KEY")
|
|
7
|
-
result = operations_for(store).
|
|
10
|
+
result = operations_for(store).policy_explain(key: key)
|
|
8
11
|
emit({ "verb" => "policy_explain" }.merge(result.transform_keys(&:to_s)))
|
|
9
12
|
end
|
|
10
13
|
end
|
|
@@ -2,9 +2,12 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Schema < Verb
|
|
5
|
+
command_name "show"
|
|
6
|
+
parent_group Group::Schema
|
|
7
|
+
|
|
5
8
|
def call(store)
|
|
6
9
|
key = positional.shift or raise UsageError.new("schema requires a key")
|
|
7
|
-
emit(operations_for(store).
|
|
10
|
+
emit(operations_for(store).schema_envelope(key))
|
|
8
11
|
end
|
|
9
12
|
end
|
|
10
13
|
end
|
data/lib/textus/cli/verb/uid.rb
CHANGED
|
@@ -2,9 +2,12 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Uid < Verb
|
|
5
|
+
command_name "uid"
|
|
6
|
+
parent_group Group::Key
|
|
7
|
+
|
|
5
8
|
def call(store)
|
|
6
9
|
key = positional.shift or raise UsageError.new("uid requires a key")
|
|
7
|
-
emit({ "key" => key, "uid" => operations_for(store).
|
|
10
|
+
emit({ "key" => key, "uid" => operations_for(store).uid(key) })
|
|
8
11
|
end
|
|
9
12
|
end
|
|
10
13
|
end
|
|
@@ -2,9 +2,11 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Where < Verb
|
|
5
|
+
command_name "where"
|
|
6
|
+
|
|
5
7
|
def call(store)
|
|
6
8
|
key = positional.shift or raise UsageError.new("where requires a key")
|
|
7
|
-
emit(operations_for(store).
|
|
9
|
+
emit(operations_for(store).where(key))
|
|
8
10
|
end
|
|
9
11
|
end
|
|
10
12
|
end
|
data/lib/textus/cli/verb.rb
CHANGED
|
@@ -22,9 +22,39 @@ module Textus
|
|
|
22
22
|
true
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# Declarative CLI name. Reader returns the registered name (or nil
|
|
26
|
+
# for verbs that aren't directly invokable, like the abstract
|
|
27
|
+
# Verb/Group base classes). Writer registers it.
|
|
28
|
+
def command_name(name = nil)
|
|
29
|
+
if name.nil?
|
|
30
|
+
@command_name
|
|
31
|
+
else
|
|
32
|
+
@command_name = name.to_s
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Declares that this verb is a subcommand of `group_klass`. When
|
|
37
|
+
# set, the verb is NOT a top-level CLI verb — it's listed under
|
|
38
|
+
# the group's subcommands instead.
|
|
39
|
+
def parent_group(group_klass = nil)
|
|
40
|
+
if group_klass.nil?
|
|
41
|
+
@parent_group
|
|
42
|
+
else
|
|
43
|
+
@parent_group = group_klass
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
25
47
|
def inherited(subclass)
|
|
26
48
|
super
|
|
27
49
|
subclass.instance_variable_set(:@options, [])
|
|
50
|
+
subclass.instance_variable_set(:@command_name, nil)
|
|
51
|
+
subclass.instance_variable_set(:@parent_group, nil)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Recursive subclass enumeration. Ruby 3.1 ships Class#subclasses
|
|
55
|
+
# but not Class#descendants, so we expand it ourselves.
|
|
56
|
+
def descendants
|
|
57
|
+
subclasses.flat_map { |k| [k] + k.descendants }
|
|
28
58
|
end
|
|
29
59
|
end
|
|
30
60
|
|
data/lib/textus/cli.rb
CHANGED
|
@@ -3,32 +3,23 @@ require "optparse"
|
|
|
3
3
|
|
|
4
4
|
module Textus
|
|
5
5
|
class CLI
|
|
6
|
-
# verb
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"list" => Verb::List,
|
|
24
|
-
"published" => Verb::Published,
|
|
25
|
-
"put" => Verb::Put,
|
|
26
|
-
"rdeps" => Verb::Rdeps,
|
|
27
|
-
"refresh" => Group::Refresh,
|
|
28
|
-
"rule" => Group::Rule,
|
|
29
|
-
"schema" => Group::Schema,
|
|
30
|
-
"where" => Verb::Where,
|
|
31
|
-
}.freeze
|
|
6
|
+
# Auto-derived verb table. Every CLI::Verb (or Group) subclass that
|
|
7
|
+
# declares `command_name "X"` and has no `parent_group` is a top-level
|
|
8
|
+
# verb. Sorted alphabetically for stable help output. Adding a new
|
|
9
|
+
# verb requires only a new file declaring its `command_name`.
|
|
10
|
+
def self.verbs
|
|
11
|
+
Verb.descendants
|
|
12
|
+
.select { |k| k.command_name && k.parent_group.nil? }
|
|
13
|
+
.sort_by(&:command_name)
|
|
14
|
+
.to_h { |k| [k.command_name, k] }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Backward-compat constant; callers should prefer `CLI.verbs`.
|
|
18
|
+
def self.const_missing(name)
|
|
19
|
+
return verbs.freeze if name == :VERBS
|
|
20
|
+
|
|
21
|
+
super
|
|
22
|
+
end
|
|
32
23
|
|
|
33
24
|
def self.run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr, cwd: Dir.pwd)
|
|
34
25
|
new(stdin: stdin, stdout: stdout, stderr: stderr, cwd: cwd).run(argv)
|
|
@@ -54,7 +45,7 @@ module Textus
|
|
|
54
45
|
when "--help", "-h" then print_help
|
|
55
46
|
0
|
|
56
47
|
else
|
|
57
|
-
klass =
|
|
48
|
+
klass = self.class.verbs[verb] or raise UsageError.new("unknown verb: #{verb}")
|
|
58
49
|
dispatch(klass, argv)
|
|
59
50
|
end
|
|
60
51
|
|
|
@@ -4,7 +4,7 @@ module Textus
|
|
|
4
4
|
class AuditLog < Check
|
|
5
5
|
def call
|
|
6
6
|
path = File.join(store.root, "audit.log")
|
|
7
|
-
Textus::
|
|
7
|
+
Textus::Infra::AuditLog.new(store.root).verify_integrity.map do |v|
|
|
8
8
|
{
|
|
9
9
|
"code" => "audit.parse_error",
|
|
10
10
|
"level" => "warning",
|
|
@@ -8,8 +8,9 @@ module Textus
|
|
|
8
8
|
def call
|
|
9
9
|
out = []
|
|
10
10
|
store.manifest.entries.each do |mentry|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
next unless mentry.is_a?(Textus::Manifest::Entry::Intake)
|
|
12
|
+
|
|
13
|
+
handler = mentry.handler
|
|
13
14
|
|
|
14
15
|
allow = store.manifest.rules_for(mentry.key).handler_allowlist
|
|
15
16
|
next if allow.nil?
|
|
@@ -8,9 +8,11 @@ module Textus
|
|
|
8
8
|
return out unless File.directory?(dir)
|
|
9
9
|
|
|
10
10
|
Dir.glob(File.join(dir, "*.rb")).sort.each do |f| # rubocop:disable Lint/RedundantDirGlobSort
|
|
11
|
-
|
|
12
|
-
Textus.
|
|
11
|
+
bus = Textus::Hooks::Bus.new
|
|
12
|
+
Textus.drain_hook_blocks
|
|
13
|
+
begin
|
|
13
14
|
load(f)
|
|
15
|
+
Textus.drain_hook_blocks.each { |b| b.call(bus) }
|
|
14
16
|
end
|
|
15
17
|
rescue StandardError, ScriptError => e
|
|
16
18
|
out << {
|
|
@@ -5,12 +5,13 @@ module Textus
|
|
|
5
5
|
def call
|
|
6
6
|
out = []
|
|
7
7
|
store.manifest.entries.each do |entry|
|
|
8
|
-
next unless entry.nested
|
|
8
|
+
next unless entry.nested?
|
|
9
9
|
|
|
10
10
|
base = File.join(store.root, "zones", entry.path)
|
|
11
11
|
next unless File.directory?(base)
|
|
12
12
|
|
|
13
|
-
entry.index_filename ?
|
|
13
|
+
index_fn = entry.respond_to?(:index_filename) ? entry.index_filename : nil
|
|
14
|
+
index_fn ? check_index_paths(entry, index_fn, base, out) : check_all_paths(base, out)
|
|
14
15
|
end
|
|
15
16
|
out
|
|
16
17
|
end
|
|
@@ -31,8 +32,8 @@ module Textus
|
|
|
31
32
|
# segments leading to each index file participate in keys. Sibling
|
|
32
33
|
# files and unrelated subtrees are not enumerated and must not be
|
|
33
34
|
# flagged. Each illegal segment is reported once per path.
|
|
34
|
-
def check_index_paths(
|
|
35
|
-
Dir.glob(File.join(base, "**",
|
|
35
|
+
def check_index_paths(_entry, index_fn, base, out)
|
|
36
|
+
Dir.glob(File.join(base, "**", index_fn)).each do |fp|
|
|
36
37
|
rel = fp.sub(%r{\A#{Regexp.escape(base)}/?}, "")
|
|
37
38
|
File.dirname(rel).split("/").reject { |s| s.empty? || s == "." }.each do |seg|
|
|
38
39
|
next if seg.match?(Key::Grammar::SEGMENT)
|
|
@@ -43,7 +44,7 @@ module Textus
|
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
def issue(abs_path, stem)
|
|
46
|
-
proposed = Textus::MigrateKeys.normalize(stem)
|
|
47
|
+
proposed = Textus::Application::Tools::MigrateKeys.normalize(stem)
|
|
47
48
|
{
|
|
48
49
|
"code" => "key.illegal",
|
|
49
50
|
"level" => "error",
|
|
@@ -6,15 +6,15 @@ module Textus
|
|
|
6
6
|
|
|
7
7
|
def call
|
|
8
8
|
declared = collect_declared_handlers
|
|
9
|
-
registered = store.
|
|
9
|
+
registered = store.bus.rpc_names(:resolve_intake).to_set
|
|
10
10
|
|
|
11
11
|
out = (declared - registered).map do |name|
|
|
12
12
|
{
|
|
13
13
|
"code" => "intake.handler_missing",
|
|
14
14
|
"level" => "error",
|
|
15
15
|
"subject" => name.to_s,
|
|
16
|
-
"message" => "manifest references intake handler '#{name}' but no
|
|
17
|
-
"fix" => "create .textus/hooks/#{name}.rb with `Textus.on(:resolve_intake, :#{name}) { ... }`",
|
|
16
|
+
"message" => "manifest references intake handler '#{name}' but no resolve_intake hook for '#{name}' is registered",
|
|
17
|
+
"fix" => "create .textus/hooks/#{name}.rb with `Textus.hook { |reg| reg.on(:resolve_intake, :#{name}) { ... } }`",
|
|
18
18
|
}
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -23,7 +23,7 @@ module Textus
|
|
|
23
23
|
"code" => "intake.handler_orphan",
|
|
24
24
|
"level" => "warning",
|
|
25
25
|
"subject" => name.to_s,
|
|
26
|
-
"message" => "
|
|
26
|
+
"message" => "resolve_intake hook '#{name}' is registered but no manifest entry references it",
|
|
27
27
|
"fix" => "remove the unused handler, or add an entry with `intake.handler: #{name}`",
|
|
28
28
|
}
|
|
29
29
|
end
|
|
@@ -36,7 +36,7 @@ module Textus
|
|
|
36
36
|
def collect_declared_handlers
|
|
37
37
|
set = Set.new
|
|
38
38
|
store.manifest.entries.each do |mentry|
|
|
39
|
-
set << mentry.
|
|
39
|
+
set << mentry.handler.to_sym if mentry.is_a?(Textus::Manifest::Entry::Intake)
|
|
40
40
|
end
|
|
41
41
|
set
|
|
42
42
|
end
|
|
@@ -19,7 +19,7 @@ module Textus
|
|
|
19
19
|
"code" => "protocol_mismatch",
|
|
20
20
|
"severity" => "error",
|
|
21
21
|
"message" => "Store reports version=#{version.inspect}; this gem expects textus/3.",
|
|
22
|
-
"hint" => "
|
|
22
|
+
"hint" => "Upgrade the store's manifest version to textus/3 (see CHANGELOG for breaking changes).",
|
|
23
23
|
}]
|
|
24
24
|
end
|
|
25
25
|
|
|
@@ -38,7 +38,7 @@ module Textus
|
|
|
38
38
|
"level" => "error",
|
|
39
39
|
"subject" => path,
|
|
40
40
|
"message" => "Store reports version=#{version.inspect}; this gem expects textus/3.",
|
|
41
|
-
"fix" => "
|
|
41
|
+
"fix" => "Upgrade the store's manifest version to textus/3 (see CHANGELOG for breaking changes).",
|
|
42
42
|
}]
|
|
43
43
|
end
|
|
44
44
|
end
|
|
@@ -3,7 +3,7 @@ module Textus
|
|
|
3
3
|
class Check
|
|
4
4
|
class SchemaViolations < Check
|
|
5
5
|
def call
|
|
6
|
-
res = Textus::Operations.for(store).
|
|
6
|
+
res = Textus::Operations.for(store).validate_all
|
|
7
7
|
res["violations"].map do |v|
|
|
8
8
|
fix = v["expected"] &&
|
|
9
9
|
"field '#{v["field"]}' should be written by '#{v["expected"]}' (last writer: #{v["last_writer"]})"
|