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
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Briefly
|
|
4
|
+
# Receives the DSL. {Briefly.define}'s block and {Briefly::Facade#configure}'s block are
|
|
5
|
+
# +instance_eval+'d here, never on the facade, so DSL verbs can never collide with shortcut names.
|
|
6
|
+
#
|
|
7
|
+
# The block only collects; {#compile!} then validates, and {Briefly::Facade#configure} installs.
|
|
8
|
+
class Builder
|
|
9
|
+
# @return [Briefly::Facade] the facade under construction, for packs that need lifecycle hooks
|
|
10
|
+
attr_reader :facade
|
|
11
|
+
|
|
12
|
+
# @api private
|
|
13
|
+
# @param facade [Briefly::Facade]
|
|
14
|
+
# @param defs [Hash{Symbol => Briefly::Definition}] copies of the facade's current definitions
|
|
15
|
+
# @param children [Hash{Symbol => Briefly::Facade}] the facade's namespaces, by name
|
|
16
|
+
def initialize(facade, defs, children)
|
|
17
|
+
@facade = facade
|
|
18
|
+
@defs = defs
|
|
19
|
+
@children = children
|
|
20
|
+
@errors = []
|
|
21
|
+
@pending = {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Applies a pack: any object responding to +#install(builder)+, or a name {Briefly.register}'d for
|
|
25
|
+
# one. Any keywords are forwarded to the pack's +install+. Ruby drops an empty +**+ splat, so a
|
|
26
|
+
# pack taking no options needs no keyword parameter.
|
|
27
|
+
#
|
|
28
|
+
# use Briefly::Rails::DB, base: "SecondaryApplicationRecord"
|
|
29
|
+
# use "rails/db", base: "SecondaryApplicationRecord"
|
|
30
|
+
#
|
|
31
|
+
# @param pack [#install, String, Symbol]
|
|
32
|
+
# @return [self]
|
|
33
|
+
# @raise [Briefly::UnknownPackError] if +pack+ is a name that is not registered
|
|
34
|
+
def use(pack, **)
|
|
35
|
+
pack = Briefly.pack(pack) unless pack.respond_to?(:install)
|
|
36
|
+
pack.install(self, **)
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Declares a namespace: a shortcut returning a child facade, so +App.db.query+ works. The block is
|
|
41
|
+
# the child's own DSL — +shortcut+, +memoize+, +use+, +rescue_from+, and further namespaces.
|
|
42
|
+
#
|
|
43
|
+
# The child is created once and reused by later passes, so its memos survive +configure+.
|
|
44
|
+
# {Briefly::Facade#clear_memos!} cascades into it. Two limits, both deliberate: a child body cannot
|
|
45
|
+
# reach a root shortcut by bare name, and a root +rescue_from+ does not scope into the child.
|
|
46
|
+
#
|
|
47
|
+
# The child's pass is *collected*, not run: its builder is held until {#compile!} has validated the
|
|
48
|
+
# whole tree, so a later failure in this pass cannot leave an already-reachable child mutated. One
|
|
49
|
+
# builder per name, so a second +namespace(:db)+ in the same pass extends the first rather than
|
|
50
|
+
# replacing it.
|
|
51
|
+
#
|
|
52
|
+
# @param name [Symbol] the shortcut the child answers to
|
|
53
|
+
# @yield [] evaluated against the child's {Briefly::Builder}
|
|
54
|
+
# @return [Symbol] +name+
|
|
55
|
+
# @raise [Briefly::ReservedNameError] if +name+ shadows a facade method
|
|
56
|
+
def namespace(name, &block)
|
|
57
|
+
raise ArgumentError, "namespace(#{name.inspect}) requires a block" unless block
|
|
58
|
+
|
|
59
|
+
validate_name!(name)
|
|
60
|
+
child = @children[name] || Facade.new
|
|
61
|
+
pending = @pending[name] || child.send(:__prepare)
|
|
62
|
+
pending.instance_eval(&block)
|
|
63
|
+
# Routes through `shortcut` for purge and validation, which drops the child this call just
|
|
64
|
+
# resolved — so re-register both afterwards. An external `shortcut(name)` gets no such reprieve.
|
|
65
|
+
shortcut(name) { child }
|
|
66
|
+
# The proc above is a literal in this file. Point the compiled method at the caller's block instead.
|
|
67
|
+
@defs[name].source_location = block.source_location
|
|
68
|
+
@children[name] = child
|
|
69
|
+
@pending[name] = pending
|
|
70
|
+
name
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Declares a shortcut. Redeclaring a name (canonical or alias) silently overrides it.
|
|
74
|
+
#
|
|
75
|
+
# @param canonical [Symbol] the primary method name; may end in +?+ or +!+
|
|
76
|
+
# @param aliases [Array<Symbol>] extra names sharing the body and the memo cell
|
|
77
|
+
# @yield the implementation, bound to the facade at call time
|
|
78
|
+
# @return [Symbol] the canonical name
|
|
79
|
+
# @raise [Briefly::ReservedNameError] if any name shadows a facade method
|
|
80
|
+
def shortcut(canonical, *aliases, &body)
|
|
81
|
+
raise ArgumentError, "shortcut(#{canonical.inspect}) requires a block" unless body
|
|
82
|
+
|
|
83
|
+
# A `&:upcase`-style Proc carries no location of its own; fall back to this call site so the compiled
|
|
84
|
+
# method points at the user's declaration, never a fabricated file. `namespace` overwrites it anyway.
|
|
85
|
+
location = body.source_location || caller_locations(1, 1).first.then { |l| [l.path, l.lineno] }
|
|
86
|
+
defn = Definition.new(canonical, aliases, body, location)
|
|
87
|
+
defn.names.each { |name| validate_name!(name) }
|
|
88
|
+
purge(defn.names, except: canonical)
|
|
89
|
+
@defs[canonical] = defn
|
|
90
|
+
canonical
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Marks an already-declared shortcut as memoized: computed once, cached for the process lifetime.
|
|
94
|
+
#
|
|
95
|
+
# @param name [Symbol] a canonical name or an alias
|
|
96
|
+
# @param opts [Hash] reserved for future use; none are accepted
|
|
97
|
+
# @return [Symbol] the canonical name
|
|
98
|
+
# @raise [Briefly::UnknownShortcutError] if +name+ does not resolve
|
|
99
|
+
def memoize(name, **opts)
|
|
100
|
+
raise ArgumentError, "memoize(#{name.inspect}) got unknown options: #{opts.keys.join(", ")}" unless opts.empty?
|
|
101
|
+
|
|
102
|
+
defn = fetch(name)
|
|
103
|
+
defn.memoize!
|
|
104
|
+
defn.canonical
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Registers an error handler. The handler's return value becomes the shortcut's return value.
|
|
108
|
+
#
|
|
109
|
+
# Because +{}+ binds to the nearest token, +rescue_from StandardError { }+ is a call to a method
|
|
110
|
+
# named +StandardError+. Use +rescue_from Err, :name do |e| ... end+ or +rescue_from(Err, :name) { |e| ... }+.
|
|
111
|
+
#
|
|
112
|
+
# @param error_class [Class] matched against the raised error and its subclasses
|
|
113
|
+
# @param names [Array<Symbol>] shortcuts to scope to; none means facade-wide
|
|
114
|
+
# @yield [error, shortcut_name] the recovery block
|
|
115
|
+
# @return [self]
|
|
116
|
+
def rescue_from(error_class, *names, &handler)
|
|
117
|
+
unless error_class.is_a?(Class)
|
|
118
|
+
raise ArgumentError, "rescue_from expects the error class first, e.g. rescue_from(StandardError, :name) { }"
|
|
119
|
+
end
|
|
120
|
+
raise ArgumentError, "rescue_from(#{error_class}) requires a block" unless handler
|
|
121
|
+
|
|
122
|
+
scoped = names.flatten.map { |name| fetch(name).canonical }
|
|
123
|
+
@errors << [error_class, (scoped.empty? ? nil : scoped), handler]
|
|
124
|
+
self
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Validates everything collected in this pass, and recursively every namespace pass it collected.
|
|
128
|
+
# {Briefly::Facade#configure} installs the result. Validation of the whole tree finishes before any
|
|
129
|
+
# of it is installed.
|
|
130
|
+
#
|
|
131
|
+
# @api private
|
|
132
|
+
# @return [Array] +[definitions, error_entries, children, child_plans]+
|
|
133
|
+
# @raise [Briefly::Error] if a memoized shortcut's body takes arguments
|
|
134
|
+
def compile!
|
|
135
|
+
@defs.each_value do |defn|
|
|
136
|
+
# `parameters`, not `arity`: a `{ |&blk| }` body has arity 0 but its block would be dropped.
|
|
137
|
+
next unless defn.memoized? && !defn.body.parameters.empty?
|
|
138
|
+
|
|
139
|
+
raise Error, "cannot memoize #{defn.canonical}: its body takes arguments"
|
|
140
|
+
end
|
|
141
|
+
[@defs, @errors, @children, @pending.map { |name, builder| [@children[name], builder.compile!] }]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def validate_name!(name)
|
|
147
|
+
raise ArgumentError, "shortcut names must be Symbols, got #{name.inspect}" unless name.is_a?(Symbol)
|
|
148
|
+
raise ReservedNameError, "#{name} is reserved by Briefly::Facade" if Facade::RESERVED.include?(name)
|
|
149
|
+
# Candor raises here too, but as an ArgumentError, and only once the pass is already committing.
|
|
150
|
+
return unless name.start_with?(Candor::BODY_PREFIX)
|
|
151
|
+
|
|
152
|
+
raise ReservedNameError, "#{name} is reserved: #{Candor::BODY_PREFIX}* names hold compiled shortcut bodies"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Frees the incoming names from any definition that currently owns them, so a redeclaration
|
|
156
|
+
# never leaves a stale alias pointing at the old body.
|
|
157
|
+
def purge(names, except:)
|
|
158
|
+
@defs.delete_if { |canonical, _| canonical != except && names.include?(canonical) }
|
|
159
|
+
@defs.each_value { |defn| defn.aliases.replace(defn.aliases - names) }
|
|
160
|
+
# A namespace is just a shortcut returning its child, so redeclaring the name — as a canonical or
|
|
161
|
+
# as someone else's alias — must drop the child too. Keeping it would leave `clear_memos!` walking
|
|
162
|
+
# a facade nothing can reach. `except` gets no exemption: `namespace` re-registers its own child.
|
|
163
|
+
@children.delete_if { |name, _| names.include?(name) }
|
|
164
|
+
@pending.delete_if { |name, _| names.include?(name) }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def fetch(name)
|
|
168
|
+
defn = @defs[name] || @defs.each_value.find { |d| d.aliases.include?(name) }
|
|
169
|
+
raise UnknownShortcutError, "unknown shortcut: #{name.inspect}" unless defn
|
|
170
|
+
|
|
171
|
+
defn
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Briefly
|
|
4
|
+
# One shortcut declaration: its canonical name, its aliases, its body, and whether it memoizes.
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
7
|
+
class Definition
|
|
8
|
+
# @return [Symbol] the primary method name
|
|
9
|
+
attr_reader :canonical
|
|
10
|
+
|
|
11
|
+
# @return [Array<Symbol>] additional method names delegating to the same body and memo cell
|
|
12
|
+
attr_reader :aliases
|
|
13
|
+
|
|
14
|
+
# @return [Proc] the implementation, bound to the facade at call time
|
|
15
|
+
attr_reader :body
|
|
16
|
+
|
|
17
|
+
# @return [Symbol] the private singleton method the body compiles to
|
|
18
|
+
attr_reader :raw_name
|
|
19
|
+
|
|
20
|
+
# The +[file, line]+ the compiled methods report. {Briefly::Builder#shortcut} resolves it — the body's
|
|
21
|
+
# own location, or the +shortcut+ call site for a bodiless-of-location Proc like +&:upcase+ — and
|
|
22
|
+
# {Briefly::Builder#namespace} overwrites it: its child-returning proc literal lives in +builder.rb+,
|
|
23
|
+
# and a namespace that pointed there would reintroduce, for namespaces, the lie this field exists to
|
|
24
|
+
# remove.
|
|
25
|
+
#
|
|
26
|
+
# @return [Array(String, Integer)]
|
|
27
|
+
attr_accessor :source_location
|
|
28
|
+
|
|
29
|
+
# @param canonical [Symbol]
|
|
30
|
+
# @param aliases [Array<Symbol>]
|
|
31
|
+
# @param body [Proc]
|
|
32
|
+
# @param source_location [Array(String, Integer)]
|
|
33
|
+
def initialize(canonical, aliases, body, source_location)
|
|
34
|
+
@canonical = canonical
|
|
35
|
+
@aliases = aliases
|
|
36
|
+
@body = body
|
|
37
|
+
@raw_name = Candor.body_name(canonical)
|
|
38
|
+
@memoized = false
|
|
39
|
+
@source_location = source_location
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Builder copies the facade's definitions before mutating them, so an aborted pass cannot
|
|
43
|
+
# leave the live facade half-configured. Hash#dup would share this array.
|
|
44
|
+
#
|
|
45
|
+
# @param other [Briefly::Definition]
|
|
46
|
+
# @return [void]
|
|
47
|
+
def initialize_copy(other)
|
|
48
|
+
super
|
|
49
|
+
@aliases = other.aliases.dup
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Marks the value as computed once and cached. A redeclared shortcut is a fresh, unmemoized
|
|
53
|
+
# Definition, so there is no un-memoize.
|
|
54
|
+
#
|
|
55
|
+
# @return [void]
|
|
56
|
+
def memoize! = @memoized = true
|
|
57
|
+
|
|
58
|
+
# @return [Boolean]
|
|
59
|
+
def memoized? = @memoized
|
|
60
|
+
|
|
61
|
+
# @return [Array<Symbol>] canonical name followed by every alias
|
|
62
|
+
def names = [@canonical, *@aliases]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Briefly
|
|
4
|
+
# An ordered list of +rescue_from+ registrations, queried at dispatch time.
|
|
5
|
+
#
|
|
6
|
+
# Entries are stored oldest-first and returned newest-first, so the most recent registration for a
|
|
7
|
+
# given error class wins. Reads are lock-free against a frozen array; writes rebind it under a
|
|
8
|
+
# mutex, because rebinding alone would drop concurrent registrations.
|
|
9
|
+
class ErrorRegistry
|
|
10
|
+
# A single registration. +names+ is +nil+ for facade-wide/global handlers.
|
|
11
|
+
Entry = Struct.new(:klass, :names, :handler)
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@entries = [].freeze
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @param klass [Class] matched against the raised error and its subclasses
|
|
19
|
+
# @param names [Array<Symbol>, nil] canonical shortcut names, or +nil+ for every shortcut
|
|
20
|
+
# @param handler [Proc] called with +(error, shortcut_name)+
|
|
21
|
+
# @return [self]
|
|
22
|
+
def add(klass, names, handler)
|
|
23
|
+
@mutex.synchronize { @entries = [*@entries, Entry.new(klass, names&.freeze, handler)].freeze }
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param name [Symbol] a canonical shortcut name
|
|
28
|
+
# @return [Array<Entry>] handlers scoped to +name+, most recently registered first
|
|
29
|
+
def scoped(name) = @entries.reverse_each.select { |entry| entry.names&.include?(name) }
|
|
30
|
+
|
|
31
|
+
# @return [Array<Entry>] handlers scoped to no shortcut in particular, most recent first
|
|
32
|
+
def wide = @entries.reverse_each.select { |entry| entry.names.nil? }
|
|
33
|
+
|
|
34
|
+
# @return [self]
|
|
35
|
+
def clear
|
|
36
|
+
@mutex.synchronize { @entries = [].freeze }
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Briefly
|
|
4
|
+
# Base class for every error raised by Briefly.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when +memoize+ or +rescue_from+ names a shortcut that does not exist.
|
|
8
|
+
class UnknownShortcutError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when a shortcut name or alias would shadow a facade method.
|
|
11
|
+
class ReservedNameError < Error; end
|
|
12
|
+
|
|
13
|
+
# Raised when +use+ names a pack that is not registered.
|
|
14
|
+
class UnknownPackError < Error; end
|
|
15
|
+
|
|
16
|
+
# Sentinel distinguishing "not memoized yet" from a memoized +nil+.
|
|
17
|
+
UNSET = Object.new.freeze
|
|
18
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module Briefly
|
|
6
|
+
# The object returned by {Briefly.define}. Shortcuts are compiled onto its singleton class as real
|
|
7
|
+
# methods, so +respond_to?+, console tab-completion and test stubbing all work unaided.
|
|
8
|
+
#
|
|
9
|
+
# Memo store: reads are lock-free against a frozen snapshot hash; writes swap in a new frozen hash
|
|
10
|
+
# under a reentrant +Monitor+, because a memoized body may call another memoized shortcut.
|
|
11
|
+
class Facade
|
|
12
|
+
def initialize
|
|
13
|
+
@__defs = {}
|
|
14
|
+
@__aliases = {}
|
|
15
|
+
@__memos = {}.freeze
|
|
16
|
+
@__monitor = Monitor.new
|
|
17
|
+
@__errors = ErrorRegistry.new
|
|
18
|
+
@__public = [].freeze
|
|
19
|
+
@__children = {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Array<Symbol>] canonical shortcut names, sorted; aliases excluded
|
|
23
|
+
def shortcuts = @__defs.keys.sort
|
|
24
|
+
|
|
25
|
+
# @param name [Symbol] a canonical name or an alias
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
def shortcut?(name) = @__defs.key?(name) || @__aliases.key?(name)
|
|
28
|
+
|
|
29
|
+
# Drops every memoized value, here and in every namespace. Thread-safe.
|
|
30
|
+
#
|
|
31
|
+
# @return [self]
|
|
32
|
+
def clear_memos!
|
|
33
|
+
@__monitor.synchronize { @__memos = {}.freeze }
|
|
34
|
+
# Each namespace owns its memo store and its own lock, so a child clears outside our monitor.
|
|
35
|
+
# One `Reload` on the root therefore clears the whole tree, including namespaces holding no pack.
|
|
36
|
+
@__children.each_value(&:clear_memos!)
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
alias reset! clear_memos!
|
|
40
|
+
|
|
41
|
+
# Reopens the facade for another builder pass and recompiles. The builder starts from copies of the
|
|
42
|
+
# current definitions, so a pass that raises leaves the live facade — and every namespace under it
|
|
43
|
+
# — untouched.
|
|
44
|
+
#
|
|
45
|
+
# @yield [] evaluated against a {Briefly::Builder}
|
|
46
|
+
# @return [self]
|
|
47
|
+
def configure(&block)
|
|
48
|
+
builder = __prepare
|
|
49
|
+
builder.instance_eval(&block) if block
|
|
50
|
+
# `compile!` validates the whole tree before `__commit` installs any of it. Splitting the two is
|
|
51
|
+
# what keeps a namespace pass atomic: `Builder#namespace` collects its child's pass rather than
|
|
52
|
+
# running it, so a later failure anywhere cannot leave an already-reachable child half-updated.
|
|
53
|
+
__commit(builder.compile!)
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [String] a summary listing shortcut names; never dumps memo internals
|
|
58
|
+
def inspect = "#<#{self.class.name} shortcuts=#{shortcuts.inspect}>"
|
|
59
|
+
alias to_s inspect
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# A builder seeded with copies of everything a pass may mutate. {Briefly::Builder#namespace} calls
|
|
64
|
+
# this on the child it collects.
|
|
65
|
+
#
|
|
66
|
+
# @return [Briefly::Builder]
|
|
67
|
+
def __prepare = Builder.new(self, @__defs.transform_values(&:dup), @__children.dup)
|
|
68
|
+
|
|
69
|
+
# Installs a validated pass, children first. Nothing here may raise: every check ran in +compile!+.
|
|
70
|
+
#
|
|
71
|
+
# @param plan [Array] +compile!+'s +[defs, error_entries, children, child_plans]+
|
|
72
|
+
# @return [self]
|
|
73
|
+
def __commit(plan)
|
|
74
|
+
defs, error_entries, children, child_plans = plan
|
|
75
|
+
child_plans.each { |child, child_plan| child.send(:__commit, child_plan) }
|
|
76
|
+
@__children = children
|
|
77
|
+
__install(defs, error_entries)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Compiles definitions onto the singleton class and appends error registrations.
|
|
81
|
+
def __install(defs, error_entries)
|
|
82
|
+
wanted = defs.each_value.flat_map(&:names)
|
|
83
|
+
__remove_methods(@__public - wanted)
|
|
84
|
+
defs.each_value { |defn| __define(defn) }
|
|
85
|
+
|
|
86
|
+
@__public = wanted.freeze
|
|
87
|
+
@__defs = defs
|
|
88
|
+
@__aliases = defs.each_value.flat_map { |d| d.aliases.map { |a| [a, d.canonical] } }.to_h
|
|
89
|
+
error_entries.each { |klass, names, handler| @__errors.add(klass, names, handler) }
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def __remove_methods(names)
|
|
94
|
+
names.each { |name| singleton_class.send(:remove_method, name) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Candor installs the body privately under its reserved prefix, reads that *compiled* method's
|
|
98
|
+
# `parameters` — never `defn.body.parameters`, which reports every positional as `:opt` and would
|
|
99
|
+
# silently destroy arity strictness — and compiles a dispatch of the same arity onto every name.
|
|
100
|
+
# Arity is enforced there, before `__call` and its rescue layer are entered.
|
|
101
|
+
#
|
|
102
|
+
# No `parameters:` override for a memoized shortcut: `compile!` has already refused any memoized body
|
|
103
|
+
# that takes an argument, so its compiled shape is empty and `App.catalog(1)` raises on its own.
|
|
104
|
+
#
|
|
105
|
+
# `source_location:` because `namespace` rewrites it: its body is a `proc { child }` literal in
|
|
106
|
+
# `builder.rb`, and the compiled method must point at the caller's block instead.
|
|
107
|
+
def __define(defn)
|
|
108
|
+
Candor.define(singleton_class, defn.canonical,
|
|
109
|
+
aliases: defn.aliases,
|
|
110
|
+
via: :__call,
|
|
111
|
+
source_location: defn.source_location,
|
|
112
|
+
body: defn.body)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def __call(name, ...)
|
|
116
|
+
# Outside the rescue: an internal KeyError must never be laundered into a user's fallback.
|
|
117
|
+
defn = @__defs.fetch(name)
|
|
118
|
+
begin
|
|
119
|
+
return __memo(name) { send(defn.raw_name) } if defn.memoized?
|
|
120
|
+
|
|
121
|
+
send(defn.raw_name, ...)
|
|
122
|
+
rescue StandardError => e
|
|
123
|
+
__handle(e, name)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def __memo(name)
|
|
128
|
+
value = @__memos.fetch(name, UNSET)
|
|
129
|
+
return value unless UNSET.equal?(value)
|
|
130
|
+
|
|
131
|
+
# One reentrant lock for the whole facade: first computations of unrelated memoized shortcuts
|
|
132
|
+
# serialize. Per-shortcut locks would deadlock on a body that reaches another memoized body.
|
|
133
|
+
@__monitor.synchronize do
|
|
134
|
+
value = @__memos.fetch(name, UNSET)
|
|
135
|
+
return value unless UNSET.equal?(value)
|
|
136
|
+
|
|
137
|
+
computed = yield # raising here stores nothing: a rescued fallback is never memoized
|
|
138
|
+
@__memos = @__memos.merge(name => computed).freeze
|
|
139
|
+
computed
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def __handle(error, name)
|
|
144
|
+
entry = @__errors.scoped(name).find { |e| error.is_a?(e.klass) } ||
|
|
145
|
+
@__errors.wide.find { |e| error.is_a?(e.klass) } ||
|
|
146
|
+
Briefly.errors.wide.find { |e| error.is_a?(e.klass) }
|
|
147
|
+
# Kernel.raise, not bare raise: a shortcut may not be named `raise`, but a pack could still
|
|
148
|
+
# define one on a subclass, and this must never dispatch back into the facade.
|
|
149
|
+
Kernel.raise(error) unless entry
|
|
150
|
+
|
|
151
|
+
entry.handler.call(error, name)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Names a shortcut may not take: every facade method, plus Briefly's own private ones, plus the
|
|
156
|
+
# Kernel private methods the facade itself calls with an implicit receiver. Other Kernel privates
|
|
157
|
+
# (+puts+, +format+, ...) are deliberately absent: shadowing those is the user's call.
|
|
158
|
+
Facade::RESERVED = (Facade.instance_methods + Facade.private_instance_methods(false) + %i[raise]).freeze
|
|
159
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Briefly
|
|
4
|
+
module Rails
|
|
5
|
+
# Shortcut pack for a single database, reached through one Active Record class.
|
|
6
|
+
#
|
|
7
|
+
# App = Briefly.define do
|
|
8
|
+
# namespace(:db) { use Briefly::Rails::DB }
|
|
9
|
+
# namespace(:db2) { use Briefly::Rails::DB, base: "SecondaryApplicationRecord" }
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# App.db.txn { App.db.query("select * from users where id = ?", 1) }
|
|
13
|
+
#
|
|
14
|
+
# +base+ is a constant path, resolved on every call. Pass a String, not the class: a pack is
|
|
15
|
+
# +use+d from an initializer, where naming an autoloadable constant is what Rails warns about, and
|
|
16
|
+
# the captured class would go stale on the first code reload — permanently, since {Reload} clears
|
|
17
|
+
# memos, not closures. A Module is accepted for applications outside the autoloader, with that caveat.
|
|
18
|
+
#
|
|
19
|
+
# Nothing here memoizes: a connection is leased from the pool, and +query+ takes arguments. So the
|
|
20
|
+
# pack does not wire {Reload}, and works without a booted application.
|
|
21
|
+
#
|
|
22
|
+
# Inside this file the framework is always +::Rails+ — bare +Rails+ would resolve to the parent module.
|
|
23
|
+
module DB
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# @param builder [Briefly::Builder]
|
|
27
|
+
# @param base [String, Symbol, Module] the Active Record class this database hangs off
|
|
28
|
+
# @return [Briefly::Builder]
|
|
29
|
+
def install(builder, base: "ApplicationRecord")
|
|
30
|
+
model = -> { base.is_a?(Module) ? base : Object.const_get(base) }
|
|
31
|
+
|
|
32
|
+
builder.shortcut(:connection, :conn) { model.call.lease_connection }
|
|
33
|
+
builder.shortcut(:transaction, :txn) { |**opts, &blk| model.call.transaction(**opts, &blk) }
|
|
34
|
+
# `with_connection`, not `connection`: the latter is soft-deprecated, and raises outright under
|
|
35
|
+
# `ActiveRecord.permanent_connection_checkout = :disallowed`.
|
|
36
|
+
#
|
|
37
|
+
# A bindless statement must skip `sanitize_sql_array`, which would fall through to its
|
|
38
|
+
# `statement % values` branch and raise on any literal `%` — `... like '%foo%'`.
|
|
39
|
+
#
|
|
40
|
+
# No `**` parameter here, deliberately: taking no keywords is what makes Ruby pack
|
|
41
|
+
# `query(sql, id: 1)` into `binds` as a trailing Hash, which is how named binds reach
|
|
42
|
+
# `sanitize_sql_array`. A `**opts` would swallow them and send the statement unbound.
|
|
43
|
+
builder.shortcut(:query) do |sql, *binds|
|
|
44
|
+
record = model.call
|
|
45
|
+
statement = binds.empty? ? sql : record.sanitize_sql_array([sql, *binds])
|
|
46
|
+
record.with_connection { |connection| connection.exec_query(statement) }
|
|
47
|
+
end
|
|
48
|
+
builder
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Briefly
|
|
4
|
+
module Rails
|
|
5
|
+
# Lifecycle pack: clears the facade's memos at boot and on every code reload.
|
|
6
|
+
#
|
|
7
|
+
# Usable on its own, for a facade that has no framework shortcuts but does memoize objects
|
|
8
|
+
# holding on to reloadable application classes:
|
|
9
|
+
#
|
|
10
|
+
# Admin = Briefly.define { use Briefly::Rails::Reload }
|
|
11
|
+
#
|
|
12
|
+
# The callback holds the facade for the process lifetime and cannot be deregistered. Install it
|
|
13
|
+
# on long-lived facades assigned to constants, not on facades built per request.
|
|
14
|
+
module Reload
|
|
15
|
+
# Marks a facade so a second +configure+ pass does not register a duplicate callback.
|
|
16
|
+
INSTALLED = :@__briefly_rails_reload
|
|
17
|
+
private_constant :INSTALLED
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# @param builder [Briefly::Builder]
|
|
22
|
+
# @return [Briefly::Builder]
|
|
23
|
+
# @raise [Briefly::Error] outside a booted application
|
|
24
|
+
def install(builder)
|
|
25
|
+
facade = builder.facade
|
|
26
|
+
return builder if facade.instance_variable_defined?(INSTALLED)
|
|
27
|
+
|
|
28
|
+
app = ::Rails.application if defined?(::Rails)
|
|
29
|
+
unless app.respond_to?(:reloader)
|
|
30
|
+
raise Briefly::Error, "Briefly::Rails::Reload needs a booted application; use it from an initializer"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
facade.instance_variable_set(INSTALLED, true)
|
|
34
|
+
app.reloader.to_prepare { facade.clear_memos! }
|
|
35
|
+
builder
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "briefly"
|
|
4
|
+
require "briefly/rails/db"
|
|
5
|
+
require "briefly/rails/reload"
|
|
6
|
+
|
|
7
|
+
module Briefly
|
|
8
|
+
# Shortcut pack for Rails applications.
|
|
9
|
+
#
|
|
10
|
+
# App = Briefly.define { use Briefly::Rails }
|
|
11
|
+
# App.c # => Rails.configuration
|
|
12
|
+
# App.render(...) # => ApplicationController.renderer.render(...)
|
|
13
|
+
# App.db.txn { } # => ApplicationRecord.transaction { }
|
|
14
|
+
#
|
|
15
|
+
# An umbrella over {Config}, {Env}, {View} and {Reload}, plus {DB} under the +db+ namespace. Each is
|
|
16
|
+
# a pack in its own right, so a facade can take only the parts it wants:
|
|
17
|
+
#
|
|
18
|
+
# Admin = Briefly.define do
|
|
19
|
+
# use "rails/env"
|
|
20
|
+
# namespace(:primary) { use "rails/db" }
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# Nothing here memoizes. Every shortcut is a live lookup: the framework already caches the
|
|
24
|
+
# expensive ones (+helpers+, +routes+, +renderer+) on objects it refreshes on reload, so caching
|
|
25
|
+
# them again would only go stale. {Reload} is still wired in, because the *application's* own
|
|
26
|
+
# memoized shortcuts need clearing; the mini-packs, having nothing to clear, leave it alone.
|
|
27
|
+
#
|
|
28
|
+
# Inside this file the framework is always +::Rails+ — bare +Rails+ would resolve to this module.
|
|
29
|
+
module Rails
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# @param builder [Briefly::Builder]
|
|
33
|
+
# @return [Briefly::Builder]
|
|
34
|
+
def install(builder)
|
|
35
|
+
builder.use(Reload)
|
|
36
|
+
builder.use(Config)
|
|
37
|
+
builder.use(Env)
|
|
38
|
+
builder.use(View)
|
|
39
|
+
builder.namespace(:db) { use DB }
|
|
40
|
+
builder
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Configuration, paths and the application-wide singletons.
|
|
44
|
+
module Config
|
|
45
|
+
module_function
|
|
46
|
+
|
|
47
|
+
# @param builder [Briefly::Builder]
|
|
48
|
+
# @return [Briefly::Builder]
|
|
49
|
+
def install(builder)
|
|
50
|
+
builder.shortcut(:config, :c) { ::Rails.configuration }
|
|
51
|
+
builder.shortcut(:config_x, :x) { ::Rails.configuration.x }
|
|
52
|
+
builder.shortcut(:root) { ::Rails.root }
|
|
53
|
+
builder.shortcut(:cache) { ::Rails.cache }
|
|
54
|
+
builder.shortcut(:logger, :log) { ::Rails.logger }
|
|
55
|
+
builder.shortcut(:credentials, :cred) { ::Rails.application.credentials }
|
|
56
|
+
builder
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The environment and its predicates.
|
|
61
|
+
module Env
|
|
62
|
+
module_function
|
|
63
|
+
|
|
64
|
+
# @param builder [Briefly::Builder]
|
|
65
|
+
# @return [Briefly::Builder]
|
|
66
|
+
def install(builder)
|
|
67
|
+
builder.shortcut(:env) { ::Rails.env }
|
|
68
|
+
builder.shortcut(:production?) { ::Rails.env.production? }
|
|
69
|
+
builder.shortcut(:development?) { ::Rails.env.development? }
|
|
70
|
+
builder.shortcut(:test?) { ::Rails.env.test? }
|
|
71
|
+
builder.shortcut(:local?) { ::Rails.env.local? }
|
|
72
|
+
builder
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Helpers, routes and rendering outside a controller.
|
|
77
|
+
module View
|
|
78
|
+
module_function
|
|
79
|
+
|
|
80
|
+
# @param builder [Briefly::Builder]
|
|
81
|
+
# @return [Briefly::Builder]
|
|
82
|
+
def install(builder)
|
|
83
|
+
builder.shortcut(:helpers, :h) { ::ApplicationController.helpers }
|
|
84
|
+
builder.shortcut(:routes, :r) { ::Rails.application.routes.url_helpers }
|
|
85
|
+
builder.shortcut(:renderer) { ::ApplicationController.renderer }
|
|
86
|
+
builder.shortcut(:render) { |*args, **kwargs, &blk| renderer.render(*args, **kwargs, &blk) }
|
|
87
|
+
builder
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|