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 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
@@ -0,0 +1,6 @@
1
+ --no-private
2
+ --readme README.md
3
+ lib/**/*.rb
4
+ -
5
+ CHANGELOG.md
6
+ LICENSE.txt
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
+ [![Gem Version](https://badge.fury.io/rb/briefly.svg)](https://rubygems.org/gems/briefly)
4
+ [![CI](https://github.com/svyatov/briefly/actions/workflows/main.yml/badge.svg)](https://github.com/svyatov/briefly/actions/workflows/main.yml)
5
+ [![codecov](https://codecov.io/gh/svyatov/briefly/branch/main/graph/badge.svg)](https://codecov.io/gh/svyatov/briefly)
6
+ [![Documentation](https://img.shields.io/badge/docs-rubydoc.info-blue.svg)](https://rubydoc.info/gems/briefly)
7
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-CC342D.svg)](https://www.ruby-lang.org)
8
+ [![Types: RBS](https://img.shields.io/badge/types-RBS-8A2BE2.svg)](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