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/store.rb
CHANGED
|
@@ -3,7 +3,7 @@ require "securerandom"
|
|
|
3
3
|
|
|
4
4
|
module Textus
|
|
5
5
|
class Store
|
|
6
|
-
attr_reader :root, :manifest, :registry
|
|
6
|
+
attr_reader :root, :manifest, :registry, :reader, :writer, :bus
|
|
7
7
|
|
|
8
8
|
# A Textus UID: 16 lowercase hex chars (SecureRandom.hex(8)). Not a UUID —
|
|
9
9
|
# short on purpose. Random enough for collision-never-in-practice within a
|
|
@@ -39,15 +39,18 @@ module Textus
|
|
|
39
39
|
def initialize(root)
|
|
40
40
|
@root = File.expand_path(root)
|
|
41
41
|
@manifest = Manifest.load(@root)
|
|
42
|
-
@
|
|
42
|
+
@bus = Hooks::Dispatcher.new(audit_log: audit_log)
|
|
43
|
+
@registry = Hooks::Registry.new(dispatcher: @bus)
|
|
43
44
|
@schemas = {}
|
|
44
45
|
load_extensions
|
|
46
|
+
@reader = Reader.new(self)
|
|
47
|
+
@writer = Writer.new(self)
|
|
45
48
|
end
|
|
46
49
|
|
|
47
50
|
def load_extensions
|
|
48
51
|
Textus.with_registry(@registry) do
|
|
49
|
-
|
|
50
|
-
dir = File.join(@root, "
|
|
52
|
+
Hooks::Builtin.register_all
|
|
53
|
+
dir = File.join(@root, "hooks")
|
|
51
54
|
return unless File.directory?(dir)
|
|
52
55
|
|
|
53
56
|
Dir.glob(File.join(dir, "*.rb")).sort.each do |f| # rubocop:disable Lint/RedundantDirGlobSort
|
|
@@ -72,241 +75,42 @@ module Textus
|
|
|
72
75
|
end
|
|
73
76
|
|
|
74
77
|
def get(key)
|
|
75
|
-
|
|
76
|
-
raise UnknownKey.new(key, suggestions: @manifest.suggestions_for(key)) unless File.exist?(path)
|
|
77
|
-
|
|
78
|
-
raw = File.binread(path)
|
|
79
|
-
parsed = Entry.for_format(mentry.format).parse(raw, path: path)
|
|
80
|
-
meta = parsed["_meta"]
|
|
81
|
-
content = parsed["content"]
|
|
82
|
-
enforce_name_match!(path, meta, mentry.format)
|
|
83
|
-
schema = schema_for(mentry.schema)
|
|
84
|
-
if schema
|
|
85
|
-
case mentry.format
|
|
86
|
-
when "markdown" then schema.validate!(meta)
|
|
87
|
-
when "json", "yaml" then schema.validate!(content || {})
|
|
88
|
-
# text: schema forbidden by manifest validation
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
build_envelope(key, mentry, path, meta, parsed["body"], Etag.for_bytes(raw), content: content)
|
|
78
|
+
@reader.get(key)
|
|
92
79
|
end
|
|
93
80
|
|
|
94
|
-
def where(key)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"protocol" => PROTOCOL,
|
|
98
|
-
"key" => key,
|
|
99
|
-
"zone" => mentry.zone,
|
|
100
|
-
"owner" => mentry.owner,
|
|
101
|
-
"path" => path,
|
|
102
|
-
}
|
|
103
|
-
end
|
|
81
|
+
def where(key) = @reader.where(key)
|
|
82
|
+
def list(**) = @reader.list(**)
|
|
83
|
+
def schema_envelope(key) = @reader.schema_envelope(key)
|
|
104
84
|
|
|
105
|
-
def
|
|
106
|
-
rows = @manifest.enumerate(prefix: prefix)
|
|
107
|
-
rows = rows.select { |r| r[:manifest_entry].zone == zone } if zone
|
|
108
|
-
rows.map do |row|
|
|
109
|
-
{
|
|
110
|
-
"key" => row[:key],
|
|
111
|
-
"zone" => row[:manifest_entry].zone,
|
|
112
|
-
"path" => row[:path],
|
|
113
|
-
}
|
|
114
|
-
end
|
|
115
|
-
end
|
|
85
|
+
def put(...) = @writer.put(...)
|
|
116
86
|
|
|
117
|
-
def
|
|
118
|
-
mentry, = @manifest.resolve(key)
|
|
119
|
-
schema = schema_for(mentry.schema)
|
|
120
|
-
{
|
|
121
|
-
"protocol" => PROTOCOL,
|
|
122
|
-
"key" => key,
|
|
123
|
-
"schema_ref" => mentry.schema,
|
|
124
|
-
"schema" => schema&.to_h,
|
|
125
|
-
}
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# rubocop:disable Metrics/ParameterLists
|
|
129
|
-
def put(key, meta: nil, body: nil, content: nil, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
|
|
130
|
-
# rubocop:enable Metrics/ParameterLists
|
|
131
|
-
@manifest.validate_key!(key)
|
|
132
|
-
mentry, path, = @manifest.resolve(key)
|
|
133
|
-
writers = @manifest.zone_writers(mentry.zone)
|
|
134
|
-
raise WriteForbidden.new(key, mentry.zone, writers: writers) unless writers.include?(as)
|
|
135
|
-
|
|
136
|
-
meta ||= {}
|
|
137
|
-
strategy = Entry.for_format(mentry.format)
|
|
138
|
-
|
|
139
|
-
existing_uid = existing_uid_for(mentry, path)
|
|
140
|
-
meta, content = ensure_uid(mentry.format, meta, content, existing_uid)
|
|
141
|
-
|
|
142
|
-
bytes, eff_meta, eff_body, eff_content = serialize_for_put(
|
|
143
|
-
mentry: mentry, path: path, strategy: strategy,
|
|
144
|
-
meta: meta, body: body, content: content
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
enforce_name_match!(path, eff_meta, mentry.format)
|
|
148
|
-
|
|
149
|
-
schema = schema_for(mentry.schema)
|
|
150
|
-
if schema
|
|
151
|
-
case mentry.format
|
|
152
|
-
when "markdown" then schema.validate!(eff_meta)
|
|
153
|
-
when "json", "yaml" then schema.validate!(eff_content || {})
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
etag_before = File.exist?(path) ? Etag.for_file(path) : nil
|
|
158
|
-
raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && (etag_before != if_etag)
|
|
159
|
-
|
|
160
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
161
|
-
File.binwrite(path, bytes)
|
|
162
|
-
etag_after = Etag.for_bytes(bytes)
|
|
163
|
-
audit_log.append(role: as, verb: "put", key: key, etag_before: etag_before, etag_after: etag_after)
|
|
164
|
-
envelope = build_envelope(key, mentry, path, eff_meta, eff_body, etag_after, content: eff_content)
|
|
165
|
-
fire_event(:put, key: key, envelope: envelope) unless suppress_events
|
|
166
|
-
envelope
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
def delete(key, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
|
|
170
|
-
mentry, path, = @manifest.resolve(key)
|
|
171
|
-
writers = @manifest.zone_writers(mentry.zone)
|
|
172
|
-
raise WriteForbidden.new(key, mentry.zone, writers: writers) unless writers.include?(as)
|
|
173
|
-
raise UnknownKey.new(key, suggestions: @manifest.suggestions_for(key)) unless File.exist?(path)
|
|
174
|
-
|
|
175
|
-
etag_before = Etag.for_file(path)
|
|
176
|
-
raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && if_etag != etag_before
|
|
177
|
-
|
|
178
|
-
File.delete(path)
|
|
179
|
-
audit_log.append(role: as, verb: "delete", key: key, etag_before: etag_before, etag_after: nil)
|
|
180
|
-
fire_event(:delete, key: key) unless suppress_events
|
|
181
|
-
{ "protocol" => PROTOCOL, "ok" => true, "key" => key, "deleted" => true }
|
|
182
|
-
end
|
|
87
|
+
def delete(...) = @writer.delete(...)
|
|
183
88
|
|
|
184
89
|
def fire_event(event, **)
|
|
185
|
-
|
|
90
|
+
view = Store::View.new(self)
|
|
91
|
+
@bus.publish(event, store: view, **)
|
|
186
92
|
end
|
|
187
93
|
|
|
188
|
-
def accept(
|
|
189
|
-
Proposal.accept(self, key, as: as)
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def deps(key) = Dependencies.deps_of(@manifest, key)
|
|
193
|
-
def rdeps(key) = Dependencies.rdeps_of(@manifest, key)
|
|
194
|
-
def published = Dependencies.published_of(@manifest)
|
|
195
|
-
|
|
196
|
-
def validate_all
|
|
197
|
-
Validator.new(self).call
|
|
198
|
-
end
|
|
94
|
+
def accept(...) = @writer.accept(...)
|
|
199
95
|
|
|
200
|
-
def
|
|
201
|
-
|
|
202
|
-
|
|
96
|
+
def deps(key) = @reader.deps(key)
|
|
97
|
+
def rdeps(key) = @reader.rdeps(key)
|
|
98
|
+
def published = @reader.published
|
|
99
|
+
def stale(**) = @reader.stale(**)
|
|
100
|
+
def validate_all = @reader.validate_all
|
|
203
101
|
|
|
204
|
-
|
|
205
|
-
# Raises UnknownKey if the key doesn't resolve to a real file.
|
|
206
|
-
def uid(key)
|
|
207
|
-
env = get(key)
|
|
208
|
-
env["uid"]
|
|
209
|
-
end
|
|
102
|
+
def uid(key) = @reader.uid(key)
|
|
210
103
|
|
|
211
104
|
# Move an entry from old_key to new_key within the same zone. Preserves
|
|
212
105
|
# uid (minting one first if absent), validates both keys against the
|
|
213
106
|
# manifest, refuses to clobber, and writes one mv audit row.
|
|
214
107
|
def mv(old_key, new_key, as: Role::DEFAULT, dry_run: false)
|
|
215
|
-
Mover.new(
|
|
108
|
+
Mover.new(reader: @reader, writer: @writer, manifest: @manifest, audit_log: audit_log)
|
|
109
|
+
.call(old_key, new_key, as: as, dry_run: dry_run)
|
|
216
110
|
end
|
|
217
111
|
|
|
218
112
|
def audit_log
|
|
219
|
-
@audit_log ||= AuditLog.new(@root)
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
private
|
|
223
|
-
|
|
224
|
-
def existing_uid_for(mentry, path)
|
|
225
|
-
return nil unless File.exist?(path)
|
|
226
|
-
|
|
227
|
-
raw = File.binread(path)
|
|
228
|
-
parsed = Entry.for_format(mentry.format).parse(raw, path: path)
|
|
229
|
-
extract_uid(parsed["_meta"])
|
|
230
|
-
rescue StandardError
|
|
231
|
-
nil
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
# Ensures the payload carries a uid: preserve existing, else mint.
|
|
235
|
-
# Returns [meta, content] possibly mutated.
|
|
236
|
-
def ensure_uid(format, meta, content, existing_uid)
|
|
237
|
-
case format
|
|
238
|
-
when "markdown", "json", "yaml"
|
|
239
|
-
m = meta.is_a?(Hash) ? meta.dup : {}
|
|
240
|
-
m["uid"] = existing_uid || Store.mint_uid unless m["uid"].is_a?(String) && !m["uid"].empty?
|
|
241
|
-
[m, content]
|
|
242
|
-
else
|
|
243
|
-
# text: no uid channel
|
|
244
|
-
[meta, content]
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
def enforce_name_match!(path, meta, format)
|
|
249
|
-
return unless %w[markdown json yaml].include?(format)
|
|
250
|
-
return unless meta.is_a?(Hash) && meta["name"]
|
|
251
|
-
|
|
252
|
-
ext = Entry.for_format(format).extensions.first
|
|
253
|
-
basename = File.basename(path, ext)
|
|
254
|
-
return if meta["name"] == basename
|
|
255
|
-
|
|
256
|
-
raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
def serialize_for_put(mentry:, path:, strategy:, meta:, body:, content:)
|
|
260
|
-
case mentry.format
|
|
261
|
-
when "markdown", "text"
|
|
262
|
-
bytes = strategy.serialize(meta: meta, body: body.to_s)
|
|
263
|
-
[bytes, meta, body.to_s, nil]
|
|
264
|
-
when "json", "yaml"
|
|
265
|
-
raise UsageError.new("put for #{mentry.format} requires content: or body:") if content.nil? && (body.nil? || body.to_s.empty?)
|
|
266
|
-
|
|
267
|
-
if content.nil?
|
|
268
|
-
# Caller passed raw body; validate by parsing.
|
|
269
|
-
begin
|
|
270
|
-
parsed = strategy.parse(body.to_s, path: path)
|
|
271
|
-
rescue BadFrontmatter => e
|
|
272
|
-
raise BadContent.new(path, "bad_content: #{e.message}")
|
|
273
|
-
end
|
|
274
|
-
eff_meta = parsed["_meta"]
|
|
275
|
-
eff_content = parsed["content"]
|
|
276
|
-
[body.to_s, eff_meta, body.to_s, eff_content]
|
|
277
|
-
else
|
|
278
|
-
bytes = strategy.serialize(meta: meta, body: "", content: content)
|
|
279
|
-
[bytes, meta, bytes, content]
|
|
280
|
-
end
|
|
281
|
-
else
|
|
282
|
-
raise UsageError.new("unknown format #{mentry.format.inspect}")
|
|
283
|
-
end
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
# rubocop:disable Metrics/ParameterLists
|
|
287
|
-
def build_envelope(key, mentry, path, meta, body, etag, content: nil)
|
|
288
|
-
# rubocop:enable Metrics/ParameterLists
|
|
289
|
-
env = {
|
|
290
|
-
"protocol" => PROTOCOL,
|
|
291
|
-
"key" => key,
|
|
292
|
-
"zone" => mentry.zone,
|
|
293
|
-
"owner" => mentry.owner,
|
|
294
|
-
"path" => path,
|
|
295
|
-
"format" => mentry.format,
|
|
296
|
-
"_meta" => meta,
|
|
297
|
-
"body" => body,
|
|
298
|
-
"etag" => etag,
|
|
299
|
-
"schema_ref" => mentry.schema,
|
|
300
|
-
"uid" => extract_uid(meta),
|
|
301
|
-
}
|
|
302
|
-
env["content"] = content unless content.nil?
|
|
303
|
-
env
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
# Pull a Textus UID out of the unified _meta hash.
|
|
307
|
-
def extract_uid(meta)
|
|
308
|
-
v = meta.is_a?(Hash) ? meta["uid"] : nil
|
|
309
|
-
v.is_a?(String) ? v : nil
|
|
113
|
+
@audit_log ||= Store::AuditLog.new(@root)
|
|
310
114
|
end
|
|
311
115
|
end
|
|
312
116
|
end
|
data/lib/textus/version.rb
CHANGED
data/lib/textus.rb
CHANGED
|
@@ -1,69 +1,16 @@
|
|
|
1
|
+
require "zeitwerk"
|
|
1
2
|
require_relative "textus/version"
|
|
2
3
|
require_relative "textus/errors"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
require_relative "textus/manifest"
|
|
17
|
-
require_relative "textus/dependencies"
|
|
18
|
-
require_relative "textus/store/events"
|
|
19
|
-
require_relative "textus/store/validator"
|
|
20
|
-
require_relative "textus/store/staleness"
|
|
21
|
-
require_relative "textus/store/mover"
|
|
22
|
-
require_relative "textus/store"
|
|
23
|
-
require_relative "textus/store_view"
|
|
24
|
-
require_relative "textus/refresh"
|
|
25
|
-
require_relative "textus/mustache"
|
|
26
|
-
require_relative "textus/projection"
|
|
27
|
-
require_relative "textus/builtin_actions"
|
|
28
|
-
require_relative "textus/publisher"
|
|
29
|
-
require_relative "textus/builder"
|
|
30
|
-
require_relative "textus/proposal"
|
|
31
|
-
require_relative "textus/init"
|
|
32
|
-
require_relative "textus/schema_tools"
|
|
33
|
-
require_relative "textus/migrate_keys"
|
|
34
|
-
require_relative "textus/migrate_v2"
|
|
35
|
-
require_relative "textus/doctor"
|
|
36
|
-
require_relative "textus/intro"
|
|
37
|
-
# CLI verb command objects — base class first, then verbs alphabetically.
|
|
38
|
-
require_relative "textus/cli/verb"
|
|
39
|
-
require_relative "textus/cli/deprecated_alias"
|
|
40
|
-
require_relative "textus/cli/accept"
|
|
41
|
-
require_relative "textus/cli/action"
|
|
42
|
-
require_relative "textus/cli/build"
|
|
43
|
-
require_relative "textus/cli/delete"
|
|
44
|
-
require_relative "textus/cli/deps"
|
|
45
|
-
require_relative "textus/cli/doctor"
|
|
46
|
-
require_relative "textus/cli/extensions"
|
|
47
|
-
require_relative "textus/cli/get"
|
|
48
|
-
require_relative "textus/cli/init"
|
|
49
|
-
require_relative "textus/cli/intro"
|
|
50
|
-
require_relative "textus/cli/list"
|
|
51
|
-
require_relative "textus/cli/migrate_keys"
|
|
52
|
-
require_relative "textus/cli/migrate"
|
|
53
|
-
require_relative "textus/cli/mv"
|
|
54
|
-
require_relative "textus/cli/published"
|
|
55
|
-
require_relative "textus/cli/put"
|
|
56
|
-
require_relative "textus/cli/rdeps"
|
|
57
|
-
require_relative "textus/cli/refresh"
|
|
58
|
-
require_relative "textus/cli/schema"
|
|
59
|
-
require_relative "textus/cli/schema_diff"
|
|
60
|
-
require_relative "textus/cli/schema_init"
|
|
61
|
-
require_relative "textus/cli/schema_migrate"
|
|
62
|
-
require_relative "textus/cli/stale"
|
|
63
|
-
require_relative "textus/cli/uid"
|
|
64
|
-
require_relative "textus/cli/where"
|
|
65
|
-
require_relative "textus/cli/group"
|
|
66
|
-
require_relative "textus/cli/key_group"
|
|
67
|
-
require_relative "textus/cli/schema_group"
|
|
68
|
-
require_relative "textus/cli/extension_group"
|
|
69
|
-
require_relative "textus/cli"
|
|
4
|
+
|
|
5
|
+
loader = Zeitwerk::Loader.for_gem
|
|
6
|
+
loader.inflector.inflect(
|
|
7
|
+
"cli" => "CLI",
|
|
8
|
+
"json" => "Json",
|
|
9
|
+
"yaml" => "Yaml",
|
|
10
|
+
)
|
|
11
|
+
loader.ignore(File.expand_path("textus/errors.rb", __dir__))
|
|
12
|
+
loader.setup
|
|
13
|
+
loader.eager_load
|
|
14
|
+
|
|
15
|
+
module Textus
|
|
16
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: textus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -51,6 +51,20 @@ dependencies:
|
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '3.2'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: zeitwerk
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '2.6'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '2.6'
|
|
54
68
|
- !ruby/object:Gem::Dependency
|
|
55
69
|
name: rake
|
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -95,59 +109,76 @@ files:
|
|
|
95
109
|
- docs/conventions.md
|
|
96
110
|
- exe/textus
|
|
97
111
|
- lib/textus.rb
|
|
98
|
-
- lib/textus/audit_log.rb
|
|
99
112
|
- lib/textus/builder.rb
|
|
100
|
-
- lib/textus/
|
|
113
|
+
- lib/textus/builder/pipeline.rb
|
|
114
|
+
- lib/textus/builder/renderer.rb
|
|
115
|
+
- lib/textus/builder/renderer/json.rb
|
|
116
|
+
- lib/textus/builder/renderer/markdown.rb
|
|
117
|
+
- lib/textus/builder/renderer/text.rb
|
|
118
|
+
- lib/textus/builder/renderer/yaml.rb
|
|
101
119
|
- lib/textus/cli.rb
|
|
102
|
-
- lib/textus/cli/accept.rb
|
|
103
|
-
- lib/textus/cli/action.rb
|
|
104
|
-
- lib/textus/cli/build.rb
|
|
105
|
-
- lib/textus/cli/delete.rb
|
|
106
|
-
- lib/textus/cli/deprecated_alias.rb
|
|
107
|
-
- lib/textus/cli/deps.rb
|
|
108
|
-
- lib/textus/cli/doctor.rb
|
|
109
|
-
- lib/textus/cli/extension_group.rb
|
|
110
|
-
- lib/textus/cli/extensions.rb
|
|
111
|
-
- lib/textus/cli/get.rb
|
|
112
120
|
- lib/textus/cli/group.rb
|
|
113
|
-
- lib/textus/cli/
|
|
114
|
-
- lib/textus/cli/
|
|
115
|
-
- lib/textus/cli/
|
|
116
|
-
- lib/textus/cli/list.rb
|
|
117
|
-
- lib/textus/cli/migrate.rb
|
|
118
|
-
- lib/textus/cli/migrate_keys.rb
|
|
119
|
-
- lib/textus/cli/mv.rb
|
|
120
|
-
- lib/textus/cli/published.rb
|
|
121
|
-
- lib/textus/cli/put.rb
|
|
122
|
-
- lib/textus/cli/rdeps.rb
|
|
123
|
-
- lib/textus/cli/refresh.rb
|
|
124
|
-
- lib/textus/cli/schema.rb
|
|
125
|
-
- lib/textus/cli/schema_diff.rb
|
|
126
|
-
- lib/textus/cli/schema_group.rb
|
|
127
|
-
- lib/textus/cli/schema_init.rb
|
|
128
|
-
- lib/textus/cli/schema_migrate.rb
|
|
129
|
-
- lib/textus/cli/stale.rb
|
|
130
|
-
- lib/textus/cli/uid.rb
|
|
121
|
+
- lib/textus/cli/group/hook.rb
|
|
122
|
+
- lib/textus/cli/group/key.rb
|
|
123
|
+
- lib/textus/cli/group/schema.rb
|
|
131
124
|
- lib/textus/cli/verb.rb
|
|
132
|
-
- lib/textus/cli/
|
|
125
|
+
- lib/textus/cli/verb/accept.rb
|
|
126
|
+
- lib/textus/cli/verb/build.rb
|
|
127
|
+
- lib/textus/cli/verb/delete.rb
|
|
128
|
+
- lib/textus/cli/verb/deps.rb
|
|
129
|
+
- lib/textus/cli/verb/doctor.rb
|
|
130
|
+
- lib/textus/cli/verb/get.rb
|
|
131
|
+
- lib/textus/cli/verb/hook_run.rb
|
|
132
|
+
- lib/textus/cli/verb/hooks.rb
|
|
133
|
+
- lib/textus/cli/verb/init.rb
|
|
134
|
+
- lib/textus/cli/verb/intro.rb
|
|
135
|
+
- lib/textus/cli/verb/list.rb
|
|
136
|
+
- lib/textus/cli/verb/migrate_keys.rb
|
|
137
|
+
- lib/textus/cli/verb/mv.rb
|
|
138
|
+
- lib/textus/cli/verb/published.rb
|
|
139
|
+
- lib/textus/cli/verb/put.rb
|
|
140
|
+
- lib/textus/cli/verb/rdeps.rb
|
|
141
|
+
- lib/textus/cli/verb/refresh.rb
|
|
142
|
+
- lib/textus/cli/verb/schema.rb
|
|
143
|
+
- lib/textus/cli/verb/schema_diff.rb
|
|
144
|
+
- lib/textus/cli/verb/schema_init.rb
|
|
145
|
+
- lib/textus/cli/verb/schema_migrate.rb
|
|
146
|
+
- lib/textus/cli/verb/stale.rb
|
|
147
|
+
- lib/textus/cli/verb/uid.rb
|
|
148
|
+
- lib/textus/cli/verb/where.rb
|
|
133
149
|
- lib/textus/dependencies.rb
|
|
134
150
|
- lib/textus/doctor.rb
|
|
151
|
+
- lib/textus/doctor/check.rb
|
|
152
|
+
- lib/textus/doctor/check/audit_log.rb
|
|
153
|
+
- lib/textus/doctor/check/hooks.rb
|
|
154
|
+
- lib/textus/doctor/check/illegal_keys.rb
|
|
155
|
+
- lib/textus/doctor/check/manifest_files.rb
|
|
156
|
+
- lib/textus/doctor/check/schema_violations.rb
|
|
157
|
+
- lib/textus/doctor/check/schemas.rb
|
|
158
|
+
- lib/textus/doctor/check/sentinels.rb
|
|
159
|
+
- lib/textus/doctor/check/templates.rb
|
|
160
|
+
- lib/textus/doctor/check/unowned_schema_fields.rb
|
|
135
161
|
- lib/textus/entry.rb
|
|
162
|
+
- lib/textus/entry/base.rb
|
|
136
163
|
- lib/textus/entry/json.rb
|
|
137
164
|
- lib/textus/entry/markdown.rb
|
|
138
165
|
- lib/textus/entry/text.rb
|
|
139
166
|
- lib/textus/entry/yaml.rb
|
|
167
|
+
- lib/textus/envelope.rb
|
|
140
168
|
- lib/textus/errors.rb
|
|
141
169
|
- lib/textus/etag.rb
|
|
142
|
-
- lib/textus/
|
|
143
|
-
- lib/textus/
|
|
170
|
+
- lib/textus/hooks/builtin.rb
|
|
171
|
+
- lib/textus/hooks/dispatcher.rb
|
|
172
|
+
- lib/textus/hooks/loader.rb
|
|
173
|
+
- lib/textus/hooks/registry.rb
|
|
144
174
|
- lib/textus/init.rb
|
|
145
175
|
- lib/textus/intro.rb
|
|
146
|
-
- lib/textus/
|
|
176
|
+
- lib/textus/key/distance.rb
|
|
177
|
+
- lib/textus/key/grammar.rb
|
|
178
|
+
- lib/textus/key/path.rb
|
|
147
179
|
- lib/textus/manifest.rb
|
|
148
|
-
- lib/textus/
|
|
180
|
+
- lib/textus/manifest/entry.rb
|
|
149
181
|
- lib/textus/migrate_keys.rb
|
|
150
|
-
- lib/textus/migrate_v2.rb
|
|
151
182
|
- lib/textus/mustache.rb
|
|
152
183
|
- lib/textus/projection.rb
|
|
153
184
|
- lib/textus/proposal.rb
|
|
@@ -155,13 +186,15 @@ files:
|
|
|
155
186
|
- lib/textus/refresh.rb
|
|
156
187
|
- lib/textus/role.rb
|
|
157
188
|
- lib/textus/schema.rb
|
|
158
|
-
- lib/textus/
|
|
189
|
+
- lib/textus/schema/tools.rb
|
|
159
190
|
- lib/textus/store.rb
|
|
160
|
-
- lib/textus/store/
|
|
191
|
+
- lib/textus/store/audit_log.rb
|
|
161
192
|
- lib/textus/store/mover.rb
|
|
193
|
+
- lib/textus/store/reader.rb
|
|
162
194
|
- lib/textus/store/staleness.rb
|
|
163
195
|
- lib/textus/store/validator.rb
|
|
164
|
-
- lib/textus/
|
|
196
|
+
- lib/textus/store/view.rb
|
|
197
|
+
- lib/textus/store/writer.rb
|
|
165
198
|
- lib/textus/version.rb
|
|
166
199
|
homepage: https://github.com/patrick204nqh/textus
|
|
167
200
|
licenses:
|
data/lib/textus/audit_log.rb
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
require "time"
|
|
3
|
-
|
|
4
|
-
module Textus
|
|
5
|
-
class AuditLog
|
|
6
|
-
def initialize(root)
|
|
7
|
-
@path = File.join(root, "audit.log")
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def last_writer_for(key)
|
|
11
|
-
return nil unless File.exist?(@path)
|
|
12
|
-
|
|
13
|
-
last_role = nil
|
|
14
|
-
File.foreach(@path) do |line|
|
|
15
|
-
parsed = parse_row(line.chomp)
|
|
16
|
-
next unless parsed
|
|
17
|
-
next unless parsed["key"] == key
|
|
18
|
-
next unless %w[put delete].include?(parsed["verb"])
|
|
19
|
-
|
|
20
|
-
last_role = parsed["role"]
|
|
21
|
-
end
|
|
22
|
-
last_role
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def append(role:, verb:, key:, etag_before:, etag_after:, extras: nil)
|
|
26
|
-
row = {
|
|
27
|
-
"ts" => Time.now.utc.iso8601,
|
|
28
|
-
"role" => role,
|
|
29
|
-
"verb" => verb,
|
|
30
|
-
"key" => key,
|
|
31
|
-
"etag_before" => etag_before,
|
|
32
|
-
"etag_after" => etag_after,
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if extras.is_a?(Hash) && !extras.empty?
|
|
36
|
-
extras = extras.dup
|
|
37
|
-
%w[from_key to_key uid].each do |k|
|
|
38
|
-
row[k] = extras.delete(k) if extras.key?(k)
|
|
39
|
-
end
|
|
40
|
-
row["extras"] = extras unless extras.empty?
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
File.open(@path, File::WRONLY | File::APPEND | File::CREAT, 0o644) do |f|
|
|
44
|
-
f.flock(File::LOCK_EX)
|
|
45
|
-
f.write(JSON.generate(row) + "\n")
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
def parse_row(line)
|
|
52
|
-
return nil if line.empty?
|
|
53
|
-
|
|
54
|
-
if line.start_with?("{")
|
|
55
|
-
JSON.parse(line)
|
|
56
|
-
else
|
|
57
|
-
# Legacy TSV: ts, role, verb, key, etag_before, etag_after [, json_extras]
|
|
58
|
-
fields = line.split("\t")
|
|
59
|
-
return nil if fields.length < 4
|
|
60
|
-
|
|
61
|
-
{ "ts" => fields[0], "role" => fields[1], "verb" => fields[2], "key" => fields[3] }
|
|
62
|
-
end
|
|
63
|
-
rescue JSON::ParserError
|
|
64
|
-
nil
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
require "csv"
|
|
3
|
-
require "yaml"
|
|
4
|
-
require "rexml/document"
|
|
5
|
-
|
|
6
|
-
module Textus
|
|
7
|
-
module BuiltinActions
|
|
8
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
9
|
-
def self.register_all
|
|
10
|
-
Textus.action(:json) do |config:, store:, args:|
|
|
11
|
-
_ = store
|
|
12
|
-
_ = args
|
|
13
|
-
data = JSON.parse(config["bytes"].to_s)
|
|
14
|
-
{ _meta: {}, body: YAML.dump(data) }
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
Textus.action(:csv) do |config:, store:, args:|
|
|
18
|
-
_ = store
|
|
19
|
-
_ = args
|
|
20
|
-
rows = CSV.parse(config["bytes"].to_s, headers: true).map(&:to_h)
|
|
21
|
-
{ _meta: {}, body: YAML.dump(rows) }
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
Textus.action(:"markdown-links") do |config:, store:, args:|
|
|
25
|
-
_ = store
|
|
26
|
-
_ = args
|
|
27
|
-
links = config["bytes"].to_s.scan(%r{\[([^\]]+)\]\((https?://[^)\s]+)\)}).map do |text, href|
|
|
28
|
-
{ "text" => text, "href" => href }
|
|
29
|
-
end
|
|
30
|
-
{ _meta: {}, body: YAML.dump(links) }
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
Textus.action(:"ical-events") do |config:, store:, args:|
|
|
34
|
-
_ = store
|
|
35
|
-
_ = args
|
|
36
|
-
events = []
|
|
37
|
-
current = nil
|
|
38
|
-
config["bytes"].to_s.each_line do |line|
|
|
39
|
-
line = line.strip
|
|
40
|
-
case line
|
|
41
|
-
when "BEGIN:VEVENT" then current = {}
|
|
42
|
-
when "END:VEVENT"
|
|
43
|
-
events << current if current
|
|
44
|
-
current = nil
|
|
45
|
-
when /\A(SUMMARY|DTSTART|DTEND|UID|LOCATION|DESCRIPTION):(.*)\z/
|
|
46
|
-
current[Regexp.last_match(1).downcase] = Regexp.last_match(2) if current
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
{ _meta: {}, body: YAML.dump(events) }
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
Textus.action(:rss) do |config:, store:, args:|
|
|
53
|
-
_ = store
|
|
54
|
-
_ = args
|
|
55
|
-
doc = REXML::Document.new(config["bytes"].to_s)
|
|
56
|
-
items = doc.elements.to_a("//item").map do |item|
|
|
57
|
-
{
|
|
58
|
-
"title" => item.elements["title"]&.text,
|
|
59
|
-
"link" => item.elements["link"]&.text,
|
|
60
|
-
"pubDate" => item.elements["pubDate"]&.text,
|
|
61
|
-
}
|
|
62
|
-
end
|
|
63
|
-
{ _meta: {}, body: YAML.dump(items) }
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
67
|
-
end
|
|
68
|
-
end
|