textus 0.8.1 → 0.10.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/CHANGELOG.md +329 -0
- data/README.md +50 -22
- data/SPEC.md +194 -63
- data/docs/architecture.md +22 -4
- data/docs/conventions.md +24 -17
- data/lib/textus/application/context.rb +44 -0
- data/lib/textus/application/reads/audit.rb +69 -0
- data/lib/textus/application/reads/blame.rb +79 -0
- data/lib/textus/application/reads/freshness.rb +77 -0
- data/lib/textus/application/reads/get.rb +62 -0
- data/lib/textus/application/reads/policy_explain.rb +39 -0
- data/lib/textus/application/refresh/all.rb +41 -0
- data/lib/textus/application/refresh/orchestrator.rb +69 -0
- data/lib/textus/application/refresh/worker.rb +79 -0
- data/lib/textus/application/writes/accept.rb +44 -0
- data/lib/textus/application/writes/build.rb +116 -0
- data/lib/textus/application/writes/delete.rb +36 -0
- data/lib/textus/application/writes/publish.rb +25 -0
- data/lib/textus/application/writes/put.rb +43 -0
- data/lib/textus/builder/pipeline.rb +1 -1
- data/lib/textus/builder/renderer/json.rb +1 -1
- data/lib/textus/builder/renderer/markdown.rb +1 -1
- data/lib/textus/builder/renderer/text.rb +1 -1
- data/lib/textus/builder/renderer/yaml.rb +1 -1
- data/lib/textus/builder/renderer.rb +1 -1
- data/lib/textus/cli/group/policy.rb +11 -0
- data/lib/textus/cli/verb/accept.rb +2 -2
- data/lib/textus/cli/verb/audit.rb +30 -0
- data/lib/textus/cli/verb/blame.rb +16 -0
- data/lib/textus/cli/verb/build.rb +2 -1
- data/lib/textus/cli/verb/delete.rb +2 -2
- data/lib/textus/cli/verb/freshness.rb +16 -0
- data/lib/textus/cli/verb/get.rb +7 -1
- data/lib/textus/cli/verb/hook_run.rb +4 -4
- data/lib/textus/cli/verb/mv.rb +1 -2
- data/lib/textus/cli/verb/policy_explain.rb +14 -0
- data/lib/textus/cli/verb/policy_list.rb +25 -0
- data/lib/textus/cli/verb/put.rb +10 -8
- data/lib/textus/cli/verb/refresh.rb +2 -2
- data/lib/textus/cli/verb/refresh_stale.rb +18 -0
- data/lib/textus/cli/verb/reject.rb +14 -0
- data/lib/textus/cli/verb.rb +14 -0
- data/lib/textus/cli.rb +16 -2
- data/lib/textus/composition.rb +72 -0
- data/lib/textus/doctor/check/handler_allowlist.rb +33 -0
- data/lib/textus/doctor/check/intake_registration.rb +46 -0
- data/lib/textus/doctor/check/legacy_intake_fields.rb +57 -0
- data/lib/textus/doctor/check/policy_ambiguity.rb +49 -0
- data/lib/textus/doctor.rb +7 -1
- data/lib/textus/domain/action.rb +9 -0
- data/lib/textus/domain/freshness/evaluator.rb +30 -0
- data/lib/textus/domain/freshness/policy.rb +18 -0
- data/lib/textus/domain/freshness/verdict.rb +12 -0
- data/lib/textus/domain/outcome.rb +10 -0
- data/lib/textus/domain/permission.rb +15 -0
- data/lib/textus/domain/policy/handler_allowlist.rb +17 -0
- data/lib/textus/domain/policy/matcher.rb +51 -0
- data/lib/textus/domain/policy/promote.rb +24 -0
- data/lib/textus/domain/policy/refresh.rb +48 -0
- data/lib/textus/domain/policy.rb +7 -0
- data/lib/textus/hooks/builtin.rb +5 -5
- data/lib/textus/hooks/dispatcher.rb +15 -1
- data/lib/textus/hooks/dsl.rb +18 -0
- data/lib/textus/hooks/registry.rb +12 -5
- data/lib/textus/infra/clock.rb +9 -0
- data/lib/textus/infra/event_bus.rb +27 -0
- data/lib/textus/infra/publisher.rb +73 -0
- data/lib/textus/infra/refresh/detached.rb +38 -0
- data/lib/textus/infra/refresh/lock.rb +44 -0
- data/lib/textus/init.rb +71 -28
- data/lib/textus/intro.rb +17 -14
- data/lib/textus/manifest/entry.rb +39 -13
- data/lib/textus/manifest/policies.rb +83 -0
- data/lib/textus/manifest.rb +30 -11
- data/lib/textus/projection.rb +1 -1
- data/lib/textus/proposal.rb +4 -21
- data/lib/textus/refresh.rb +9 -45
- data/lib/textus/store/mover.rb +14 -9
- data/lib/textus/store/reader.rb +10 -8
- data/lib/textus/store/staleness.rb +5 -17
- data/lib/textus/store/validator.rb +46 -20
- data/lib/textus/store/writer.rb +51 -14
- data/lib/textus/store.rb +30 -10
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +1 -0
- metadata +46 -5
- data/lib/textus/builder.rb +0 -86
- data/lib/textus/cli/verb/stale.rb +0 -14
- data/lib/textus/publisher.rb +0 -71
- data/lib/textus/store/view.rb +0 -29
data/lib/textus/store/mover.rb
CHANGED
|
@@ -4,14 +4,15 @@ module Textus
|
|
|
4
4
|
class Store
|
|
5
5
|
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
6
6
|
class Mover
|
|
7
|
-
def initialize(reader:, writer:, manifest:, audit_log:)
|
|
7
|
+
def initialize(store:, reader:, writer:, manifest:, audit_log:)
|
|
8
|
+
@store = store
|
|
8
9
|
@reader = reader
|
|
9
10
|
@writer = writer
|
|
10
11
|
@manifest = manifest
|
|
11
12
|
@audit_log = audit_log
|
|
12
13
|
end
|
|
13
14
|
|
|
14
|
-
def call(old_key, new_key, as: Role::DEFAULT, dry_run: false)
|
|
15
|
+
def call(old_key, new_key, as: Role::DEFAULT, dry_run: false, correlation_id: nil)
|
|
15
16
|
@manifest.validate_key!(old_key)
|
|
16
17
|
@manifest.validate_key!(new_key)
|
|
17
18
|
raise UsageError.new("mv: old and new keys are identical") if old_key == new_key
|
|
@@ -69,23 +70,27 @@ module Textus
|
|
|
69
70
|
rewrite_name_for_mv!(new_mentry, new_path, new_key)
|
|
70
71
|
etag_after = Etag.for_file(new_path)
|
|
71
72
|
|
|
73
|
+
extras = {
|
|
74
|
+
"from_key" => old_key, "to_key" => new_key,
|
|
75
|
+
"from_path" => old_path, "to_path" => new_path,
|
|
76
|
+
"uid" => current_uid
|
|
77
|
+
}
|
|
78
|
+
extras["correlation_id"] = correlation_id if correlation_id
|
|
79
|
+
|
|
72
80
|
@audit_log.append(
|
|
73
81
|
role: as, verb: "mv", key: new_key,
|
|
74
82
|
etag_before: etag_before, etag_after: etag_after,
|
|
75
|
-
extras:
|
|
76
|
-
"from_key" => old_key, "to_key" => new_key,
|
|
77
|
-
"from_path" => old_path, "to_path" => new_path,
|
|
78
|
-
"uid" => current_uid
|
|
79
|
-
}
|
|
83
|
+
extras: extras
|
|
80
84
|
)
|
|
81
85
|
|
|
82
|
-
|
|
86
|
+
new_envelope = @reader.get(new_key)
|
|
87
|
+
@store.fire_event(:mv, key: new_key, from_key: old_key, to_key: new_key, envelope: new_envelope)
|
|
83
88
|
{
|
|
84
89
|
"protocol" => PROTOCOL, "ok" => true,
|
|
85
90
|
"from_key" => old_key, "to_key" => new_key,
|
|
86
91
|
"from_path" => old_path, "to_path" => new_path,
|
|
87
92
|
"uid" => current_uid,
|
|
88
|
-
"envelope" =>
|
|
93
|
+
"envelope" => new_envelope
|
|
89
94
|
}
|
|
90
95
|
end
|
|
91
96
|
|
data/lib/textus/store/reader.rb
CHANGED
|
@@ -7,20 +7,22 @@ module Textus
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def get(key)
|
|
10
|
+
read_raw_envelope(key) || raise(UnknownKey.new(key, suggestions: @manifest.suggestions_for(key)))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Reads the current on-disk state of key as a bare envelope, skipping
|
|
14
|
+
# freshness annotation to avoid recursion. Used by Freshness.refresh_sync
|
|
15
|
+
# after a sync refresh completes.
|
|
16
|
+
def read_raw_envelope(key)
|
|
10
17
|
mentry, path, = @manifest.resolve(key)
|
|
11
|
-
|
|
18
|
+
return nil unless File.exist?(path)
|
|
12
19
|
|
|
13
20
|
raw = File.binread(path)
|
|
14
21
|
parsed = Entry.for_format(mentry.format).parse(raw, path: path)
|
|
15
|
-
meta = parsed["_meta"]
|
|
16
|
-
content = parsed["content"]
|
|
17
|
-
@store.writer.enforce_name_match!(path, meta, mentry.format)
|
|
18
|
-
schema = @store.schema_for(mentry.schema)
|
|
19
|
-
Entry.for_format(mentry.format).validate_against(schema, parsed) if schema
|
|
20
22
|
Envelope.build(
|
|
21
23
|
key: key, mentry: mentry, path: path,
|
|
22
|
-
meta:
|
|
23
|
-
etag: Etag.for_bytes(raw), content: content
|
|
24
|
+
meta: parsed["_meta"], body: parsed["body"],
|
|
25
|
+
etag: Etag.for_bytes(raw), content: parsed["content"]
|
|
24
26
|
)
|
|
25
27
|
end
|
|
26
28
|
|
|
@@ -11,7 +11,7 @@ module Textus
|
|
|
11
11
|
def call(prefix: nil, zone: nil)
|
|
12
12
|
out = []
|
|
13
13
|
@manifest.entries.each do |mentry|
|
|
14
|
-
next unless mentry.
|
|
14
|
+
next unless mentry.in_generator_zone?
|
|
15
15
|
next if zone && mentry.zone != zone
|
|
16
16
|
|
|
17
17
|
gen = mentry.generator
|
|
@@ -47,11 +47,12 @@ module Textus
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
@manifest.entries.each do |mentry|
|
|
50
|
-
next unless mentry.
|
|
50
|
+
next unless mentry.intake_handler
|
|
51
51
|
next if zone && mentry.zone != zone
|
|
52
52
|
next if prefix && !(mentry.key == prefix || mentry.key.start_with?("#{prefix}."))
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
policy_set = @manifest.policies_for(mentry.key)
|
|
55
|
+
ttl = policy_set.refresh&.ttl_seconds
|
|
55
56
|
next unless ttl
|
|
56
57
|
|
|
57
58
|
path = Textus::Key::Path.resolve(@manifest, mentry)
|
|
@@ -102,21 +103,8 @@ module Textus
|
|
|
102
103
|
nil
|
|
103
104
|
end
|
|
104
105
|
|
|
105
|
-
def parse_ttl(s)
|
|
106
|
-
return nil unless s
|
|
107
|
-
|
|
108
|
-
m = s.to_s.match(/\A(\d+)([smhd])\z/) or return nil
|
|
109
|
-
n = m[1].to_i
|
|
110
|
-
case m[2]
|
|
111
|
-
when "s" then n
|
|
112
|
-
when "m" then n * 60
|
|
113
|
-
when "h" then n * 3600
|
|
114
|
-
when "d" then n * 86_400
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
106
|
def intake_stale_row(mentry, path, reason)
|
|
119
|
-
{ "key" => mentry.key, "path" => path, "
|
|
107
|
+
{ "key" => mentry.key, "path" => path, "handler" => mentry.intake_handler, "reason" => reason }
|
|
120
108
|
end
|
|
121
109
|
|
|
122
110
|
def stale_row(mentry, path, reason)
|
|
@@ -10,14 +10,30 @@ module Textus
|
|
|
10
10
|
|
|
11
11
|
def call
|
|
12
12
|
violations = []
|
|
13
|
+
check_content_violations(violations)
|
|
14
|
+
check_role_authority_violations(violations)
|
|
15
|
+
{ "protocol" => PROTOCOL, "ok" => violations.empty?, "violations" => violations }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def check_content_violations(violations)
|
|
13
21
|
@manifest.enumerate.each do |row|
|
|
22
|
+
key = row[:key]
|
|
23
|
+
mentry = row[:manifest_entry]
|
|
24
|
+
env = fetch_envelope(key, violations) or next
|
|
25
|
+
schema = mentry.schema && @schema_for.call(mentry.schema)
|
|
26
|
+
next unless schema
|
|
27
|
+
|
|
14
28
|
begin
|
|
15
|
-
|
|
29
|
+
validate_schema!(schema, env, mentry.format)
|
|
16
30
|
rescue Textus::Error => e
|
|
17
|
-
violations << { "key" =>
|
|
31
|
+
violations << { "key" => key, "code" => e.code, "message" => e.message }
|
|
18
32
|
end
|
|
19
33
|
end
|
|
34
|
+
end
|
|
20
35
|
|
|
36
|
+
def check_role_authority_violations(violations)
|
|
21
37
|
@manifest.enumerate.each do |row|
|
|
22
38
|
mentry = row[:manifest_entry]
|
|
23
39
|
next unless mentry.schema
|
|
@@ -30,26 +46,36 @@ module Textus
|
|
|
30
46
|
rescue StandardError
|
|
31
47
|
next
|
|
32
48
|
end
|
|
33
|
-
|
|
34
|
-
next if last_writer.nil?
|
|
35
|
-
|
|
36
|
-
env["_meta"].each_key do |field|
|
|
37
|
-
owner = schema.maintained_by(field)
|
|
38
|
-
next if owner.nil?
|
|
39
|
-
next if last_writer == owner
|
|
40
|
-
next if last_writer == "human"
|
|
41
|
-
|
|
42
|
-
violations << {
|
|
43
|
-
"key" => row[:key],
|
|
44
|
-
"code" => "role_authority",
|
|
45
|
-
"field" => field,
|
|
46
|
-
"expected" => owner,
|
|
47
|
-
"last_writer" => last_writer,
|
|
48
|
-
}
|
|
49
|
-
end
|
|
49
|
+
append_authority_violations(violations, row[:key], env, schema)
|
|
50
50
|
end
|
|
51
|
+
end
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
def append_authority_violations(violations, key, env, schema)
|
|
54
|
+
last_writer = @audit_log.last_writer_for(key)
|
|
55
|
+
return if last_writer.nil?
|
|
56
|
+
|
|
57
|
+
env["_meta"].each_key do |field|
|
|
58
|
+
owner = schema.maintained_by(field)
|
|
59
|
+
next if owner.nil? || last_writer == owner || last_writer == "human"
|
|
60
|
+
|
|
61
|
+
violations << { "key" => key, "code" => "role_authority",
|
|
62
|
+
"field" => field, "expected" => owner, "last_writer" => last_writer }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def fetch_envelope(key, violations)
|
|
67
|
+
@reader.get(key)
|
|
68
|
+
rescue Textus::Error => e
|
|
69
|
+
violations << { "key" => key, "code" => e.code, "message" => e.message }
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def validate_schema!(schema, envelope, format)
|
|
74
|
+
payload = case format
|
|
75
|
+
when "json", "yaml" then envelope["content"] || {}
|
|
76
|
+
else envelope["_meta"] || {}
|
|
77
|
+
end
|
|
78
|
+
schema.validate!(payload)
|
|
53
79
|
end
|
|
54
80
|
end
|
|
55
81
|
end
|
data/lib/textus/store/writer.rb
CHANGED
|
@@ -10,11 +10,19 @@ module Textus
|
|
|
10
10
|
@reader = store.reader
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
# Backward-compat shim — orchestration now lives in Application::Writes::Put.
|
|
13
14
|
def put(key, meta: nil, body: nil, content: nil, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
ctx = Textus::Application::Context.new(store: @store, role: as)
|
|
16
|
+
Textus::Application::Writes::Put.new(ctx: ctx, bus: @store.bus).call(
|
|
17
|
+
key, meta: meta, body: body, content: content, if_etag: if_etag, suppress_events: suppress_events
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Pure I/O: validate, serialize, etag-check, write to disk, audit. No
|
|
22
|
+
# permission check and no event firing — those are handled by the caller
|
|
23
|
+
# (Application::Writes::Put).
|
|
24
|
+
def write_envelope_to_disk(key, mentry:, meta: nil, body: nil, content: nil, if_etag: nil, as: Role::DEFAULT, correlation_id: nil)
|
|
25
|
+
_, path, = @manifest.resolve(key)
|
|
18
26
|
|
|
19
27
|
meta ||= {}
|
|
20
28
|
strategy = Entry.for_format(mentry.format)
|
|
@@ -43,13 +51,15 @@ module Textus
|
|
|
43
51
|
FileUtils.mkdir_p(File.dirname(path))
|
|
44
52
|
File.binwrite(path, bytes)
|
|
45
53
|
etag_after = Etag.for_bytes(bytes)
|
|
46
|
-
@store.audit_log.append(
|
|
47
|
-
|
|
54
|
+
@store.audit_log.append(
|
|
55
|
+
role: as, verb: "put", key: key,
|
|
56
|
+
etag_before: etag_before, etag_after: etag_after,
|
|
57
|
+
extras: correlation_id ? { "correlation_id" => correlation_id } : nil
|
|
58
|
+
)
|
|
59
|
+
Envelope.build(
|
|
48
60
|
key: key, mentry: mentry, path: path,
|
|
49
61
|
meta: eff_meta, body: eff_body, etag: etag_after, content: eff_content
|
|
50
62
|
)
|
|
51
|
-
@store.fire_event(:put, key: key, envelope: envelope) unless suppress_events
|
|
52
|
-
envelope
|
|
53
63
|
end
|
|
54
64
|
|
|
55
65
|
def existing_uid_for(mentry, path)
|
|
@@ -108,24 +118,51 @@ module Textus
|
|
|
108
118
|
end
|
|
109
119
|
end
|
|
110
120
|
|
|
121
|
+
# Backward-compat shim — orchestration now lives in Application::Writes::Delete.
|
|
111
122
|
def delete(key, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
123
|
+
ctx = Textus::Application::Context.new(store: @store, role: as)
|
|
124
|
+
Textus::Application::Writes::Delete.new(ctx: ctx, bus: @store.bus).call(
|
|
125
|
+
key, if_etag: if_etag, suppress_events: suppress_events
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Pure I/O: resolve path, validate etag, delete from disk, audit. No
|
|
130
|
+
# permission check and no event firing — those are handled by the caller
|
|
131
|
+
# (Application::Writes::Delete).
|
|
132
|
+
def delete_envelope_from_disk(key, if_etag: nil, as: Role::DEFAULT, correlation_id: nil)
|
|
133
|
+
_, path, = @manifest.resolve(key)
|
|
115
134
|
raise UnknownKey.new(key, suggestions: @manifest.suggestions_for(key)) unless File.exist?(path)
|
|
116
135
|
|
|
117
136
|
etag_before = Etag.for_file(path)
|
|
118
137
|
raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && if_etag != etag_before
|
|
119
138
|
|
|
120
139
|
File.delete(path)
|
|
121
|
-
@store.audit_log.append(
|
|
122
|
-
|
|
123
|
-
|
|
140
|
+
@store.audit_log.append(
|
|
141
|
+
role: as, verb: "delete", key: key,
|
|
142
|
+
etag_before: etag_before, etag_after: nil,
|
|
143
|
+
extras: correlation_id ? { "correlation_id" => correlation_id } : nil
|
|
144
|
+
)
|
|
124
145
|
end
|
|
125
146
|
|
|
126
147
|
def accept(key, as:)
|
|
127
148
|
Proposal.accept(@store, key, as: as)
|
|
128
149
|
end
|
|
150
|
+
|
|
151
|
+
def reject(pending_key, as: Role::DEFAULT)
|
|
152
|
+
raise ProposalError.new("only human role can reject proposals; got '#{as}'") unless as == "human"
|
|
153
|
+
|
|
154
|
+
mentry, = @store.manifest.resolve(pending_key)
|
|
155
|
+
raise ProposalError.new("reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.zone})") unless mentry.in_proposal_zone?
|
|
156
|
+
|
|
157
|
+
env = @store.get(pending_key)
|
|
158
|
+
proposal = env.dig("_meta", "proposal") or
|
|
159
|
+
raise ProposalError.new("entry has no proposal block: #{pending_key}")
|
|
160
|
+
target_key = proposal["target_key"] or raise ProposalError.new("proposal missing target_key")
|
|
161
|
+
|
|
162
|
+
delete(pending_key, as: as)
|
|
163
|
+
@store.fire_event(:reject, key: pending_key, target_key: target_key)
|
|
164
|
+
{ "protocol" => PROTOCOL, "rejected" => pending_key, "target_key" => target_key }
|
|
165
|
+
end
|
|
129
166
|
end
|
|
130
167
|
# rubocop:enable Metrics/ParameterLists
|
|
131
168
|
end
|
data/lib/textus/store.rb
CHANGED
|
@@ -45,6 +45,7 @@ module Textus
|
|
|
45
45
|
load_hooks
|
|
46
46
|
@reader = Reader.new(self)
|
|
47
47
|
@writer = Writer.new(self)
|
|
48
|
+
fire_event(:loaded)
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
def load_hooks
|
|
@@ -53,7 +54,7 @@ module Textus
|
|
|
53
54
|
dir = File.join(@root, "hooks")
|
|
54
55
|
return unless File.directory?(dir)
|
|
55
56
|
|
|
56
|
-
Dir.glob(File.join(dir, "
|
|
57
|
+
Dir.glob(File.join(dir, "**/*.rb")).sort.each do |f| # rubocop:disable Lint/RedundantDirGlobSort
|
|
57
58
|
begin
|
|
58
59
|
load(f)
|
|
59
60
|
rescue StandardError, ScriptError => e
|
|
@@ -74,24 +75,43 @@ module Textus
|
|
|
74
75
|
end
|
|
75
76
|
end
|
|
76
77
|
|
|
77
|
-
def get(key)
|
|
78
|
-
|
|
78
|
+
def get(key, as: Textus::Role::DEFAULT)
|
|
79
|
+
ctx = Textus::Composition.context(self, role: as)
|
|
80
|
+
result = Textus::Composition.reads_get(ctx).call(key)
|
|
81
|
+
raise UnknownKey.new(key, suggestions: manifest.suggestions_for(key)) if result.nil?
|
|
82
|
+
|
|
83
|
+
result
|
|
79
84
|
end
|
|
80
85
|
|
|
81
86
|
def where(key) = @reader.where(key)
|
|
82
87
|
def list(**) = @reader.list(**)
|
|
83
88
|
def schema_envelope(key) = @reader.schema_envelope(key)
|
|
84
89
|
|
|
85
|
-
|
|
90
|
+
# rubocop:disable Metrics/ParameterLists
|
|
91
|
+
def put(key, meta: nil, body: nil, content: nil, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
|
|
92
|
+
ctx = Textus::Composition.context(self, role: as)
|
|
93
|
+
Textus::Composition.writes_put(ctx).call(
|
|
94
|
+
key, meta: meta, body: body, content: content, if_etag: if_etag, suppress_events: suppress_events
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
# rubocop:enable Metrics/ParameterLists
|
|
86
98
|
|
|
87
|
-
def delete(
|
|
99
|
+
def delete(key, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
|
|
100
|
+
ctx = Textus::Composition.context(self, role: as)
|
|
101
|
+
Textus::Composition.writes_delete(ctx).call(key, if_etag: if_etag, suppress_events: suppress_events)
|
|
102
|
+
end
|
|
88
103
|
|
|
89
104
|
def fire_event(event, **)
|
|
90
|
-
view =
|
|
105
|
+
view = Textus::Application::Context.new(store: self, role: "human")
|
|
91
106
|
@bus.publish(event, store: view, **)
|
|
92
107
|
end
|
|
93
108
|
|
|
94
|
-
def accept(
|
|
109
|
+
def accept(key, as: Role::DEFAULT)
|
|
110
|
+
ctx = Textus::Composition.context(self, role: as)
|
|
111
|
+
Textus::Composition.writes_accept(ctx).call(key)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def reject(...) = @writer.reject(...)
|
|
95
115
|
|
|
96
116
|
def deps(key) = @reader.deps(key)
|
|
97
117
|
def rdeps(key) = @reader.rdeps(key)
|
|
@@ -104,9 +124,9 @@ module Textus
|
|
|
104
124
|
# Move an entry from old_key to new_key within the same zone. Preserves
|
|
105
125
|
# uid (minting one first if absent), validates both keys against the
|
|
106
126
|
# manifest, refuses to clobber, and writes one mv audit row.
|
|
107
|
-
def mv(old_key, new_key, as: Role::DEFAULT, dry_run: false)
|
|
108
|
-
Mover.new(reader: @reader, writer: @writer, manifest: @manifest, audit_log: audit_log)
|
|
109
|
-
.call(old_key, new_key, as: as, dry_run: dry_run)
|
|
127
|
+
def mv(old_key, new_key, as: Role::DEFAULT, dry_run: false, correlation_id: nil)
|
|
128
|
+
Mover.new(store: self, reader: @reader, writer: @writer, manifest: @manifest, audit_log: audit_log)
|
|
129
|
+
.call(old_key, new_key, as: as, dry_run: dry_run, correlation_id: correlation_id)
|
|
110
130
|
end
|
|
111
131
|
|
|
112
132
|
def audit_log
|
data/lib/textus/version.rb
CHANGED
data/lib/textus.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.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -109,7 +109,20 @@ files:
|
|
|
109
109
|
- docs/conventions.md
|
|
110
110
|
- exe/textus
|
|
111
111
|
- lib/textus.rb
|
|
112
|
-
- lib/textus/
|
|
112
|
+
- lib/textus/application/context.rb
|
|
113
|
+
- lib/textus/application/reads/audit.rb
|
|
114
|
+
- lib/textus/application/reads/blame.rb
|
|
115
|
+
- lib/textus/application/reads/freshness.rb
|
|
116
|
+
- lib/textus/application/reads/get.rb
|
|
117
|
+
- lib/textus/application/reads/policy_explain.rb
|
|
118
|
+
- lib/textus/application/refresh/all.rb
|
|
119
|
+
- lib/textus/application/refresh/orchestrator.rb
|
|
120
|
+
- lib/textus/application/refresh/worker.rb
|
|
121
|
+
- lib/textus/application/writes/accept.rb
|
|
122
|
+
- lib/textus/application/writes/build.rb
|
|
123
|
+
- lib/textus/application/writes/delete.rb
|
|
124
|
+
- lib/textus/application/writes/publish.rb
|
|
125
|
+
- lib/textus/application/writes/put.rb
|
|
113
126
|
- lib/textus/builder/pipeline.rb
|
|
114
127
|
- lib/textus/builder/renderer.rb
|
|
115
128
|
- lib/textus/builder/renderer/json.rb
|
|
@@ -120,13 +133,17 @@ files:
|
|
|
120
133
|
- lib/textus/cli/group.rb
|
|
121
134
|
- lib/textus/cli/group/hook.rb
|
|
122
135
|
- lib/textus/cli/group/key.rb
|
|
136
|
+
- lib/textus/cli/group/policy.rb
|
|
123
137
|
- lib/textus/cli/group/schema.rb
|
|
124
138
|
- lib/textus/cli/verb.rb
|
|
125
139
|
- lib/textus/cli/verb/accept.rb
|
|
140
|
+
- lib/textus/cli/verb/audit.rb
|
|
141
|
+
- lib/textus/cli/verb/blame.rb
|
|
126
142
|
- lib/textus/cli/verb/build.rb
|
|
127
143
|
- lib/textus/cli/verb/delete.rb
|
|
128
144
|
- lib/textus/cli/verb/deps.rb
|
|
129
145
|
- lib/textus/cli/verb/doctor.rb
|
|
146
|
+
- lib/textus/cli/verb/freshness.rb
|
|
130
147
|
- lib/textus/cli/verb/get.rb
|
|
131
148
|
- lib/textus/cli/verb/hook_run.rb
|
|
132
149
|
- lib/textus/cli/verb/hooks.rb
|
|
@@ -135,29 +152,48 @@ files:
|
|
|
135
152
|
- lib/textus/cli/verb/list.rb
|
|
136
153
|
- lib/textus/cli/verb/migrate_keys.rb
|
|
137
154
|
- lib/textus/cli/verb/mv.rb
|
|
155
|
+
- lib/textus/cli/verb/policy_explain.rb
|
|
156
|
+
- lib/textus/cli/verb/policy_list.rb
|
|
138
157
|
- lib/textus/cli/verb/published.rb
|
|
139
158
|
- lib/textus/cli/verb/put.rb
|
|
140
159
|
- lib/textus/cli/verb/rdeps.rb
|
|
141
160
|
- lib/textus/cli/verb/refresh.rb
|
|
161
|
+
- lib/textus/cli/verb/refresh_stale.rb
|
|
162
|
+
- lib/textus/cli/verb/reject.rb
|
|
142
163
|
- lib/textus/cli/verb/schema.rb
|
|
143
164
|
- lib/textus/cli/verb/schema_diff.rb
|
|
144
165
|
- lib/textus/cli/verb/schema_init.rb
|
|
145
166
|
- lib/textus/cli/verb/schema_migrate.rb
|
|
146
|
-
- lib/textus/cli/verb/stale.rb
|
|
147
167
|
- lib/textus/cli/verb/uid.rb
|
|
148
168
|
- lib/textus/cli/verb/where.rb
|
|
169
|
+
- lib/textus/composition.rb
|
|
149
170
|
- lib/textus/dependencies.rb
|
|
150
171
|
- lib/textus/doctor.rb
|
|
151
172
|
- lib/textus/doctor/check.rb
|
|
152
173
|
- lib/textus/doctor/check/audit_log.rb
|
|
174
|
+
- lib/textus/doctor/check/handler_allowlist.rb
|
|
153
175
|
- lib/textus/doctor/check/hooks.rb
|
|
154
176
|
- lib/textus/doctor/check/illegal_keys.rb
|
|
177
|
+
- lib/textus/doctor/check/intake_registration.rb
|
|
178
|
+
- lib/textus/doctor/check/legacy_intake_fields.rb
|
|
155
179
|
- lib/textus/doctor/check/manifest_files.rb
|
|
180
|
+
- lib/textus/doctor/check/policy_ambiguity.rb
|
|
156
181
|
- lib/textus/doctor/check/schema_violations.rb
|
|
157
182
|
- lib/textus/doctor/check/schemas.rb
|
|
158
183
|
- lib/textus/doctor/check/sentinels.rb
|
|
159
184
|
- lib/textus/doctor/check/templates.rb
|
|
160
185
|
- lib/textus/doctor/check/unowned_schema_fields.rb
|
|
186
|
+
- lib/textus/domain/action.rb
|
|
187
|
+
- lib/textus/domain/freshness/evaluator.rb
|
|
188
|
+
- lib/textus/domain/freshness/policy.rb
|
|
189
|
+
- lib/textus/domain/freshness/verdict.rb
|
|
190
|
+
- lib/textus/domain/outcome.rb
|
|
191
|
+
- lib/textus/domain/permission.rb
|
|
192
|
+
- lib/textus/domain/policy.rb
|
|
193
|
+
- lib/textus/domain/policy/handler_allowlist.rb
|
|
194
|
+
- lib/textus/domain/policy/matcher.rb
|
|
195
|
+
- lib/textus/domain/policy/promote.rb
|
|
196
|
+
- lib/textus/domain/policy/refresh.rb
|
|
161
197
|
- lib/textus/entry.rb
|
|
162
198
|
- lib/textus/entry/base.rb
|
|
163
199
|
- lib/textus/entry/json.rb
|
|
@@ -169,8 +205,14 @@ files:
|
|
|
169
205
|
- lib/textus/etag.rb
|
|
170
206
|
- lib/textus/hooks/builtin.rb
|
|
171
207
|
- lib/textus/hooks/dispatcher.rb
|
|
208
|
+
- lib/textus/hooks/dsl.rb
|
|
172
209
|
- lib/textus/hooks/loader.rb
|
|
173
210
|
- lib/textus/hooks/registry.rb
|
|
211
|
+
- lib/textus/infra/clock.rb
|
|
212
|
+
- lib/textus/infra/event_bus.rb
|
|
213
|
+
- lib/textus/infra/publisher.rb
|
|
214
|
+
- lib/textus/infra/refresh/detached.rb
|
|
215
|
+
- lib/textus/infra/refresh/lock.rb
|
|
174
216
|
- lib/textus/init.rb
|
|
175
217
|
- lib/textus/intro.rb
|
|
176
218
|
- lib/textus/key/distance.rb
|
|
@@ -178,11 +220,11 @@ files:
|
|
|
178
220
|
- lib/textus/key/path.rb
|
|
179
221
|
- lib/textus/manifest.rb
|
|
180
222
|
- lib/textus/manifest/entry.rb
|
|
223
|
+
- lib/textus/manifest/policies.rb
|
|
181
224
|
- lib/textus/migrate_keys.rb
|
|
182
225
|
- lib/textus/mustache.rb
|
|
183
226
|
- lib/textus/projection.rb
|
|
184
227
|
- lib/textus/proposal.rb
|
|
185
|
-
- lib/textus/publisher.rb
|
|
186
228
|
- lib/textus/refresh.rb
|
|
187
229
|
- lib/textus/role.rb
|
|
188
230
|
- lib/textus/schema.rb
|
|
@@ -193,7 +235,6 @@ files:
|
|
|
193
235
|
- lib/textus/store/reader.rb
|
|
194
236
|
- lib/textus/store/staleness.rb
|
|
195
237
|
- lib/textus/store/validator.rb
|
|
196
|
-
- lib/textus/store/view.rb
|
|
197
238
|
- lib/textus/store/writer.rb
|
|
198
239
|
- lib/textus/version.rb
|
|
199
240
|
homepage: https://github.com/patrick204nqh/textus
|
data/lib/textus/builder.rb
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
class Builder
|
|
5
|
-
def initialize(store)
|
|
6
|
-
@store = store
|
|
7
|
-
@manifest = store.manifest
|
|
8
|
-
@root = store.root
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def build(prefix: nil)
|
|
12
|
-
built = []
|
|
13
|
-
@manifest.entries.each do |mentry|
|
|
14
|
-
next unless derived_zone?(mentry)
|
|
15
|
-
next unless mentry.projection || mentry.template
|
|
16
|
-
next if prefix && !mentry.key.start_with?(prefix)
|
|
17
|
-
|
|
18
|
-
result = materialize(mentry)
|
|
19
|
-
built << result
|
|
20
|
-
end
|
|
21
|
-
published_leaves = publish_leaves(prefix: prefix)
|
|
22
|
-
{ "protocol" => Textus::PROTOCOL, "built" => built, "published_leaves" => published_leaves }
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
private
|
|
26
|
-
|
|
27
|
-
def publish_leaves(prefix: nil)
|
|
28
|
-
repo_root = File.dirname(@root)
|
|
29
|
-
out = []
|
|
30
|
-
@manifest.entries.each do |mentry|
|
|
31
|
-
next unless mentry.nested && mentry.publish_each
|
|
32
|
-
next if prefix && !mentry.key.start_with?(prefix) && !prefix.start_with?("#{mentry.key}.")
|
|
33
|
-
|
|
34
|
-
@manifest.enumerate(prefix: mentry.key).each do |row|
|
|
35
|
-
next unless row[:manifest_entry].equal?(mentry)
|
|
36
|
-
next if prefix && !row[:key].start_with?(prefix) && row[:key] != prefix
|
|
37
|
-
|
|
38
|
-
target_rel = mentry.publish_target_for(row[:key])
|
|
39
|
-
target_abs = File.expand_path(File.join(repo_root, target_rel))
|
|
40
|
-
unless target_abs.start_with?(File.expand_path(repo_root) + File::SEPARATOR)
|
|
41
|
-
raise PublishError.new(
|
|
42
|
-
"entry '#{mentry.key}': publish_each target '#{target_rel}' for key '#{row[:key]}' escapes repo root",
|
|
43
|
-
)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
Publisher.publish(source: row[:path], target: target_abs, store_root: @root)
|
|
47
|
-
out << { "key" => row[:key], "source" => row[:path], "target" => target_abs }
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
out
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def derived_zone?(mentry)
|
|
54
|
-
writers = @manifest.zone_writers(mentry.zone)
|
|
55
|
-
writers.include?("build")
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def materialize(mentry)
|
|
59
|
-
target_path = Pipeline.run(
|
|
60
|
-
store: @store,
|
|
61
|
-
mentry: mentry,
|
|
62
|
-
template_loader: ->(name) { read_template(name) },
|
|
63
|
-
)
|
|
64
|
-
publish_and_fire(mentry, target_path)
|
|
65
|
-
{ "key" => mentry.key, "path" => target_path, "published_to" => mentry.publish_to }
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def read_template(name)
|
|
69
|
-
tpl_path = File.join(@root, "templates", name)
|
|
70
|
-
raise TemplateError.new("template not found: #{tpl_path}", template_name: name) unless File.exist?(tpl_path)
|
|
71
|
-
|
|
72
|
-
File.read(tpl_path)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def publish_and_fire(mentry, target_path)
|
|
76
|
-
mentry.publish_to.each do |rel|
|
|
77
|
-
repo_root = File.dirname(@root)
|
|
78
|
-
Publisher.publish(source: target_path, target: File.join(repo_root, rel), store_root: @root)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
envelope = @store.get(mentry.key)
|
|
82
|
-
@store.fire_event(:build, key: mentry.key, envelope: envelope,
|
|
83
|
-
sources: Array(mentry.projection&.fetch("select", nil)).compact)
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|