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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +147 -2
- data/README.md +38 -28
- data/SPEC.md +84 -147
- data/docs/architecture.md +82 -28
- 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/group.rb +51 -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/verb.rb +62 -0
- data/lib/textus/cli.rb +44 -385
- 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 +29 -264
- data/lib/textus/entry/base.rb +30 -0
- data/lib/textus/entry/json.rb +11 -5
- data/lib/textus/entry/markdown.rb +5 -5
- data/lib/textus/entry/text.rb +4 -4
- data/lib/textus/entry/yaml.rb +11 -5
- data/lib/textus/entry.rb +2 -7
- data/lib/textus/envelope.rb +30 -0
- data/lib/textus/errors.rb +2 -2
- 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 +14 -11
- data/lib/textus/intro.rb +16 -18
- 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 +20 -254
- data/lib/textus/migrate_keys.rb +1 -1
- data/lib/textus/projection.rb +6 -5
- data/lib/textus/proposal.rb +4 -4
- data/lib/textus/refresh.rb +17 -17
- data/lib/textus/schema/tools.rb +89 -0
- data/lib/textus/store/audit_log.rb +71 -0
- data/lib/textus/store/mover.rb +121 -0
- data/lib/textus/store/reader.rb +67 -0
- data/lib/textus/store/staleness.rb +133 -0
- data/lib/textus/store/validator.rb +56 -0
- data/lib/textus/store/view.rb +29 -0
- data/lib/textus/store/writer.rb +132 -0
- data/lib/textus/store.rb +26 -527
- data/lib/textus/version.rb +2 -2
- data/lib/textus.rb +14 -29
- metadata +78 -8
- data/lib/textus/audit_log.rb +0 -32
- data/lib/textus/builtin_actions.rb +0 -68
- 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/schema_tools.rb +0 -87
- data/lib/textus/store_view.rb +0 -27
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
class Store
|
|
5
|
+
# rubocop:disable Metrics/ParameterLists
|
|
6
|
+
class Writer
|
|
7
|
+
def initialize(store)
|
|
8
|
+
@store = store
|
|
9
|
+
@manifest = store.manifest
|
|
10
|
+
@reader = store.reader
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def put(key, meta: nil, body: nil, content: nil, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
|
|
14
|
+
@manifest.validate_key!(key)
|
|
15
|
+
mentry, path, = @manifest.resolve(key)
|
|
16
|
+
writers = @manifest.zone_writers(mentry.zone)
|
|
17
|
+
raise WriteForbidden.new(key, mentry.zone, writers: writers) unless writers.include?(as)
|
|
18
|
+
|
|
19
|
+
meta ||= {}
|
|
20
|
+
strategy = Entry.for_format(mentry.format)
|
|
21
|
+
|
|
22
|
+
existing_uid = existing_uid_for(mentry, path)
|
|
23
|
+
meta, content = ensure_uid(mentry.format, meta, content, existing_uid)
|
|
24
|
+
|
|
25
|
+
bytes, eff_meta, eff_body, eff_content = serialize_for_put(
|
|
26
|
+
mentry: mentry, path: path, strategy: strategy,
|
|
27
|
+
meta: meta, body: body, content: content
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
enforce_name_match!(path, eff_meta, mentry.format)
|
|
31
|
+
|
|
32
|
+
schema = @store.schema_for(mentry.schema)
|
|
33
|
+
if schema
|
|
34
|
+
Entry.for_format(mentry.format).validate_against(
|
|
35
|
+
schema,
|
|
36
|
+
{ "_meta" => eff_meta, "content" => eff_content },
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
etag_before = File.exist?(path) ? Etag.for_file(path) : nil
|
|
41
|
+
raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && (etag_before != if_etag)
|
|
42
|
+
|
|
43
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
44
|
+
File.binwrite(path, bytes)
|
|
45
|
+
etag_after = Etag.for_bytes(bytes)
|
|
46
|
+
@store.audit_log.append(role: as, verb: "put", key: key, etag_before: etag_before, etag_after: etag_after)
|
|
47
|
+
envelope = Envelope.build(
|
|
48
|
+
key: key, mentry: mentry, path: path,
|
|
49
|
+
meta: eff_meta, body: eff_body, etag: etag_after, content: eff_content
|
|
50
|
+
)
|
|
51
|
+
@store.fire_event(:put, key: key, envelope: envelope) unless suppress_events
|
|
52
|
+
envelope
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def existing_uid_for(mentry, path)
|
|
56
|
+
return nil unless File.exist?(path)
|
|
57
|
+
|
|
58
|
+
raw = File.binread(path)
|
|
59
|
+
parsed = Entry.for_format(mentry.format).parse(raw, path: path)
|
|
60
|
+
Envelope.extract_uid(parsed["_meta"])
|
|
61
|
+
rescue StandardError
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def ensure_uid(format, meta, content, existing_uid)
|
|
66
|
+
case format
|
|
67
|
+
when "markdown", "json", "yaml"
|
|
68
|
+
m = meta.is_a?(Hash) ? meta.dup : {}
|
|
69
|
+
m["uid"] = existing_uid || Store.mint_uid unless m["uid"].is_a?(String) && !m["uid"].empty?
|
|
70
|
+
[m, content]
|
|
71
|
+
else
|
|
72
|
+
[meta, content]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def enforce_name_match!(path, meta, format)
|
|
77
|
+
return unless %w[markdown json yaml].include?(format)
|
|
78
|
+
return unless meta.is_a?(Hash) && meta["name"]
|
|
79
|
+
|
|
80
|
+
ext = Entry.for_format(format).extensions.first
|
|
81
|
+
basename = File.basename(path, ext)
|
|
82
|
+
return if meta["name"] == basename
|
|
83
|
+
|
|
84
|
+
raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def serialize_for_put(mentry:, path:, strategy:, meta:, body:, content:)
|
|
88
|
+
case mentry.format
|
|
89
|
+
when "markdown", "text"
|
|
90
|
+
bytes = strategy.serialize(meta: meta, body: body.to_s)
|
|
91
|
+
[bytes, meta, body.to_s, nil]
|
|
92
|
+
when "json", "yaml"
|
|
93
|
+
raise UsageError.new("put for #{mentry.format} requires content: or body:") if content.nil? && (body.nil? || body.to_s.empty?)
|
|
94
|
+
|
|
95
|
+
if content.nil?
|
|
96
|
+
begin
|
|
97
|
+
parsed = strategy.parse(body.to_s, path: path)
|
|
98
|
+
rescue BadFrontmatter => e
|
|
99
|
+
raise BadContent.new(path, "bad_content: #{e.message}")
|
|
100
|
+
end
|
|
101
|
+
[body.to_s, parsed["_meta"], body.to_s, parsed["content"]]
|
|
102
|
+
else
|
|
103
|
+
bytes = strategy.serialize(meta: meta, body: "", content: content)
|
|
104
|
+
[bytes, meta, bytes, content]
|
|
105
|
+
end
|
|
106
|
+
else
|
|
107
|
+
raise UsageError.new("unknown format #{mentry.format.inspect}")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def delete(key, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
|
|
112
|
+
mentry, path, = @manifest.resolve(key)
|
|
113
|
+
writers = @manifest.zone_writers(mentry.zone)
|
|
114
|
+
raise WriteForbidden.new(key, mentry.zone, writers: writers) unless writers.include?(as)
|
|
115
|
+
raise UnknownKey.new(key, suggestions: @manifest.suggestions_for(key)) unless File.exist?(path)
|
|
116
|
+
|
|
117
|
+
etag_before = Etag.for_file(path)
|
|
118
|
+
raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && if_etag != etag_before
|
|
119
|
+
|
|
120
|
+
File.delete(path)
|
|
121
|
+
@store.audit_log.append(role: as, verb: "delete", key: key, etag_before: etag_before, etag_after: nil)
|
|
122
|
+
@store.fire_event(:delete, key: key) unless suppress_events
|
|
123
|
+
{ "protocol" => PROTOCOL, "ok" => true, "key" => key, "deleted" => true }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def accept(key, as:)
|
|
127
|
+
Proposal.accept(@store, key, as: as)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
# rubocop:enable Metrics/ParameterLists
|
|
131
|
+
end
|
|
132
|
+
end
|