textus 0.12.1 → 0.14.1
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 +231 -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/builder/pipeline.rb +46 -2
- 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 +19 -32
- 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
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
|