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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +147 -2
  3. data/README.md +38 -28
  4. data/SPEC.md +84 -147
  5. data/docs/architecture.md +82 -28
  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/group.rb +51 -0
  17. data/lib/textus/cli/verb/accept.rb +15 -0
  18. data/lib/textus/cli/verb/build.rb +13 -0
  19. data/lib/textus/cli/verb/delete.rb +16 -0
  20. data/lib/textus/cli/verb/deps.rb +12 -0
  21. data/lib/textus/cli/verb/doctor.rb +15 -0
  22. data/lib/textus/cli/verb/get.rb +12 -0
  23. data/lib/textus/cli/verb/hook_run.rb +48 -0
  24. data/lib/textus/cli/verb/hooks.rb +50 -0
  25. data/lib/textus/cli/verb/init.rb +14 -0
  26. data/lib/textus/cli/verb/intro.rb +11 -0
  27. data/lib/textus/cli/verb/list.rb +14 -0
  28. data/lib/textus/cli/verb/migrate_keys.rb +16 -0
  29. data/lib/textus/cli/verb/mv.rb +17 -0
  30. data/lib/textus/cli/verb/published.rb +11 -0
  31. data/lib/textus/cli/verb/put.rb +50 -0
  32. data/lib/textus/cli/verb/rdeps.rb +12 -0
  33. data/lib/textus/cli/verb/refresh.rb +15 -0
  34. data/lib/textus/cli/verb/schema.rb +12 -0
  35. data/lib/textus/cli/verb/schema_diff.rb +12 -0
  36. data/lib/textus/cli/verb/schema_init.rb +16 -0
  37. data/lib/textus/cli/verb/schema_migrate.rb +16 -0
  38. data/lib/textus/cli/verb/stale.rb +14 -0
  39. data/lib/textus/cli/verb/uid.rb +12 -0
  40. data/lib/textus/cli/verb/where.rb +12 -0
  41. data/lib/textus/cli/verb.rb +62 -0
  42. data/lib/textus/cli.rb +44 -385
  43. data/lib/textus/doctor/check/audit_log.rb +50 -0
  44. data/lib/textus/doctor/check/hooks.rb +29 -0
  45. data/lib/textus/doctor/check/illegal_keys.rb +49 -0
  46. data/lib/textus/doctor/check/manifest_files.rb +38 -0
  47. data/lib/textus/doctor/check/schema_violations.rb +22 -0
  48. data/lib/textus/doctor/check/schemas.rb +26 -0
  49. data/lib/textus/doctor/check/sentinels.rb +57 -0
  50. data/lib/textus/doctor/check/templates.rb +26 -0
  51. data/lib/textus/doctor/check/unowned_schema_fields.rb +34 -0
  52. data/lib/textus/doctor/check.rb +30 -0
  53. data/lib/textus/doctor.rb +29 -264
  54. data/lib/textus/entry/base.rb +30 -0
  55. data/lib/textus/entry/json.rb +11 -5
  56. data/lib/textus/entry/markdown.rb +5 -5
  57. data/lib/textus/entry/text.rb +4 -4
  58. data/lib/textus/entry/yaml.rb +11 -5
  59. data/lib/textus/entry.rb +2 -7
  60. data/lib/textus/envelope.rb +30 -0
  61. data/lib/textus/errors.rb +2 -2
  62. data/lib/textus/hooks/builtin.rb +70 -0
  63. data/lib/textus/hooks/dispatcher.rb +49 -0
  64. data/lib/textus/hooks/loader.rb +26 -0
  65. data/lib/textus/hooks/registry.rb +73 -0
  66. data/lib/textus/init.rb +14 -11
  67. data/lib/textus/intro.rb +16 -18
  68. data/lib/textus/key/distance.rb +55 -0
  69. data/lib/textus/key/grammar.rb +33 -0
  70. data/lib/textus/key/path.rb +17 -0
  71. data/lib/textus/manifest/entry.rb +199 -0
  72. data/lib/textus/manifest.rb +20 -254
  73. data/lib/textus/migrate_keys.rb +1 -1
  74. data/lib/textus/projection.rb +6 -5
  75. data/lib/textus/proposal.rb +4 -4
  76. data/lib/textus/refresh.rb +17 -17
  77. data/lib/textus/schema/tools.rb +89 -0
  78. data/lib/textus/store/audit_log.rb +71 -0
  79. data/lib/textus/store/mover.rb +121 -0
  80. data/lib/textus/store/reader.rb +67 -0
  81. data/lib/textus/store/staleness.rb +133 -0
  82. data/lib/textus/store/validator.rb +56 -0
  83. data/lib/textus/store/view.rb +29 -0
  84. data/lib/textus/store/writer.rb +132 -0
  85. data/lib/textus/store.rb +26 -527
  86. data/lib/textus/version.rb +2 -2
  87. data/lib/textus.rb +14 -29
  88. metadata +78 -8
  89. data/lib/textus/audit_log.rb +0 -32
  90. data/lib/textus/builtin_actions.rb +0 -68
  91. data/lib/textus/extension_registry.rb +0 -61
  92. data/lib/textus/extensions.rb +0 -33
  93. data/lib/textus/key_distance.rb +0 -53
  94. data/lib/textus/schema_tools.rb +0 -87
  95. data/lib/textus/store_view.rb +0 -27
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.4.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,25 +109,75 @@ 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
120
+ - lib/textus/cli/group.rb
121
+ - lib/textus/cli/group/hook.rb
122
+ - lib/textus/cli/group/key.rb
123
+ - lib/textus/cli/group/schema.rb
124
+ - lib/textus/cli/verb.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
102
149
  - lib/textus/dependencies.rb
103
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
104
161
  - lib/textus/entry.rb
162
+ - lib/textus/entry/base.rb
105
163
  - lib/textus/entry/json.rb
106
164
  - lib/textus/entry/markdown.rb
107
165
  - lib/textus/entry/text.rb
108
166
  - lib/textus/entry/yaml.rb
167
+ - lib/textus/envelope.rb
109
168
  - lib/textus/errors.rb
110
169
  - lib/textus/etag.rb
111
- - lib/textus/extension_registry.rb
112
- - 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
113
174
  - lib/textus/init.rb
114
175
  - lib/textus/intro.rb
115
- - lib/textus/key_distance.rb
176
+ - lib/textus/key/distance.rb
177
+ - lib/textus/key/grammar.rb
178
+ - lib/textus/key/path.rb
116
179
  - lib/textus/manifest.rb
180
+ - lib/textus/manifest/entry.rb
117
181
  - lib/textus/migrate_keys.rb
118
182
  - lib/textus/mustache.rb
119
183
  - lib/textus/projection.rb
@@ -122,9 +186,15 @@ files:
122
186
  - lib/textus/refresh.rb
123
187
  - lib/textus/role.rb
124
188
  - lib/textus/schema.rb
125
- - lib/textus/schema_tools.rb
189
+ - lib/textus/schema/tools.rb
126
190
  - lib/textus/store.rb
127
- - lib/textus/store_view.rb
191
+ - lib/textus/store/audit_log.rb
192
+ - lib/textus/store/mover.rb
193
+ - lib/textus/store/reader.rb
194
+ - lib/textus/store/staleness.rb
195
+ - lib/textus/store/validator.rb
196
+ - lib/textus/store/view.rb
197
+ - lib/textus/store/writer.rb
128
198
  - lib/textus/version.rb
129
199
  homepage: https://github.com/patrick204nqh/textus
130
200
  licenses:
@@ -1,32 +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
- File.foreach(@path).map { |l| l.chomp.split("\t") }
14
- .select { |row| row[3] == key && %w[put delete].include?(row[2]) }
15
- .last&.fetch(1)
16
- end
17
-
18
- def append(role:, verb:, key:, etag_before:, etag_after:, extras: nil)
19
- fields = [
20
- Time.now.utc.iso8601, role, verb, key,
21
- etag_before || "NULL",
22
- etag_after || "NULL"
23
- ]
24
- fields << JSON.generate(extras) if extras && !extras.empty?
25
- line = fields.join("\t") + "\n"
26
- File.open(@path, File::WRONLY | File::APPEND | File::CREAT, 0o644) do |f|
27
- f.flock(File::LOCK_EX)
28
- f.write(line)
29
- end
30
- end
31
- end
32
- 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
- { frontmatter: {}, 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
- { frontmatter: {}, 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
- { frontmatter: {}, 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
- { frontmatter: {}, 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
- { frontmatter: {}, body: YAML.dump(items) }
64
- end
65
- end
66
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
67
- end
68
- end
@@ -1,61 +0,0 @@
1
- module Textus
2
- class ExtensionRegistry
3
- EVENTS = %i[put delete refresh build accept].freeze
4
-
5
- def initialize
6
- @actions = {}
7
- @reducers = {}
8
- @hooks = {}
9
- @doctor_checks = {}
10
- end
11
-
12
- def register_action(name, &blk)
13
- name = name.to_sym
14
- raise UsageError.new("action '#{name}' already registered") if @actions.key?(name)
15
-
16
- @actions[name] = blk
17
- end
18
-
19
- def register_reducer(name, &blk)
20
- name = name.to_sym
21
- raise UsageError.new("reducer '#{name}' already registered") if @reducers.key?(name)
22
-
23
- @reducers[name] = blk
24
- end
25
-
26
- def register_hook(event, name, &blk)
27
- event = event.to_sym
28
- raise UsageError.new("unknown event: #{event}") unless EVENTS.include?(event)
29
-
30
- (@hooks[event] ||= []) << { name: name.to_sym, callable: blk }
31
- end
32
-
33
- def register_doctor_check(name, &blk)
34
- name = name.to_sym
35
- raise UsageError.new("doctor_check '#{name}' already registered") if @doctor_checks.key?(name)
36
-
37
- @doctor_checks[name] = blk
38
- end
39
-
40
- def action(name)
41
- @actions[name.to_sym] or raise UsageError.new("unknown action: #{name}")
42
- end
43
-
44
- def reducer(name)
45
- @reducers[name.to_sym] or raise UsageError.new("unknown reducer: #{name}")
46
- end
47
-
48
- def hooks(event)
49
- @hooks[event.to_sym] || []
50
- end
51
-
52
- def doctor_check(name)
53
- @doctor_checks[name.to_sym] or raise UsageError.new("unknown doctor_check: #{name}")
54
- end
55
-
56
- def action_names = @actions.keys
57
- def reducer_names = @reducers.keys
58
- def hook_events = @hooks.keys
59
- def doctor_check_names = @doctor_checks.keys
60
- end
61
- end
@@ -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,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
- fm = env["frontmatter"]
10
- schema = {
11
- "name" => name,
12
- "required" => fm.keys,
13
- "optional" => [],
14
- "fields" => fm.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["frontmatter"])
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
- fm = env["frontmatter"]
55
- changed = false
56
- renames.each do |old, new|
57
- if fm.key?(old)
58
- fm[new] = fm.delete(old)
59
- changed = true
60
- end
61
- end
62
- next unless changed
63
-
64
- store.put(row[:key], frontmatter: fm, 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,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