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