textus 0.12.1 → 0.14.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 +60 -40
- data/CHANGELOG.md +214 -0
- data/README.md +6 -12
- data/SPEC.md +4 -1
- data/docs/conventions.md +8 -8
- data/lib/textus/application/context.rb +4 -0
- data/lib/textus/application/reads/blame.rb +1 -1
- data/lib/textus/application/reads/deps.rb +15 -0
- data/lib/textus/application/reads/freshness.rb +2 -2
- data/lib/textus/application/reads/get.rb +8 -11
- data/lib/textus/application/reads/list.rb +15 -0
- data/lib/textus/application/reads/published.rb +15 -0
- data/lib/textus/application/reads/rdeps.rb +15 -0
- data/lib/textus/application/reads/schema_envelope.rb +15 -0
- data/lib/textus/application/reads/stale.rb +15 -0
- data/lib/textus/application/reads/uid.rb +15 -0
- data/lib/textus/application/reads/validate_all.rb +15 -0
- data/lib/textus/application/reads/where.rb +15 -0
- data/lib/textus/application/refresh/all.rb +2 -2
- data/lib/textus/application/refresh/worker.rb +3 -3
- data/lib/textus/application/writes/accept.rb +7 -7
- data/lib/textus/application/writes/build.rb +10 -47
- data/lib/textus/application/writes/mv.rb +144 -0
- data/lib/textus/application/writes/publish.rb +41 -9
- data/lib/textus/application/writes/reject.rb +37 -0
- data/lib/textus/cli/verb/accept.rb +1 -2
- data/lib/textus/cli/verb/audit.rb +3 -3
- data/lib/textus/cli/verb/blame.rb +1 -2
- data/lib/textus/cli/verb/build.rb +6 -2
- data/lib/textus/cli/verb/delete.rb +1 -2
- data/lib/textus/cli/verb/deps.rb +1 -1
- data/lib/textus/cli/verb/freshness.rb +1 -2
- data/lib/textus/cli/verb/get.rb +2 -3
- data/lib/textus/cli/verb/list.rb +1 -1
- data/lib/textus/cli/verb/mv.rb +1 -1
- data/lib/textus/cli/verb/published.rb +1 -1
- data/lib/textus/cli/verb/put.rb +2 -2
- data/lib/textus/cli/verb/rdeps.rb +1 -1
- data/lib/textus/cli/verb/refresh.rb +1 -2
- data/lib/textus/cli/verb/reject.rb +1 -1
- data/lib/textus/cli/verb/rule_explain.rb +1 -2
- data/lib/textus/cli/verb/schema.rb +1 -1
- data/lib/textus/cli/verb/uid.rb +1 -1
- data/lib/textus/cli/verb/where.rb +1 -1
- data/lib/textus/cli/verb.rb +6 -1
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor.rb +1 -1
- data/lib/textus/domain/freshness/evaluator.rb +1 -1
- data/lib/textus/domain/policy/predicates/schema_valid.rb +3 -3
- data/lib/textus/entry/base.rb +28 -0
- data/lib/textus/entry/json.rb +59 -0
- data/lib/textus/entry/markdown.rb +46 -0
- data/lib/textus/entry/text.rb +35 -0
- data/lib/textus/entry/yaml.rb +59 -0
- data/lib/textus/entry.rb +16 -0
- data/lib/textus/envelope.rb +44 -14
- data/lib/textus/intro.rb +56 -0
- data/lib/textus/manifest/entry/parser.rb +84 -0
- data/lib/textus/manifest/entry/validators/events.rb +21 -0
- data/lib/textus/manifest/entry/validators/format_matrix.rb +26 -0
- data/lib/textus/manifest/entry/validators/index_filename.rb +45 -0
- data/lib/textus/manifest/entry/validators/inject_intro.rb +18 -0
- data/lib/textus/manifest/entry/validators/publish_each.rb +37 -0
- data/lib/textus/manifest/entry/validators.rb +20 -0
- data/lib/textus/manifest/entry.rb +35 -213
- data/lib/textus/manifest.rb +6 -16
- data/lib/textus/operations/reads.rb +39 -0
- data/lib/textus/operations/refresh.rb +27 -0
- data/lib/textus/operations/writes.rb +21 -0
- data/lib/textus/operations.rb +44 -0
- data/lib/textus/projection.rb +5 -4
- data/lib/textus/refresh.rb +3 -4
- data/lib/textus/schema/tools.rb +8 -7
- data/lib/textus/store/reader.rb +1 -1
- data/lib/textus/store/validator.rb +3 -3
- data/lib/textus/store/writer.rb +5 -74
- data/lib/textus/store.rb +1 -55
- data/lib/textus/version.rb +1 -1
- metadata +23 -4
- data/lib/textus/composition.rb +0 -72
- data/lib/textus/proposal.rb +0 -10
- data/lib/textus/store/mover.rb +0 -167
|
@@ -54,7 +54,7 @@ module Textus
|
|
|
54
54
|
last_writer = @audit_log.last_writer_for(key)
|
|
55
55
|
return if last_writer.nil?
|
|
56
56
|
|
|
57
|
-
env
|
|
57
|
+
env.meta.each_key do |field|
|
|
58
58
|
owner = schema.maintained_by(field)
|
|
59
59
|
next if owner.nil? || last_writer == owner || last_writer == "human"
|
|
60
60
|
|
|
@@ -72,8 +72,8 @@ module Textus
|
|
|
72
72
|
|
|
73
73
|
def validate_schema!(schema, envelope, format)
|
|
74
74
|
payload = case format
|
|
75
|
-
when "json", "yaml" then envelope
|
|
76
|
-
else envelope
|
|
75
|
+
when "json", "yaml" then envelope.content || {}
|
|
76
|
+
else envelope.meta || {}
|
|
77
77
|
end
|
|
78
78
|
schema.validate!(payload)
|
|
79
79
|
end
|
data/lib/textus/store/writer.rb
CHANGED
|
@@ -11,16 +11,6 @@ module Textus
|
|
|
11
11
|
@reader = store.reader
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
# Backward-compat shim — orchestration now lives in Application::Writes::Put.
|
|
15
|
-
# rubocop:disable Metrics/ParameterLists
|
|
16
|
-
def put(key, meta: nil, body: nil, content: nil, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
|
|
17
|
-
ctx = Textus::Application::Context.new(store: @store, role: as)
|
|
18
|
-
Textus::Application::Writes::Put.new(ctx: ctx, bus: @store.bus).call(
|
|
19
|
-
key, meta: meta, body: body, content: content, if_etag: if_etag, suppress_events: suppress_events
|
|
20
|
-
)
|
|
21
|
-
end
|
|
22
|
-
# rubocop:enable Metrics/ParameterLists
|
|
23
|
-
|
|
24
14
|
# Pure I/O: validate, serialize, etag-check, write to disk, audit. No
|
|
25
15
|
# permission check and no event firing — those are handled by the caller
|
|
26
16
|
# (Application::Writes::Put).
|
|
@@ -76,56 +66,17 @@ module Textus
|
|
|
76
66
|
end
|
|
77
67
|
|
|
78
68
|
def ensure_uid(format, meta, content, existing_uid)
|
|
79
|
-
|
|
80
|
-
when "markdown", "json", "yaml"
|
|
81
|
-
m = meta.is_a?(Hash) ? meta.dup : {}
|
|
82
|
-
m["uid"] = existing_uid || Store.mint_uid unless m["uid"].is_a?(String) && !m["uid"].empty?
|
|
83
|
-
[m, content]
|
|
84
|
-
else
|
|
85
|
-
[meta, content]
|
|
86
|
-
end
|
|
69
|
+
Textus::Entry.for_format(format).inject_uid(meta, content, existing_uid)
|
|
87
70
|
end
|
|
88
71
|
|
|
89
72
|
def enforce_name_match!(path, meta, format)
|
|
90
|
-
|
|
91
|
-
return unless meta.is_a?(Hash) && meta["name"]
|
|
92
|
-
|
|
93
|
-
ext = Entry.for_format(format).extensions.first
|
|
94
|
-
basename = File.basename(path, ext)
|
|
95
|
-
return if meta["name"] == basename
|
|
96
|
-
|
|
97
|
-
raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
|
|
73
|
+
Textus::Entry.for_format(format).enforce_name_match!(path, meta)
|
|
98
74
|
end
|
|
99
75
|
|
|
100
76
|
def serialize_for_put(mentry:, path:, strategy:, meta:, body:, content:)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
[bytes, meta, body.to_s, nil]
|
|
105
|
-
when "json", "yaml"
|
|
106
|
-
raise UsageError.new("put for #{mentry.format} requires content: or body:") if content.nil? && (body.nil? || body.to_s.empty?)
|
|
107
|
-
|
|
108
|
-
if content.nil?
|
|
109
|
-
begin
|
|
110
|
-
parsed = strategy.parse(body.to_s, path: path)
|
|
111
|
-
rescue BadFrontmatter => e
|
|
112
|
-
raise BadContent.new(path, "bad_content: #{e.message}")
|
|
113
|
-
end
|
|
114
|
-
[body.to_s, parsed["_meta"], body.to_s, parsed["content"]]
|
|
115
|
-
else
|
|
116
|
-
bytes = strategy.serialize(meta: meta, body: "", content: content)
|
|
117
|
-
[bytes, meta, bytes, content]
|
|
118
|
-
end
|
|
119
|
-
else
|
|
120
|
-
raise UsageError.new("unknown format #{mentry.format.inspect}")
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Backward-compat shim — orchestration now lives in Application::Writes::Delete.
|
|
125
|
-
def delete(key, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
|
|
126
|
-
ctx = Textus::Application::Context.new(store: @store, role: as)
|
|
127
|
-
Textus::Application::Writes::Delete.new(ctx: ctx, bus: @store.bus).call(
|
|
128
|
-
key, if_etag: if_etag, suppress_events: suppress_events
|
|
77
|
+
_ = strategy
|
|
78
|
+
Textus::Entry.for_format(mentry.format).serialize_for_put(
|
|
79
|
+
meta: meta, body: body, content: content, path: path,
|
|
129
80
|
)
|
|
130
81
|
end
|
|
131
82
|
|
|
@@ -146,26 +97,6 @@ module Textus
|
|
|
146
97
|
extras: ctx.correlation_id ? { "correlation_id" => ctx.correlation_id } : nil
|
|
147
98
|
)
|
|
148
99
|
end
|
|
149
|
-
|
|
150
|
-
def accept(key, as:)
|
|
151
|
-
Proposal.accept(@store, key, as: as)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def reject(pending_key, as: Role::DEFAULT)
|
|
155
|
-
raise ProposalError.new("only human role can reject proposals; got '#{as}'") unless as == "human"
|
|
156
|
-
|
|
157
|
-
mentry, = @store.manifest.resolve(pending_key)
|
|
158
|
-
raise ProposalError.new("reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.zone})") unless mentry.in_proposal_zone?
|
|
159
|
-
|
|
160
|
-
env = @store.get(pending_key)
|
|
161
|
-
proposal = env.dig("_meta", "proposal") or
|
|
162
|
-
raise ProposalError.new("entry has no proposal block: #{pending_key}")
|
|
163
|
-
target_key = proposal["target_key"] or raise ProposalError.new("proposal missing target_key")
|
|
164
|
-
|
|
165
|
-
delete(pending_key, as: as)
|
|
166
|
-
@store.fire_event(:proposal_rejected, key: pending_key, target_key: target_key)
|
|
167
|
-
{ "protocol" => PROTOCOL, "rejected" => pending_key, "target_key" => target_key }
|
|
168
|
-
end
|
|
169
100
|
end
|
|
170
101
|
end
|
|
171
102
|
end
|
data/lib/textus/store.rb
CHANGED
|
@@ -45,7 +45,7 @@ module Textus
|
|
|
45
45
|
load_hooks
|
|
46
46
|
@reader = Reader.new(self)
|
|
47
47
|
@writer = Writer.new(self)
|
|
48
|
-
|
|
48
|
+
@bus.publish(:store_loaded, store: Textus::Application::Context.system(self))
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def load_hooks
|
|
@@ -75,60 +75,6 @@ module Textus
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
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
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def where(key) = @reader.where(key)
|
|
87
|
-
def list(**) = @reader.list(**)
|
|
88
|
-
def schema_envelope(key) = @reader.schema_envelope(key)
|
|
89
|
-
|
|
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
|
|
98
|
-
|
|
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
|
|
103
|
-
|
|
104
|
-
def fire_event(event, **)
|
|
105
|
-
view = Textus::Application::Context.new(store: self, role: "human")
|
|
106
|
-
@bus.publish(event, store: view, **)
|
|
107
|
-
end
|
|
108
|
-
|
|
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(...)
|
|
115
|
-
|
|
116
|
-
def deps(key) = @reader.deps(key)
|
|
117
|
-
def rdeps(key) = @reader.rdeps(key)
|
|
118
|
-
def published = @reader.published
|
|
119
|
-
def stale(**) = @reader.stale(**)
|
|
120
|
-
def validate_all = @reader.validate_all
|
|
121
|
-
|
|
122
|
-
def uid(key) = @reader.uid(key)
|
|
123
|
-
|
|
124
|
-
# Move an entry from old_key to new_key within the same zone. Preserves
|
|
125
|
-
# uid (minting one first if absent), validates both keys against the
|
|
126
|
-
# manifest, refuses to clobber, and writes one mv audit row.
|
|
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)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
78
|
def audit_log
|
|
133
79
|
@audit_log ||= Store::AuditLog.new(@root)
|
|
134
80
|
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.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -112,17 +112,28 @@ files:
|
|
|
112
112
|
- lib/textus/application/context.rb
|
|
113
113
|
- lib/textus/application/reads/audit.rb
|
|
114
114
|
- lib/textus/application/reads/blame.rb
|
|
115
|
+
- lib/textus/application/reads/deps.rb
|
|
115
116
|
- lib/textus/application/reads/freshness.rb
|
|
116
117
|
- lib/textus/application/reads/get.rb
|
|
118
|
+
- lib/textus/application/reads/list.rb
|
|
117
119
|
- lib/textus/application/reads/policy_explain.rb
|
|
120
|
+
- lib/textus/application/reads/published.rb
|
|
121
|
+
- lib/textus/application/reads/rdeps.rb
|
|
122
|
+
- lib/textus/application/reads/schema_envelope.rb
|
|
123
|
+
- lib/textus/application/reads/stale.rb
|
|
124
|
+
- lib/textus/application/reads/uid.rb
|
|
125
|
+
- lib/textus/application/reads/validate_all.rb
|
|
126
|
+
- lib/textus/application/reads/where.rb
|
|
118
127
|
- lib/textus/application/refresh/all.rb
|
|
119
128
|
- lib/textus/application/refresh/orchestrator.rb
|
|
120
129
|
- lib/textus/application/refresh/worker.rb
|
|
121
130
|
- lib/textus/application/writes/accept.rb
|
|
122
131
|
- lib/textus/application/writes/build.rb
|
|
123
132
|
- lib/textus/application/writes/delete.rb
|
|
133
|
+
- lib/textus/application/writes/mv.rb
|
|
124
134
|
- lib/textus/application/writes/publish.rb
|
|
125
135
|
- lib/textus/application/writes/put.rb
|
|
136
|
+
- lib/textus/application/writes/reject.rb
|
|
126
137
|
- lib/textus/builder/pipeline.rb
|
|
127
138
|
- lib/textus/builder/renderer.rb
|
|
128
139
|
- lib/textus/builder/renderer/json.rb
|
|
@@ -167,7 +178,6 @@ files:
|
|
|
167
178
|
- lib/textus/cli/verb/schema_migrate.rb
|
|
168
179
|
- lib/textus/cli/verb/uid.rb
|
|
169
180
|
- lib/textus/cli/verb/where.rb
|
|
170
|
-
- lib/textus/composition.rb
|
|
171
181
|
- lib/textus/dependencies.rb
|
|
172
182
|
- lib/textus/doctor.rb
|
|
173
183
|
- lib/textus/doctor/check.rb
|
|
@@ -225,19 +235,28 @@ files:
|
|
|
225
235
|
- lib/textus/key/path.rb
|
|
226
236
|
- lib/textus/manifest.rb
|
|
227
237
|
- lib/textus/manifest/entry.rb
|
|
238
|
+
- lib/textus/manifest/entry/parser.rb
|
|
239
|
+
- lib/textus/manifest/entry/validators.rb
|
|
240
|
+
- lib/textus/manifest/entry/validators/events.rb
|
|
241
|
+
- lib/textus/manifest/entry/validators/format_matrix.rb
|
|
242
|
+
- lib/textus/manifest/entry/validators/index_filename.rb
|
|
243
|
+
- lib/textus/manifest/entry/validators/inject_intro.rb
|
|
244
|
+
- lib/textus/manifest/entry/validators/publish_each.rb
|
|
228
245
|
- lib/textus/manifest/rules.rb
|
|
229
246
|
- lib/textus/manifest/schema.rb
|
|
230
247
|
- lib/textus/migrate_keys.rb
|
|
231
248
|
- lib/textus/mustache.rb
|
|
249
|
+
- lib/textus/operations.rb
|
|
250
|
+
- lib/textus/operations/reads.rb
|
|
251
|
+
- lib/textus/operations/refresh.rb
|
|
252
|
+
- lib/textus/operations/writes.rb
|
|
232
253
|
- lib/textus/projection.rb
|
|
233
|
-
- lib/textus/proposal.rb
|
|
234
254
|
- lib/textus/refresh.rb
|
|
235
255
|
- lib/textus/role.rb
|
|
236
256
|
- lib/textus/schema.rb
|
|
237
257
|
- lib/textus/schema/tools.rb
|
|
238
258
|
- lib/textus/store.rb
|
|
239
259
|
- lib/textus/store/audit_log.rb
|
|
240
|
-
- lib/textus/store/mover.rb
|
|
241
260
|
- lib/textus/store/reader.rb
|
|
242
261
|
- lib/textus/store/sentinel.rb
|
|
243
262
|
- lib/textus/store/staleness.rb
|
data/lib/textus/composition.rb
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Composition
|
|
3
|
-
module_function
|
|
4
|
-
|
|
5
|
-
def context(store, role:, correlation_id: nil, dry_run: false)
|
|
6
|
-
Textus::Application::Context.new(
|
|
7
|
-
store: store,
|
|
8
|
-
role: role,
|
|
9
|
-
correlation_id: correlation_id,
|
|
10
|
-
dry_run: dry_run,
|
|
11
|
-
)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def reads_get(ctx)
|
|
15
|
-
Textus::Application::Reads::Get.new(ctx: ctx, orchestrator: refresh_orchestrator(ctx))
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def freshness(ctx)
|
|
19
|
-
Textus::Application::Reads::Freshness.new(ctx: ctx)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def audit(ctx)
|
|
23
|
-
Textus::Application::Reads::Audit.new(ctx: ctx)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def blame(ctx)
|
|
27
|
-
Textus::Application::Reads::Blame.new(ctx: ctx)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def policy_explain(ctx)
|
|
31
|
-
Textus::Application::Reads::PolicyExplain.new(ctx: ctx)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def refresh_worker(ctx)
|
|
35
|
-
Textus::Application::Refresh::Worker.new(ctx: ctx, bus: ctx.store.bus)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def refresh_orchestrator(ctx)
|
|
39
|
-
Textus::Application::Refresh::Orchestrator.new(
|
|
40
|
-
worker: refresh_worker(ctx),
|
|
41
|
-
bus: ctx.store.bus,
|
|
42
|
-
store_root: ctx.store.root,
|
|
43
|
-
store: ctx.store,
|
|
44
|
-
role: ctx.role,
|
|
45
|
-
)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def writes_put(ctx)
|
|
49
|
-
Textus::Application::Writes::Put.new(ctx: ctx, bus: ctx.store.bus)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def writes_delete(ctx)
|
|
53
|
-
Textus::Application::Writes::Delete.new(ctx: ctx, bus: ctx.store.bus)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def writes_build(ctx)
|
|
57
|
-
Textus::Application::Writes::Build.new(ctx: ctx, bus: ctx.store.bus)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def writes_accept(ctx)
|
|
61
|
-
Textus::Application::Writes::Accept.new(ctx: ctx, bus: ctx.store.bus)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def writes_publish(ctx)
|
|
65
|
-
Textus::Application::Writes::Publish.new(ctx: ctx, bus: ctx.store.bus)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def event_bus(ctx)
|
|
69
|
-
Textus::Infra::EventBus.new(registry: ctx.store.registry)
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
data/lib/textus/proposal.rb
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Proposal
|
|
3
|
-
# Deprecated as of 0.9.1: use Textus::Application::Writes::Accept (via
|
|
4
|
-
# Textus::Composition.writes_accept).
|
|
5
|
-
def self.accept(store, pending_key, as:)
|
|
6
|
-
ctx = Textus::Composition.context(store, role: as)
|
|
7
|
-
Textus::Application::Writes::Accept.new(ctx: ctx, bus: store.bus).call(pending_key)
|
|
8
|
-
end
|
|
9
|
-
end
|
|
10
|
-
end
|
data/lib/textus/store/mover.rb
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
class Store
|
|
5
|
-
class Mover
|
|
6
|
-
MovePlan = Data.define(
|
|
7
|
-
:old_key, :new_key, :old_path, :new_path,
|
|
8
|
-
:new_mentry, :uid, :etag_before, :as
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
def initialize(store:, reader:, writer:, manifest:, audit_log:)
|
|
12
|
-
@store = store
|
|
13
|
-
@reader = reader
|
|
14
|
-
@writer = writer
|
|
15
|
-
@manifest = manifest
|
|
16
|
-
@audit_log = audit_log
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def call(old_key, new_key, as: Role::DEFAULT, dry_run: false, correlation_id: nil)
|
|
20
|
-
plan, pre_env = prepare_plan(old_key, new_key, as: as)
|
|
21
|
-
return dry_run_result(plan) if dry_run
|
|
22
|
-
|
|
23
|
-
plan = ensure_uid!(plan, pre_env: pre_env)
|
|
24
|
-
etag_after = perform_move!(plan)
|
|
25
|
-
new_envelope = record_move(plan, etag_after: etag_after, correlation_id: correlation_id)
|
|
26
|
-
success_result(plan, new_envelope: new_envelope)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
# Validates inputs, resolves manifest entries, and reads the source
|
|
32
|
-
# envelope. Returns [MovePlan, pre_envelope]; the pre_envelope is only
|
|
33
|
-
# needed by ensure_uid! and is threaded separately to keep MovePlan
|
|
34
|
-
# focused on the planned operation.
|
|
35
|
-
def prepare_plan(old_key, new_key, as:)
|
|
36
|
-
@manifest.validate_key!(old_key)
|
|
37
|
-
@manifest.validate_key!(new_key)
|
|
38
|
-
raise UsageError.new("mv: old and new keys are identical") if old_key == new_key
|
|
39
|
-
|
|
40
|
-
old_mentry, old_path, = @manifest.resolve(old_key)
|
|
41
|
-
raise UnknownKey.new(old_key) unless File.exist?(old_path)
|
|
42
|
-
|
|
43
|
-
new_mentry, new_path, = @manifest.resolve(new_key)
|
|
44
|
-
validate_zone_and_format!(old_mentry, new_mentry)
|
|
45
|
-
validate_writer!(old_mentry, old_key, as)
|
|
46
|
-
raise UsageError.new("mv: target '#{new_key}' already exists at #{new_path}") if File.exist?(new_path)
|
|
47
|
-
|
|
48
|
-
pre_env = @reader.get(old_key)
|
|
49
|
-
plan = MovePlan.new(
|
|
50
|
-
old_key: old_key, new_key: new_key,
|
|
51
|
-
old_path: old_path, new_path: new_path,
|
|
52
|
-
new_mentry: new_mentry,
|
|
53
|
-
uid: pre_env["uid"], etag_before: pre_env["etag"], as: as
|
|
54
|
-
)
|
|
55
|
-
[plan, pre_env]
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def validate_zone_and_format!(old_mentry, new_mentry)
|
|
59
|
-
if old_mentry.zone != new_mentry.zone
|
|
60
|
-
raise UsageError.new(
|
|
61
|
-
"mv: cross-zone move refused (#{old_mentry.zone} → #{new_mentry.zone}). " \
|
|
62
|
-
"Use put+delete for cross-zone moves.",
|
|
63
|
-
)
|
|
64
|
-
end
|
|
65
|
-
return if old_mentry.format == new_mentry.format
|
|
66
|
-
|
|
67
|
-
raise UsageError.new(
|
|
68
|
-
"mv: format mismatch (#{old_mentry.format} → #{new_mentry.format}); refusing.",
|
|
69
|
-
)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def validate_writer!(mentry, key, as)
|
|
73
|
-
writers = @manifest.zone_writers(mentry.zone)
|
|
74
|
-
return if writers.include?(as)
|
|
75
|
-
|
|
76
|
-
raise WriteForbidden.new(key, mentry.zone, writers: writers)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def ensure_uid!(plan, pre_env:)
|
|
80
|
-
return plan if plan.uid
|
|
81
|
-
|
|
82
|
-
env = @writer.put(
|
|
83
|
-
plan.old_key,
|
|
84
|
-
meta: pre_env["_meta"],
|
|
85
|
-
body: pre_env["body"],
|
|
86
|
-
content: pre_env["content"],
|
|
87
|
-
as: plan.as,
|
|
88
|
-
suppress_events: true,
|
|
89
|
-
)
|
|
90
|
-
plan.with(uid: env["uid"], etag_before: env["etag"])
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def perform_move!(plan)
|
|
94
|
-
FileUtils.mkdir_p(File.dirname(plan.new_path))
|
|
95
|
-
FileUtils.mv(plan.old_path, plan.new_path)
|
|
96
|
-
rewrite_name_for_mv!(plan.new_mentry, plan.new_path, plan.new_key)
|
|
97
|
-
Etag.for_file(plan.new_path)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def record_move(plan, etag_after:, correlation_id:)
|
|
101
|
-
extras = {
|
|
102
|
-
"from_key" => plan.old_key, "to_key" => plan.new_key,
|
|
103
|
-
"from_path" => plan.old_path, "to_path" => plan.new_path,
|
|
104
|
-
"uid" => plan.uid
|
|
105
|
-
}
|
|
106
|
-
extras["correlation_id"] = correlation_id if correlation_id
|
|
107
|
-
|
|
108
|
-
@audit_log.append(
|
|
109
|
-
role: plan.as, verb: "mv", key: plan.new_key,
|
|
110
|
-
etag_before: plan.etag_before, etag_after: etag_after,
|
|
111
|
-
extras: extras
|
|
112
|
-
)
|
|
113
|
-
new_envelope = @reader.get(plan.new_key)
|
|
114
|
-
@store.fire_event(
|
|
115
|
-
:entry_renamed,
|
|
116
|
-
key: plan.new_key, from_key: plan.old_key, to_key: plan.new_key,
|
|
117
|
-
envelope: new_envelope
|
|
118
|
-
)
|
|
119
|
-
new_envelope
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def dry_run_result(plan)
|
|
123
|
-
{
|
|
124
|
-
"protocol" => PROTOCOL, "ok" => true, "dry_run" => true,
|
|
125
|
-
"from_key" => plan.old_key, "to_key" => plan.new_key,
|
|
126
|
-
"from_path" => plan.old_path, "to_path" => plan.new_path,
|
|
127
|
-
"uid" => plan.uid
|
|
128
|
-
}
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def success_result(plan, new_envelope:)
|
|
132
|
-
{
|
|
133
|
-
"protocol" => PROTOCOL, "ok" => true,
|
|
134
|
-
"from_key" => plan.old_key, "to_key" => plan.new_key,
|
|
135
|
-
"from_path" => plan.old_path, "to_path" => plan.new_path,
|
|
136
|
-
"uid" => plan.uid,
|
|
137
|
-
"envelope" => new_envelope
|
|
138
|
-
}
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# If the moved file carries a `name:` field (markdown) or `_meta.name`
|
|
142
|
-
# (json/yaml), rewrite it to the new basename so enforce_name_match! stays
|
|
143
|
-
# happy on the next read. Only touches the bytes when name actually changes.
|
|
144
|
-
def rewrite_name_for_mv!(mentry, new_path, new_key)
|
|
145
|
-
strategy = Entry.for_format(mentry.format)
|
|
146
|
-
raw = File.binread(new_path)
|
|
147
|
-
parsed = strategy.parse(raw, path: new_path)
|
|
148
|
-
basename = new_key.split(".").last
|
|
149
|
-
|
|
150
|
-
case mentry.format
|
|
151
|
-
when "markdown"
|
|
152
|
-
meta = parsed["_meta"] || {}
|
|
153
|
-
return unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
|
|
154
|
-
|
|
155
|
-
meta = meta.merge("name" => basename)
|
|
156
|
-
File.binwrite(new_path, strategy.serialize(meta: meta, body: parsed["body"]))
|
|
157
|
-
when "json", "yaml"
|
|
158
|
-
meta = parsed["_meta"]
|
|
159
|
-
return unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
|
|
160
|
-
|
|
161
|
-
new_meta = meta.merge("name" => basename)
|
|
162
|
-
File.binwrite(new_path, strategy.serialize(meta: new_meta, body: "", content: parsed["content"]))
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
end
|