textus 0.4.0 → 0.5.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 +64 -1
- data/README.md +13 -11
- data/SPEC.md +13 -9
- data/docs/architecture.md +63 -28
- data/lib/textus/audit_log.rb +46 -11
- data/lib/textus/builder.rb +3 -3
- data/lib/textus/builtin_actions.rb +5 -5
- data/lib/textus/cli/accept.rb +13 -0
- data/lib/textus/cli/action.rb +51 -0
- data/lib/textus/cli/build.rb +11 -0
- data/lib/textus/cli/delete.rb +14 -0
- data/lib/textus/cli/deprecated_alias.rb +31 -0
- data/lib/textus/cli/deps.rb +10 -0
- data/lib/textus/cli/doctor.rb +13 -0
- data/lib/textus/cli/extension_group.rb +9 -0
- data/lib/textus/cli/extensions.rb +49 -0
- data/lib/textus/cli/get.rb +10 -0
- data/lib/textus/cli/group.rb +51 -0
- data/lib/textus/cli/init.rb +12 -0
- data/lib/textus/cli/intro.rb +9 -0
- data/lib/textus/cli/key_group.rb +10 -0
- data/lib/textus/cli/list.rb +12 -0
- data/lib/textus/cli/migrate.rb +41 -0
- data/lib/textus/cli/migrate_keys.rb +19 -0
- data/lib/textus/cli/mv.rb +20 -0
- data/lib/textus/cli/published.rb +9 -0
- data/lib/textus/cli/put.rb +48 -0
- data/lib/textus/cli/rdeps.rb +10 -0
- data/lib/textus/cli/refresh.rb +13 -0
- data/lib/textus/cli/schema.rb +10 -0
- data/lib/textus/cli/schema_diff.rb +15 -0
- data/lib/textus/cli/schema_group.rb +33 -0
- data/lib/textus/cli/schema_init.rb +19 -0
- data/lib/textus/cli/schema_migrate.rb +19 -0
- data/lib/textus/cli/stale.rb +12 -0
- data/lib/textus/cli/uid.rb +15 -0
- data/lib/textus/cli/verb.rb +62 -0
- data/lib/textus/cli/where.rb +10 -0
- data/lib/textus/cli.rb +65 -387
- data/lib/textus/doctor.rb +64 -33
- data/lib/textus/entry/json.rb +6 -4
- data/lib/textus/entry/markdown.rb +4 -4
- data/lib/textus/entry/text.rb +3 -3
- data/lib/textus/entry/yaml.rb +6 -4
- data/lib/textus/entry.rb +2 -2
- data/lib/textus/errors.rb +2 -2
- data/lib/textus/init.rb +1 -1
- data/lib/textus/intro.rb +2 -2
- data/lib/textus/manifest.rb +11 -221
- data/lib/textus/manifest_entry.rb +185 -0
- data/lib/textus/migrate_v2.rb +27 -0
- data/lib/textus/projection.rb +1 -1
- data/lib/textus/proposal.rb +3 -3
- data/lib/textus/refresh.rb +7 -7
- data/lib/textus/schema_tools.rb +8 -8
- data/lib/textus/store/events.rb +31 -0
- data/lib/textus/store/mover.rb +118 -0
- data/lib/textus/store/staleness.rb +142 -0
- data/lib/textus/store/validator.rb +53 -0
- data/lib/textus/store.rb +49 -354
- data/lib/textus/version.rb +2 -2
- data/lib/textus.rb +38 -0
- metadata +38 -1
data/lib/textus/store.rb
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
require "fileutils"
|
|
2
2
|
require "securerandom"
|
|
3
|
-
require "time"
|
|
4
|
-
require "timeout"
|
|
5
3
|
|
|
6
4
|
module Textus
|
|
7
|
-
# rubocop:disable Metrics/ClassLength
|
|
8
5
|
class Store
|
|
9
|
-
HOOK_TIMEOUT_SECONDS = 2
|
|
10
|
-
|
|
11
6
|
attr_reader :root, :manifest, :registry
|
|
12
7
|
|
|
13
8
|
# A Textus UID: 16 lowercase hex chars (SecureRandom.hex(8)). Not a UUID —
|
|
@@ -82,18 +77,18 @@ module Textus
|
|
|
82
77
|
|
|
83
78
|
raw = File.binread(path)
|
|
84
79
|
parsed = Entry.for_format(mentry.format).parse(raw, path: path)
|
|
85
|
-
|
|
80
|
+
meta = parsed["_meta"]
|
|
86
81
|
content = parsed["content"]
|
|
87
|
-
enforce_name_match!(path,
|
|
82
|
+
enforce_name_match!(path, meta, mentry.format)
|
|
88
83
|
schema = schema_for(mentry.schema)
|
|
89
84
|
if schema
|
|
90
85
|
case mentry.format
|
|
91
|
-
when "markdown" then schema.validate!(
|
|
86
|
+
when "markdown" then schema.validate!(meta)
|
|
92
87
|
when "json", "yaml" then schema.validate!(content || {})
|
|
93
88
|
# text: schema forbidden by manifest validation
|
|
94
89
|
end
|
|
95
90
|
end
|
|
96
|
-
build_envelope(key, mentry, path,
|
|
91
|
+
build_envelope(key, mentry, path, meta, parsed["body"], Etag.for_bytes(raw), content: content)
|
|
97
92
|
end
|
|
98
93
|
|
|
99
94
|
def where(key)
|
|
@@ -131,30 +126,30 @@ module Textus
|
|
|
131
126
|
end
|
|
132
127
|
|
|
133
128
|
# rubocop:disable Metrics/ParameterLists
|
|
134
|
-
def put(key,
|
|
129
|
+
def put(key, meta: nil, body: nil, content: nil, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
|
|
135
130
|
# rubocop:enable Metrics/ParameterLists
|
|
136
131
|
@manifest.validate_key!(key)
|
|
137
132
|
mentry, path, = @manifest.resolve(key)
|
|
138
133
|
writers = @manifest.zone_writers(mentry.zone)
|
|
139
134
|
raise WriteForbidden.new(key, mentry.zone, writers: writers) unless writers.include?(as)
|
|
140
135
|
|
|
141
|
-
|
|
136
|
+
meta ||= {}
|
|
142
137
|
strategy = Entry.for_format(mentry.format)
|
|
143
138
|
|
|
144
139
|
existing_uid = existing_uid_for(mentry, path)
|
|
145
|
-
|
|
140
|
+
meta, content = ensure_uid(mentry.format, meta, content, existing_uid)
|
|
146
141
|
|
|
147
|
-
bytes,
|
|
142
|
+
bytes, eff_meta, eff_body, eff_content = serialize_for_put(
|
|
148
143
|
mentry: mentry, path: path, strategy: strategy,
|
|
149
|
-
|
|
144
|
+
meta: meta, body: body, content: content
|
|
150
145
|
)
|
|
151
146
|
|
|
152
|
-
enforce_name_match!(path,
|
|
147
|
+
enforce_name_match!(path, eff_meta, mentry.format)
|
|
153
148
|
|
|
154
149
|
schema = schema_for(mentry.schema)
|
|
155
150
|
if schema
|
|
156
151
|
case mentry.format
|
|
157
|
-
when "markdown" then schema.validate!(
|
|
152
|
+
when "markdown" then schema.validate!(eff_meta)
|
|
158
153
|
when "json", "yaml" then schema.validate!(eff_content || {})
|
|
159
154
|
end
|
|
160
155
|
end
|
|
@@ -166,7 +161,7 @@ module Textus
|
|
|
166
161
|
File.binwrite(path, bytes)
|
|
167
162
|
etag_after = Etag.for_bytes(bytes)
|
|
168
163
|
audit_log.append(role: as, verb: "put", key: key, etag_before: etag_before, etag_after: etag_after)
|
|
169
|
-
envelope = build_envelope(key, mentry, path,
|
|
164
|
+
envelope = build_envelope(key, mentry, path, eff_meta, eff_body, etag_after, content: eff_content)
|
|
170
165
|
fire_event(:put, key: key, envelope: envelope) unless suppress_events
|
|
171
166
|
envelope
|
|
172
167
|
end
|
|
@@ -186,22 +181,8 @@ module Textus
|
|
|
186
181
|
{ "protocol" => PROTOCOL, "ok" => true, "key" => key, "deleted" => true }
|
|
187
182
|
end
|
|
188
183
|
|
|
189
|
-
def fire_event(event, **
|
|
190
|
-
|
|
191
|
-
@registry.hooks(event).each do |entry|
|
|
192
|
-
name = entry[:name]
|
|
193
|
-
Timeout.timeout(HOOK_TIMEOUT_SECONDS) { entry[:callable].call(store: view, **kwargs) }
|
|
194
|
-
rescue StandardError => e
|
|
195
|
-
extras = { "event" => event.to_s, "hook" => name.to_s, "error" => "#{e.class}: #{e.message}" }
|
|
196
|
-
extras["target_key"] = kwargs[:target_key] if kwargs.key?(:target_key)
|
|
197
|
-
extras["pending_key"] = kwargs[:pending_key] if kwargs.key?(:pending_key)
|
|
198
|
-
audit_log.append(
|
|
199
|
-
role: "script", verb: "event_error",
|
|
200
|
-
key: kwargs[:key] || kwargs[:target_key] || kwargs[:pending_key] || "-",
|
|
201
|
-
etag_before: nil, etag_after: nil,
|
|
202
|
-
extras: extras
|
|
203
|
-
)
|
|
204
|
-
end
|
|
184
|
+
def fire_event(event, **)
|
|
185
|
+
Events.new(self).call(event, **)
|
|
205
186
|
end
|
|
206
187
|
|
|
207
188
|
def accept(key, as:)
|
|
@@ -213,121 +194,12 @@ module Textus
|
|
|
213
194
|
def published = Dependencies.published_of(@manifest)
|
|
214
195
|
|
|
215
196
|
def validate_all
|
|
216
|
-
|
|
217
|
-
@manifest.enumerate.each do |row|
|
|
218
|
-
begin
|
|
219
|
-
get(row[:key])
|
|
220
|
-
rescue Textus::Error => e
|
|
221
|
-
violations << { "key" => row[:key], "code" => e.code, "message" => e.message }
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
@manifest.enumerate.each do |row|
|
|
226
|
-
mentry = row[:manifest_entry]
|
|
227
|
-
next unless mentry.schema
|
|
228
|
-
|
|
229
|
-
schema = schema_for(mentry.schema)
|
|
230
|
-
next unless schema
|
|
231
|
-
|
|
232
|
-
env = begin
|
|
233
|
-
get(row[:key])
|
|
234
|
-
rescue StandardError
|
|
235
|
-
next
|
|
236
|
-
end
|
|
237
|
-
last_writer = audit_log.last_writer_for(row[:key])
|
|
238
|
-
next if last_writer.nil?
|
|
239
|
-
|
|
240
|
-
env["frontmatter"].each_key do |field|
|
|
241
|
-
owner = schema.maintained_by(field)
|
|
242
|
-
next if owner.nil?
|
|
243
|
-
next if last_writer == owner
|
|
244
|
-
next if last_writer == "human"
|
|
245
|
-
|
|
246
|
-
violations << {
|
|
247
|
-
"key" => row[:key],
|
|
248
|
-
"code" => "role_authority",
|
|
249
|
-
"field" => field,
|
|
250
|
-
"expected" => owner,
|
|
251
|
-
"last_writer" => last_writer,
|
|
252
|
-
}
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
{ "protocol" => PROTOCOL, "ok" => violations.empty?, "violations" => violations }
|
|
197
|
+
Validator.new(self).call
|
|
257
198
|
end
|
|
258
199
|
|
|
259
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockLength
|
|
260
200
|
def stale(prefix: nil, zone: nil)
|
|
261
|
-
|
|
262
|
-
@manifest.entries.each do |mentry|
|
|
263
|
-
next unless mentry.zone == "derived"
|
|
264
|
-
next if zone && mentry.zone != zone
|
|
265
|
-
|
|
266
|
-
gen = mentry.generator
|
|
267
|
-
next unless gen
|
|
268
|
-
next if prefix && !(mentry.key == prefix || mentry.key.start_with?("#{prefix}."))
|
|
269
|
-
|
|
270
|
-
path = path_for_entry(mentry)
|
|
271
|
-
|
|
272
|
-
unless File.exist?(path)
|
|
273
|
-
out << stale_row(mentry, path, "derived entry has never been generated")
|
|
274
|
-
next
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
raw = File.binread(path)
|
|
278
|
-
parsed = Entry.for_format(mentry.format).parse(raw, path: path)
|
|
279
|
-
generated_at = parsed["frontmatter"].dig("generated", "at")
|
|
280
|
-
unless generated_at
|
|
281
|
-
out << stale_row(mentry, path, "missing generated.at frontmatter")
|
|
282
|
-
next
|
|
283
|
-
end
|
|
284
|
-
gen_time = begin
|
|
285
|
-
Time.parse(generated_at.to_s)
|
|
286
|
-
rescue StandardError
|
|
287
|
-
nil
|
|
288
|
-
end
|
|
289
|
-
unless gen_time
|
|
290
|
-
out << stale_row(mentry, path, "unparseable generated.at: #{generated_at.inspect}")
|
|
291
|
-
next
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
offender = newest_source_after(gen, gen_time)
|
|
295
|
-
out << stale_row(mentry, path, "source '#{offender}' modified after generated.at") if offender
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
@manifest.entries.each do |mentry|
|
|
299
|
-
next unless mentry.action
|
|
300
|
-
next if zone && mentry.zone != zone
|
|
301
|
-
next if prefix && !(mentry.key == prefix || mentry.key.start_with?("#{prefix}."))
|
|
302
|
-
|
|
303
|
-
ttl = parse_ttl(mentry.ttl)
|
|
304
|
-
next unless ttl
|
|
305
|
-
|
|
306
|
-
path = path_for_entry(mentry)
|
|
307
|
-
|
|
308
|
-
unless File.exist?(path)
|
|
309
|
-
out << intake_stale_row(mentry, path, "never refreshed")
|
|
310
|
-
next
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
fm = Entry.for_format(mentry.format).parse(File.binread(path), path: path)["frontmatter"]
|
|
314
|
-
last_str = fm["last_refreshed_at"]
|
|
315
|
-
if last_str.nil?
|
|
316
|
-
out << intake_stale_row(mentry, path, "never refreshed (no last_refreshed_at)")
|
|
317
|
-
next
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
last = begin
|
|
321
|
-
Time.parse(last_str.to_s)
|
|
322
|
-
rescue StandardError
|
|
323
|
-
nil
|
|
324
|
-
end
|
|
325
|
-
out << intake_stale_row(mentry, path, "ttl exceeded (#{ttl}s)") if last.nil? || (Time.now - last) > ttl
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
out
|
|
201
|
+
Staleness.new(self).call(prefix: prefix, zone: zone)
|
|
329
202
|
end
|
|
330
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockLength
|
|
331
203
|
|
|
332
204
|
# Returns the Textus UID for a key (or nil if the entry has none yet).
|
|
333
205
|
# Raises UnknownKey if the key doesn't resolve to a real file.
|
|
@@ -339,223 +211,56 @@ module Textus
|
|
|
339
211
|
# Move an entry from old_key to new_key within the same zone. Preserves
|
|
340
212
|
# uid (minting one first if absent), validates both keys against the
|
|
341
213
|
# manifest, refuses to clobber, and writes one mv audit row.
|
|
342
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
343
214
|
def mv(old_key, new_key, as: Role::DEFAULT, dry_run: false)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
raise UsageError.new("mv: old and new keys are identical") if old_key == new_key
|
|
347
|
-
|
|
348
|
-
old_mentry, old_path, = @manifest.resolve(old_key)
|
|
349
|
-
raise UnknownKey.new(old_key) unless File.exist?(old_path)
|
|
350
|
-
|
|
351
|
-
new_mentry, new_path, = @manifest.resolve(new_key)
|
|
352
|
-
|
|
353
|
-
if old_mentry.zone != new_mentry.zone
|
|
354
|
-
raise UsageError.new(
|
|
355
|
-
"mv: cross-zone move refused (#{old_mentry.zone} → #{new_mentry.zone}). " \
|
|
356
|
-
"Use put+delete for cross-zone moves.",
|
|
357
|
-
)
|
|
358
|
-
end
|
|
359
|
-
if old_mentry.format != new_mentry.format
|
|
360
|
-
raise UsageError.new(
|
|
361
|
-
"mv: format mismatch (#{old_mentry.format} → #{new_mentry.format}); refusing.",
|
|
362
|
-
)
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
writers = @manifest.zone_writers(old_mentry.zone)
|
|
366
|
-
raise WriteForbidden.new(old_key, old_mentry.zone, writers: writers) unless writers.include?(as)
|
|
367
|
-
|
|
368
|
-
raise UsageError.new("mv: target '#{new_key}' already exists at #{new_path}") if File.exist?(new_path)
|
|
369
|
-
|
|
370
|
-
# Mint uid before the move so the audit row carries it.
|
|
371
|
-
pre_env = get(old_key)
|
|
372
|
-
current_uid = pre_env["uid"]
|
|
373
|
-
etag_before = pre_env["etag"]
|
|
374
|
-
|
|
375
|
-
if dry_run
|
|
376
|
-
return {
|
|
377
|
-
"protocol" => PROTOCOL, "ok" => true, "dry_run" => true,
|
|
378
|
-
"from_key" => old_key, "to_key" => new_key,
|
|
379
|
-
"from_path" => old_path, "to_path" => new_path,
|
|
380
|
-
"uid" => current_uid
|
|
381
|
-
}
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
if current_uid.nil?
|
|
385
|
-
# Write the uid in place first so the source file carries it before mv.
|
|
386
|
-
pre_env = put(old_key,
|
|
387
|
-
frontmatter: pre_env["frontmatter"],
|
|
388
|
-
body: pre_env["body"],
|
|
389
|
-
content: pre_env["content"],
|
|
390
|
-
as: as,
|
|
391
|
-
suppress_events: true)
|
|
392
|
-
current_uid = pre_env["uid"]
|
|
393
|
-
etag_before = pre_env["etag"]
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
FileUtils.mkdir_p(File.dirname(new_path))
|
|
397
|
-
FileUtils.mv(old_path, new_path)
|
|
398
|
-
rewrite_name_for_mv!(new_mentry, new_path, new_key)
|
|
399
|
-
etag_after = Etag.for_file(new_path)
|
|
400
|
-
|
|
401
|
-
audit_log.append(
|
|
402
|
-
role: as, verb: "mv", key: new_key,
|
|
403
|
-
etag_before: etag_before, etag_after: etag_after,
|
|
404
|
-
extras: {
|
|
405
|
-
"from_key" => old_key, "to_key" => new_key,
|
|
406
|
-
"from_path" => old_path, "to_path" => new_path,
|
|
407
|
-
"uid" => current_uid
|
|
408
|
-
}
|
|
409
|
-
)
|
|
215
|
+
Mover.new(self).call(old_key, new_key, as: as, dry_run: dry_run)
|
|
216
|
+
end
|
|
410
217
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
"protocol" => PROTOCOL, "ok" => true,
|
|
414
|
-
"from_key" => old_key, "to_key" => new_key,
|
|
415
|
-
"from_path" => old_path, "to_path" => new_path,
|
|
416
|
-
"uid" => current_uid,
|
|
417
|
-
"envelope" => env
|
|
418
|
-
}
|
|
218
|
+
def audit_log
|
|
219
|
+
@audit_log ||= AuditLog.new(@root)
|
|
419
220
|
end
|
|
420
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
421
221
|
|
|
422
222
|
private
|
|
423
223
|
|
|
424
|
-
# If the moved file carries a `name:` field (markdown) or `_meta.name`
|
|
425
|
-
# (json/yaml), rewrite it to the new basename so enforce_name_match! stays
|
|
426
|
-
# happy on the next read. Only touches the bytes when name actually changes.
|
|
427
|
-
def rewrite_name_for_mv!(mentry, new_path, new_key)
|
|
428
|
-
strategy = Entry.for_format(mentry.format)
|
|
429
|
-
raw = File.binread(new_path)
|
|
430
|
-
parsed = strategy.parse(raw, path: new_path)
|
|
431
|
-
basename = new_key.split(".").last
|
|
432
|
-
|
|
433
|
-
case mentry.format
|
|
434
|
-
when "markdown"
|
|
435
|
-
fm = parsed["frontmatter"] || {}
|
|
436
|
-
return unless fm.is_a?(Hash) && fm["name"].is_a?(String) && fm["name"] != basename
|
|
437
|
-
|
|
438
|
-
fm = fm.merge("name" => basename)
|
|
439
|
-
File.binwrite(new_path, strategy.serialize(frontmatter: fm, body: parsed["body"]))
|
|
440
|
-
when "json", "yaml"
|
|
441
|
-
content = parsed["content"]
|
|
442
|
-
return unless content.is_a?(Hash) && content["_meta"].is_a?(Hash) &&
|
|
443
|
-
content["_meta"]["name"].is_a?(String) && content["_meta"]["name"] != basename
|
|
444
|
-
|
|
445
|
-
meta = content["_meta"].merge("name" => basename)
|
|
446
|
-
content = { "_meta" => meta }.merge(content.except("_meta"))
|
|
447
|
-
File.binwrite(new_path, strategy.serialize(frontmatter: {}, body: "", content: content))
|
|
448
|
-
end
|
|
449
|
-
end
|
|
450
|
-
|
|
451
224
|
def existing_uid_for(mentry, path)
|
|
452
225
|
return nil unless File.exist?(path)
|
|
453
226
|
|
|
454
227
|
raw = File.binread(path)
|
|
455
228
|
parsed = Entry.for_format(mentry.format).parse(raw, path: path)
|
|
456
|
-
extract_uid(
|
|
229
|
+
extract_uid(parsed["_meta"])
|
|
457
230
|
rescue StandardError
|
|
458
231
|
nil
|
|
459
232
|
end
|
|
460
233
|
|
|
461
234
|
# Ensures the payload carries a uid: preserve existing, else mint.
|
|
462
|
-
# Returns [
|
|
463
|
-
def ensure_uid(format,
|
|
235
|
+
# Returns [meta, content] possibly mutated.
|
|
236
|
+
def ensure_uid(format, meta, content, existing_uid)
|
|
464
237
|
case format
|
|
465
|
-
when "markdown"
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
[
|
|
469
|
-
when "json", "yaml"
|
|
470
|
-
c = content.is_a?(Hash) ? content.dup : nil
|
|
471
|
-
if c
|
|
472
|
-
meta = c["_meta"].is_a?(Hash) ? c["_meta"].dup : {}
|
|
473
|
-
meta["uid"] = existing_uid || Store.mint_uid if !meta["uid"].is_a?(String) || meta["uid"].empty?
|
|
474
|
-
# Keep _meta first for etag stability.
|
|
475
|
-
c = { "_meta" => meta }.merge(c.except("_meta"))
|
|
476
|
-
end
|
|
477
|
-
[frontmatter, c]
|
|
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]
|
|
478
242
|
else
|
|
479
243
|
# text: no uid channel
|
|
480
|
-
[
|
|
244
|
+
[meta, content]
|
|
481
245
|
end
|
|
482
246
|
end
|
|
483
247
|
|
|
484
|
-
def
|
|
485
|
-
|
|
486
|
-
|
|
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"]
|
|
487
251
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
if
|
|
491
|
-
File.join(@root, "zones", mentry.path + primary_ext)
|
|
492
|
-
else
|
|
493
|
-
File.join(@root, "zones", mentry.path)
|
|
494
|
-
end
|
|
495
|
-
end
|
|
252
|
+
ext = Entry.for_format(format).extensions.first
|
|
253
|
+
basename = File.basename(path, ext)
|
|
254
|
+
return if meta["name"] == basename
|
|
496
255
|
|
|
497
|
-
|
|
498
|
-
Array(gen["sources"]).each do |src|
|
|
499
|
-
if src.match?(/\A[a-z0-9.][a-z0-9._-]*\z/) && !src.include?("/")
|
|
500
|
-
@manifest.enumerate(prefix: src).each do |row|
|
|
501
|
-
return src if File.mtime(row[:path]) > gen_time
|
|
502
|
-
end
|
|
503
|
-
else
|
|
504
|
-
abs = File.absolute_path?(src) ? src : File.join(File.dirname(@root), src)
|
|
505
|
-
if File.directory?(abs)
|
|
506
|
-
Dir.glob(File.join(abs, "**", "*")).each do |fp|
|
|
507
|
-
next unless File.file?(fp)
|
|
508
|
-
return src if File.mtime(fp) > gen_time
|
|
509
|
-
end
|
|
510
|
-
elsif File.exist?(abs)
|
|
511
|
-
return src if File.mtime(abs) > gen_time
|
|
512
|
-
end
|
|
513
|
-
end
|
|
514
|
-
end
|
|
515
|
-
nil
|
|
516
|
-
end
|
|
517
|
-
|
|
518
|
-
def parse_ttl(s)
|
|
519
|
-
return nil unless s
|
|
520
|
-
|
|
521
|
-
m = s.to_s.match(/\A(\d+)([smhd])\z/) or return nil
|
|
522
|
-
n = m[1].to_i
|
|
523
|
-
case m[2]
|
|
524
|
-
when "s" then n
|
|
525
|
-
when "m" then n * 60
|
|
526
|
-
when "h" then n * 3600
|
|
527
|
-
when "d" then n * 86_400
|
|
528
|
-
end
|
|
256
|
+
raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
|
|
529
257
|
end
|
|
530
258
|
|
|
531
|
-
def
|
|
532
|
-
{ "key" => mentry.key, "path" => path, "action" => mentry.action, "reason" => reason }
|
|
533
|
-
end
|
|
534
|
-
|
|
535
|
-
def stale_row(mentry, path, reason)
|
|
536
|
-
{
|
|
537
|
-
"key" => mentry.key,
|
|
538
|
-
"path" => path,
|
|
539
|
-
"generator" => mentry.generator,
|
|
540
|
-
"reason" => reason,
|
|
541
|
-
}
|
|
542
|
-
end
|
|
543
|
-
|
|
544
|
-
def enforce_name_match!(path, fm, format)
|
|
545
|
-
# Name<->basename check only meaningful for markdown frontmatter.
|
|
546
|
-
return unless format == "markdown"
|
|
547
|
-
|
|
548
|
-
basename = File.basename(path, ".md")
|
|
549
|
-
return unless fm.is_a?(Hash) && fm["name"] && fm["name"] != basename
|
|
550
|
-
|
|
551
|
-
raise BadFrontmatter.new(path, "frontmatter name '#{fm["name"]}' does not match basename '#{basename}'")
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
def serialize_for_put(mentry:, path:, strategy:, frontmatter:, body:, content:)
|
|
259
|
+
def serialize_for_put(mentry:, path:, strategy:, meta:, body:, content:)
|
|
555
260
|
case mentry.format
|
|
556
261
|
when "markdown", "text"
|
|
557
|
-
bytes = strategy.serialize(
|
|
558
|
-
[bytes,
|
|
262
|
+
bytes = strategy.serialize(meta: meta, body: body.to_s)
|
|
263
|
+
[bytes, meta, body.to_s, nil]
|
|
559
264
|
when "json", "yaml"
|
|
560
265
|
raise UsageError.new("put for #{mentry.format} requires content: or body:") if content.nil? && (body.nil? || body.to_s.empty?)
|
|
561
266
|
|
|
@@ -566,13 +271,12 @@ module Textus
|
|
|
566
271
|
rescue BadFrontmatter => e
|
|
567
272
|
raise BadContent.new(path, "bad_content: #{e.message}")
|
|
568
273
|
end
|
|
274
|
+
eff_meta = parsed["_meta"]
|
|
569
275
|
eff_content = parsed["content"]
|
|
570
|
-
|
|
571
|
-
[body.to_s, eff_fm, body.to_s, eff_content]
|
|
276
|
+
[body.to_s, eff_meta, body.to_s, eff_content]
|
|
572
277
|
else
|
|
573
|
-
bytes = strategy.serialize(
|
|
574
|
-
|
|
575
|
-
[bytes, eff_fm, bytes, content]
|
|
278
|
+
bytes = strategy.serialize(meta: meta, body: "", content: content)
|
|
279
|
+
[bytes, meta, bytes, content]
|
|
576
280
|
end
|
|
577
281
|
else
|
|
578
282
|
raise UsageError.new("unknown format #{mentry.format.inspect}")
|
|
@@ -580,7 +284,7 @@ module Textus
|
|
|
580
284
|
end
|
|
581
285
|
|
|
582
286
|
# rubocop:disable Metrics/ParameterLists
|
|
583
|
-
def build_envelope(key, mentry, path,
|
|
287
|
+
def build_envelope(key, mentry, path, meta, body, etag, content: nil)
|
|
584
288
|
# rubocop:enable Metrics/ParameterLists
|
|
585
289
|
env = {
|
|
586
290
|
"protocol" => PROTOCOL,
|
|
@@ -589,29 +293,20 @@ module Textus
|
|
|
589
293
|
"owner" => mentry.owner,
|
|
590
294
|
"path" => path,
|
|
591
295
|
"format" => mentry.format,
|
|
592
|
-
"
|
|
296
|
+
"_meta" => meta,
|
|
593
297
|
"body" => body,
|
|
594
298
|
"etag" => etag,
|
|
595
299
|
"schema_ref" => mentry.schema,
|
|
596
|
-
"uid" => extract_uid(
|
|
300
|
+
"uid" => extract_uid(meta),
|
|
597
301
|
}
|
|
598
302
|
env["content"] = content unless content.nil?
|
|
599
303
|
env
|
|
600
304
|
end
|
|
601
305
|
|
|
602
|
-
# Pull a Textus UID out of the
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
when "markdown"
|
|
607
|
-
v = fm.is_a?(Hash) ? fm["uid"] : nil
|
|
608
|
-
v.is_a?(String) ? v : nil
|
|
609
|
-
when "json", "yaml"
|
|
610
|
-
meta = content.is_a?(Hash) ? content["_meta"] : nil
|
|
611
|
-
v = meta.is_a?(Hash) ? meta["uid"] : nil
|
|
612
|
-
v.is_a?(String) ? v : nil
|
|
613
|
-
end
|
|
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
|
|
614
310
|
end
|
|
615
311
|
end
|
|
616
|
-
# rubocop:enable Metrics/ClassLength
|
|
617
312
|
end
|
data/lib/textus/version.rb
CHANGED
data/lib/textus.rb
CHANGED
|
@@ -12,8 +12,13 @@ require_relative "textus/entry/text"
|
|
|
12
12
|
require_relative "textus/entry"
|
|
13
13
|
require_relative "textus/schema"
|
|
14
14
|
require_relative "textus/key_distance"
|
|
15
|
+
require_relative "textus/manifest_entry"
|
|
15
16
|
require_relative "textus/manifest"
|
|
16
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"
|
|
17
22
|
require_relative "textus/store"
|
|
18
23
|
require_relative "textus/store_view"
|
|
19
24
|
require_relative "textus/refresh"
|
|
@@ -26,6 +31,39 @@ require_relative "textus/proposal"
|
|
|
26
31
|
require_relative "textus/init"
|
|
27
32
|
require_relative "textus/schema_tools"
|
|
28
33
|
require_relative "textus/migrate_keys"
|
|
34
|
+
require_relative "textus/migrate_v2"
|
|
29
35
|
require_relative "textus/doctor"
|
|
30
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"
|
|
31
69
|
require_relative "textus/cli"
|
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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -99,6 +99,37 @@ files:
|
|
|
99
99
|
- lib/textus/builder.rb
|
|
100
100
|
- lib/textus/builtin_actions.rb
|
|
101
101
|
- 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
|
+
- lib/textus/cli/group.rb
|
|
113
|
+
- lib/textus/cli/init.rb
|
|
114
|
+
- lib/textus/cli/intro.rb
|
|
115
|
+
- lib/textus/cli/key_group.rb
|
|
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
|
|
131
|
+
- lib/textus/cli/verb.rb
|
|
132
|
+
- lib/textus/cli/where.rb
|
|
102
133
|
- lib/textus/dependencies.rb
|
|
103
134
|
- lib/textus/doctor.rb
|
|
104
135
|
- lib/textus/entry.rb
|
|
@@ -114,7 +145,9 @@ files:
|
|
|
114
145
|
- lib/textus/intro.rb
|
|
115
146
|
- lib/textus/key_distance.rb
|
|
116
147
|
- lib/textus/manifest.rb
|
|
148
|
+
- lib/textus/manifest_entry.rb
|
|
117
149
|
- lib/textus/migrate_keys.rb
|
|
150
|
+
- lib/textus/migrate_v2.rb
|
|
118
151
|
- lib/textus/mustache.rb
|
|
119
152
|
- lib/textus/projection.rb
|
|
120
153
|
- lib/textus/proposal.rb
|
|
@@ -124,6 +157,10 @@ files:
|
|
|
124
157
|
- lib/textus/schema.rb
|
|
125
158
|
- lib/textus/schema_tools.rb
|
|
126
159
|
- lib/textus/store.rb
|
|
160
|
+
- lib/textus/store/events.rb
|
|
161
|
+
- lib/textus/store/mover.rb
|
|
162
|
+
- lib/textus/store/staleness.rb
|
|
163
|
+
- lib/textus/store/validator.rb
|
|
127
164
|
- lib/textus/store_view.rb
|
|
128
165
|
- lib/textus/version.rb
|
|
129
166
|
homepage: https://github.com/patrick204nqh/textus
|