briefly 0.1.0 → 0.2.1
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 +4 -4
- data/CHANGELOG.md +95 -10
- data/README.md +235 -115
- data/briefly.gemspec +1 -1
- data/lib/briefly/builder.rb +58 -42
- data/lib/briefly/errors.rb +1 -1
- data/lib/briefly/facade.rb +79 -31
- data/lib/briefly/rails/config.rb +35 -0
- data/lib/briefly/rails/db.rb +33 -8
- data/lib/briefly/rails/env.rb +27 -0
- data/lib/briefly/rails/instrument.rb +29 -0
- data/lib/briefly/rails/reload.rb +1 -1
- data/lib/briefly/rails/view.rb +27 -0
- data/lib/briefly/rails.rb +7 -50
- data/lib/briefly/rescues.rb +47 -0
- data/lib/briefly/shortcut.rb +104 -0
- data/lib/briefly/version.rb +1 -1
- data/lib/briefly.rb +26 -9
- data/lib/generators/briefly/install/install_generator.rb +29 -0
- data/lib/generators/briefly/install/templates/briefly.rb.tt +47 -0
- data/sig/briefly/builder.rbs +18 -8
- data/sig/briefly/facade.rbs +26 -12
- data/sig/briefly/rails.rbs +7 -2
- data/sig/briefly/rescues.rbs +21 -0
- data/sig/briefly/{definition.rbs → shortcut.rbs} +8 -4
- data/sig/briefly.rbs +7 -10
- metadata +12 -6
- data/lib/briefly/definition.rb +0 -64
- data/lib/briefly/error_registry.rb +0 -40
- data/sig/briefly/error_registry.rbs +0 -22
data/lib/briefly/builder.rb
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Briefly
|
|
4
|
-
# Receives the DSL. {Briefly.define}'s block and {Briefly::Facade#configure}'s block are
|
|
4
|
+
# Receives the DSL. {Briefly.define}'s block and {Briefly::Facade::Control#configure}'s block are
|
|
5
5
|
# +instance_eval+'d here, never on the facade, so DSL verbs can never collide with shortcut names.
|
|
6
6
|
#
|
|
7
|
-
# The block only collects; {#compile!} then validates, and {Briefly::Facade#configure} installs.
|
|
7
|
+
# The block only collects; {#compile!} then validates, and {Briefly::Facade::Control#configure} installs.
|
|
8
8
|
class Builder
|
|
9
|
+
# A validated {#compile!} pass, ready for a {Briefly::Facade} to install. +child_plans+ holds one
|
|
10
|
+
# +[child_facade, child_plan]+ pair per namespace this pass collected, so +__commit+ can walk the
|
|
11
|
+
# tree children first.
|
|
12
|
+
Plan = Struct.new(:defs, :rescue_entries, :children, :child_plans)
|
|
13
|
+
|
|
9
14
|
# @return [Briefly::Facade] the facade under construction, for packs that need lifecycle hooks
|
|
10
15
|
attr_reader :facade
|
|
11
16
|
|
|
12
17
|
# @api private
|
|
13
18
|
# @param facade [Briefly::Facade]
|
|
14
|
-
# @param defs [Hash{Symbol => Briefly::
|
|
19
|
+
# @param defs [Hash{Symbol => Briefly::Shortcut}] copies of the facade's current shortcuts
|
|
15
20
|
# @param children [Hash{Symbol => Briefly::Facade}] the facade's namespaces, by name
|
|
16
21
|
def initialize(facade, defs, children)
|
|
17
22
|
@facade = facade
|
|
18
23
|
@defs = defs
|
|
19
24
|
@children = children
|
|
20
|
-
@
|
|
25
|
+
@rescues = []
|
|
21
26
|
@pending = {}
|
|
22
27
|
end
|
|
23
28
|
|
|
@@ -38,10 +43,10 @@ module Briefly
|
|
|
38
43
|
end
|
|
39
44
|
|
|
40
45
|
# Declares a namespace: a shortcut returning a child facade, so +App.db.query+ works. The block is
|
|
41
|
-
# the child's own DSL — +shortcut
|
|
46
|
+
# the child's own DSL — +shortcut+ (and the shortcut it returns), +use+, +rescue_from+, and namespaces.
|
|
42
47
|
#
|
|
43
48
|
# 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
|
|
49
|
+
# {Briefly::Facade::Control#clear_memos!} cascades into it. Two limits, both deliberate: a child body cannot
|
|
45
50
|
# reach a root shortcut by bare name, and a root +rescue_from+ does not scope into the child.
|
|
46
51
|
#
|
|
47
52
|
# The child's pass is *collected*, not run: its builder is held until {#compile!} has validated the
|
|
@@ -70,66 +75,67 @@ module Briefly
|
|
|
70
75
|
name
|
|
71
76
|
end
|
|
72
77
|
|
|
73
|
-
# Declares a shortcut
|
|
78
|
+
# Declares a shortcut, or fetches an already-declared one to refine.
|
|
79
|
+
#
|
|
80
|
+
# With a block, declares the shortcut (redeclaring a name, canonical or alias, silently overrides it)
|
|
81
|
+
# and returns the {Briefly::Shortcut}, so you can chain +.memoize+ or +.rescue_from+ onto it. With no
|
|
82
|
+
# block, +shortcut(name)+ fetches the shortcut +name+ resolves to (canonical or alias) so a pack's
|
|
83
|
+
# shortcut can be refined; it never declares, purges, or overrides.
|
|
74
84
|
#
|
|
75
85
|
# @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
|
|
86
|
+
# @param aliases [Array<Symbol>] extra names sharing the body and the memo cell (declare only)
|
|
77
87
|
# @yield the implementation, bound to the facade at call time
|
|
78
|
-
# @return [
|
|
88
|
+
# @return [Briefly::Shortcut] the declared or fetched shortcut, ready to refine
|
|
89
|
+
# @raise [ArgumentError] if a bodiless call is given aliases — a fetch ignores them, so a
|
|
90
|
+
# non-empty list means the block was forgotten
|
|
79
91
|
# @raise [Briefly::ReservedNameError] if any name shadows a facade method
|
|
92
|
+
# @raise [Briefly::UnknownShortcutError] if a bodiless +name+ resolves to nothing
|
|
80
93
|
def shortcut(canonical, *aliases, &body)
|
|
81
|
-
|
|
94
|
+
unless body
|
|
95
|
+
# A bodiless `shortcut(name)` fetches an existing shortcut to refine; aliases are meaningless
|
|
96
|
+
# there, so a non-empty list means a block was forgotten — fail loudly, don't drop them silently.
|
|
97
|
+
raise ArgumentError, "shortcut(#{canonical.inspect}, ...) with aliases requires a block" unless aliases.empty?
|
|
82
98
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
defn =
|
|
99
|
+
return fetch(canonical)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
defn = Shortcut.new(canonical, aliases, body, source_location_for(body))
|
|
87
103
|
defn.names.each { |name| validate_name!(name) }
|
|
88
104
|
purge(defn.names, except: canonical)
|
|
89
105
|
@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
106
|
end
|
|
106
107
|
|
|
107
|
-
# Registers
|
|
108
|
+
# Registers a facade-wide error handler: one applying to every shortcut, consulted after each
|
|
109
|
+
# shortcut's own handlers. The handler's return value becomes the shortcut's return value. To guard a
|
|
110
|
+
# single shortcut, chain onto it instead — +shortcut(name).rescue_from(error_class) { ... }+ — which
|
|
111
|
+
# is why this verb takes no shortcut names.
|
|
108
112
|
#
|
|
109
113
|
# Because +{}+ binds to the nearest token, +rescue_from StandardError { }+ is a call to a method
|
|
110
|
-
# named +StandardError+. Use +rescue_from Err
|
|
114
|
+
# named +StandardError+. Use +rescue_from Err do |e| ... end+ or +rescue_from(Err) { |e| ... }+.
|
|
111
115
|
#
|
|
112
116
|
# @param error_class [Class] matched against the raised error and its subclasses
|
|
113
|
-
# @param names [Array<Symbol>]
|
|
117
|
+
# @param names [Array<Symbol>] must be empty; any name is refused because the shortcut expresses it
|
|
114
118
|
# @yield [error, shortcut_name] the recovery block
|
|
115
119
|
# @return [self]
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
# @raise [ArgumentError] if any shortcut names are given
|
|
121
|
+
def rescue_from(error_class, *names, &block)
|
|
122
|
+
Briefly.send(:validate_rescue!, error_class, block)
|
|
123
|
+
unless names.flatten.empty?
|
|
124
|
+
raise ArgumentError, "rescue_from(#{error_class}) takes no shortcut names — scope one to its " \
|
|
125
|
+
"shortcut with shortcut(name).rescue_from(#{error_class}) { ... }, or omit " \
|
|
126
|
+
"names for a facade-wide handler"
|
|
119
127
|
end
|
|
120
|
-
raise ArgumentError, "rescue_from(#{error_class}) requires a block" unless handler
|
|
121
128
|
|
|
122
|
-
|
|
123
|
-
@errors << [error_class, (scoped.empty? ? nil : scoped), handler]
|
|
129
|
+
@rescues << [error_class, block]
|
|
124
130
|
self
|
|
125
131
|
end
|
|
126
132
|
|
|
127
133
|
# 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
|
|
134
|
+
# {Briefly::Facade::Control#configure} installs the result. Validation of the whole tree finishes before any
|
|
129
135
|
# of it is installed.
|
|
130
136
|
#
|
|
131
137
|
# @api private
|
|
132
|
-
# @return [
|
|
138
|
+
# @return [Briefly::Builder::Plan]
|
|
133
139
|
# @raise [Briefly::Error] if a memoized shortcut's body takes arguments
|
|
134
140
|
def compile!
|
|
135
141
|
@defs.each_value do |defn|
|
|
@@ -138,11 +144,21 @@ module Briefly
|
|
|
138
144
|
|
|
139
145
|
raise Error, "cannot memoize #{defn.canonical}: its body takes arguments"
|
|
140
146
|
end
|
|
141
|
-
|
|
147
|
+
Plan.new(@defs, @rescues, @children, @pending.map { |name, builder| [@children[name], builder.compile!] })
|
|
142
148
|
end
|
|
143
149
|
|
|
144
150
|
private
|
|
145
151
|
|
|
152
|
+
# A `&:upcase`-style Proc carries no location of its own; fall back to the caller's declaration site
|
|
153
|
+
# so the compiled method points at the user's code, never a fabricated file. `namespace` overwrites it
|
|
154
|
+
# anyway. Depth 2 skips this helper and `shortcut` to land on the DSL block — or a pack's `install`.
|
|
155
|
+
#
|
|
156
|
+
# @param body [Proc]
|
|
157
|
+
# @return [Array(String, Integer)]
|
|
158
|
+
def source_location_for(body)
|
|
159
|
+
body.source_location || caller_locations(2, 1).first.then { |l| [l.path, l.lineno] }
|
|
160
|
+
end
|
|
161
|
+
|
|
146
162
|
def validate_name!(name)
|
|
147
163
|
raise ArgumentError, "shortcut names must be Symbols, got #{name.inspect}" unless name.is_a?(Symbol)
|
|
148
164
|
raise ReservedNameError, "#{name} is reserved by Briefly::Facade" if Facade::RESERVED.include?(name)
|
|
@@ -152,7 +168,7 @@ module Briefly
|
|
|
152
168
|
raise ReservedNameError, "#{name} is reserved: #{Candor::BODY_PREFIX}* names hold compiled shortcut bodies"
|
|
153
169
|
end
|
|
154
170
|
|
|
155
|
-
# Frees the incoming names from any
|
|
171
|
+
# Frees the incoming names from any shortcut that currently owns them, so a redeclaration
|
|
156
172
|
# never leaves a stale alias pointing at the old body.
|
|
157
173
|
def purge(names, except:)
|
|
158
174
|
@defs.delete_if { |canonical, _| canonical != except && names.include?(canonical) }
|
data/lib/briefly/errors.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Briefly
|
|
|
4
4
|
# Base class for every error raised by Briefly.
|
|
5
5
|
class Error < StandardError; end
|
|
6
6
|
|
|
7
|
-
# Raised when
|
|
7
|
+
# Raised when a bodiless +shortcut+ names a shortcut that does not exist.
|
|
8
8
|
class UnknownShortcutError < Error; end
|
|
9
9
|
|
|
10
10
|
# Raised when a shortcut name or alias would shadow a facade method.
|
data/lib/briefly/facade.rb
CHANGED
|
@@ -14,29 +14,75 @@ module Briefly
|
|
|
14
14
|
@__aliases = {}
|
|
15
15
|
@__memos = {}.freeze
|
|
16
16
|
@__monitor = Monitor.new
|
|
17
|
-
@
|
|
18
|
-
@
|
|
17
|
+
@__rescues = Rescues.new
|
|
18
|
+
@__installed = [].freeze
|
|
19
19
|
@__children = {}
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
# The single public door to a facade's management operations, so those five names stay free for
|
|
23
|
+
# shortcuts. A fresh {Control} each call is stateless and thread-safe — nothing to race on — and an
|
|
24
|
+
# allocation is trivial next to how rarely management runs; identity across calls is not guaranteed.
|
|
25
|
+
#
|
|
26
|
+
# @return [Control]
|
|
27
|
+
def briefly = Control.new(self)
|
|
28
|
+
|
|
29
|
+
# @return [String] a summary listing shortcut names; never dumps memo internals
|
|
30
|
+
def inspect = "#<#{self.class.name} shortcuts=#{__shortcuts.inspect}>"
|
|
31
|
+
alias to_s inspect
|
|
32
|
+
|
|
33
|
+
# The management surface, forwarded to the facade's private +__+-prefixed methods. Mutators return
|
|
34
|
+
# the facade so +Briefly.define+-style chaining and the return-self contract hold; readers return
|
|
35
|
+
# their values. It forwards via +send+, matching the +child.send(:__commit, …)+ pattern +__commit+
|
|
36
|
+
# already uses to cross the private boundary.
|
|
37
|
+
class Control
|
|
38
|
+
# @param facade [Facade]
|
|
39
|
+
def initialize(facade) = @facade = facade
|
|
40
|
+
|
|
41
|
+
# @return [Array<Symbol>] canonical shortcut names, sorted; aliases excluded
|
|
42
|
+
def shortcuts = @facade.send(:__shortcuts)
|
|
43
|
+
|
|
44
|
+
# @param name [Symbol] a canonical name or an alias
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
def shortcut?(name) = @facade.send(:__shortcut?, name)
|
|
47
|
+
|
|
48
|
+
# Drops every memoized value, here and in every namespace. Thread-safe.
|
|
49
|
+
#
|
|
50
|
+
# @return [Facade] the facade
|
|
51
|
+
def clear_memos! = @facade.send(:__clear_memos!)
|
|
52
|
+
|
|
53
|
+
# Reopens the facade for another builder pass and recompiles.
|
|
54
|
+
#
|
|
55
|
+
# @yield [] evaluated against a {Briefly::Builder}
|
|
56
|
+
# @return [Facade] the facade
|
|
57
|
+
def configure(&) = @facade.send(:__configure, &)
|
|
58
|
+
|
|
59
|
+
# Names the management operations, so typing `App.briefly` at a console self-describes what the
|
|
60
|
+
# door offers instead of echoing the facade's shortcut list. Keep in sync with the methods above.
|
|
61
|
+
#
|
|
62
|
+
# @return [String]
|
|
63
|
+
def inspect = "#<#{self.class.name} shortcuts shortcut? clear_memos! configure>"
|
|
64
|
+
alias to_s inspect
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
22
69
|
# @return [Array<Symbol>] canonical shortcut names, sorted; aliases excluded
|
|
23
|
-
def
|
|
70
|
+
def __shortcuts = @__defs.keys.sort
|
|
24
71
|
|
|
25
72
|
# @param name [Symbol] a canonical name or an alias
|
|
26
73
|
# @return [Boolean]
|
|
27
|
-
def
|
|
74
|
+
def __shortcut?(name) = @__defs.key?(name) || @__aliases.key?(name)
|
|
28
75
|
|
|
29
76
|
# Drops every memoized value, here and in every namespace. Thread-safe.
|
|
30
77
|
#
|
|
31
78
|
# @return [self]
|
|
32
|
-
def
|
|
79
|
+
def __clear_memos!
|
|
33
80
|
@__monitor.synchronize { @__memos = {}.freeze }
|
|
34
81
|
# Each namespace owns its memo store and its own lock, so a child clears outside our monitor.
|
|
35
82
|
# One `Reload` on the root therefore clears the whole tree, including namespaces holding no pack.
|
|
36
|
-
@__children.each_value(
|
|
83
|
+
@__children.each_value { |child| child.send(:__clear_memos!) }
|
|
37
84
|
self
|
|
38
85
|
end
|
|
39
|
-
alias reset! clear_memos!
|
|
40
86
|
|
|
41
87
|
# Reopens the facade for another builder pass and recompiles. The builder starts from copies of the
|
|
42
88
|
# current definitions, so a pass that raises leaves the live facade — and every namespace under it
|
|
@@ -44,7 +90,7 @@ module Briefly
|
|
|
44
90
|
#
|
|
45
91
|
# @yield [] evaluated against a {Briefly::Builder}
|
|
46
92
|
# @return [self]
|
|
47
|
-
def
|
|
93
|
+
def __configure(&block)
|
|
48
94
|
builder = __prepare
|
|
49
95
|
builder.instance_eval(&block) if block
|
|
50
96
|
# `compile!` validates the whole tree before `__commit` installs any of it. Splitting the two is
|
|
@@ -54,12 +100,6 @@ module Briefly
|
|
|
54
100
|
self
|
|
55
101
|
end
|
|
56
102
|
|
|
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
103
|
# A builder seeded with copies of everything a pass may mutate. {Briefly::Builder#namespace} calls
|
|
64
104
|
# this on the child it collects.
|
|
65
105
|
#
|
|
@@ -68,25 +108,28 @@ module Briefly
|
|
|
68
108
|
|
|
69
109
|
# Installs a validated pass, children first. Nothing here may raise: every check ran in +compile!+.
|
|
70
110
|
#
|
|
71
|
-
# @param plan [
|
|
111
|
+
# @param plan [Briefly::Builder::Plan] a validated +compile!+ pass
|
|
72
112
|
# @return [self]
|
|
73
113
|
def __commit(plan)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
__install(defs, error_entries)
|
|
114
|
+
plan.child_plans.each { |child, child_plan| child.send(:__commit, child_plan) }
|
|
115
|
+
@__children = plan.children
|
|
116
|
+
__install(plan.defs, plan.rescue_entries)
|
|
78
117
|
end
|
|
79
118
|
|
|
80
|
-
# Compiles definitions onto the singleton class and appends
|
|
81
|
-
|
|
119
|
+
# Compiles definitions onto the singleton class and appends rescue registrations.
|
|
120
|
+
#
|
|
121
|
+
# @param defs [Hash{Symbol => Briefly::Shortcut}] the validated shortcuts, keyed by canonical name
|
|
122
|
+
# @param rescue_entries [Array<Array(Class, Proc)>] facade-wide +[error_class, handler]+ pairs
|
|
123
|
+
# @return [self]
|
|
124
|
+
def __install(defs, rescue_entries)
|
|
82
125
|
wanted = defs.each_value.flat_map(&:names)
|
|
83
|
-
__remove_methods(@
|
|
126
|
+
__remove_methods(@__installed - wanted)
|
|
84
127
|
defs.each_value { |defn| __define(defn) }
|
|
85
128
|
|
|
86
|
-
@
|
|
129
|
+
@__installed = wanted.freeze
|
|
87
130
|
@__defs = defs
|
|
88
131
|
@__aliases = defs.each_value.flat_map { |d| d.aliases.map { |a| [a, d.canonical] } }.to_h
|
|
89
|
-
|
|
132
|
+
rescue_entries.each { |klass, handler| @__rescues.add(klass, handler) }
|
|
90
133
|
self
|
|
91
134
|
end
|
|
92
135
|
|
|
@@ -120,7 +163,7 @@ module Briefly
|
|
|
120
163
|
|
|
121
164
|
send(defn.raw_name, ...)
|
|
122
165
|
rescue StandardError => e
|
|
123
|
-
__handle(e,
|
|
166
|
+
__handle(e, defn)
|
|
124
167
|
end
|
|
125
168
|
end
|
|
126
169
|
|
|
@@ -140,15 +183,20 @@ module Briefly
|
|
|
140
183
|
end
|
|
141
184
|
end
|
|
142
185
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
186
|
+
# Three tiers, most specific first: this shortcut's own handlers, then the facade-wide ones, then
|
|
187
|
+
# the global ones. Each container matches its own handlers (newest wins within a tier); this only
|
|
188
|
+
# picks the first tier that answers.
|
|
189
|
+
#
|
|
190
|
+
# @param error [StandardError]
|
|
191
|
+
# @param defn [Briefly::Shortcut]
|
|
192
|
+
# @return [Object] the matching handler's return value; if none matches, +error+ is re-raised
|
|
193
|
+
def __handle(error, defn)
|
|
194
|
+
handler = defn.handler_for(error) || @__rescues.handler_for(error) || Briefly.rescues.handler_for(error)
|
|
147
195
|
# Kernel.raise, not bare raise: a shortcut may not be named `raise`, but a pack could still
|
|
148
196
|
# define one on a subclass, and this must never dispatch back into the facade.
|
|
149
|
-
Kernel.raise(error) unless
|
|
197
|
+
Kernel.raise(error) unless handler
|
|
150
198
|
|
|
151
|
-
|
|
199
|
+
handler.call(error, defn.canonical)
|
|
152
200
|
end
|
|
153
201
|
end
|
|
154
202
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Briefly
|
|
4
|
+
module Rails
|
|
5
|
+
# Configuration, paths and the application-wide singletons.
|
|
6
|
+
#
|
|
7
|
+
# App = Briefly.define { use Briefly::Rails::Config }
|
|
8
|
+
# App.c # => Rails.configuration
|
|
9
|
+
# App.root # => Rails.root
|
|
10
|
+
#
|
|
11
|
+
# +error+ is the framework's handled-error reporter: +App.error.report(e)+, +App.error.handle { }+.
|
|
12
|
+
# +config_for+ reads a per-environment YAML config on every call, forwarding any keyword (such as
|
|
13
|
+
# +env:+) to +::Rails.application.config_for+; it takes an argument and is therefore never memoized —
|
|
14
|
+
# compose one that is with +shortcut(:payments) { config_for(:payments) }.memoize+.
|
|
15
|
+
#
|
|
16
|
+
# Inside this file the framework is always +::Rails+ — bare +Rails+ would resolve to the parent module.
|
|
17
|
+
module Config
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# @param builder [Briefly::Builder]
|
|
21
|
+
# @return [Briefly::Builder]
|
|
22
|
+
def install(builder)
|
|
23
|
+
builder.shortcut(:config, :c) { ::Rails.configuration }
|
|
24
|
+
builder.shortcut(:config_x, :x) { ::Rails.configuration.x }
|
|
25
|
+
builder.shortcut(:root) { ::Rails.root }
|
|
26
|
+
builder.shortcut(:cache) { ::Rails.cache }
|
|
27
|
+
builder.shortcut(:logger, :log) { ::Rails.logger }
|
|
28
|
+
builder.shortcut(:credentials, :cred) { ::Rails.application.credentials }
|
|
29
|
+
builder.shortcut(:error) { ::Rails.error }
|
|
30
|
+
builder.shortcut(:config_for) { |name, **opts| ::Rails.application.config_for(name, **opts) }
|
|
31
|
+
builder
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/briefly/rails/db.rb
CHANGED
|
@@ -9,15 +9,22 @@ module Briefly
|
|
|
9
9
|
# namespace(:db2) { use Briefly::Rails::DB, base: "SecondaryApplicationRecord" }
|
|
10
10
|
# end
|
|
11
11
|
#
|
|
12
|
-
# App.db.txn { App.db.
|
|
12
|
+
# App.db.txn { App.db.select("select * from users where id = ?", 1) }
|
|
13
13
|
#
|
|
14
14
|
# +base+ is a constant path, resolved on every call. Pass a String, not the class: a pack is
|
|
15
15
|
# +use+d from an initializer, where naming an autoloadable constant is what Rails warns about, and
|
|
16
16
|
# the captured class would go stale on the first code reload — permanently, since {Reload} clears
|
|
17
17
|
# memos, not closures. A Module is accepted for applications outside the autoloader, with that caveat.
|
|
18
18
|
#
|
|
19
|
-
# Nothing here memoizes:
|
|
20
|
-
# pack does not wire {Reload}, and works without a booted application.
|
|
19
|
+
# Nothing here memoizes: the block-form shortcuts can't be memoized, and +select+/+query+ take
|
|
20
|
+
# arguments. So the pack does not wire {Reload}, and works without a booted application.
|
|
21
|
+
#
|
|
22
|
+
# +connected_to+ forwards every argument to +base.connected_to+ — +role:+, +shard:+, +prevent_writes:+,
|
|
23
|
+
# and any custom role — so the full Rails multi-database surface is reachable. +reading+ and +writing+
|
|
24
|
+
# are sugar for the two common roles; they forward the rest (+shard:+, +prevent_writes:+) but pin the
|
|
25
|
+
# role, so +reading(role: :writing)+ still reads. Rails only allows +connected_to+ on +ActiveRecord::Base+
|
|
26
|
+
# or an abstract class — the one that declared +connects_to+ — so +base+ must be such a class; on a
|
|
27
|
+
# concrete model it raises +NotImplementedError+.
|
|
21
28
|
#
|
|
22
29
|
# Inside this file the framework is always +::Rails+ — bare +Rails+ would resolve to the parent module.
|
|
23
30
|
module DB
|
|
@@ -29,22 +36,40 @@ module Briefly
|
|
|
29
36
|
def install(builder, base: "ApplicationRecord")
|
|
30
37
|
model = -> { base.is_a?(Module) ? base : Object.const_get(base) }
|
|
31
38
|
|
|
32
|
-
|
|
39
|
+
# `connection`/`conn` mirrors `transaction`: a `with_connection` passthrough that yields the
|
|
40
|
+
# leased connection and auto-releases at block exit, forwarding every keyword (`prevent_permanent_checkout:`
|
|
41
|
+
# today, anything Rails adds later). A bare lease would leak outside a request; there is no
|
|
42
|
+
# `release`, so the block form is the whole story. No block gives a `LocalJumpError` from
|
|
43
|
+
# Rails' own `with_connection`, which is exactly the "requires a block" contract — no guard needed.
|
|
44
|
+
builder.shortcut(:connection, :conn) { |**opts, &blk| model.call.with_connection(**opts, &blk) }
|
|
33
45
|
builder.shortcut(:transaction, :txn) { |**opts, &blk| model.call.transaction(**opts, &blk) }
|
|
46
|
+
builder.shortcut(:connected_to) { |**opts, &blk| model.call.connected_to(**opts, &blk) }
|
|
47
|
+
# `**opts` first, `role:` last: the pinned role wins over any `role:` in `opts`, so `reading`
|
|
48
|
+
# always reads, while `shard:` and `prevent_writes:` still forward.
|
|
49
|
+
builder.shortcut(:reading) { |**opts, &blk| connected_to(**opts, role: :reading, &blk) }
|
|
50
|
+
builder.shortcut(:writing) { |**opts, &blk| connected_to(**opts, role: :writing, &blk) }
|
|
51
|
+
# `select` and `query` share one closure, differing only by the adapter method. `select` runs a
|
|
52
|
+
# read through `select_all` — the read-optimized path for a raw SELECT, returning an
|
|
53
|
+
# `ActiveRecord::Result` without clearing the query cache. `query` runs arbitrary SQL through
|
|
54
|
+
# `exec_query` — writes and DDL included. Neither polices the SQL it's given; the split is name
|
|
55
|
+
# and cache-path, not a runtime read/write guard.
|
|
56
|
+
#
|
|
34
57
|
# `with_connection`, not `connection`: the latter is soft-deprecated, and raises outright under
|
|
35
58
|
# `ActiveRecord.permanent_connection_checkout = :disallowed`.
|
|
36
59
|
#
|
|
37
60
|
# A bindless statement must skip `sanitize_sql_array`, which would fall through to its
|
|
38
61
|
# `statement % values` branch and raise on any literal `%` — `... like '%foo%'`.
|
|
39
62
|
#
|
|
40
|
-
# No `**` parameter
|
|
41
|
-
# `
|
|
63
|
+
# No `**` parameter on either shortcut, deliberately: taking no keywords is what makes Ruby pack
|
|
64
|
+
# `select(sql, id: 1)` into `binds` as a trailing Hash, which is how named binds reach
|
|
42
65
|
# `sanitize_sql_array`. A `**opts` would swallow them and send the statement unbound.
|
|
43
|
-
|
|
66
|
+
run = lambda do |method, sql, binds|
|
|
44
67
|
record = model.call
|
|
45
68
|
statement = binds.empty? ? sql : record.sanitize_sql_array([sql, *binds])
|
|
46
|
-
record.with_connection { |connection| connection.
|
|
69
|
+
record.with_connection { |connection| connection.public_send(method, statement) }
|
|
47
70
|
end
|
|
71
|
+
builder.shortcut(:select) { |sql, *binds| run.call(:select_all, sql, binds) }
|
|
72
|
+
builder.shortcut(:query) { |sql, *binds| run.call(:exec_query, sql, binds) }
|
|
48
73
|
builder
|
|
49
74
|
end
|
|
50
75
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Briefly
|
|
4
|
+
module Rails
|
|
5
|
+
# The environment and its predicates.
|
|
6
|
+
#
|
|
7
|
+
# App = Briefly.define { use Briefly::Rails::Env }
|
|
8
|
+
# App.env # => Rails.env
|
|
9
|
+
# App.production? # => Rails.env.production?
|
|
10
|
+
#
|
|
11
|
+
# Inside this file the framework is always +::Rails+ — bare +Rails+ would resolve to the parent module.
|
|
12
|
+
module Env
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# @param builder [Briefly::Builder]
|
|
16
|
+
# @return [Briefly::Builder]
|
|
17
|
+
def install(builder)
|
|
18
|
+
builder.shortcut(:env) { ::Rails.env }
|
|
19
|
+
builder.shortcut(:production?, :prod?) { ::Rails.env.production? }
|
|
20
|
+
builder.shortcut(:development?, :dev?) { ::Rails.env.development? }
|
|
21
|
+
builder.shortcut(:test?) { ::Rails.env.test? }
|
|
22
|
+
builder.shortcut(:local?) { ::Rails.env.local? }
|
|
23
|
+
builder
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Briefly
|
|
4
|
+
module Rails
|
|
5
|
+
# ActiveSupport::Notifications instrumentation, as a one-shortcut pack.
|
|
6
|
+
#
|
|
7
|
+
# App = Briefly.define { use Briefly::Rails::Instrument }
|
|
8
|
+
# App.instrument("sql.query", model: "User") { run_the_query }
|
|
9
|
+
#
|
|
10
|
+
# A lean worker can +use "rails/instrument"+ on its own; the umbrella +use "rails"+ pulls it in too.
|
|
11
|
+
# +payload+ is an optional trailing hash, so +instrument("evt", key: 1) { }+ threads +{ key: 1 }+
|
|
12
|
+
# through to the subscribers, and the block's return value is preserved.
|
|
13
|
+
#
|
|
14
|
+
# +::ActiveSupport+ is fully qualified to match the sibling packs' +::Rails+ discipline; unlike
|
|
15
|
+
# +Rails+ under +Briefly::Rails+, nothing here shadows the name, so it is style, not necessity.
|
|
16
|
+
module Instrument
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# @param builder [Briefly::Builder]
|
|
20
|
+
# @return [Briefly::Builder]
|
|
21
|
+
def install(builder)
|
|
22
|
+
builder.shortcut(:instrument) do |name, payload = {}, &blk|
|
|
23
|
+
::ActiveSupport::Notifications.instrument(name, payload, &blk)
|
|
24
|
+
end
|
|
25
|
+
builder
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/briefly/rails/reload.rb
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Briefly
|
|
4
|
+
module Rails
|
|
5
|
+
# Helpers, routes and rendering outside a controller.
|
|
6
|
+
#
|
|
7
|
+
# App = Briefly.define { use Briefly::Rails::View }
|
|
8
|
+
# App.h # => ApplicationController.helpers
|
|
9
|
+
# App.render(...) # => ApplicationController.renderer.render(...)
|
|
10
|
+
#
|
|
11
|
+
# Inside this file the framework is always +::Rails+ / +::ApplicationController+ — the bare names
|
|
12
|
+
# would resolve to the parent module.
|
|
13
|
+
module View
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# @param builder [Briefly::Builder]
|
|
17
|
+
# @return [Briefly::Builder]
|
|
18
|
+
def install(builder)
|
|
19
|
+
builder.shortcut(:helpers, :h) { ::ApplicationController.helpers }
|
|
20
|
+
builder.shortcut(:routes, :r) { ::Rails.application.routes.url_helpers }
|
|
21
|
+
builder.shortcut(:renderer) { ::ApplicationController.renderer }
|
|
22
|
+
builder.shortcut(:render) { |*args, **kwargs, &blk| renderer.render(*args, **kwargs, &blk) }
|
|
23
|
+
builder
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/briefly/rails.rb
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "briefly"
|
|
4
|
+
require "briefly/rails/config"
|
|
5
|
+
require "briefly/rails/env"
|
|
6
|
+
require "briefly/rails/view"
|
|
4
7
|
require "briefly/rails/db"
|
|
8
|
+
require "briefly/rails/instrument"
|
|
5
9
|
require "briefly/rails/reload"
|
|
6
10
|
|
|
7
11
|
module Briefly
|
|
@@ -12,8 +16,8 @@ module Briefly
|
|
|
12
16
|
# App.render(...) # => ApplicationController.renderer.render(...)
|
|
13
17
|
# App.db.txn { } # => ApplicationRecord.transaction { }
|
|
14
18
|
#
|
|
15
|
-
# An umbrella over {Config}, {Env}, {View} and {Reload}, plus {DB} under the +db+
|
|
16
|
-
# a pack in its own right, so a facade can take only the parts it wants:
|
|
19
|
+
# An umbrella over {Config}, {Env}, {View}, {Instrument} and {Reload}, plus {DB} under the +db+
|
|
20
|
+
# namespace. Each is a pack in its own right, so a facade can take only the parts it wants:
|
|
17
21
|
#
|
|
18
22
|
# Admin = Briefly.define do
|
|
19
23
|
# use "rails/env"
|
|
@@ -36,56 +40,9 @@ module Briefly
|
|
|
36
40
|
builder.use(Config)
|
|
37
41
|
builder.use(Env)
|
|
38
42
|
builder.use(View)
|
|
43
|
+
builder.use(Instrument)
|
|
39
44
|
builder.namespace(:db) { use DB }
|
|
40
45
|
builder
|
|
41
46
|
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
47
|
end
|
|
91
48
|
end
|