briefly

Gem Version CI codecov Documentation Ruby Types: RBS

A terse, curated facade over your application's most reached-for objects — thread-safe, reload-correct, with a batteries-included Rails pack.

Every app grows an App module full of def self.config = Rails.configuration. briefly gives you that module without writing it, as real methods — no method_missing, so respond_to?, console tab-completion and test stubbing all just work. Each shortcut carries its body's arity and parameter kinds — keyword names are exact, positionals get generated ones — and its source_location is the block you declared, so jump-to-definition lands in your initializer rather than inside the gem. That fabrication is candor, extracted from this gem and its only runtime dependency; candor itself has none.

# config/initializers/app.rb
App = Briefly.define do
  use Briefly::Rails
  shortcut(:redis) { REDIS_POOL }
end

App.c                        # => Rails.configuration
App.x.stripe_key             # => Rails.configuration.x.stripe_key
App.render(:receipt, locals: { order: })
App.redis                    # => REDIS_POOL
App.local?                   # => true in development and test

Installation

gem "briefly"

Ruby >= 3.2. The one runtime dependency is candor. Rails is optional: the gem does not declare it, and Briefly::Rails is autoloaded only when you name it.

Core concepts

A facade is the object Briefly.define returns. You assign it to a constant of your choosing; briefly never installs one for you. Multiple independent facades share no state:

App   = Briefly.define { use Briefly::Rails }
Admin = Briefly.define { shortcut(:audit_log) { AuditLog } }

A shortcut is a name plus a body. The body is always attached to shortcut — one block, one place — and runs bound to the facade, so it can reach the facade's other shortcuts:

Briefly.define do
  shortcut(:config, :c) { Rails.configuration }   # `:c` is an alias
  shortcut(:timeout)    { config.x.timeout }      # bodies see other shortcuts
  shortcut(:ready?)     { !timeout.nil? }         # `?` and `!` names are fine
  shortcut(:fetch)      { |key, &blk| store.get(key, &blk) }  # args and blocks forward
end

Aliases are real methods delegating to the same body and the same memo cell. Redeclaring a name overrides it silently — that is how you override a pack's shortcut.

memoize

Annotate an already-declared shortcut by name, on its own line:

Briefly.define do
  shortcut(:catalog) { Catalog.load_from_disk }
  memoize :catalog
end

Memoization is permanent for the process — the core has no idea what a "reload" is. It caches nil and false correctly, and a body that takes any parameter — positional, keyword, or block — cannot be memoized (raises at build time). The compiled method takes no arguments either, so App.catalog(:x) is an ArgumentError, never a silent cache hit. If a memoized body raises and a handler supplies a fallback, that shortcut's own cell is not filled: the transient failure is retried on next call.

That guarantee is per-cell, and does not compose. A memoized shortcut whose body reads a rescue-backed shortcut succeeds, so its own value — containing the fallback — is cached for the process lifetime, even after the inner shortcut recovers:

shortcut(:flaky) { external_call }        # rescue_from(..., :flaky) { "unknown" }
memoize :flaky
shortcut(:summary) { "build #{flaky}" }   # caches "build unknown" forever
memoize :summary                          # <- don't memoize over a rescue-backed shortcut

Clearing is a neutral primitive:

App.clear_memos!   # => App     (thread-safe; `reset!` is an alias)

When to clear is a pack's business. See Reloading.

rescue_from

Error class first, shortcut names optional and trailing. The handler's return value becomes the shortcut's return value:

Briefly.define do
  use Briefly::Rails
  shortcut(:redis) { REDIS_POOL }
  rescue_from(Redis::BaseError, :redis) { |e| Sentry.capture_exception(e); nil }
  rescue_from(StandardError) { |e, name| Rails.logger.warn("#{name}: #{e.message}"); raise }
end

Unlike a shortcut body, a handler is not bound to the facade — it is called as handler.call(error, name), so self stays whatever it was where you wrote the block. Reach for constants (Rails.logger, Sentry) rather than bare shortcut names inside a handler.

A facade-wide rescue_from(StandardError) catches your own bugs, not just your app's. briefly cannot tell an error raised by a shortcut body from one raised about the call — a typo and a dead Redis both arrive as a StandardError:

shortcut(:host) { Rails.aplication.config.host }   # typo -> NoMethodError
rescue_from(StandardError) { nil }

App.host     # => nil. No exception, no log, no clue.

Three ways out, in order of preference: scope handlers to the shortcuts that can actually fail; match the narrowest error class you mean; and if you do want a facade-wide handler, make it log and raise — a bare raise inside a handler re-raises the original, backtrace intact.

A bad call from outside the facade is not affected. Every shortcut carries its body's arity, so App.env(1) raises ArgumentError at the call site, before any handler is consulted. One shortcut body calling another with a bad argument list is a different matter: that raises inside the calling body, where the calling shortcut's own handler sees it like any other error.

⚠️ {} needs parentheses. rescue_from StandardError { ... } binds the block to StandardError, not to rescue_from, and raises NoMethodError. Use either form:

rescue_from StandardError, :redis do |e| ... end   # do/end, no parens
rescue_from(StandardError, :redis) { |e| ... }     # braces REQUIRE parens

Handlers are plain procs, so { |e| } and { |e, name| } both work. Re-raising propagates. If no handler matches, the original error propagates unchanged — never silently swallowed. Only StandardError and its descendants participate.

Briefly.rescue_from(error_class, &handler) registers a global default across every facade. It takes no shortcut names.

Resolution order

For a raised error, the first is_a? match wins, searching in this order:

# Level Within the level
1 Facade handlers scoped to this shortcut last registered first
2 Facade handlers with no names last registered first
3 Global handlers (Briefly.rescue_from) last registered first

No match → the error propagates.

namespace

namespace groups shortcuts behind a name, so the root keyspace stays yours:

App = Briefly.define do
  shortcut(:redis) { REDIS_POOL }

  namespace :db do
    shortcut(:pool) { ActiveRecord::Base.connection_pool }
    memoize :pool
  end
end

App.redis     # => REDIS_POOL
App.db        # => #<Briefly::Facade shortcuts=[:pool]>
App.db.pool   # => the pool

A namespace is a child Briefly::Facade, reached by a real method like any other shortcut — so App.db is a value you can pass around, and App.db.pool is not a method_missing trick. It takes the whole DSL: shortcut, memoize, rescue_from, use, and further namespaces.

clear_memos! cascades, so one Briefly::Rails::Reload on the root clears the whole tree. The child is created once and reused, so its memos survive a later configure.

configure is atomic across the whole tree: a pass that raises anywhere leaves the root and every namespace under it exactly as they were. Declaring shortcut(:db) over a namespace(:db) overrides it and drops the child.

Two limits, both deliberate:

  • A child body cannot call a root shortcut by bare name. Namespaces are self-contained.
  • A root rescue_from does not scope into a child. Register handlers inside the namespace, or globally with Briefly.rescue_from.

Packs

A pack is any object responding to #install(builder, **opts). That's the whole protocol. Options are optional: Ruby drops an empty ** splat, so a pack taking none needs no keyword parameter.

module RedisPack
  module_function

  def install(builder)
    builder.shortcut(:redis) { ConnectionPool.new { Redis.new } }
    builder.memoize(:redis)
    builder.rescue_from(Redis::CannotConnectError, :redis) { nil }
  end
end

Api = Briefly.define { use RedisPack }

Packs may use other packs, and may reach builder.facade to wire lifecycle hooks — that is exactly what Briefly::Rails::Reload does. The core stays framework-agnostic; packs do not have to.

Options

Keywords passed to use reach the pack's install. Ruby drops an empty ** splat, so a pack that takes no options needs no keyword parameter:

module RedisPack
  module_function

  def install(builder, url: ENV.fetch("REDIS_URL"))
    builder.shortcut(:redis) { ConnectionPool.new { Redis.new(url: url) } }
  end
end

Api = Briefly.define { use RedisPack, url: "redis://cache:6379" }

Short names

Briefly.register maps a name to a pack, so use can take a string or symbol. There is no inflection and no path guessing — the registry is the only source of truth:

Briefly.register("myapp/redis", RedisPack)          # a pack object
Briefly.register("myapp/redis", "MyApp::RedisPack") # or a constant path, resolved on first use

Api = Briefly.define { use "myapp/redis", url: "redis://cache:6379" }

An unregistered name raises Briefly::UnknownPackError. The packs this gem ships are registered as "rails", "rails/config", "rails/env", "rails/view", "rails/db" and "rails/reload".

Briefly::Rails

shortcut aliases value
config c Rails.configuration
config_x x Rails.configuration.x
env Rails.env
production? development? test? local? Rails.env.*
root Rails.root
cache Rails.cache
logger log Rails.logger
credentials cred Rails.application.credentials
helpers h ApplicationController.helpers
routes r Rails.application.routes.url_helpers
renderer ApplicationController.renderer
render forwards to renderer.render

Plus db, a namespace holding Briefly::Rails::DB.

Requires Rails >= 7.2. There is no secrets shortcut: Rails.application.secrets was removed in 7.2. Use credentials.

Nothing in the pack is memoized. helpers, routes and renderer are live lookups: Rails already caches them on objects it refreshes on reload, so caching them again would only go stale. It still composes Briefly::Rails::Reload, because your memoized shortcuts need clearing.

Need a custom renderer? Override it — last declaration wins:

App = Briefly.define do
  use Briefly::Rails
  shortcut(:renderer) { ApplicationController.renderer.new(http_host: x.domain, https: !development?) }
end

Briefly::Rails is an umbrella over four packs, each usable on its own:

pack short name shortcuts
Briefly::Rails::Config "rails/config" config, config_x, root, cache, logger, credentials
Briefly::Rails::Env "rails/env" env and its predicates
Briefly::Rails::View "rails/view" helpers, routes, renderer, render
Briefly::Rails::DB "rails/db" connection, transaction, query
Worker = Briefly.define do
  use "rails/env"
  use "rails/reload"
  namespace(:db) { use "rails/db" }
end

Briefly::Rails::DB

shortcut aliases value
connection conn base.lease_connection
transaction txn forwards keywords and the block to base.transaction
query `base.with_connection { \ c\ c.exec_query(sql) }`
App = Briefly.define do
  use Briefly::Rails
  namespace(:db2) { use "rails/db", base: "SecondaryApplicationRecord" }
end

App.db.txn { App.db.query("select * from users where id = ?", 1) }
App.db2.conn

query(sql, *binds) sanitizes through base.sanitize_sql_array when binds are given, and passes the statement through untouched when they are not. Positional and named binds both work:

App.db.query("select * from users where name like '%ada%'")          # no binds, passed through
App.db.query("select * from users where id = ?", 123)                # positional
App.db.query("select * from users where id = :id", id: 123)          # named

Binds are bound, never interpolated, so a value like "x' OR '1'='1" matches nothing. A bindless statement is deliberately not sanitized: sanitize_sql_array would fall through to its statement % values branch and raise on the literal % above.

query uses with_connection, so the connection returns to the pool; connection/conn necessarily leases one.

Pass base: as a String, not the class. A pack is used from an initializer, where naming an autoloadable constant is what Rails warns about, and the captured class would go stale on the first code reload — permanently, since Reload clears memos, not closures. A Module is accepted for applications outside the autoloader, with that caveat.

The pack memoizes nothing and wires no lifecycle hook, so it works without a booted application.

Briefly::Rails::Reload

Briefly::Rails composes it. Use it alone for a facade with no framework shortcuts that still memoizes objects holding on to reloadable application classes:

Admin = Briefly.define do
  use Briefly::Rails::Reload
  shortcut(:policy) { Admin::Policy.new }
  memoize :policy
end

It registers Rails.application.reloader.to_prepare { facade.clear_memos! }, so memos are dropped at boot and on every code reload in development. In production nothing reloads, so they persist for the process lifetime. It raises Briefly::Error outside a booted app — call it from an initializer.

The callback holds its facade for the process lifetime and cannot be deregistered. Install it on long-lived facades assigned to constants, not on facades built per request.

Reloading and thread-safety

Memo reads are lock-free against a frozen snapshot; writes swap in a new frozen hash under a reentrant lock, so a memoized body may safely call another memoized shortcut. Under Puma, a memoized body runs exactly once no matter how many threads race for it.

clear_memos! guarantees the next read recomputes. It does not undo in-place mutation of an already-handed-out object, and it does not survive a process restart. Two facades whose memoized bodies call into each other can deadlock, like any pair of mutually-locking objects; don't do that.

Testing

Shortcuts are real methods, so nothing special is needed:

allow(App).to receive(:redis).and_return(fake_redis)   # rspec-mocks verifies it
App.stub(:redis, fake_redis) { ... }                   # minitest

Call App.reset! between examples if a memoized value would leak. Briefly.errors.clear resets globally registered handlers.

Types

briefly ships RBS signatures in sig/. Shortcuts are compiled at runtime, so RBS cannot see themBriefly.define, Facade's lifecycle API and the Builder DSL are fully typed, but App.config is invisible to Steep. Declare the ones you rely on in your own sig/:

App: Briefly::Facade

def App.config: () -> untyped
def App.redis: () -> untyped

We deliberately do not fake this with an RBS-only method_missing; the gem genuinely has none.

Migrating a hand-rolled App

# before
module App
  def self.config      = Rails.configuration
  def self.c           = config
  def self.x           = config.x
  def self.env         = Rails.env
  def self.helpers     = @helpers ||= ApplicationController.helpers    # stale after reload
  def self.routes      = @routes  ||= Rails.application.routes.url_helpers
  def self.render(...) = ApplicationController.renderer.render(...)
  def self.secrets     = Rails.application.secrets                     # gone in Rails 7.2
  def self.redis       = @redis ||= REDIS_POOL
end

# after
App = Briefly.define do
  use Briefly::Rails
  shortcut(:redis) { REDIS_POOL }
  memoize :redis
end

secrets becomes credentials. helpers/routes stop going stale because they are no longer memoized, and redis — which you do want memoized — is cleared on every dev reload by the Reload pack that Briefly::Rails composes.

Contributing

Bug reports and pull requests are welcome — see CONTRIBUTING.md.

License

MIT.