textus 0.12.1 → 0.14.1

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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +60 -40
  3. data/CHANGELOG.md +231 -0
  4. data/README.md +6 -12
  5. data/SPEC.md +4 -1
  6. data/docs/conventions.md +8 -8
  7. data/lib/textus/application/context.rb +4 -0
  8. data/lib/textus/application/reads/blame.rb +1 -1
  9. data/lib/textus/application/reads/deps.rb +15 -0
  10. data/lib/textus/application/reads/freshness.rb +2 -2
  11. data/lib/textus/application/reads/get.rb +8 -11
  12. data/lib/textus/application/reads/list.rb +15 -0
  13. data/lib/textus/application/reads/published.rb +15 -0
  14. data/lib/textus/application/reads/rdeps.rb +15 -0
  15. data/lib/textus/application/reads/schema_envelope.rb +15 -0
  16. data/lib/textus/application/reads/stale.rb +15 -0
  17. data/lib/textus/application/reads/uid.rb +15 -0
  18. data/lib/textus/application/reads/validate_all.rb +15 -0
  19. data/lib/textus/application/reads/where.rb +15 -0
  20. data/lib/textus/application/refresh/all.rb +2 -2
  21. data/lib/textus/application/refresh/worker.rb +3 -3
  22. data/lib/textus/application/writes/accept.rb +7 -7
  23. data/lib/textus/application/writes/build.rb +10 -47
  24. data/lib/textus/application/writes/mv.rb +144 -0
  25. data/lib/textus/application/writes/publish.rb +41 -9
  26. data/lib/textus/application/writes/reject.rb +37 -0
  27. data/lib/textus/builder/pipeline.rb +46 -2
  28. data/lib/textus/cli/verb/accept.rb +1 -2
  29. data/lib/textus/cli/verb/audit.rb +3 -3
  30. data/lib/textus/cli/verb/blame.rb +1 -2
  31. data/lib/textus/cli/verb/build.rb +6 -2
  32. data/lib/textus/cli/verb/delete.rb +1 -2
  33. data/lib/textus/cli/verb/deps.rb +1 -1
  34. data/lib/textus/cli/verb/freshness.rb +1 -2
  35. data/lib/textus/cli/verb/get.rb +2 -3
  36. data/lib/textus/cli/verb/list.rb +1 -1
  37. data/lib/textus/cli/verb/mv.rb +1 -1
  38. data/lib/textus/cli/verb/published.rb +1 -1
  39. data/lib/textus/cli/verb/put.rb +2 -2
  40. data/lib/textus/cli/verb/rdeps.rb +1 -1
  41. data/lib/textus/cli/verb/refresh.rb +1 -2
  42. data/lib/textus/cli/verb/reject.rb +1 -1
  43. data/lib/textus/cli/verb/rule_explain.rb +1 -2
  44. data/lib/textus/cli/verb/schema.rb +1 -1
  45. data/lib/textus/cli/verb/uid.rb +1 -1
  46. data/lib/textus/cli/verb/where.rb +1 -1
  47. data/lib/textus/cli/verb.rb +6 -1
  48. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  49. data/lib/textus/doctor.rb +1 -1
  50. data/lib/textus/domain/freshness/evaluator.rb +1 -1
  51. data/lib/textus/domain/policy/predicates/schema_valid.rb +3 -3
  52. data/lib/textus/entry/base.rb +28 -0
  53. data/lib/textus/entry/json.rb +59 -0
  54. data/lib/textus/entry/markdown.rb +46 -0
  55. data/lib/textus/entry/text.rb +35 -0
  56. data/lib/textus/entry/yaml.rb +59 -0
  57. data/lib/textus/entry.rb +16 -0
  58. data/lib/textus/envelope.rb +44 -14
  59. data/lib/textus/intro.rb +56 -0
  60. data/lib/textus/manifest/entry/parser.rb +84 -0
  61. data/lib/textus/manifest/entry/validators/events.rb +21 -0
  62. data/lib/textus/manifest/entry/validators/format_matrix.rb +26 -0
  63. data/lib/textus/manifest/entry/validators/index_filename.rb +45 -0
  64. data/lib/textus/manifest/entry/validators/inject_intro.rb +18 -0
  65. data/lib/textus/manifest/entry/validators/publish_each.rb +37 -0
  66. data/lib/textus/manifest/entry/validators.rb +20 -0
  67. data/lib/textus/manifest/entry.rb +35 -213
  68. data/lib/textus/manifest.rb +19 -32
  69. data/lib/textus/operations/reads.rb +39 -0
  70. data/lib/textus/operations/refresh.rb +27 -0
  71. data/lib/textus/operations/writes.rb +21 -0
  72. data/lib/textus/operations.rb +44 -0
  73. data/lib/textus/projection.rb +5 -4
  74. data/lib/textus/refresh.rb +3 -4
  75. data/lib/textus/schema/tools.rb +8 -7
  76. data/lib/textus/store/reader.rb +1 -1
  77. data/lib/textus/store/validator.rb +3 -3
  78. data/lib/textus/store/writer.rb +5 -74
  79. data/lib/textus/store.rb +1 -55
  80. data/lib/textus/version.rb +1 -1
  81. metadata +23 -4
  82. data/lib/textus/composition.rb +0 -72
  83. data/lib/textus/proposal.rb +0 -10
  84. data/lib/textus/store/mover.rb +0 -167
@@ -1,167 +0,0 @@
1
- require "fileutils"
2
-
3
- module Textus
4
- class Store
5
- class Mover
6
- MovePlan = Data.define(
7
- :old_key, :new_key, :old_path, :new_path,
8
- :new_mentry, :uid, :etag_before, :as
9
- )
10
-
11
- def initialize(store:, reader:, writer:, manifest:, audit_log:)
12
- @store = store
13
- @reader = reader
14
- @writer = writer
15
- @manifest = manifest
16
- @audit_log = audit_log
17
- end
18
-
19
- def call(old_key, new_key, as: Role::DEFAULT, dry_run: false, correlation_id: nil)
20
- plan, pre_env = prepare_plan(old_key, new_key, as: as)
21
- return dry_run_result(plan) if dry_run
22
-
23
- plan = ensure_uid!(plan, pre_env: pre_env)
24
- etag_after = perform_move!(plan)
25
- new_envelope = record_move(plan, etag_after: etag_after, correlation_id: correlation_id)
26
- success_result(plan, new_envelope: new_envelope)
27
- end
28
-
29
- private
30
-
31
- # Validates inputs, resolves manifest entries, and reads the source
32
- # envelope. Returns [MovePlan, pre_envelope]; the pre_envelope is only
33
- # needed by ensure_uid! and is threaded separately to keep MovePlan
34
- # focused on the planned operation.
35
- def prepare_plan(old_key, new_key, as:)
36
- @manifest.validate_key!(old_key)
37
- @manifest.validate_key!(new_key)
38
- raise UsageError.new("mv: old and new keys are identical") if old_key == new_key
39
-
40
- old_mentry, old_path, = @manifest.resolve(old_key)
41
- raise UnknownKey.new(old_key) unless File.exist?(old_path)
42
-
43
- new_mentry, new_path, = @manifest.resolve(new_key)
44
- validate_zone_and_format!(old_mentry, new_mentry)
45
- validate_writer!(old_mentry, old_key, as)
46
- raise UsageError.new("mv: target '#{new_key}' already exists at #{new_path}") if File.exist?(new_path)
47
-
48
- pre_env = @reader.get(old_key)
49
- plan = MovePlan.new(
50
- old_key: old_key, new_key: new_key,
51
- old_path: old_path, new_path: new_path,
52
- new_mentry: new_mentry,
53
- uid: pre_env["uid"], etag_before: pre_env["etag"], as: as
54
- )
55
- [plan, pre_env]
56
- end
57
-
58
- def validate_zone_and_format!(old_mentry, new_mentry)
59
- if old_mentry.zone != new_mentry.zone
60
- raise UsageError.new(
61
- "mv: cross-zone move refused (#{old_mentry.zone} → #{new_mentry.zone}). " \
62
- "Use put+delete for cross-zone moves.",
63
- )
64
- end
65
- return if old_mentry.format == new_mentry.format
66
-
67
- raise UsageError.new(
68
- "mv: format mismatch (#{old_mentry.format} → #{new_mentry.format}); refusing.",
69
- )
70
- end
71
-
72
- def validate_writer!(mentry, key, as)
73
- writers = @manifest.zone_writers(mentry.zone)
74
- return if writers.include?(as)
75
-
76
- raise WriteForbidden.new(key, mentry.zone, writers: writers)
77
- end
78
-
79
- def ensure_uid!(plan, pre_env:)
80
- return plan if plan.uid
81
-
82
- env = @writer.put(
83
- plan.old_key,
84
- meta: pre_env["_meta"],
85
- body: pre_env["body"],
86
- content: pre_env["content"],
87
- as: plan.as,
88
- suppress_events: true,
89
- )
90
- plan.with(uid: env["uid"], etag_before: env["etag"])
91
- end
92
-
93
- def perform_move!(plan)
94
- FileUtils.mkdir_p(File.dirname(plan.new_path))
95
- FileUtils.mv(plan.old_path, plan.new_path)
96
- rewrite_name_for_mv!(plan.new_mentry, plan.new_path, plan.new_key)
97
- Etag.for_file(plan.new_path)
98
- end
99
-
100
- def record_move(plan, etag_after:, correlation_id:)
101
- extras = {
102
- "from_key" => plan.old_key, "to_key" => plan.new_key,
103
- "from_path" => plan.old_path, "to_path" => plan.new_path,
104
- "uid" => plan.uid
105
- }
106
- extras["correlation_id"] = correlation_id if correlation_id
107
-
108
- @audit_log.append(
109
- role: plan.as, verb: "mv", key: plan.new_key,
110
- etag_before: plan.etag_before, etag_after: etag_after,
111
- extras: extras
112
- )
113
- new_envelope = @reader.get(plan.new_key)
114
- @store.fire_event(
115
- :entry_renamed,
116
- key: plan.new_key, from_key: plan.old_key, to_key: plan.new_key,
117
- envelope: new_envelope
118
- )
119
- new_envelope
120
- end
121
-
122
- def dry_run_result(plan)
123
- {
124
- "protocol" => PROTOCOL, "ok" => true, "dry_run" => true,
125
- "from_key" => plan.old_key, "to_key" => plan.new_key,
126
- "from_path" => plan.old_path, "to_path" => plan.new_path,
127
- "uid" => plan.uid
128
- }
129
- end
130
-
131
- def success_result(plan, new_envelope:)
132
- {
133
- "protocol" => PROTOCOL, "ok" => true,
134
- "from_key" => plan.old_key, "to_key" => plan.new_key,
135
- "from_path" => plan.old_path, "to_path" => plan.new_path,
136
- "uid" => plan.uid,
137
- "envelope" => new_envelope
138
- }
139
- end
140
-
141
- # If the moved file carries a `name:` field (markdown) or `_meta.name`
142
- # (json/yaml), rewrite it to the new basename so enforce_name_match! stays
143
- # happy on the next read. Only touches the bytes when name actually changes.
144
- def rewrite_name_for_mv!(mentry, new_path, new_key)
145
- strategy = Entry.for_format(mentry.format)
146
- raw = File.binread(new_path)
147
- parsed = strategy.parse(raw, path: new_path)
148
- basename = new_key.split(".").last
149
-
150
- case mentry.format
151
- when "markdown"
152
- meta = parsed["_meta"] || {}
153
- return unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
154
-
155
- meta = meta.merge("name" => basename)
156
- File.binwrite(new_path, strategy.serialize(meta: meta, body: parsed["body"]))
157
- when "json", "yaml"
158
- meta = parsed["_meta"]
159
- return unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
160
-
161
- new_meta = meta.merge("name" => basename)
162
- File.binwrite(new_path, strategy.serialize(meta: new_meta, body: "", content: parsed["content"]))
163
- end
164
- end
165
- end
166
- end
167
- end