textus 0.10.5 → 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 +318 -3
- data/README.md +34 -27
- data/SPEC.md +226 -145
- 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 +4 -4
- data/lib/textus/application/reads/get.rb +9 -12
- data/lib/textus/application/reads/list.rb +15 -0
- data/lib/textus/application/reads/policy_explain.rb +2 -2
- 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/orchestrator.rb +1 -1
- data/lib/textus/application/refresh/worker.rb +8 -8
- data/lib/textus/application/writes/accept.rb +26 -8
- data/lib/textus/application/writes/build.rb +12 -49
- data/lib/textus/application/writes/delete.rb +1 -1
- data/lib/textus/application/writes/mv.rb +144 -0
- data/lib/textus/application/writes/publish.rb +42 -10
- data/lib/textus/application/writes/put.rb +1 -1
- data/lib/textus/application/writes/reject.rb +37 -0
- data/lib/textus/builder/pipeline.rb +1 -1
- data/lib/textus/builder/renderer/json.rb +1 -1
- data/lib/textus/builder/renderer/yaml.rb +1 -1
- data/lib/textus/cli/group/key.rb +1 -1
- data/lib/textus/cli/group/refresh.rb +21 -0
- data/lib/textus/cli/group/rule.rb +11 -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/hook_run.rb +3 -2
- data/lib/textus/cli/verb/hooks.rb +1 -1
- data/lib/textus/cli/verb/{migrate_keys.rb → key_normalize.rb} +1 -1
- 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 +3 -3
- 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/{policy_explain.rb → rule_explain.rb} +2 -3
- data/lib/textus/cli/verb/{policy_list.rb → rule_list.rb} +3 -3
- 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 +9 -3
- data/lib/textus/cli.rb +6 -6
- data/lib/textus/doctor/check/handler_allowlist.rb +1 -1
- data/lib/textus/doctor/check/illegal_keys.rb +39 -16
- data/lib/textus/doctor/check/intake_registration.rb +4 -4
- data/lib/textus/doctor/check/protocol_version.rb +47 -0
- data/lib/textus/doctor/check/{policy_ambiguity.rb → rule_ambiguity.rb} +6 -6
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor.rb +6 -5
- data/lib/textus/domain/freshness/evaluator.rb +1 -1
- data/lib/textus/domain/permission.rb +4 -4
- data/lib/textus/domain/policy/predicates/human_accept.rb +31 -0
- data/lib/textus/domain/policy/predicates/schema_valid.rb +50 -0
- data/lib/textus/domain/policy/promotion.rb +45 -0
- 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/errors.rb +24 -5
- data/lib/textus/hooks/builtin.rb +5 -5
- data/lib/textus/hooks/dispatcher.rb +1 -1
- data/lib/textus/hooks/dsl.rb +3 -10
- data/lib/textus/hooks/loader.rb +1 -2
- data/lib/textus/hooks/registry.rb +22 -21
- data/lib/textus/infra/refresh/detached.rb +1 -1
- data/lib/textus/init.rb +25 -34
- data/lib/textus/intro.rb +65 -9
- 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 +38 -189
- data/lib/textus/manifest/{policies.rb → rules.rb} +12 -10
- data/lib/textus/manifest/schema.rb +49 -0
- data/lib/textus/manifest.rb +50 -24
- data/lib/textus/migrate_keys.rb +1 -1
- 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 +9 -8
- data/lib/textus/refresh.rb +4 -5
- data/lib/textus/schema/tools.rb +8 -7
- data/lib/textus/store/reader.rb +1 -1
- data/lib/textus/store/staleness/intake_check.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 +2 -2
- data/lib/textus.rb +1 -0
- metadata +35 -10
- data/lib/textus/cli/group/policy.rb +0 -11
- 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/manifest.rb
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
require "yaml"
|
|
2
|
+
require_relative "manifest/schema"
|
|
2
3
|
|
|
3
4
|
module Textus
|
|
4
5
|
class Manifest
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
TEXTUS_2_HINT = "Install textus 0.11.x to run the migrator, then upgrade to this version. " \
|
|
7
|
+
"See https://github.com/patrick204nqh/textus/blob/main/CHANGELOG.md#0110".freeze
|
|
8
|
+
|
|
9
|
+
def self.version_hint_for(version)
|
|
10
|
+
version == "textus/2" ? TEXTUS_2_HINT : nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private_class_method :version_hint_for
|
|
12
14
|
|
|
13
15
|
attr_reader :root, :entries, :raw
|
|
14
16
|
|
|
15
17
|
def zones
|
|
16
|
-
@zones ||= Array(@raw["zones"]).to_h { |z| [z["name"], Array(z["
|
|
18
|
+
@zones ||= Array(@raw["zones"]).to_h { |z| [z["name"], Array(z["write_policy"])] }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def zone_readers
|
|
22
|
+
@zone_readers ||= Array(@raw["zones"]).to_h do |z|
|
|
23
|
+
rp = z["read_policy"]
|
|
24
|
+
[z["name"], rp.nil? ? :all : Array(rp)]
|
|
25
|
+
end
|
|
17
26
|
end
|
|
18
27
|
|
|
19
28
|
def zone_writers(zone_name)
|
|
@@ -23,18 +32,35 @@ module Textus
|
|
|
23
32
|
def permission_for(zone_name)
|
|
24
33
|
Textus::Domain::Permission.new(
|
|
25
34
|
zone: zone_name,
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
write_policy: zone_writers(zone_name),
|
|
36
|
+
read_policy: zone_readers[zone_name] || :all,
|
|
28
37
|
)
|
|
29
38
|
end
|
|
30
39
|
|
|
40
|
+
def self.parse(yaml_text, root: ".")
|
|
41
|
+
raw = YAML.safe_load(yaml_text, aliases: false)
|
|
42
|
+
unless raw["version"] == PROTOCOL
|
|
43
|
+
raise BadFrontmatter.new(
|
|
44
|
+
"<string>",
|
|
45
|
+
"unsupported manifest version #{raw["version"].inspect}; expected #{PROTOCOL.inspect}",
|
|
46
|
+
hint: version_hint_for(raw["version"]),
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
new(root, raw)
|
|
51
|
+
end
|
|
52
|
+
|
|
31
53
|
def self.load(root)
|
|
32
54
|
manifest_path = File.join(root, "manifest.yaml")
|
|
33
55
|
raise IoError.new("manifest not found: #{manifest_path}") unless File.exist?(manifest_path)
|
|
34
56
|
|
|
35
57
|
raw = YAML.safe_load_file(manifest_path, aliases: false)
|
|
36
58
|
unless raw["version"] == PROTOCOL
|
|
37
|
-
raise BadFrontmatter.new(
|
|
59
|
+
raise BadFrontmatter.new(
|
|
60
|
+
manifest_path,
|
|
61
|
+
"unsupported manifest version #{raw["version"].inspect}; expected #{PROTOCOL.inspect}",
|
|
62
|
+
hint: version_hint_for(raw["version"]),
|
|
63
|
+
)
|
|
38
64
|
end
|
|
39
65
|
|
|
40
66
|
new(root, raw)
|
|
@@ -45,16 +71,22 @@ module Textus
|
|
|
45
71
|
@raw = raw
|
|
46
72
|
raise BadFrontmatter.new(File.join(root, "manifest.yaml"), "manifest must declare zones:") if Array(raw["zones"]).empty?
|
|
47
73
|
|
|
48
|
-
|
|
74
|
+
Schema.validate!(raw)
|
|
75
|
+
|
|
76
|
+
@entries = Array(raw["entries"]).map do |e|
|
|
77
|
+
entry = Manifest::Entry::Parser.call(self, e)
|
|
78
|
+
Manifest::Entry::Validators.run_all(entry)
|
|
79
|
+
entry
|
|
80
|
+
end
|
|
49
81
|
validate_declared_keys!
|
|
50
82
|
end
|
|
51
83
|
|
|
52
|
-
def
|
|
53
|
-
@
|
|
84
|
+
def rules
|
|
85
|
+
@rules ||= Textus::Manifest::Rules.parse(@raw["rules"] || [])
|
|
54
86
|
end
|
|
55
87
|
|
|
56
|
-
def
|
|
57
|
-
|
|
88
|
+
def rules_for(key)
|
|
89
|
+
rules.for(key)
|
|
58
90
|
end
|
|
59
91
|
|
|
60
92
|
# Returns [Manifest::Entry, resolved_path, remaining_segments]
|
|
@@ -135,7 +167,7 @@ module Textus
|
|
|
135
167
|
|
|
136
168
|
illegal = segs.find { |s| !valid_segment?(s) }
|
|
137
169
|
if illegal
|
|
138
|
-
warn("textus: skipping illegal key segment '#{illegal}' at #{path} — run 'textus key
|
|
170
|
+
warn("textus: skipping illegal key segment '#{illegal}' at #{path} — run 'textus key normalize --dry-run'")
|
|
139
171
|
return nil
|
|
140
172
|
end
|
|
141
173
|
|
|
@@ -158,13 +190,7 @@ module Textus
|
|
|
158
190
|
end
|
|
159
191
|
|
|
160
192
|
def nested_glob(format)
|
|
161
|
-
|
|
162
|
-
when "markdown" then "**/*.md"
|
|
163
|
-
when "json" then "**/*.json"
|
|
164
|
-
when "yaml" then "**/*.{yaml,yml}"
|
|
165
|
-
when "text" then "**/*.txt"
|
|
166
|
-
else raise UsageError.new("unknown format #{format.inspect} for nested glob")
|
|
167
|
-
end
|
|
193
|
+
Textus::Entry.for_format(format).nested_glob
|
|
168
194
|
end
|
|
169
195
|
end
|
|
170
196
|
end
|
data/lib/textus/migrate_keys.rb
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class Operations
|
|
3
|
+
class Reads
|
|
4
|
+
def initialize(ctx)
|
|
5
|
+
@ctx = ctx
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def get
|
|
9
|
+
Application::Reads::Get.new(ctx: @ctx, orchestrator: orchestrator)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def freshness = Application::Reads::Freshness.new(ctx: @ctx)
|
|
13
|
+
def audit = Application::Reads::Audit.new(ctx: @ctx)
|
|
14
|
+
def blame = Application::Reads::Blame.new(ctx: @ctx)
|
|
15
|
+
def policy_explain = Application::Reads::PolicyExplain.new(ctx: @ctx)
|
|
16
|
+
def list = Application::Reads::List.new(ctx: @ctx)
|
|
17
|
+
def where = Application::Reads::Where.new(ctx: @ctx)
|
|
18
|
+
def uid = Application::Reads::Uid.new(ctx: @ctx)
|
|
19
|
+
def schema_envelope = Application::Reads::SchemaEnvelope.new(ctx: @ctx)
|
|
20
|
+
def deps = Application::Reads::Deps.new(ctx: @ctx)
|
|
21
|
+
def rdeps = Application::Reads::Rdeps.new(ctx: @ctx)
|
|
22
|
+
def published = Application::Reads::Published.new(ctx: @ctx)
|
|
23
|
+
def stale = Application::Reads::Stale.new(ctx: @ctx)
|
|
24
|
+
def validate_all = Application::Reads::ValidateAll.new(ctx: @ctx)
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def orchestrator
|
|
29
|
+
Application::Refresh::Orchestrator.new(
|
|
30
|
+
worker: Application::Refresh::Worker.new(ctx: @ctx, bus: @ctx.store.bus),
|
|
31
|
+
bus: @ctx.store.bus,
|
|
32
|
+
store_root: @ctx.store.root,
|
|
33
|
+
store: @ctx.store,
|
|
34
|
+
role: @ctx.role,
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class Operations
|
|
3
|
+
class Refresh
|
|
4
|
+
def initialize(ctx)
|
|
5
|
+
@ctx = ctx
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def worker
|
|
9
|
+
Application::Refresh::Worker.new(ctx: @ctx, bus: @ctx.store.bus)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def orchestrator
|
|
13
|
+
Application::Refresh::Orchestrator.new(
|
|
14
|
+
worker: worker,
|
|
15
|
+
bus: @ctx.store.bus,
|
|
16
|
+
store_root: @ctx.store.root,
|
|
17
|
+
store: @ctx.store,
|
|
18
|
+
role: @ctx.role,
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def all
|
|
23
|
+
Application::Refresh::All.new(ctx: @ctx, bus: @ctx.store.bus)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class Operations
|
|
3
|
+
class Writes
|
|
4
|
+
def initialize(ctx)
|
|
5
|
+
@ctx = ctx
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def put = Application::Writes::Put.new(ctx: @ctx, bus: bus)
|
|
9
|
+
def delete = Application::Writes::Delete.new(ctx: @ctx, bus: bus)
|
|
10
|
+
def mv = Application::Writes::Mv.new(ctx: @ctx, bus: bus)
|
|
11
|
+
def accept = Application::Writes::Accept.new(ctx: @ctx, bus: bus)
|
|
12
|
+
def build = Application::Writes::Build.new(ctx: @ctx, bus: bus)
|
|
13
|
+
def publish = Application::Writes::Publish.new(ctx: @ctx, bus: bus)
|
|
14
|
+
def reject = Application::Writes::Reject.new(ctx: @ctx, bus: bus)
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def bus = @ctx.store.bus
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
# Single canonical entrypoint for invoking application use-cases against a
|
|
3
|
+
# store. Mirrors the directory structure under `lib/textus/application/`:
|
|
4
|
+
#
|
|
5
|
+
# ops = Textus::Operations.for(store, role: "agent")
|
|
6
|
+
# ops.writes.put.call(key, body: "...")
|
|
7
|
+
# ops.reads.get.call(key)
|
|
8
|
+
# ops.refresh.worker.call(key)
|
|
9
|
+
#
|
|
10
|
+
# Replaces the prior `Textus::Composition` module (deleted in v0.12.2).
|
|
11
|
+
class Operations
|
|
12
|
+
def self.for(store, role: Role::DEFAULT, correlation_id: nil, dry_run: false)
|
|
13
|
+
ctx = Application::Context.new(
|
|
14
|
+
store: store,
|
|
15
|
+
role: role,
|
|
16
|
+
correlation_id: correlation_id,
|
|
17
|
+
dry_run: dry_run,
|
|
18
|
+
)
|
|
19
|
+
new(ctx)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :ctx
|
|
23
|
+
|
|
24
|
+
def initialize(ctx)
|
|
25
|
+
@ctx = ctx
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def writes
|
|
29
|
+
@writes ||= Writes.new(@ctx)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def reads
|
|
33
|
+
@reads ||= Reads.new(@ctx)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def refresh
|
|
37
|
+
@refresh ||= Refresh.new(@ctx)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def with_role(role)
|
|
41
|
+
self.class.new(@ctx.with_role(role))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/textus/projection.rb
CHANGED
|
@@ -17,8 +17,8 @@ module Textus
|
|
|
17
17
|
keys = collect_keys
|
|
18
18
|
explicit_pluck = !@spec["pluck"].nil? && @spec["pluck"] != "*"
|
|
19
19
|
rows = keys.map do |key|
|
|
20
|
-
env = @store.get(key)
|
|
21
|
-
row = pluck(env
|
|
20
|
+
env = Operations.for(@store).reads.get.call(key)
|
|
21
|
+
row = pluck(env.meta, env.body)
|
|
22
22
|
explicit_pluck ? row : row.merge("_key" => key)
|
|
23
23
|
end
|
|
24
24
|
reduced = apply_reducer(rows)
|
|
@@ -38,19 +38,20 @@ module Textus
|
|
|
38
38
|
private
|
|
39
39
|
|
|
40
40
|
def apply_reducer(rows)
|
|
41
|
-
name = @spec["
|
|
42
|
-
callable = @store.registry.rpc_callable(:
|
|
43
|
-
view = Application::Context.
|
|
41
|
+
name = @spec["transform"] or return rows
|
|
42
|
+
callable = @store.registry.rpc_callable(:transform_rows, name)
|
|
43
|
+
view = Application::Context.system(@store)
|
|
44
44
|
Timeout.timeout(REDUCER_TIMEOUT_SECONDS) do
|
|
45
|
-
callable.call(store: view, rows: rows, config: @spec["
|
|
45
|
+
callable.call(store: view, rows: rows, config: @spec["transform_config"] || {})
|
|
46
46
|
end
|
|
47
47
|
rescue Timeout::Error
|
|
48
|
-
raise UsageError.new("
|
|
48
|
+
raise UsageError.new("transform_rows '#{name}' exceeded #{REDUCER_TIMEOUT_SECONDS}s timeout")
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def collect_keys
|
|
52
52
|
prefixes = Array(@spec["select"])
|
|
53
|
-
|
|
53
|
+
ops = Operations.for(@store)
|
|
54
|
+
prefixes.flat_map { |p| ops.reads.list.call(prefix: p).map { |row| row["key"] } }.uniq
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
def pluck(frontmatter, _body)
|
data/lib/textus/refresh.rb
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Refresh
|
|
3
3
|
def self.call(store, key, as:)
|
|
4
|
-
|
|
5
|
-
Textus::Composition.refresh_worker(ctx).run(key)
|
|
4
|
+
Textus::Operations.for(store, role: as).refresh.worker.run(key)
|
|
6
5
|
end
|
|
7
6
|
|
|
8
|
-
def self.refresh_stale(store, prefix: nil, zone: nil, as: "
|
|
9
|
-
|
|
10
|
-
Textus::Application::Refresh::All.call(ctx, prefix: prefix, zone: zone)
|
|
7
|
+
def self.refresh_stale(store, prefix: nil, zone: nil, as: "runner")
|
|
8
|
+
ops = Textus::Operations.for(store, role: as)
|
|
9
|
+
Textus::Application::Refresh::All.call(ops.ctx, prefix: prefix, zone: zone)
|
|
11
10
|
end
|
|
12
11
|
|
|
13
12
|
# Normalize the three accepted intake return shapes into the store's
|
data/lib/textus/schema/tools.rb
CHANGED
|
@@ -6,8 +6,8 @@ module Textus
|
|
|
6
6
|
module Tools
|
|
7
7
|
# textus schema init NAME --from=KEY → infer YAML schema from an entry's frontmatter
|
|
8
8
|
def self.init(store, name:, from:)
|
|
9
|
-
env = store.get(from)
|
|
10
|
-
meta = env
|
|
9
|
+
env = Textus::Operations.for(store).reads.get.call(from)
|
|
10
|
+
meta = env.meta
|
|
11
11
|
schema = {
|
|
12
12
|
"name" => name,
|
|
13
13
|
"required" => meta.keys,
|
|
@@ -25,9 +25,9 @@ module Textus
|
|
|
25
25
|
schema = load_schema(store, name)
|
|
26
26
|
drift = []
|
|
27
27
|
store.manifest.enumerate.each do |row|
|
|
28
|
-
env = store.get(row[:key])
|
|
28
|
+
env = Textus::Operations.for(store).reads.get.call(row[:key])
|
|
29
29
|
begin
|
|
30
|
-
schema.validate!(env
|
|
30
|
+
schema.validate!(env.meta)
|
|
31
31
|
rescue SchemaViolation => e
|
|
32
32
|
drift << { "key" => row[:key], "details" => e.details }
|
|
33
33
|
end
|
|
@@ -49,10 +49,11 @@ module Textus
|
|
|
49
49
|
end
|
|
50
50
|
raise UsageError.new("schema migrate needs --rename=OLD:NEW or schema.evolution.migrate_from") if renames.empty?
|
|
51
51
|
|
|
52
|
+
ops = Textus::Operations.for(store, role: "human")
|
|
52
53
|
touched = []
|
|
53
54
|
store.manifest.enumerate.each do |row|
|
|
54
|
-
env =
|
|
55
|
-
meta = env
|
|
55
|
+
env = ops.reads.get.call(row[:key])
|
|
56
|
+
meta = env.meta.dup
|
|
56
57
|
changed = false
|
|
57
58
|
renames.each do |old, new|
|
|
58
59
|
if meta.key?(old)
|
|
@@ -62,7 +63,7 @@ module Textus
|
|
|
62
63
|
end
|
|
63
64
|
next unless changed
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
ops.writes.put.call(row[:key], meta: meta, body: env.body)
|
|
66
67
|
touched << row[:key]
|
|
67
68
|
end
|
|
68
69
|
{ "protocol" => PROTOCOL, "migrated" => touched, "renames" => renames }
|
data/lib/textus/store/reader.rb
CHANGED
|
@@ -46,7 +46,7 @@ module Textus
|
|
|
46
46
|
# Returns the Textus UID for a key (or nil if the entry has none yet).
|
|
47
47
|
# Raises UnknownKey if the key doesn't resolve to a real file.
|
|
48
48
|
def uid(key)
|
|
49
|
-
get(key)
|
|
49
|
+
get(key).uid
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
def deps(key) = Dependencies.deps_of(@manifest, key)
|
|
@@ -13,7 +13,7 @@ module Textus
|
|
|
13
13
|
def rows_for(mentry)
|
|
14
14
|
return [] unless mentry.intake_handler
|
|
15
15
|
|
|
16
|
-
ttl = @manifest.
|
|
16
|
+
ttl = @manifest.rules_for(mentry.key).refresh&.ttl_seconds
|
|
17
17
|
return [] unless ttl
|
|
18
18
|
|
|
19
19
|
path = Textus::Key::Path.resolve(@manifest, mentry)
|
|
@@ -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(:reject, 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