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.
Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +83 -1
  3. data/README.md +29 -21
  4. data/SPEC.md +75 -142
  5. data/docs/architecture.md +42 -23
  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/verb/accept.rb +15 -0
  17. data/lib/textus/cli/verb/build.rb +13 -0
  18. data/lib/textus/cli/verb/delete.rb +16 -0
  19. data/lib/textus/cli/verb/deps.rb +12 -0
  20. data/lib/textus/cli/verb/doctor.rb +15 -0
  21. data/lib/textus/cli/verb/get.rb +12 -0
  22. data/lib/textus/cli/verb/hook_run.rb +48 -0
  23. data/lib/textus/cli/verb/hooks.rb +50 -0
  24. data/lib/textus/cli/verb/init.rb +14 -0
  25. data/lib/textus/cli/verb/intro.rb +11 -0
  26. data/lib/textus/cli/verb/list.rb +14 -0
  27. data/lib/textus/cli/verb/migrate_keys.rb +16 -0
  28. data/lib/textus/cli/verb/mv.rb +17 -0
  29. data/lib/textus/cli/verb/published.rb +11 -0
  30. data/lib/textus/cli/verb/put.rb +50 -0
  31. data/lib/textus/cli/verb/rdeps.rb +12 -0
  32. data/lib/textus/cli/verb/refresh.rb +15 -0
  33. data/lib/textus/cli/verb/schema.rb +12 -0
  34. data/lib/textus/cli/verb/schema_diff.rb +12 -0
  35. data/lib/textus/cli/verb/schema_init.rb +16 -0
  36. data/lib/textus/cli/verb/schema_migrate.rb +16 -0
  37. data/lib/textus/cli/verb/stale.rb +14 -0
  38. data/lib/textus/cli/verb/uid.rb +12 -0
  39. data/lib/textus/cli/verb/where.rb +12 -0
  40. data/lib/textus/cli.rb +23 -42
  41. data/lib/textus/doctor/check/audit_log.rb +50 -0
  42. data/lib/textus/doctor/check/hooks.rb +29 -0
  43. data/lib/textus/doctor/check/illegal_keys.rb +49 -0
  44. data/lib/textus/doctor/check/manifest_files.rb +38 -0
  45. data/lib/textus/doctor/check/schema_violations.rb +22 -0
  46. data/lib/textus/doctor/check/schemas.rb +26 -0
  47. data/lib/textus/doctor/check/sentinels.rb +57 -0
  48. data/lib/textus/doctor/check/templates.rb +26 -0
  49. data/lib/textus/doctor/check/unowned_schema_fields.rb +34 -0
  50. data/lib/textus/doctor/check.rb +30 -0
  51. data/lib/textus/doctor.rb +22 -288
  52. data/lib/textus/entry/base.rb +30 -0
  53. data/lib/textus/entry/json.rb +5 -1
  54. data/lib/textus/entry/markdown.rb +1 -1
  55. data/lib/textus/entry/text.rb +1 -1
  56. data/lib/textus/entry/yaml.rb +5 -1
  57. data/lib/textus/entry.rb +0 -5
  58. data/lib/textus/envelope.rb +30 -0
  59. data/lib/textus/hooks/builtin.rb +70 -0
  60. data/lib/textus/hooks/dispatcher.rb +49 -0
  61. data/lib/textus/hooks/loader.rb +26 -0
  62. data/lib/textus/hooks/registry.rb +73 -0
  63. data/lib/textus/init.rb +13 -10
  64. data/lib/textus/intro.rb +14 -16
  65. data/lib/textus/key/distance.rb +55 -0
  66. data/lib/textus/key/grammar.rb +33 -0
  67. data/lib/textus/key/path.rb +17 -0
  68. data/lib/textus/manifest/entry.rb +199 -0
  69. data/lib/textus/manifest.rb +10 -34
  70. data/lib/textus/migrate_keys.rb +1 -1
  71. data/lib/textus/projection.rb +5 -4
  72. data/lib/textus/proposal.rb +1 -1
  73. data/lib/textus/refresh.rb +11 -11
  74. data/lib/textus/schema/tools.rb +89 -0
  75. data/lib/textus/store/audit_log.rb +71 -0
  76. data/lib/textus/store/mover.rb +19 -16
  77. data/lib/textus/store/reader.rb +67 -0
  78. data/lib/textus/store/staleness.rb +10 -19
  79. data/lib/textus/store/validator.rb +11 -8
  80. data/lib/textus/store/view.rb +29 -0
  81. data/lib/textus/store/writer.rb +132 -0
  82. data/lib/textus/store.rb +25 -221
  83. data/lib/textus/version.rb +1 -1
  84. data/lib/textus.rb +14 -67
  85. metadata +73 -40
  86. data/lib/textus/audit_log.rb +0 -67
  87. data/lib/textus/builtin_actions.rb +0 -68
  88. data/lib/textus/cli/accept.rb +0 -13
  89. data/lib/textus/cli/action.rb +0 -51
  90. data/lib/textus/cli/build.rb +0 -11
  91. data/lib/textus/cli/delete.rb +0 -14
  92. data/lib/textus/cli/deprecated_alias.rb +0 -31
  93. data/lib/textus/cli/deps.rb +0 -10
  94. data/lib/textus/cli/doctor.rb +0 -13
  95. data/lib/textus/cli/extension_group.rb +0 -9
  96. data/lib/textus/cli/extensions.rb +0 -49
  97. data/lib/textus/cli/get.rb +0 -10
  98. data/lib/textus/cli/init.rb +0 -12
  99. data/lib/textus/cli/intro.rb +0 -9
  100. data/lib/textus/cli/key_group.rb +0 -10
  101. data/lib/textus/cli/list.rb +0 -12
  102. data/lib/textus/cli/migrate.rb +0 -41
  103. data/lib/textus/cli/migrate_keys.rb +0 -19
  104. data/lib/textus/cli/mv.rb +0 -20
  105. data/lib/textus/cli/published.rb +0 -9
  106. data/lib/textus/cli/put.rb +0 -48
  107. data/lib/textus/cli/rdeps.rb +0 -10
  108. data/lib/textus/cli/refresh.rb +0 -13
  109. data/lib/textus/cli/schema.rb +0 -10
  110. data/lib/textus/cli/schema_diff.rb +0 -15
  111. data/lib/textus/cli/schema_group.rb +0 -33
  112. data/lib/textus/cli/schema_init.rb +0 -19
  113. data/lib/textus/cli/schema_migrate.rb +0 -19
  114. data/lib/textus/cli/stale.rb +0 -12
  115. data/lib/textus/cli/uid.rb +0 -15
  116. data/lib/textus/cli/where.rb +0 -10
  117. data/lib/textus/extension_registry.rb +0 -61
  118. data/lib/textus/extensions.rb +0 -33
  119. data/lib/textus/key_distance.rb +0 -53
  120. data/lib/textus/manifest_entry.rb +0 -185
  121. data/lib/textus/migrate_v2.rb +0 -27
  122. data/lib/textus/schema_tools.rb +0 -87
  123. data/lib/textus/store/events.rb +0 -31
  124. 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
- @registry = ExtensionRegistry.new
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
- BuiltinActions.register_all
50
- dir = File.join(@root, "extensions")
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
- mentry, path, = @manifest.resolve(key)
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
- mentry, path, = @manifest.resolve(key)
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 list(prefix: nil, zone: nil)
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 schema_envelope(key)
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
- Events.new(self).call(event, **)
90
+ view = Store::View.new(self)
91
+ @bus.publish(event, store: view, **)
186
92
  end
187
93
 
188
- def accept(key, as:)
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 stale(prefix: nil, zone: nil)
201
- Staleness.new(self).call(prefix: prefix, zone: zone)
202
- end
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
- # Returns the Textus UID for a key (or nil if the entry has none yet).
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(self).call(old_key, new_key, as: as, dry_run: dry_run)
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
@@ -1,4 +1,4 @@
1
1
  module Textus
2
- VERSION = "0.5.0"
2
+ VERSION = "0.8.0"
3
3
  PROTOCOL = "textus/2"
4
4
  end
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
- require_relative "textus/extension_registry"
4
- require_relative "textus/extensions"
5
- require_relative "textus/role"
6
- require_relative "textus/audit_log"
7
- require_relative "textus/etag"
8
- require_relative "textus/entry/markdown"
9
- require_relative "textus/entry/json"
10
- require_relative "textus/entry/yaml"
11
- require_relative "textus/entry/text"
12
- require_relative "textus/entry"
13
- require_relative "textus/schema"
14
- require_relative "textus/key_distance"
15
- require_relative "textus/manifest_entry"
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.5.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/builtin_actions.rb
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/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
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/where.rb
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/extension_registry.rb
143
- - lib/textus/extensions.rb
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/key_distance.rb
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/manifest_entry.rb
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/schema_tools.rb
189
+ - lib/textus/schema/tools.rb
159
190
  - lib/textus/store.rb
160
- - lib/textus/store/events.rb
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/store_view.rb
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:
@@ -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