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,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)
|
|
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)
|
|
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
|
|
@@ -1,33 +1,46 @@
|
|
|
1
|
+
require_relative "authority_gate"
|
|
2
|
+
|
|
1
3
|
module Textus
|
|
2
4
|
module Application
|
|
3
5
|
module Writes
|
|
4
6
|
class Reject
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
include AuthorityGate
|
|
8
|
+
|
|
9
|
+
def initialize(ctx:, manifest:, file_store:, envelope_io:, bus:, authorizer:, hook_context:) # rubocop:disable Metrics/ParameterLists
|
|
10
|
+
@ctx = ctx
|
|
11
|
+
@manifest = manifest
|
|
12
|
+
@file_store = file_store
|
|
13
|
+
@envelope_io = envelope_io
|
|
14
|
+
@bus = bus
|
|
15
|
+
@authorizer = authorizer
|
|
16
|
+
@hook_context = hook_context
|
|
8
17
|
end
|
|
9
18
|
|
|
10
19
|
def call(pending_key)
|
|
11
|
-
|
|
20
|
+
assert_accept_authority!("reject")
|
|
12
21
|
|
|
13
|
-
mentry = @
|
|
22
|
+
mentry = @manifest.resolver.resolve(pending_key).entry
|
|
14
23
|
unless mentry.in_proposal_zone?
|
|
15
24
|
raise ProposalError.new("reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.zone})")
|
|
16
25
|
end
|
|
17
26
|
|
|
18
|
-
env = Textus::Application::Reads::Get.new(
|
|
27
|
+
env = Textus::Application::Reads::Get.new(
|
|
28
|
+
ctx: @ctx, manifest: @manifest, file_store: @file_store,
|
|
29
|
+
).call(pending_key)
|
|
19
30
|
proposal = env.meta&.dig("proposal") or
|
|
20
31
|
raise ProposalError.new("entry has no proposal block: #{pending_key}")
|
|
21
32
|
target_key = proposal["target_key"] or
|
|
22
33
|
raise ProposalError.new("proposal missing target_key")
|
|
23
34
|
|
|
24
|
-
Textus::Application::Writes::Delete.new(
|
|
35
|
+
Textus::Application::Writes::Delete.new(
|
|
36
|
+
ctx: @ctx, manifest: @manifest, envelope_io: @envelope_io,
|
|
37
|
+
bus: @bus, authorizer: @authorizer, hook_context: @hook_context
|
|
38
|
+
).call(pending_key, suppress_events: true)
|
|
25
39
|
|
|
26
|
-
@
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
correlation_id: @ctx.correlation_id)
|
|
40
|
+
@bus.publish(:proposal_rejected,
|
|
41
|
+
ctx: @hook_context,
|
|
42
|
+
key: pending_key,
|
|
43
|
+
target_key: target_key)
|
|
31
44
|
|
|
32
45
|
{ "protocol" => PROTOCOL, "rejected" => pending_key, "target_key" => target_key }
|
|
33
46
|
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"] }
|
|
@@ -8,12 +8,10 @@ module Textus
|
|
|
8
8
|
|
|
9
9
|
def call(store)
|
|
10
10
|
Textus::Infra::BuildLock.with(root: store.root) do
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
emit(
|
|
15
|
-
"built" => build_res["built"],
|
|
16
|
-
"published_leaves" => publish_res["published_leaves"] })
|
|
11
|
+
role = store.manifest.roles_with_kind(:generator).first || "builder"
|
|
12
|
+
ops = Textus::Operations.for(store, role: role)
|
|
13
|
+
result = ops.publish(prefix: prefix)
|
|
14
|
+
emit(result)
|
|
17
15
|
end
|
|
18
16
|
end
|
|
19
17
|
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(
|
|
@@ -7,7 +7,7 @@ module Textus
|
|
|
7
7
|
|
|
8
8
|
option :event_filter, "--event=E"
|
|
9
9
|
|
|
10
|
-
def call(store) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
10
|
+
def call(store) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
11
11
|
subcommand = positional.first
|
|
12
12
|
if subcommand
|
|
13
13
|
raise UsageError.new("hook requires 'list'") unless subcommand == "list"
|
|
@@ -16,15 +16,15 @@ module Textus
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
rows = []
|
|
19
|
-
Textus::Hooks::
|
|
19
|
+
Textus::Hooks::Bus::EVENTS.each do |event, spec|
|
|
20
20
|
mode = spec[:mode].to_s
|
|
21
21
|
case spec[:mode]
|
|
22
22
|
when :rpc
|
|
23
|
-
store.
|
|
23
|
+
store.bus.rpc_names(event).each do |name|
|
|
24
24
|
rows << { "event" => event.to_s, "mode" => mode, "name" => name.to_s }
|
|
25
25
|
end
|
|
26
26
|
when :pubsub
|
|
27
|
-
store.
|
|
27
|
+
store.bus.pubsub_handlers(event).each do |h|
|
|
28
28
|
row = { "event" => event.to_s, "mode" => mode, "name" => h[:name].to_s }
|
|
29
29
|
row["keys"] = Array(h[:keys]) if h[:keys]
|
|
30
30
|
rows << row
|
|
@@ -32,7 +32,7 @@ module Textus
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
store.manifest.entries.each do |e|
|
|
35
|
-
e.events.each do |evt, defs|
|
|
35
|
+
(e.respond_to?(:events) ? e.events : {}).each do |evt, defs|
|
|
36
36
|
Array(defs).each do |defn|
|
|
37
37
|
next unless defn["exec"]
|
|
38
38
|
|
data/lib/textus/cli/verb/put.rb
CHANGED
|
@@ -17,12 +17,11 @@ module Textus
|
|
|
17
17
|
raw = @stdin.read
|
|
18
18
|
payload =
|
|
19
19
|
if fetch_name
|
|
20
|
-
callable = store.
|
|
20
|
+
callable = store.bus.rpc_callable(:resolve_intake, fetch_name)
|
|
21
21
|
result =
|
|
22
22
|
begin
|
|
23
23
|
Timeout.timeout(Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS) do
|
|
24
|
-
callable.call(config: { "bytes" => raw },
|
|
25
|
-
store: Textus::Application::Context.new(store: store, role: role), args: {})
|
|
24
|
+
callable.call(config: { "bytes" => raw }, store: store, args: {})
|
|
26
25
|
end
|
|
27
26
|
rescue Timeout::Error
|
|
28
27
|
raise UsageError.new(
|
|
@@ -10,8 +10,7 @@ module Textus
|
|
|
10
10
|
option :as_flag, "--as=ROLE"
|
|
11
11
|
|
|
12
12
|
def call(store)
|
|
13
|
-
|
|
14
|
-
result = Textus::Application::Refresh::All.call(ctx, prefix: prefix, zone: zone)
|
|
13
|
+
result = operations_for(store).refresh_all(prefix: prefix, zone: zone)
|
|
15
14
|
emit(result)
|
|
16
15
|
result["ok"] ? 0 : 1
|
|
17
16
|
end
|
|
@@ -8,8 +8,9 @@ module Textus
|
|
|
8
8
|
def call
|
|
9
9
|
out = []
|
|
10
10
|
store.manifest.entries.each do |mentry|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
next unless mentry.is_a?(Textus::Manifest::Entry::Intake)
|
|
12
|
+
|
|
13
|
+
handler = mentry.handler
|
|
13
14
|
|
|
14
15
|
allow = store.manifest.rules_for(mentry.key).handler_allowlist
|
|
15
16
|
next if allow.nil?
|
|
@@ -8,11 +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
|
-
|
|
11
|
+
bus = Textus::Hooks::Bus.new
|
|
12
12
|
Textus.drain_hook_blocks
|
|
13
13
|
begin
|
|
14
14
|
load(f)
|
|
15
|
-
Textus.drain_hook_blocks.each { |b| b.call(
|
|
15
|
+
Textus.drain_hook_blocks.each { |b| b.call(bus) }
|
|
16
16
|
end
|
|
17
17
|
rescue StandardError, ScriptError => e
|
|
18
18
|
out << {
|
|
@@ -5,12 +5,13 @@ module Textus
|
|
|
5
5
|
def call
|
|
6
6
|
out = []
|
|
7
7
|
store.manifest.entries.each do |entry|
|
|
8
|
-
next unless entry.nested
|
|
8
|
+
next unless entry.nested?
|
|
9
9
|
|
|
10
10
|
base = File.join(store.root, "zones", entry.path)
|
|
11
11
|
next unless File.directory?(base)
|
|
12
12
|
|
|
13
|
-
entry.index_filename ?
|
|
13
|
+
index_fn = entry.respond_to?(:index_filename) ? entry.index_filename : nil
|
|
14
|
+
index_fn ? check_index_paths(entry, index_fn, base, out) : check_all_paths(base, out)
|
|
14
15
|
end
|
|
15
16
|
out
|
|
16
17
|
end
|
|
@@ -31,8 +32,8 @@ module Textus
|
|
|
31
32
|
# segments leading to each index file participate in keys. Sibling
|
|
32
33
|
# files and unrelated subtrees are not enumerated and must not be
|
|
33
34
|
# flagged. Each illegal segment is reported once per path.
|
|
34
|
-
def check_index_paths(
|
|
35
|
-
Dir.glob(File.join(base, "**",
|
|
35
|
+
def check_index_paths(_entry, index_fn, base, out)
|
|
36
|
+
Dir.glob(File.join(base, "**", index_fn)).each do |fp|
|
|
36
37
|
rel = fp.sub(%r{\A#{Regexp.escape(base)}/?}, "")
|
|
37
38
|
File.dirname(rel).split("/").reject { |s| s.empty? || s == "." }.each do |seg|
|
|
38
39
|
next if seg.match?(Key::Grammar::SEGMENT)
|
|
@@ -43,15 +44,14 @@ module Textus
|
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
def issue(abs_path, stem)
|
|
46
|
-
proposed = Textus::MigrateKeys.normalize(stem)
|
|
47
47
|
{
|
|
48
48
|
"code" => "key.illegal",
|
|
49
49
|
"level" => "error",
|
|
50
50
|
"subject" => abs_path,
|
|
51
51
|
"path" => abs_path,
|
|
52
|
-
"proposed_key" => proposed,
|
|
53
52
|
"message" => "illegal key segment '#{stem}' at #{abs_path}",
|
|
54
|
-
"fix" => "
|
|
53
|
+
"fix" => "rename the file/directory so each segment matches [a-z0-9][a-z0-9-]* " \
|
|
54
|
+
"(lowercase, digits, hyphens)",
|
|
55
55
|
}
|
|
56
56
|
end
|
|
57
57
|
|
|
@@ -6,7 +6,7 @@ module Textus
|
|
|
6
6
|
|
|
7
7
|
def call
|
|
8
8
|
declared = collect_declared_handlers
|
|
9
|
-
registered = store.
|
|
9
|
+
registered = store.bus.rpc_names(:resolve_intake).to_set
|
|
10
10
|
|
|
11
11
|
out = (declared - registered).map do |name|
|
|
12
12
|
{
|
|
@@ -36,7 +36,7 @@ module Textus
|
|
|
36
36
|
def collect_declared_handlers
|
|
37
37
|
set = Set.new
|
|
38
38
|
store.manifest.entries.each do |mentry|
|
|
39
|
-
set << mentry.
|
|
39
|
+
set << mentry.handler.to_sym if mentry.is_a?(Textus::Manifest::Entry::Intake)
|
|
40
40
|
end
|
|
41
41
|
set
|
|
42
42
|
end
|
|
@@ -19,7 +19,7 @@ module Textus
|
|
|
19
19
|
"code" => "protocol_mismatch",
|
|
20
20
|
"severity" => "error",
|
|
21
21
|
"message" => "Store reports version=#{version.inspect}; this gem expects textus/3.",
|
|
22
|
-
"hint" => "
|
|
22
|
+
"hint" => "Upgrade the store's manifest version to textus/3 (see CHANGELOG for breaking changes).",
|
|
23
23
|
}]
|
|
24
24
|
end
|
|
25
25
|
|
|
@@ -38,7 +38,7 @@ module Textus
|
|
|
38
38
|
"level" => "error",
|
|
39
39
|
"subject" => path,
|
|
40
40
|
"message" => "Store reports version=#{version.inspect}; this gem expects textus/3.",
|
|
41
|
-
"fix" => "
|
|
41
|
+
"fix" => "Upgrade the store's manifest version to textus/3 (see CHANGELOG for breaking changes).",
|
|
42
42
|
}]
|
|
43
43
|
end
|
|
44
44
|
end
|