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.
@@ -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::Definition}] copies of the facade's current definitions
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
- @errors = []
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+, +memoize+, +use+, +rescue_from+, and further namespaces.
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. Redeclaring a name (canonical or alias) silently overrides it.
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 [Symbol] the canonical name
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
- raise ArgumentError, "shortcut(#{canonical.inspect}) requires a block" unless body
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
- # 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)
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 an error handler. The handler's return value becomes the shortcut's return value.
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, :name do |e| ... end+ or +rescue_from(Err, :name) { |e| ... }+.
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>] shortcuts to scope to; none means facade-wide
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
- 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) { }"
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
- scoped = names.flatten.map { |name| fetch(name).canonical }
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 [Array] +[definitions, error_entries, children, child_plans]+
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
- [@defs, @errors, @children, @pending.map { |name, builder| [@children[name], builder.compile!] }]
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 definition that currently owns them, so a redeclaration
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) }
@@ -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 +memoize+ or +rescue_from+ names a shortcut that does not exist.
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.
@@ -14,29 +14,75 @@ module Briefly
14
14
  @__aliases = {}
15
15
  @__memos = {}.freeze
16
16
  @__monitor = Monitor.new
17
- @__errors = ErrorRegistry.new
18
- @__public = [].freeze
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 shortcuts = @__defs.keys.sort
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 shortcut?(name) = @__defs.key?(name) || @__aliases.key?(name)
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 clear_memos!
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(&:clear_memos!)
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 configure(&block)
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 [Array] +compile!+'s +[defs, error_entries, children, child_plans]+
111
+ # @param plan [Briefly::Builder::Plan] a validated +compile!+ pass
72
112
  # @return [self]
73
113
  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)
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 error registrations.
81
- def __install(defs, error_entries)
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(@__public - wanted)
126
+ __remove_methods(@__installed - wanted)
84
127
  defs.each_value { |defn| __define(defn) }
85
128
 
86
- @__public = wanted.freeze
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
- error_entries.each { |klass, names, handler| @__errors.add(klass, names, handler) }
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, name)
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
- 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) }
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 entry
197
+ Kernel.raise(error) unless handler
150
198
 
151
- entry.handler.call(error, name)
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
@@ -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.query("select * from users where id = ?", 1) }
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: 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.
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
- builder.shortcut(:connection, :conn) { model.call.lease_connection }
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 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
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
- builder.shortcut(:query) do |sql, *binds|
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.exec_query(statement) }
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
@@ -31,7 +31,7 @@ module Briefly
31
31
  end
32
32
 
33
33
  facade.instance_variable_set(INSTALLED, true)
34
- app.reloader.to_prepare { facade.clear_memos! }
34
+ app.reloader.to_prepare { facade.briefly.clear_memos! }
35
35
  builder
36
36
  end
37
37
  end
@@ -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+ namespace. Each is
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