textus 0.5.0 → 0.8.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/CHANGELOG.md +83 -1
- data/README.md +29 -21
- data/SPEC.md +75 -142
- data/docs/architecture.md +42 -23
- data/lib/textus/builder/pipeline.rb +56 -0
- data/lib/textus/builder/renderer/json.rb +42 -0
- data/lib/textus/builder/renderer/markdown.rb +22 -0
- data/lib/textus/builder/renderer/text.rb +14 -0
- data/lib/textus/builder/renderer/yaml.rb +42 -0
- data/lib/textus/builder/renderer.rb +17 -0
- data/lib/textus/builder.rb +9 -114
- data/lib/textus/cli/group/hook.rb +11 -0
- data/lib/textus/cli/group/key.rb +12 -0
- data/lib/textus/cli/group/schema.rb +13 -0
- data/lib/textus/cli/verb/accept.rb +15 -0
- data/lib/textus/cli/verb/build.rb +13 -0
- data/lib/textus/cli/verb/delete.rb +16 -0
- data/lib/textus/cli/verb/deps.rb +12 -0
- data/lib/textus/cli/verb/doctor.rb +15 -0
- data/lib/textus/cli/verb/get.rb +12 -0
- data/lib/textus/cli/verb/hook_run.rb +48 -0
- data/lib/textus/cli/verb/hooks.rb +50 -0
- data/lib/textus/cli/verb/init.rb +14 -0
- data/lib/textus/cli/verb/intro.rb +11 -0
- data/lib/textus/cli/verb/list.rb +14 -0
- data/lib/textus/cli/verb/migrate_keys.rb +16 -0
- data/lib/textus/cli/verb/mv.rb +17 -0
- data/lib/textus/cli/verb/published.rb +11 -0
- data/lib/textus/cli/verb/put.rb +50 -0
- data/lib/textus/cli/verb/rdeps.rb +12 -0
- data/lib/textus/cli/verb/refresh.rb +15 -0
- data/lib/textus/cli/verb/schema.rb +12 -0
- data/lib/textus/cli/verb/schema_diff.rb +12 -0
- data/lib/textus/cli/verb/schema_init.rb +16 -0
- data/lib/textus/cli/verb/schema_migrate.rb +16 -0
- data/lib/textus/cli/verb/stale.rb +14 -0
- data/lib/textus/cli/verb/uid.rb +12 -0
- data/lib/textus/cli/verb/where.rb +12 -0
- data/lib/textus/cli.rb +23 -42
- data/lib/textus/doctor/check/audit_log.rb +50 -0
- data/lib/textus/doctor/check/hooks.rb +29 -0
- data/lib/textus/doctor/check/illegal_keys.rb +49 -0
- data/lib/textus/doctor/check/manifest_files.rb +38 -0
- data/lib/textus/doctor/check/schema_violations.rb +22 -0
- data/lib/textus/doctor/check/schemas.rb +26 -0
- data/lib/textus/doctor/check/sentinels.rb +57 -0
- data/lib/textus/doctor/check/templates.rb +26 -0
- data/lib/textus/doctor/check/unowned_schema_fields.rb +34 -0
- data/lib/textus/doctor/check.rb +30 -0
- data/lib/textus/doctor.rb +22 -288
- data/lib/textus/entry/base.rb +30 -0
- data/lib/textus/entry/json.rb +5 -1
- data/lib/textus/entry/markdown.rb +1 -1
- data/lib/textus/entry/text.rb +1 -1
- data/lib/textus/entry/yaml.rb +5 -1
- data/lib/textus/entry.rb +0 -5
- data/lib/textus/envelope.rb +30 -0
- data/lib/textus/hooks/builtin.rb +70 -0
- data/lib/textus/hooks/dispatcher.rb +49 -0
- data/lib/textus/hooks/loader.rb +26 -0
- data/lib/textus/hooks/registry.rb +73 -0
- data/lib/textus/init.rb +13 -10
- data/lib/textus/intro.rb +14 -16
- data/lib/textus/key/distance.rb +55 -0
- data/lib/textus/key/grammar.rb +33 -0
- data/lib/textus/key/path.rb +17 -0
- data/lib/textus/manifest/entry.rb +199 -0
- data/lib/textus/manifest.rb +10 -34
- data/lib/textus/migrate_keys.rb +1 -1
- data/lib/textus/projection.rb +5 -4
- data/lib/textus/proposal.rb +1 -1
- data/lib/textus/refresh.rb +11 -11
- data/lib/textus/schema/tools.rb +89 -0
- data/lib/textus/store/audit_log.rb +71 -0
- data/lib/textus/store/mover.rb +19 -16
- data/lib/textus/store/reader.rb +67 -0
- data/lib/textus/store/staleness.rb +10 -19
- data/lib/textus/store/validator.rb +11 -8
- data/lib/textus/store/view.rb +29 -0
- data/lib/textus/store/writer.rb +132 -0
- data/lib/textus/store.rb +25 -221
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +14 -67
- metadata +73 -40
- data/lib/textus/audit_log.rb +0 -67
- data/lib/textus/builtin_actions.rb +0 -68
- data/lib/textus/cli/accept.rb +0 -13
- data/lib/textus/cli/action.rb +0 -51
- data/lib/textus/cli/build.rb +0 -11
- data/lib/textus/cli/delete.rb +0 -14
- data/lib/textus/cli/deprecated_alias.rb +0 -31
- data/lib/textus/cli/deps.rb +0 -10
- data/lib/textus/cli/doctor.rb +0 -13
- data/lib/textus/cli/extension_group.rb +0 -9
- data/lib/textus/cli/extensions.rb +0 -49
- data/lib/textus/cli/get.rb +0 -10
- data/lib/textus/cli/init.rb +0 -12
- data/lib/textus/cli/intro.rb +0 -9
- data/lib/textus/cli/key_group.rb +0 -10
- data/lib/textus/cli/list.rb +0 -12
- data/lib/textus/cli/migrate.rb +0 -41
- data/lib/textus/cli/migrate_keys.rb +0 -19
- data/lib/textus/cli/mv.rb +0 -20
- data/lib/textus/cli/published.rb +0 -9
- data/lib/textus/cli/put.rb +0 -48
- data/lib/textus/cli/rdeps.rb +0 -10
- data/lib/textus/cli/refresh.rb +0 -13
- data/lib/textus/cli/schema.rb +0 -10
- data/lib/textus/cli/schema_diff.rb +0 -15
- data/lib/textus/cli/schema_group.rb +0 -33
- data/lib/textus/cli/schema_init.rb +0 -19
- data/lib/textus/cli/schema_migrate.rb +0 -19
- data/lib/textus/cli/stale.rb +0 -12
- data/lib/textus/cli/uid.rb +0 -15
- data/lib/textus/cli/where.rb +0 -10
- data/lib/textus/extension_registry.rb +0 -61
- data/lib/textus/extensions.rb +0 -33
- data/lib/textus/key_distance.rb +0 -53
- data/lib/textus/manifest_entry.rb +0 -185
- data/lib/textus/migrate_v2.rb +0 -27
- data/lib/textus/schema_tools.rb +0 -87
- data/lib/textus/store/events.rb +0 -31
- data/lib/textus/store_view.rb +0 -27
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
class Builder
|
|
5
|
+
class Renderer
|
|
6
|
+
class Json < Renderer
|
|
7
|
+
def call(mentry:, data:)
|
|
8
|
+
content = mentry.template ? parse_rendered_template!(mentry, data) : default_shape(mentry, data)
|
|
9
|
+
final = InjectMeta.call(content, mentry)
|
|
10
|
+
Entry.for_format("json").serialize(meta: {}, body: "", content: final)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def parse_rendered_template!(mentry, data)
|
|
16
|
+
rendered = Mustache.render(@template_loader.call(mentry.template), data)
|
|
17
|
+
begin
|
|
18
|
+
parsed = ::JSON.parse(rendered)
|
|
19
|
+
rescue ::JSON::ParserError => e
|
|
20
|
+
raise BadRender.new("entry '#{mentry.key}': template did not render valid json: #{e.message}", format: "json")
|
|
21
|
+
end
|
|
22
|
+
unless parsed.is_a?(Hash)
|
|
23
|
+
raise BadRender.new("entry '#{mentry.key}': template must render a top-level object/mapping",
|
|
24
|
+
format: "json")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
parsed
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def default_shape(mentry, data)
|
|
31
|
+
if mentry.projection && mentry.projection["reduce"] && data.is_a?(Hash) && !data.key?("entries")
|
|
32
|
+
data
|
|
33
|
+
elsif data.is_a?(Hash) && data["entries"].is_a?(Array)
|
|
34
|
+
{ "entries" => data["entries"] }
|
|
35
|
+
else
|
|
36
|
+
data.is_a?(Hash) ? data : { "entries" => Array(data) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
class Builder
|
|
5
|
+
class Renderer
|
|
6
|
+
class Markdown < Renderer
|
|
7
|
+
def call(mentry:, data:)
|
|
8
|
+
raise TemplateError.new("entry '#{mentry.key}': markdown build requires a template") unless mentry.template
|
|
9
|
+
|
|
10
|
+
body = Mustache.render(@template_loader.call(mentry.template), data)
|
|
11
|
+
frontmatter = {
|
|
12
|
+
"generated" => {
|
|
13
|
+
"at" => Time.now.utc.iso8601,
|
|
14
|
+
"from" => Array(mentry.projection&.fetch("select", nil)).compact,
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
Entry.for_format("markdown").serialize(meta: frontmatter, body: body)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class Builder
|
|
3
|
+
class Renderer
|
|
4
|
+
class Text < Renderer
|
|
5
|
+
def call(mentry:, data:)
|
|
6
|
+
raise TemplateError.new("entry '#{mentry.key}': text build requires a template") unless mentry.template
|
|
7
|
+
|
|
8
|
+
body = Mustache.render(@template_loader.call(mentry.template), data)
|
|
9
|
+
Entry.for_format("text").serialize(meta: {}, body: body)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
class Builder
|
|
5
|
+
class Renderer
|
|
6
|
+
class Yaml < Renderer
|
|
7
|
+
def call(mentry:, data:)
|
|
8
|
+
content = mentry.template ? parse_rendered_template!(mentry, data) : default_shape(mentry, data)
|
|
9
|
+
final = InjectMeta.call(content, mentry)
|
|
10
|
+
Entry.for_format("yaml").serialize(meta: {}, body: "", content: final)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def parse_rendered_template!(mentry, data)
|
|
16
|
+
rendered = Mustache.render(@template_loader.call(mentry.template), data)
|
|
17
|
+
begin
|
|
18
|
+
parsed = ::YAML.safe_load(rendered, permitted_classes: [Date, Time], aliases: false)
|
|
19
|
+
rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::AliasesNotEnabled => e
|
|
20
|
+
raise BadRender.new("entry '#{mentry.key}': template did not render valid yaml: #{e.message}", format: "yaml")
|
|
21
|
+
end
|
|
22
|
+
unless parsed.is_a?(Hash)
|
|
23
|
+
raise BadRender.new("entry '#{mentry.key}': template must render a top-level object/mapping",
|
|
24
|
+
format: "yaml")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
parsed
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def default_shape(mentry, data)
|
|
31
|
+
if mentry.projection && mentry.projection["reduce"] && data.is_a?(Hash) && !data.key?("entries")
|
|
32
|
+
data
|
|
33
|
+
elsif data.is_a?(Hash) && data["entries"].is_a?(Array)
|
|
34
|
+
{ "entries" => data["entries"] }
|
|
35
|
+
else
|
|
36
|
+
data.is_a?(Hash) ? data : { "entries" => Array(data) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class Builder
|
|
3
|
+
# Abstract base for output renderers. Each concrete renderer owns
|
|
4
|
+
# producing the bytes for one manifest format (markdown/json/yaml/text).
|
|
5
|
+
class Renderer
|
|
6
|
+
def initialize(template_loader:)
|
|
7
|
+
@template_loader = template_loader
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(mentry:, data:)
|
|
11
|
+
_ = mentry
|
|
12
|
+
_ = data
|
|
13
|
+
raise NotImplementedError.new("#{self.class.name}#call not implemented")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/textus/builder.rb
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
require "fileutils"
|
|
2
|
-
require "json"
|
|
3
|
-
require "time"
|
|
4
|
-
require "yaml"
|
|
5
2
|
|
|
6
3
|
module Textus
|
|
7
4
|
class Builder
|
|
@@ -59,122 +56,20 @@ module Textus
|
|
|
59
56
|
end
|
|
60
57
|
|
|
61
58
|
def materialize(mentry)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
bytes =
|
|
70
|
-
case mentry.format
|
|
71
|
-
when "markdown" then build_markdown(mentry, data)
|
|
72
|
-
when "text" then build_text(mentry, data)
|
|
73
|
-
when "json" then build_structured(mentry, data, "json")
|
|
74
|
-
when "yaml" then build_structured(mentry, data, "yaml")
|
|
75
|
-
else raise UsageError.new("builder: unsupported format #{mentry.format.inspect} for '#{mentry.key}'")
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
target_path = File.join(@root, "zones", mentry.path)
|
|
79
|
-
FileUtils.mkdir_p(File.dirname(target_path))
|
|
80
|
-
File.binwrite(target_path, bytes)
|
|
81
|
-
|
|
59
|
+
target_path = Pipeline.run(
|
|
60
|
+
store: @store,
|
|
61
|
+
mentry: mentry,
|
|
62
|
+
template_loader: ->(name) { read_template(name) },
|
|
63
|
+
)
|
|
82
64
|
publish_and_fire(mentry, target_path)
|
|
83
65
|
{ "key" => mentry.key, "path" => target_path, "published_to" => mentry.publish_to }
|
|
84
66
|
end
|
|
85
67
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
# for backward compat with consumers reading frontmatter["generated"]["at"].
|
|
90
|
-
def build_markdown(mentry, data)
|
|
91
|
-
data = data.merge("intro" => Intro.run(@store)) if mentry.inject_intro
|
|
92
|
-
body = render_template!(mentry, data)
|
|
93
|
-
frontmatter = {
|
|
94
|
-
"generated" => {
|
|
95
|
-
"at" => Time.now.utc.iso8601,
|
|
96
|
-
"from" => Array(mentry.projection&.fetch("select", nil)).compact,
|
|
97
|
-
},
|
|
98
|
-
}
|
|
99
|
-
Entry.for_format("markdown").serialize(meta: frontmatter, body: body)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Text: projection -> template -> text.serialize(body). No frontmatter, no _meta.
|
|
103
|
-
def build_text(mentry, data)
|
|
104
|
-
data = data.merge("intro" => Intro.run(@store)) if mentry.inject_intro
|
|
105
|
-
body = render_template!(mentry, data)
|
|
106
|
-
Entry.for_format("text").serialize(meta: {}, body: body)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# JSON / YAML pipeline. Templateless = default; template = escape hatch.
|
|
110
|
-
def build_structured(mentry, data, format)
|
|
111
|
-
strategy = Entry.for_format(format)
|
|
112
|
-
|
|
113
|
-
content =
|
|
114
|
-
if mentry.template
|
|
115
|
-
parse_rendered_template!(mentry, data, format)
|
|
116
|
-
else
|
|
117
|
-
# Default rule: if the reducer returned a Hash (it replaced `rows`), use it as-is.
|
|
118
|
-
# Otherwise wrap the entries list as { "entries" => [...] } so the top level is a Hash
|
|
119
|
-
# (required to carry _meta).
|
|
120
|
-
if mentry.projection && mentry.projection["reducer"] && data.is_a?(Hash) && !data.key?("entries")
|
|
121
|
-
data
|
|
122
|
-
elsif data.is_a?(Hash) && data["entries"].is_a?(Array)
|
|
123
|
-
{ "entries" => data["entries"] }
|
|
124
|
-
else
|
|
125
|
-
data.is_a?(Hash) ? data : { "entries" => Array(data) }
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
final = inject_meta(content, mentry)
|
|
130
|
-
strategy.serialize(meta: {}, body: "", content: final)
|
|
131
|
-
end
|
|
68
|
+
def read_template(name)
|
|
69
|
+
tpl_path = File.join(@root, "templates", name)
|
|
70
|
+
raise TemplateError.new("template not found: #{tpl_path}", template_name: name) unless File.exist?(tpl_path)
|
|
132
71
|
|
|
133
|
-
|
|
134
|
-
raise TemplateError.new("entry '#{mentry.key}': #{mentry.format} build requires a template") unless mentry.template
|
|
135
|
-
|
|
136
|
-
tpl_path = File.join(@root, "templates", mentry.template)
|
|
137
|
-
raise TemplateError.new("template not found: #{tpl_path}", template_name: mentry.template) unless File.exist?(tpl_path)
|
|
138
|
-
|
|
139
|
-
Mustache.render(File.read(tpl_path), data)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def parse_rendered_template!(mentry, data, format)
|
|
143
|
-
tpl_path = File.join(@root, "templates", mentry.template)
|
|
144
|
-
raise TemplateError.new("template not found: #{tpl_path}", template_name: mentry.template) unless File.exist?(tpl_path)
|
|
145
|
-
|
|
146
|
-
rendered = Mustache.render(File.read(tpl_path), data)
|
|
147
|
-
begin
|
|
148
|
-
parsed =
|
|
149
|
-
case format
|
|
150
|
-
when "json" then ::JSON.parse(rendered)
|
|
151
|
-
when "yaml" then ::YAML.safe_load(rendered, permitted_classes: [Date, Time], aliases: false)
|
|
152
|
-
end
|
|
153
|
-
rescue ::JSON::ParserError, Psych::SyntaxError, Psych::DisallowedClass, Psych::AliasesNotEnabled => e
|
|
154
|
-
raise BadRender.new("entry '#{mentry.key}': template did not render valid #{format}: #{e.message}", format: format)
|
|
155
|
-
end
|
|
156
|
-
unless parsed.is_a?(Hash)
|
|
157
|
-
raise BadRender.new("entry '#{mentry.key}': template must render a top-level object/mapping",
|
|
158
|
-
format: format)
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
parsed
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# Builds the _meta block per §6 ordering and inserts it as the first top-level key.
|
|
165
|
-
def inject_meta(content_hash, mentry)
|
|
166
|
-
meta = {}
|
|
167
|
-
meta["generated_at"] = Time.now.utc.iso8601
|
|
168
|
-
from = Array(mentry.projection&.fetch("select", nil)).compact
|
|
169
|
-
meta["from"] = from unless from.empty?
|
|
170
|
-
meta["template"] = mentry.template if mentry.template
|
|
171
|
-
reducer = mentry.projection&.dig("reducer")
|
|
172
|
-
meta["reducer"] = reducer if reducer
|
|
173
|
-
|
|
174
|
-
# Rebuild so _meta appears first; user content follows.
|
|
175
|
-
out = { "_meta" => meta }
|
|
176
|
-
content_hash.each { |k, v| out[k] = v unless k == "_meta" }
|
|
177
|
-
out
|
|
72
|
+
File.read(tpl_path)
|
|
178
73
|
end
|
|
179
74
|
|
|
180
75
|
def publish_and_fire(mentry, target_path)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Group
|
|
4
|
+
class Schema < Group
|
|
5
|
+
self.cli_name = "schema"
|
|
6
|
+
subcommands["show"] = Verb::Schema
|
|
7
|
+
subcommands["init"] = Verb::SchemaInit
|
|
8
|
+
subcommands["diff"] = Verb::SchemaDiff
|
|
9
|
+
subcommands["migrate"] = Verb::SchemaMigrate
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class Accept < Verb
|
|
5
|
+
option :as_flag, "--as=ROLE"
|
|
6
|
+
|
|
7
|
+
def call(store)
|
|
8
|
+
key = positional.shift or raise UsageError.new("accept requires a key")
|
|
9
|
+
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
10
|
+
emit(store.accept(key, as: role))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class Delete < Verb
|
|
5
|
+
option :as_flag, "--as=ROLE"
|
|
6
|
+
option :if_etag, "--if-etag=E"
|
|
7
|
+
|
|
8
|
+
def call(store)
|
|
9
|
+
key = positional.shift or raise UsageError.new("delete requires a key")
|
|
10
|
+
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
11
|
+
emit(store.delete(key, if_etag: if_etag, as: role))
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class Doctor < Verb
|
|
5
|
+
option :checks, "--check=NAME"
|
|
6
|
+
|
|
7
|
+
def call(store)
|
|
8
|
+
check_list = checks&.split(",")&.map(&:strip)
|
|
9
|
+
res = Textus::Doctor.run(store, checks: check_list)
|
|
10
|
+
emit(res, exit_code: res["ok"] ? 0 : 1)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class HookRun < Verb
|
|
5
|
+
def parse(argv)
|
|
6
|
+
@raw_argv = argv
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(store)
|
|
10
|
+
name = @raw_argv.shift
|
|
11
|
+
raise UsageError.new("hook run requires a name") if name.nil?
|
|
12
|
+
|
|
13
|
+
as_flag = nil
|
|
14
|
+
args = {}
|
|
15
|
+
@raw_argv.each do |tok|
|
|
16
|
+
case tok
|
|
17
|
+
when /\A--as=(.+)\z/ then as_flag = ::Regexp.last_match(1)
|
|
18
|
+
when /\A--format=/ then next
|
|
19
|
+
when /\A--([\w-]+)=(.*)\z/ then args[::Regexp.last_match(1)] = ::Regexp.last_match(2)
|
|
20
|
+
else
|
|
21
|
+
raise UsageError.new("unknown arg to 'hook run #{name}': #{tok}")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
26
|
+
callable = store.registry.rpc_callable(:fetch, name)
|
|
27
|
+
view = Store::View.new(store, writable: true, as: role)
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
Timeout.timeout(Textus::Refresh::FETCH_TIMEOUT_SECONDS) do
|
|
31
|
+
callable.call(config: {}, store: view, args: args)
|
|
32
|
+
end
|
|
33
|
+
rescue Timeout::Error
|
|
34
|
+
raise UsageError.new(
|
|
35
|
+
"hook run '#{name}' exceeded #{Textus::Refresh::FETCH_TIMEOUT_SECONDS}s timeout",
|
|
36
|
+
)
|
|
37
|
+
rescue Textus::Error
|
|
38
|
+
raise
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
raise UsageError.new("hook run '#{name}' raised: #{e.class}: #{e.message}")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
emit({ "action" => name, "ok" => true })
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class Hooks < Verb
|
|
5
|
+
option :event_filter, "--event=E"
|
|
6
|
+
|
|
7
|
+
def call(store) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
8
|
+
subcommand = positional.first
|
|
9
|
+
if subcommand
|
|
10
|
+
raise UsageError.new("hook requires 'list'") unless subcommand == "list"
|
|
11
|
+
|
|
12
|
+
positional.shift
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
rows = []
|
|
16
|
+
Textus::Hooks::Registry::EVENTS.each do |event, spec|
|
|
17
|
+
mode = spec[:mode].to_s
|
|
18
|
+
case spec[:mode]
|
|
19
|
+
when :rpc
|
|
20
|
+
store.registry.rpc_names(event).each do |name|
|
|
21
|
+
rows << { "event" => event.to_s, "mode" => mode, "name" => name.to_s }
|
|
22
|
+
end
|
|
23
|
+
when :pubsub
|
|
24
|
+
store.registry.pubsub_handlers(event).each do |h|
|
|
25
|
+
row = { "event" => event.to_s, "mode" => mode, "name" => h[:name].to_s }
|
|
26
|
+
row["keys"] = Array(h[:keys]) if h[:keys]
|
|
27
|
+
rows << row
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
store.manifest.entries.each do |e|
|
|
32
|
+
e.events.each do |evt, defs|
|
|
33
|
+
Array(defs).each do |defn|
|
|
34
|
+
next unless defn["exec"]
|
|
35
|
+
|
|
36
|
+
rows << {
|
|
37
|
+
"event" => evt.to_s, "mode" => "manifest", "exec" => defn["exec"],
|
|
38
|
+
"key" => e.key, "as" => defn["as"] || "script"
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
rows.select! { |r| r["event"] == event_filter } if event_filter
|
|
44
|
+
|
|
45
|
+
emit({ "hooks" => rows })
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class MigrateKeys < Verb
|
|
5
|
+
option :write, "--write"
|
|
6
|
+
option :dry_run, "--dry-run"
|
|
7
|
+
|
|
8
|
+
def call(store)
|
|
9
|
+
effective_write = write && !dry_run
|
|
10
|
+
res = Textus::MigrateKeys.run(store, write: effective_write || false)
|
|
11
|
+
emit(res, exit_code: res["ok"] ? 0 : 1)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class Mv < Verb
|
|
5
|
+
option :as_flag, "--as=ROLE"
|
|
6
|
+
option :dry_run, "--dry-run"
|
|
7
|
+
|
|
8
|
+
def call(store)
|
|
9
|
+
old_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
|
|
10
|
+
new_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
|
|
11
|
+
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
12
|
+
emit(store.mv(old_key, new_key, as: role, dry_run: dry_run || false))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class Put < Verb
|
|
5
|
+
option :as_flag, "--as=ROLE"
|
|
6
|
+
option :use_stdin, "--stdin"
|
|
7
|
+
option :fetch_name, "--fetch=NAME"
|
|
8
|
+
|
|
9
|
+
def call(store) # rubocop:disable Metrics/AbcSize
|
|
10
|
+
key = positional.shift or raise UsageError.new("put requires a key")
|
|
11
|
+
raise UsageError.new("put requires --stdin in v1") unless use_stdin
|
|
12
|
+
|
|
13
|
+
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
14
|
+
|
|
15
|
+
raw = @stdin.read
|
|
16
|
+
payload =
|
|
17
|
+
if fetch_name
|
|
18
|
+
callable = store.registry.rpc_callable(:fetch, fetch_name)
|
|
19
|
+
result =
|
|
20
|
+
begin
|
|
21
|
+
Timeout.timeout(Textus::Refresh::FETCH_TIMEOUT_SECONDS) do
|
|
22
|
+
callable.call(config: { "bytes" => raw }, store: Textus::Store::View.new(store), args: {})
|
|
23
|
+
end
|
|
24
|
+
rescue Timeout::Error
|
|
25
|
+
raise UsageError.new(
|
|
26
|
+
"fetch '#{fetch_name}' exceeded #{Textus::Refresh::FETCH_TIMEOUT_SECONDS}s timeout",
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
basename = key.split(".").last
|
|
30
|
+
{
|
|
31
|
+
"_meta" => {
|
|
32
|
+
"name" => basename,
|
|
33
|
+
"last_refreshed_at" => Time.now.utc.iso8601,
|
|
34
|
+
"fetched_with" => fetch_name,
|
|
35
|
+
}.merge(result[:_meta] || result["_meta"] || result[:frontmatter] || result["frontmatter"] || {}),
|
|
36
|
+
"body" => result[:body] || result["body"] || "",
|
|
37
|
+
}
|
|
38
|
+
else
|
|
39
|
+
JSON.parse(raw)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
meta = payload["_meta"] || payload["frontmatter"] || {}
|
|
43
|
+
body = payload["body"] || ""
|
|
44
|
+
if_etag = payload["if_etag"]
|
|
45
|
+
emit(store.put(key, meta: meta, body: body, if_etag: if_etag, as: role))
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|