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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class Manifest
|
|
3
|
+
class Entry
|
|
4
|
+
module Validators
|
|
5
|
+
module IndexFilename
|
|
6
|
+
def self.call(entry)
|
|
7
|
+
return if entry.index_filename.nil?
|
|
8
|
+
|
|
9
|
+
check_shape!(entry)
|
|
10
|
+
check_extension!(entry)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.check_shape!(entry)
|
|
14
|
+
raise UsageError.new("entry '#{entry.key}': index_filename requires nested: true") unless entry.nested
|
|
15
|
+
|
|
16
|
+
unless entry.index_filename.is_a?(String) && !entry.index_filename.empty?
|
|
17
|
+
raise UsageError.new("entry '#{entry.key}': index_filename must be a non-empty string")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
return unless entry.index_filename.include?("/") || File.basename(entry.index_filename) != entry.index_filename
|
|
21
|
+
|
|
22
|
+
raise UsageError.new("entry '#{entry.key}': index_filename must be a bare basename (no slashes)")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.check_extension!(entry)
|
|
26
|
+
ext = File.extname(entry.index_filename)
|
|
27
|
+
inferred = Textus::Entry.infer_from_extension(ext)
|
|
28
|
+
|
|
29
|
+
if inferred.nil?
|
|
30
|
+
raise UsageError.new(
|
|
31
|
+
"entry '#{entry.key}': index_filename #{entry.index_filename.inspect} has unknown extension #{ext.inspect}",
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
return if inferred == entry.format
|
|
35
|
+
|
|
36
|
+
raise UsageError.new(
|
|
37
|
+
"entry '#{entry.key}': index_filename extension #{ext.inspect} implies format #{inferred.inspect}, " \
|
|
38
|
+
"but entry format is #{entry.format.inspect}",
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class Manifest
|
|
3
|
+
class Entry
|
|
4
|
+
module Validators
|
|
5
|
+
module InjectIntro
|
|
6
|
+
def self.call(entry)
|
|
7
|
+
return unless entry.inject_intro
|
|
8
|
+
|
|
9
|
+
raise UsageError.new("entry '#{entry.key}': inject_intro: is only valid on derived entries") unless entry.in_generator_zone?
|
|
10
|
+
return unless entry.template.nil?
|
|
11
|
+
|
|
12
|
+
raise UsageError.new("entry '#{entry.key}': inject_intro: requires a template:")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class Manifest
|
|
3
|
+
class Entry
|
|
4
|
+
module Validators
|
|
5
|
+
module PublishEach
|
|
6
|
+
KNOWN_VARS = %w[leaf basename key ext].freeze
|
|
7
|
+
VAR_RE = /\{([a-z]+)\}/
|
|
8
|
+
REQUIRED_DISCRIMINATOR_VARS = %w[leaf basename key].freeze
|
|
9
|
+
|
|
10
|
+
def self.call(entry)
|
|
11
|
+
return if entry.publish_each.nil?
|
|
12
|
+
|
|
13
|
+
raise UsageError.new("entry '#{entry.key}': publish_each requires nested: true") unless entry.nested
|
|
14
|
+
raise UsageError.new("entry '#{entry.key}': publish_to and publish_each are mutually exclusive") unless entry.publish_to.empty?
|
|
15
|
+
raise UsageError.new("entry '#{entry.key}': publish_each must be a string") unless entry.publish_each.is_a?(String)
|
|
16
|
+
|
|
17
|
+
used_vars = entry.publish_each.scan(VAR_RE).flatten
|
|
18
|
+
unknown = used_vars - KNOWN_VARS
|
|
19
|
+
unless unknown.empty?
|
|
20
|
+
raise UsageError.new(
|
|
21
|
+
"entry '#{entry.key}': publish_each uses unknown template variable(s) " \
|
|
22
|
+
"#{unknown.map { |v| "{#{v}}" }.join(", ")}. Known: #{KNOWN_VARS.map { |v| "{#{v}}" }.join(", ")}.",
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
return if used_vars.any? { |v| REQUIRED_DISCRIMINATOR_VARS.include?(v) }
|
|
27
|
+
|
|
28
|
+
raise UsageError.new(
|
|
29
|
+
"entry '#{entry.key}': publish_each must reference at least one of {leaf}, {basename}, or {key} " \
|
|
30
|
+
"(else every leaf would clobber the same target).",
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class Manifest
|
|
3
|
+
class Entry
|
|
4
|
+
module Validators
|
|
5
|
+
REGISTERED = [
|
|
6
|
+
Events,
|
|
7
|
+
PublishEach,
|
|
8
|
+
InjectIntro,
|
|
9
|
+
IndexFilename,
|
|
10
|
+
FormatMatrix,
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
def self.run_all(entry)
|
|
14
|
+
REGISTERED.each { |v| v.call(entry) }
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -1,41 +1,44 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
class Manifest
|
|
3
3
|
class Entry
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
attr_reader :key, :path, :zone, :schema, :owner, :nested,
|
|
10
|
-
:
|
|
11
|
-
:
|
|
12
|
-
:
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
# Re-exported for backward compatibility with callers that referenced these
|
|
5
|
+
# constants on Entry. Canonical source is the PublishEach validator.
|
|
6
|
+
PUBLISH_EACH_VARS = Validators::PublishEach::KNOWN_VARS
|
|
7
|
+
PUBLISH_EACH_VAR_RE = Validators::PublishEach::VAR_RE
|
|
8
|
+
|
|
9
|
+
attr_reader :raw, :key, :path, :zone, :schema, :owner, :nested,
|
|
10
|
+
:template, :publish_to, :publish_each,
|
|
11
|
+
:events, :inject_intro, :index_filename, :format,
|
|
12
|
+
:compute, :projection, :generator,
|
|
13
|
+
:intake_handler, :intake_config
|
|
14
|
+
|
|
15
|
+
# rubocop:disable Metrics/ParameterLists
|
|
16
|
+
def initialize(manifest:, raw:, key:, path:, zone:, schema:, owner:, nested:,
|
|
17
|
+
template:, publish_to:, publish_each:, events:, inject_intro:,
|
|
18
|
+
index_filename:, format:, compute:, projection:, generator:,
|
|
19
|
+
intake_handler:, intake_config:)
|
|
15
20
|
@manifest = manifest
|
|
16
21
|
@raw = raw
|
|
17
|
-
@key =
|
|
18
|
-
@path =
|
|
19
|
-
@zone =
|
|
20
|
-
@schema =
|
|
21
|
-
@owner =
|
|
22
|
-
@nested =
|
|
23
|
-
|
|
24
|
-
@
|
|
25
|
-
@
|
|
26
|
-
@
|
|
27
|
-
@
|
|
28
|
-
@
|
|
29
|
-
@
|
|
30
|
-
@
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
validate_publish_each!
|
|
36
|
-
validate_inject_intro!
|
|
37
|
-
validate_index_filename!
|
|
22
|
+
@key = key
|
|
23
|
+
@path = path
|
|
24
|
+
@zone = zone
|
|
25
|
+
@schema = schema
|
|
26
|
+
@owner = owner
|
|
27
|
+
@nested = nested
|
|
28
|
+
@template = template
|
|
29
|
+
@publish_to = publish_to
|
|
30
|
+
@publish_each = publish_each
|
|
31
|
+
@events = events
|
|
32
|
+
@inject_intro = inject_intro
|
|
33
|
+
@index_filename = index_filename
|
|
34
|
+
@format = format
|
|
35
|
+
@compute = compute
|
|
36
|
+
@projection = projection
|
|
37
|
+
@generator = generator
|
|
38
|
+
@intake_handler = intake_handler
|
|
39
|
+
@intake_config = intake_config
|
|
38
40
|
end
|
|
41
|
+
# rubocop:enable Metrics/ParameterLists
|
|
39
42
|
|
|
40
43
|
# Resolves the per-leaf target path (relative to repo root) for a full
|
|
41
44
|
# dotted key under this entry's prefix. Returns nil if this entry has no
|
|
@@ -69,192 +72,11 @@ module Textus
|
|
|
69
72
|
|
|
70
73
|
private
|
|
71
74
|
|
|
72
|
-
# `index_filename:` makes a nested entry treat a fixed basename (e.g.
|
|
73
|
-
# `SKILL.md`) as the per-directory row. The directory path becomes the
|
|
74
|
-
# key suffix; sibling files are not enumerated. Allows projecting
|
|
75
|
-
# spec-mandated filenames that would otherwise be rejected by the
|
|
76
|
-
# lowercase-only key segment grammar.
|
|
77
|
-
def validate_index_filename!
|
|
78
|
-
return if @index_filename.nil?
|
|
79
|
-
|
|
80
|
-
raise UsageError.new("entry '#{@key}': index_filename requires nested: true") unless @nested
|
|
81
|
-
unless @index_filename.is_a?(String) && !@index_filename.empty?
|
|
82
|
-
raise UsageError.new("entry '#{@key}': index_filename must be a non-empty string")
|
|
83
|
-
end
|
|
84
|
-
if @index_filename.include?("/") || File.basename(@index_filename) != @index_filename
|
|
85
|
-
raise UsageError.new("entry '#{@key}': index_filename must be a bare basename (no slashes)")
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
ext = File.extname(@index_filename)
|
|
89
|
-
inferred = Manifest::EXT_TO_FORMAT[ext]
|
|
90
|
-
if inferred.nil?
|
|
91
|
-
raise UsageError.new(
|
|
92
|
-
"entry '#{@key}': index_filename #{@index_filename.inspect} has unknown extension #{ext.inspect}",
|
|
93
|
-
)
|
|
94
|
-
end
|
|
95
|
-
return if inferred == @format
|
|
96
|
-
|
|
97
|
-
raise UsageError.new(
|
|
98
|
-
"entry '#{@key}': index_filename extension #{ext.inspect} implies format #{inferred.inspect}, " \
|
|
99
|
-
"but entry format is #{@format.inspect}",
|
|
100
|
-
)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
75
|
def zone_writers
|
|
104
76
|
@manifest.zone_writers(@zone)
|
|
105
77
|
rescue UsageError => e
|
|
106
78
|
raise UsageError.new("entry '#{@key}': #{e.message}")
|
|
107
79
|
end
|
|
108
|
-
|
|
109
|
-
def validate_inject_intro!
|
|
110
|
-
return unless @inject_intro
|
|
111
|
-
|
|
112
|
-
unless in_generator_zone?
|
|
113
|
-
raise UsageError.new(
|
|
114
|
-
"entry '#{@key}': inject_intro: is only valid on derived entries",
|
|
115
|
-
)
|
|
116
|
-
end
|
|
117
|
-
return unless @template.nil?
|
|
118
|
-
|
|
119
|
-
raise UsageError.new(
|
|
120
|
-
"entry '#{@key}': inject_intro: requires a template:",
|
|
121
|
-
)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def validate_publish_each!
|
|
125
|
-
return if @publish_each.nil?
|
|
126
|
-
|
|
127
|
-
raise UsageError.new("entry '#{@key}': publish_each requires nested: true") unless @nested
|
|
128
|
-
raise UsageError.new("entry '#{@key}': publish_to and publish_each are mutually exclusive") unless @publish_to.empty?
|
|
129
|
-
raise UsageError.new("entry '#{@key}': publish_each must be a string") unless @publish_each.is_a?(String)
|
|
130
|
-
|
|
131
|
-
used_vars = @publish_each.scan(PUBLISH_EACH_VAR_RE).flatten
|
|
132
|
-
unknown = used_vars - PUBLISH_EACH_VARS
|
|
133
|
-
unless unknown.empty?
|
|
134
|
-
raise UsageError.new(
|
|
135
|
-
"entry '#{@key}': publish_each uses unknown template variable(s) " \
|
|
136
|
-
"#{unknown.map { |v| "{#{v}}" }.join(", ")}. Known: #{PUBLISH_EACH_VARS.map { |v| "{#{v}}" }.join(", ")}.",
|
|
137
|
-
)
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
required = %w[leaf basename key]
|
|
141
|
-
return if used_vars.any? { |v| required.include?(v) }
|
|
142
|
-
|
|
143
|
-
raise UsageError.new(
|
|
144
|
-
"entry '#{@key}': publish_each must reference at least one of {leaf}, {basename}, or {key} " \
|
|
145
|
-
"(else every leaf would clobber the same target).",
|
|
146
|
-
)
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def resolve_format!(declared)
|
|
150
|
-
ext = File.extname(@path)
|
|
151
|
-
inferred = Manifest::EXT_TO_FORMAT[ext]
|
|
152
|
-
|
|
153
|
-
if declared.nil?
|
|
154
|
-
return inferred if inferred
|
|
155
|
-
# No extension: nested defaults to markdown, leaf with no ext also markdown.
|
|
156
|
-
return "markdown" if ext == "" && @nested
|
|
157
|
-
return "markdown" if ext == ""
|
|
158
|
-
else
|
|
159
|
-
unless Manifest::EXT_TO_FORMAT.values.include?(declared)
|
|
160
|
-
raise UsageError.new("entry '#{@key}': unknown format #{declared.inspect}")
|
|
161
|
-
end
|
|
162
|
-
# If the path has an extension, the declared format must match.
|
|
163
|
-
if ext != "" && inferred && inferred != declared
|
|
164
|
-
raise UsageError.new(
|
|
165
|
-
"entry '#{@key}': path extension #{ext.inspect} does not match declared format #{declared.inspect}",
|
|
166
|
-
)
|
|
167
|
-
end
|
|
168
|
-
return declared
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
"markdown"
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
175
|
-
def validate_format_matrix!
|
|
176
|
-
ext = File.extname(@path)
|
|
177
|
-
|
|
178
|
-
case @format
|
|
179
|
-
when "markdown"
|
|
180
|
-
# .md, or no extension (will be appended). Anything else is a mismatch caught above.
|
|
181
|
-
raise UsageError.new("entry '#{@key}': markdown format requires '.md' path (got #{ext.inspect})") if ext != "" && ext != ".md"
|
|
182
|
-
when "json"
|
|
183
|
-
if @nested
|
|
184
|
-
# nested json: path is a directory; ext must be empty.
|
|
185
|
-
raise UsageError.new("entry '#{@key}': nested json path must not have an extension") if ext != ""
|
|
186
|
-
elsif ext != ".json"
|
|
187
|
-
raise UsageError.new("entry '#{@key}': json format requires '.json' path (got #{ext.inspect})")
|
|
188
|
-
end
|
|
189
|
-
when "yaml"
|
|
190
|
-
if @nested
|
|
191
|
-
raise UsageError.new("entry '#{@key}': nested yaml path must not have an extension") if ext != ""
|
|
192
|
-
elsif ext != ".yaml" && ext != ".yml"
|
|
193
|
-
raise UsageError.new("entry '#{@key}': yaml format requires '.yaml' or '.yml' path (got #{ext.inspect})")
|
|
194
|
-
end
|
|
195
|
-
when "text"
|
|
196
|
-
if @nested
|
|
197
|
-
raise UsageError.new("entry '#{@key}': nested text path must not have an extension") if ext != ""
|
|
198
|
-
elsif ext != ".txt" && ext != ""
|
|
199
|
-
raise UsageError.new("entry '#{@key}': text format requires '.txt' or no extension (got #{ext.inspect})")
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
# Schema rules.
|
|
204
|
-
raise UsageError.new("entry '#{@key}': text format must not declare a schema") if @format == "text" && !@schema.nil?
|
|
205
|
-
|
|
206
|
-
# Template-required-for-derived rules. Skipped for entries materialized by an
|
|
207
|
-
# external generator: command (those produce the bytes themselves).
|
|
208
|
-
if in_generator_zone? && @template.nil? && @generator.nil? &&
|
|
209
|
-
(@format == "markdown" || @format == "text") && !@nested
|
|
210
|
-
raise UsageError.new("entry '#{@key}': derived #{@format} entries require a template")
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
214
|
-
|
|
215
|
-
def parse_compute!(raw)
|
|
216
|
-
src = raw["compute"]
|
|
217
|
-
unless src
|
|
218
|
-
@compute = nil
|
|
219
|
-
@projection = nil
|
|
220
|
-
@generator = nil
|
|
221
|
-
return
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
kind = src["kind"]
|
|
225
|
-
unless COMPUTE_KINDS.include?(kind)
|
|
226
|
-
raise BadManifest.new(
|
|
227
|
-
"entry '#{@key}': compute.kind must be one of #{COMPUTE_KINDS.join(", ")} (got #{kind.inspect})",
|
|
228
|
-
)
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
@compute = src.freeze
|
|
232
|
-
if kind == "projection"
|
|
233
|
-
@projection = @compute
|
|
234
|
-
@generator = nil
|
|
235
|
-
else
|
|
236
|
-
@generator = @compute
|
|
237
|
-
@projection = nil
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
def parse_intake!(src)
|
|
242
|
-
src ||= {}
|
|
243
|
-
@intake_handler = src["handler"]
|
|
244
|
-
@intake_config = src["config"] || {}
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
def validate_events!
|
|
248
|
-
pubsub_events = Hooks::Registry::EVENTS.select { |_, s| s[:mode] == :pubsub }.keys
|
|
249
|
-
@events.each_key do |evt|
|
|
250
|
-
next if pubsub_events.include?(evt.to_sym)
|
|
251
|
-
|
|
252
|
-
raise UsageError.new(
|
|
253
|
-
"entry '#{@key}': unknown event '#{evt}' in events: block. " \
|
|
254
|
-
"Known events: #{pubsub_events.join(", ")}.",
|
|
255
|
-
)
|
|
256
|
-
end
|
|
257
|
-
end
|
|
258
80
|
end
|
|
259
81
|
end
|
|
260
82
|
end
|
data/lib/textus/manifest.rb
CHANGED
|
@@ -3,14 +3,6 @@ require_relative "manifest/schema"
|
|
|
3
3
|
|
|
4
4
|
module Textus
|
|
5
5
|
class Manifest
|
|
6
|
-
EXT_TO_FORMAT = {
|
|
7
|
-
".md" => "markdown",
|
|
8
|
-
".json" => "json",
|
|
9
|
-
".yaml" => "yaml",
|
|
10
|
-
".yml" => "yaml",
|
|
11
|
-
".txt" => "text",
|
|
12
|
-
}.freeze
|
|
13
|
-
|
|
14
6
|
TEXTUS_2_HINT = "Install textus 0.11.x to run the migrator, then upgrade to this version. " \
|
|
15
7
|
"See https://github.com/patrick204nqh/textus/blob/main/CHANGELOG.md#0110".freeze
|
|
16
8
|
|
|
@@ -81,7 +73,11 @@ module Textus
|
|
|
81
73
|
|
|
82
74
|
Schema.validate!(raw)
|
|
83
75
|
|
|
84
|
-
@entries = Array(raw["entries"]).map
|
|
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
|
|
85
81
|
validate_declared_keys!
|
|
86
82
|
end
|
|
87
83
|
|
|
@@ -194,13 +190,7 @@ module Textus
|
|
|
194
190
|
end
|
|
195
191
|
|
|
196
192
|
def nested_glob(format)
|
|
197
|
-
|
|
198
|
-
when "markdown" then "**/*.md"
|
|
199
|
-
when "json" then "**/*.json"
|
|
200
|
-
when "yaml" then "**/*.{yaml,yml}"
|
|
201
|
-
when "text" then "**/*.txt"
|
|
202
|
-
else raise UsageError.new("unknown format #{format.inspect} for nested glob")
|
|
203
|
-
end
|
|
193
|
+
Textus::Entry.for_format(format).nested_glob
|
|
204
194
|
end
|
|
205
195
|
end
|
|
206
196
|
end
|
|
@@ -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)
|
|
@@ -40,7 +40,7 @@ module Textus
|
|
|
40
40
|
def apply_reducer(rows)
|
|
41
41
|
name = @spec["transform"] or return rows
|
|
42
42
|
callable = @store.registry.rpc_callable(:transform_rows, name)
|
|
43
|
-
view = Application::Context.
|
|
43
|
+
view = Application::Context.system(@store)
|
|
44
44
|
Timeout.timeout(REDUCER_TIMEOUT_SECONDS) do
|
|
45
45
|
callable.call(store: view, rows: rows, config: @spec["transform_config"] || {})
|
|
46
46
|
end
|
|
@@ -50,7 +50,8 @@ module Textus
|
|
|
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
7
|
def self.refresh_stale(store, prefix: nil, zone: nil, as: "runner")
|
|
9
|
-
|
|
10
|
-
Textus::Application::Refresh::All.call(ctx, prefix: prefix, zone: zone)
|
|
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)
|