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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class Manifest
|
|
3
|
+
class Entry
|
|
4
|
+
module Parser
|
|
5
|
+
COMPUTE_KINDS = %w[projection external].freeze
|
|
6
|
+
|
|
7
|
+
def self.call(manifest, raw)
|
|
8
|
+
key = raw["key"] or raise UsageError.new("manifest entry missing key")
|
|
9
|
+
path = raw["path"] or raise UsageError.new("manifest entry '#{key}' missing path")
|
|
10
|
+
zone = raw["zone"] or raise UsageError.new("manifest entry '#{key}' missing zone")
|
|
11
|
+
|
|
12
|
+
nested = raw["nested"] == true
|
|
13
|
+
compute, projection, generator = parse_compute(raw, key)
|
|
14
|
+
intake_handler, intake_config = parse_intake(raw["intake"])
|
|
15
|
+
format = resolve_format(raw, path, nested)
|
|
16
|
+
|
|
17
|
+
Textus::Manifest::Entry.new(
|
|
18
|
+
manifest: manifest, raw: raw,
|
|
19
|
+
key: key, path: path, zone: zone,
|
|
20
|
+
schema: raw["schema"], owner: raw["owner"],
|
|
21
|
+
nested: nested,
|
|
22
|
+
template: raw["template"],
|
|
23
|
+
publish_to: Array(raw["publish_to"]),
|
|
24
|
+
publish_each: raw["publish_each"],
|
|
25
|
+
events: raw["events"] || {},
|
|
26
|
+
inject_intro: raw["inject_intro"] == true,
|
|
27
|
+
index_filename: raw["index_filename"],
|
|
28
|
+
format: format,
|
|
29
|
+
compute: compute, projection: projection, generator: generator,
|
|
30
|
+
intake_handler: intake_handler, intake_config: intake_config
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.parse_compute(raw, key)
|
|
35
|
+
src = raw["compute"]
|
|
36
|
+
return [nil, nil, nil] if src.nil?
|
|
37
|
+
|
|
38
|
+
kind = src["kind"]
|
|
39
|
+
unless COMPUTE_KINDS.include?(kind)
|
|
40
|
+
raise BadManifest.new(
|
|
41
|
+
"entry '#{key}': compute.kind must be one of #{COMPUTE_KINDS.join(", ")} (got #{kind.inspect})",
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
frozen = src.freeze
|
|
46
|
+
if kind == "projection"
|
|
47
|
+
[frozen, frozen, nil]
|
|
48
|
+
else
|
|
49
|
+
[frozen, nil, frozen]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.parse_intake(src)
|
|
54
|
+
src ||= {}
|
|
55
|
+
[src["handler"], src["config"] || {}]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.resolve_format(raw, path, nested)
|
|
59
|
+
declared = raw["format"]
|
|
60
|
+
ext = File.extname(path)
|
|
61
|
+
inferred = Textus::Entry.infer_from_extension(ext)
|
|
62
|
+
|
|
63
|
+
if declared.nil?
|
|
64
|
+
return inferred if inferred
|
|
65
|
+
return "markdown" if ext == "" && nested
|
|
66
|
+
return "markdown" if ext == ""
|
|
67
|
+
|
|
68
|
+
return "markdown"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
raise UsageError.new("entry '#{raw["key"]}': unknown format #{declared.inspect}") unless Textus::Entry.formats.include?(declared)
|
|
72
|
+
|
|
73
|
+
if ext != "" && inferred && inferred != declared
|
|
74
|
+
raise UsageError.new(
|
|
75
|
+
"entry '#{raw["key"]}': path extension #{ext.inspect} does not match declared format #{declared.inspect}",
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
declared
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class Manifest
|
|
3
|
+
class Entry
|
|
4
|
+
module Validators
|
|
5
|
+
module Events
|
|
6
|
+
def self.call(entry)
|
|
7
|
+
pubsub_events = Textus::Hooks::Registry::EVENTS.select { |_, s| s[:mode] == :pubsub }.keys
|
|
8
|
+
entry.events.each_key do |evt|
|
|
9
|
+
next if pubsub_events.include?(evt.to_sym)
|
|
10
|
+
|
|
11
|
+
raise UsageError.new(
|
|
12
|
+
"entry '#{entry.key}': unknown event '#{evt}' in events: block. " \
|
|
13
|
+
"Known events: #{pubsub_events.join(", ")}.",
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class Manifest
|
|
3
|
+
class Entry
|
|
4
|
+
module Validators
|
|
5
|
+
module FormatMatrix
|
|
6
|
+
def self.call(entry)
|
|
7
|
+
begin
|
|
8
|
+
Textus::Entry.for_format(entry.format).validate_path_extension(entry.path, entry.nested)
|
|
9
|
+
rescue UsageError => e
|
|
10
|
+
raise UsageError.new("entry '#{entry.key}': #{e.message}")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
if entry.format == "text" && !entry.schema.nil?
|
|
14
|
+
raise UsageError.new("entry '#{entry.key}': text format must not declare a schema")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
return unless entry.in_generator_zone? && entry.template.nil? && entry.generator.nil? &&
|
|
18
|
+
%w[markdown text].include?(entry.format) && !entry.nested
|
|
19
|
+
|
|
20
|
+
raise UsageError.new("entry '#{entry.key}': derived #{entry.format} entries require a template")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -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
|
|
|
@@ -47,14 +39,7 @@ module Textus
|
|
|
47
39
|
|
|
48
40
|
def self.parse(yaml_text, root: ".")
|
|
49
41
|
raw = YAML.safe_load(yaml_text, aliases: false)
|
|
50
|
-
|
|
51
|
-
raise BadFrontmatter.new(
|
|
52
|
-
"<string>",
|
|
53
|
-
"unsupported manifest version #{raw["version"].inspect}; expected #{PROTOCOL.inspect}",
|
|
54
|
-
hint: version_hint_for(raw["version"]),
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
|
|
42
|
+
check_version!(raw, "<string>")
|
|
58
43
|
new(root, raw)
|
|
59
44
|
end
|
|
60
45
|
|
|
@@ -63,17 +48,21 @@ module Textus
|
|
|
63
48
|
raise IoError.new("manifest not found: #{manifest_path}") unless File.exist?(manifest_path)
|
|
64
49
|
|
|
65
50
|
raw = YAML.safe_load_file(manifest_path, aliases: false)
|
|
66
|
-
|
|
67
|
-
raise BadFrontmatter.new(
|
|
68
|
-
manifest_path,
|
|
69
|
-
"unsupported manifest version #{raw["version"].inspect}; expected #{PROTOCOL.inspect}",
|
|
70
|
-
hint: version_hint_for(raw["version"]),
|
|
71
|
-
)
|
|
72
|
-
end
|
|
73
|
-
|
|
51
|
+
check_version!(raw, manifest_path)
|
|
74
52
|
new(root, raw)
|
|
75
53
|
end
|
|
76
54
|
|
|
55
|
+
def self.check_version!(raw, source)
|
|
56
|
+
return if raw["version"] == PROTOCOL
|
|
57
|
+
|
|
58
|
+
raise BadFrontmatter.new(
|
|
59
|
+
source,
|
|
60
|
+
"unsupported manifest version #{raw["version"].inspect}; expected #{PROTOCOL.inspect}",
|
|
61
|
+
hint: version_hint_for(raw["version"]),
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
private_class_method :check_version!
|
|
65
|
+
|
|
77
66
|
def initialize(root, raw)
|
|
78
67
|
@root = root
|
|
79
68
|
@raw = raw
|
|
@@ -81,7 +70,11 @@ module Textus
|
|
|
81
70
|
|
|
82
71
|
Schema.validate!(raw)
|
|
83
72
|
|
|
84
|
-
@entries = Array(raw["entries"]).map
|
|
73
|
+
@entries = Array(raw["entries"]).map do |e|
|
|
74
|
+
entry = Manifest::Entry::Parser.call(self, e)
|
|
75
|
+
Manifest::Entry::Validators.run_all(entry)
|
|
76
|
+
entry
|
|
77
|
+
end
|
|
85
78
|
validate_declared_keys!
|
|
86
79
|
end
|
|
87
80
|
|
|
@@ -194,13 +187,7 @@ module Textus
|
|
|
194
187
|
end
|
|
195
188
|
|
|
196
189
|
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
|
|
190
|
+
Textus::Entry.for_format(format).nested_glob
|
|
204
191
|
end
|
|
205
192
|
end
|
|
206
193
|
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
|