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
data/lib/textus/extensions.rb
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
THREAD_REGISTRY_KEY = :__textus_active_registry__
|
|
3
|
-
private_constant :THREAD_REGISTRY_KEY
|
|
4
|
-
|
|
5
|
-
def self.with_registry(registry)
|
|
6
|
-
prev = Thread.current[THREAD_REGISTRY_KEY]
|
|
7
|
-
Thread.current[THREAD_REGISTRY_KEY] = registry
|
|
8
|
-
yield
|
|
9
|
-
ensure
|
|
10
|
-
Thread.current[THREAD_REGISTRY_KEY] = prev
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def self.current_registry
|
|
14
|
-
Thread.current[THREAD_REGISTRY_KEY] or
|
|
15
|
-
raise UsageError.new("no active registry; extension code must be loaded by a Store")
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def self.action(name, &)
|
|
19
|
-
current_registry.register_action(name, &)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def self.reducer(name, &)
|
|
23
|
-
current_registry.register_reducer(name, &)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def self.hook(event, name, &)
|
|
27
|
-
current_registry.register_hook(event, name, &)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def self.doctor_check(name, &)
|
|
31
|
-
current_registry.register_doctor_check(name, &)
|
|
32
|
-
end
|
|
33
|
-
end
|
data/lib/textus/key_distance.rb
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
# Small utilities for ranking key suggestions. Bounded inputs only —
|
|
3
|
-
# Levenshtein is O(n*m) so we refuse to compute on long strings.
|
|
4
|
-
module KeyDistance
|
|
5
|
-
MAX_LEN = 200
|
|
6
|
-
|
|
7
|
-
# Length of the shared dot-separated prefix between two dotted keys.
|
|
8
|
-
def self.shared_prefix_segments(left, right)
|
|
9
|
-
asegs = left.split(".")
|
|
10
|
-
bsegs = right.split(".")
|
|
11
|
-
n = [asegs.length, bsegs.length].min
|
|
12
|
-
i = 0
|
|
13
|
-
i += 1 while i < n && asegs[i] == bsegs[i]
|
|
14
|
-
i
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# Classic iterative Levenshtein with two rows. Bounded to MAX_LEN.
|
|
18
|
-
def self.levenshtein(left, right)
|
|
19
|
-
return nil if left.length > MAX_LEN || right.length > MAX_LEN
|
|
20
|
-
return right.length if left.empty?
|
|
21
|
-
return left.length if right.empty?
|
|
22
|
-
|
|
23
|
-
prev = (0..right.length).to_a
|
|
24
|
-
curr = Array.new(right.length + 1, 0)
|
|
25
|
-
(1..left.length).each do |i|
|
|
26
|
-
curr[0] = i
|
|
27
|
-
(1..right.length).each do |j|
|
|
28
|
-
cost = left[i - 1] == right[j - 1] ? 0 : 1
|
|
29
|
-
curr[j] = [
|
|
30
|
-
curr[j - 1] + 1, # insertion
|
|
31
|
-
prev[j] + 1, # deletion
|
|
32
|
-
prev[j - 1] + cost, # substitution
|
|
33
|
-
].min
|
|
34
|
-
end
|
|
35
|
-
prev, curr = curr, prev
|
|
36
|
-
end
|
|
37
|
-
prev[right.length]
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Rank candidate keys against requested. Returns up to `limit` keys.
|
|
41
|
-
# Sort: longer shared prefix first; then smaller Levenshtein distance.
|
|
42
|
-
def self.suggest(requested, candidates, limit: 5)
|
|
43
|
-
return [] if requested.nil? || requested.empty?
|
|
44
|
-
|
|
45
|
-
scored = candidates.first(200).map do |k|
|
|
46
|
-
prefix = shared_prefix_segments(requested, k)
|
|
47
|
-
dist = levenshtein(requested, k) || Float::INFINITY
|
|
48
|
-
[k, prefix, dist]
|
|
49
|
-
end
|
|
50
|
-
scored.sort_by { |(_, prefix, dist)| [-prefix, dist] }.first(limit).map(&:first)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class ManifestEntry
|
|
3
|
-
PUBLISH_EACH_VARS = %w[leaf basename key ext].freeze
|
|
4
|
-
PUBLISH_EACH_VAR_RE = /\{([a-z]+)\}/
|
|
5
|
-
|
|
6
|
-
attr_reader :key, :path, :zone, :schema, :owner, :nested, :generator, :raw, :format,
|
|
7
|
-
:projection, :template, :publish_to, :publish_each, :action, :action_config, :ttl, :events,
|
|
8
|
-
:inject_intro
|
|
9
|
-
|
|
10
|
-
def initialize(manifest, raw)
|
|
11
|
-
@manifest = manifest
|
|
12
|
-
@raw = raw
|
|
13
|
-
@key = raw["key"] or raise UsageError.new("manifest entry missing key")
|
|
14
|
-
@path = raw["path"] or raise UsageError.new("manifest entry '#{@key}' missing path")
|
|
15
|
-
@zone = raw["zone"] or raise UsageError.new("manifest entry '#{@key}' missing zone")
|
|
16
|
-
@schema = raw["schema"]
|
|
17
|
-
@owner = raw["owner"]
|
|
18
|
-
@nested = raw["nested"] == true
|
|
19
|
-
@generator = raw["generator"]
|
|
20
|
-
@projection = raw["projection"]
|
|
21
|
-
@template = raw["template"]
|
|
22
|
-
@publish_to = Array(raw["publish_to"])
|
|
23
|
-
@publish_each = raw["publish_each"]
|
|
24
|
-
@events = raw["events"] || {}
|
|
25
|
-
@inject_intro = raw["inject_intro"] == true
|
|
26
|
-
@format = resolve_format!(raw["format"])
|
|
27
|
-
|
|
28
|
-
validate_events!
|
|
29
|
-
parse_source!(raw["source"])
|
|
30
|
-
validate_format_matrix!
|
|
31
|
-
validate_publish_each!
|
|
32
|
-
validate_inject_intro!
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Resolves the per-leaf target path (relative to repo root) for a full
|
|
36
|
-
# dotted key under this entry's prefix. Returns nil if this entry has no
|
|
37
|
-
# publish_each template.
|
|
38
|
-
def publish_target_for(full_key)
|
|
39
|
-
return nil if @publish_each.nil?
|
|
40
|
-
|
|
41
|
-
entry_segs = @key.split(".")
|
|
42
|
-
key_segs = full_key.split(".")
|
|
43
|
-
raise UsageError.new("key '#{full_key}' is not under entry '#{@key}'") unless key_segs[0, entry_segs.length] == entry_segs
|
|
44
|
-
|
|
45
|
-
remaining = key_segs[entry_segs.length..] || []
|
|
46
|
-
leaf = remaining.join("/")
|
|
47
|
-
basename = remaining.last || ""
|
|
48
|
-
ext = Entry.for_format(@format).extensions.first.to_s.sub(/^\./, "")
|
|
49
|
-
|
|
50
|
-
vars = { "leaf" => leaf, "basename" => basename, "key" => full_key, "ext" => ext }
|
|
51
|
-
@publish_each.gsub(PUBLISH_EACH_VAR_RE) { vars.fetch(::Regexp.last_match(1)) }
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def derived?
|
|
55
|
-
writers = @manifest.zone_writers(@zone)
|
|
56
|
-
writers.include?("build")
|
|
57
|
-
rescue UsageError => e
|
|
58
|
-
raise UsageError.new("entry '#{@key}': #{e.message}")
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
def validate_inject_intro!
|
|
64
|
-
return unless @inject_intro
|
|
65
|
-
|
|
66
|
-
unless derived?
|
|
67
|
-
raise UsageError.new(
|
|
68
|
-
"entry '#{@key}': inject_intro: is only valid on derived entries",
|
|
69
|
-
)
|
|
70
|
-
end
|
|
71
|
-
return unless @template.nil?
|
|
72
|
-
|
|
73
|
-
raise UsageError.new(
|
|
74
|
-
"entry '#{@key}': inject_intro: requires a template:",
|
|
75
|
-
)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def validate_publish_each!
|
|
79
|
-
return if @publish_each.nil?
|
|
80
|
-
|
|
81
|
-
raise UsageError.new("entry '#{@key}': publish_each requires nested: true") unless @nested
|
|
82
|
-
raise UsageError.new("entry '#{@key}': publish_to and publish_each are mutually exclusive") unless @publish_to.empty?
|
|
83
|
-
raise UsageError.new("entry '#{@key}': publish_each must be a string") unless @publish_each.is_a?(String)
|
|
84
|
-
|
|
85
|
-
used_vars = @publish_each.scan(PUBLISH_EACH_VAR_RE).flatten
|
|
86
|
-
unknown = used_vars - PUBLISH_EACH_VARS
|
|
87
|
-
unless unknown.empty?
|
|
88
|
-
raise UsageError.new(
|
|
89
|
-
"entry '#{@key}': publish_each uses unknown template variable(s) " \
|
|
90
|
-
"#{unknown.map { |v| "{#{v}}" }.join(", ")}. Known: #{PUBLISH_EACH_VARS.map { |v| "{#{v}}" }.join(", ")}.",
|
|
91
|
-
)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
required = %w[leaf basename key]
|
|
95
|
-
return if used_vars.any? { |v| required.include?(v) }
|
|
96
|
-
|
|
97
|
-
raise UsageError.new(
|
|
98
|
-
"entry '#{@key}': publish_each must reference at least one of {leaf}, {basename}, or {key} " \
|
|
99
|
-
"(else every leaf would clobber the same target).",
|
|
100
|
-
)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def resolve_format!(declared)
|
|
104
|
-
ext = File.extname(@path)
|
|
105
|
-
inferred = Manifest::EXT_TO_FORMAT[ext]
|
|
106
|
-
|
|
107
|
-
if declared.nil?
|
|
108
|
-
return inferred if inferred
|
|
109
|
-
# No extension: nested defaults to markdown, leaf with no ext also markdown.
|
|
110
|
-
return "markdown" if ext == "" && @nested
|
|
111
|
-
return "markdown" if ext == ""
|
|
112
|
-
else
|
|
113
|
-
raise UsageError.new("entry '#{@key}': unknown format #{declared.inspect}") unless Manifest::EXT_TO_FORMAT.values.include?(declared)
|
|
114
|
-
# If the path has an extension, the declared format must match.
|
|
115
|
-
if ext != "" && inferred && inferred != declared
|
|
116
|
-
raise UsageError.new(
|
|
117
|
-
"entry '#{@key}': path extension #{ext.inspect} does not match declared format #{declared.inspect}",
|
|
118
|
-
)
|
|
119
|
-
end
|
|
120
|
-
return declared
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
"markdown"
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
127
|
-
def validate_format_matrix!
|
|
128
|
-
ext = File.extname(@path)
|
|
129
|
-
|
|
130
|
-
case @format
|
|
131
|
-
when "markdown"
|
|
132
|
-
# .md, or no extension (will be appended). Anything else is a mismatch caught above.
|
|
133
|
-
raise UsageError.new("entry '#{@key}': markdown format requires '.md' path (got #{ext.inspect})") if ext != "" && ext != ".md"
|
|
134
|
-
when "json"
|
|
135
|
-
if @nested
|
|
136
|
-
# nested json: path is a directory; ext must be empty.
|
|
137
|
-
raise UsageError.new("entry '#{@key}': nested json path must not have an extension") if ext != ""
|
|
138
|
-
elsif ext != ".json"
|
|
139
|
-
raise UsageError.new("entry '#{@key}': json format requires '.json' path (got #{ext.inspect})")
|
|
140
|
-
end
|
|
141
|
-
when "yaml"
|
|
142
|
-
if @nested
|
|
143
|
-
raise UsageError.new("entry '#{@key}': nested yaml path must not have an extension") if ext != ""
|
|
144
|
-
elsif ext != ".yaml" && ext != ".yml"
|
|
145
|
-
raise UsageError.new("entry '#{@key}': yaml format requires '.yaml' or '.yml' path (got #{ext.inspect})")
|
|
146
|
-
end
|
|
147
|
-
when "text"
|
|
148
|
-
if @nested
|
|
149
|
-
raise UsageError.new("entry '#{@key}': nested text path must not have an extension") if ext != ""
|
|
150
|
-
elsif ext != ".txt" && ext != ""
|
|
151
|
-
raise UsageError.new("entry '#{@key}': text format requires '.txt' or no extension (got #{ext.inspect})")
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# Schema rules.
|
|
156
|
-
raise UsageError.new("entry '#{@key}': text format must not declare a schema") if @format == "text" && !@schema.nil?
|
|
157
|
-
|
|
158
|
-
# Template-required-for-derived rules. Skipped for entries materialized by an
|
|
159
|
-
# external generator: command (those produce the bytes themselves).
|
|
160
|
-
if derived? && @template.nil? && @generator.nil? &&
|
|
161
|
-
(@format == "markdown" || @format == "text") && !@nested
|
|
162
|
-
raise UsageError.new("entry '#{@key}': derived #{@format} entries require a template")
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
166
|
-
|
|
167
|
-
def parse_source!(src)
|
|
168
|
-
src ||= {}
|
|
169
|
-
@action = src["action"]
|
|
170
|
-
@action_config = src["config"] || {}
|
|
171
|
-
@ttl = src["ttl"]
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def validate_events!
|
|
175
|
-
@events.each_key do |evt|
|
|
176
|
-
next if ExtensionRegistry::EVENTS.include?(evt.to_sym)
|
|
177
|
-
|
|
178
|
-
raise UsageError.new(
|
|
179
|
-
"entry '#{@key}': unknown event '#{evt}' in events: block. " \
|
|
180
|
-
"Known events: #{ExtensionRegistry::EVENTS.join(", ")}.",
|
|
181
|
-
)
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
end
|
data/lib/textus/migrate_v2.rb
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
require "yaml"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
# One-shot migration: rewrites the manifest version string from textus/1
|
|
5
|
-
# to textus/2. On-disk entry file shapes are unchanged — the only change
|
|
6
|
-
# needed is the version: line in manifest.yaml.
|
|
7
|
-
module MigrateV2
|
|
8
|
-
def self.run(root)
|
|
9
|
-
manifest_path = File.join(root, "manifest.yaml")
|
|
10
|
-
raise IoError.new("manifest not found: #{manifest_path}") unless File.exist?(manifest_path)
|
|
11
|
-
|
|
12
|
-
content = File.read(manifest_path)
|
|
13
|
-
raw = YAML.safe_load(content, aliases: false)
|
|
14
|
-
|
|
15
|
-
case raw["version"]
|
|
16
|
-
when PROTOCOL
|
|
17
|
-
{ "protocol" => PROTOCOL, "ok" => true, "no_op" => true, "message" => "already #{PROTOCOL}" }
|
|
18
|
-
when "textus/1"
|
|
19
|
-
new_content = content.sub(%r{^version:\s*textus/1\s*$}, "version: #{PROTOCOL}")
|
|
20
|
-
File.write(manifest_path, new_content)
|
|
21
|
-
{ "protocol" => PROTOCOL, "ok" => true, "from" => "textus/1", "to" => PROTOCOL }
|
|
22
|
-
else
|
|
23
|
-
raise UsageError.new("cannot migrate from #{raw["version"].inspect}")
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
data/lib/textus/schema_tools.rb
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
require "yaml"
|
|
2
|
-
require "fileutils"
|
|
3
|
-
|
|
4
|
-
module Textus
|
|
5
|
-
module SchemaTools
|
|
6
|
-
# textus schema-init NAME --from=KEY → infer YAML schema from an entry's frontmatter
|
|
7
|
-
def self.init(store, name:, from:)
|
|
8
|
-
env = store.get(from)
|
|
9
|
-
meta = env["_meta"]
|
|
10
|
-
schema = {
|
|
11
|
-
"name" => name,
|
|
12
|
-
"required" => meta.keys,
|
|
13
|
-
"optional" => [],
|
|
14
|
-
"fields" => meta.each_with_object({}) { |(k, v), h| h[k] = { "type" => infer_type(v) } },
|
|
15
|
-
}
|
|
16
|
-
FileUtils.mkdir_p(File.join(store.root, "schemas"))
|
|
17
|
-
target = File.join(store.root, "schemas", "#{name}.yaml")
|
|
18
|
-
File.write(target, YAML.dump(schema))
|
|
19
|
-
{ "protocol" => PROTOCOL, "schema_name" => name, "path" => target }
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# textus schema-diff NAME → list keys whose frontmatter violates the schema
|
|
23
|
-
def self.diff(store, name:)
|
|
24
|
-
schema = load_schema(store, name)
|
|
25
|
-
drift = []
|
|
26
|
-
store.manifest.enumerate.each do |row|
|
|
27
|
-
env = store.get(row[:key])
|
|
28
|
-
begin
|
|
29
|
-
schema.validate!(env["_meta"])
|
|
30
|
-
rescue SchemaViolation => e
|
|
31
|
-
drift << { "key" => row[:key], "details" => e.details }
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
{ "protocol" => PROTOCOL, "schema_name" => name, "drift" => drift }
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# textus schema-migrate NAME --rename=OLD:NEW → rewrites frontmatter across affected entries
|
|
38
|
-
# If --rename is omitted, falls back to schema.evolution.migrate_from.
|
|
39
|
-
def self.migrate(store, name:, rename: nil)
|
|
40
|
-
renames =
|
|
41
|
-
if rename
|
|
42
|
-
old_field, new_field = rename.split(":", 2)
|
|
43
|
-
raise UsageError.new("--rename=OLD:NEW") unless old_field && new_field && !new_field.empty?
|
|
44
|
-
|
|
45
|
-
{ old_field => new_field }
|
|
46
|
-
else
|
|
47
|
-
load_schema(store, name).evolution["migrate_from"] || {}
|
|
48
|
-
end
|
|
49
|
-
raise UsageError.new("schema-migrate needs --rename=OLD:NEW or schema.evolution.migrate_from") if renames.empty?
|
|
50
|
-
|
|
51
|
-
touched = []
|
|
52
|
-
store.manifest.enumerate.each do |row|
|
|
53
|
-
env = store.get(row[:key])
|
|
54
|
-
meta = env["_meta"]
|
|
55
|
-
changed = false
|
|
56
|
-
renames.each do |old, new|
|
|
57
|
-
if meta.key?(old)
|
|
58
|
-
meta[new] = meta.delete(old)
|
|
59
|
-
changed = true
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
next unless changed
|
|
63
|
-
|
|
64
|
-
store.put(row[:key], meta: meta, body: env["body"], as: "human")
|
|
65
|
-
touched << row[:key]
|
|
66
|
-
end
|
|
67
|
-
{ "protocol" => PROTOCOL, "migrated" => touched, "renames" => renames }
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def self.infer_type(value)
|
|
71
|
-
case value
|
|
72
|
-
when String then "string"
|
|
73
|
-
when Numeric then "number"
|
|
74
|
-
when true, false then "boolean"
|
|
75
|
-
when Array then "array"
|
|
76
|
-
when Hash then "object"
|
|
77
|
-
else "string"
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def self.load_schema(store, name)
|
|
82
|
-
store.schema_for(name)
|
|
83
|
-
rescue IoError
|
|
84
|
-
raise UsageError.new("schema not found: #{name}")
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
data/lib/textus/store/events.rb
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
require "timeout"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
class Store
|
|
5
|
-
class Events
|
|
6
|
-
HOOK_TIMEOUT_SECONDS = 2
|
|
7
|
-
|
|
8
|
-
def initialize(store)
|
|
9
|
-
@store = store
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def call(event, **kwargs)
|
|
13
|
-
view = StoreView.new(@store)
|
|
14
|
-
@store.registry.hooks(event).each do |entry|
|
|
15
|
-
name = entry[:name]
|
|
16
|
-
Timeout.timeout(HOOK_TIMEOUT_SECONDS) { entry[:callable].call(store: view, **kwargs) }
|
|
17
|
-
rescue StandardError => e
|
|
18
|
-
extras = { "event" => event.to_s, "hook" => name.to_s, "error" => "#{e.class}: #{e.message}" }
|
|
19
|
-
extras["target_key"] = kwargs[:target_key] if kwargs.key?(:target_key)
|
|
20
|
-
extras["pending_key"] = kwargs[:pending_key] if kwargs.key?(:pending_key)
|
|
21
|
-
@store.audit_log.append(
|
|
22
|
-
role: "script", verb: "event_error",
|
|
23
|
-
key: kwargs[:key] || kwargs[:target_key] || kwargs[:pending_key] || "-",
|
|
24
|
-
etag_before: nil, etag_after: nil,
|
|
25
|
-
extras: extras
|
|
26
|
-
)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
data/lib/textus/store_view.rb
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class StoreView
|
|
3
|
-
READ_METHODS = %i[get list where schema_envelope deps rdeps published stale validate_all].freeze
|
|
4
|
-
WRITE_METHODS = %i[put delete accept].freeze
|
|
5
|
-
|
|
6
|
-
def initialize(store, writable: false, as: nil)
|
|
7
|
-
raise UsageError.new("writable StoreView requires an as: role") if writable && (as.nil? || as.to_s.empty?)
|
|
8
|
-
|
|
9
|
-
@store = store
|
|
10
|
-
@writable = writable
|
|
11
|
-
@as = as
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
READ_METHODS.each do |m|
|
|
15
|
-
define_method(m) { |*args, **kw| @store.public_send(m, *args, **kw) }
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
WRITE_METHODS.each do |m|
|
|
19
|
-
define_method(m) do |*args, **kw|
|
|
20
|
-
raise UsageError.new("StoreView is read-only") unless @writable
|
|
21
|
-
|
|
22
|
-
kw[:as] = @as unless kw.key?(:as)
|
|
23
|
-
@store.public_send(m, *args, **kw)
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|