textus 0.4.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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +147 -2
  3. data/README.md +38 -28
  4. data/SPEC.md +84 -147
  5. data/docs/architecture.md +82 -28
  6. data/lib/textus/builder/pipeline.rb +56 -0
  7. data/lib/textus/builder/renderer/json.rb +42 -0
  8. data/lib/textus/builder/renderer/markdown.rb +22 -0
  9. data/lib/textus/builder/renderer/text.rb +14 -0
  10. data/lib/textus/builder/renderer/yaml.rb +42 -0
  11. data/lib/textus/builder/renderer.rb +17 -0
  12. data/lib/textus/builder.rb +9 -114
  13. data/lib/textus/cli/group/hook.rb +11 -0
  14. data/lib/textus/cli/group/key.rb +12 -0
  15. data/lib/textus/cli/group/schema.rb +13 -0
  16. data/lib/textus/cli/group.rb +51 -0
  17. data/lib/textus/cli/verb/accept.rb +15 -0
  18. data/lib/textus/cli/verb/build.rb +13 -0
  19. data/lib/textus/cli/verb/delete.rb +16 -0
  20. data/lib/textus/cli/verb/deps.rb +12 -0
  21. data/lib/textus/cli/verb/doctor.rb +15 -0
  22. data/lib/textus/cli/verb/get.rb +12 -0
  23. data/lib/textus/cli/verb/hook_run.rb +48 -0
  24. data/lib/textus/cli/verb/hooks.rb +50 -0
  25. data/lib/textus/cli/verb/init.rb +14 -0
  26. data/lib/textus/cli/verb/intro.rb +11 -0
  27. data/lib/textus/cli/verb/list.rb +14 -0
  28. data/lib/textus/cli/verb/migrate_keys.rb +16 -0
  29. data/lib/textus/cli/verb/mv.rb +17 -0
  30. data/lib/textus/cli/verb/published.rb +11 -0
  31. data/lib/textus/cli/verb/put.rb +50 -0
  32. data/lib/textus/cli/verb/rdeps.rb +12 -0
  33. data/lib/textus/cli/verb/refresh.rb +15 -0
  34. data/lib/textus/cli/verb/schema.rb +12 -0
  35. data/lib/textus/cli/verb/schema_diff.rb +12 -0
  36. data/lib/textus/cli/verb/schema_init.rb +16 -0
  37. data/lib/textus/cli/verb/schema_migrate.rb +16 -0
  38. data/lib/textus/cli/verb/stale.rb +14 -0
  39. data/lib/textus/cli/verb/uid.rb +12 -0
  40. data/lib/textus/cli/verb/where.rb +12 -0
  41. data/lib/textus/cli/verb.rb +62 -0
  42. data/lib/textus/cli.rb +44 -385
  43. data/lib/textus/doctor/check/audit_log.rb +50 -0
  44. data/lib/textus/doctor/check/hooks.rb +29 -0
  45. data/lib/textus/doctor/check/illegal_keys.rb +49 -0
  46. data/lib/textus/doctor/check/manifest_files.rb +38 -0
  47. data/lib/textus/doctor/check/schema_violations.rb +22 -0
  48. data/lib/textus/doctor/check/schemas.rb +26 -0
  49. data/lib/textus/doctor/check/sentinels.rb +57 -0
  50. data/lib/textus/doctor/check/templates.rb +26 -0
  51. data/lib/textus/doctor/check/unowned_schema_fields.rb +34 -0
  52. data/lib/textus/doctor/check.rb +30 -0
  53. data/lib/textus/doctor.rb +29 -264
  54. data/lib/textus/entry/base.rb +30 -0
  55. data/lib/textus/entry/json.rb +11 -5
  56. data/lib/textus/entry/markdown.rb +5 -5
  57. data/lib/textus/entry/text.rb +4 -4
  58. data/lib/textus/entry/yaml.rb +11 -5
  59. data/lib/textus/entry.rb +2 -7
  60. data/lib/textus/envelope.rb +30 -0
  61. data/lib/textus/errors.rb +2 -2
  62. data/lib/textus/hooks/builtin.rb +70 -0
  63. data/lib/textus/hooks/dispatcher.rb +49 -0
  64. data/lib/textus/hooks/loader.rb +26 -0
  65. data/lib/textus/hooks/registry.rb +73 -0
  66. data/lib/textus/init.rb +14 -11
  67. data/lib/textus/intro.rb +16 -18
  68. data/lib/textus/key/distance.rb +55 -0
  69. data/lib/textus/key/grammar.rb +33 -0
  70. data/lib/textus/key/path.rb +17 -0
  71. data/lib/textus/manifest/entry.rb +199 -0
  72. data/lib/textus/manifest.rb +20 -254
  73. data/lib/textus/migrate_keys.rb +1 -1
  74. data/lib/textus/projection.rb +6 -5
  75. data/lib/textus/proposal.rb +4 -4
  76. data/lib/textus/refresh.rb +17 -17
  77. data/lib/textus/schema/tools.rb +89 -0
  78. data/lib/textus/store/audit_log.rb +71 -0
  79. data/lib/textus/store/mover.rb +121 -0
  80. data/lib/textus/store/reader.rb +67 -0
  81. data/lib/textus/store/staleness.rb +133 -0
  82. data/lib/textus/store/validator.rb +56 -0
  83. data/lib/textus/store/view.rb +29 -0
  84. data/lib/textus/store/writer.rb +132 -0
  85. data/lib/textus/store.rb +26 -527
  86. data/lib/textus/version.rb +2 -2
  87. data/lib/textus.rb +14 -29
  88. metadata +78 -8
  89. data/lib/textus/audit_log.rb +0 -32
  90. data/lib/textus/builtin_actions.rb +0 -68
  91. data/lib/textus/extension_registry.rb +0 -61
  92. data/lib/textus/extensions.rb +0 -33
  93. data/lib/textus/key_distance.rb +0 -53
  94. data/lib/textus/schema_tools.rb +0 -87
  95. data/lib/textus/store_view.rb +0 -27
@@ -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,11 @@
1
+ module Textus
2
+ class CLI
3
+ class Verb
4
+ class Published < Verb
5
+ def call(store)
6
+ emit({ "published" => store.published })
7
+ end
8
+ end
9
+ end
10
+ end
11
+ 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
@@ -0,0 +1,12 @@
1
+ module Textus
2
+ class CLI
3
+ class Verb
4
+ class Rdeps < Verb
5
+ def call(store)
6
+ key = positional.shift or raise UsageError.new("rdeps requires a key")
7
+ emit({ "key" => key, "rdeps" => store.rdeps(key) })
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ module Textus
2
+ class CLI
3
+ class Verb
4
+ class Refresh < Verb
5
+ option :as_flag, "--as=ROLE"
6
+
7
+ def call(store)
8
+ key = positional.shift or raise UsageError.new("refresh requires a key")
9
+ role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
10
+ emit(Textus::Refresh.call(store, key, as: role))
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module Textus
2
+ class CLI
3
+ class Verb
4
+ class Schema < Verb
5
+ def call(store)
6
+ key = positional.shift or raise UsageError.new("schema requires a key")
7
+ emit(store.schema_envelope(key))
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module Textus
2
+ class CLI
3
+ class Verb
4
+ class SchemaDiff < Verb
5
+ def call(store)
6
+ name = positional.shift or raise UsageError.new("schema diff NAME")
7
+ emit(Textus::Schema::Tools.diff(store, name: name))
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ module Textus
2
+ class CLI
3
+ class Verb
4
+ class SchemaInit < Verb
5
+ option :from_key, "--from=KEY"
6
+
7
+ def call(store)
8
+ name = positional.shift or raise UsageError.new("schema init NAME")
9
+ raise UsageError.new("schema init requires --from=KEY") unless from_key
10
+
11
+ emit(Textus::Schema::Tools.init(store, name: name, from: from_key))
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Textus
2
+ class CLI
3
+ class Verb
4
+ class SchemaMigrate < Verb
5
+ option :rename, "--rename=O:N"
6
+
7
+ def call(store)
8
+ name = positional.shift or raise UsageError.new("schema migrate NAME")
9
+ raise UsageError.new("schema migrate requires --rename=OLD:NEW") unless rename
10
+
11
+ emit(Textus::Schema::Tools.migrate(store, name: name, rename: rename))
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ module Textus
2
+ class CLI
3
+ class Verb
4
+ class Stale < Verb
5
+ option :prefix, "--prefix=KEY"
6
+ option :zone, "--zone=Z"
7
+
8
+ def call(store)
9
+ emit(store.stale(prefix: prefix, zone: zone))
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ module Textus
2
+ class CLI
3
+ class Verb
4
+ class Uid < Verb
5
+ def call(store)
6
+ key = positional.shift or raise UsageError.new("uid requires a key")
7
+ emit({ "key" => key, "uid" => store.uid(key) })
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module Textus
2
+ class CLI
3
+ class Verb
4
+ class Where < Verb
5
+ def call(store)
6
+ key = positional.shift or raise UsageError.new("where requires a key")
7
+ emit(store.where(key))
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,62 @@
1
+ require "json"
2
+ require "optparse"
3
+
4
+ module Textus
5
+ class CLI
6
+ # Subclasses must implement #call(store) and return an integer exit code.
7
+ # Use #emit(obj) for normal JSON output (returns 0).
8
+ # Subclasses that don't need a Textus store (e.g. Init) override
9
+ # `.needs_store?` to return false; dispatch will pass nil instead.
10
+ class Verb
11
+ class << self
12
+ def option(name, optspec)
13
+ options << [name, optspec]
14
+ attr_accessor(name)
15
+ end
16
+
17
+ def options
18
+ @options ||= []
19
+ end
20
+
21
+ def needs_store?
22
+ true
23
+ end
24
+
25
+ def inherited(subclass)
26
+ super
27
+ subclass.instance_variable_set(:@options, [])
28
+ end
29
+ end
30
+
31
+ def initialize(stdin:, stdout:, stderr:, cwd: nil)
32
+ @stdin = stdin
33
+ @stdout = stdout
34
+ @stderr = stderr
35
+ @cwd = cwd
36
+ end
37
+
38
+ def parse(argv)
39
+ fmt = "json"
40
+ OptionParser.new do |o|
41
+ self.class.options.each do |name, optspec|
42
+ o.on(optspec) { |v| public_send(:"#{name}=", v) }
43
+ end
44
+ o.on("--format=FMT") { |v| fmt = v }
45
+ end.permute!(argv)
46
+ raise UsageError.new("only --format=json is supported in v1") unless fmt == "json"
47
+
48
+ @positional = argv.dup
49
+ end
50
+
51
+ attr_reader :positional
52
+
53
+ # Hashes get "protocol" => PROTOCOL prepended unless they already
54
+ # carry one (Store envelopes do). Caller's value wins on collision.
55
+ def emit(obj, exit_code: 0)
56
+ payload = obj.is_a?(Hash) ? { "protocol" => PROTOCOL }.merge(obj) : obj
57
+ @stdout.puts(JSON.generate(payload))
58
+ exit_code
59
+ end
60
+ end
61
+ end
62
+ end