Class: Briefly::Builder

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

Overview

Receives the DSL. define's block and Facade#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#configure installs.

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:



16
17
18
19
20
21
22
# File 'lib/briefly/builder.rb', line 16

def initialize(facade, defs, children)
  @facade = facade
  @defs = defs
  @children = children
  @errors = []
  @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



10
11
12
# File 'lib/briefly/builder.rb', line 10

def facade
  @facade
end

Instance Method Details

#compile!Array

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#configure installs the result. Validation of the whole tree finishes before any of it is installed.

Returns:

  • (Array)

    [definitions, error_entries, children, child_plans]

Raises:



134
135
136
137
138
139
140
141
142
# File 'lib/briefly/builder.rb', line 134

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
  [@defs, @errors, @children, @pending.map { |name, builder| [@children[name], builder.compile!] }]
end

#memoize(name, **opts) ⇒ Symbol

Marks an already-declared shortcut as memoized: computed once, cached for the process lifetime.

Parameters:

  • name (Symbol)

    a canonical name or an alias

  • opts (Hash)

    reserved for future use; none are accepted

Returns:

  • (Symbol)

    the canonical name

Raises:



99
100
101
102
103
104
105
# File 'lib/briefly/builder.rb', line 99

def memoize(name, **opts)
  raise ArgumentError, "memoize(#{name.inspect}) got unknown options: #{opts.keys.join(", ")}" unless opts.empty?

  defn = fetch(name)
  defn.memoize!
  defn.canonical
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, memoize, use, rescue_from, and further namespaces.

The child is created once and reused by later passes, so its memos survive configure. Facade#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:



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/briefly/builder.rb', line 56

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 an error handler. The handler's return value becomes the shortcut's return value.

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

Parameters:

  • error_class (Class)

    matched against the raised error and its subclasses

  • names (Array<Symbol>)

    shortcuts to scope to; none means facade-wide

Yields:

  • (error, shortcut_name)

    the recovery block

Returns:

  • (self)

Raises:

  • (ArgumentError)


116
117
118
119
120
121
122
123
124
125
# File 'lib/briefly/builder.rb', line 116

def rescue_from(error_class, *names, &handler)
  unless error_class.is_a?(Class)
    raise ArgumentError, "rescue_from expects the error class first, e.g. rescue_from(StandardError, :name) { }"
  end
  raise ArgumentError, "rescue_from(#{error_class}) requires a block" unless handler

  scoped = names.flatten.map { |name| fetch(name).canonical }
  @errors << [error_class, (scoped.empty? ? nil : scoped), handler]
  self
end

#shortcut(canonical, *aliases) { ... } ⇒ Symbol

Declares a shortcut. Redeclaring a name (canonical or alias) silently overrides it.

Parameters:

  • canonical (Symbol)

    the primary method name; may end in ? or !

  • aliases (Array<Symbol>)

    extra names sharing the body and the memo cell

Yields:

  • the implementation, bound to the facade at call time

Returns:

  • (Symbol)

    the canonical name

Raises:



80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/briefly/builder.rb', line 80

def shortcut(canonical, *aliases, &body)
  raise ArgumentError, "shortcut(#{canonical.inspect}) requires a block" unless body

  # A `&:upcase`-style Proc carries no location of its own; fall back to this call site so the compiled
  # method points at the user's declaration, never a fabricated file. `namespace` overwrites it anyway.
  location = body.source_location || caller_locations(1, 1).first.then { |l| [l.path, l.lineno] }
  defn = Definition.new(canonical, aliases, body, location)
  defn.names.each { |name| validate_name!(name) }
  purge(defn.names, except: canonical)
  @defs[canonical] = defn
  canonical
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:



34
35
36
37
38
# File 'lib/briefly/builder.rb', line 34

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