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.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +43 -48
- data/CHANGELOG.md +173 -0
- data/lib/textus/application/context.rb +20 -58
- 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 +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 +2 -2
- 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/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 +38 -15
- 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 +20 -11
- 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 +2 -5
- 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/key_normalize.rb +32 -3
- 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 +6 -5
- 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/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 +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/resolver.rb +109 -0
- data/lib/textus/manifest/schema.rb +1 -1
- data/lib/textus/manifest.rb +3 -100
- data/lib/textus/operations.rb +131 -74
- data/lib/textus/schema/tools.rb +2 -2
- data/lib/textus/store.rb +6 -6
- data/lib/textus/version.rb +1 -1
- metadata +18 -11
- data/lib/textus/application/writes/build.rb +0 -78
- 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 -71
- data/lib/textus/hooks/registry.rb +0 -85
- data/lib/textus/migrate_keys.rb +0 -187
- data/lib/textus/projection.rb +0 -89
- data/lib/textus/refresh.rb +0 -39
|
@@ -1,60 +1,46 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
|
|
3
1
|
module Textus
|
|
4
2
|
module Application
|
|
5
3
|
module Writes
|
|
6
4
|
class Mv
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@
|
|
14
|
-
@envelope_io = envelope_io
|
|
5
|
+
def initialize(ctx:, manifest:, envelope_io:, bus:, authorizer:, hook_context:)
|
|
6
|
+
@ctx = ctx
|
|
7
|
+
@manifest = manifest
|
|
8
|
+
@envelope_io = envelope_io
|
|
9
|
+
@bus = bus
|
|
10
|
+
@authorizer = authorizer
|
|
11
|
+
@hook_context = hook_context
|
|
15
12
|
end
|
|
16
13
|
|
|
17
14
|
def call(old_key, new_key, dry_run: false)
|
|
18
|
-
|
|
19
|
-
return dry_run_result(
|
|
15
|
+
old_res, new_res = prepare(old_key, new_key)
|
|
16
|
+
return dry_run_result(old_key, new_key, old_res, new_res) if dry_run
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
ensure_uid!(old_key, old_res.entry)
|
|
19
|
+
envelope = @envelope_io.move(
|
|
20
|
+
from_key: old_key, to_key: new_key,
|
|
21
|
+
new_mentry: new_res.entry
|
|
22
|
+
)
|
|
23
|
+
publish_renamed(old_key, new_key, envelope)
|
|
24
|
+
success_result(old_key, new_key, old_res, new_res, envelope)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
private
|
|
28
28
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def prepare_plan(old_key, new_key)
|
|
33
|
-
manifest.validate_key!(old_key)
|
|
34
|
-
manifest.validate_key!(new_key)
|
|
29
|
+
def prepare(old_key, new_key)
|
|
30
|
+
@manifest.validate_key!(old_key)
|
|
31
|
+
@manifest.validate_key!(new_key)
|
|
35
32
|
raise UsageError.new("mv: old and new keys are identical") if old_key == new_key
|
|
36
33
|
|
|
37
|
-
old_res = manifest.resolve(old_key)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
raise UnknownKey.new(old_key) unless @ctx.file_store.exists?(old_path)
|
|
34
|
+
old_res = @manifest.resolver.resolve(old_key)
|
|
35
|
+
new_res = @manifest.resolver.resolve(new_key)
|
|
36
|
+
raise UnknownKey.new(old_key) unless @envelope_io.exists?(old_res.path)
|
|
41
37
|
|
|
42
|
-
new_res
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@ctx.authorize_write!(old_mentry)
|
|
47
|
-
@ctx.authorize_write!(new_mentry)
|
|
48
|
-
raise UsageError.new("mv: target '#{new_key}' already exists at #{new_path}") if @ctx.file_store.exists?(new_path)
|
|
38
|
+
validate_zone_and_format!(old_res.entry, new_res.entry)
|
|
39
|
+
@authorizer.authorize_write!(old_res.entry, role: @ctx.role)
|
|
40
|
+
@authorizer.authorize_write!(new_res.entry, role: @ctx.role)
|
|
41
|
+
raise UsageError.new("mv: target '#{new_key}' already exists at #{new_res.path}") if @envelope_io.exists?(new_res.path)
|
|
49
42
|
|
|
50
|
-
|
|
51
|
-
plan = MovePlan.new(
|
|
52
|
-
old_key: old_key, new_key: new_key,
|
|
53
|
-
old_path: old_path, new_path: new_path,
|
|
54
|
-
new_mentry: new_mentry,
|
|
55
|
-
uid: pre_env.uid, etag_before: pre_env.etag
|
|
56
|
-
)
|
|
57
|
-
[plan, pre_env]
|
|
43
|
+
[old_res, new_res]
|
|
58
44
|
end
|
|
59
45
|
|
|
60
46
|
def validate_zone_and_format!(old_mentry, new_mentry)
|
|
@@ -69,73 +55,50 @@ module Textus
|
|
|
69
55
|
raise UsageError.new("mv: format mismatch (#{old_mentry.format} → #{new_mentry.format}); refusing.")
|
|
70
56
|
end
|
|
71
57
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
58
|
+
# If the source file lacks a UID, rewrite it in-place via EnvelopeIO#write
|
|
59
|
+
# so a UID gets injected before the move. This replaces the previous
|
|
60
|
+
# Put(suppress_events: true) bypass with a direct EnvelopeIO call —
|
|
61
|
+
# producing one "put" audit row, then the "mv" row from EnvelopeIO#move.
|
|
62
|
+
def ensure_uid!(old_key, old_mentry)
|
|
63
|
+
pre_env = @envelope_io.read_envelope(old_key)
|
|
64
|
+
return if pre_env.uid
|
|
65
|
+
|
|
66
|
+
@envelope_io.write(
|
|
67
|
+
old_key, mentry: old_mentry,
|
|
68
|
+
payload: EnvelopeIO::Payload.new(
|
|
69
|
+
meta: pre_env.meta, body: pre_env.body, content: pre_env.content,
|
|
70
|
+
)
|
|
81
71
|
)
|
|
82
|
-
plan.with(uid: env.uid, etag_before: env.etag)
|
|
83
72
|
end
|
|
84
73
|
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
74
|
+
def publish_renamed(old_key, new_key, envelope)
|
|
75
|
+
@bus.publish(:entry_renamed,
|
|
76
|
+
ctx: @hook_context,
|
|
77
|
+
key: new_key,
|
|
78
|
+
from_key: old_key,
|
|
79
|
+
to_key: new_key,
|
|
80
|
+
envelope: envelope)
|
|
90
81
|
end
|
|
91
82
|
|
|
92
|
-
def
|
|
93
|
-
|
|
94
|
-
"from_key" => plan.old_key, "to_key" => plan.new_key,
|
|
95
|
-
"from_path" => plan.old_path, "to_path" => plan.new_path,
|
|
96
|
-
"uid" => plan.uid
|
|
97
|
-
}
|
|
98
|
-
extras["correlation_id"] = @ctx.correlation_id if @ctx.correlation_id
|
|
99
|
-
|
|
100
|
-
@ctx.audit_log.append(
|
|
101
|
-
role: @ctx.role, verb: "mv", key: plan.new_key,
|
|
102
|
-
etag_before: plan.etag_before, etag_after: etag_after,
|
|
103
|
-
extras: extras
|
|
104
|
-
)
|
|
105
|
-
new_envelope = reader_get(plan.new_key)
|
|
106
|
-
@ctx.bus.publish(:entry_renamed,
|
|
107
|
-
store: @ctx.with_role(@ctx.role),
|
|
108
|
-
key: plan.new_key,
|
|
109
|
-
from_key: plan.old_key,
|
|
110
|
-
to_key: plan.new_key,
|
|
111
|
-
envelope: new_envelope,
|
|
112
|
-
correlation_id: @ctx.correlation_id)
|
|
113
|
-
new_envelope
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def dry_run_result(plan)
|
|
83
|
+
def dry_run_result(old_key, new_key, old_res, new_res)
|
|
84
|
+
pre_env = @envelope_io.read_envelope(old_key)
|
|
117
85
|
{
|
|
118
86
|
"protocol" => PROTOCOL, "ok" => true, "dry_run" => true,
|
|
119
|
-
"from_key" =>
|
|
120
|
-
"from_path" =>
|
|
121
|
-
"uid" =>
|
|
87
|
+
"from_key" => old_key, "to_key" => new_key,
|
|
88
|
+
"from_path" => old_res.path, "to_path" => new_res.path,
|
|
89
|
+
"uid" => pre_env.uid
|
|
122
90
|
}
|
|
123
91
|
end
|
|
124
92
|
|
|
125
|
-
def success_result(
|
|
93
|
+
def success_result(old_key, new_key, old_res, new_res, envelope)
|
|
126
94
|
{
|
|
127
95
|
"protocol" => PROTOCOL, "ok" => true,
|
|
128
|
-
"from_key" =>
|
|
129
|
-
"from_path" =>
|
|
130
|
-
"uid" =>
|
|
131
|
-
"envelope" =>
|
|
96
|
+
"from_key" => old_key, "to_key" => new_key,
|
|
97
|
+
"from_path" => old_res.path, "to_path" => new_res.path,
|
|
98
|
+
"uid" => envelope.uid,
|
|
99
|
+
"envelope" => envelope.to_h_for_wire
|
|
132
100
|
}
|
|
133
101
|
end
|
|
134
|
-
|
|
135
|
-
def rewrite_name_for_mv!(mentry, new_path, new_key)
|
|
136
|
-
basename = new_key.split(".").last
|
|
137
|
-
Entry.for_format(mentry.format).rewrite_name(new_path, basename)
|
|
138
|
-
end
|
|
139
102
|
end
|
|
140
103
|
end
|
|
141
104
|
end
|
|
@@ -1,37 +1,102 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Application
|
|
3
3
|
module Writes
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# Single-pass publish use case: materializes Derived entries (template +
|
|
5
|
+
# projection + external runner) AND copies Leaf/Nested entries to their
|
|
6
|
+
# publish targets. Replaces the former two-step Build + Publish split.
|
|
7
|
+
#
|
|
8
|
+
# Return shape: { "protocol", "built", "published_leaves" }
|
|
9
|
+
# — wire-compatible with what the `textus build` CLI verb previously
|
|
10
|
+
# assembled by merging Build + old Publish results.
|
|
7
11
|
class Publish
|
|
8
|
-
def initialize(ctx:)
|
|
9
|
-
@ctx
|
|
12
|
+
def initialize(ctx:, manifest:, file_store:, bus:, root:, store:, hook_context:) # rubocop:disable Metrics/ParameterLists
|
|
13
|
+
@ctx = ctx
|
|
14
|
+
@manifest = manifest
|
|
15
|
+
@file_store = file_store
|
|
16
|
+
@bus = bus
|
|
17
|
+
@root = root
|
|
18
|
+
@store = store
|
|
19
|
+
@hook_context = hook_context
|
|
10
20
|
end
|
|
11
21
|
|
|
12
22
|
def call(prefix: nil)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
next unless mentry.nested && mentry.publish_each
|
|
17
|
-
next if prefix && !mentry.key.start_with?(prefix) && !prefix.start_with?("#{mentry.key}.")
|
|
23
|
+
built = []
|
|
24
|
+
leaves = []
|
|
25
|
+
repo_root = File.dirname(@root)
|
|
18
26
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
next if prefix && !row[:key].start_with?(prefix) && row[:key] != prefix
|
|
27
|
+
@manifest.entries.each do |mentry|
|
|
28
|
+
next if prefix && !entry_matches_prefix?(mentry, prefix)
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
case mentry
|
|
31
|
+
when Textus::Manifest::Entry::Derived
|
|
32
|
+
next unless mentry.in_generator_zone?
|
|
33
|
+
|
|
34
|
+
result = materialize_derived(mentry, repo_root)
|
|
35
|
+
built << result if result
|
|
36
|
+
when Textus::Manifest::Entry::Nested
|
|
37
|
+
next unless mentry.publish_each
|
|
38
|
+
|
|
39
|
+
publish_nested(mentry, repo_root, prefix, leaves)
|
|
40
|
+
when Textus::Manifest::Entry::Leaf
|
|
41
|
+
next if Array(mentry.publish_to).empty?
|
|
42
|
+
|
|
43
|
+
result = publish_leaf_entry(mentry, repo_root)
|
|
44
|
+
built << result if result
|
|
24
45
|
end
|
|
25
46
|
end
|
|
26
|
-
|
|
47
|
+
|
|
48
|
+
{ "protocol" => Textus::PROTOCOL, "built" => built, "published_leaves" => leaves }
|
|
27
49
|
end
|
|
28
50
|
|
|
29
51
|
private
|
|
30
52
|
|
|
31
|
-
|
|
32
|
-
def
|
|
53
|
+
# Materialize a Derived entry and copy to publish_to targets.
|
|
54
|
+
def materialize_derived(mentry, repo_root)
|
|
55
|
+
target_path = Materializer.new(
|
|
56
|
+
ctx: @ctx, manifest: @manifest, file_store: @file_store,
|
|
57
|
+
bus: @bus, root: @root, store: @store
|
|
58
|
+
).run(mentry)
|
|
33
59
|
|
|
34
|
-
|
|
60
|
+
publish_derived_copies(mentry, target_path, repo_root)
|
|
61
|
+
fire_build_completed(mentry, target_path)
|
|
62
|
+
|
|
63
|
+
{ "key" => mentry.key, "path" => target_path, "published_to" => mentry.publish_to }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def publish_derived_copies(mentry, target_path, repo_root)
|
|
67
|
+
envelope = reader.call(mentry.key)
|
|
68
|
+
mentry.publish_to.each do |rel|
|
|
69
|
+
target_abs = File.join(repo_root, rel)
|
|
70
|
+
Textus::Infra::Publisher.publish(source: target_path, target: target_abs, store_root: @root)
|
|
71
|
+
publish_event(:file_published,
|
|
72
|
+
key: mentry.key,
|
|
73
|
+
envelope: envelope,
|
|
74
|
+
source: target_path,
|
|
75
|
+
target: target_abs)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def fire_build_completed(mentry, target_path) # rubocop:disable Lint/UnusedMethodArgument
|
|
80
|
+
envelope = reader.call(mentry.key)
|
|
81
|
+
src = mentry.source
|
|
82
|
+
selects = src.is_a?(Textus::Manifest::Entry::Derived::Projection) ? Array(src.select).compact : []
|
|
83
|
+
publish_event(:build_completed,
|
|
84
|
+
key: mentry.key,
|
|
85
|
+
envelope: envelope,
|
|
86
|
+
sources: selects)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Publish each leaf under a Nested entry's publish_each pattern.
|
|
90
|
+
def publish_nested(mentry, repo_root, prefix, accumulator)
|
|
91
|
+
@manifest.resolver.enumerate(prefix: mentry.key).each do |row|
|
|
92
|
+
next unless row[:manifest_entry].equal?(mentry)
|
|
93
|
+
next if prefix && !row[:key].start_with?(prefix) && row[:key] != prefix
|
|
94
|
+
|
|
95
|
+
accumulator << publish_nested_leaf(mentry, row, repo_root)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def publish_nested_leaf(mentry, row, repo_root)
|
|
35
100
|
target_rel = mentry.publish_target_for(row[:key])
|
|
36
101
|
target_abs = File.expand_path(File.join(repo_root, target_rel))
|
|
37
102
|
unless target_abs.start_with?(File.expand_path(repo_root) + File::SEPARATOR)
|
|
@@ -40,16 +105,57 @@ module Textus
|
|
|
40
105
|
)
|
|
41
106
|
end
|
|
42
107
|
|
|
43
|
-
Textus::Infra::Publisher.publish(source: row[:path], target: target_abs, store_root:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
target: target_abs,
|
|
50
|
-
correlation_id: @ctx.correlation_id)
|
|
108
|
+
Textus::Infra::Publisher.publish(source: row[:path], target: target_abs, store_root: @root)
|
|
109
|
+
publish_event(:file_published,
|
|
110
|
+
key: row[:key],
|
|
111
|
+
envelope: reader.call(row[:key]),
|
|
112
|
+
source: row[:path],
|
|
113
|
+
target: target_abs)
|
|
51
114
|
{ "key" => row[:key], "source" => row[:path], "target" => target_abs }
|
|
52
115
|
end
|
|
116
|
+
|
|
117
|
+
# Publish a standalone Leaf entry that has publish_to targets.
|
|
118
|
+
def publish_leaf_entry(mentry, repo_root)
|
|
119
|
+
source_path = @manifest.resolver.resolve(mentry.key).path
|
|
120
|
+
envelope = reader.call(mentry.key)
|
|
121
|
+
|
|
122
|
+
mentry.publish_to.each do |rel|
|
|
123
|
+
target_abs = File.join(repo_root, rel)
|
|
124
|
+
Textus::Infra::Publisher.publish(source: source_path, target: target_abs, store_root: @root)
|
|
125
|
+
publish_event(:file_published,
|
|
126
|
+
key: mentry.key,
|
|
127
|
+
envelope: envelope,
|
|
128
|
+
source: source_path,
|
|
129
|
+
target: target_abs)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
{ "key" => mentry.key, "path" => source_path, "published_to" => mentry.publish_to }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Whether the entry should be processed for the given prefix filter.
|
|
136
|
+
def entry_matches_prefix?(mentry, prefix)
|
|
137
|
+
return true unless prefix
|
|
138
|
+
|
|
139
|
+
case mentry
|
|
140
|
+
when Textus::Manifest::Entry::Nested
|
|
141
|
+
# Nested: process if the entry key is a prefix of `prefix` or
|
|
142
|
+
# `prefix` is a prefix of the entry key (a leaf under it).
|
|
143
|
+
mentry.key.start_with?(prefix) ||
|
|
144
|
+
prefix.start_with?("#{mentry.key}.")
|
|
145
|
+
else
|
|
146
|
+
mentry.key.start_with?(prefix)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def reader
|
|
151
|
+
@reader ||= Textus::Application::Reads::Get.new(
|
|
152
|
+
ctx: @ctx, manifest: @manifest, file_store: @file_store,
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def publish_event(event, **payload)
|
|
157
|
+
@bus.publish(event, ctx: @hook_context, **payload)
|
|
158
|
+
end
|
|
53
159
|
end
|
|
54
160
|
end
|
|
55
161
|
end
|
|
@@ -2,16 +2,20 @@ module Textus
|
|
|
2
2
|
module Application
|
|
3
3
|
module Writes
|
|
4
4
|
class Put
|
|
5
|
-
def initialize(ctx:, envelope_io:)
|
|
6
|
-
@ctx
|
|
7
|
-
@
|
|
5
|
+
def initialize(ctx:, manifest:, envelope_io:, bus:, authorizer:, hook_context:)
|
|
6
|
+
@ctx = ctx
|
|
7
|
+
@manifest = manifest
|
|
8
|
+
@envelope_io = envelope_io
|
|
9
|
+
@bus = bus
|
|
10
|
+
@authorizer = authorizer
|
|
11
|
+
@hook_context = hook_context
|
|
8
12
|
end
|
|
9
13
|
|
|
10
|
-
def call(key, meta: nil, body: nil, content: nil, if_etag: nil
|
|
11
|
-
@
|
|
12
|
-
mentry = @
|
|
14
|
+
def call(key, meta: nil, body: nil, content: nil, if_etag: nil)
|
|
15
|
+
@manifest.validate_key!(key)
|
|
16
|
+
mentry = @manifest.resolver.resolve(key).entry
|
|
13
17
|
|
|
14
|
-
@
|
|
18
|
+
@authorizer.authorize_write!(mentry, role: @ctx.role)
|
|
15
19
|
|
|
16
20
|
envelope = @envelope_io.write(
|
|
17
21
|
key,
|
|
@@ -20,13 +24,10 @@ module Textus
|
|
|
20
24
|
if_etag: if_etag,
|
|
21
25
|
)
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
envelope: envelope,
|
|
28
|
-
correlation_id: @ctx.correlation_id)
|
|
29
|
-
end
|
|
27
|
+
@bus.publish(:entry_put,
|
|
28
|
+
ctx: @hook_context,
|
|
29
|
+
key: key,
|
|
30
|
+
envelope: envelope)
|
|
30
31
|
|
|
31
32
|
envelope
|
|
32
33
|
end
|
|
@@ -2,32 +2,41 @@ module Textus
|
|
|
2
2
|
module Application
|
|
3
3
|
module Writes
|
|
4
4
|
class Reject
|
|
5
|
-
def initialize(ctx:, envelope_io:)
|
|
6
|
-
@ctx
|
|
7
|
-
@
|
|
5
|
+
def initialize(ctx:, manifest:, file_store:, envelope_io:, bus:, authorizer:, hook_context:) # rubocop:disable Metrics/ParameterLists
|
|
6
|
+
@ctx = ctx
|
|
7
|
+
@manifest = manifest
|
|
8
|
+
@file_store = file_store
|
|
9
|
+
@envelope_io = envelope_io
|
|
10
|
+
@bus = bus
|
|
11
|
+
@authorizer = authorizer
|
|
12
|
+
@hook_context = hook_context
|
|
8
13
|
end
|
|
9
14
|
|
|
10
15
|
def call(pending_key)
|
|
11
16
|
raise ProposalError.new("only human role can reject proposals; got '#{@ctx.role}'") unless @ctx.role == "human"
|
|
12
17
|
|
|
13
|
-
mentry = @
|
|
18
|
+
mentry = @manifest.resolver.resolve(pending_key).entry
|
|
14
19
|
unless mentry.in_proposal_zone?
|
|
15
20
|
raise ProposalError.new("reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.zone})")
|
|
16
21
|
end
|
|
17
22
|
|
|
18
|
-
env = Textus::Application::Reads::Get.new(
|
|
23
|
+
env = Textus::Application::Reads::Get.new(
|
|
24
|
+
ctx: @ctx, manifest: @manifest, file_store: @file_store,
|
|
25
|
+
).call(pending_key)
|
|
19
26
|
proposal = env.meta&.dig("proposal") or
|
|
20
27
|
raise ProposalError.new("entry has no proposal block: #{pending_key}")
|
|
21
28
|
target_key = proposal["target_key"] or
|
|
22
29
|
raise ProposalError.new("proposal missing target_key")
|
|
23
30
|
|
|
24
|
-
Textus::Application::Writes::Delete.new(
|
|
31
|
+
Textus::Application::Writes::Delete.new(
|
|
32
|
+
ctx: @ctx, manifest: @manifest, envelope_io: @envelope_io,
|
|
33
|
+
bus: @bus, authorizer: @authorizer, hook_context: @hook_context
|
|
34
|
+
).call(pending_key, suppress_events: true)
|
|
25
35
|
|
|
26
|
-
@
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
correlation_id: @ctx.correlation_id)
|
|
36
|
+
@bus.publish(:proposal_rejected,
|
|
37
|
+
ctx: @hook_context,
|
|
38
|
+
key: pending_key,
|
|
39
|
+
target_key: target_key)
|
|
31
40
|
|
|
32
41
|
{ "protocol" => PROTOCOL, "rejected" => pending_key, "target_key" => target_key }
|
|
33
42
|
end
|
|
@@ -7,11 +7,15 @@ module Textus
|
|
|
7
7
|
# Returns a new hash with _meta as the first key, per SPEC §6 ordering.
|
|
8
8
|
def self.call(content_hash, mentry)
|
|
9
9
|
meta = { "generated_at" => Time.now.utc.iso8601 }
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
if mentry.is_a?(Textus::Manifest::Entry::Derived)
|
|
11
|
+
src = mentry.source
|
|
12
|
+
if src.is_a?(Textus::Manifest::Entry::Derived::Projection)
|
|
13
|
+
from = Array(src.select).compact
|
|
14
|
+
meta["from"] = from unless from.empty?
|
|
15
|
+
meta["reduce"] = src.transform if src.transform
|
|
16
|
+
end
|
|
17
|
+
end
|
|
12
18
|
meta["template"] = mentry.template if mentry.template
|
|
13
|
-
reduce = mentry.projection&.dig("transform")
|
|
14
|
-
meta["reduce"] = reduce if reduce
|
|
15
19
|
|
|
16
20
|
out = { "_meta" => meta }
|
|
17
21
|
content_hash.each { |k, v| out[k] = v unless k == "_meta" }
|
|
@@ -58,22 +62,23 @@ module Textus
|
|
|
58
62
|
}
|
|
59
63
|
end
|
|
60
64
|
|
|
61
|
-
|
|
65
|
+
# rubocop:disable Metrics/ParameterLists
|
|
66
|
+
def self.run(mentry:, manifest:, reader:, lister:, transform_resolver:, template_loader:,
|
|
67
|
+
transform_context: nil, inject_intro: nil)
|
|
62
68
|
# 1. Load sources + project + reduce
|
|
63
69
|
data =
|
|
64
|
-
if mentry.projection
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
transform_context: Application::Context.system(store),
|
|
70
|
+
if mentry.is_a?(Textus::Manifest::Entry::Derived) && mentry.projection?
|
|
71
|
+
Application::Projection.new(
|
|
72
|
+
reader: reader,
|
|
73
|
+
spec: mentry.source.to_h.transform_keys(&:to_s),
|
|
74
|
+
lister: lister,
|
|
75
|
+
transform_resolver: transform_resolver,
|
|
76
|
+
transform_context: transform_context,
|
|
72
77
|
).run
|
|
73
78
|
else
|
|
74
79
|
{ "entries" => [], "count" => 0, "generated_at" => Time.now.utc.iso8601 }
|
|
75
80
|
end
|
|
76
|
-
data = data.merge("intro" =>
|
|
81
|
+
data = data.merge("intro" => inject_intro.call) if mentry.inject_intro && inject_intro
|
|
77
82
|
|
|
78
83
|
# 2. Render
|
|
79
84
|
klass = renderers[mentry.format] or
|
|
@@ -81,12 +86,13 @@ module Textus
|
|
|
81
86
|
bytes = klass.new(template_loader: template_loader).call(mentry: mentry, data: data)
|
|
82
87
|
|
|
83
88
|
# 3. Write (idempotent: skip if only generated_at would differ)
|
|
84
|
-
target_path = Key::Path.resolve(
|
|
89
|
+
target_path = Key::Path.resolve(manifest, mentry)
|
|
85
90
|
FileUtils.mkdir_p(File.dirname(target_path))
|
|
86
91
|
write_if_changed(target_path, bytes, mentry.format)
|
|
87
92
|
|
|
88
93
|
target_path
|
|
89
94
|
end
|
|
95
|
+
# rubocop:enable Metrics/ParameterLists
|
|
90
96
|
|
|
91
97
|
def self.write_if_changed(target_path, bytes, format)
|
|
92
98
|
if File.exist?(target_path)
|
|
@@ -28,7 +28,10 @@ module Textus
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def default_shape(mentry, data)
|
|
31
|
-
|
|
31
|
+
has_transform = mentry.is_a?(Textus::Manifest::Entry::Derived) &&
|
|
32
|
+
mentry.source.is_a?(Textus::Manifest::Entry::Derived::Projection) &&
|
|
33
|
+
mentry.source.transform
|
|
34
|
+
if has_transform && data.is_a?(Hash) && !data.key?("entries")
|
|
32
35
|
data
|
|
33
36
|
elsif data.is_a?(Hash) && data["entries"].is_a?(Array)
|
|
34
37
|
{ "entries" => data["entries"] }
|
|
@@ -8,10 +8,16 @@ module Textus
|
|
|
8
8
|
raise TemplateError.new("entry '#{mentry.key}': markdown build requires a template") unless mentry.template
|
|
9
9
|
|
|
10
10
|
body = Mustache.render(@template_loader.call(mentry.template), data)
|
|
11
|
+
from = if mentry.is_a?(Textus::Manifest::Entry::Derived) &&
|
|
12
|
+
mentry.source.is_a?(Textus::Manifest::Entry::Derived::Projection)
|
|
13
|
+
Array(mentry.source.select).compact
|
|
14
|
+
else
|
|
15
|
+
[]
|
|
16
|
+
end
|
|
11
17
|
frontmatter = {
|
|
12
18
|
"generated" => {
|
|
13
19
|
"at" => Time.now.utc.iso8601,
|
|
14
|
-
"from" =>
|
|
20
|
+
"from" => from,
|
|
15
21
|
},
|
|
16
22
|
}
|
|
17
23
|
Entry.for_format("markdown").serialize(meta: frontmatter, body: body)
|
|
@@ -28,7 +28,10 @@ module Textus
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def default_shape(mentry, data)
|
|
31
|
-
|
|
31
|
+
has_transform = mentry.is_a?(Textus::Manifest::Entry::Derived) &&
|
|
32
|
+
mentry.source.is_a?(Textus::Manifest::Entry::Derived::Projection) &&
|
|
33
|
+
mentry.source.transform
|
|
34
|
+
if has_transform && data.is_a?(Hash) && !data.key?("entries")
|
|
32
35
|
data
|
|
33
36
|
elsif data.is_a?(Hash) && data["entries"].is_a?(Array)
|
|
34
37
|
{ "entries" => data["entries"] }
|
|
@@ -9,11 +9,8 @@ module Textus
|
|
|
9
9
|
def call(store)
|
|
10
10
|
Textus::Infra::BuildLock.with(root: store.root) do
|
|
11
11
|
ops = Textus::Operations.for(store, role: "builder")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
emit({ "protocol" => Textus::PROTOCOL,
|
|
15
|
-
"built" => build_res["built"],
|
|
16
|
-
"published_leaves" => publish_res["published_leaves"] })
|
|
12
|
+
result = ops.publish(prefix: prefix)
|
|
13
|
+
emit(result)
|
|
17
14
|
end
|
|
18
15
|
end
|
|
19
16
|
end
|
data/lib/textus/cli/verb/get.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Textus
|
|
|
9
9
|
def call(store)
|
|
10
10
|
key = positional.shift or raise UsageError.new("get requires a key")
|
|
11
11
|
result = operations_for(store).get_or_refresh(key)
|
|
12
|
-
raise Textus::UnknownKey.new(key, suggestions: store.manifest.suggestions_for(key)) if result.nil?
|
|
12
|
+
raise Textus::UnknownKey.new(key, suggestions: store.manifest.resolver.suggestions_for(key)) if result.nil?
|
|
13
13
|
|
|
14
14
|
emit(result.to_h_for_wire)
|
|
15
15
|
end
|
|
@@ -26,13 +26,12 @@ module Textus
|
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
callable = store.
|
|
31
|
-
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)
|
|
32
31
|
|
|
33
32
|
begin
|
|
34
33
|
Timeout.timeout(Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS) do
|
|
35
|
-
callable.call(config: {}, store:
|
|
34
|
+
callable.call(config: {}, store: store, args: args)
|
|
36
35
|
end
|
|
37
36
|
rescue Timeout::Error
|
|
38
37
|
raise UsageError.new(
|