Class: Axn::Webhooks::Inbound::DSL
- Inherits:
-
Object
- Object
- Axn::Webhooks::Inbound::DSL
- Defined in:
- lib/axn/webhooks/inbound/dsl.rb
Overview
Receiver for an inbound block: captures declarations (Phase 2: verify) and
exposes request resolvers. Later phases add dispatch/challenge/respond here.
Constant Summary collapse
- INHERITED_IVARS =
Every declaration a child
endpointinherits. Deliberately excludes @child_endpoints (nesting is one level deep) and @nested.Also excludes @dispatch_spec, which cannot be inherited because a parent declaring
dispatchalongsideendpointblocks is rejected at registration (see Axn::Webhooks.inbound). Listing it would be inert, and it would advertise a shared-dispatch feature that does not work anyway:dispatchcaptures ONE spec hash, so a child re-declaring it replaces the parent's wholesale — there is no partial override, and a parent dispatch every child copies verbatim leaves the children differing by nothing (Codex review asked for this; declined for that reason plus the migration hazard the guard exists to catch — silently losing Inbound out from under a mounted route). %i[ @verify_spec @unauthorized_headers @challenge_required @respond_block @static_respond_block @challenge_spec ].freeze
Instance Method Summary collapse
-
#__challenge__ ⇒ Object
Internal: the captured { resolver:, guard: } challenge declaration, or nil if none.
-
#__challenge_required__ ⇒ Object
Internal: the declared challenge-required predicate, or nil to ask the verifier.
-
#__child_dsl__(block) ⇒ Object
Internal: a fresh DSL seeded with this one's captured declarations, with
blockevaluated against it — so a child inherits everything and overrides by re-declaring. -
#__children__ ⇒ Object
Internal: declared child endpoints, name => block.
-
#__dispatch__ ⇒ Object
Internal: build the { router:, parse:, mode: } dispatch config, or nil if none declared.
-
#__dispatch_declared? ⇒ Boolean
Internal: whether
dispatchwas declared directly on THIS DSL. -
#__respond__ ⇒ Object
Internal: the captured respond block, or nil if none declared.
-
#__static_respond__ ⇒ Object
Internal: the captured static_respond block, or nil if none declared.
-
#__unauthorized_headers__ ⇒ Object
Internal: the declared 401 headers, or nil to let the verifier speak for itself.
-
#__verifier__ ⇒ Object
Internal: build the verifier callable from the captured declaration.
-
#async(call, **opts) ⇒ Object
Dispatch-map sugar:
async("H")=={ call: "H", async: true };syncforces sync. - #challenge(resolver, if: nil) ⇒ Object
-
#challenge_required(&block) ⇒ Object
challenge_required { |req| req.header("Authorization").to_s.empty? }.
-
#claim_ownership(ivar) ⇒ Object
Marks an ivar as belonging to THIS block from here on.
-
#discard_inherited(ivar) ⇒ Object
Drops an ivar this DSL INHERITED from a parent
endpointcontainer, leaving one declared in this very block untouched. -
#dispatch(to: nil, on: nil, otherwise: nil, via: nil, parse: :json, mode: :auto, unparseable_status: nil) ⇒ Object
dispatch to: "Handler" | dispatch on: ->(e)…, to: map, otherwise:, via: | parse: | mode:
unparseable_status:overrides Axn::Webhooks.config.unparseable_status for THIS endpoint — it belongs here, next toparse:, because it only describes what happens when that parse fails, and because the right value is a fact about one vendor's retry policy (PRO-3143). -
#endpoint(name, &block) ⇒ Object
endpoint(:events) { dispatch … } — declares a CHILD endpoint that inherits everything the parent block declared and may override any of it by re-declaring.
- #header(name) ⇒ Object
- #params ⇒ Object
- #raw_body ⇒ Object
-
#respond(&block) ⇒ Object
respond { |handler_result| text("...") } — maps a genuine handler success to a Response.
-
#static_respond(&block) ⇒ Object
static_respond { text("...") } — a body that does NOT read the handler result (block takes zero args, unlike respond's
|handler_result|), so it renders on every non-error outcome: sync success, async enqueue, otherwise: :ack, and business fail! — see Endpoint#default_ack. - #sync(call, **opts) ⇒ Object
-
#unauthorized_headers(headers) ⇒ Object
unauthorized_headers "WWW-Authenticate" => %(Basic realm="Webhook").
- #url ⇒ Object
-
#verify(strategy = nil, **opts, &block) ⇒ Object
verify :hmac, **opts | verify :standard_webhooks, **opts | verify { |req| ... }.
Instance Method Details
#__challenge__ ⇒ Object
Internal: the captured { resolver:, guard: } challenge declaration, or nil if none.
230 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 230 def __challenge__ = @challenge_spec |
#__challenge_required__ ⇒ Object
Internal: the declared challenge-required predicate, or nil to ask the verifier.
236 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 236 def __challenge_required__ = @challenge_required |
#__child_dsl__(block) ⇒ Object
Internal: a fresh DSL seeded with this one's captured declarations, with block evaluated
against it — so a child inherits everything and overrides by re-declaring.
Copies the ivars rather than re-instance_execing the parent block per child (the obvious
alternative): replaying the parent block would re-run any side effects in it, and would
re-enter endpoint recursively, registering each child once per sibling.
166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 166 def __child_dsl__(block) child = self.class.new inherited = INHERITED_IVARS.select { |ivar| instance_variable_defined?(ivar) } inherited.each { |ivar| child.instance_variable_set(ivar, instance_variable_get(ivar)) } # Recorded so `respond`/`static_respond` can tell an inherited value from one declared in # the child's own block, and clear only the former. child.instance_variable_set(:@inherited_ivars, inherited.dup) child.instance_variable_set(:@nested, true) child.instance_exec(&block) child end |
#__children__ ⇒ Object
Internal: declared child endpoints, name => block. Empty for a plain inbound block.
154 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 154 def __children__ = @child_endpoints || {} |
#__dispatch__ ⇒ Object
Internal: build the { router:, parse:, mode: } dispatch config, or nil if none declared.
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 205 def __dispatch__ return nil unless @dispatch_spec spec = @dispatch_spec unless %i[auto sync async].include?(spec[:mode]) raise Axn::Webhooks::Error, "dispatch mode: must be :sync, :async, or :auto (got #{spec[:mode].inspect})" end unless spec[:unparseable_status].nil? || Response.valid_status?(spec[:unparseable_status]) raise Axn::Webhooks::Error, "dispatch unparseable_status: must be an Integer HTTP status between 200 and 599 " \ "(got #{spec[:unparseable_status].inspect})" end router = Router.new(to: spec[:to], on: spec[:on], otherwise: spec[:otherwise], via: spec[:via]) { router:, parse: Parsers.build(spec[:parse]), mode: spec[:mode], unparseable_status: spec[:unparseable_status] } end |
#__dispatch_declared? ⇒ Boolean
Internal: whether dispatch was declared directly on THIS DSL. Read instead of
__dispatch__ so the parent-with-children check doesn't build a Router just to ask.
158 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 158 def __dispatch_declared? = !@dispatch_spec.nil? |
#__respond__ ⇒ Object
Internal: the captured respond block, or nil if none declared.
224 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 224 def __respond__ = @respond_block |
#__static_respond__ ⇒ Object
Internal: the captured static_respond block, or nil if none declared.
227 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 227 def __static_respond__ = @static_respond_block |
#__unauthorized_headers__ ⇒ Object
Internal: the declared 401 headers, or nil to let the verifier speak for itself.
233 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 233 def = @unauthorized_headers |
#__verifier__ ⇒ Object
Internal: build the verifier callable from the captured declaration.
For challenge-only endpoints (no dispatch, no verify declared), return a no-op verifier
that always succeeds — a challenge-only endpoint just handshakes the GET and 200-acks any
POST, so there's no unverified processing to guard against.
verify is REQUIRED whenever dispatch is declared — dispatching an unverified webhook
would run the handler on an unauthenticated request.
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 184 def __verifier__ unless @verify_spec # Nothing declared at all: bare endpoint, always an error. raise Axn::Webhooks::Error, "inbound endpoint declared no `verify`" if @dispatch_spec.nil? && @challenge_spec.nil? # `dispatch` without `verify` is unsafe regardless of whether `challenge` is also present. if @dispatch_spec raise Axn::Webhooks::Error, "inbound endpoint with `dispatch` must declare `verify` — dispatching an unverified webhook is unsafe" end # Challenge-only endpoint (no dispatch): return a no-op verifier. return ->(_request) { true } end raise Axn::Webhooks::Error, "inbound endpoint `verify` needs a strategy or a block" if @verify_spec[:strategy].nil? && @verify_spec[:block].nil? Verifiers.build(**@verify_spec) end |
#async(call, **opts) ⇒ Object
Dispatch-map sugar: async("H") == { call: "H", async: true }; sync forces sync.
Callable inside a dispatch to: { … } map because the inbound block is instance_exec'd
against this DSL. Extra kwargs (e.g. with:) pass through: async("H", with: ->(e){ … }).
**opts is spread FIRST so the fixed mode and the positional handler always win — a
splatted shared options hash carrying :async/:call can never silently flip the mode
or retarget the handler (the helper's name is its contract).
121 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 121 def async(call, **opts) = { **opts, call:, async: true } |
#challenge(resolver, if: nil) ⇒ Object
103 104 105 106 107 108 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 103 def challenge(resolver, if: nil) # `if:` shadows Ruby's `if` keyword inside this method body — must read it back via # binding.local_variable_get, not a bare `if` reference (that's a syntax trap, not a var). guard = binding.local_variable_get(:if) @challenge_spec = { resolver:, guard: } end |
#challenge_required(&block) ⇒ Object
challenge_required { |req| req.header("Authorization").to_s.empty? }
Declares which requests are not authentication attempts at all, and so get the challenge
(401 + unauthorized_headers) instead of being run through verify and recorded as
verify failures. verify :basic_auth answers this itself; declare it only for a custom
verify block that wraps a two-legged scheme the gem can't see through — the shape
buyout's Twilio routes use, where the BasicAuth verifier sits inside a block:
challenge_required { |req| my_basic_auth.challenge_required?(req) }
48 49 50 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 48 def challenge_required(&block) @challenge_required = block end |
#claim_ownership(ivar) ⇒ Object
Marks an ivar as belonging to THIS block from here on. Without it, a child that
re-declared respond left @respond_block still listed as inherited, so a following
static_respond discarded the child's OWN block and the pair was accepted — exactly the
same-block conflict the discard rule is supposed to keep raising (Codex review).
151 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 151 def claim_ownership(ivar) = @inherited_ivars&.delete(ivar) |
#discard_inherited(ivar) ⇒ Object
Drops an ivar this DSL INHERITED from a parent endpoint container, leaving one declared
in this very block untouched. Backs the mutually-exclusive renderer override above.
140 141 142 143 144 145 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 140 def discard_inherited(ivar) return unless @inherited_ivars&.include?(ivar) instance_variable_set(ivar, nil) @inherited_ivars.delete(ivar) end |
#dispatch(to: nil, on: nil, otherwise: nil, via: nil, parse: :json, mode: :auto, unparseable_status: nil) ⇒ Object
dispatch to: "Handler" | dispatch on: ->(e)…, to: map, otherwise:, via: | parse: | mode:
unparseable_status: overrides Axn::Webhooks.config.unparseable_status for THIS endpoint —
it belongs here, next to parse:, because it only describes what happens when that parse
fails, and because the right value is a fact about one vendor's retry policy (PRO-3143).
rubocop:disable-next Naming/MethodParameterName
57 58 59 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 57 def dispatch(to: nil, on: nil, otherwise: nil, via: nil, parse: :json, mode: :auto, unparseable_status: nil) @dispatch_spec = { to:, on:, otherwise:, via:, parse:, mode:, unparseable_status: } end |
#endpoint(name, &block) ⇒ Object
128 129 130 131 132 133 134 135 136 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 128 def endpoint(name, &block) raise ArgumentError, "`endpoint #{name.inspect}` requires a block" unless block raise ArgumentError, "`endpoint #{name.inspect}` cannot be nested inside another `endpoint` — one level only" if @nested @child_endpoints ||= {} raise ArgumentError, "duplicate `endpoint #{name.inspect}` in the same inbound block" if @child_endpoints.key?(name.to_sym) @child_endpoints[name.to_sym] = block end |
#header(name) ⇒ Object
110 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 110 def header(name) = Resolvers.header(name) |
#params ⇒ Object
112 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 112 def params = Resolvers.params |
#raw_body ⇒ Object
111 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 111 def raw_body = Resolvers.raw_body |
#respond(&block) ⇒ Object
respond { |handler_result| text("...") } — maps a genuine handler success to a
Response. Every other outcome (ack, business fail!, verify failure/exception, or a
no-dispatch endpoint) always gets the default bare ack (or a declared static_respond
body — see below), regardless of this declaration — see Endpoint#to_response.
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 65 def respond(&block) # Checked BEFORE the discard below: without a block this stored nil after clearing the # inherited alternative, booting an endpoint with no renderer at all — an undocumented way # to un-declare a parent's renderer, and a typo that silently downgraded responses to bare # acks (Codex review). raise Axn::Webhooks::Error, "inbound endpoint's `respond` requires a block" unless block # Endpoint rejects having both renderers set, and a child inherits BOTH ivars — so # overriding an inherited `static_respond` with a `respond` has to clear it, or the child # raises. Clears only an INHERITED one: declaring both in the same block stays an error # rather than silently becoming last-one-wins (Codex review). discard_inherited(:@static_respond_block) @respond_block = block claim_ownership(:@respond_block) end |
#static_respond(&block) ⇒ Object
static_respond { text("...") } — a body that does NOT read the handler result (block
takes zero args, unlike respond's |handler_result|), so it renders on every non-error
outcome: sync success, async enqueue, otherwise: :ack, and business fail! — see
Endpoint#default_ack. Mutually exclusive with respond (Endpoint#initialize raises if
both are declared) and never forces sync dispatch (Dispatch#async? never reads it).
86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 86 def static_respond(&block) raise Axn::Webhooks::Error, "inbound endpoint's `static_respond` requires a block" unless block if block.parameters.any? raise Axn::Webhooks::Error, "inbound endpoint's static_respond block must take no arguments (it never reads the " \ "handler's result, unlike respond) — got a parameter; use `respond` instead if you need " \ "to read the handler's result" end discard_inherited(:@respond_block) # see `respond` — cross-form override, inherited only @static_respond_block = block claim_ownership(:@static_respond_block) end |
#sync(call, **opts) ⇒ Object
122 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 122 def sync(call, **opts) = { **opts, call:, async: false } |
#unauthorized_headers(headers) ⇒ Object
unauthorized_headers "WWW-Authenticate" => %(Basic realm="Webhook")
Headers to attach to the 401 a verify failure produces. verify :basic_auth supplies
this itself; declare it only for a custom verify block that has to challenge a client
into retrying with credentials (see Endpoint#unauthorized_headers).
35 36 37 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 35 def (headers) @unauthorized_headers = headers end |
#verify(strategy = nil, **opts, &block) ⇒ Object
verify :hmac, **opts | verify :standard_webhooks, **opts | verify { |req| ... }
26 27 28 |
# File 'lib/axn/webhooks/inbound/dsl.rb', line 26 def verify(strategy = nil, **opts, &block) @verify_spec = { strategy:, opts:, block: } end |