textus 0.15.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +50 -55
- data/CHANGELOG.md +486 -0
- data/README.md +13 -9
- data/SPEC.md +13 -10
- data/docs/conventions.md +2 -2
- data/lib/textus/application/context.rb +20 -34
- 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 +11 -8
- data/lib/textus/application/reads/deps.rb +14 -3
- data/lib/textus/application/reads/freshness.rb +17 -6
- data/lib/textus/application/reads/get.rb +37 -11
- data/lib/textus/application/reads/get_or_refresh.rb +8 -8
- data/lib/textus/application/reads/list.rb +5 -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 +6 -3
- 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 +12 -3
- data/lib/textus/application/reads/validator.rb +84 -0
- data/lib/textus/application/reads/where.rb +6 -3
- data/lib/textus/application/refresh/all.rb +16 -5
- data/lib/textus/application/refresh/orchestrator.rb +9 -9
- data/lib/textus/application/refresh/worker.rb +59 -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 +36 -13
- data/lib/textus/application/writes/delete.rb +13 -15
- data/lib/textus/application/writes/envelope_io.rb +166 -0
- data/lib/textus/application/writes/materializer.rb +50 -0
- data/lib/textus/application/writes/mv.rb +56 -95
- data/lib/textus/application/writes/publish.rb +132 -27
- data/lib/textus/application/writes/put.rb +17 -20
- data/lib/textus/application/writes/reject.rb +18 -9
- 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/group/hook.rb +1 -3
- data/lib/textus/cli/group/key.rb +1 -4
- data/lib/textus/cli/group/refresh.rb +1 -2
- data/lib/textus/cli/group/rule.rb +1 -3
- data/lib/textus/cli/group/schema.rb +1 -5
- data/lib/textus/cli/group.rb +12 -16
- data/lib/textus/cli/verb/accept.rb +3 -1
- data/lib/textus/cli/verb/audit.rb +3 -1
- data/lib/textus/cli/verb/blame.rb +3 -1
- data/lib/textus/cli/verb/build.rb +4 -5
- data/lib/textus/cli/verb/delete.rb +3 -1
- data/lib/textus/cli/verb/deps.rb +3 -1
- data/lib/textus/cli/verb/doctor.rb +2 -0
- data/lib/textus/cli/verb/freshness.rb +3 -1
- data/lib/textus/cli/verb/get.rb +4 -2
- data/lib/textus/cli/verb/hook_run.rb +6 -4
- data/lib/textus/cli/verb/hooks.rb +8 -5
- data/lib/textus/cli/verb/init.rb +2 -0
- data/lib/textus/cli/verb/intro.rb +2 -0
- data/lib/textus/cli/verb/key_normalize.rb +35 -3
- data/lib/textus/cli/verb/list.rb +3 -1
- data/lib/textus/cli/verb/mv.rb +4 -1
- data/lib/textus/cli/verb/published.rb +3 -1
- data/lib/textus/cli/verb/put.rb +5 -4
- data/lib/textus/cli/verb/rdeps.rb +3 -1
- data/lib/textus/cli/verb/refresh.rb +1 -1
- data/lib/textus/cli/verb/refresh_stale.rb +4 -2
- data/lib/textus/cli/verb/reject.rb +3 -1
- data/lib/textus/cli/verb/rule_explain.rb +4 -1
- data/lib/textus/cli/verb/rule_list.rb +3 -0
- data/lib/textus/cli/verb/schema.rb +4 -1
- data/lib/textus/cli/verb/schema_diff.rb +3 -0
- data/lib/textus/cli/verb/schema_init.rb +3 -0
- data/lib/textus/cli/verb/schema_migrate.rb +3 -0
- data/lib/textus/cli/verb/uid.rb +4 -1
- data/lib/textus/cli/verb/where.rb +3 -1
- data/lib/textus/cli/verb.rb +30 -0
- data/lib/textus/cli.rb +18 -27
- data/lib/textus/doctor/check/audit_log.rb +1 -1
- data/lib/textus/doctor/check/handler_allowlist.rb +3 -2
- data/lib/textus/doctor/check/hooks.rb +4 -2
- data/lib/textus/doctor/check/illegal_keys.rb +6 -5
- data/lib/textus/doctor/check/intake_registration.rb +5 -5
- 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/schema_violations.rb +1 -1
- data/lib/textus/doctor/check/sentinels.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/freshness/evaluator.rb +1 -1
- data/lib/textus/domain/freshness/policy.rb +1 -1
- data/lib/textus/domain/freshness/verdict.rb +1 -1
- data/lib/textus/domain/freshness.rb +40 -0
- data/lib/textus/{store → domain}/sentinel.rb +1 -1
- data/lib/textus/{store → domain}/staleness/generator_check.rb +9 -8
- data/lib/textus/{store → domain}/staleness/intake_check.rb +3 -3
- data/lib/textus/{store → domain}/staleness.rb +1 -1
- data/lib/textus/entry/json.rb +1 -1
- data/lib/textus/entry/markdown.rb +1 -1
- data/lib/textus/entry/yaml.rb +1 -1
- data/lib/textus/envelope.rb +7 -3
- data/lib/textus/errors.rb +19 -0
- 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 +20 -17
- data/lib/textus/{store → infra}/audit_log.rb +1 -1
- data/lib/textus/infra/audit_subscriber.rb +43 -0
- data/lib/textus/infra/event_bus.rb +3 -3
- data/lib/textus/infra/publisher.rb +3 -3
- data/lib/textus/infra/refresh/detached.rb +1 -1
- data/lib/textus/infra/storage/file_store.rb +26 -0
- data/lib/textus/init.rb +14 -11
- 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/resolution.rb +5 -0
- data/lib/textus/manifest/resolver.rb +109 -0
- data/lib/textus/manifest/schema.rb +1 -1
- data/lib/textus/manifest.rb +4 -100
- data/lib/textus/operations.rb +147 -23
- data/lib/textus/schema/tools.rb +7 -7
- data/lib/textus/schemas.rb +46 -0
- data/lib/textus/store.rb +12 -49
- data/lib/textus/uid.rb +18 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +17 -1
- metadata +31 -23
- data/lib/textus/application/writes/build.rb +0 -79
- 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 -63
- data/lib/textus/hooks/dsl.rb +0 -11
- data/lib/textus/hooks/registry.rb +0 -81
- data/lib/textus/migrate_keys.rb +0 -187
- data/lib/textus/operations/reads.rb +0 -56
- data/lib/textus/operations/refresh.rb +0 -27
- data/lib/textus/operations/writes.rb +0 -21
- data/lib/textus/projection.rb +0 -89
- data/lib/textus/refresh.rb +0 -39
- data/lib/textus/store/reader.rb +0 -69
- data/lib/textus/store/validator.rb +0 -82
- data/lib/textus/store/writer.rb +0 -102
|
@@ -1,38 +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:, bus:)
|
|
9
|
-
@ctx
|
|
10
|
-
@
|
|
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
|
|
11
20
|
end
|
|
12
21
|
|
|
13
22
|
def call(prefix: nil)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
next unless mentry.nested && mentry.publish_each
|
|
18
|
-
next if prefix && !mentry.key.start_with?(prefix) && !prefix.start_with?("#{mentry.key}.")
|
|
23
|
+
built = []
|
|
24
|
+
leaves = []
|
|
25
|
+
repo_root = File.dirname(@root)
|
|
19
26
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
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)
|
|
23
29
|
|
|
24
|
-
|
|
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
|
|
25
45
|
end
|
|
26
46
|
end
|
|
27
|
-
|
|
47
|
+
|
|
48
|
+
{ "protocol" => Textus::PROTOCOL, "built" => built, "published_leaves" => leaves }
|
|
28
49
|
end
|
|
29
50
|
|
|
30
51
|
private
|
|
31
52
|
|
|
32
|
-
|
|
33
|
-
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)
|
|
34
59
|
|
|
35
|
-
|
|
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)
|
|
36
100
|
target_rel = mentry.publish_target_for(row[:key])
|
|
37
101
|
target_abs = File.expand_path(File.join(repo_root, target_rel))
|
|
38
102
|
unless target_abs.start_with?(File.expand_path(repo_root) + File::SEPARATOR)
|
|
@@ -41,16 +105,57 @@ module Textus
|
|
|
41
105
|
)
|
|
42
106
|
end
|
|
43
107
|
|
|
44
|
-
Textus::Infra::Publisher.publish(source: row[:path], target: target_abs, store_root:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
target: target_abs,
|
|
51
|
-
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)
|
|
52
114
|
{ "key" => row[:key], "source" => row[:path], "target" => target_abs }
|
|
53
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
|
|
54
159
|
end
|
|
55
160
|
end
|
|
56
161
|
end
|
|
@@ -2,35 +2,32 @@ module Textus
|
|
|
2
2
|
module Application
|
|
3
3
|
module Writes
|
|
4
4
|
class Put
|
|
5
|
-
def initialize(ctx:, bus:)
|
|
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
|
-
|
|
15
|
-
raise WriteForbidden.new(key, mentry.zone,
|
|
16
|
-
writers: @ctx.store.manifest.zone_writers(mentry.zone))
|
|
17
|
-
end
|
|
18
|
+
@authorizer.authorize_write!(mentry, role: @ctx.role)
|
|
18
19
|
|
|
19
|
-
envelope = @
|
|
20
|
+
envelope = @envelope_io.write(
|
|
20
21
|
key,
|
|
21
22
|
mentry: mentry,
|
|
22
|
-
payload: Textus::
|
|
23
|
-
ctx: @ctx,
|
|
23
|
+
payload: Textus::Application::Writes::EnvelopeIO::Payload.new(meta: meta, body: body, content: content),
|
|
24
24
|
if_etag: if_etag,
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
envelope: envelope,
|
|
32
|
-
correlation_id: @ctx.correlation_id)
|
|
33
|
-
end
|
|
27
|
+
@bus.publish(:entry_put,
|
|
28
|
+
ctx: @hook_context,
|
|
29
|
+
key: key,
|
|
30
|
+
envelope: envelope)
|
|
34
31
|
|
|
35
32
|
envelope
|
|
36
33
|
end
|
|
@@ -2,32 +2,41 @@ module Textus
|
|
|
2
2
|
module Application
|
|
3
3
|
module Writes
|
|
4
4
|
class Reject
|
|
5
|
-
def initialize(ctx:, bus:)
|
|
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 =
|
|
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
36
|
@bus.publish(:proposal_rejected,
|
|
27
|
-
|
|
37
|
+
ctx: @hook_context,
|
|
28
38
|
key: pending_key,
|
|
29
|
-
target_key: target_key
|
|
30
|
-
correlation_id: @ctx.correlation_id)
|
|
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"] }
|
data/lib/textus/cli/group/key.rb
CHANGED
|
@@ -2,11 +2,7 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Group
|
|
4
4
|
class Schema < Group
|
|
5
|
-
|
|
6
|
-
subcommands["show"] = Verb::Schema
|
|
7
|
-
subcommands["init"] = Verb::SchemaInit
|
|
8
|
-
subcommands["diff"] = Verb::SchemaDiff
|
|
9
|
-
subcommands["migrate"] = Verb::SchemaMigrate
|
|
5
|
+
command_name "schema"
|
|
10
6
|
end
|
|
11
7
|
end
|
|
12
8
|
end
|
data/lib/textus/cli/group.rb
CHANGED
|
@@ -2,19 +2,14 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Group < Verb
|
|
4
4
|
class << self
|
|
5
|
+
# Subcommands are auto-derived: any Verb descendant whose
|
|
6
|
+
# `parent_group` is this group counts as a subcommand. Sorted
|
|
7
|
+
# alphabetically by command_name for stable help output.
|
|
5
8
|
def subcommands
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@cli_name || raise("subclass must define cli_name")
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
attr_writer :cli_name
|
|
14
|
-
|
|
15
|
-
def inherited(subclass)
|
|
16
|
-
super
|
|
17
|
-
subclass.instance_variable_set(:@subcommands, {})
|
|
9
|
+
Verb.descendants
|
|
10
|
+
.select { |k| k.parent_group == self && k.command_name }
|
|
11
|
+
.sort_by(&:command_name)
|
|
12
|
+
.to_h { |k| [k.command_name, k] }
|
|
18
13
|
end
|
|
19
14
|
|
|
20
15
|
def needs_store?
|
|
@@ -24,18 +19,19 @@ module Textus
|
|
|
24
19
|
end
|
|
25
20
|
|
|
26
21
|
def parse(argv)
|
|
22
|
+
subs = self.class.subcommands
|
|
27
23
|
subname = argv.shift
|
|
28
24
|
if subname.nil?
|
|
29
25
|
raise UsageError.new(
|
|
30
|
-
"#{self.class.
|
|
26
|
+
"#{self.class.command_name} requires a subcommand: #{subs.keys.join(", ")}",
|
|
31
27
|
)
|
|
32
28
|
end
|
|
33
29
|
|
|
34
|
-
@sub_klass =
|
|
30
|
+
@sub_klass = subs[subname]
|
|
35
31
|
unless @sub_klass
|
|
36
32
|
raise UsageError.new(
|
|
37
|
-
"unknown #{self.class.
|
|
38
|
-
"Valid: #{
|
|
33
|
+
"unknown #{self.class.command_name} subcommand '#{subname}'. " \
|
|
34
|
+
"Valid: #{subs.keys.join(", ")}",
|
|
39
35
|
)
|
|
40
36
|
end
|
|
41
37
|
|
|
@@ -2,11 +2,13 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Accept < Verb
|
|
5
|
+
command_name "accept"
|
|
6
|
+
|
|
5
7
|
option :as_flag, "--as=ROLE"
|
|
6
8
|
|
|
7
9
|
def call(store)
|
|
8
10
|
key = positional.shift or raise UsageError.new("accept requires a key")
|
|
9
|
-
emit(operations_for(store).
|
|
11
|
+
emit(operations_for(store).accept(key))
|
|
10
12
|
end
|
|
11
13
|
end
|
|
12
14
|
end
|
|
@@ -2,6 +2,8 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Audit < Verb
|
|
5
|
+
command_name "audit"
|
|
6
|
+
|
|
5
7
|
option :key_filter, "--key=KEY"
|
|
6
8
|
option :zone, "--zone=Z"
|
|
7
9
|
option :role_filter, "--role=ROLE"
|
|
@@ -13,7 +15,7 @@ module Textus
|
|
|
13
15
|
def call(store)
|
|
14
16
|
ops = operations_for(store)
|
|
15
17
|
since_time = since && Textus::Application::Reads::Audit.parse_since(since, now: ops.ctx.now)
|
|
16
|
-
rows = ops.
|
|
18
|
+
rows = ops.audit(
|
|
17
19
|
key: key_filter,
|
|
18
20
|
zone: zone,
|
|
19
21
|
role: role_filter,
|
|
@@ -2,11 +2,13 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Blame < Verb
|
|
5
|
+
command_name "blame"
|
|
6
|
+
|
|
5
7
|
option :limit, "--limit=N"
|
|
6
8
|
|
|
7
9
|
def call(store)
|
|
8
10
|
key = positional.shift or raise UsageError.new("blame requires a key")
|
|
9
|
-
rows = operations_for(store).
|
|
11
|
+
rows = operations_for(store).blame(key: key, limit: limit&.to_i)
|
|
10
12
|
emit({ "verb" => "blame", "key" => key, "rows" => rows })
|
|
11
13
|
end
|
|
12
14
|
end
|
|
@@ -2,16 +2,15 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Build < Verb
|
|
5
|
+
command_name "build"
|
|
6
|
+
|
|
5
7
|
option :prefix, "--prefix=K"
|
|
6
8
|
|
|
7
9
|
def call(store)
|
|
8
10
|
Textus::Infra::BuildLock.with(root: store.root) do
|
|
9
11
|
ops = Textus::Operations.for(store, role: "builder")
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
emit({ "protocol" => Textus::PROTOCOL,
|
|
13
|
-
"built" => build_res["built"],
|
|
14
|
-
"published_leaves" => publish_res["published_leaves"] })
|
|
12
|
+
result = ops.publish(prefix: prefix)
|
|
13
|
+
emit(result)
|
|
15
14
|
end
|
|
16
15
|
end
|
|
17
16
|
end
|
|
@@ -2,12 +2,14 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Delete < Verb
|
|
5
|
+
command_name "delete"
|
|
6
|
+
|
|
5
7
|
option :as_flag, "--as=ROLE"
|
|
6
8
|
option :if_etag, "--if-etag=E"
|
|
7
9
|
|
|
8
10
|
def call(store)
|
|
9
11
|
key = positional.shift or raise UsageError.new("delete requires a key")
|
|
10
|
-
emit(operations_for(store).
|
|
12
|
+
emit(operations_for(store).delete(key, if_etag: if_etag))
|
|
11
13
|
end
|
|
12
14
|
end
|
|
13
15
|
end
|
data/lib/textus/cli/verb/deps.rb
CHANGED
|
@@ -2,9 +2,11 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Deps < Verb
|
|
5
|
+
command_name "deps"
|
|
6
|
+
|
|
5
7
|
def call(store)
|
|
6
8
|
key = positional.shift or raise UsageError.new("deps requires a key")
|
|
7
|
-
emit({ "key" => key, "deps" => operations_for(store).
|
|
9
|
+
emit({ "key" => key, "deps" => operations_for(store).deps(key) })
|
|
8
10
|
end
|
|
9
11
|
end
|
|
10
12
|
end
|
|
@@ -2,11 +2,13 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Freshness < Verb
|
|
5
|
+
command_name "freshness"
|
|
6
|
+
|
|
5
7
|
option :prefix, "--prefix=KEY"
|
|
6
8
|
option :zone, "--zone=Z"
|
|
7
9
|
|
|
8
10
|
def call(store)
|
|
9
|
-
rows = operations_for(store).
|
|
11
|
+
rows = operations_for(store).freshness(prefix: prefix, zone: zone)
|
|
10
12
|
emit({ "verb" => "freshness", "rows" => rows })
|
|
11
13
|
end
|
|
12
14
|
end
|