Class: Briefly::Builder

Inherits:
Object
  • Object
show all
Defined in:
lib/briefly/builder.rb

Overview

Receives the DSL. define's block and Facade::Control#configure's block are +instance_eval+'d here, never on the facade, so DSL verbs can never collide with shortcut names.

The block only collects; #compile! then validates, and Facade::Control#configure installs.

Defined Under Namespace

Classes: Plan

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(facade, defs, children) ⇒ Builder

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a new instance of Builder.

Parameters:



21
22
23
24
25
26
27
# File 'lib/briefly/builder.rb', line 21

def initialize(facade, defs, children)
  @facade = facade
  @defs = defs
  @children = children
  @rescues = []
  @pending = {}
end

Instance Attribute Details

#facadeBriefly::Facade (readonly)

Returns the facade under construction, for packs that need lifecycle hooks.

Returns:

  • (Briefly::Facade)

    the facade under construction, for packs that need lifecycle hooks



15
16
17
# File 'lib/briefly/builder.rb', line 15

def facade
  @facade
end

Instance Method Details

#compile!Briefly::Builder::Plan

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Validates everything collected in this pass, and recursively every namespace pass it collected. Facade::Control#configure installs the result. Validation of the whole tree finishes before any of it is installed.

Returns:

Raises:



140
141
142
143
144
145
146
147
148
# File 'lib/briefly/builder.rb', line 140

def compile!
  @defs.each_value do |defn|
    # `parameters`, not `arity`: a `{ |&blk| }` body has arity 0 but its block would be dropped.
    next unless defn.memoized? && !defn.body.parameters.empty?

    raise Error, "cannot memoize #{defn.canonical}: its body takes arguments"
  end
  Plan.new(@defs, @rescues, @children, @pending.map { |name, builder| [@children[name], builder.compile!] })
end

#namespace(name) { ... } ⇒ Symbol

Declares a namespace: a shortcut returning a child facade, so App.db.query works. The block is the child's own DSL — shortcut (and the shortcut it returns), use, rescue_from, and namespaces.

The child is created once and reused by later passes, so its memos survive configure. Facade::Control#clear_memos! cascades into it. Two limits, both deliberate: a child body cannot reach a root shortcut by bare name, and a root rescue_from does not scope into the child.

The child's pass is collected, not run: its builder is held until #compile! has validated the whole tree, so a later failure in this pass cannot leave an already-reachable child mutated. One builder per name, so a second namespace(:db) in the same pass extends the first rather than replacing it.

Parameters:

  • name (Symbol)

    the shortcut the child answers to

Yields:

Returns:

  • (Symbol)

    name

Raises:



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/briefly/builder.rb', line 61

def namespace(name, &block)
  raise ArgumentError, "namespace(#{name.inspect}) requires a block" unless block

  validate_name!(name)
  child = @children[name] || Facade.new
  pending = @pending[name] || child.send(:__prepare)
  pending.instance_eval(&block)
  # Routes through `shortcut` for purge and validation, which drops the child this call just
  # resolved — so re-register both afterwards. An external `shortcut(name)` gets no such reprieve.
  shortcut(name) { child }
  # The proc above is a literal in this file. Point the compiled method at the caller's block instead.
  @defs[name].source_location = block.source_location
  @children[name] = child
  @pending[name] = pending
  name
end

#rescue_from(error_class, *names) {|error, shortcut_name| ... } ⇒ self

Registers a facade-wide error handler: one applying to every shortcut, consulted after each shortcut's own handlers. The handler's return value becomes the shortcut's return value. To guard a single shortcut, chain onto it instead — shortcut(name).rescue_from(error_class) { ... } — which is why this verb takes no shortcut names.

Because {} binds to the nearest token, rescue_from StandardError { } is a call to a method named StandardError. Use rescue_from Err do |e| ... end or rescue_from(Err) { |e| ... }.

Parameters:

  • error_class (Class)

    matched against the raised error and its subclasses

  • names (Array<Symbol>)

    must be empty; any name is refused because the shortcut expresses it

Yields:

  • (error, shortcut_name)

    the recovery block

Returns:

  • (self)

Raises:

  • (ArgumentError)

    if any shortcut names are given



121
122
123
124
125
126
127
128
129
130
131
# File 'lib/briefly/builder.rb', line 121

def rescue_from(error_class, *names, &block)
  Briefly.send(:validate_rescue!, error_class, block)
  unless names.flatten.empty?
    raise ArgumentError, "rescue_from(#{error_class}) takes no shortcut names — scope one to its " \
                         "shortcut with shortcut(name).rescue_from(#{error_class}) { ... }, or omit " \
                         "names for a facade-wide handler"
  end

  @rescues << [error_class, block]
  self
end

#shortcut(canonical, *aliases) { ... } ⇒ Briefly::Shortcut

Declares a shortcut, or fetches an already-declared one to refine.

With a block, declares the shortcut (redeclaring a name, canonical or alias, silently overrides it) and returns the Shortcut, so you can chain .memoize or .rescue_from onto it. With no block, shortcut(name) fetches the shortcut name resolves to (canonical or alias) so a pack's shortcut can be refined; it never declares, purges, or overrides.

Parameters:

  • canonical (Symbol)

    the primary method name; may end in ? or !

  • aliases (Array<Symbol>)

    extra names sharing the body and the memo cell (declare only)

Yields:

  • the implementation, bound to the facade at call time

Returns:

Raises:

  • (ArgumentError)

    if a bodiless call is given aliases — a fetch ignores them, so a non-empty list means the block was forgotten

  • (Briefly::ReservedNameError)

    if any name shadows a facade method

  • (Briefly::UnknownShortcutError)

    if a bodiless name resolves to nothing



93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/briefly/builder.rb', line 93

def shortcut(canonical, *aliases, &body)
  unless body
    # A bodiless `shortcut(name)` fetches an existing shortcut to refine; aliases are meaningless
    # there, so a non-empty list means a block was forgotten — fail loudly, don't drop them silently.
    raise ArgumentError, "shortcut(#{canonical.inspect}, ...) with aliases requires a block" unless aliases.empty?

    return fetch(canonical)
  end

  defn = Shortcut.new(canonical, aliases, body, source_location_for(body))
  defn.names.each { |name| validate_name!(name) }
  purge(defn.names, except: canonical)
  @defs[canonical] = defn
end

#use(pack) ⇒ self

Applies a pack: any object responding to #install(builder), or a name Briefly.register'd for one. Any keywords are forwarded to the pack's install. Ruby drops an empty ** splat, so a pack taking no options needs no keyword parameter.

use Briefly::Rails::DB, base: "SecondaryApplicationRecord"
use "rails/db", base: "SecondaryApplicationRecord"

Parameters:

  • pack (#install, String, Symbol)

Returns:

  • (self)

Raises:



39
40
41
42
43
# File 'lib/briefly/builder.rb', line 39

def use(pack, **)
  pack = Briefly.pack(pack) unless pack.respond_to?(:install)
  pack.install(self, **)
  self
end