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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +60 -40
  3. data/CHANGELOG.md +231 -0
  4. data/README.md +6 -12
  5. data/SPEC.md +4 -1
  6. data/docs/conventions.md +8 -8
  7. data/lib/textus/application/context.rb +4 -0
  8. data/lib/textus/application/reads/blame.rb +1 -1
  9. data/lib/textus/application/reads/deps.rb +15 -0
  10. data/lib/textus/application/reads/freshness.rb +2 -2
  11. data/lib/textus/application/reads/get.rb +8 -11
  12. data/lib/textus/application/reads/list.rb +15 -0
  13. data/lib/textus/application/reads/published.rb +15 -0
  14. data/lib/textus/application/reads/rdeps.rb +15 -0
  15. data/lib/textus/application/reads/schema_envelope.rb +15 -0
  16. data/lib/textus/application/reads/stale.rb +15 -0
  17. data/lib/textus/application/reads/uid.rb +15 -0
  18. data/lib/textus/application/reads/validate_all.rb +15 -0
  19. data/lib/textus/application/reads/where.rb +15 -0
  20. data/lib/textus/application/refresh/all.rb +2 -2
  21. data/lib/textus/application/refresh/worker.rb +3 -3
  22. data/lib/textus/application/writes/accept.rb +7 -7
  23. data/lib/textus/application/writes/build.rb +10 -47
  24. data/lib/textus/application/writes/mv.rb +144 -0
  25. data/lib/textus/application/writes/publish.rb +41 -9
  26. data/lib/textus/application/writes/reject.rb +37 -0
  27. data/lib/textus/builder/pipeline.rb +46 -2
  28. data/lib/textus/cli/verb/accept.rb +1 -2
  29. data/lib/textus/cli/verb/audit.rb +3 -3
  30. data/lib/textus/cli/verb/blame.rb +1 -2
  31. data/lib/textus/cli/verb/build.rb +6 -2
  32. data/lib/textus/cli/verb/delete.rb +1 -2
  33. data/lib/textus/cli/verb/deps.rb +1 -1
  34. data/lib/textus/cli/verb/freshness.rb +1 -2
  35. data/lib/textus/cli/verb/get.rb +2 -3
  36. data/lib/textus/cli/verb/list.rb +1 -1
  37. data/lib/textus/cli/verb/mv.rb +1 -1
  38. data/lib/textus/cli/verb/published.rb +1 -1
  39. data/lib/textus/cli/verb/put.rb +2 -2
  40. data/lib/textus/cli/verb/rdeps.rb +1 -1
  41. data/lib/textus/cli/verb/refresh.rb +1 -2
  42. data/lib/textus/cli/verb/reject.rb +1 -1
  43. data/lib/textus/cli/verb/rule_explain.rb +1 -2
  44. data/lib/textus/cli/verb/schema.rb +1 -1
  45. data/lib/textus/cli/verb/uid.rb +1 -1
  46. data/lib/textus/cli/verb/where.rb +1 -1
  47. data/lib/textus/cli/verb.rb +6 -1
  48. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  49. data/lib/textus/doctor.rb +1 -1
  50. data/lib/textus/domain/freshness/evaluator.rb +1 -1
  51. data/lib/textus/domain/policy/predicates/schema_valid.rb +3 -3
  52. data/lib/textus/entry/base.rb +28 -0
  53. data/lib/textus/entry/json.rb +59 -0
  54. data/lib/textus/entry/markdown.rb +46 -0
  55. data/lib/textus/entry/text.rb +35 -0
  56. data/lib/textus/entry/yaml.rb +59 -0
  57. data/lib/textus/entry.rb +16 -0
  58. data/lib/textus/envelope.rb +44 -14
  59. data/lib/textus/intro.rb +56 -0
  60. data/lib/textus/manifest/entry/parser.rb +84 -0
  61. data/lib/textus/manifest/entry/validators/events.rb +21 -0
  62. data/lib/textus/manifest/entry/validators/format_matrix.rb +26 -0
  63. data/lib/textus/manifest/entry/validators/index_filename.rb +45 -0
  64. data/lib/textus/manifest/entry/validators/inject_intro.rb +18 -0
  65. data/lib/textus/manifest/entry/validators/publish_each.rb +37 -0
  66. data/lib/textus/manifest/entry/validators.rb +20 -0
  67. data/lib/textus/manifest/entry.rb +35 -213
  68. data/lib/textus/manifest.rb +19 -32
  69. data/lib/textus/operations/reads.rb +39 -0
  70. data/lib/textus/operations/refresh.rb +27 -0
  71. data/lib/textus/operations/writes.rb +21 -0
  72. data/lib/textus/operations.rb +44 -0
  73. data/lib/textus/projection.rb +5 -4
  74. data/lib/textus/refresh.rb +3 -4
  75. data/lib/textus/schema/tools.rb +8 -7
  76. data/lib/textus/store/reader.rb +1 -1
  77. data/lib/textus/store/validator.rb +3 -3
  78. data/lib/textus/store/writer.rb +5 -74
  79. data/lib/textus/store.rb +1 -55
  80. data/lib/textus/version.rb +1 -1
  81. metadata +23 -4
  82. data/lib/textus/composition.rb +0 -72
  83. data/lib/textus/proposal.rb +0 -10
  84. 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
- PUBLISH_EACH_VARS = %w[leaf basename key ext].freeze
5
- PUBLISH_EACH_VAR_RE = /\{([a-z]+)\}/
6
-
7
- COMPUTE_KINDS = %w[projection external].freeze
8
-
9
- attr_reader :key, :path, :zone, :schema, :owner, :nested, :generator, :raw, :format,
10
- :projection, :template, :publish_to, :publish_each,
11
- :intake_handler, :intake_config,
12
- :events, :inject_intro, :index_filename, :compute
13
-
14
- def initialize(manifest, raw)
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 = raw["key"] or raise UsageError.new("manifest entry missing key")
18
- @path = raw["path"] or raise UsageError.new("manifest entry '#{@key}' missing path")
19
- @zone = raw["zone"] or raise UsageError.new("manifest entry '#{@key}' missing zone")
20
- @schema = raw["schema"]
21
- @owner = raw["owner"]
22
- @nested = raw["nested"] == true
23
- parse_compute!(raw)
24
- @template = raw["template"]
25
- @publish_to = Array(raw["publish_to"])
26
- @publish_each = raw["publish_each"]
27
- @events = raw["events"] || {}
28
- @inject_intro = raw["inject_intro"] == true
29
- @index_filename = raw["index_filename"]
30
- @format = resolve_format!(raw["format"])
31
-
32
- validate_events!
33
- parse_intake!(raw["intake"])
34
- validate_format_matrix!
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
@@ -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
- unless raw["version"] == PROTOCOL
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
- unless raw["version"] == PROTOCOL
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 { |e| Manifest::Entry.new(self, e) }
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
- case format
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