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.
@@ -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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Briefly
4
+ # The gem version.
5
+ VERSION = "0.1.0"
6
+ end