textus 0.14.4 → 0.18.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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +14 -14
  3. data/CHANGELOG.md +378 -0
  4. data/README.md +13 -9
  5. data/SPEC.md +13 -10
  6. data/docs/conventions.md +11 -0
  7. data/lib/textus/application/context.rb +25 -7
  8. data/lib/textus/application/reads/audit.rb +1 -1
  9. data/lib/textus/application/reads/blame.rb +3 -1
  10. data/lib/textus/application/reads/deps.rb +1 -1
  11. data/lib/textus/application/reads/freshness.rb +12 -3
  12. data/lib/textus/application/reads/get.rb +38 -33
  13. data/lib/textus/application/reads/get_or_refresh.rb +51 -0
  14. data/lib/textus/application/reads/list.rb +3 -1
  15. data/lib/textus/application/reads/published.rb +1 -1
  16. data/lib/textus/application/reads/rdeps.rb +1 -1
  17. data/lib/textus/application/reads/schema_envelope.rb +3 -1
  18. data/lib/textus/application/reads/stale.rb +1 -1
  19. data/lib/textus/application/reads/uid.rb +1 -1
  20. data/lib/textus/application/reads/validate_all.rb +6 -1
  21. data/lib/textus/application/reads/validator.rb +84 -0
  22. data/lib/textus/application/reads/where.rb +4 -1
  23. data/lib/textus/application/refresh/all.rb +8 -1
  24. data/lib/textus/application/refresh/orchestrator.rb +11 -3
  25. data/lib/textus/application/refresh/worker.rb +27 -20
  26. data/lib/textus/application/writes/accept.rb +12 -12
  27. data/lib/textus/application/writes/build.rb +3 -4
  28. data/lib/textus/application/writes/delete.rb +10 -15
  29. data/lib/textus/application/writes/envelope_io.rb +106 -0
  30. data/lib/textus/application/writes/mv.rb +25 -27
  31. data/lib/textus/application/writes/publish.rb +8 -9
  32. data/lib/textus/application/writes/put.rb +12 -16
  33. data/lib/textus/application/writes/reject.rb +10 -10
  34. data/lib/textus/builder/pipeline.rb +8 -1
  35. data/lib/textus/cli/group/hook.rb +1 -3
  36. data/lib/textus/cli/group/key.rb +1 -4
  37. data/lib/textus/cli/group/refresh.rb +1 -2
  38. data/lib/textus/cli/group/rule.rb +1 -3
  39. data/lib/textus/cli/group/schema.rb +1 -5
  40. data/lib/textus/cli/group.rb +12 -16
  41. data/lib/textus/cli/verb/accept.rb +3 -1
  42. data/lib/textus/cli/verb/audit.rb +3 -1
  43. data/lib/textus/cli/verb/blame.rb +3 -1
  44. data/lib/textus/cli/verb/build.rb +4 -2
  45. data/lib/textus/cli/verb/delete.rb +3 -1
  46. data/lib/textus/cli/verb/deps.rb +3 -1
  47. data/lib/textus/cli/verb/doctor.rb +2 -0
  48. data/lib/textus/cli/verb/freshness.rb +3 -1
  49. data/lib/textus/cli/verb/get.rb +3 -1
  50. data/lib/textus/cli/verb/hook_run.rb +3 -0
  51. data/lib/textus/cli/verb/hooks.rb +3 -0
  52. data/lib/textus/cli/verb/init.rb +2 -0
  53. data/lib/textus/cli/verb/intro.rb +2 -0
  54. data/lib/textus/cli/verb/key_normalize.rb +3 -0
  55. data/lib/textus/cli/verb/list.rb +3 -1
  56. data/lib/textus/cli/verb/mv.rb +4 -1
  57. data/lib/textus/cli/verb/published.rb +3 -1
  58. data/lib/textus/cli/verb/put.rb +3 -1
  59. data/lib/textus/cli/verb/rdeps.rb +3 -1
  60. data/lib/textus/cli/verb/refresh.rb +1 -1
  61. data/lib/textus/cli/verb/refresh_stale.rb +4 -1
  62. data/lib/textus/cli/verb/reject.rb +3 -1
  63. data/lib/textus/cli/verb/rule_explain.rb +4 -1
  64. data/lib/textus/cli/verb/rule_list.rb +3 -0
  65. data/lib/textus/cli/verb/schema.rb +4 -1
  66. data/lib/textus/cli/verb/schema_diff.rb +3 -0
  67. data/lib/textus/cli/verb/schema_init.rb +3 -0
  68. data/lib/textus/cli/verb/schema_migrate.rb +3 -0
  69. data/lib/textus/cli/verb/uid.rb +4 -1
  70. data/lib/textus/cli/verb/where.rb +3 -1
  71. data/lib/textus/cli/verb.rb +30 -0
  72. data/lib/textus/cli.rb +40 -35
  73. data/lib/textus/doctor/check/audit_log.rb +1 -1
  74. data/lib/textus/doctor/check/hooks.rb +3 -1
  75. data/lib/textus/doctor/check/intake_registration.rb +3 -3
  76. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  77. data/lib/textus/doctor/check/sentinels.rb +2 -2
  78. data/lib/textus/domain/freshness/evaluator.rb +1 -1
  79. data/lib/textus/domain/freshness/policy.rb +1 -1
  80. data/lib/textus/domain/freshness/verdict.rb +1 -1
  81. data/lib/textus/domain/freshness.rb +40 -0
  82. data/lib/textus/domain/policy/predicates/schema_valid.rb +2 -2
  83. data/lib/textus/{store → domain}/sentinel.rb +1 -1
  84. data/lib/textus/{store → domain}/staleness/generator_check.rb +1 -1
  85. data/lib/textus/{store → domain}/staleness/intake_check.rb +1 -1
  86. data/lib/textus/{store → domain}/staleness.rb +1 -1
  87. data/lib/textus/entry/json.rb +1 -1
  88. data/lib/textus/entry/markdown.rb +1 -1
  89. data/lib/textus/entry/yaml.rb +1 -1
  90. data/lib/textus/envelope.rb +7 -3
  91. data/lib/textus/errors.rb +19 -0
  92. data/lib/textus/hooks/builtin.rb +6 -6
  93. data/lib/textus/hooks/dispatcher.rb +17 -9
  94. data/lib/textus/hooks/loader.rb +20 -17
  95. data/lib/textus/hooks/registry.rb +4 -0
  96. data/lib/textus/{store → infra}/audit_log.rb +1 -1
  97. data/lib/textus/infra/audit_subscriber.rb +43 -0
  98. data/lib/textus/infra/publisher.rb +3 -3
  99. data/lib/textus/infra/storage/file_store.rb +26 -0
  100. data/lib/textus/init.rb +11 -9
  101. data/lib/textus/manifest/resolution.rb +5 -0
  102. data/lib/textus/manifest.rb +4 -3
  103. data/lib/textus/migrate_keys.rb +1 -1
  104. data/lib/textus/operations.rb +84 -17
  105. data/lib/textus/projection.rb +16 -11
  106. data/lib/textus/refresh.rb +1 -1
  107. data/lib/textus/schema/tools.rb +5 -5
  108. data/lib/textus/schemas.rb +46 -0
  109. data/lib/textus/store.rb +12 -49
  110. data/lib/textus/uid.rb +18 -0
  111. data/lib/textus/version.rb +1 -1
  112. data/lib/textus.rb +17 -1
  113. metadata +15 -13
  114. data/lib/textus/hooks/dsl.rb +0 -11
  115. data/lib/textus/operations/reads.rb +0 -39
  116. data/lib/textus/operations/refresh.rb +0 -27
  117. data/lib/textus/operations/writes.rb +0 -21
  118. data/lib/textus/store/reader.rb +0 -69
  119. data/lib/textus/store/validator.rb +0 -82
  120. data/lib/textus/store/writer.rb +0 -102
data/lib/textus/store.rb CHANGED
@@ -1,16 +1,8 @@
1
1
  require "fileutils"
2
- require "securerandom"
3
2
 
4
3
  module Textus
5
4
  class Store
6
- attr_reader :root, :manifest, :registry, :reader, :writer, :bus
7
-
8
- # A Textus UID: 16 lowercase hex chars (SecureRandom.hex(8)). Not a UUID —
9
- # short on purpose. Random enough for collision-never-in-practice within a
10
- # single store.
11
- def self.mint_uid
12
- SecureRandom.hex(8)
13
- end
5
+ attr_reader :root, :manifest, :schemas, :file_store, :audit_log, :bus, :registry
14
6
 
15
7
  def self.discover(start_dir = Dir.pwd, root: nil)
16
8
  explicit = root || ENV.fetch("TEXTUS_ROOT", nil)
@@ -37,46 +29,17 @@ module Textus
37
29
  end
38
30
 
39
31
  def initialize(root)
40
- @root = File.expand_path(root)
41
- @manifest = Manifest.load(@root)
42
- @bus = Hooks::Dispatcher.new(audit_log: audit_log)
43
- @registry = Hooks::Registry.new(dispatcher: @bus)
44
- @schemas = {}
45
- load_hooks
46
- @reader = Reader.new(self)
47
- @writer = Writer.new(self)
48
- @bus.publish(:store_loaded, store: Textus::Application::Context.system(self))
49
- end
50
-
51
- def load_hooks
52
- Textus.with_registry(@registry) do
53
- Hooks::Builtin.register_all
54
- dir = File.join(@root, "hooks")
55
- return unless File.directory?(dir)
56
-
57
- Dir.glob(File.join(dir, "**/*.rb")).sort.each do |f| # rubocop:disable Lint/RedundantDirGlobSort
58
- begin
59
- load(f)
60
- rescue StandardError, ScriptError => e
61
- raise UsageError.new("failed loading hook #{File.basename(f)}: #{e.class}: #{e.message}")
62
- end
63
- end
64
- end
65
- end
66
-
67
- def schema_for(name)
68
- return nil if name.nil?
69
-
70
- @schemas[name] ||= begin
71
- sp = File.join(@root, "schemas", "#{name}.yaml")
72
- raise IoError.new("schema not found: #{sp}") unless File.exist?(sp)
73
-
74
- Schema.load(sp)
75
- end
76
- end
77
-
78
- def audit_log
79
- @audit_log ||= Store::AuditLog.new(@root)
32
+ @root = File.expand_path(root)
33
+ @manifest = Manifest.load(@root)
34
+ @schemas = Schemas.new(File.join(@root, "schemas"))
35
+ @file_store = Infra::Storage::FileStore.new
36
+ @audit_log = Infra::AuditLog.new(@root)
37
+ @bus = Hooks::Dispatcher.new
38
+ @registry = Hooks::Registry.new(dispatcher: @bus)
39
+ Infra::AuditSubscriber.new(@audit_log).attach(@bus)
40
+ Hooks::Builtin.register_all(@registry)
41
+ Hooks::Loader.new(registry: @registry).load_dir(File.join(@root, "hooks"))
42
+ @bus.publish(:store_loaded, store: Application::Context.system(self))
80
43
  end
81
44
  end
82
45
  end
data/lib/textus/uid.rb ADDED
@@ -0,0 +1,18 @@
1
+ require "securerandom"
2
+
3
+ module Textus
4
+ # A Textus UID: 16 lowercase hex chars (SecureRandom.hex(8)). Not a UUID —
5
+ # short on purpose. Random enough for collision-never-in-practice within a
6
+ # single store.
7
+ module Uid
8
+ module_function
9
+
10
+ def mint
11
+ SecureRandom.hex(8)
12
+ end
13
+
14
+ def valid?(str)
15
+ str.is_a?(String) && str.match?(/\A[0-9a-f]{16}\z/)
16
+ end
17
+ end
18
+ end
@@ -1,4 +1,4 @@
1
1
  module Textus
2
- VERSION = "0.14.4"
2
+ VERSION = "0.18.0"
3
3
  PROTOCOL = "textus/3"
4
4
  end
data/lib/textus.rb CHANGED
@@ -8,11 +8,27 @@ loader.inflector.inflect(
8
8
  "json" => "Json",
9
9
  "yaml" => "Yaml",
10
10
  "hook_dsl_scanner" => "HookDSLScanner",
11
+ "envelope_io" => "EnvelopeIO",
11
12
  )
12
13
  loader.ignore(File.expand_path("textus/errors.rb", __dir__))
13
14
  loader.setup
14
15
  loader.eager_load
15
16
 
16
17
  module Textus
17
- extend Hooks::Dsl
18
+ @hook_mutex = Mutex.new
19
+ @hook_blocks = []
20
+
21
+ def self.hook(&blk)
22
+ raise UsageError.new("hook block required") unless blk
23
+
24
+ @hook_mutex.synchronize { @hook_blocks << blk }
25
+ end
26
+
27
+ def self.drain_hook_blocks
28
+ @hook_mutex.synchronize do
29
+ blocks = @hook_blocks
30
+ @hook_blocks = []
31
+ blocks
32
+ end
33
+ end
18
34
  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.14.4
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick
@@ -115,6 +115,7 @@ files:
115
115
  - lib/textus/application/reads/deps.rb
116
116
  - lib/textus/application/reads/freshness.rb
117
117
  - lib/textus/application/reads/get.rb
118
+ - lib/textus/application/reads/get_or_refresh.rb
118
119
  - lib/textus/application/reads/list.rb
119
120
  - lib/textus/application/reads/policy_explain.rb
120
121
  - lib/textus/application/reads/published.rb
@@ -123,6 +124,7 @@ files:
123
124
  - lib/textus/application/reads/stale.rb
124
125
  - lib/textus/application/reads/uid.rb
125
126
  - lib/textus/application/reads/validate_all.rb
127
+ - lib/textus/application/reads/validator.rb
126
128
  - lib/textus/application/reads/where.rb
127
129
  - lib/textus/application/refresh/all.rb
128
130
  - lib/textus/application/refresh/orchestrator.rb
@@ -130,6 +132,7 @@ files:
130
132
  - lib/textus/application/writes/accept.rb
131
133
  - lib/textus/application/writes/build.rb
132
134
  - lib/textus/application/writes/delete.rb
135
+ - lib/textus/application/writes/envelope_io.rb
133
136
  - lib/textus/application/writes/mv.rb
134
137
  - lib/textus/application/writes/publish.rb
135
138
  - lib/textus/application/writes/put.rb
@@ -197,6 +200,7 @@ files:
197
200
  - lib/textus/doctor/check/templates.rb
198
201
  - lib/textus/doctor/check/unowned_schema_fields.rb
199
202
  - lib/textus/domain/action.rb
203
+ - lib/textus/domain/freshness.rb
200
204
  - lib/textus/domain/freshness/evaluator.rb
201
205
  - lib/textus/domain/freshness/policy.rb
202
206
  - lib/textus/domain/freshness/verdict.rb
@@ -210,6 +214,10 @@ files:
210
214
  - lib/textus/domain/policy/promote.rb
211
215
  - lib/textus/domain/policy/promotion.rb
212
216
  - lib/textus/domain/policy/refresh.rb
217
+ - lib/textus/domain/sentinel.rb
218
+ - lib/textus/domain/staleness.rb
219
+ - lib/textus/domain/staleness/generator_check.rb
220
+ - lib/textus/domain/staleness/intake_check.rb
213
221
  - lib/textus/entry.rb
214
222
  - lib/textus/entry/base.rb
215
223
  - lib/textus/entry/json.rb
@@ -221,15 +229,17 @@ files:
221
229
  - lib/textus/etag.rb
222
230
  - lib/textus/hooks/builtin.rb
223
231
  - lib/textus/hooks/dispatcher.rb
224
- - lib/textus/hooks/dsl.rb
225
232
  - lib/textus/hooks/loader.rb
226
233
  - lib/textus/hooks/registry.rb
234
+ - lib/textus/infra/audit_log.rb
235
+ - lib/textus/infra/audit_subscriber.rb
227
236
  - lib/textus/infra/build_lock.rb
228
237
  - lib/textus/infra/clock.rb
229
238
  - lib/textus/infra/event_bus.rb
230
239
  - lib/textus/infra/publisher.rb
231
240
  - lib/textus/infra/refresh/detached.rb
232
241
  - lib/textus/infra/refresh/lock.rb
242
+ - lib/textus/infra/storage/file_store.rb
233
243
  - lib/textus/init.rb
234
244
  - lib/textus/intro.rb
235
245
  - lib/textus/key/distance.rb
@@ -244,28 +254,20 @@ files:
244
254
  - lib/textus/manifest/entry/validators/index_filename.rb
245
255
  - lib/textus/manifest/entry/validators/inject_intro.rb
246
256
  - lib/textus/manifest/entry/validators/publish_each.rb
257
+ - lib/textus/manifest/resolution.rb
247
258
  - lib/textus/manifest/rules.rb
248
259
  - lib/textus/manifest/schema.rb
249
260
  - lib/textus/migrate_keys.rb
250
261
  - lib/textus/mustache.rb
251
262
  - lib/textus/operations.rb
252
- - lib/textus/operations/reads.rb
253
- - lib/textus/operations/refresh.rb
254
- - lib/textus/operations/writes.rb
255
263
  - lib/textus/projection.rb
256
264
  - lib/textus/refresh.rb
257
265
  - lib/textus/role.rb
258
266
  - lib/textus/schema.rb
259
267
  - lib/textus/schema/tools.rb
268
+ - lib/textus/schemas.rb
260
269
  - lib/textus/store.rb
261
- - lib/textus/store/audit_log.rb
262
- - lib/textus/store/reader.rb
263
- - lib/textus/store/sentinel.rb
264
- - lib/textus/store/staleness.rb
265
- - lib/textus/store/staleness/generator_check.rb
266
- - lib/textus/store/staleness/intake_check.rb
267
- - lib/textus/store/validator.rb
268
- - lib/textus/store/writer.rb
270
+ - lib/textus/uid.rb
269
271
  - lib/textus/version.rb
270
272
  homepage: https://github.com/patrick204nqh/textus
271
273
  licenses:
@@ -1,11 +0,0 @@
1
- module Textus
2
- module Hooks
3
- module Dsl
4
- def on(event, name, **, &blk)
5
- raise UsageError.new("hook needs a block") unless blk
6
-
7
- Loader.current_registry.register(event, name, **, &blk)
8
- end
9
- end
10
- end
11
- end
@@ -1,39 +0,0 @@
1
- module Textus
2
- class Operations
3
- class Reads
4
- def initialize(ctx)
5
- @ctx = ctx
6
- end
7
-
8
- def get
9
- Application::Reads::Get.new(ctx: @ctx, orchestrator: orchestrator)
10
- end
11
-
12
- def freshness = Application::Reads::Freshness.new(ctx: @ctx)
13
- def audit = Application::Reads::Audit.new(ctx: @ctx)
14
- def blame = Application::Reads::Blame.new(ctx: @ctx)
15
- def policy_explain = Application::Reads::PolicyExplain.new(ctx: @ctx)
16
- def list = Application::Reads::List.new(ctx: @ctx)
17
- def where = Application::Reads::Where.new(ctx: @ctx)
18
- def uid = Application::Reads::Uid.new(ctx: @ctx)
19
- def schema_envelope = Application::Reads::SchemaEnvelope.new(ctx: @ctx)
20
- def deps = Application::Reads::Deps.new(ctx: @ctx)
21
- def rdeps = Application::Reads::Rdeps.new(ctx: @ctx)
22
- def published = Application::Reads::Published.new(ctx: @ctx)
23
- def stale = Application::Reads::Stale.new(ctx: @ctx)
24
- def validate_all = Application::Reads::ValidateAll.new(ctx: @ctx)
25
-
26
- private
27
-
28
- def orchestrator
29
- Application::Refresh::Orchestrator.new(
30
- worker: Application::Refresh::Worker.new(ctx: @ctx, bus: @ctx.store.bus),
31
- bus: @ctx.store.bus,
32
- store_root: @ctx.store.root,
33
- store: @ctx.store,
34
- role: @ctx.role,
35
- )
36
- end
37
- end
38
- end
39
- end
@@ -1,27 +0,0 @@
1
- module Textus
2
- class Operations
3
- class Refresh
4
- def initialize(ctx)
5
- @ctx = ctx
6
- end
7
-
8
- def worker
9
- Application::Refresh::Worker.new(ctx: @ctx, bus: @ctx.store.bus)
10
- end
11
-
12
- def orchestrator
13
- Application::Refresh::Orchestrator.new(
14
- worker: worker,
15
- bus: @ctx.store.bus,
16
- store_root: @ctx.store.root,
17
- store: @ctx.store,
18
- role: @ctx.role,
19
- )
20
- end
21
-
22
- def all
23
- Application::Refresh::All.new(ctx: @ctx, bus: @ctx.store.bus)
24
- end
25
- end
26
- end
27
- end
@@ -1,21 +0,0 @@
1
- module Textus
2
- class Operations
3
- class Writes
4
- def initialize(ctx)
5
- @ctx = ctx
6
- end
7
-
8
- def put = Application::Writes::Put.new(ctx: @ctx, bus: bus)
9
- def delete = Application::Writes::Delete.new(ctx: @ctx, bus: bus)
10
- def mv = Application::Writes::Mv.new(ctx: @ctx, bus: bus)
11
- def accept = Application::Writes::Accept.new(ctx: @ctx, bus: bus)
12
- def build = Application::Writes::Build.new(ctx: @ctx, bus: bus)
13
- def publish = Application::Writes::Publish.new(ctx: @ctx, bus: bus)
14
- def reject = Application::Writes::Reject.new(ctx: @ctx, bus: bus)
15
-
16
- private
17
-
18
- def bus = @ctx.store.bus
19
- end
20
- end
21
- end
@@ -1,69 +0,0 @@
1
- module Textus
2
- class Store
3
- class Reader
4
- def initialize(store)
5
- @store = store
6
- @manifest = store.manifest
7
- end
8
-
9
- def get(key)
10
- read_raw_envelope(key) || raise(UnknownKey.new(key, suggestions: @manifest.suggestions_for(key)))
11
- end
12
-
13
- # Reads the current on-disk state of key as a bare envelope, skipping
14
- # freshness annotation to avoid recursion. Used by Freshness.refresh_sync
15
- # after a sync refresh completes.
16
- def read_raw_envelope(key)
17
- mentry, path, = @manifest.resolve(key)
18
- return nil unless File.exist?(path)
19
-
20
- raw = File.binread(path)
21
- parsed = Entry.for_format(mentry.format).parse(raw, path: path)
22
- Envelope.build(
23
- key: key, mentry: mentry, path: path,
24
- meta: parsed["_meta"], body: parsed["body"],
25
- etag: Etag.for_bytes(raw), content: parsed["content"]
26
- )
27
- end
28
-
29
- def list(prefix: nil, zone: nil)
30
- rows = @manifest.enumerate(prefix: prefix)
31
- rows = rows.select { |r| r[:manifest_entry].zone == zone } if zone
32
- rows.map { |row| { "key" => row[:key], "zone" => row[:manifest_entry].zone, "path" => row[:path] } }
33
- end
34
-
35
- def where(key)
36
- mentry, path, = @manifest.resolve(key)
37
- { "protocol" => PROTOCOL, "key" => key, "zone" => mentry.zone, "owner" => mentry.owner, "path" => path }
38
- end
39
-
40
- def schema_envelope(key)
41
- mentry, = @manifest.resolve(key)
42
- schema = @store.schema_for(mentry.schema)
43
- { "protocol" => PROTOCOL, "key" => key, "schema_ref" => mentry.schema, "schema" => schema&.to_h }
44
- end
45
-
46
- # Returns the Textus UID for a key (or nil if the entry has none yet).
47
- # Raises UnknownKey if the key doesn't resolve to a real file.
48
- def uid(key)
49
- get(key).uid
50
- end
51
-
52
- def deps(key) = Dependencies.deps_of(@manifest, key)
53
- def rdeps(key) = Dependencies.rdeps_of(@manifest, key)
54
- def published = Dependencies.published_of(@manifest)
55
-
56
- def stale(prefix: nil, zone: nil)
57
- Staleness.new(manifest: @manifest).call(prefix: prefix, zone: zone)
58
- end
59
-
60
- def validate_all
61
- Validator.new(
62
- reader: self, manifest: @manifest,
63
- audit_log: @store.audit_log,
64
- schema_for: ->(name) { @store.schema_for(name) }
65
- ).call
66
- end
67
- end
68
- end
69
- end
@@ -1,82 +0,0 @@
1
- module Textus
2
- class Store
3
- class Validator
4
- def initialize(reader:, manifest:, audit_log:, schema_for:)
5
- @reader = reader
6
- @manifest = manifest
7
- @audit_log = audit_log
8
- @schema_for = schema_for
9
- end
10
-
11
- def call
12
- violations = []
13
- check_content_violations(violations)
14
- check_role_authority_violations(violations)
15
- { "protocol" => PROTOCOL, "ok" => violations.empty?, "violations" => violations }
16
- end
17
-
18
- private
19
-
20
- def check_content_violations(violations)
21
- @manifest.enumerate.each do |row|
22
- key = row[:key]
23
- mentry = row[:manifest_entry]
24
- env = fetch_envelope(key, violations) or next
25
- schema = mentry.schema && @schema_for.call(mentry.schema)
26
- next unless schema
27
-
28
- begin
29
- validate_schema!(schema, env, mentry.format)
30
- rescue Textus::Error => e
31
- violations << { "key" => key, "code" => e.code, "message" => e.message }
32
- end
33
- end
34
- end
35
-
36
- def check_role_authority_violations(violations)
37
- @manifest.enumerate.each do |row|
38
- mentry = row[:manifest_entry]
39
- next unless mentry.schema
40
-
41
- schema = @schema_for.call(mentry.schema)
42
- next unless schema
43
-
44
- env = begin
45
- @reader.get(row[:key])
46
- rescue StandardError
47
- next
48
- end
49
- append_authority_violations(violations, row[:key], env, schema)
50
- end
51
- end
52
-
53
- def append_authority_violations(violations, key, env, schema)
54
- last_writer = @audit_log.last_writer_for(key)
55
- return if last_writer.nil?
56
-
57
- env.meta.each_key do |field|
58
- owner = schema.maintained_by(field)
59
- next if owner.nil? || last_writer == owner || last_writer == "human"
60
-
61
- violations << { "key" => key, "code" => "role_authority",
62
- "field" => field, "expected" => owner, "last_writer" => last_writer }
63
- end
64
- end
65
-
66
- def fetch_envelope(key, violations)
67
- @reader.get(key)
68
- rescue Textus::Error => e
69
- violations << { "key" => key, "code" => e.code, "message" => e.message }
70
- nil
71
- end
72
-
73
- def validate_schema!(schema, envelope, format)
74
- payload = case format
75
- when "json", "yaml" then envelope.content || {}
76
- else envelope.meta || {}
77
- end
78
- schema.validate!(payload)
79
- end
80
- end
81
- end
82
- end
@@ -1,102 +0,0 @@
1
- require "fileutils"
2
-
3
- module Textus
4
- class Store
5
- class Writer
6
- Payload = Data.define(:meta, :body, :content)
7
-
8
- def initialize(store)
9
- @store = store
10
- @manifest = store.manifest
11
- @reader = store.reader
12
- end
13
-
14
- # Pure I/O: validate, serialize, etag-check, write to disk, audit. No
15
- # permission check and no event firing — those are handled by the caller
16
- # (Application::Writes::Put).
17
- def write_envelope_to_disk(key, mentry:, payload:, ctx:, if_etag: nil)
18
- _, path, = @manifest.resolve(key)
19
-
20
- meta = payload.meta || {}
21
- strategy = Entry.for_format(mentry.format)
22
-
23
- existing_uid = existing_uid_for(mentry, path)
24
- meta, content = ensure_uid(mentry.format, meta, payload.content, existing_uid)
25
-
26
- bytes, eff_meta, eff_body, eff_content = serialize_for_put(
27
- mentry: mentry, path: path, strategy: strategy,
28
- meta: meta, body: payload.body, content: content
29
- )
30
-
31
- enforce_name_match!(path, eff_meta, mentry.format)
32
-
33
- schema = @store.schema_for(mentry.schema)
34
- if schema
35
- Entry.for_format(mentry.format).validate_against(
36
- schema,
37
- { "_meta" => eff_meta, "content" => eff_content },
38
- )
39
- end
40
-
41
- etag_before = File.exist?(path) ? Etag.for_file(path) : nil
42
- raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && (etag_before != if_etag)
43
-
44
- FileUtils.mkdir_p(File.dirname(path))
45
- File.binwrite(path, bytes)
46
- etag_after = Etag.for_bytes(bytes)
47
- @store.audit_log.append(
48
- role: ctx.role, verb: "put", key: key,
49
- etag_before: etag_before, etag_after: etag_after,
50
- extras: ctx.correlation_id ? { "correlation_id" => ctx.correlation_id } : nil
51
- )
52
- Envelope.build(
53
- key: key, mentry: mentry, path: path,
54
- meta: eff_meta, body: eff_body, etag: etag_after, content: eff_content
55
- )
56
- end
57
-
58
- def existing_uid_for(mentry, path)
59
- return nil unless File.exist?(path)
60
-
61
- raw = File.binread(path)
62
- parsed = Entry.for_format(mentry.format).parse(raw, path: path)
63
- Envelope.extract_uid(parsed["_meta"])
64
- rescue StandardError
65
- nil
66
- end
67
-
68
- def ensure_uid(format, meta, content, existing_uid)
69
- Textus::Entry.for_format(format).inject_uid(meta, content, existing_uid)
70
- end
71
-
72
- def enforce_name_match!(path, meta, format)
73
- Textus::Entry.for_format(format).enforce_name_match!(path, meta)
74
- end
75
-
76
- def serialize_for_put(mentry:, path:, strategy:, meta:, body:, content:)
77
- _ = strategy
78
- Textus::Entry.for_format(mentry.format).serialize_for_put(
79
- meta: meta, body: body, content: content, path: path,
80
- )
81
- end
82
-
83
- # Pure I/O: resolve path, validate etag, delete from disk, audit. No
84
- # permission check and no event firing — those are handled by the caller
85
- # (Application::Writes::Delete).
86
- def delete_envelope_from_disk(key, ctx:, if_etag: nil)
87
- _, path, = @manifest.resolve(key)
88
- raise UnknownKey.new(key, suggestions: @manifest.suggestions_for(key)) unless File.exist?(path)
89
-
90
- etag_before = Etag.for_file(path)
91
- raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && if_etag != etag_before
92
-
93
- File.delete(path)
94
- @store.audit_log.append(
95
- role: ctx.role, verb: "delete", key: key,
96
- etag_before: etag_before, etag_after: nil,
97
- extras: ctx.correlation_id ? { "correlation_id" => ctx.correlation_id } : nil
98
- )
99
- end
100
- end
101
- end
102
- end