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
|
@@ -4,7 +4,7 @@ module Textus
|
|
|
4
4
|
ROOT_KEYS = %w[version zones entries rules].freeze
|
|
5
5
|
ZONE_KEYS = %w[name write_policy read_policy].freeze
|
|
6
6
|
ENTRY_KEYS = %w[
|
|
7
|
-
key path zone schema owner nested format
|
|
7
|
+
key path zone kind schema owner nested format
|
|
8
8
|
compute template publish_to publish_each
|
|
9
9
|
intake events inject_intro index_filename
|
|
10
10
|
].freeze
|
data/lib/textus/manifest.rb
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
require "yaml"
|
|
2
2
|
require_relative "manifest/schema"
|
|
3
3
|
require_relative "manifest/resolution"
|
|
4
|
+
require_relative "manifest/resolver"
|
|
4
5
|
|
|
5
6
|
module Textus
|
|
6
7
|
class Manifest
|
|
7
|
-
TEXTUS_2_HINT = "Install textus 0.11.x to run the migrator, then upgrade to this version. " \
|
|
8
|
-
"See https://github.com/patrick204nqh/textus/blob/main/CHANGELOG.md#0110".freeze
|
|
9
|
-
|
|
10
|
-
def self.version_hint_for(version)
|
|
11
|
-
version == "textus/2" ? TEXTUS_2_HINT : nil
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
private_class_method :version_hint_for
|
|
15
|
-
|
|
16
8
|
attr_reader :root, :entries, :raw
|
|
17
9
|
|
|
18
10
|
def zones
|
|
@@ -59,7 +51,6 @@ module Textus
|
|
|
59
51
|
raise BadFrontmatter.new(
|
|
60
52
|
source,
|
|
61
53
|
"unsupported manifest version #{raw["version"].inspect}; expected #{PROTOCOL.inspect}",
|
|
62
|
-
hint: version_hint_for(raw["version"]),
|
|
63
54
|
)
|
|
64
55
|
end
|
|
65
56
|
private_class_method :check_version!
|
|
@@ -87,53 +78,8 @@ module Textus
|
|
|
87
78
|
rules.for(key)
|
|
88
79
|
end
|
|
89
80
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
validate_key!(key)
|
|
93
|
-
segments = key.split(".")
|
|
94
|
-
# longest-prefix match
|
|
95
|
-
candidates = @entries
|
|
96
|
-
.map { |e| [e, e.key.split(".")] }
|
|
97
|
-
.select { |(_, esegs)| esegs == segments[0, esegs.length] }
|
|
98
|
-
.sort_by { |(_, esegs)| -esegs.length }
|
|
99
|
-
raise UnknownKey.new(key, suggestions: suggestions_for(key)) if candidates.empty?
|
|
100
|
-
|
|
101
|
-
entry, esegs = candidates.first
|
|
102
|
-
remaining = segments[esegs.length..]
|
|
103
|
-
if remaining.empty?
|
|
104
|
-
path = resolve_leaf_path(entry)
|
|
105
|
-
Resolution.new(entry: entry, path: path, remaining: [])
|
|
106
|
-
else
|
|
107
|
-
raise UnknownKey.new(key, suggestions: suggestions_for(key)) unless entry.nested
|
|
108
|
-
|
|
109
|
-
path = if entry.index_filename
|
|
110
|
-
File.join(@root, "zones", entry.path, *remaining, entry.index_filename)
|
|
111
|
-
else
|
|
112
|
-
primary_ext = Textus::Entry.for_format(entry.format).extensions.first
|
|
113
|
-
File.join(@root, "zones", entry.path, *remaining) + primary_ext
|
|
114
|
-
end
|
|
115
|
-
Resolution.new(entry: entry, path: path, remaining: remaining)
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Returns up to 5 dotted keys from the manifest that look similar to the
|
|
120
|
-
# requested key, ranked by shared-prefix length then Levenshtein distance.
|
|
121
|
-
def suggestions_for(key)
|
|
122
|
-
candidates = enumerate.map { |r| r[:key] }
|
|
123
|
-
# Include declared (non-nested) entry keys even if file is missing.
|
|
124
|
-
candidates.concat(@entries.reject(&:nested).map(&:key))
|
|
125
|
-
candidates.uniq!
|
|
126
|
-
Key::Distance.suggest(key, candidates, limit: 5)
|
|
127
|
-
rescue StandardError
|
|
128
|
-
[]
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# Enumerate all entry files reachable through the manifest. Returns
|
|
132
|
-
# [{ key:, path:, manifest_entry: }, ...]
|
|
133
|
-
def enumerate(prefix: nil)
|
|
134
|
-
out = @entries.flat_map { |entry| entry.nested ? enumerate_nested(entry) : enumerate_leaf(entry) }
|
|
135
|
-
out.select! { |row| row[:key] == prefix || row[:key].start_with?("#{prefix}.") } if prefix
|
|
136
|
-
out.sort_by { |row| row[:key] }
|
|
81
|
+
def resolver
|
|
82
|
+
@resolver ||= Resolver.new(self)
|
|
137
83
|
end
|
|
138
84
|
|
|
139
85
|
def validate_key!(key)
|
|
@@ -144,51 +90,8 @@ module Textus
|
|
|
144
90
|
|
|
145
91
|
private
|
|
146
92
|
|
|
147
|
-
def enumerate_leaf(entry)
|
|
148
|
-
fp = resolve_leaf_path(entry)
|
|
149
|
-
File.exist?(fp) ? [{ key: entry.key, path: fp, manifest_entry: entry }] : []
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def enumerate_nested(entry)
|
|
153
|
-
base = File.join(@root, "zones", entry.path)
|
|
154
|
-
return [] unless File.directory?(base)
|
|
155
|
-
|
|
156
|
-
glob_pattern = entry.index_filename ? "**/#{entry.index_filename}" : nested_glob(entry.format)
|
|
157
|
-
Dir.glob(File.join(base, glob_pattern)).filter_map { |path| nested_row_for(entry, base, path) }
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def nested_row_for(entry, base, path)
|
|
161
|
-
rel = path.sub(%r{\A#{Regexp.escape(base)}/?}, "")
|
|
162
|
-
stripped = entry.index_filename ? File.dirname(rel) : rel.sub(/#{Regexp.escape(File.extname(rel))}\z/, "")
|
|
163
|
-
segs = stripped.split("/").reject { |s| s.empty? || s == "." }
|
|
164
|
-
return nil if segs.empty?
|
|
165
|
-
|
|
166
|
-
illegal = segs.find { |s| !valid_segment?(s) }
|
|
167
|
-
if illegal
|
|
168
|
-
warn("textus: skipping illegal key segment '#{illegal}' at #{path} — run 'textus key normalize --dry-run'")
|
|
169
|
-
return nil
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
{ key: (entry.key.split(".") + segs).join("."), path: path, manifest_entry: entry }
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def valid_segment?(seg)
|
|
176
|
-
return false if seg.nil? || seg.empty?
|
|
177
|
-
return false if seg.length > Key::Grammar::MAX_SEGMENT_LEN
|
|
178
|
-
|
|
179
|
-
seg.match?(Key::Grammar::SEGMENT)
|
|
180
|
-
end
|
|
181
|
-
|
|
182
93
|
def validate_declared_keys!
|
|
183
94
|
@entries.each { |e| validate_key!(e.key) }
|
|
184
95
|
end
|
|
185
|
-
|
|
186
|
-
def resolve_leaf_path(entry)
|
|
187
|
-
Textus::Key::Path.resolve(self, entry)
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def nested_glob(format)
|
|
191
|
-
Textus::Entry.for_format(format).nested_glob
|
|
192
|
-
end
|
|
193
96
|
end
|
|
194
97
|
end
|
data/lib/textus/operations.rb
CHANGED
|
@@ -10,102 +10,159 @@ module Textus
|
|
|
10
10
|
# ops.refresh_all(prefix: ..., zone: ...)
|
|
11
11
|
class Operations
|
|
12
12
|
def self.for(store, role: Role::DEFAULT, correlation_id: nil, dry_run: false)
|
|
13
|
-
|
|
13
|
+
new(
|
|
14
|
+
ctx: Application::Context.build(role: role, correlation_id: correlation_id, dry_run: dry_run),
|
|
15
|
+
manifest: store.manifest,
|
|
16
|
+
file_store: store.file_store,
|
|
17
|
+
schemas: store.schemas,
|
|
18
|
+
audit_log: store.audit_log,
|
|
19
|
+
bus: store.bus,
|
|
20
|
+
root: store.root,
|
|
14
21
|
store: store,
|
|
15
|
-
role: role,
|
|
16
|
-
correlation_id: correlation_id,
|
|
17
|
-
dry_run: dry_run,
|
|
18
22
|
)
|
|
19
|
-
new(ctx)
|
|
20
23
|
end
|
|
21
24
|
|
|
22
|
-
attr_reader :ctx
|
|
25
|
+
attr_reader :ctx, :store
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
# rubocop:disable Metrics/ParameterLists
|
|
28
|
+
def initialize(ctx:, manifest:, file_store:, schemas:, audit_log:, bus:, root:, store:)
|
|
29
|
+
@ctx = ctx
|
|
30
|
+
@manifest = manifest
|
|
31
|
+
@file_store = file_store
|
|
32
|
+
@schemas = schemas
|
|
33
|
+
@audit_log = audit_log
|
|
34
|
+
@bus = bus
|
|
35
|
+
@root = root
|
|
36
|
+
@store = store
|
|
37
|
+
@authorizer = Textus::Domain::Authorizer.new(manifest: @manifest)
|
|
26
38
|
end
|
|
39
|
+
# rubocop:enable Metrics/ParameterLists
|
|
27
40
|
|
|
28
|
-
def with_role(role)
|
|
41
|
+
def with_role(role)
|
|
42
|
+
self.class.new(
|
|
43
|
+
ctx: @ctx.with_role(role),
|
|
44
|
+
manifest: @manifest, file_store: @file_store, schemas: @schemas,
|
|
45
|
+
audit_log: @audit_log, bus: @bus,
|
|
46
|
+
root: @root, store: @store
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def hook_context
|
|
51
|
+
@hook_context ||= Textus::Hooks::Context.new(ops: self)
|
|
52
|
+
end
|
|
29
53
|
|
|
30
54
|
# writes
|
|
31
|
-
def put(...)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
def put(...)
|
|
56
|
+
Application::Writes::Put.new(
|
|
57
|
+
ctx: @ctx, manifest: @manifest, envelope_io: envelope_io,
|
|
58
|
+
bus: @bus, authorizer: @authorizer, hook_context: hook_context
|
|
59
|
+
).call(...)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def delete(...)
|
|
63
|
+
Application::Writes::Delete.new(
|
|
64
|
+
ctx: @ctx, manifest: @manifest, envelope_io: envelope_io,
|
|
65
|
+
bus: @bus, authorizer: @authorizer, hook_context: hook_context
|
|
66
|
+
).call(...)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def mv(...)
|
|
70
|
+
Application::Writes::Mv.new(
|
|
71
|
+
ctx: @ctx, manifest: @manifest, envelope_io: envelope_io,
|
|
72
|
+
bus: @bus, authorizer: @authorizer, hook_context: hook_context
|
|
73
|
+
).call(...)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def accept(...)
|
|
77
|
+
Application::Writes::Accept.new(
|
|
78
|
+
ctx: @ctx, manifest: @manifest, file_store: @file_store, schemas: @schemas,
|
|
79
|
+
envelope_io: envelope_io, bus: @bus, authorizer: @authorizer, hook_context: hook_context
|
|
80
|
+
).call(...)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def reject(...)
|
|
84
|
+
Application::Writes::Reject.new(
|
|
85
|
+
ctx: @ctx, manifest: @manifest, file_store: @file_store,
|
|
86
|
+
envelope_io: envelope_io, bus: @bus, authorizer: @authorizer, hook_context: hook_context
|
|
87
|
+
).call(...)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def publish(...)
|
|
91
|
+
Application::Writes::Publish.new(
|
|
92
|
+
ctx: @ctx, manifest: @manifest, file_store: @file_store,
|
|
93
|
+
bus: @bus, root: @root, store: @store, hook_context: hook_context
|
|
94
|
+
).call(...)
|
|
95
|
+
end
|
|
38
96
|
|
|
39
97
|
# reads
|
|
40
|
-
def get(...)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def
|
|
53
|
-
def
|
|
54
|
-
def
|
|
98
|
+
def get(...)
|
|
99
|
+
Application::Reads::Get.new(ctx: @ctx, manifest: @manifest, file_store: @file_store).call(...)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def get_or_refresh(...)
|
|
103
|
+
Application::Reads::GetOrRefresh.new(
|
|
104
|
+
manifest: @manifest,
|
|
105
|
+
get: Application::Reads::Get.new(ctx: @ctx, manifest: @manifest, file_store: @file_store),
|
|
106
|
+
orchestrator: orchestrator,
|
|
107
|
+
).call(...)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def list(...) = Application::Reads::List.new(manifest: @manifest).call(...)
|
|
111
|
+
def where(...) = Application::Reads::Where.new(manifest: @manifest).call(...)
|
|
112
|
+
def uid(...) = Application::Reads::Uid.new(ctx: @ctx, manifest: @manifest, file_store: @file_store).call(...)
|
|
113
|
+
def schema_envelope(...) = Application::Reads::SchemaEnvelope.new(manifest: @manifest, schemas: @schemas).call(...)
|
|
114
|
+
def deps(...) = Application::Reads::Deps.new(manifest: @manifest).call(...)
|
|
115
|
+
def rdeps(...) = Application::Reads::Rdeps.new(manifest: @manifest).call(...)
|
|
116
|
+
def published(...) = Application::Reads::Published.new(manifest: @manifest).call(...)
|
|
117
|
+
def stale(...) = Application::Reads::Stale.new(manifest: @manifest).call(...)
|
|
118
|
+
def audit(...) = Application::Reads::Audit.new(manifest: @manifest, root: @root).call(...)
|
|
119
|
+
def blame(...) = Application::Reads::Blame.new(manifest: @manifest, root: @root).call(...)
|
|
120
|
+
def policy_explain(...) = Application::Reads::PolicyExplain.new(manifest: @manifest).call(...)
|
|
121
|
+
def freshness(...) = Application::Reads::Freshness.new(ctx: @ctx, manifest: @manifest, file_store: @file_store).call(...)
|
|
122
|
+
|
|
123
|
+
def validate_all(...)
|
|
124
|
+
Application::Reads::ValidateAll.new(
|
|
125
|
+
ctx: @ctx, manifest: @manifest, file_store: @file_store, schemas: @schemas, audit_log: @audit_log,
|
|
126
|
+
).call(...)
|
|
127
|
+
end
|
|
55
128
|
|
|
56
129
|
# refresh
|
|
57
|
-
def refresh(key) =
|
|
58
|
-
|
|
130
|
+
def refresh(key) = refresh_worker.run(key)
|
|
131
|
+
|
|
132
|
+
def refresh_all(**)
|
|
133
|
+
Application::Refresh::All.new(
|
|
134
|
+
ctx: @ctx, manifest: @manifest, envelope_io: envelope_io, bus: @bus,
|
|
135
|
+
store: @store, authorizer: @authorizer, hook_context: hook_context
|
|
136
|
+
).call(**)
|
|
137
|
+
end
|
|
59
138
|
|
|
60
139
|
private
|
|
61
140
|
|
|
62
141
|
def envelope_io
|
|
63
142
|
@envelope_io ||= Application::Writes::EnvelopeIO.new(
|
|
64
|
-
file_store: @
|
|
65
|
-
manifest: @
|
|
66
|
-
schemas: @
|
|
67
|
-
audit_log: @
|
|
143
|
+
file_store: @file_store,
|
|
144
|
+
manifest: @manifest,
|
|
145
|
+
schemas: @schemas,
|
|
146
|
+
audit_log: @audit_log,
|
|
68
147
|
ctx: @ctx,
|
|
69
148
|
)
|
|
70
149
|
end
|
|
71
150
|
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def list_op = @list_op ||= Application::Reads::List.new(ctx: @ctx)
|
|
88
|
-
def where_op = @where_op ||= Application::Reads::Where.new(ctx: @ctx)
|
|
89
|
-
def uid_op = @uid_op ||= Application::Reads::Uid.new(ctx: @ctx)
|
|
90
|
-
def schema_envelope_op = @schema_envelope_op ||= Application::Reads::SchemaEnvelope.new(ctx: @ctx)
|
|
91
|
-
def deps_op = @deps_op ||= Application::Reads::Deps.new(ctx: @ctx)
|
|
92
|
-
def rdeps_op = @rdeps_op ||= Application::Reads::Rdeps.new(ctx: @ctx)
|
|
93
|
-
def published_op = @published_op ||= Application::Reads::Published.new(ctx: @ctx)
|
|
94
|
-
def stale_op = @stale_op ||= Application::Reads::Stale.new(ctx: @ctx)
|
|
95
|
-
def audit_op = @audit_op ||= Application::Reads::Audit.new(ctx: @ctx)
|
|
96
|
-
def blame_op = @blame_op ||= Application::Reads::Blame.new(ctx: @ctx)
|
|
97
|
-
def policy_explain_op = @policy_explain_op ||= Application::Reads::PolicyExplain.new(ctx: @ctx)
|
|
98
|
-
def freshness_op = @freshness_op ||= Application::Reads::Freshness.new(ctx: @ctx)
|
|
99
|
-
def validate_all_op = @validate_all_op ||= Application::Reads::ValidateAll.new(ctx: @ctx)
|
|
100
|
-
|
|
101
|
-
def refresh_worker_op = @refresh_worker_op ||= Application::Refresh::Worker.new(ctx: @ctx, envelope_io: envelope_io)
|
|
102
|
-
|
|
103
|
-
def orchestrator_op
|
|
104
|
-
@orchestrator_op ||= Application::Refresh::Orchestrator.new(
|
|
105
|
-
worker: refresh_worker_op,
|
|
106
|
-
store_root: @ctx.store.root,
|
|
107
|
-
store: @ctx.store,
|
|
108
|
-
role: @ctx.role,
|
|
151
|
+
def refresh_worker
|
|
152
|
+
@refresh_worker ||= Application::Refresh::Worker.new(
|
|
153
|
+
ctx: @ctx, manifest: @manifest, envelope_io: envelope_io, bus: @bus,
|
|
154
|
+
store: @store, authorizer: @authorizer, hook_context: hook_context
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def orchestrator
|
|
159
|
+
@orchestrator ||= Application::Refresh::Orchestrator.new(
|
|
160
|
+
worker: refresh_worker,
|
|
161
|
+
store_root: @root,
|
|
162
|
+
bus: @bus,
|
|
163
|
+
store: @store,
|
|
164
|
+
ctx: @ctx,
|
|
165
|
+
hook_context: hook_context,
|
|
109
166
|
)
|
|
110
167
|
end
|
|
111
168
|
end
|
data/lib/textus/schema/tools.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Textus
|
|
|
24
24
|
def self.diff(store, name:)
|
|
25
25
|
schema = load_schema(store, name)
|
|
26
26
|
drift = []
|
|
27
|
-
store.manifest.enumerate.each do |row|
|
|
27
|
+
store.manifest.resolver.enumerate.each do |row|
|
|
28
28
|
env = Textus::Operations.for(store).get(row[:key])
|
|
29
29
|
begin
|
|
30
30
|
schema.validate!(env.meta)
|
|
@@ -51,7 +51,7 @@ module Textus
|
|
|
51
51
|
|
|
52
52
|
ops = Textus::Operations.for(store, role: "human")
|
|
53
53
|
touched = []
|
|
54
|
-
store.manifest.enumerate.each do |row|
|
|
54
|
+
store.manifest.resolver.enumerate.each do |row|
|
|
55
55
|
env = ops.get(row[:key])
|
|
56
56
|
meta = env.meta.dup
|
|
57
57
|
changed = false
|
data/lib/textus/store.rb
CHANGED
|
@@ -2,7 +2,7 @@ require "fileutils"
|
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
4
|
class Store
|
|
5
|
-
attr_reader :root, :manifest, :schemas, :file_store, :audit_log, :bus
|
|
5
|
+
attr_reader :root, :manifest, :schemas, :file_store, :audit_log, :bus
|
|
6
6
|
|
|
7
7
|
def self.discover(start_dir = Dir.pwd, root: nil)
|
|
8
8
|
explicit = root || ENV.fetch("TEXTUS_ROOT", nil)
|
|
@@ -34,12 +34,12 @@ module Textus
|
|
|
34
34
|
@schemas = Schemas.new(File.join(@root, "schemas"))
|
|
35
35
|
@file_store = Infra::Storage::FileStore.new
|
|
36
36
|
@audit_log = Infra::AuditLog.new(@root)
|
|
37
|
-
@bus
|
|
38
|
-
@registry = Hooks::Registry.new(dispatcher: @bus)
|
|
37
|
+
@bus = Hooks::Bus.new
|
|
39
38
|
Infra::AuditSubscriber.new(@audit_log).attach(@bus)
|
|
40
|
-
Hooks::Builtin.register_all(@
|
|
41
|
-
Hooks::Loader.new(
|
|
42
|
-
|
|
39
|
+
Hooks::Builtin.register_all(@bus)
|
|
40
|
+
Hooks::Loader.new(bus: @bus).load_dir(File.join(@root, "hooks"))
|
|
41
|
+
ops = Operations.for(self, role: Role::DEFAULT)
|
|
42
|
+
@bus.publish(:store_loaded, ctx: ops.hook_context)
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
end
|
data/lib/textus/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: textus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.20.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -110,6 +110,10 @@ files:
|
|
|
110
110
|
- exe/textus
|
|
111
111
|
- lib/textus.rb
|
|
112
112
|
- lib/textus/application/context.rb
|
|
113
|
+
- lib/textus/application/policy/predicates/human_accept.rb
|
|
114
|
+
- lib/textus/application/policy/predicates/schema_valid.rb
|
|
115
|
+
- lib/textus/application/policy/promotion.rb
|
|
116
|
+
- lib/textus/application/projection.rb
|
|
113
117
|
- lib/textus/application/reads/audit.rb
|
|
114
118
|
- lib/textus/application/reads/blame.rb
|
|
115
119
|
- lib/textus/application/reads/deps.rb
|
|
@@ -129,10 +133,12 @@ files:
|
|
|
129
133
|
- lib/textus/application/refresh/all.rb
|
|
130
134
|
- lib/textus/application/refresh/orchestrator.rb
|
|
131
135
|
- lib/textus/application/refresh/worker.rb
|
|
136
|
+
- lib/textus/application/tools/migrate_keys.rb
|
|
137
|
+
- lib/textus/application/tools/migrate_manifest_to_kinds.rb
|
|
132
138
|
- lib/textus/application/writes/accept.rb
|
|
133
|
-
- lib/textus/application/writes/build.rb
|
|
134
139
|
- lib/textus/application/writes/delete.rb
|
|
135
140
|
- lib/textus/application/writes/envelope_io.rb
|
|
141
|
+
- lib/textus/application/writes/materializer.rb
|
|
136
142
|
- lib/textus/application/writes/mv.rb
|
|
137
143
|
- lib/textus/application/writes/publish.rb
|
|
138
144
|
- lib/textus/application/writes/put.rb
|
|
@@ -181,7 +187,6 @@ files:
|
|
|
181
187
|
- lib/textus/cli/verb/schema_migrate.rb
|
|
182
188
|
- lib/textus/cli/verb/uid.rb
|
|
183
189
|
- lib/textus/cli/verb/where.rb
|
|
184
|
-
- lib/textus/dependencies.rb
|
|
185
190
|
- lib/textus/doctor.rb
|
|
186
191
|
- lib/textus/doctor/check.rb
|
|
187
192
|
- lib/textus/doctor/check/audit_log.rb
|
|
@@ -200,6 +205,7 @@ files:
|
|
|
200
205
|
- lib/textus/doctor/check/templates.rb
|
|
201
206
|
- lib/textus/doctor/check/unowned_schema_fields.rb
|
|
202
207
|
- lib/textus/domain/action.rb
|
|
208
|
+
- lib/textus/domain/authorizer.rb
|
|
203
209
|
- lib/textus/domain/freshness.rb
|
|
204
210
|
- lib/textus/domain/freshness/evaluator.rb
|
|
205
211
|
- lib/textus/domain/freshness/policy.rb
|
|
@@ -209,10 +215,7 @@ files:
|
|
|
209
215
|
- lib/textus/domain/policy.rb
|
|
210
216
|
- lib/textus/domain/policy/handler_allowlist.rb
|
|
211
217
|
- lib/textus/domain/policy/matcher.rb
|
|
212
|
-
- lib/textus/domain/policy/predicates/human_accept.rb
|
|
213
|
-
- lib/textus/domain/policy/predicates/schema_valid.rb
|
|
214
218
|
- lib/textus/domain/policy/promote.rb
|
|
215
|
-
- lib/textus/domain/policy/promotion.rb
|
|
216
219
|
- lib/textus/domain/policy/refresh.rb
|
|
217
220
|
- lib/textus/domain/sentinel.rb
|
|
218
221
|
- lib/textus/domain/staleness.rb
|
|
@@ -228,9 +231,10 @@ files:
|
|
|
228
231
|
- lib/textus/errors.rb
|
|
229
232
|
- lib/textus/etag.rb
|
|
230
233
|
- lib/textus/hooks/builtin.rb
|
|
231
|
-
- lib/textus/hooks/
|
|
234
|
+
- lib/textus/hooks/bus.rb
|
|
235
|
+
- lib/textus/hooks/context.rb
|
|
236
|
+
- lib/textus/hooks/fire_report.rb
|
|
232
237
|
- lib/textus/hooks/loader.rb
|
|
233
|
-
- lib/textus/hooks/registry.rb
|
|
234
238
|
- lib/textus/infra/audit_log.rb
|
|
235
239
|
- lib/textus/infra/audit_subscriber.rb
|
|
236
240
|
- lib/textus/infra/build_lock.rb
|
|
@@ -247,6 +251,11 @@ files:
|
|
|
247
251
|
- lib/textus/key/path.rb
|
|
248
252
|
- lib/textus/manifest.rb
|
|
249
253
|
- lib/textus/manifest/entry.rb
|
|
254
|
+
- lib/textus/manifest/entry/base.rb
|
|
255
|
+
- lib/textus/manifest/entry/derived.rb
|
|
256
|
+
- lib/textus/manifest/entry/intake.rb
|
|
257
|
+
- lib/textus/manifest/entry/leaf.rb
|
|
258
|
+
- lib/textus/manifest/entry/nested.rb
|
|
250
259
|
- lib/textus/manifest/entry/parser.rb
|
|
251
260
|
- lib/textus/manifest/entry/validators.rb
|
|
252
261
|
- lib/textus/manifest/entry/validators/events.rb
|
|
@@ -255,13 +264,11 @@ files:
|
|
|
255
264
|
- lib/textus/manifest/entry/validators/inject_intro.rb
|
|
256
265
|
- lib/textus/manifest/entry/validators/publish_each.rb
|
|
257
266
|
- lib/textus/manifest/resolution.rb
|
|
267
|
+
- lib/textus/manifest/resolver.rb
|
|
258
268
|
- lib/textus/manifest/rules.rb
|
|
259
269
|
- lib/textus/manifest/schema.rb
|
|
260
|
-
- lib/textus/migrate_keys.rb
|
|
261
270
|
- lib/textus/mustache.rb
|
|
262
271
|
- lib/textus/operations.rb
|
|
263
|
-
- lib/textus/projection.rb
|
|
264
|
-
- lib/textus/refresh.rb
|
|
265
272
|
- lib/textus/role.rb
|
|
266
273
|
- lib/textus/schema.rb
|
|
267
274
|
- lib/textus/schema/tools.rb
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Application
|
|
5
|
-
module Writes
|
|
6
|
-
# Materializes generator-zone entries (template + projection) onto disk
|
|
7
|
-
# and copies the result to any configured `publish_to:` targets. Fires
|
|
8
|
-
# `:build_completed` and `:file_published` events.
|
|
9
|
-
#
|
|
10
|
-
# For `publish_each:` (per-leaf publishing of nested entries), see
|
|
11
|
-
# `Application::Writes::Publish`. The CLI verb `textus build` calls
|
|
12
|
-
# both classes and merges the results.
|
|
13
|
-
class Build
|
|
14
|
-
def initialize(ctx:)
|
|
15
|
-
@ctx = ctx
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def call(prefix: nil)
|
|
19
|
-
built = manifest.entries.filter_map do |mentry|
|
|
20
|
-
next unless mentry.in_generator_zone?
|
|
21
|
-
next unless mentry.projection || mentry.template
|
|
22
|
-
next if prefix && !mentry.key.start_with?(prefix)
|
|
23
|
-
|
|
24
|
-
materialize(mentry)
|
|
25
|
-
end
|
|
26
|
-
{ "protocol" => Textus::PROTOCOL, "built" => built }
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
def store = @ctx.store
|
|
32
|
-
def manifest = store.manifest
|
|
33
|
-
def root = store.root
|
|
34
|
-
|
|
35
|
-
def materialize(mentry)
|
|
36
|
-
target_path = Builder::Pipeline.run(
|
|
37
|
-
store: store,
|
|
38
|
-
mentry: mentry,
|
|
39
|
-
template_loader: ->(name) { read_template(name) },
|
|
40
|
-
)
|
|
41
|
-
publish_and_fire(mentry, target_path)
|
|
42
|
-
{ "key" => mentry.key, "path" => target_path, "published_to" => mentry.publish_to }
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def read_template(name)
|
|
46
|
-
tpl_path = File.join(root, "templates", name)
|
|
47
|
-
raise TemplateError.new("template not found: #{tpl_path}", template_name: name) unless File.exist?(tpl_path)
|
|
48
|
-
|
|
49
|
-
File.read(tpl_path)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def publish_and_fire(mentry, target_path)
|
|
53
|
-
envelope = Textus::Application::Reads::Get.new(ctx: @ctx).call(mentry.key)
|
|
54
|
-
repo_root = File.dirname(root)
|
|
55
|
-
|
|
56
|
-
mentry.publish_to.each do |rel|
|
|
57
|
-
target_abs = File.join(repo_root, rel)
|
|
58
|
-
Textus::Infra::Publisher.publish(source: target_path, target: target_abs, store_root: root)
|
|
59
|
-
publish_event(:file_published,
|
|
60
|
-
key: mentry.key,
|
|
61
|
-
envelope: envelope,
|
|
62
|
-
source: target_path,
|
|
63
|
-
target: target_abs)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
publish_event(:build_completed,
|
|
67
|
-
key: mentry.key,
|
|
68
|
-
envelope: envelope,
|
|
69
|
-
sources: Array(mentry.projection&.fetch("select", nil)).compact)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def publish_event(event, **payload)
|
|
73
|
-
@ctx.bus.publish(event, store: @ctx.with_role(@ctx.role), correlation_id: @ctx.correlation_id, **payload)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
data/lib/textus/dependencies.rb
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Dependencies
|
|
3
|
-
def self.deps_of(manifest, key)
|
|
4
|
-
entry = manifest.entries.find { |e| e.key == key } or return []
|
|
5
|
-
result = Array(entry.projection&.fetch("select", nil)).map { |s| s }
|
|
6
|
-
Array(entry.generator&.fetch("sources", nil)).each { |s| result << s }
|
|
7
|
-
result.uniq
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def self.rdeps_of(manifest, key)
|
|
11
|
-
manifest.entries.each_with_object([]) do |e, acc|
|
|
12
|
-
sources = Array(e.projection&.fetch("select", nil)) + Array(e.generator&.fetch("sources", nil))
|
|
13
|
-
acc << e.key if sources.any? { |s| s == key || key.start_with?("#{s}.") }
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def self.published_of(manifest)
|
|
18
|
-
manifest.entries.reject { |e| e.publish_to.empty? }.map do |e|
|
|
19
|
-
{ "key" => e.key, "publish_to" => e.publish_to }
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Domain
|
|
3
|
-
module Policy
|
|
4
|
-
module Predicates
|
|
5
|
-
class HumanAccept
|
|
6
|
-
attr_reader :reason
|
|
7
|
-
|
|
8
|
-
def name
|
|
9
|
-
"human_accept"
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
# The role is passed via `store` (an Application::Context-like object
|
|
13
|
-
# with a `role` reader) or through the entry metadata. In practice,
|
|
14
|
-
# Accept already enforces role == "human" before reaching the
|
|
15
|
-
# promotion gate, so this predicate trivially passes. It documents
|
|
16
|
-
# intent and future-proofs multi-actor accept flows.
|
|
17
|
-
def call(store:, entry: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
18
|
-
role = store.respond_to?(:role) ? store.role.to_s : nil
|
|
19
|
-
# If we cannot determine the role (e.g. store doesn't expose it),
|
|
20
|
-
# we trust that Accept has already checked — allow through.
|
|
21
|
-
return true if role.nil?
|
|
22
|
-
|
|
23
|
-
ok = (role == "human")
|
|
24
|
-
@reason = "current role is '#{role}', expected 'human'" unless ok
|
|
25
|
-
ok
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|