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.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +50 -55
  3. data/CHANGELOG.md +486 -0
  4. data/README.md +13 -9
  5. data/SPEC.md +13 -10
  6. data/docs/conventions.md +2 -2
  7. data/lib/textus/application/context.rb +20 -34
  8. data/lib/textus/application/policy/predicates/human_accept.rb +30 -0
  9. data/lib/textus/{domain → application}/policy/predicates/schema_valid.rb +5 -5
  10. data/lib/textus/{domain → application}/policy/promotion.rb +20 -3
  11. data/lib/textus/application/projection.rb +91 -0
  12. data/lib/textus/application/reads/audit.rb +4 -4
  13. data/lib/textus/application/reads/blame.rb +11 -8
  14. data/lib/textus/application/reads/deps.rb +14 -3
  15. data/lib/textus/application/reads/freshness.rb +17 -6
  16. data/lib/textus/application/reads/get.rb +37 -11
  17. data/lib/textus/application/reads/get_or_refresh.rb +8 -8
  18. data/lib/textus/application/reads/list.rb +5 -3
  19. data/lib/textus/application/reads/policy_explain.rb +3 -3
  20. data/lib/textus/application/reads/published.rb +5 -3
  21. data/lib/textus/application/reads/rdeps.rb +15 -3
  22. data/lib/textus/application/reads/schema_envelope.rb +6 -3
  23. data/lib/textus/application/reads/stale.rb +3 -3
  24. data/lib/textus/application/reads/uid.rb +11 -3
  25. data/lib/textus/application/reads/validate_all.rb +12 -3
  26. data/lib/textus/application/reads/validator.rb +84 -0
  27. data/lib/textus/application/reads/where.rb +6 -3
  28. data/lib/textus/application/refresh/all.rb +16 -5
  29. data/lib/textus/application/refresh/orchestrator.rb +9 -9
  30. data/lib/textus/application/refresh/worker.rb +59 -32
  31. data/lib/textus/application/tools/migrate_keys.rb +191 -0
  32. data/lib/textus/application/tools/migrate_manifest_to_kinds.rb +31 -0
  33. data/lib/textus/application/writes/accept.rb +36 -13
  34. data/lib/textus/application/writes/delete.rb +13 -15
  35. data/lib/textus/application/writes/envelope_io.rb +166 -0
  36. data/lib/textus/application/writes/materializer.rb +50 -0
  37. data/lib/textus/application/writes/mv.rb +56 -95
  38. data/lib/textus/application/writes/publish.rb +132 -27
  39. data/lib/textus/application/writes/put.rb +17 -20
  40. data/lib/textus/application/writes/reject.rb +18 -9
  41. data/lib/textus/builder/pipeline.rb +21 -15
  42. data/lib/textus/builder/renderer/json.rb +4 -1
  43. data/lib/textus/builder/renderer/markdown.rb +7 -1
  44. data/lib/textus/builder/renderer/yaml.rb +4 -1
  45. data/lib/textus/cli/group/hook.rb +1 -3
  46. data/lib/textus/cli/group/key.rb +1 -4
  47. data/lib/textus/cli/group/refresh.rb +1 -2
  48. data/lib/textus/cli/group/rule.rb +1 -3
  49. data/lib/textus/cli/group/schema.rb +1 -5
  50. data/lib/textus/cli/group.rb +12 -16
  51. data/lib/textus/cli/verb/accept.rb +3 -1
  52. data/lib/textus/cli/verb/audit.rb +3 -1
  53. data/lib/textus/cli/verb/blame.rb +3 -1
  54. data/lib/textus/cli/verb/build.rb +4 -5
  55. data/lib/textus/cli/verb/delete.rb +3 -1
  56. data/lib/textus/cli/verb/deps.rb +3 -1
  57. data/lib/textus/cli/verb/doctor.rb +2 -0
  58. data/lib/textus/cli/verb/freshness.rb +3 -1
  59. data/lib/textus/cli/verb/get.rb +4 -2
  60. data/lib/textus/cli/verb/hook_run.rb +6 -4
  61. data/lib/textus/cli/verb/hooks.rb +8 -5
  62. data/lib/textus/cli/verb/init.rb +2 -0
  63. data/lib/textus/cli/verb/intro.rb +2 -0
  64. data/lib/textus/cli/verb/key_normalize.rb +35 -3
  65. data/lib/textus/cli/verb/list.rb +3 -1
  66. data/lib/textus/cli/verb/mv.rb +4 -1
  67. data/lib/textus/cli/verb/published.rb +3 -1
  68. data/lib/textus/cli/verb/put.rb +5 -4
  69. data/lib/textus/cli/verb/rdeps.rb +3 -1
  70. data/lib/textus/cli/verb/refresh.rb +1 -1
  71. data/lib/textus/cli/verb/refresh_stale.rb +4 -2
  72. data/lib/textus/cli/verb/reject.rb +3 -1
  73. data/lib/textus/cli/verb/rule_explain.rb +4 -1
  74. data/lib/textus/cli/verb/rule_list.rb +3 -0
  75. data/lib/textus/cli/verb/schema.rb +4 -1
  76. data/lib/textus/cli/verb/schema_diff.rb +3 -0
  77. data/lib/textus/cli/verb/schema_init.rb +3 -0
  78. data/lib/textus/cli/verb/schema_migrate.rb +3 -0
  79. data/lib/textus/cli/verb/uid.rb +4 -1
  80. data/lib/textus/cli/verb/where.rb +3 -1
  81. data/lib/textus/cli/verb.rb +30 -0
  82. data/lib/textus/cli.rb +18 -27
  83. data/lib/textus/doctor/check/audit_log.rb +1 -1
  84. data/lib/textus/doctor/check/handler_allowlist.rb +3 -2
  85. data/lib/textus/doctor/check/hooks.rb +4 -2
  86. data/lib/textus/doctor/check/illegal_keys.rb +6 -5
  87. data/lib/textus/doctor/check/intake_registration.rb +5 -5
  88. data/lib/textus/doctor/check/manifest_files.rb +1 -1
  89. data/lib/textus/doctor/check/protocol_version.rb +2 -2
  90. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  91. data/lib/textus/doctor/check/sentinels.rb +2 -2
  92. data/lib/textus/doctor/check/templates.rb +4 -3
  93. data/lib/textus/doctor.rb +3 -4
  94. data/lib/textus/domain/authorizer.rb +37 -0
  95. data/lib/textus/domain/freshness/evaluator.rb +1 -1
  96. data/lib/textus/domain/freshness/policy.rb +1 -1
  97. data/lib/textus/domain/freshness/verdict.rb +1 -1
  98. data/lib/textus/domain/freshness.rb +40 -0
  99. data/lib/textus/{store → domain}/sentinel.rb +1 -1
  100. data/lib/textus/{store → domain}/staleness/generator_check.rb +9 -8
  101. data/lib/textus/{store → domain}/staleness/intake_check.rb +3 -3
  102. data/lib/textus/{store → domain}/staleness.rb +1 -1
  103. data/lib/textus/entry/json.rb +1 -1
  104. data/lib/textus/entry/markdown.rb +1 -1
  105. data/lib/textus/entry/yaml.rb +1 -1
  106. data/lib/textus/envelope.rb +7 -3
  107. data/lib/textus/errors.rb +19 -0
  108. data/lib/textus/hooks/builtin.rb +6 -6
  109. data/lib/textus/hooks/bus.rb +155 -0
  110. data/lib/textus/hooks/context.rb +38 -0
  111. data/lib/textus/hooks/fire_report.rb +23 -0
  112. data/lib/textus/hooks/loader.rb +20 -17
  113. data/lib/textus/{store → infra}/audit_log.rb +1 -1
  114. data/lib/textus/infra/audit_subscriber.rb +43 -0
  115. data/lib/textus/infra/event_bus.rb +3 -3
  116. data/lib/textus/infra/publisher.rb +3 -3
  117. data/lib/textus/infra/refresh/detached.rb +1 -1
  118. data/lib/textus/infra/storage/file_store.rb +26 -0
  119. data/lib/textus/init.rb +14 -11
  120. data/lib/textus/intro.rb +7 -7
  121. data/lib/textus/manifest/entry/base.rb +38 -0
  122. data/lib/textus/manifest/entry/derived.rb +25 -0
  123. data/lib/textus/manifest/entry/intake.rb +19 -0
  124. data/lib/textus/manifest/entry/leaf.rb +16 -0
  125. data/lib/textus/manifest/entry/nested.rb +39 -0
  126. data/lib/textus/manifest/entry/parser.rb +64 -31
  127. data/lib/textus/manifest/entry/validators/events.rb +3 -2
  128. data/lib/textus/manifest/entry/validators/format_matrix.rb +5 -3
  129. data/lib/textus/manifest/entry/validators/index_filename.rb +11 -10
  130. data/lib/textus/manifest/entry/validators/inject_intro.rb +5 -2
  131. data/lib/textus/manifest/entry/validators/publish_each.rb +9 -6
  132. data/lib/textus/manifest/entry.rb +0 -72
  133. data/lib/textus/manifest/resolution.rb +5 -0
  134. data/lib/textus/manifest/resolver.rb +109 -0
  135. data/lib/textus/manifest/schema.rb +1 -1
  136. data/lib/textus/manifest.rb +4 -100
  137. data/lib/textus/operations.rb +147 -23
  138. data/lib/textus/schema/tools.rb +7 -7
  139. data/lib/textus/schemas.rb +46 -0
  140. data/lib/textus/store.rb +12 -49
  141. data/lib/textus/uid.rb +18 -0
  142. data/lib/textus/version.rb +1 -1
  143. data/lib/textus.rb +17 -1
  144. metadata +31 -23
  145. data/lib/textus/application/writes/build.rb +0 -79
  146. data/lib/textus/dependencies.rb +0 -23
  147. data/lib/textus/domain/policy/predicates/human_accept.rb +0 -31
  148. data/lib/textus/hooks/dispatcher.rb +0 -63
  149. data/lib/textus/hooks/dsl.rb +0 -11
  150. data/lib/textus/hooks/registry.rb +0 -81
  151. data/lib/textus/migrate_keys.rb +0 -187
  152. data/lib/textus/operations/reads.rb +0 -56
  153. data/lib/textus/operations/refresh.rb +0 -27
  154. data/lib/textus/operations/writes.rb +0 -21
  155. data/lib/textus/projection.rb +0 -89
  156. data/lib/textus/refresh.rb +0 -39
  157. data/lib/textus/store/reader.rb +0 -69
  158. data/lib/textus/store/validator.rb +0 -82
  159. data/lib/textus/store/writer.rb +0 -102
@@ -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).reads.get_or_refresh.call(key)
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
- role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
27
- callable = store.registry.rpc_callable(:resolve_intake, name)
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: view, args: args)
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::Registry::EVENTS.each do |event, spec|
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.registry.rpc_names(event).each do |name|
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.registry.pubsub_handlers(event).each do |h|
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
 
@@ -2,6 +2,8 @@ module Textus
2
2
  class CLI
3
3
  class Verb
4
4
  class Init < Verb
5
+ command_name "init"
6
+
5
7
  def self.needs_store? = false
6
8
 
7
9
  def call(_store)
@@ -2,6 +2,8 @@ module Textus
2
2
  class CLI
3
3
  class Verb
4
4
  class Intro < Verb
5
+ command_name "intro"
6
+
5
7
  def call(store)
6
8
  emit(Textus::Intro.run(store))
7
9
  end
@@ -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
- effective_write = write && !dry_run
10
- res = Textus::MigrateKeys.run(store, write: effective_write || false)
11
- emit(res, exit_code: res["ok"] ? 0 : 1)
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
@@ -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).reads.list.call(prefix: prefix, zone: zone) })
11
+ emit({ "entries" => operations_for(store).list(prefix: prefix, zone: zone) })
10
12
  end
11
13
  end
12
14
  end
@@ -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).writes.mv.call(old_key, new_key, dry_run: dry_run || false))
14
+ emit(operations_for(store).mv(old_key, new_key, dry_run: dry_run || false))
12
15
  end
13
16
  end
14
17
  end
@@ -2,8 +2,10 @@ module Textus
2
2
  class CLI
3
3
  class Verb
4
4
  class Published < Verb
5
+ command_name "published"
6
+
5
7
  def call(store)
6
- emit({ "published" => operations_for(store).reads.published.call })
8
+ emit({ "published" => operations_for(store).published })
7
9
  end
8
10
  end
9
11
  end
@@ -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.registry.rpc_callable(:resolve_intake, fetch_name)
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).writes.put.call(key, meta: meta, body: body, if_etag: if_etag)
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).reads.rdeps.call(key) })
9
+ emit({ "key" => key, "rdeps" => operations_for(store).rdeps(key) })
8
10
  end
9
11
  end
10
12
  end
@@ -6,7 +6,7 @@ module Textus
6
6
 
7
7
  def call(store)
8
8
  key = positional.shift or raise UsageError.new("refresh requires a key")
9
- emit(operations_for(store).refresh.worker.run(key).to_h_for_wire)
9
+ emit(operations_for(store).refresh(key).to_h_for_wire)
10
10
  end
11
11
  end
12
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
- ctx = context_for(store)
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).writes.reject.call(key))
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).reads.policy_explain.call(key: key)
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,6 +2,9 @@ module Textus
2
2
  class CLI
3
3
  class Verb
4
4
  class RuleList < Verb
5
+ command_name "list"
6
+ parent_group Group::Rule
7
+
5
8
  def call(store)
6
9
  policies = store.manifest.rules.blocks.map do |b|
7
10
  row = { "match" => b.match }
@@ -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).reads.schema_envelope.call(key))
10
+ emit(operations_for(store).schema_envelope(key))
8
11
  end
9
12
  end
10
13
  end
@@ -2,6 +2,9 @@ module Textus
2
2
  class CLI
3
3
  class Verb
4
4
  class SchemaDiff < Verb
5
+ command_name "diff"
6
+ parent_group Group::Schema
7
+
5
8
  def call(store)
6
9
  name = positional.shift or raise UsageError.new("schema diff NAME")
7
10
  emit(Textus::Schema::Tools.diff(store, name: name))
@@ -2,6 +2,9 @@ module Textus
2
2
  class CLI
3
3
  class Verb
4
4
  class SchemaInit < Verb
5
+ command_name "init"
6
+ parent_group Group::Schema
7
+
5
8
  option :from_key, "--from=KEY"
6
9
 
7
10
  def call(store)
@@ -2,6 +2,9 @@ module Textus
2
2
  class CLI
3
3
  class Verb
4
4
  class SchemaMigrate < Verb
5
+ command_name "migrate"
6
+ parent_group Group::Schema
7
+
5
8
  option :rename, "--rename=O:N"
6
9
 
7
10
  def call(store)
@@ -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).reads.uid.call(key) })
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).reads.where.call(key))
9
+ emit(operations_for(store).where(key))
8
10
  end
9
11
  end
10
12
  end
@@ -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 name Verb subclass. Adding a new verb is a one-line entry here
7
- # plus a new file under lib/textus/cli/.
8
- VERBS = {
9
- "accept" => Verb::Accept,
10
- "audit" => Verb::Audit,
11
- "blame" => Verb::Blame,
12
- "reject" => Verb::Reject,
13
- "build" => Verb::Build,
14
- "delete" => Verb::Delete,
15
- "deps" => Verb::Deps,
16
- "doctor" => Verb::Doctor,
17
- "freshness" => Verb::Freshness,
18
- "get" => Verb::Get,
19
- "hook" => Group::Hook,
20
- "init" => Verb::Init,
21
- "intro" => Verb::Intro,
22
- "key" => Group::Key,
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 = VERBS[verb] or raise UsageError.new("unknown verb: #{verb}")
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::Store::AuditLog.new(store.root).verify_integrity.map do |v|
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
- handler = mentry.intake_handler
12
- next if handler.nil?
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
- registry = Textus::Hooks::Registry.new
12
- Textus.with_registry(registry) do
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 ? check_index_paths(entry, base, out) : check_all_paths(base, out)
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(entry, base, out)
35
- Dir.glob(File.join(base, "**", entry.index_filename)).each do |fp|
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.registry.rpc_names(:resolve_intake).to_set
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 Textus.on(:resolve_intake, :#{name}) is registered",
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" => "Textus.on(:resolve_intake, :#{name}) is registered but no manifest entry references it",
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.intake_handler.to_sym if mentry.intake_handler
39
+ set << mentry.handler.to_sym if mentry.is_a?(Textus::Manifest::Entry::Intake)
40
40
  end
41
41
  set
42
42
  end
@@ -4,7 +4,7 @@ module Textus
4
4
  class ManifestFiles < Check
5
5
  def call
6
6
  store.manifest.entries.each_with_object([]) do |entry, out|
7
- next if entry.nested
7
+ next if entry.nested?
8
8
 
9
9
  path = Textus::Key::Path.resolve(store.manifest, entry)
10
10
  next if File.exist?(path)
@@ -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" => "Install textus 0.11.x to run the migrator, then upgrade to this version. See https://github.com/patrick204nqh/textus/blob/main/CHANGELOG.md#0110",
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" => "Install textus 0.11.x to run the migrator, then upgrade to this version. See https://github.com/patrick204nqh/textus/blob/main/CHANGELOG.md#0110",
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).reads.validate_all.call
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"]})"