textus 0.18.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +43 -48
  3. data/CHANGELOG.md +173 -0
  4. data/lib/textus/application/context.rb +20 -58
  5. data/lib/textus/application/policy/predicates/human_accept.rb +30 -0
  6. data/lib/textus/{domain → application}/policy/predicates/schema_valid.rb +5 -5
  7. data/lib/textus/{domain → application}/policy/promotion.rb +20 -3
  8. data/lib/textus/application/projection.rb +91 -0
  9. data/lib/textus/application/reads/audit.rb +4 -4
  10. data/lib/textus/application/reads/blame.rb +9 -8
  11. data/lib/textus/application/reads/deps.rb +14 -3
  12. data/lib/textus/application/reads/freshness.rb +10 -8
  13. data/lib/textus/application/reads/get.rb +10 -8
  14. data/lib/textus/application/reads/get_or_refresh.rb +3 -3
  15. data/lib/textus/application/reads/list.rb +3 -3
  16. data/lib/textus/application/reads/policy_explain.rb +3 -3
  17. data/lib/textus/application/reads/published.rb +5 -3
  18. data/lib/textus/application/reads/rdeps.rb +15 -3
  19. data/lib/textus/application/reads/schema_envelope.rb +5 -4
  20. data/lib/textus/application/reads/stale.rb +3 -3
  21. data/lib/textus/application/reads/uid.rb +11 -3
  22. data/lib/textus/application/reads/validate_all.rb +10 -6
  23. data/lib/textus/application/reads/validator.rb +2 -2
  24. data/lib/textus/application/reads/where.rb +3 -3
  25. data/lib/textus/application/refresh/all.rb +15 -11
  26. data/lib/textus/application/refresh/orchestrator.rb +9 -8
  27. data/lib/textus/application/refresh/worker.rb +56 -32
  28. data/lib/textus/application/tools/migrate_keys.rb +191 -0
  29. data/lib/textus/application/tools/migrate_manifest_to_kinds.rb +31 -0
  30. data/lib/textus/application/writes/accept.rb +38 -15
  31. data/lib/textus/application/writes/delete.rb +13 -10
  32. data/lib/textus/application/writes/envelope_io.rb +64 -4
  33. data/lib/textus/application/writes/materializer.rb +50 -0
  34. data/lib/textus/application/writes/mv.rb +57 -94
  35. data/lib/textus/application/writes/publish.rb +132 -26
  36. data/lib/textus/application/writes/put.rb +15 -14
  37. data/lib/textus/application/writes/reject.rb +20 -11
  38. data/lib/textus/builder/pipeline.rb +21 -15
  39. data/lib/textus/builder/renderer/json.rb +4 -1
  40. data/lib/textus/builder/renderer/markdown.rb +7 -1
  41. data/lib/textus/builder/renderer/yaml.rb +4 -1
  42. data/lib/textus/cli/verb/build.rb +2 -5
  43. data/lib/textus/cli/verb/get.rb +1 -1
  44. data/lib/textus/cli/verb/hook_run.rb +3 -4
  45. data/lib/textus/cli/verb/hooks.rb +5 -5
  46. data/lib/textus/cli/verb/key_normalize.rb +32 -3
  47. data/lib/textus/cli/verb/put.rb +2 -3
  48. data/lib/textus/cli/verb/refresh_stale.rb +1 -2
  49. data/lib/textus/doctor/check/handler_allowlist.rb +3 -2
  50. data/lib/textus/doctor/check/hooks.rb +2 -2
  51. data/lib/textus/doctor/check/illegal_keys.rb +6 -5
  52. data/lib/textus/doctor/check/intake_registration.rb +2 -2
  53. data/lib/textus/doctor/check/manifest_files.rb +1 -1
  54. data/lib/textus/doctor/check/protocol_version.rb +2 -2
  55. data/lib/textus/doctor/check/templates.rb +4 -3
  56. data/lib/textus/doctor.rb +3 -4
  57. data/lib/textus/domain/authorizer.rb +37 -0
  58. data/lib/textus/domain/staleness/generator_check.rb +8 -7
  59. data/lib/textus/domain/staleness/intake_check.rb +2 -2
  60. data/lib/textus/hooks/builtin.rb +6 -6
  61. data/lib/textus/hooks/bus.rb +155 -0
  62. data/lib/textus/hooks/context.rb +38 -0
  63. data/lib/textus/hooks/fire_report.rb +23 -0
  64. data/lib/textus/hooks/loader.rb +3 -3
  65. data/lib/textus/infra/audit_subscriber.rb +4 -4
  66. data/lib/textus/infra/event_bus.rb +3 -3
  67. data/lib/textus/infra/refresh/detached.rb +1 -1
  68. data/lib/textus/init.rb +3 -2
  69. data/lib/textus/intro.rb +7 -7
  70. data/lib/textus/manifest/entry/base.rb +38 -0
  71. data/lib/textus/manifest/entry/derived.rb +25 -0
  72. data/lib/textus/manifest/entry/intake.rb +19 -0
  73. data/lib/textus/manifest/entry/leaf.rb +16 -0
  74. data/lib/textus/manifest/entry/nested.rb +39 -0
  75. data/lib/textus/manifest/entry/parser.rb +64 -31
  76. data/lib/textus/manifest/entry/validators/events.rb +3 -2
  77. data/lib/textus/manifest/entry/validators/format_matrix.rb +5 -3
  78. data/lib/textus/manifest/entry/validators/index_filename.rb +11 -10
  79. data/lib/textus/manifest/entry/validators/inject_intro.rb +5 -2
  80. data/lib/textus/manifest/entry/validators/publish_each.rb +9 -6
  81. data/lib/textus/manifest/entry.rb +0 -72
  82. data/lib/textus/manifest/resolver.rb +109 -0
  83. data/lib/textus/manifest/schema.rb +1 -1
  84. data/lib/textus/manifest.rb +3 -100
  85. data/lib/textus/operations.rb +131 -74
  86. data/lib/textus/schema/tools.rb +2 -2
  87. data/lib/textus/store.rb +6 -6
  88. data/lib/textus/version.rb +1 -1
  89. metadata +18 -11
  90. data/lib/textus/application/writes/build.rb +0 -78
  91. data/lib/textus/dependencies.rb +0 -23
  92. data/lib/textus/domain/policy/predicates/human_accept.rb +0 -31
  93. data/lib/textus/hooks/dispatcher.rb +0 -71
  94. data/lib/textus/hooks/registry.rb +0 -85
  95. data/lib/textus/migrate_keys.rb +0 -187
  96. data/lib/textus/projection.rb +0 -89
  97. data/lib/textus/refresh.rb +0 -39
@@ -8,12 +8,13 @@ module Textus
8
8
  # row. Falls back to `git => nil` when not in a git repo or when the
9
9
  # file is untracked.
10
10
  class Blame
11
- def initialize(ctx:)
12
- @ctx = ctx
11
+ def initialize(manifest:, root:)
12
+ @manifest = manifest
13
+ @root = root
13
14
  end
14
15
 
15
16
  def call(key:, limit: nil)
16
- audit_rows = Textus::Application::Reads::Audit.new(ctx: @ctx).call(key: key, limit: limit)
17
+ audit_rows = Textus::Application::Reads::Audit.new(manifest: @manifest, root: @root).call(key: key, limit: limit)
17
18
  path = resolve_path(key)
18
19
  return audit_rows.map { |r| r.merge("git" => nil) } unless git_tracked?(path)
19
20
 
@@ -23,13 +24,13 @@ module Textus
23
24
  private
24
25
 
25
26
  def resolve_path(key)
26
- res = @ctx.store.manifest.resolve(key)
27
+ res = @manifest.resolver.resolve(key)
27
28
  mentry = res.entry
28
29
  path = res.path
29
30
  # Nested entries resolve to a file under the entry path; leaf entries
30
31
  # already have a fully-resolved path. Either way `path` is what git
31
32
  # needs to know about.
32
- path || Textus::Key::Path.resolve(@ctx.store.manifest, mentry)
33
+ path || Textus::Key::Path.resolve(@manifest, mentry)
33
34
  rescue Textus::Error
34
35
  nil
35
36
  end
@@ -41,7 +42,7 @@ module Textus
41
42
 
42
43
  _out, _err, status = Open3.capture3(
43
44
  "git", "ls-files", "--error-unmatch", path,
44
- chdir: @ctx.store.root
45
+ chdir: @root
45
46
  )
46
47
  status.success?
47
48
  rescue Errno::ENOENT
@@ -50,7 +51,7 @@ module Textus
50
51
 
51
52
  def git_repo?
52
53
  # Walk up from store root to find a .git directory.
53
- dir = @ctx.store.root
54
+ dir = @root
54
55
  loop do
55
56
  return true if File.directory?(File.join(dir, ".git"))
56
57
 
@@ -65,7 +66,7 @@ module Textus
65
66
  args = ["git", "log", "-1"]
66
67
  args << "--before=#{timestamp}" if timestamp
67
68
  args += ["--format=%H%x09%an%x09%aI%x09%s", "--", path]
68
- out, _err, status = Open3.capture3(*args, chdir: @ctx.store.root)
69
+ out, _err, status = Open3.capture3(*args, chdir: @root)
69
70
  return nil unless status.success?
70
71
 
71
72
  sha, author, date, subject = out.strip.split("\t", 4)
@@ -2,12 +2,23 @@ module Textus
2
2
  module Application
3
3
  module Reads
4
4
  class Deps
5
- def initialize(ctx:)
6
- @ctx = ctx
5
+ def initialize(manifest:)
6
+ @manifest = manifest
7
7
  end
8
8
 
9
9
  def call(key)
10
- Dependencies.deps_of(@ctx.manifest, key)
10
+ entry = @manifest.entries.find { |e| e.key == key } or return []
11
+ return [] unless entry.is_a?(Textus::Manifest::Entry::Derived)
12
+
13
+ src = entry.source
14
+ result = if src.is_a?(Textus::Manifest::Entry::Derived::Projection)
15
+ Array(src.select).compact
16
+ elsif src.is_a?(Textus::Manifest::Entry::Derived::External)
17
+ Array(src.sources).compact
18
+ else
19
+ []
20
+ end
21
+ result.uniq
11
22
  end
12
23
  end
13
24
  end
@@ -8,14 +8,16 @@ module Textus
8
8
  # current status. Status is one of :fresh, :stale, :never_refreshed, or
9
9
  # :no_policy.
10
10
  class Freshness
11
- def initialize(ctx:, evaluator: Textus::Domain::Freshness::Evaluator)
12
- @ctx = ctx
13
- @evaluator = evaluator
11
+ def initialize(ctx:, manifest:, file_store:, evaluator: Textus::Domain::Freshness::Evaluator)
12
+ @ctx = ctx
13
+ @manifest = manifest
14
+ @file_store = file_store
15
+ @evaluator = evaluator
14
16
  end
15
17
 
16
18
  def call(prefix: nil, zone: nil)
17
19
  rows = []
18
- @ctx.manifest.entries.each do |mentry|
20
+ @manifest.entries.each do |mentry|
19
21
  next if prefix && !mentry.key.start_with?(prefix)
20
22
  next if zone && mentry.zone != zone
21
23
 
@@ -27,7 +29,7 @@ module Textus
27
29
  private
28
30
 
29
31
  def row_for(mentry)
30
- set = @ctx.manifest.rules_for(mentry.key)
32
+ set = @manifest.rules_for(mentry.key)
31
33
  refresh = set.refresh
32
34
  envelope = safe_get(mentry.key)
33
35
  last = envelope&.meta&.dig("last_refreshed_at")
@@ -61,10 +63,10 @@ module Textus
61
63
  # Returns the raw envelope or nil. Nested entries (mentry.key is a
62
64
  # prefix, not a leaf) and missing files both resolve to nil.
63
65
  def safe_get(key)
64
- res = @ctx.manifest.resolve(key)
65
- return nil unless @ctx.file_store.exists?(res.path)
66
+ res = @manifest.resolver.resolve(key)
67
+ return nil unless @file_store.exists?(res.path)
66
68
 
67
- raw = @ctx.file_store.read(res.path)
69
+ raw = @file_store.read(res.path)
68
70
  parsed = Entry.for_format(res.entry.format).parse(raw, path: res.path)
69
71
  Envelope.build(
70
72
  key: key, mentry: res.entry, path: res.path,
@@ -7,16 +7,18 @@ module Textus
7
7
  # For interactive reads that want refresh-on-stale, use
8
8
  # `Reads::GetOrRefresh`, which composes this with the orchestrator.
9
9
  class Get
10
- def initialize(ctx:, evaluator: Textus::Domain::Freshness::Evaluator)
11
- @ctx = ctx
12
- @evaluator = evaluator
10
+ def initialize(ctx:, manifest:, file_store:, evaluator: Textus::Domain::Freshness::Evaluator)
11
+ @ctx = ctx
12
+ @manifest = manifest
13
+ @file_store = file_store
14
+ @evaluator = evaluator
13
15
  end
14
16
 
15
17
  def call(key)
16
18
  envelope = read_raw_envelope(key)
17
19
  return nil if envelope.nil?
18
20
 
19
- policy_set = @ctx.manifest.rules_for(key)
21
+ policy_set = @manifest.rules_for(key)
20
22
  refresh_policy = policy_set.refresh
21
23
  return annotate_fresh(envelope) if refresh_policy.nil?
22
24
 
@@ -34,18 +36,18 @@ module Textus
34
36
  # Used by consumers (e.g. Validator) that need to distinguish absence
35
37
  # from emptiness.
36
38
  def get(key)
37
- call(key) || raise(UnknownKey.new(key, suggestions: @ctx.manifest.suggestions_for(key)))
39
+ call(key) || raise(UnknownKey.new(key, suggestions: @manifest.resolver.suggestions_for(key)))
38
40
  end
39
41
 
40
42
  private
41
43
 
42
44
  def read_raw_envelope(key)
43
- res = @ctx.manifest.resolve(key)
45
+ res = @manifest.resolver.resolve(key)
44
46
  mentry = res.entry
45
47
  path = res.path
46
- return nil unless @ctx.file_store.exists?(path)
48
+ return nil unless @file_store.exists?(path)
47
49
 
48
- raw = @ctx.file_store.read(path)
50
+ raw = @file_store.read(path)
49
51
  parsed = Entry.for_format(mentry.format).parse(raw, path: path)
50
52
  Envelope.build(
51
53
  key: key, mentry: mentry, path: path,
@@ -10,8 +10,8 @@ module Textus
10
10
  # Pure reads (build, projection, schema tooling) should use
11
11
  # `Reads::Get` directly; it has no orchestrator dependency.
12
12
  class GetOrRefresh
13
- def initialize(ctx:, get:, orchestrator:)
14
- @ctx = ctx
13
+ def initialize(manifest:, get:, orchestrator:)
14
+ @manifest = manifest
15
15
  @get = get
16
16
  @orchestrator = orchestrator
17
17
  end
@@ -21,7 +21,7 @@ module Textus
21
21
  return nil if envelope.nil?
22
22
  return envelope unless envelope.freshness&.stale
23
23
 
24
- policy_set = @ctx.store.manifest.rules_for(key)
24
+ policy_set = @manifest.rules_for(key)
25
25
  refresh_policy = policy_set.refresh
26
26
  return envelope if refresh_policy.nil?
27
27
 
@@ -2,12 +2,12 @@ module Textus
2
2
  module Application
3
3
  module Reads
4
4
  class List
5
- def initialize(ctx:)
6
- @ctx = ctx
5
+ def initialize(manifest:)
6
+ @manifest = manifest
7
7
  end
8
8
 
9
9
  def call(prefix: nil, zone: nil)
10
- rows = @ctx.manifest.enumerate(prefix: prefix)
10
+ rows = @manifest.resolver.enumerate(prefix: prefix)
11
11
  rows = rows.select { |r| r[:manifest_entry].zone == zone } if zone
12
12
  rows.map { |row| { "key" => row[:key], "zone" => row[:manifest_entry].zone, "path" => row[:path] } }
13
13
  end
@@ -4,12 +4,12 @@ module Textus
4
4
  # For one key, surface every matching policy block along with the
5
5
  # per-slot effective value (which loses ties win-by-specificity).
6
6
  class PolicyExplain
7
- def initialize(ctx:)
8
- @ctx = ctx
7
+ def initialize(manifest:)
8
+ @manifest = manifest
9
9
  end
10
10
 
11
11
  def call(key:)
12
- policies = @ctx.store.manifest.rules
12
+ policies = @manifest.rules
13
13
  matching = policies.explain(key)
14
14
  winners = policies.for(key)
15
15
 
@@ -2,12 +2,14 @@ module Textus
2
2
  module Application
3
3
  module Reads
4
4
  class Published
5
- def initialize(ctx:)
6
- @ctx = ctx
5
+ def initialize(manifest:)
6
+ @manifest = manifest
7
7
  end
8
8
 
9
9
  def call
10
- Dependencies.published_of(@ctx.manifest)
10
+ @manifest.entries.reject { |e| e.publish_to.empty? }.map do |e|
11
+ { "key" => e.key, "publish_to" => e.publish_to }
12
+ end
11
13
  end
12
14
  end
13
15
  end
@@ -2,12 +2,24 @@ module Textus
2
2
  module Application
3
3
  module Reads
4
4
  class Rdeps
5
- def initialize(ctx:)
6
- @ctx = ctx
5
+ def initialize(manifest:)
6
+ @manifest = manifest
7
7
  end
8
8
 
9
9
  def call(key)
10
- Dependencies.rdeps_of(@ctx.manifest, key)
10
+ @manifest.entries.each_with_object([]) do |e, acc|
11
+ next unless e.is_a?(Textus::Manifest::Entry::Derived)
12
+
13
+ src = e.source
14
+ sources = if src.is_a?(Textus::Manifest::Entry::Derived::Projection)
15
+ Array(src.select).compact
16
+ elsif src.is_a?(Textus::Manifest::Entry::Derived::External)
17
+ Array(src.sources).compact
18
+ else
19
+ []
20
+ end
21
+ acc << e.key if sources.any? { |s| s == key || key.start_with?("#{s}.") }
22
+ end
11
23
  end
12
24
  end
13
25
  end
@@ -2,13 +2,14 @@ module Textus
2
2
  module Application
3
3
  module Reads
4
4
  class SchemaEnvelope
5
- def initialize(ctx:)
6
- @ctx = ctx
5
+ def initialize(manifest:, schemas:)
6
+ @manifest = manifest
7
+ @schemas = schemas
7
8
  end
8
9
 
9
10
  def call(key)
10
- mentry = @ctx.manifest.resolve(key).entry
11
- schema = @ctx.schemas.fetch_or_nil(mentry.schema)
11
+ mentry = @manifest.resolver.resolve(key).entry
12
+ schema = @schemas.fetch_or_nil(mentry.schema)
12
13
  { "protocol" => PROTOCOL, "key" => key, "schema_ref" => mentry.schema, "schema" => schema&.to_h }
13
14
  end
14
15
  end
@@ -2,12 +2,12 @@ module Textus
2
2
  module Application
3
3
  module Reads
4
4
  class Stale
5
- def initialize(ctx:)
6
- @ctx = ctx
5
+ def initialize(manifest:)
6
+ @manifest = manifest
7
7
  end
8
8
 
9
9
  def call(prefix: nil, zone: nil)
10
- Textus::Domain::Staleness.new(manifest: @ctx.manifest).call(prefix: prefix, zone: zone)
10
+ Textus::Domain::Staleness.new(manifest: @manifest).call(prefix: prefix, zone: zone)
11
11
  end
12
12
  end
13
13
  end
@@ -2,12 +2,20 @@ module Textus
2
2
  module Application
3
3
  module Reads
4
4
  class Uid
5
- def initialize(ctx:)
6
- @ctx = ctx
5
+ def initialize(ctx:, manifest:, file_store:)
6
+ @ctx = ctx
7
+ @manifest = manifest
8
+ @file_store = file_store
7
9
  end
8
10
 
9
11
  def call(key)
10
- Get.new(ctx: @ctx).get(key).uid
12
+ get.get(key).uid
13
+ end
14
+
15
+ private
16
+
17
+ def get
18
+ @get ||= Get.new(ctx: @ctx, manifest: @manifest, file_store: @file_store)
11
19
  end
12
20
  end
13
21
  end
@@ -2,16 +2,20 @@ module Textus
2
2
  module Application
3
3
  module Reads
4
4
  class ValidateAll
5
- def initialize(ctx:)
6
- @ctx = ctx
5
+ def initialize(ctx:, manifest:, file_store:, schemas:, audit_log:)
6
+ @ctx = ctx
7
+ @manifest = manifest
8
+ @file_store = file_store
9
+ @schemas = schemas
10
+ @audit_log = audit_log
7
11
  end
8
12
 
9
13
  def call
10
14
  Validator.new(
11
- reader: Get.new(ctx: @ctx),
12
- manifest: @ctx.manifest,
13
- audit_log: @ctx.audit_log,
14
- schema_for: ->(name) { @ctx.schemas.fetch_or_nil(name) },
15
+ reader: Get.new(ctx: @ctx, manifest: @manifest, file_store: @file_store),
16
+ manifest: @manifest,
17
+ audit_log: @audit_log,
18
+ schema_for: ->(name) { @schemas.fetch_or_nil(name) },
15
19
  ).call
16
20
  end
17
21
  end
@@ -19,7 +19,7 @@ module Textus
19
19
  private
20
20
 
21
21
  def check_content_violations(violations)
22
- @manifest.enumerate.each do |row|
22
+ @manifest.resolver.enumerate.each do |row|
23
23
  key = row[:key]
24
24
  mentry = row[:manifest_entry]
25
25
  env = fetch_envelope(key, violations) or next
@@ -35,7 +35,7 @@ module Textus
35
35
  end
36
36
 
37
37
  def check_role_authority_violations(violations)
38
- @manifest.enumerate.each do |row|
38
+ @manifest.resolver.enumerate.each do |row|
39
39
  mentry = row[:manifest_entry]
40
40
  next unless mentry.schema
41
41
 
@@ -2,12 +2,12 @@ module Textus
2
2
  module Application
3
3
  module Reads
4
4
  class Where
5
- def initialize(ctx:)
6
- @ctx = ctx
5
+ def initialize(manifest:)
6
+ @manifest = manifest
7
7
  end
8
8
 
9
9
  def call(key)
10
- res = @ctx.manifest.resolve(key)
10
+ res = @manifest.resolver.resolve(key)
11
11
  mentry = res.entry
12
12
  path = res.path
13
13
  { "protocol" => PROTOCOL, "key" => key, "zone" => mentry.zone, "owner" => mentry.owner, "path" => path }
@@ -1,20 +1,24 @@
1
1
  module Textus
2
2
  module Application
3
3
  module Refresh
4
- module All
5
- module_function
4
+ class All
5
+ def initialize(ctx:, manifest:, envelope_io:, bus:, store:, authorizer:, hook_context:) # rubocop:disable Metrics/ParameterLists
6
+ @ctx = ctx
7
+ @manifest = manifest
8
+ @envelope_io = envelope_io
9
+ @bus = bus
10
+ @store = store
11
+ @authorizer = authorizer
12
+ @hook_context = hook_context
13
+ end
6
14
 
7
- def call(ctx, prefix: nil, zone: nil)
8
- envelope_io = Textus::Application::Writes::EnvelopeIO.new(
9
- file_store: ctx.file_store,
10
- manifest: ctx.manifest,
11
- schemas: ctx.schemas,
12
- audit_log: ctx.audit_log,
13
- ctx: ctx,
15
+ def call(prefix: nil, zone: nil)
16
+ worker = Textus::Application::Refresh::Worker.new(
17
+ ctx: @ctx, manifest: @manifest, envelope_io: @envelope_io, bus: @bus,
18
+ store: @store, authorizer: @authorizer, hook_context: @hook_context
14
19
  )
15
- worker = Textus::Application::Refresh::Worker.new(ctx: ctx, envelope_io: envelope_io)
16
20
 
17
- stale_rows = Textus::Application::Reads::Stale.new(ctx: ctx).call(prefix: prefix, zone: zone)
21
+ stale_rows = Textus::Application::Reads::Stale.new(manifest: @manifest).call(prefix: prefix, zone: zone)
18
22
  refreshed = []
19
23
  failed = []
20
24
  skipped = []
@@ -2,11 +2,13 @@ module Textus
2
2
  module Application
3
3
  module Refresh
4
4
  class Orchestrator
5
- def initialize(worker:, store_root:, store: nil, role: "human", detached_spawner: nil)
6
- @worker = worker
7
- @store_root = store_root
8
- @store = store
9
- @role = role
5
+ def initialize(worker:, store_root:, bus: nil, store: nil, ctx: nil, hook_context: nil, detached_spawner: nil) # rubocop:disable Metrics/ParameterLists
6
+ @worker = worker
7
+ @store_root = store_root
8
+ @bus = bus
9
+ @store = store
10
+ @ctx = ctx
11
+ @hook_context = hook_context
10
12
  @detached_spawner = detached_spawner || default_spawner
11
13
  end
12
14
 
@@ -55,10 +57,9 @@ module Textus
55
57
 
56
58
  probe.release
57
59
 
58
- store_view = @store ? Textus::Application::Context.new(store: @store, role: @role) : nil
59
60
  payload = { key: key, started_at: Time.now.utc.iso8601, budget_ms: budget_ms }
60
- payload[:store] = store_view if store_view
61
- @store&.bus&.publish(:refresh_backgrounded, **payload)
61
+ payload[:ctx] = @hook_context if @hook_context
62
+ @bus&.publish(:refresh_backgrounded, **payload)
62
63
  @detached_spawner.call(store_root: @store_root, key: key)
63
64
  Textus::Domain::Outcome::Detached.new
64
65
  elsif result.is_a?(Textus::Error)
@@ -6,17 +6,22 @@ module Textus
6
6
  class Worker
7
7
  FETCH_TIMEOUT_SECONDS = 30
8
8
 
9
- def initialize(ctx:, envelope_io:)
10
- @ctx = ctx
11
- @envelope_io = envelope_io
9
+ def initialize(ctx:, manifest:, envelope_io:, bus:, store:, authorizer:, hook_context:) # rubocop:disable Metrics/ParameterLists
10
+ @ctx = ctx
11
+ @manifest = manifest
12
+ @envelope_io = envelope_io
13
+ @bus = bus
14
+ @store = store
15
+ @authorizer = authorizer
16
+ @hook_context = hook_context
12
17
  end
13
18
 
14
19
  def run(key)
15
- res = @ctx.store.manifest.resolve(key)
20
+ res = @manifest.resolver.resolve(key)
16
21
  mentry = res.entry
17
22
  path = res.path
18
23
  remaining = res.remaining
19
- raise UsageError.new("no intake declared for '#{key}'") unless mentry.intake_handler
24
+ raise UsageError.new("no intake declared for '#{key}'") unless mentry.is_a?(Textus::Manifest::Entry::Intake)
20
25
 
21
26
  before_etag = File.exist?(path) ? Etag.for_file(path) : nil
22
27
  result = fetch_with_bus(key, mentry, remaining)
@@ -25,19 +30,14 @@ module Textus
25
30
 
26
31
  private
27
32
 
28
- def read_view
29
- Application::Context.new(store: @ctx.store, role: @ctx.role)
30
- end
31
-
32
33
  def fetch_timeout_for(key)
33
- rule = @ctx.store.manifest.rules_for(key)
34
+ rule = @manifest.rules_for(key)
34
35
  rule&.refresh&.fetch_timeout_seconds || FETCH_TIMEOUT_SECONDS
35
36
  end
36
37
 
37
38
  def fetch_with_bus(key, mentry, remaining)
38
- callable = @ctx.store.registry.rpc_callable(:resolve_intake, mentry.intake_handler)
39
- @ctx.bus.publish(:refresh_started, store: read_view, key: key, mode: :sync,
40
- correlation_id: @ctx.correlation_id)
39
+ callable = @bus.rpc_callable(:resolve_intake, mentry.handler)
40
+ @bus.publish(:refresh_started, ctx: @hook_context, key: key, mode: :sync)
41
41
  call_intake(key, mentry, callable, remaining)
42
42
  end
43
43
 
@@ -45,38 +45,38 @@ module Textus
45
45
  timeout = fetch_timeout_for(key)
46
46
  Timeout.timeout(timeout) do
47
47
  callable.call(
48
- store: @ctx,
49
- config: mentry.intake_config,
48
+ store: @store,
49
+ config: mentry.config,
50
50
  args: { trigger_key: key, leaf_segments: remaining || [] },
51
51
  )
52
52
  end
53
53
  rescue Timeout::Error
54
- @ctx.bus.publish(:refresh_failed, store: read_view, key: key, error_class: "Timeout::Error",
55
- error_message: "intake '#{mentry.intake_handler}' exceeded #{timeout}s",
56
- correlation_id: @ctx.correlation_id)
57
- raise UsageError.new("intake '#{mentry.intake_handler}' exceeded #{timeout}s timeout")
54
+ @bus.publish(:refresh_failed, ctx: @hook_context, key: key,
55
+ error_class: "Timeout::Error",
56
+ error_message: "intake '#{mentry.handler}' exceeded #{timeout}s")
57
+ raise UsageError.new("intake '#{mentry.handler}' exceeded #{timeout}s timeout")
58
58
  rescue Textus::Error => e
59
- @ctx.bus.publish(:refresh_failed, store: read_view, key: key, error_class: e.class.name,
60
- error_message: e.message, correlation_id: @ctx.correlation_id)
59
+ @bus.publish(:refresh_failed, ctx: @hook_context, key: key, error_class: e.class.name,
60
+ error_message: e.message)
61
61
  raise
62
62
  rescue StandardError => e
63
- @ctx.bus.publish(:refresh_failed, store: read_view, key: key, error_class: e.class.name,
64
- error_message: e.message, correlation_id: @ctx.correlation_id)
65
- raise UsageError.new("intake '#{mentry.intake_handler}' raised: #{e.class}: #{e.message}")
63
+ @bus.publish(:refresh_failed, ctx: @hook_context, key: key, error_class: e.class.name,
64
+ error_message: e.message)
65
+ raise UsageError.new("intake '#{mentry.handler}' raised: #{e.class}: #{e.message}")
66
66
  end
67
67
 
68
68
  def persist_and_notify(key, mentry, result, before_etag)
69
- normalized = Textus::Refresh.normalize_action_result(result, format: mentry.format)
70
- envelope = Textus::Application::Writes::Put.new(ctx: @ctx, envelope_io: @envelope_io).call(
69
+ normalized = self.class.send(:normalize_action_result, result, format: mentry.format)
70
+ @authorizer.authorize_write!(mentry, role: @ctx.role)
71
+ envelope = @envelope_io.write(
71
72
  key,
72
- meta: normalized[:meta], body: normalized[:body], content: normalized[:content],
73
- suppress_events: true
73
+ mentry: mentry,
74
+ payload: Textus::Application::Writes::EnvelopeIO::Payload.new(
75
+ meta: normalized[:meta], body: normalized[:body], content: normalized[:content],
76
+ ),
74
77
  )
75
78
  change = detect_change(before_etag, envelope)
76
- unless change == :unchanged
77
- @ctx.bus.publish(:entry_refreshed, store: read_view, key: key, envelope: envelope, change: change,
78
- correlation_id: @ctx.correlation_id)
79
- end
79
+ @bus.publish(:entry_refreshed, ctx: @hook_context, key: key, envelope: envelope, change: change) unless change == :unchanged
80
80
  envelope
81
81
  end
82
82
 
@@ -86,6 +86,30 @@ module Textus
86
86
  else :updated
87
87
  end
88
88
  end
89
+
90
+ def self.normalize_action_result(res, format:)
91
+ res = res.transform_keys(&:to_s) if res.is_a?(Hash)
92
+ res ||= {}
93
+ meta_val = res["_meta"]
94
+ body = res["body"]
95
+ content = res["content"]
96
+
97
+ case format
98
+ when "markdown" then { meta: meta_val || {}, body: body.to_s, content: nil }
99
+ when "text" then { meta: {}, body: body.to_s, content: nil }
100
+ when "json", "yaml"
101
+ if !content.nil?
102
+ { meta: meta_val || {}, body: nil, content: content }
103
+ elsif !body.nil?
104
+ { meta: {}, body: body.to_s, content: nil }
105
+ else
106
+ raise Textus::UsageError.new("intake for #{format} returned neither content nor body")
107
+ end
108
+ else
109
+ raise Textus::UsageError.new("unknown format #{format.inspect}")
110
+ end
111
+ end
112
+ private_class_method :normalize_action_result
89
113
  end
90
114
  end
91
115
  end