briefly 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +95 -10
- data/README.md +235 -115
- data/briefly.gemspec +1 -1
- data/lib/briefly/builder.rb +58 -42
- data/lib/briefly/errors.rb +1 -1
- data/lib/briefly/facade.rb +79 -31
- data/lib/briefly/rails/config.rb +35 -0
- data/lib/briefly/rails/db.rb +33 -8
- data/lib/briefly/rails/env.rb +27 -0
- data/lib/briefly/rails/instrument.rb +29 -0
- data/lib/briefly/rails/reload.rb +1 -1
- data/lib/briefly/rails/view.rb +27 -0
- data/lib/briefly/rails.rb +7 -50
- data/lib/briefly/rescues.rb +47 -0
- data/lib/briefly/shortcut.rb +104 -0
- data/lib/briefly/version.rb +1 -1
- data/lib/briefly.rb +26 -9
- data/lib/generators/briefly/install/install_generator.rb +29 -0
- data/lib/generators/briefly/install/templates/briefly.rb.tt +47 -0
- data/sig/briefly/builder.rbs +18 -8
- data/sig/briefly/facade.rbs +26 -12
- data/sig/briefly/rails.rbs +7 -2
- data/sig/briefly/rescues.rbs +21 -0
- data/sig/briefly/{definition.rbs → shortcut.rbs} +8 -4
- data/sig/briefly.rbs +7 -10
- metadata +12 -6
- data/lib/briefly/definition.rb +0 -64
- data/lib/briefly/error_registry.rb +0 -40
- data/sig/briefly/error_registry.rbs +0 -22
data/README.md
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
# briefly
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://github.com/svyatov/briefly/actions/workflows/main.yml)
|
|
5
|
-
[](https://codecov.io/gh/svyatov/briefly)
|
|
6
|
-
[](https://rubydoc.info/gems/briefly)
|
|
7
|
-
[](https://www.ruby-lang.org)
|
|
8
|
-
[](https://github.com/svyatov/briefly/tree/main/sig)
|
|
3
|
+
A terse, curated facade over your application's most reached-for objects.
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
[](https://rubygems.org/gems/briefly)
|
|
6
|
+
[](https://github.com/svyatov/briefly/actions/workflows/main.yml)
|
|
7
|
+
[](https://app.codecov.io/gh/svyatov/briefly)
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
- **Ruby 3.2 and up.** Rails is optional: the gem does not declare it, and `Briefly::Rails` is
|
|
10
|
+
autoloaded only when you name it. The Rails packs themselves need Rails 7.2 or newer.
|
|
11
|
+
- **25 shortcuts across six packs.** Config, env, view, db, instrument and reload, each usable on
|
|
12
|
+
its own or taken together through the `"rails"` umbrella.
|
|
13
|
+
- **One runtime dependency.** [candor](https://github.com/svyatov/candor), which is itself
|
|
14
|
+
dependency-free.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bundle add briefly
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then, in `config/initializers/app.rb`:
|
|
20
21
|
|
|
21
22
|
```ruby
|
|
22
|
-
# config/initializers/app.rb
|
|
23
23
|
App = Briefly.define do
|
|
24
|
-
use
|
|
24
|
+
use "rails"
|
|
25
25
|
shortcut(:redis) { REDIS_POOL }
|
|
26
26
|
end
|
|
27
27
|
|
|
@@ -32,27 +32,35 @@ App.redis # => REDIS_POOL
|
|
|
32
32
|
App.local? # => true in development and test
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Every app grows an `App` module full of `def self.config = Rails.configuration`. `briefly` gives
|
|
36
|
+
you that module without writing it, as real methods. There is no `method_missing`, so `respond_to?`,
|
|
37
|
+
console tab-completion and test stubbing all work. Each shortcut carries its body's `arity` and
|
|
38
|
+
parameter kinds (keyword names are exact, positionals get generated ones), and its `source_location`
|
|
39
|
+
is the block you declared, so jump-to-definition lands in your initializer rather than inside the gem.
|
|
40
|
+
That fabrication is [candor](https://github.com/svyatov/candor), extracted from this gem.
|
|
36
41
|
|
|
37
|
-
|
|
38
|
-
gem "briefly"
|
|
39
|
-
```
|
|
42
|
+
## The Rails generator
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
In a Rails app, `rails g briefly:install` writes `config/initializers/briefly.rb`: a working `App`
|
|
45
|
+
facade plus a commented, concern-grouped map of every shortcut the `rails` pack gives you. Pass a
|
|
46
|
+
name (`rails g briefly:install Facade`) to call the constant something else. Re-run it after upgrading
|
|
47
|
+
to refresh the map; Rails prompts before overwriting. The generator loads only under `rails generate`,
|
|
48
|
+
so it adds no runtime dependency.
|
|
43
49
|
|
|
44
50
|
## Core concepts
|
|
45
51
|
|
|
46
52
|
A **facade** is the object `Briefly.define` returns. You assign it to a constant of your choosing;
|
|
47
|
-
`briefly` never installs one for you. Multiple independent facades share no state
|
|
53
|
+
`briefly` never installs one for you. Multiple independent facades share no state, so an initializer
|
|
54
|
+
can declare as many as it needs:
|
|
48
55
|
|
|
49
56
|
```ruby
|
|
50
|
-
App = Briefly.define { use
|
|
57
|
+
App = Briefly.define { use "rails" }
|
|
51
58
|
Admin = Briefly.define { shortcut(:audit_log) { AuditLog } }
|
|
52
59
|
```
|
|
53
60
|
|
|
54
|
-
A **shortcut** is a name plus a body. The body is always attached to `shortcut
|
|
55
|
-
place
|
|
61
|
+
A **shortcut** is a name plus a body. The body is always attached to `shortcut`, one block in one
|
|
62
|
+
place, and runs bound to the facade, so it can reach the facade's other shortcuts. Inside the
|
|
63
|
+
initializer's `Briefly.define` block:
|
|
56
64
|
|
|
57
65
|
```ruby
|
|
58
66
|
Briefly.define do
|
|
@@ -64,66 +72,90 @@ end
|
|
|
64
72
|
```
|
|
65
73
|
|
|
66
74
|
Aliases are real methods delegating to the same body and the same memo cell. Redeclaring a name
|
|
67
|
-
overrides it silently
|
|
75
|
+
overrides it silently; that is how you override a pack's shortcut.
|
|
68
76
|
|
|
69
77
|
## `memoize`
|
|
70
78
|
|
|
71
|
-
|
|
79
|
+
`shortcut` returns the shortcut you refine. Chain `.memoize` onto it in your initializer to cache the
|
|
80
|
+
value, computed once, then reused for the process lifetime:
|
|
72
81
|
|
|
73
82
|
```ruby
|
|
74
83
|
Briefly.define do
|
|
75
|
-
shortcut(:catalog) { Catalog.load_from_disk }
|
|
76
|
-
memoize :catalog
|
|
84
|
+
shortcut(:catalog) { Catalog.load_from_disk }.memoize
|
|
77
85
|
end
|
|
78
86
|
```
|
|
79
87
|
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
To memoize a shortcut you didn't declare yourself, one a pack installed, say, call `shortcut(name)`
|
|
89
|
+
with no block to fetch it, then chain onto that. A bodiless `shortcut` never re-declares, so this
|
|
90
|
+
also belongs in the initializer:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
Briefly.define do
|
|
94
|
+
use "rails"
|
|
95
|
+
shortcut(:cache).memoize
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Memoization is permanent for the process; the core has no idea what a "reload" is. It caches
|
|
100
|
+
`nil` and `false` correctly, and a body that takes any parameter (positional, keyword, or block)
|
|
82
101
|
cannot be memoized (raises at build time). The compiled method takes no arguments either, so
|
|
83
102
|
`App.catalog(:x)` is an `ArgumentError`, never a silent cache hit. If a memoized body raises and a
|
|
84
|
-
handler supplies a fallback,
|
|
103
|
+
handler supplies a fallback, that shortcut's own cell is not filled: the transient failure is
|
|
85
104
|
retried on next call.
|
|
86
105
|
|
|
87
106
|
That guarantee is per-cell, and does not compose. A memoized shortcut whose body *reads* a
|
|
88
|
-
rescue-backed shortcut succeeds, so its own value
|
|
89
|
-
process lifetime, even after the inner shortcut recovers
|
|
107
|
+
rescue-backed shortcut succeeds, so its own value (containing the fallback) is cached for the
|
|
108
|
+
process lifetime, even after the inner shortcut recovers. Two initializer declarations that trap
|
|
109
|
+
you:
|
|
90
110
|
|
|
91
111
|
```ruby
|
|
92
|
-
shortcut(:flaky) { external_call }
|
|
93
|
-
memoize
|
|
94
|
-
shortcut(:summary) { "build #{flaky}" } # caches "build unknown" forever
|
|
95
|
-
memoize :summary # <- don't memoize over a rescue-backed shortcut
|
|
112
|
+
shortcut(:flaky) { external_call }.rescue_from(SomeError) { "unknown" }.memoize
|
|
113
|
+
shortcut(:summary) { "build #{flaky}" }.memoize # <- caches "build unknown" forever
|
|
96
114
|
```
|
|
97
115
|
|
|
98
|
-
Clearing is a neutral primitive
|
|
116
|
+
Clearing is a neutral primitive. Management lives behind one accessor, `App.briefly`, so names like
|
|
117
|
+
`configure`, `shortcuts`, `shortcut?` and `clear_memos!` stay yours to use as shortcuts. Call it
|
|
118
|
+
from application code:
|
|
99
119
|
|
|
100
120
|
```ruby
|
|
101
|
-
App.clear_memos! # => App (thread-safe
|
|
121
|
+
App.briefly.clear_memos! # => App (thread-safe)
|
|
102
122
|
```
|
|
103
123
|
|
|
124
|
+
Reclaim one of those names and `App.configure` calls *your* shortcut; the old management call now
|
|
125
|
+
lives only at `App.briefly.configure`. Worth knowing when porting pre-0.2.0 code: a leftover
|
|
126
|
+
`App.configure { ... }` won't raise if a `configure` shortcut exists, it just runs the shortcut and,
|
|
127
|
+
like any non-yielding method, drops the block.
|
|
128
|
+
|
|
104
129
|
*When* to clear is a pack's business. See [Reloading](#reloading-and-thread-safety).
|
|
105
130
|
|
|
106
131
|
## `rescue_from`
|
|
107
132
|
|
|
108
|
-
|
|
109
|
-
|
|
133
|
+
The handler's return value becomes the shortcut's return value. To guard one shortcut, chain
|
|
134
|
+
`.rescue_from(error_class) { |e, name| ... }` onto it. The top-level `rescue_from` verb is for what a
|
|
135
|
+
single shortcut can't voice: a facade-wide handler, consulted after each shortcut's own. Error class
|
|
136
|
+
first, always. Both forms belong in the initializer:
|
|
110
137
|
|
|
111
138
|
```ruby
|
|
112
139
|
Briefly.define do
|
|
113
|
-
use
|
|
114
|
-
shortcut(:redis) { REDIS_POOL }
|
|
115
|
-
rescue_from(
|
|
116
|
-
rescue_from(StandardError) { |e, name| Rails.logger.warn("#{name}: #{e.message}"); raise }
|
|
140
|
+
use "rails"
|
|
141
|
+
shortcut(:redis) { REDIS_POOL }.rescue_from(Redis::BaseError) { |e| Sentry.capture_exception(e); nil }
|
|
142
|
+
rescue_from(StandardError) { |e, name| Rails.logger.warn("#{name}: #{e.message}"); raise } # facade-wide
|
|
117
143
|
end
|
|
118
144
|
```
|
|
119
145
|
|
|
120
|
-
|
|
146
|
+
The top-level `rescue_from` takes no shortcut names. Pass any and it raises `ArgumentError`, pointing
|
|
147
|
+
you at `shortcut(name).rescue_from(...)`. Scoping a handler to a shortcut lives in one channel, the
|
|
148
|
+
shortcut itself, so its name is never written twice to annotate it. To share one handler across a few
|
|
149
|
+
shortcuts, chain `.rescue_from` onto each; to cover them all, register it facade-wide.
|
|
150
|
+
|
|
151
|
+
Unlike a shortcut body, a handler is not bound to the facade. It is called as
|
|
121
152
|
`handler.call(error, name)`, so `self` stays whatever it was where you wrote the block. Reach for
|
|
122
153
|
constants (`Rails.logger`, `Sentry`) rather than bare shortcut names inside a handler.
|
|
123
154
|
|
|
124
|
-
>
|
|
125
|
-
> `
|
|
126
|
-
>
|
|
155
|
+
> [!WARNING]
|
|
156
|
+
> A facade-wide `rescue_from(StandardError)` catches your own bugs, not just your app's.
|
|
157
|
+
> `briefly` cannot tell an error raised *by* a shortcut body from one raised *about* the call; a
|
|
158
|
+
> typo and a dead Redis both arrive as a `StandardError`. Written in an initializer:
|
|
127
159
|
>
|
|
128
160
|
> ```ruby
|
|
129
161
|
> shortcut(:host) { Rails.aplication.config.host } # typo -> NoMethodError
|
|
@@ -134,23 +166,28 @@ constants (`Rails.logger`, `Sentry`) rather than bare shortcut names inside a ha
|
|
|
134
166
|
>
|
|
135
167
|
> Three ways out, in order of preference: scope handlers to the shortcuts that can actually fail;
|
|
136
168
|
> match the narrowest error class you mean; and if you do want a facade-wide handler, make it log
|
|
137
|
-
> and `raise
|
|
169
|
+
> and `raise`. A bare `raise` inside a handler re-raises the original, backtrace intact.
|
|
138
170
|
>
|
|
139
171
|
> A bad *call* from outside the facade is not affected. Every shortcut carries its body's arity, so
|
|
140
172
|
> `App.env(1)` raises `ArgumentError` at the call site, before any handler is consulted. One shortcut
|
|
141
173
|
> body calling another with a bad argument list is a different matter: that raises *inside* the
|
|
142
174
|
> calling body, where the calling shortcut's own handler sees it like any other error.
|
|
143
175
|
|
|
144
|
-
>
|
|
145
|
-
> `
|
|
176
|
+
> [!IMPORTANT]
|
|
177
|
+
> `{}` needs parentheses, on the standalone `rescue_from` only. `rescue_from StandardError { ... }`
|
|
178
|
+
> binds the block to `StandardError`, not to `rescue_from`, and raises `NoMethodError`. Use either
|
|
179
|
+
> form inside your `Briefly.define` block:
|
|
146
180
|
>
|
|
147
181
|
> ```ruby
|
|
148
|
-
> rescue_from StandardError
|
|
149
|
-
> rescue_from(StandardError
|
|
182
|
+
> rescue_from StandardError do |e| ... end # do/end, no parens
|
|
183
|
+
> rescue_from(StandardError) { |e| ... } # braces REQUIRE parens
|
|
150
184
|
> ```
|
|
185
|
+
>
|
|
186
|
+
> Scoping to the shortcut sidesteps this: `shortcut(:redis) { ... }.rescue_from(Redis::BaseError) { |e| ... }`
|
|
187
|
+
> already has a receiver and parens, so `{ }` binds where you mean it.
|
|
151
188
|
|
|
152
189
|
Handlers are plain procs, so `{ |e| }` and `{ |e, name| }` both work. Re-raising propagates. If no
|
|
153
|
-
handler matches, the original error propagates unchanged
|
|
190
|
+
handler matches, the original error propagates unchanged, never silently swallowed. Only
|
|
154
191
|
`StandardError` and its descendants participate.
|
|
155
192
|
|
|
156
193
|
`Briefly.rescue_from(error_class, &handler)` registers a global default across every facade. It
|
|
@@ -162,23 +199,23 @@ For a raised error, the first `is_a?` match wins, searching in this order:
|
|
|
162
199
|
|
|
163
200
|
| # | Level | Within the level |
|
|
164
201
|
|---|-------|------------------|
|
|
165
|
-
| 1 |
|
|
166
|
-
| 2 | Facade handlers
|
|
202
|
+
| 1 | This shortcut's own handlers | last registered first |
|
|
203
|
+
| 2 | Facade-wide handlers | last registered first |
|
|
167
204
|
| 3 | Global handlers (`Briefly.rescue_from`) | last registered first |
|
|
168
205
|
|
|
169
|
-
|
|
206
|
+
With no match, the error propagates.
|
|
170
207
|
|
|
171
208
|
## `namespace`
|
|
172
209
|
|
|
173
|
-
`namespace` groups shortcuts behind a name, so the root keyspace stays yours
|
|
210
|
+
`namespace` groups shortcuts behind a name, so the root keyspace stays yours. In the initializer,
|
|
211
|
+
and then anywhere in the app:
|
|
174
212
|
|
|
175
213
|
```ruby
|
|
176
214
|
App = Briefly.define do
|
|
177
215
|
shortcut(:redis) { REDIS_POOL }
|
|
178
216
|
|
|
179
217
|
namespace :db do
|
|
180
|
-
shortcut(:pool) { ActiveRecord::Base.connection_pool }
|
|
181
|
-
memoize :pool
|
|
218
|
+
shortcut(:pool) { ActiveRecord::Base.connection_pool }.memoize
|
|
182
219
|
end
|
|
183
220
|
end
|
|
184
221
|
|
|
@@ -187,9 +224,9 @@ App.db # => #<Briefly::Facade shortcuts=[:pool]>
|
|
|
187
224
|
App.db.pool # => the pool
|
|
188
225
|
```
|
|
189
226
|
|
|
190
|
-
A namespace is a child `Briefly::Facade`, reached by a real method like any other shortcut
|
|
227
|
+
A namespace is a child `Briefly::Facade`, reached by a real method like any other shortcut, so
|
|
191
228
|
`App.db` is a value you can pass around, and `App.db.pool` is not a `method_missing` trick. It takes
|
|
192
|
-
the whole DSL: `shortcut
|
|
229
|
+
the whole DSL: `shortcut` (and the shortcut it returns), `rescue_from`, `use`, and further namespaces.
|
|
193
230
|
|
|
194
231
|
`clear_memos!` cascades, so one `Briefly::Rails::Reload` on the root clears the whole tree. The child
|
|
195
232
|
is created once and reused, so its memos survive a later `configure`.
|
|
@@ -206,8 +243,9 @@ Two limits, both deliberate:
|
|
|
206
243
|
|
|
207
244
|
## Packs
|
|
208
245
|
|
|
209
|
-
A pack is
|
|
210
|
-
|
|
246
|
+
A pack is any object responding to `#install(builder, **opts)`. Options are optional: Ruby drops an
|
|
247
|
+
empty `**` splat, so a pack taking none needs no keyword parameter. Put the pack in a file your
|
|
248
|
+
app already loads, such as `lib/redis_pack.rb`, and `use` it from the initializer:
|
|
211
249
|
|
|
212
250
|
```ruby
|
|
213
251
|
module RedisPack
|
|
@@ -215,21 +253,21 @@ module RedisPack
|
|
|
215
253
|
|
|
216
254
|
def install(builder)
|
|
217
255
|
builder.shortcut(:redis) { ConnectionPool.new { Redis.new } }
|
|
218
|
-
|
|
219
|
-
|
|
256
|
+
.memoize
|
|
257
|
+
.rescue_from(Redis::CannotConnectError) { nil }
|
|
220
258
|
end
|
|
221
259
|
end
|
|
222
260
|
|
|
223
261
|
Api = Briefly.define { use RedisPack }
|
|
224
262
|
```
|
|
225
263
|
|
|
226
|
-
Packs may `use` other packs, and may reach `builder.facade` to wire lifecycle hooks
|
|
264
|
+
Packs may `use` other packs, and may reach `builder.facade` to wire lifecycle hooks, which is
|
|
227
265
|
exactly what `Briefly::Rails::Reload` does. The core stays framework-agnostic; packs do not have to.
|
|
228
266
|
|
|
229
267
|
### Options
|
|
230
268
|
|
|
231
269
|
Keywords passed to `use` reach the pack's `install`. Ruby drops an empty `**` splat, so a pack that
|
|
232
|
-
takes no options needs no keyword parameter
|
|
270
|
+
takes no options needs no keyword parameter. The same pack file, now taking a `url:`:
|
|
233
271
|
|
|
234
272
|
```ruby
|
|
235
273
|
module RedisPack
|
|
@@ -246,7 +284,8 @@ Api = Briefly.define { use RedisPack, url: "redis://cache:6379" }
|
|
|
246
284
|
### Short names
|
|
247
285
|
|
|
248
286
|
`Briefly.register` maps a name to a pack, so `use` can take a string or symbol. There is no
|
|
249
|
-
inflection and no path guessing
|
|
287
|
+
inflection and no path guessing; the registry is the only source of truth. Register in the
|
|
288
|
+
initializer, before the `define` that names it:
|
|
250
289
|
|
|
251
290
|
```ruby
|
|
252
291
|
Briefly.register("myapp/redis", RedisPack) # a pack object
|
|
@@ -256,7 +295,8 @@ Api = Briefly.define { use "myapp/redis", url: "redis://cache:6379" }
|
|
|
256
295
|
```
|
|
257
296
|
|
|
258
297
|
An unregistered name raises `Briefly::UnknownPackError`. The packs this gem ships are registered as
|
|
259
|
-
`"rails"`, `"rails/config"`, `"rails/env"`, `"rails/view"`, `"rails/db"
|
|
298
|
+
`"rails"`, `"rails/config"`, `"rails/env"`, `"rails/view"`, `"rails/db"`, `"rails/instrument"` and
|
|
299
|
+
`"rails/reload"`.
|
|
260
300
|
|
|
261
301
|
### `Briefly::Rails`
|
|
262
302
|
|
|
@@ -265,42 +305,55 @@ An unregistered name raises `Briefly::UnknownPackError`. The packs this gem ship
|
|
|
265
305
|
| `config` | `c` | `Rails.configuration` |
|
|
266
306
|
| `config_x` | `x` | `Rails.configuration.x` |
|
|
267
307
|
| `env` | | `Rails.env` |
|
|
268
|
-
| `production?` `development?` `test?` `local?` | | `Rails.env.*` |
|
|
308
|
+
| `production?` `development?` `test?` `local?` | `prod?` `dev?` | `Rails.env.*` |
|
|
269
309
|
| `root` | | `Rails.root` |
|
|
270
310
|
| `cache` | | `Rails.cache` |
|
|
271
311
|
| `logger` | `log` | `Rails.logger` |
|
|
272
312
|
| `credentials` | `cred` | `Rails.application.credentials` |
|
|
313
|
+
| `error` | | `Rails.error` |
|
|
314
|
+
| `config_for` | | `Rails.application.config_for(name, **opts)` |
|
|
273
315
|
| `helpers` | `h` | `ApplicationController.helpers` |
|
|
274
316
|
| `routes` | `r` | `Rails.application.routes.url_helpers` |
|
|
275
317
|
| `renderer` | | `ApplicationController.renderer` |
|
|
276
318
|
| `render` | | forwards to `renderer.render` |
|
|
319
|
+
| `instrument` | | `ActiveSupport::Notifications.instrument(name, payload) { }` |
|
|
277
320
|
|
|
278
321
|
Plus `db`, a namespace holding `Briefly::Rails::DB`.
|
|
279
322
|
|
|
323
|
+
`prod?` and `dev?` alias `production?` and `development?`. `error` is the framework's handled-error
|
|
324
|
+
reporter, so `App.error.report(e)` and `App.error.handle { }` reach it off one live lookup.
|
|
325
|
+
`config_for` reads a per-environment YAML config on every call and forwards any extra keyword (such
|
|
326
|
+
as `env:`) to `Rails.application.config_for`: it takes an argument, so it never memoizes; compose one
|
|
327
|
+
that does by chaining `.memoize` onto a shortcut: `shortcut(:payments) { config_for(:payments) }.memoize`.
|
|
328
|
+
|
|
280
329
|
Requires Rails >= 7.2. There is no `secrets` shortcut: `Rails.application.secrets` was removed in
|
|
281
330
|
7.2. Use `credentials`.
|
|
282
331
|
|
|
283
|
-
|
|
332
|
+
Nothing in the pack is memoized. `helpers`, `routes` and `renderer` are live lookups: Rails
|
|
284
333
|
already caches them on objects it refreshes on reload, so caching them again would only go stale.
|
|
285
334
|
It still composes `Briefly::Rails::Reload`, because *your* memoized shortcuts need clearing.
|
|
286
335
|
|
|
287
|
-
Need a custom renderer? Override it
|
|
336
|
+
Need a custom renderer? Override it in the initializer; last declaration wins:
|
|
288
337
|
|
|
289
338
|
```ruby
|
|
290
339
|
App = Briefly.define do
|
|
291
|
-
use
|
|
340
|
+
use "rails"
|
|
292
341
|
shortcut(:renderer) { ApplicationController.renderer.new(http_host: x.domain, https: !development?) }
|
|
293
342
|
end
|
|
294
343
|
```
|
|
295
344
|
|
|
296
|
-
`Briefly::Rails` is an umbrella over
|
|
345
|
+
`Briefly::Rails` is an umbrella over six packs, each usable on its own:
|
|
297
346
|
|
|
298
347
|
| pack | short name | shortcuts |
|
|
299
348
|
|---|---|---|
|
|
300
|
-
| `Briefly::Rails::Config` | `"rails/config"` | `config`, `config_x`, `root`, `cache`, `logger`, `credentials` |
|
|
349
|
+
| `Briefly::Rails::Config` | `"rails/config"` | `config`, `config_x`, `root`, `cache`, `logger`, `credentials`, `error`, `config_for` |
|
|
301
350
|
| `Briefly::Rails::Env` | `"rails/env"` | `env` and its predicates |
|
|
302
351
|
| `Briefly::Rails::View` | `"rails/view"` | `helpers`, `routes`, `renderer`, `render` |
|
|
303
|
-
| `Briefly::Rails::DB` | `"rails/db"` | `connection`, `transaction`, `query` |
|
|
352
|
+
| `Briefly::Rails::DB` | `"rails/db"` | `connection`, `transaction`, `select`, `query`, `connected_to`, `reading`, `writing` |
|
|
353
|
+
| `Briefly::Rails::Instrument` | `"rails/instrument"` | `instrument` |
|
|
354
|
+
| `Briefly::Rails::Reload` | `"rails/reload"` | none; clears memos on every code reload |
|
|
355
|
+
|
|
356
|
+
Take only the parts you want, in the initializer:
|
|
304
357
|
|
|
305
358
|
```ruby
|
|
306
359
|
Worker = Briefly.define do
|
|
@@ -314,59 +367,101 @@ end
|
|
|
314
367
|
|
|
315
368
|
| shortcut | aliases | value |
|
|
316
369
|
|---|---|---|
|
|
317
|
-
| `connection` | `conn` | `base.
|
|
370
|
+
| `connection` | `conn` | forwards keywords and the block to `base.with_connection`, yielding the connection and auto-releasing |
|
|
318
371
|
| `transaction` | `txn` | forwards keywords and the block to `base.transaction` |
|
|
319
|
-
| `
|
|
372
|
+
| `select` | | `base.with_connection { \|c\| c.select_all(sql) }`, a read (SELECT) on the cache-aware path |
|
|
373
|
+
| `query` | | `base.with_connection { \|c\| c.exec_query(sql) }`, arbitrary SQL, writes and DDL included |
|
|
374
|
+
| `connected_to` | | forwards every argument to `base.connected_to` (`role:`, `shard:`, `prevent_writes:`) |
|
|
375
|
+
| `reading` | | runs the block under the `:reading` role |
|
|
376
|
+
| `writing` | | runs the block under the `:writing` role |
|
|
377
|
+
|
|
378
|
+
Declare the namespace in the initializer, then call it from application code:
|
|
320
379
|
|
|
321
380
|
```ruby
|
|
322
381
|
App = Briefly.define do
|
|
323
|
-
use
|
|
382
|
+
use "rails"
|
|
324
383
|
namespace(:db2) { use "rails/db", base: "SecondaryApplicationRecord" }
|
|
325
384
|
end
|
|
326
385
|
|
|
327
|
-
App.db.txn { App.db.
|
|
328
|
-
App.
|
|
386
|
+
App.db.txn { App.db.select("select * from users where id = ?", 1) }
|
|
387
|
+
App.db.conn { |c| c.select_value("select count(*) from users") }
|
|
329
388
|
```
|
|
330
389
|
|
|
331
|
-
`
|
|
332
|
-
|
|
390
|
+
`select` and `query` are the two raw-SQL helpers, differing only in which adapter path they take.
|
|
391
|
+
`select(sql, *binds)` runs a read through `select_all`, the path Rails recommends for a raw SELECT,
|
|
392
|
+
returning an `ActiveRecord::Result` without clearing the query cache. `query(sql, *binds)` runs
|
|
393
|
+
arbitrary SQL through `exec_query`: reads, writes, and DDL all execute. The name tells you which
|
|
394
|
+
you're reaching for; neither polices the SQL it's handed, so `select` will happily run a write you
|
|
395
|
+
give it. The split is name and cache-path, not a runtime guard.
|
|
396
|
+
|
|
397
|
+
Both sanitize through `base.sanitize_sql_array` when binds are given, and pass the statement through
|
|
398
|
+
untouched when they are not. Positional and named binds both work, from application code:
|
|
333
399
|
|
|
334
400
|
```ruby
|
|
335
|
-
App.db.
|
|
336
|
-
App.db.
|
|
337
|
-
App.db.query("
|
|
401
|
+
App.db.select("select * from users where name like '%ada%'") # no binds, passed through
|
|
402
|
+
App.db.select("select * from users where id = ?", 123) # positional
|
|
403
|
+
App.db.query("update users set active = true where id = :id", id: 1) # named, a write via `query`
|
|
338
404
|
```
|
|
339
405
|
|
|
340
406
|
Binds are bound, never interpolated, so a value like `"x' OR '1'='1"` matches nothing. A bindless
|
|
341
|
-
statement is
|
|
342
|
-
`statement % values` branch and raise on the literal `%` above.
|
|
407
|
+
statement is not sanitized on purpose: `sanitize_sql_array` would fall through to its
|
|
408
|
+
`statement % values` branch and raise on the literal `%` above. That makes binds a safety contract,
|
|
409
|
+
not a convenience: always pass untrusted values as binds. The bindless path is unsanitized, and since
|
|
410
|
+
`query` now runs writes and DDL, an interpolated statement is a destructive-write injection risk, not
|
|
411
|
+
merely a read one.
|
|
412
|
+
|
|
413
|
+
`connection`/`conn` mirrors `transaction`: it forwards to `with_connection`, yields the leased
|
|
414
|
+
connection, and auto-releases at block exit, so nothing leaks outside a request. It requires a block;
|
|
415
|
+
there is no bare-lease accessor. Anyone who genuinely needs a held raw lease calls `lease_connection`
|
|
416
|
+
on their model directly.
|
|
417
|
+
|
|
418
|
+
`reading` and `writing` *route* a block. Everything inside runs under that connection role, so from
|
|
419
|
+
application code you send specific reads to a replica or pin a write to the primary:
|
|
420
|
+
|
|
421
|
+
```ruby
|
|
422
|
+
App.db.reading { App.db.select("select * from reports") } # runs on the replica
|
|
423
|
+
App.db.writing { Audit.create!(event: "export") } # pinned to the primary
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
They're sugar over `connected_to`, which is also a shortcut in its own right and forwards the whole
|
|
427
|
+
Rails surface: any role, not just reading and writing, plus `shard:` and `prevent_writes:`. Also
|
|
428
|
+
from application code:
|
|
429
|
+
|
|
430
|
+
```ruby
|
|
431
|
+
App.db.connected_to(role: :analytics) { Report.all.to_a } # a custom role
|
|
432
|
+
App.db.connected_to(shard: :one) { Order.find(id) } # a shard
|
|
433
|
+
App.db.reading(shard: :two) { ... } # sugar forwards the rest too
|
|
434
|
+
```
|
|
343
435
|
|
|
344
|
-
`
|
|
345
|
-
|
|
436
|
+
The `reading`/`writing` sugar pins its role: a `role:` you pass through it can't win, so `reading`
|
|
437
|
+
always reads. All of this needs a multi-database app: `base` must be `ActiveRecord::Base` or the
|
|
438
|
+
abstract class that declared `connects_to(database: { ... })`. On a concrete model `connected_to`
|
|
439
|
+
raises `NotImplementedError`, and in a single-database app with no matching connection
|
|
440
|
+
`App.db.reading { ... }` raises `ActiveRecord::ConnectionNotDefined`. These are a multi-DB tool, not a
|
|
441
|
+
role flag.
|
|
346
442
|
|
|
347
443
|
**Pass `base:` as a String, not the class.** A pack is `use`d from an initializer, where naming an
|
|
348
444
|
autoloadable constant is what Rails warns about, and the captured class would go stale on the first
|
|
349
|
-
code reload
|
|
445
|
+
code reload and stay stale, since `Reload` clears memos, not closures. A `Module` is accepted for
|
|
350
446
|
applications outside the autoloader, with that caveat.
|
|
351
447
|
|
|
352
448
|
The pack memoizes nothing and wires no lifecycle hook, so it works without a booted application.
|
|
353
449
|
|
|
354
450
|
### `Briefly::Rails::Reload`
|
|
355
451
|
|
|
356
|
-
`Briefly::Rails` composes it. Use it alone for a facade with no framework
|
|
357
|
-
memoizes objects holding on to reloadable application classes:
|
|
452
|
+
`Briefly::Rails` composes it. Use it alone, in an initializer, for a facade with no framework
|
|
453
|
+
shortcuts that still memoizes objects holding on to reloadable application classes:
|
|
358
454
|
|
|
359
455
|
```ruby
|
|
360
456
|
Admin = Briefly.define do
|
|
361
|
-
use
|
|
362
|
-
shortcut(:policy) { Admin::Policy.new }
|
|
363
|
-
memoize :policy
|
|
457
|
+
use "rails/reload"
|
|
458
|
+
shortcut(:policy) { Admin::Policy.new }.memoize
|
|
364
459
|
end
|
|
365
460
|
```
|
|
366
461
|
|
|
367
|
-
It registers `Rails.application.reloader.to_prepare { facade.clear_memos! }`, so memos are dropped
|
|
462
|
+
It registers `Rails.application.reloader.to_prepare { facade.briefly.clear_memos! }`, so memos are dropped
|
|
368
463
|
at boot and on every code reload in development. In production nothing reloads, so they persist for
|
|
369
|
-
the process lifetime. It raises `Briefly::Error` outside a booted app
|
|
464
|
+
the process lifetime. It raises `Briefly::Error` outside a booted app, so call it from an initializer.
|
|
370
465
|
|
|
371
466
|
The callback holds its facade for the process lifetime and cannot be deregistered. Install it on
|
|
372
467
|
long-lived facades assigned to constants, not on facades built per request.
|
|
@@ -377,26 +472,26 @@ Memo reads are lock-free against a frozen snapshot; writes swap in a new frozen
|
|
|
377
472
|
reentrant lock, so a memoized body may safely call another memoized shortcut. Under Puma, a
|
|
378
473
|
memoized body runs exactly once no matter how many threads race for it.
|
|
379
474
|
|
|
380
|
-
`clear_memos!` guarantees the *next* read recomputes. It does
|
|
475
|
+
`clear_memos!` guarantees the *next* read recomputes. It does not undo in-place mutation of an
|
|
381
476
|
already-handed-out object, and it does not survive a process restart. Two facades whose memoized
|
|
382
477
|
bodies call into each other can deadlock, like any pair of mutually-locking objects; don't do that.
|
|
383
478
|
|
|
384
479
|
## Testing
|
|
385
480
|
|
|
386
|
-
Shortcuts are real methods, so nothing special is needed:
|
|
481
|
+
Shortcuts are real methods, so nothing special is needed in a test file:
|
|
387
482
|
|
|
388
483
|
```ruby
|
|
389
484
|
allow(App).to receive(:redis).and_return(fake_redis) # rspec-mocks verifies it
|
|
390
485
|
App.stub(:redis, fake_redis) { ... } # minitest
|
|
391
486
|
```
|
|
392
487
|
|
|
393
|
-
Call `App.
|
|
394
|
-
globally registered handlers.
|
|
488
|
+
Call `App.briefly.clear_memos!` between examples if a memoized value would leak. `Briefly.rescues.clear`
|
|
489
|
+
resets globally registered handlers.
|
|
395
490
|
|
|
396
491
|
## Types
|
|
397
492
|
|
|
398
|
-
`briefly` ships RBS signatures in [`sig/`](sig). Shortcuts are compiled at runtime, so
|
|
399
|
-
see them
|
|
493
|
+
`briefly` ships RBS signatures in [`sig/`](sig). Shortcuts are compiled at runtime, so RBS cannot
|
|
494
|
+
see them. `Briefly.define`, `Facade`'s lifecycle API and the `Builder` DSL are fully typed, but
|
|
400
495
|
`App.config` is invisible to Steep. Declare the ones you rely on in your own `sig/`:
|
|
401
496
|
|
|
402
497
|
```rbs
|
|
@@ -406,10 +501,12 @@ def App.config: () -> untyped
|
|
|
406
501
|
def App.redis: () -> untyped
|
|
407
502
|
```
|
|
408
503
|
|
|
409
|
-
We
|
|
504
|
+
We do not fake this with an RBS-only `method_missing`; the gem has none.
|
|
410
505
|
|
|
411
506
|
## Migrating a hand-rolled `App`
|
|
412
507
|
|
|
508
|
+
What the initializer replaces, and what it becomes:
|
|
509
|
+
|
|
413
510
|
```ruby
|
|
414
511
|
# before
|
|
415
512
|
module App
|
|
@@ -426,19 +523,42 @@ end
|
|
|
426
523
|
|
|
427
524
|
# after
|
|
428
525
|
App = Briefly.define do
|
|
429
|
-
use
|
|
430
|
-
shortcut(:redis) { REDIS_POOL }
|
|
431
|
-
memoize :redis
|
|
526
|
+
use "rails"
|
|
527
|
+
shortcut(:redis) { REDIS_POOL }.memoize
|
|
432
528
|
end
|
|
433
529
|
```
|
|
434
530
|
|
|
435
531
|
`secrets` becomes `credentials`. `helpers`/`routes` stop going stale because they are no longer
|
|
436
|
-
memoized, and `redis`
|
|
532
|
+
memoized, and `redis` (which you *do* want memoized) is cleared on every dev reload by the Reload
|
|
437
533
|
pack that `Briefly::Rails` composes.
|
|
438
534
|
|
|
535
|
+
## Versioning
|
|
536
|
+
|
|
537
|
+
`briefly` follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The public API it
|
|
538
|
+
covers is what an application touches: `Briefly.define`, `Briefly.register`, `Briefly.pack`,
|
|
539
|
+
`Briefly.rescue_from` and `Briefly.rescues`; the `Builder` DSL inside a `define` block (`shortcut`,
|
|
540
|
+
`namespace`, `rescue_from`, `use`); the refinements `shortcut` returns (`memoize`, `rescue_from`);
|
|
541
|
+
the management surface behind `App.briefly`; the canonical names and aliases every shipped pack
|
|
542
|
+
installs; and the error classes under `Briefly::Error`.
|
|
543
|
+
|
|
544
|
+
Outside that promise: anything a shortcut compiles to internally, every `__`-prefixed private method
|
|
545
|
+
on `Facade`, and the RBS signatures in `sig/`, which describe the static surface and are revised
|
|
546
|
+
alongside it.
|
|
547
|
+
|
|
548
|
+
While the version stays below `1.0.0`, an incompatible change ships in a MINOR bump and a compatible
|
|
549
|
+
fix in a PATCH, so a MINOR bump is the one that may need you to read the changelog.
|
|
550
|
+
|
|
439
551
|
## Contributing
|
|
440
552
|
|
|
441
|
-
Bug reports and pull requests are welcome
|
|
553
|
+
Bug reports and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md), and
|
|
554
|
+
[CHANGELOG.md](CHANGELOG.md) for what has changed between releases.
|
|
555
|
+
|
|
556
|
+
Questions and bug reports both go to [GitHub Issues](https://github.com/svyatov/briefly/issues).
|
|
557
|
+
|
|
558
|
+
## Maintenance
|
|
559
|
+
|
|
560
|
+
One person maintains `briefly`, actively, in their spare time. Issues and pull requests get a reply,
|
|
561
|
+
and a bug on a supported Ruby or Rails version gets a fix. A large feature request may not.
|
|
442
562
|
|
|
443
563
|
## License
|
|
444
564
|
|
data/briefly.gemspec
CHANGED
|
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
spec.required_ruby_version = ">= 3.2.0"
|
|
19
19
|
|
|
20
20
|
spec.require_paths = ["lib"]
|
|
21
|
-
spec.files = Dir["lib/**/*.rb"] + Dir["sig/**/*"] +
|
|
21
|
+
spec.files = Dir["lib/**/*.{rb,tt}"] + Dir["sig/**/*"] +
|
|
22
22
|
%w[.yardopts CHANGELOG.md LICENSE.txt README.md briefly.gemspec]
|
|
23
23
|
|
|
24
24
|
spec.add_dependency "candor", "~> 0.2.0"
|