briefly 0.1.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 +7 -0
- data/.yardopts +6 -0
- data/CHANGELOG.md +43 -0
- data/LICENSE.txt +21 -0
- data/README.md +445 -0
- data/briefly.gemspec +31 -0
- data/lib/briefly/builder.rb +174 -0
- data/lib/briefly/definition.rb +64 -0
- data/lib/briefly/error_registry.rb +40 -0
- data/lib/briefly/errors.rb +18 -0
- data/lib/briefly/facade.rb +159 -0
- data/lib/briefly/rails/db.rb +52 -0
- data/lib/briefly/rails/reload.rb +39 -0
- data/lib/briefly/rails.rb +91 -0
- data/lib/briefly/version.rb +6 -0
- data/lib/briefly.rb +94 -0
- data/sig/briefly/builder.rbs +28 -0
- data/sig/briefly/definition.rbs +20 -0
- data/sig/briefly/error_registry.rbs +22 -0
- data/sig/briefly/facade.rbs +36 -0
- data/sig/briefly/rails.rbs +30 -0
- data/sig/briefly.rbs +57 -0
- data/sig/manifest.yaml +4 -0
- metadata +84 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b06a4a7f10ae74a9abd66fd882812d0af1a469d09916da7f898375b659bbdbdf
|
|
4
|
+
data.tar.gz: a72ed9ae644fa4bca67fa2f671486945ddd90872a78171864fa896b33d778443
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 72e7861ca614204a8536bdd8b0c40c48c480981c9cd494dc5d4de6b14bdbca1e016c0c658e728843b29afcf5b878020ae7e511869ad5ec666d825938afdfc885
|
|
7
|
+
data.tar.gz: ed27d0120874e0ff825a0ac0859b71c75947bccbe5c9dadf70b20de8d7f7eb46b3212e1f23b52e924e2a8b46b4c31b8950944875fcc94b341039e21b80c816c4
|
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and to [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
|
|
6
|
+
|
|
7
|
+
## Unreleased
|
|
8
|
+
|
|
9
|
+
## v0.1.0 (2026-07-10)
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `Briefly.define` builds a facade of real, introspectable, stubbable methods — no `method_missing`.
|
|
13
|
+
- `shortcut(canonical, *aliases, &body)` with argument, keyword and block forwarding; predicate and
|
|
14
|
+
bang names; last-declaration-wins overrides. Each shortcut compiles to a real method carrying its
|
|
15
|
+
body's `arity` and parameter kinds, with a `source_location` pointing at the block you declared —
|
|
16
|
+
keyword names are exact, positionals get generated ones (`__p0`, `__r1`, …). A wrong-arity call, a
|
|
17
|
+
missing required keyword and an unknown keyword raise `ArgumentError` at the call site, before any
|
|
18
|
+
`rescue_from` handler is consulted.
|
|
19
|
+
- `namespace(name) { ... }` — groups shortcuts behind a child facade, so `App.db.query` works without
|
|
20
|
+
burning root-level names. Takes the whole DSL, including nested namespaces. `clear_memos!` cascades
|
|
21
|
+
into it. `configure` stays atomic across the whole tree: a pass that raises anywhere leaves the root
|
|
22
|
+
and every namespace untouched. A `shortcut` of the same name overrides a namespace and drops the child.
|
|
23
|
+
- `memoize` — permanent per-process caching, correct for `nil`/`false`, never caching a rescued fallback.
|
|
24
|
+
- `rescue_from` — facade-scoped, facade-wide and global (`Briefly.rescue_from`) handlers whose return
|
|
25
|
+
value becomes the shortcut's value.
|
|
26
|
+
- `clear_memos!` / `reset!`, `configure`, `shortcuts`, `shortcut?`, `inspect`.
|
|
27
|
+
- Pack protocol: any object responding to `#install(builder, **opts)`. `use` accepts keywords,
|
|
28
|
+
forwarded to the pack's `install`. `Briefly.register(name, pack)` / `Briefly.pack(name)` provide a
|
|
29
|
+
pack registry, so `use "rails/db"` resolves; values may be a pack or a constant path resolved on
|
|
30
|
+
first use, and an unknown name raises `Briefly::UnknownPackError`.
|
|
31
|
+
- Method fabrication — the arity, `parameters` and `source_location` each shortcut reports — is
|
|
32
|
+
provided by [candor](https://github.com/svyatov/candor), the sole runtime dependency (candor itself
|
|
33
|
+
has none). Compiled bodies live under `Candor::BODY_PREFIX`; a shortcut may not take a name there.
|
|
34
|
+
- `Briefly::Rails` — `config`/`c`, `config_x`/`x`, `env` and its predicates, `root`, `cache`,
|
|
35
|
+
`logger`/`log`, `credentials`/`cred`, `helpers`/`h`, `routes`/`r`, `renderer`, `render`, plus a `db`
|
|
36
|
+
namespace. Nothing in the pack is memoized. Its `Config`, `Env` and `View` groups are usable alone.
|
|
37
|
+
- `Briefly::Rails::DB` — `connection`/`conn`, `transaction`/`txn`, `query`. `query` takes positional
|
|
38
|
+
(`query(sql, 1)`) or named (`query(sql, id: 1)`) binds, and passes a bindless statement through
|
|
39
|
+
unsanitized. Takes `base:` (default `"ApplicationRecord"`), resolved on every call so a reloaded
|
|
40
|
+
class is never captured. Memoizes nothing and wires no lifecycle hook, so it works without a booted
|
|
41
|
+
application.
|
|
42
|
+
- `Briefly::Rails::Reload` — clears memos via `Rails.application.reloader.to_prepare`.
|
|
43
|
+
- RBS signatures in `sig/`.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Leonid Svyatov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
# briefly
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/briefly)
|
|
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)
|
|
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.
|
|
12
|
+
|
|
13
|
+
Every app grows an `App` module full of `def self.config = Rails.configuration`. `briefly` gives
|
|
14
|
+
you that module without writing it, as **real methods** — no `method_missing`, so `respond_to?`,
|
|
15
|
+
console tab-completion and test stubbing all just work. Each shortcut carries its body's `arity` and
|
|
16
|
+
parameter kinds — keyword names are exact, positionals get generated ones — and its `source_location`
|
|
17
|
+
is the block you declared, so jump-to-definition lands in your initializer rather than inside the gem.
|
|
18
|
+
That fabrication is [candor](https://github.com/svyatov/candor), extracted from this gem and its only
|
|
19
|
+
runtime dependency; candor itself has none.
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# config/initializers/app.rb
|
|
23
|
+
App = Briefly.define do
|
|
24
|
+
use Briefly::Rails
|
|
25
|
+
shortcut(:redis) { REDIS_POOL }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
App.c # => Rails.configuration
|
|
29
|
+
App.x.stripe_key # => Rails.configuration.x.stripe_key
|
|
30
|
+
App.render(:receipt, locals: { order: })
|
|
31
|
+
App.redis # => REDIS_POOL
|
|
32
|
+
App.local? # => true in development and test
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
gem "briefly"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Ruby >= 3.2. The one runtime dependency is `candor`. Rails is **optional**: the gem does not declare
|
|
42
|
+
it, and `Briefly::Rails` is autoloaded only when you name it.
|
|
43
|
+
|
|
44
|
+
## Core concepts
|
|
45
|
+
|
|
46
|
+
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:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
App = Briefly.define { use Briefly::Rails }
|
|
51
|
+
Admin = Briefly.define { shortcut(:audit_log) { AuditLog } }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
A **shortcut** is a name plus a body. The body is always attached to `shortcut` — one block, one
|
|
55
|
+
place — and runs bound to the facade, so it can reach the facade's other shortcuts:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
Briefly.define do
|
|
59
|
+
shortcut(:config, :c) { Rails.configuration } # `:c` is an alias
|
|
60
|
+
shortcut(:timeout) { config.x.timeout } # bodies see other shortcuts
|
|
61
|
+
shortcut(:ready?) { !timeout.nil? } # `?` and `!` names are fine
|
|
62
|
+
shortcut(:fetch) { |key, &blk| store.get(key, &blk) } # args and blocks forward
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Aliases are real methods delegating to the same body and the same memo cell. Redeclaring a name
|
|
67
|
+
overrides it silently — that is how you override a pack's shortcut.
|
|
68
|
+
|
|
69
|
+
## `memoize`
|
|
70
|
+
|
|
71
|
+
Annotate an already-declared shortcut by name, on its own line:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
Briefly.define do
|
|
75
|
+
shortcut(:catalog) { Catalog.load_from_disk }
|
|
76
|
+
memoize :catalog
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Memoization is **permanent for the process** — the core has no idea what a "reload" is. It caches
|
|
81
|
+
`nil` and `false` correctly, and a body that takes any parameter — positional, keyword, or block —
|
|
82
|
+
cannot be memoized (raises at build time). The compiled method takes no arguments either, so
|
|
83
|
+
`App.catalog(:x)` is an `ArgumentError`, never a silent cache hit. If a memoized body raises and a
|
|
84
|
+
handler supplies a fallback, **that shortcut's own cell is not filled**: the transient failure is
|
|
85
|
+
retried on next call.
|
|
86
|
+
|
|
87
|
+
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 — containing the fallback — is cached for the
|
|
89
|
+
process lifetime, even after the inner shortcut recovers:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
shortcut(:flaky) { external_call } # rescue_from(..., :flaky) { "unknown" }
|
|
93
|
+
memoize :flaky
|
|
94
|
+
shortcut(:summary) { "build #{flaky}" } # caches "build unknown" forever
|
|
95
|
+
memoize :summary # <- don't memoize over a rescue-backed shortcut
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Clearing is a neutral primitive:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
App.clear_memos! # => App (thread-safe; `reset!` is an alias)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
*When* to clear is a pack's business. See [Reloading](#reloading-and-thread-safety).
|
|
105
|
+
|
|
106
|
+
## `rescue_from`
|
|
107
|
+
|
|
108
|
+
Error class first, shortcut names optional and trailing. The handler's **return value becomes the
|
|
109
|
+
shortcut's return value**:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
Briefly.define do
|
|
113
|
+
use Briefly::Rails
|
|
114
|
+
shortcut(:redis) { REDIS_POOL }
|
|
115
|
+
rescue_from(Redis::BaseError, :redis) { |e| Sentry.capture_exception(e); nil }
|
|
116
|
+
rescue_from(StandardError) { |e, name| Rails.logger.warn("#{name}: #{e.message}"); raise }
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Unlike a shortcut body, a handler is **not** bound to the facade — it is called as
|
|
121
|
+
`handler.call(error, name)`, so `self` stays whatever it was where you wrote the block. Reach for
|
|
122
|
+
constants (`Rails.logger`, `Sentry`) rather than bare shortcut names inside a handler.
|
|
123
|
+
|
|
124
|
+
> **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 — a
|
|
126
|
+
> typo and a dead Redis both arrive as a `StandardError`:
|
|
127
|
+
>
|
|
128
|
+
> ```ruby
|
|
129
|
+
> shortcut(:host) { Rails.aplication.config.host } # typo -> NoMethodError
|
|
130
|
+
> rescue_from(StandardError) { nil }
|
|
131
|
+
>
|
|
132
|
+
> App.host # => nil. No exception, no log, no clue.
|
|
133
|
+
> ```
|
|
134
|
+
>
|
|
135
|
+
> Three ways out, in order of preference: scope handlers to the shortcuts that can actually fail;
|
|
136
|
+
> match the narrowest error class you mean; and if you do want a facade-wide handler, make it log
|
|
137
|
+
> and `raise` — a bare `raise` inside a handler re-raises the original, backtrace intact.
|
|
138
|
+
>
|
|
139
|
+
> A bad *call* from outside the facade is not affected. Every shortcut carries its body's arity, so
|
|
140
|
+
> `App.env(1)` raises `ArgumentError` at the call site, before any handler is consulted. One shortcut
|
|
141
|
+
> body calling another with a bad argument list is a different matter: that raises *inside* the
|
|
142
|
+
> calling body, where the calling shortcut's own handler sees it like any other error.
|
|
143
|
+
|
|
144
|
+
> **⚠️ `{}` needs parentheses.** `rescue_from StandardError { ... }` binds the block to
|
|
145
|
+
> `StandardError`, not to `rescue_from`, and raises `NoMethodError`. Use **either** form:
|
|
146
|
+
>
|
|
147
|
+
> ```ruby
|
|
148
|
+
> rescue_from StandardError, :redis do |e| ... end # do/end, no parens
|
|
149
|
+
> rescue_from(StandardError, :redis) { |e| ... } # braces REQUIRE parens
|
|
150
|
+
> ```
|
|
151
|
+
|
|
152
|
+
Handlers are plain procs, so `{ |e| }` and `{ |e, name| }` both work. Re-raising propagates. If no
|
|
153
|
+
handler matches, the original error propagates unchanged — never silently swallowed. Only
|
|
154
|
+
`StandardError` and its descendants participate.
|
|
155
|
+
|
|
156
|
+
`Briefly.rescue_from(error_class, &handler)` registers a global default across every facade. It
|
|
157
|
+
takes no shortcut names.
|
|
158
|
+
|
|
159
|
+
### Resolution order
|
|
160
|
+
|
|
161
|
+
For a raised error, the first `is_a?` match wins, searching in this order:
|
|
162
|
+
|
|
163
|
+
| # | Level | Within the level |
|
|
164
|
+
|---|-------|------------------|
|
|
165
|
+
| 1 | Facade handlers scoped to this shortcut | last registered first |
|
|
166
|
+
| 2 | Facade handlers with no names | last registered first |
|
|
167
|
+
| 3 | Global handlers (`Briefly.rescue_from`) | last registered first |
|
|
168
|
+
|
|
169
|
+
No match → the error propagates.
|
|
170
|
+
|
|
171
|
+
## `namespace`
|
|
172
|
+
|
|
173
|
+
`namespace` groups shortcuts behind a name, so the root keyspace stays yours:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
App = Briefly.define do
|
|
177
|
+
shortcut(:redis) { REDIS_POOL }
|
|
178
|
+
|
|
179
|
+
namespace :db do
|
|
180
|
+
shortcut(:pool) { ActiveRecord::Base.connection_pool }
|
|
181
|
+
memoize :pool
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
App.redis # => REDIS_POOL
|
|
186
|
+
App.db # => #<Briefly::Facade shortcuts=[:pool]>
|
|
187
|
+
App.db.pool # => the pool
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
A namespace is a child `Briefly::Facade`, reached by a real method like any other shortcut — so
|
|
191
|
+
`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`, `memoize`, `rescue_from`, `use`, and further namespaces.
|
|
193
|
+
|
|
194
|
+
`clear_memos!` cascades, so one `Briefly::Rails::Reload` on the root clears the whole tree. The child
|
|
195
|
+
is created once and reused, so its memos survive a later `configure`.
|
|
196
|
+
|
|
197
|
+
`configure` is atomic across the whole tree: a pass that raises anywhere leaves the root and every
|
|
198
|
+
namespace under it exactly as they were. Declaring `shortcut(:db)` over a `namespace(:db)` overrides
|
|
199
|
+
it and drops the child.
|
|
200
|
+
|
|
201
|
+
Two limits, both deliberate:
|
|
202
|
+
|
|
203
|
+
- A child body cannot call a root shortcut by bare name. Namespaces are self-contained.
|
|
204
|
+
- A root `rescue_from` does not scope into a child. Register handlers inside the namespace, or
|
|
205
|
+
globally with `Briefly.rescue_from`.
|
|
206
|
+
|
|
207
|
+
## Packs
|
|
208
|
+
|
|
209
|
+
A pack is **any object responding to `#install(builder, **opts)`**. That's the whole protocol. Options
|
|
210
|
+
are optional: Ruby drops an empty `**` splat, so a pack taking none needs no keyword parameter.
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
module RedisPack
|
|
214
|
+
module_function
|
|
215
|
+
|
|
216
|
+
def install(builder)
|
|
217
|
+
builder.shortcut(:redis) { ConnectionPool.new { Redis.new } }
|
|
218
|
+
builder.memoize(:redis)
|
|
219
|
+
builder.rescue_from(Redis::CannotConnectError, :redis) { nil }
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
Api = Briefly.define { use RedisPack }
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Packs may `use` other packs, and may reach `builder.facade` to wire lifecycle hooks — that is
|
|
227
|
+
exactly what `Briefly::Rails::Reload` does. The core stays framework-agnostic; packs do not have to.
|
|
228
|
+
|
|
229
|
+
### Options
|
|
230
|
+
|
|
231
|
+
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:
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
module RedisPack
|
|
236
|
+
module_function
|
|
237
|
+
|
|
238
|
+
def install(builder, url: ENV.fetch("REDIS_URL"))
|
|
239
|
+
builder.shortcut(:redis) { ConnectionPool.new { Redis.new(url: url) } }
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
Api = Briefly.define { use RedisPack, url: "redis://cache:6379" }
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Short names
|
|
247
|
+
|
|
248
|
+
`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 — the registry is the only source of truth:
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
Briefly.register("myapp/redis", RedisPack) # a pack object
|
|
253
|
+
Briefly.register("myapp/redis", "MyApp::RedisPack") # or a constant path, resolved on first use
|
|
254
|
+
|
|
255
|
+
Api = Briefly.define { use "myapp/redis", url: "redis://cache:6379" }
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
An unregistered name raises `Briefly::UnknownPackError`. The packs this gem ships are registered as
|
|
259
|
+
`"rails"`, `"rails/config"`, `"rails/env"`, `"rails/view"`, `"rails/db"` and `"rails/reload"`.
|
|
260
|
+
|
|
261
|
+
### `Briefly::Rails`
|
|
262
|
+
|
|
263
|
+
| shortcut | aliases | value |
|
|
264
|
+
|---|---|---|
|
|
265
|
+
| `config` | `c` | `Rails.configuration` |
|
|
266
|
+
| `config_x` | `x` | `Rails.configuration.x` |
|
|
267
|
+
| `env` | | `Rails.env` |
|
|
268
|
+
| `production?` `development?` `test?` `local?` | | `Rails.env.*` |
|
|
269
|
+
| `root` | | `Rails.root` |
|
|
270
|
+
| `cache` | | `Rails.cache` |
|
|
271
|
+
| `logger` | `log` | `Rails.logger` |
|
|
272
|
+
| `credentials` | `cred` | `Rails.application.credentials` |
|
|
273
|
+
| `helpers` | `h` | `ApplicationController.helpers` |
|
|
274
|
+
| `routes` | `r` | `Rails.application.routes.url_helpers` |
|
|
275
|
+
| `renderer` | | `ApplicationController.renderer` |
|
|
276
|
+
| `render` | | forwards to `renderer.render` |
|
|
277
|
+
|
|
278
|
+
Plus `db`, a namespace holding `Briefly::Rails::DB`.
|
|
279
|
+
|
|
280
|
+
Requires Rails >= 7.2. There is no `secrets` shortcut: `Rails.application.secrets` was removed in
|
|
281
|
+
7.2. Use `credentials`.
|
|
282
|
+
|
|
283
|
+
**Nothing in the pack is memoized.** `helpers`, `routes` and `renderer` are live lookups: Rails
|
|
284
|
+
already caches them on objects it refreshes on reload, so caching them again would only go stale.
|
|
285
|
+
It still composes `Briefly::Rails::Reload`, because *your* memoized shortcuts need clearing.
|
|
286
|
+
|
|
287
|
+
Need a custom renderer? Override it — last declaration wins:
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
App = Briefly.define do
|
|
291
|
+
use Briefly::Rails
|
|
292
|
+
shortcut(:renderer) { ApplicationController.renderer.new(http_host: x.domain, https: !development?) }
|
|
293
|
+
end
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
`Briefly::Rails` is an umbrella over four packs, each usable on its own:
|
|
297
|
+
|
|
298
|
+
| pack | short name | shortcuts |
|
|
299
|
+
|---|---|---|
|
|
300
|
+
| `Briefly::Rails::Config` | `"rails/config"` | `config`, `config_x`, `root`, `cache`, `logger`, `credentials` |
|
|
301
|
+
| `Briefly::Rails::Env` | `"rails/env"` | `env` and its predicates |
|
|
302
|
+
| `Briefly::Rails::View` | `"rails/view"` | `helpers`, `routes`, `renderer`, `render` |
|
|
303
|
+
| `Briefly::Rails::DB` | `"rails/db"` | `connection`, `transaction`, `query` |
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
Worker = Briefly.define do
|
|
307
|
+
use "rails/env"
|
|
308
|
+
use "rails/reload"
|
|
309
|
+
namespace(:db) { use "rails/db" }
|
|
310
|
+
end
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### `Briefly::Rails::DB`
|
|
314
|
+
|
|
315
|
+
| shortcut | aliases | value |
|
|
316
|
+
|---|---|---|
|
|
317
|
+
| `connection` | `conn` | `base.lease_connection` |
|
|
318
|
+
| `transaction` | `txn` | forwards keywords and the block to `base.transaction` |
|
|
319
|
+
| `query` | | `base.with_connection { \|c\| c.exec_query(sql) }` |
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
App = Briefly.define do
|
|
323
|
+
use Briefly::Rails
|
|
324
|
+
namespace(:db2) { use "rails/db", base: "SecondaryApplicationRecord" }
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
App.db.txn { App.db.query("select * from users where id = ?", 1) }
|
|
328
|
+
App.db2.conn
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
`query(sql, *binds)` sanitizes through `base.sanitize_sql_array` when binds are given, and passes the
|
|
332
|
+
statement through untouched when they are not. Positional and named binds both work:
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
App.db.query("select * from users where name like '%ada%'") # no binds, passed through
|
|
336
|
+
App.db.query("select * from users where id = ?", 123) # positional
|
|
337
|
+
App.db.query("select * from users where id = :id", id: 123) # named
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Binds are bound, never interpolated, so a value like `"x' OR '1'='1"` matches nothing. A bindless
|
|
341
|
+
statement is deliberately *not* sanitized: `sanitize_sql_array` would fall through to its
|
|
342
|
+
`statement % values` branch and raise on the literal `%` above.
|
|
343
|
+
|
|
344
|
+
`query` uses `with_connection`, so the connection returns to the pool; `connection`/`conn`
|
|
345
|
+
necessarily leases one.
|
|
346
|
+
|
|
347
|
+
**Pass `base:` as a String, not the class.** A pack is `use`d from an initializer, where naming an
|
|
348
|
+
autoloadable constant is what Rails warns about, and the captured class would go stale on the first
|
|
349
|
+
code reload — permanently, since `Reload` clears memos, not closures. A `Module` is accepted for
|
|
350
|
+
applications outside the autoloader, with that caveat.
|
|
351
|
+
|
|
352
|
+
The pack memoizes nothing and wires no lifecycle hook, so it works without a booted application.
|
|
353
|
+
|
|
354
|
+
### `Briefly::Rails::Reload`
|
|
355
|
+
|
|
356
|
+
`Briefly::Rails` composes it. Use it alone for a facade with no framework shortcuts that still
|
|
357
|
+
memoizes objects holding on to reloadable application classes:
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
Admin = Briefly.define do
|
|
361
|
+
use Briefly::Rails::Reload
|
|
362
|
+
shortcut(:policy) { Admin::Policy.new }
|
|
363
|
+
memoize :policy
|
|
364
|
+
end
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
It registers `Rails.application.reloader.to_prepare { facade.clear_memos! }`, so memos are dropped
|
|
368
|
+
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 — call it from an initializer.
|
|
370
|
+
|
|
371
|
+
The callback holds its facade for the process lifetime and cannot be deregistered. Install it on
|
|
372
|
+
long-lived facades assigned to constants, not on facades built per request.
|
|
373
|
+
|
|
374
|
+
## Reloading and thread-safety
|
|
375
|
+
|
|
376
|
+
Memo reads are lock-free against a frozen snapshot; writes swap in a new frozen hash under a
|
|
377
|
+
reentrant lock, so a memoized body may safely call another memoized shortcut. Under Puma, a
|
|
378
|
+
memoized body runs exactly once no matter how many threads race for it.
|
|
379
|
+
|
|
380
|
+
`clear_memos!` guarantees the *next* read recomputes. It does **not** undo in-place mutation of an
|
|
381
|
+
already-handed-out object, and it does not survive a process restart. Two facades whose memoized
|
|
382
|
+
bodies call into each other can deadlock, like any pair of mutually-locking objects; don't do that.
|
|
383
|
+
|
|
384
|
+
## Testing
|
|
385
|
+
|
|
386
|
+
Shortcuts are real methods, so nothing special is needed:
|
|
387
|
+
|
|
388
|
+
```ruby
|
|
389
|
+
allow(App).to receive(:redis).and_return(fake_redis) # rspec-mocks verifies it
|
|
390
|
+
App.stub(:redis, fake_redis) { ... } # minitest
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Call `App.reset!` between examples if a memoized value would leak. `Briefly.errors.clear` resets
|
|
394
|
+
globally registered handlers.
|
|
395
|
+
|
|
396
|
+
## Types
|
|
397
|
+
|
|
398
|
+
`briefly` ships RBS signatures in [`sig/`](sig). Shortcuts are compiled at runtime, so **RBS cannot
|
|
399
|
+
see them** — `Briefly.define`, `Facade`'s lifecycle API and the `Builder` DSL are fully typed, but
|
|
400
|
+
`App.config` is invisible to Steep. Declare the ones you rely on in your own `sig/`:
|
|
401
|
+
|
|
402
|
+
```rbs
|
|
403
|
+
App: Briefly::Facade
|
|
404
|
+
|
|
405
|
+
def App.config: () -> untyped
|
|
406
|
+
def App.redis: () -> untyped
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
We deliberately do not fake this with an RBS-only `method_missing`; the gem genuinely has none.
|
|
410
|
+
|
|
411
|
+
## Migrating a hand-rolled `App`
|
|
412
|
+
|
|
413
|
+
```ruby
|
|
414
|
+
# before
|
|
415
|
+
module App
|
|
416
|
+
def self.config = Rails.configuration
|
|
417
|
+
def self.c = config
|
|
418
|
+
def self.x = config.x
|
|
419
|
+
def self.env = Rails.env
|
|
420
|
+
def self.helpers = @helpers ||= ApplicationController.helpers # stale after reload
|
|
421
|
+
def self.routes = @routes ||= Rails.application.routes.url_helpers
|
|
422
|
+
def self.render(...) = ApplicationController.renderer.render(...)
|
|
423
|
+
def self.secrets = Rails.application.secrets # gone in Rails 7.2
|
|
424
|
+
def self.redis = @redis ||= REDIS_POOL
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# after
|
|
428
|
+
App = Briefly.define do
|
|
429
|
+
use Briefly::Rails
|
|
430
|
+
shortcut(:redis) { REDIS_POOL }
|
|
431
|
+
memoize :redis
|
|
432
|
+
end
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
`secrets` becomes `credentials`. `helpers`/`routes` stop going stale because they are no longer
|
|
436
|
+
memoized, and `redis` — which you *do* want memoized — is cleared on every dev reload by the Reload
|
|
437
|
+
pack that `Briefly::Rails` composes.
|
|
438
|
+
|
|
439
|
+
## Contributing
|
|
440
|
+
|
|
441
|
+
Bug reports and pull requests are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
442
|
+
|
|
443
|
+
## License
|
|
444
|
+
|
|
445
|
+
[MIT](LICENSE.txt).
|
data/briefly.gemspec
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/briefly/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "briefly"
|
|
7
|
+
spec.version = Briefly::VERSION
|
|
8
|
+
spec.authors = ["Leonid Svyatov"]
|
|
9
|
+
spec.email = ["leonid@svyatov.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "A terse, curated facade over your application's most reached-for objects."
|
|
12
|
+
spec.description = "Declare one facade object per application area and reach your framework, config and " \
|
|
13
|
+
"clients through short, real, introspectable methods. One runtime dependency (candor), " \
|
|
14
|
+
"correct under Puma and Rails code reloading, with a batteries-included Rails pack."
|
|
15
|
+
spec.homepage = "https://github.com/svyatov/briefly"
|
|
16
|
+
spec.license = "MIT"
|
|
17
|
+
|
|
18
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
19
|
+
|
|
20
|
+
spec.require_paths = ["lib"]
|
|
21
|
+
spec.files = Dir["lib/**/*.rb"] + Dir["sig/**/*"] +
|
|
22
|
+
%w[.yardopts CHANGELOG.md LICENSE.txt README.md briefly.gemspec]
|
|
23
|
+
|
|
24
|
+
spec.add_dependency "candor", "~> 0.2.0"
|
|
25
|
+
|
|
26
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
27
|
+
spec.metadata["documentation_uri"] = "https://rubydoc.info/gems/briefly"
|
|
28
|
+
spec.metadata["source_code_uri"] = "https://github.com/svyatov/briefly"
|
|
29
|
+
spec.metadata["changelog_uri"] = "https://github.com/svyatov/briefly/blob/main/CHANGELOG.md"
|
|
30
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/svyatov/briefly/issues"
|
|
31
|
+
end
|