textus 0.18.0 → 0.20.2
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 +43 -48
- data/CHANGELOG.md +238 -0
- data/SPEC.md +35 -2
- data/lib/textus/application/context.rb +20 -58
- data/lib/textus/application/policy/predicates/accept_authority_signed.rb +33 -0
- data/lib/textus/{domain → application}/policy/predicates/schema_valid.rb +5 -5
- data/lib/textus/{domain → application}/policy/promotion.rb +18 -6
- data/lib/textus/application/projection.rb +91 -0
- data/lib/textus/application/reads/audit.rb +4 -4
- data/lib/textus/application/reads/blame.rb +9 -8
- data/lib/textus/application/reads/deps.rb +14 -3
- data/lib/textus/application/reads/freshness.rb +10 -8
- data/lib/textus/application/reads/get.rb +10 -8
- data/lib/textus/application/reads/get_or_refresh.rb +3 -3
- data/lib/textus/application/reads/list.rb +3 -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 +5 -4
- 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 +10 -6
- data/lib/textus/application/reads/validator.rb +5 -3
- data/lib/textus/application/reads/where.rb +3 -3
- data/lib/textus/application/refresh/all.rb +15 -11
- data/lib/textus/application/refresh/orchestrator.rb +9 -8
- data/lib/textus/application/refresh/worker.rb +56 -32
- data/lib/textus/application/writes/accept.rb +43 -16
- data/lib/textus/application/writes/authority_gate.rb +26 -0
- data/lib/textus/application/writes/delete.rb +13 -10
- data/lib/textus/application/writes/envelope_io.rb +64 -4
- data/lib/textus/application/writes/materializer.rb +50 -0
- data/lib/textus/application/writes/mv.rb +57 -94
- data/lib/textus/application/writes/publish.rb +132 -26
- data/lib/textus/application/writes/put.rb +15 -14
- data/lib/textus/application/writes/reject.rb +25 -12
- 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/verb/build.rb +4 -6
- data/lib/textus/cli/verb/get.rb +1 -1
- data/lib/textus/cli/verb/hook_run.rb +3 -4
- data/lib/textus/cli/verb/hooks.rb +5 -5
- data/lib/textus/cli/verb/put.rb +2 -3
- data/lib/textus/cli/verb/refresh_stale.rb +1 -2
- data/lib/textus/doctor/check/handler_allowlist.rb +3 -2
- data/lib/textus/doctor/check/hooks.rb +2 -2
- data/lib/textus/doctor/check/illegal_keys.rb +7 -7
- data/lib/textus/doctor/check/intake_registration.rb +2 -2
- 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/templates.rb +4 -3
- data/lib/textus/doctor.rb +3 -4
- data/lib/textus/domain/authorizer.rb +37 -0
- data/lib/textus/domain/policy/promote.rb +4 -2
- data/lib/textus/domain/policy/refresh.rb +2 -0
- data/lib/textus/domain/staleness/generator_check.rb +8 -7
- data/lib/textus/domain/staleness/intake_check.rb +2 -2
- 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 +3 -3
- data/lib/textus/infra/audit_subscriber.rb +4 -4
- data/lib/textus/infra/event_bus.rb +3 -3
- data/lib/textus/infra/refresh/detached.rb +1 -1
- data/lib/textus/init.rb +3 -2
- data/lib/textus/intro.rb +51 -27
- 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 +58 -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/resolver.rb +112 -0
- data/lib/textus/manifest/role_kinds.rb +21 -0
- data/lib/textus/manifest/schema.rb +46 -2
- data/lib/textus/manifest.rb +24 -101
- data/lib/textus/operations.rb +131 -74
- data/lib/textus/schema/tools.rb +10 -3
- data/lib/textus/store.rb +6 -6
- data/lib/textus/version.rb +1 -1
- metadata +18 -14
- data/lib/textus/application/writes/build.rb +0 -78
- data/lib/textus/cli/verb/key_normalize.rb +0 -19
- data/lib/textus/dependencies.rb +0 -23
- data/lib/textus/domain/policy/predicates/human_accept.rb +0 -31
- data/lib/textus/domain/policy.rb +0 -7
- data/lib/textus/hooks/dispatcher.rb +0 -71
- data/lib/textus/hooks/registry.rb +0 -85
- data/lib/textus/manifest/resolution.rb +0 -5
- data/lib/textus/migrate_keys.rb +0 -187
- data/lib/textus/projection.rb +0 -89
- data/lib/textus/refresh.rb +0 -39
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
require_relative "predicates/schema_valid"
|
|
2
|
+
require_relative "predicates/accept_authority_signed"
|
|
3
|
+
|
|
1
4
|
module Textus
|
|
2
|
-
module
|
|
5
|
+
module Application
|
|
3
6
|
module Policy
|
|
4
|
-
# Promotion evaluates a list of named predicates against a pending-proposal
|
|
5
|
-
# entry and returns a Result indicating whether all requirements are met.
|
|
6
7
|
class Promotion
|
|
7
8
|
Result = Struct.new(:ok?, :reasons, keyword_init: true)
|
|
8
9
|
|
|
9
10
|
REGISTRY = {
|
|
10
11
|
"schema_valid" => -> { Predicates::SchemaValid.new },
|
|
11
|
-
"
|
|
12
|
+
"accept_authority_signed" => -> { Predicates::AcceptAuthoritySigned.new },
|
|
12
13
|
}.freeze
|
|
13
14
|
|
|
14
15
|
def self.from_names(names)
|
|
@@ -31,14 +32,25 @@ module Textus
|
|
|
31
32
|
@predicates.map(&:name)
|
|
32
33
|
end
|
|
33
34
|
|
|
34
|
-
def evaluate(entry:,
|
|
35
|
+
def evaluate(entry:, schemas:, manifest:, role:)
|
|
35
36
|
reasons = []
|
|
36
37
|
@predicates.each do |pred|
|
|
37
|
-
ok = pred
|
|
38
|
+
ok = invoke(pred, entry: entry, schemas: schemas, manifest: manifest, role: role)
|
|
38
39
|
reasons << "#{pred.name}: #{pred.reason || "predicate failed"}" unless ok
|
|
39
40
|
end
|
|
40
41
|
Result.new(ok?: reasons.empty?, reasons: reasons)
|
|
41
42
|
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def invoke(pred, entry:, schemas:, manifest:, role:)
|
|
47
|
+
case pred.name
|
|
48
|
+
when "accept_authority_signed"
|
|
49
|
+
pred.call(role: role, manifest: manifest, entry: entry)
|
|
50
|
+
else
|
|
51
|
+
pred.call(entry: entry, schemas: schemas, manifest: manifest)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
42
54
|
end
|
|
43
55
|
end
|
|
44
56
|
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
require "timeout"
|
|
3
|
+
|
|
4
|
+
module Textus
|
|
5
|
+
module Application
|
|
6
|
+
class Projection
|
|
7
|
+
MAX_LIMIT = 1000
|
|
8
|
+
REDUCER_TIMEOUT_SECONDS = 2
|
|
9
|
+
|
|
10
|
+
# `reader` — a callable `->(key) { envelope_or_nil }`. Caller picks
|
|
11
|
+
# semantics: pure read (`ops.get`) for materialization paths;
|
|
12
|
+
# `ops.get_or_refresh` if you want refresh-on-stale.
|
|
13
|
+
# `lister` — a callable `->(prefix:) { [ { "key" => ... }, ... ] }`.
|
|
14
|
+
# `transform_resolver` — a callable `->(name) { callable_or_raise }`.
|
|
15
|
+
# `transform_context` — `Application::Context` handed to the transform reducer.
|
|
16
|
+
def initialize(reader:, spec:, lister:, transform_resolver:, transform_context:)
|
|
17
|
+
@reader = reader
|
|
18
|
+
@spec = spec || {}
|
|
19
|
+
@lister = lister
|
|
20
|
+
@transform_resolver = transform_resolver
|
|
21
|
+
@transform_context = transform_context
|
|
22
|
+
@limit = (@spec["limit"] || MAX_LIMIT).to_i
|
|
23
|
+
raise InvalidProjection.new("limit #{@limit} exceeds max #{MAX_LIMIT}") if @limit > MAX_LIMIT
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def run
|
|
27
|
+
keys = collect_keys
|
|
28
|
+
explicit_pluck = !@spec["pluck"].nil? && @spec["pluck"] != "*"
|
|
29
|
+
rows = keys.map do |key|
|
|
30
|
+
env = @reader.call(key)
|
|
31
|
+
row = pluck(env.meta, env.body)
|
|
32
|
+
explicit_pluck ? row : row.merge("_key" => key)
|
|
33
|
+
end
|
|
34
|
+
reduced = apply_reducer(rows)
|
|
35
|
+
# Reducers may return either an Array of rows (legacy / templated builds)
|
|
36
|
+
# or a Hash that becomes the structured-format payload base. In the Hash
|
|
37
|
+
# case, downstream sort/limit/position markers don't apply, and the
|
|
38
|
+
# builder owns `_meta.generated_at` so we don't stamp it here.
|
|
39
|
+
return reduced if reduced.is_a?(Hash)
|
|
40
|
+
|
|
41
|
+
rows = reduced
|
|
42
|
+
rows = sort(rows)
|
|
43
|
+
rows = rows.first(@limit)
|
|
44
|
+
mark_positions(rows)
|
|
45
|
+
{ "entries" => rows, "count" => rows.length, "generated_at" => Time.now.utc.iso8601 }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def apply_reducer(rows)
|
|
51
|
+
name = @spec["transform"] or return rows
|
|
52
|
+
callable = @transform_resolver.call(name)
|
|
53
|
+
Timeout.timeout(REDUCER_TIMEOUT_SECONDS) do
|
|
54
|
+
callable.call(store: @transform_context, rows: rows, config: @spec["transform_config"] || {})
|
|
55
|
+
end
|
|
56
|
+
rescue Timeout::Error
|
|
57
|
+
raise UsageError.new("transform_rows '#{name}' exceeded #{REDUCER_TIMEOUT_SECONDS}s timeout")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def collect_keys
|
|
61
|
+
prefixes = Array(@spec["select"])
|
|
62
|
+
prefixes.flat_map { |p| @lister.call(prefix: p).map { |row| row["key"] } }.uniq
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def pluck(frontmatter, _body)
|
|
66
|
+
fields = @spec["pluck"]
|
|
67
|
+
if fields.nil? || fields == "*"
|
|
68
|
+
frontmatter
|
|
69
|
+
else
|
|
70
|
+
Array(fields).each_with_object({}) { |f, h| h[f] = frontmatter[f] if frontmatter.key?(f) }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Adds `_first`, `_last`, and `_index` markers so templates can emit
|
|
75
|
+
# delimiters (e.g. JSON commas) via {{^_last}},{{/_last}}.
|
|
76
|
+
def mark_positions(rows)
|
|
77
|
+
last_idx = rows.length - 1
|
|
78
|
+
rows.each_with_index do |row, i|
|
|
79
|
+
row["_index"] = i
|
|
80
|
+
row["_first"] = i.zero?
|
|
81
|
+
row["_last"] = (i == last_idx)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def sort(rows)
|
|
86
|
+
sb = @spec["sort_by"] or return rows
|
|
87
|
+
rows.sort_by { |r| r[sb].to_s }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -8,9 +8,9 @@ module Textus
|
|
|
8
8
|
# correlation_id, limit. Reads the log file as JSON-Lines (legacy TSV
|
|
9
9
|
# rows produce nil and are skipped).
|
|
10
10
|
class Audit
|
|
11
|
-
def initialize(
|
|
12
|
-
@
|
|
13
|
-
@log_path = File.join(
|
|
11
|
+
def initialize(manifest:, root:)
|
|
12
|
+
@manifest = manifest
|
|
13
|
+
@log_path = File.join(root, "audit.log")
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
# rubocop:disable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
@@ -58,7 +58,7 @@ module Textus
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def key_in_zone?(key, zone)
|
|
61
|
-
mentry = @
|
|
61
|
+
mentry = @manifest.resolver.resolve(key).entry
|
|
62
62
|
mentry && mentry.zone == zone
|
|
63
63
|
rescue Textus::Error
|
|
64
64
|
false
|
|
@@ -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(
|
|
12
|
-
@
|
|
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(
|
|
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 = @
|
|
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(@
|
|
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: @
|
|
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 = @
|
|
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: @
|
|
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(
|
|
6
|
-
@
|
|
5
|
+
def initialize(manifest:)
|
|
6
|
+
@manifest = manifest
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def call(key)
|
|
10
|
-
|
|
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
|
|
13
|
-
@
|
|
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
|
-
@
|
|
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 = @
|
|
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 = @
|
|
65
|
-
return nil unless @
|
|
66
|
+
res = @manifest.resolver.resolve(key)
|
|
67
|
+
return nil unless @file_store.exists?(res.path)
|
|
66
68
|
|
|
67
|
-
raw = @
|
|
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
|
|
12
|
-
@
|
|
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 = @
|
|
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: @
|
|
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 = @
|
|
45
|
+
res = @manifest.resolver.resolve(key)
|
|
44
46
|
mentry = res.entry
|
|
45
47
|
path = res.path
|
|
46
|
-
return nil unless @
|
|
48
|
+
return nil unless @file_store.exists?(path)
|
|
47
49
|
|
|
48
|
-
raw = @
|
|
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(
|
|
14
|
-
@
|
|
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 = @
|
|
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(
|
|
6
|
-
@
|
|
5
|
+
def initialize(manifest:)
|
|
6
|
+
@manifest = manifest
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def call(prefix: nil, zone: nil)
|
|
10
|
-
rows = @
|
|
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(
|
|
8
|
-
@
|
|
7
|
+
def initialize(manifest:)
|
|
8
|
+
@manifest = manifest
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def call(key:)
|
|
12
|
-
policies = @
|
|
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(
|
|
6
|
-
@
|
|
5
|
+
def initialize(manifest:)
|
|
6
|
+
@manifest = manifest
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def call
|
|
10
|
-
|
|
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(
|
|
6
|
-
@
|
|
5
|
+
def initialize(manifest:)
|
|
6
|
+
@manifest = manifest
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def call(key)
|
|
10
|
-
|
|
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(
|
|
6
|
-
@
|
|
5
|
+
def initialize(manifest:, schemas:)
|
|
6
|
+
@manifest = manifest
|
|
7
|
+
@schemas = schemas
|
|
7
8
|
end
|
|
8
9
|
|
|
9
10
|
def call(key)
|
|
10
|
-
mentry = @
|
|
11
|
-
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(
|
|
6
|
-
@
|
|
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: @
|
|
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
|
|
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
|
-
|
|
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
|
|
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: @
|
|
13
|
-
audit_log: @
|
|
14
|
-
schema_for: ->(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
|
|
|
@@ -55,9 +55,11 @@ module Textus
|
|
|
55
55
|
last_writer = @audit_log.last_writer_for(key)
|
|
56
56
|
return if last_writer.nil?
|
|
57
57
|
|
|
58
|
+
last_writer_is_authority = @manifest.role_kind(last_writer) == :accept_authority
|
|
59
|
+
|
|
58
60
|
env.meta.each_key do |field|
|
|
59
61
|
owner = schema.maintained_by(field)
|
|
60
|
-
next if owner.nil? || last_writer == owner ||
|
|
62
|
+
next if owner.nil? || last_writer == owner || last_writer_is_authority
|
|
61
63
|
|
|
62
64
|
violations << { "key" => key, "code" => "role_authority",
|
|
63
65
|
"field" => field, "expected" => owner, "last_writer" => last_writer }
|
|
@@ -2,12 +2,12 @@ module Textus
|
|
|
2
2
|
module Application
|
|
3
3
|
module Reads
|
|
4
4
|
class Where
|
|
5
|
-
def initialize(
|
|
6
|
-
@
|
|
5
|
+
def initialize(manifest:)
|
|
6
|
+
@manifest = manifest
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def call(key)
|
|
10
|
-
res = @
|
|
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
|
-
|
|
5
|
-
|
|
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(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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(
|
|
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,
|
|
6
|
-
@worker
|
|
7
|
-
@store_root
|
|
8
|
-
@
|
|
9
|
-
@
|
|
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[:
|
|
61
|
-
@
|
|
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)
|