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
@@ -1,33 +0,0 @@
1
- module Textus
2
- THREAD_REGISTRY_KEY = :__textus_active_registry__
3
- private_constant :THREAD_REGISTRY_KEY
4
-
5
- def self.with_registry(registry)
6
- prev = Thread.current[THREAD_REGISTRY_KEY]
7
- Thread.current[THREAD_REGISTRY_KEY] = registry
8
- yield
9
- ensure
10
- Thread.current[THREAD_REGISTRY_KEY] = prev
11
- end
12
-
13
- def self.current_registry
14
- Thread.current[THREAD_REGISTRY_KEY] or
15
- raise UsageError.new("no active registry; extension code must be loaded by a Store")
16
- end
17
-
18
- def self.action(name, &)
19
- current_registry.register_action(name, &)
20
- end
21
-
22
- def self.reducer(name, &)
23
- current_registry.register_reducer(name, &)
24
- end
25
-
26
- def self.hook(event, name, &)
27
- current_registry.register_hook(event, name, &)
28
- end
29
-
30
- def self.doctor_check(name, &)
31
- current_registry.register_doctor_check(name, &)
32
- end
33
- end
@@ -1,53 +0,0 @@
1
- module Textus
2
- # Small utilities for ranking key suggestions. Bounded inputs only —
3
- # Levenshtein is O(n*m) so we refuse to compute on long strings.
4
- module KeyDistance
5
- MAX_LEN = 200
6
-
7
- # Length of the shared dot-separated prefix between two dotted keys.
8
- def self.shared_prefix_segments(left, right)
9
- asegs = left.split(".")
10
- bsegs = right.split(".")
11
- n = [asegs.length, bsegs.length].min
12
- i = 0
13
- i += 1 while i < n && asegs[i] == bsegs[i]
14
- i
15
- end
16
-
17
- # Classic iterative Levenshtein with two rows. Bounded to MAX_LEN.
18
- def self.levenshtein(left, right)
19
- return nil if left.length > MAX_LEN || right.length > MAX_LEN
20
- return right.length if left.empty?
21
- return left.length if right.empty?
22
-
23
- prev = (0..right.length).to_a
24
- curr = Array.new(right.length + 1, 0)
25
- (1..left.length).each do |i|
26
- curr[0] = i
27
- (1..right.length).each do |j|
28
- cost = left[i - 1] == right[j - 1] ? 0 : 1
29
- curr[j] = [
30
- curr[j - 1] + 1, # insertion
31
- prev[j] + 1, # deletion
32
- prev[j - 1] + cost, # substitution
33
- ].min
34
- end
35
- prev, curr = curr, prev
36
- end
37
- prev[right.length]
38
- end
39
-
40
- # Rank candidate keys against requested. Returns up to `limit` keys.
41
- # Sort: longer shared prefix first; then smaller Levenshtein distance.
42
- def self.suggest(requested, candidates, limit: 5)
43
- return [] if requested.nil? || requested.empty?
44
-
45
- scored = candidates.first(200).map do |k|
46
- prefix = shared_prefix_segments(requested, k)
47
- dist = levenshtein(requested, k) || Float::INFINITY
48
- [k, prefix, dist]
49
- end
50
- scored.sort_by { |(_, prefix, dist)| [-prefix, dist] }.first(limit).map(&:first)
51
- end
52
- end
53
- end
@@ -1,185 +0,0 @@
1
- module Textus
2
- class ManifestEntry
3
- PUBLISH_EACH_VARS = %w[leaf basename key ext].freeze
4
- PUBLISH_EACH_VAR_RE = /\{([a-z]+)\}/
5
-
6
- attr_reader :key, :path, :zone, :schema, :owner, :nested, :generator, :raw, :format,
7
- :projection, :template, :publish_to, :publish_each, :action, :action_config, :ttl, :events,
8
- :inject_intro
9
-
10
- def initialize(manifest, raw)
11
- @manifest = manifest
12
- @raw = raw
13
- @key = raw["key"] or raise UsageError.new("manifest entry missing key")
14
- @path = raw["path"] or raise UsageError.new("manifest entry '#{@key}' missing path")
15
- @zone = raw["zone"] or raise UsageError.new("manifest entry '#{@key}' missing zone")
16
- @schema = raw["schema"]
17
- @owner = raw["owner"]
18
- @nested = raw["nested"] == true
19
- @generator = raw["generator"]
20
- @projection = raw["projection"]
21
- @template = raw["template"]
22
- @publish_to = Array(raw["publish_to"])
23
- @publish_each = raw["publish_each"]
24
- @events = raw["events"] || {}
25
- @inject_intro = raw["inject_intro"] == true
26
- @format = resolve_format!(raw["format"])
27
-
28
- validate_events!
29
- parse_source!(raw["source"])
30
- validate_format_matrix!
31
- validate_publish_each!
32
- validate_inject_intro!
33
- end
34
-
35
- # Resolves the per-leaf target path (relative to repo root) for a full
36
- # dotted key under this entry's prefix. Returns nil if this entry has no
37
- # publish_each template.
38
- def publish_target_for(full_key)
39
- return nil if @publish_each.nil?
40
-
41
- entry_segs = @key.split(".")
42
- key_segs = full_key.split(".")
43
- raise UsageError.new("key '#{full_key}' is not under entry '#{@key}'") unless key_segs[0, entry_segs.length] == entry_segs
44
-
45
- remaining = key_segs[entry_segs.length..] || []
46
- leaf = remaining.join("/")
47
- basename = remaining.last || ""
48
- ext = Entry.for_format(@format).extensions.first.to_s.sub(/^\./, "")
49
-
50
- vars = { "leaf" => leaf, "basename" => basename, "key" => full_key, "ext" => ext }
51
- @publish_each.gsub(PUBLISH_EACH_VAR_RE) { vars.fetch(::Regexp.last_match(1)) }
52
- end
53
-
54
- def derived?
55
- writers = @manifest.zone_writers(@zone)
56
- writers.include?("build")
57
- rescue UsageError => e
58
- raise UsageError.new("entry '#{@key}': #{e.message}")
59
- end
60
-
61
- private
62
-
63
- def validate_inject_intro!
64
- return unless @inject_intro
65
-
66
- unless derived?
67
- raise UsageError.new(
68
- "entry '#{@key}': inject_intro: is only valid on derived entries",
69
- )
70
- end
71
- return unless @template.nil?
72
-
73
- raise UsageError.new(
74
- "entry '#{@key}': inject_intro: requires a template:",
75
- )
76
- end
77
-
78
- def validate_publish_each!
79
- return if @publish_each.nil?
80
-
81
- raise UsageError.new("entry '#{@key}': publish_each requires nested: true") unless @nested
82
- raise UsageError.new("entry '#{@key}': publish_to and publish_each are mutually exclusive") unless @publish_to.empty?
83
- raise UsageError.new("entry '#{@key}': publish_each must be a string") unless @publish_each.is_a?(String)
84
-
85
- used_vars = @publish_each.scan(PUBLISH_EACH_VAR_RE).flatten
86
- unknown = used_vars - PUBLISH_EACH_VARS
87
- unless unknown.empty?
88
- raise UsageError.new(
89
- "entry '#{@key}': publish_each uses unknown template variable(s) " \
90
- "#{unknown.map { |v| "{#{v}}" }.join(", ")}. Known: #{PUBLISH_EACH_VARS.map { |v| "{#{v}}" }.join(", ")}.",
91
- )
92
- end
93
-
94
- required = %w[leaf basename key]
95
- return if used_vars.any? { |v| required.include?(v) }
96
-
97
- raise UsageError.new(
98
- "entry '#{@key}': publish_each must reference at least one of {leaf}, {basename}, or {key} " \
99
- "(else every leaf would clobber the same target).",
100
- )
101
- end
102
-
103
- def resolve_format!(declared)
104
- ext = File.extname(@path)
105
- inferred = Manifest::EXT_TO_FORMAT[ext]
106
-
107
- if declared.nil?
108
- return inferred if inferred
109
- # No extension: nested defaults to markdown, leaf with no ext also markdown.
110
- return "markdown" if ext == "" && @nested
111
- return "markdown" if ext == ""
112
- else
113
- raise UsageError.new("entry '#{@key}': unknown format #{declared.inspect}") unless Manifest::EXT_TO_FORMAT.values.include?(declared)
114
- # If the path has an extension, the declared format must match.
115
- if ext != "" && inferred && inferred != declared
116
- raise UsageError.new(
117
- "entry '#{@key}': path extension #{ext.inspect} does not match declared format #{declared.inspect}",
118
- )
119
- end
120
- return declared
121
- end
122
-
123
- "markdown"
124
- end
125
-
126
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
127
- def validate_format_matrix!
128
- ext = File.extname(@path)
129
-
130
- case @format
131
- when "markdown"
132
- # .md, or no extension (will be appended). Anything else is a mismatch caught above.
133
- raise UsageError.new("entry '#{@key}': markdown format requires '.md' path (got #{ext.inspect})") if ext != "" && ext != ".md"
134
- when "json"
135
- if @nested
136
- # nested json: path is a directory; ext must be empty.
137
- raise UsageError.new("entry '#{@key}': nested json path must not have an extension") if ext != ""
138
- elsif ext != ".json"
139
- raise UsageError.new("entry '#{@key}': json format requires '.json' path (got #{ext.inspect})")
140
- end
141
- when "yaml"
142
- if @nested
143
- raise UsageError.new("entry '#{@key}': nested yaml path must not have an extension") if ext != ""
144
- elsif ext != ".yaml" && ext != ".yml"
145
- raise UsageError.new("entry '#{@key}': yaml format requires '.yaml' or '.yml' path (got #{ext.inspect})")
146
- end
147
- when "text"
148
- if @nested
149
- raise UsageError.new("entry '#{@key}': nested text path must not have an extension") if ext != ""
150
- elsif ext != ".txt" && ext != ""
151
- raise UsageError.new("entry '#{@key}': text format requires '.txt' or no extension (got #{ext.inspect})")
152
- end
153
- end
154
-
155
- # Schema rules.
156
- raise UsageError.new("entry '#{@key}': text format must not declare a schema") if @format == "text" && !@schema.nil?
157
-
158
- # Template-required-for-derived rules. Skipped for entries materialized by an
159
- # external generator: command (those produce the bytes themselves).
160
- if derived? && @template.nil? && @generator.nil? &&
161
- (@format == "markdown" || @format == "text") && !@nested
162
- raise UsageError.new("entry '#{@key}': derived #{@format} entries require a template")
163
- end
164
- end
165
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
166
-
167
- def parse_source!(src)
168
- src ||= {}
169
- @action = src["action"]
170
- @action_config = src["config"] || {}
171
- @ttl = src["ttl"]
172
- end
173
-
174
- def validate_events!
175
- @events.each_key do |evt|
176
- next if ExtensionRegistry::EVENTS.include?(evt.to_sym)
177
-
178
- raise UsageError.new(
179
- "entry '#{@key}': unknown event '#{evt}' in events: block. " \
180
- "Known events: #{ExtensionRegistry::EVENTS.join(", ")}.",
181
- )
182
- end
183
- end
184
- end
185
- end
@@ -1,27 +0,0 @@
1
- require "yaml"
2
-
3
- module Textus
4
- # One-shot migration: rewrites the manifest version string from textus/1
5
- # to textus/2. On-disk entry file shapes are unchanged — the only change
6
- # needed is the version: line in manifest.yaml.
7
- module MigrateV2
8
- def self.run(root)
9
- manifest_path = File.join(root, "manifest.yaml")
10
- raise IoError.new("manifest not found: #{manifest_path}") unless File.exist?(manifest_path)
11
-
12
- content = File.read(manifest_path)
13
- raw = YAML.safe_load(content, aliases: false)
14
-
15
- case raw["version"]
16
- when PROTOCOL
17
- { "protocol" => PROTOCOL, "ok" => true, "no_op" => true, "message" => "already #{PROTOCOL}" }
18
- when "textus/1"
19
- new_content = content.sub(%r{^version:\s*textus/1\s*$}, "version: #{PROTOCOL}")
20
- File.write(manifest_path, new_content)
21
- { "protocol" => PROTOCOL, "ok" => true, "from" => "textus/1", "to" => PROTOCOL }
22
- else
23
- raise UsageError.new("cannot migrate from #{raw["version"].inspect}")
24
- end
25
- end
26
- end
27
- end
@@ -1,87 +0,0 @@
1
- require "yaml"
2
- require "fileutils"
3
-
4
- module Textus
5
- module SchemaTools
6
- # textus schema-init NAME --from=KEY → infer YAML schema from an entry's frontmatter
7
- def self.init(store, name:, from:)
8
- env = store.get(from)
9
- meta = env["_meta"]
10
- schema = {
11
- "name" => name,
12
- "required" => meta.keys,
13
- "optional" => [],
14
- "fields" => meta.each_with_object({}) { |(k, v), h| h[k] = { "type" => infer_type(v) } },
15
- }
16
- FileUtils.mkdir_p(File.join(store.root, "schemas"))
17
- target = File.join(store.root, "schemas", "#{name}.yaml")
18
- File.write(target, YAML.dump(schema))
19
- { "protocol" => PROTOCOL, "schema_name" => name, "path" => target }
20
- end
21
-
22
- # textus schema-diff NAME → list keys whose frontmatter violates the schema
23
- def self.diff(store, name:)
24
- schema = load_schema(store, name)
25
- drift = []
26
- store.manifest.enumerate.each do |row|
27
- env = store.get(row[:key])
28
- begin
29
- schema.validate!(env["_meta"])
30
- rescue SchemaViolation => e
31
- drift << { "key" => row[:key], "details" => e.details }
32
- end
33
- end
34
- { "protocol" => PROTOCOL, "schema_name" => name, "drift" => drift }
35
- end
36
-
37
- # textus schema-migrate NAME --rename=OLD:NEW → rewrites frontmatter across affected entries
38
- # If --rename is omitted, falls back to schema.evolution.migrate_from.
39
- def self.migrate(store, name:, rename: nil)
40
- renames =
41
- if rename
42
- old_field, new_field = rename.split(":", 2)
43
- raise UsageError.new("--rename=OLD:NEW") unless old_field && new_field && !new_field.empty?
44
-
45
- { old_field => new_field }
46
- else
47
- load_schema(store, name).evolution["migrate_from"] || {}
48
- end
49
- raise UsageError.new("schema-migrate needs --rename=OLD:NEW or schema.evolution.migrate_from") if renames.empty?
50
-
51
- touched = []
52
- store.manifest.enumerate.each do |row|
53
- env = store.get(row[:key])
54
- meta = env["_meta"]
55
- changed = false
56
- renames.each do |old, new|
57
- if meta.key?(old)
58
- meta[new] = meta.delete(old)
59
- changed = true
60
- end
61
- end
62
- next unless changed
63
-
64
- store.put(row[:key], meta: meta, body: env["body"], as: "human")
65
- touched << row[:key]
66
- end
67
- { "protocol" => PROTOCOL, "migrated" => touched, "renames" => renames }
68
- end
69
-
70
- def self.infer_type(value)
71
- case value
72
- when String then "string"
73
- when Numeric then "number"
74
- when true, false then "boolean"
75
- when Array then "array"
76
- when Hash then "object"
77
- else "string"
78
- end
79
- end
80
-
81
- def self.load_schema(store, name)
82
- store.schema_for(name)
83
- rescue IoError
84
- raise UsageError.new("schema not found: #{name}")
85
- end
86
- end
87
- end
@@ -1,31 +0,0 @@
1
- require "timeout"
2
-
3
- module Textus
4
- class Store
5
- class Events
6
- HOOK_TIMEOUT_SECONDS = 2
7
-
8
- def initialize(store)
9
- @store = store
10
- end
11
-
12
- def call(event, **kwargs)
13
- view = StoreView.new(@store)
14
- @store.registry.hooks(event).each do |entry|
15
- name = entry[:name]
16
- Timeout.timeout(HOOK_TIMEOUT_SECONDS) { entry[:callable].call(store: view, **kwargs) }
17
- rescue StandardError => e
18
- extras = { "event" => event.to_s, "hook" => name.to_s, "error" => "#{e.class}: #{e.message}" }
19
- extras["target_key"] = kwargs[:target_key] if kwargs.key?(:target_key)
20
- extras["pending_key"] = kwargs[:pending_key] if kwargs.key?(:pending_key)
21
- @store.audit_log.append(
22
- role: "script", verb: "event_error",
23
- key: kwargs[:key] || kwargs[:target_key] || kwargs[:pending_key] || "-",
24
- etag_before: nil, etag_after: nil,
25
- extras: extras
26
- )
27
- end
28
- end
29
- end
30
- end
31
- end
@@ -1,27 +0,0 @@
1
- module Textus
2
- class StoreView
3
- READ_METHODS = %i[get list where schema_envelope deps rdeps published stale validate_all].freeze
4
- WRITE_METHODS = %i[put delete accept].freeze
5
-
6
- def initialize(store, writable: false, as: nil)
7
- raise UsageError.new("writable StoreView requires an as: role") if writable && (as.nil? || as.to_s.empty?)
8
-
9
- @store = store
10
- @writable = writable
11
- @as = as
12
- end
13
-
14
- READ_METHODS.each do |m|
15
- define_method(m) { |*args, **kw| @store.public_send(m, *args, **kw) }
16
- end
17
-
18
- WRITE_METHODS.each do |m|
19
- define_method(m) do |*args, **kw|
20
- raise UsageError.new("StoreView is read-only") unless @writable
21
-
22
- kw[:as] = @as unless kw.key?(:as)
23
- @store.public_send(m, *args, **kw)
24
- end
25
- end
26
- end
27
- end