Module: Rooibos::Router::ClassMethods
- Defined in:
- lib/rooibos/router.rb
Overview
The Router declaration surface.
Fragments grow. One Update handles dozens of cases. Routing logic, keybindings, and guard conditions tangle together.
These class methods decompose that logic into declarative rules. Declare routes, forwards, receives, observes, and otherwises. Call from_router to freeze them into an Update callable.
Use it inside any module that includes Rooibos::Router.
Instance Method Summary collapse
-
#action(name = nil, handler = nil, **kwargs) ⇒ Object
Defines a named action referenceable by symbol.
-
#forward(predicate, to: @_scoped_target) ⇒ Object
Routes messages matching a custom predicate to a declared route.
-
#forward_all(to: @_scoped_target, **guard_opts) ⇒ Object
Routes any message to a declared route.
-
#forward_events(keys, to: @_scoped_target) ⇒ Object
Routes matching key events to a declared route.
-
#forward_instances_of(klass, to: @_scoped_target) ⇒ Object
Forwards all instances of a class to routes.
-
#forward_routed(envelopes, to: @_scoped_target) ⇒ Object
Routes matching routed messages to a declared route.
-
#from_router ⇒ Object
Assembles all declared routes, forwards, receives, observes, and otherwises into a frozen RouterUpdate callable.
-
#observe ⇒ Object
Observes messages matching a custom predicate.
-
#observe_all ⇒ Object
Observes any message.
-
#observe_events ⇒ Object
Observes matching key events.
-
#observe_instances_of ⇒ Object
Observes matching class instances.
-
#observe_routed ⇒ Object
Observes matching routed messages.
-
#otherwise ⇒ Object
Catches unhandled messages as a router-level fallback.
-
#receive ⇒ Object
(also: #intercept)
Handles messages matching a custom predicate.
-
#receive_all ⇒ Object
(also: #intercept_all)
Handles any message.
-
#receive_events ⇒ Object
(also: #intercept_events)
Handles matching key events directly.
-
#receive_instances_of ⇒ Object
(also: #intercept_instances_of)
Handles matching class instances.
-
#receive_routed ⇒ Object
(also: #intercept_routed)
Handles matching routed messages.
-
#route(prefix = nil, to:, read: nil, write: nil) ⇒ Object
Declares a child route binding a nested fragment to a model slice.
Instance Method Details
#action(name = nil, handler = nil, **kwargs) ⇒ Object
Defines a named action referenceable by symbol.
Actions are reusable handlers. Reference them by name in receive*, intercept*, and observe* methods anywhere a handler lambda is accepted.
Lambda actions run directly. Routed actions dispatch a Message::Routed to a fragment, using the action name as the envelope.
- name
-
Symbol identifying the action.
- handler
-
A lambda or a fragment Module for routed dispatch.
Example
# Lambda action
action :quit, -> { Rooibos::Command.exit }
# Keyword form
action scroll_up: ->(_, model) { model.with(offset: model.offset - 1) }
# Routed action (dispatches :go_back to HistoryPanel)
action :go_back, HistoryPanel
191 192 193 194 195 196 197 198 199 |
# File 'lib/rooibos/router.rb', line 191 def action(name = nil, handler = nil, **kwargs) if name && handler actions.add(name, handler) elsif kwargs.any? kwargs.each { |k, v| actions.add(k, v) } else raise ArgumentError, "action requires name and handler, or keyword arguments" end end |
#forward(predicate, to: @_scoped_target) ⇒ Object
Routes messages matching a custom predicate to a declared route.
The predicate lambda receives (message, model). If it returns a truthy value, the message is forwarded. Use this for complex matching logic that the specialized variants cannot express.
Example
forward ->(msg, _) { msg.key? && msg.ctrl? }, to: :editor
forward ->(msg, _) { msg.key? && msg.shift? }, to: Sidebar
374 375 376 |
# File 'lib/rooibos/router.rb', line 374 def forward(predicate, to: @_scoped_target, **) forwards.add_custom(predicate, to:, **) end |
#forward_all(to: @_scoped_target, **guard_opts) ⇒ Object
Routes any message to a declared route.
Matches every message. Combine with guards to conditionally route unhandled messages. Without guards, acts as a catch-all forward.
Example
only when: -> (_, model) { model.active_tab == :counter_tab } do
forward_all to: :counter_tab
end
forward_all to: :active_panel
360 361 362 |
# File 'lib/rooibos/router.rb', line 360 def forward_all(to: @_scoped_target, **guard_opts) forwards.add_custom(Predicate::Always.new, to:, **guard_opts) end |
#forward_events(keys, to: @_scoped_target) ⇒ Object
Routes matching key events to a declared route.
Matches raw RatatuiRuby events by their to_sym value. Pass a symbol for a single event or an array for multiple events that route to the same destination.
The to: parameter accepts a symbol (model attribute), a module (fragment), or a Route (return value of route).
Use as: to wrap the event in a Message::Routed with a semantic envelope. This decouples keybindings from nested fragment internals.
Use broadcast: true to send to all declared routes, or broadcast_to: with an array of specific route targets.
Example
forward_events :enter, to: :active_form, as: :submit
forward_events [:up, :k], to: :list, as: :move_up
308 309 310 |
# File 'lib/rooibos/router.rb', line 308 def forward_events(keys, to: @_scoped_target, **) forwards.add_events(keys, to:, **) end |
#forward_instances_of(klass, to: @_scoped_target) ⇒ Object
Forwards all instances of a class to routes.
Matches messages by class. Ideal for custom message types or RatatuiRuby event classes like Event::Resize.
Use broadcast: true to send to all declared routes, or broadcast_to: with an array of specific route targets.
Example
forward_instances_of RatatuiRuby::Event::Resize, to: :main_layout
forward_instances_of ThemeChanged, broadcast: true
324 325 326 |
# File 'lib/rooibos/router.rb', line 324 def forward_instances_of(klass, to: @_scoped_target, **) forwards.add_instances_of(klass, to:, **) end |
#forward_routed(envelopes, to: @_scoped_target) ⇒ Object
Routes matching routed messages to a declared route.
Matches Message::Routed messages by envelope. Use this when an outer fragment has already routed an event and you need to route it further to a nested fragment.
Use as: to transform the envelope before forwarding. Each layer speaks its inner fragment’s API without knowing what lies deeper.
Use broadcast: true to send to all declared routes, or broadcast_to: with an array of specific route targets.
Example
forward_routed :leaf_1, to: :top_leaf, as: :increment
forward_routed :leaf_2, to: :bottom_leaf, as: :increment
345 346 347 |
# File 'lib/rooibos/router.rb', line 345 def forward_routed(envelopes, to: @_scoped_target, **) forwards.add_routed(envelopes, to:, **) end |
#from_router ⇒ Object
Assembles all declared routes, forwards, receives, observes, and otherwises into a frozen RouterUpdate callable.
Call this once at the end of your Router declarations. Assign the result to Update so the runtime dispatches messages through your router.
Raises Rooibos::Error::Invariant if any forward or otherwise target is ambiguous (e.g. two routes share the same prefix or fragment).
Example
module MyFragment
include Rooibos::Router
route :child, to: ChildFragment
forward_events :enter, to: :child, as: :submit
Update = from_router
end
123 124 125 126 127 128 |
# File 'lib/rooibos/router.rb', line 123 def from_router RouterUpdate.new( inward: Flow::Inward.new(observes:, receives:, forwards:, otherwises:, routes:), outward: Flow::Outward.new(observes:, receives:, forwards:, routes:) ) end |
#observe ⇒ Object
Observes messages matching a custom predicate. Does not stop further processing.
The predicate lambda receives (message, model). All matching observers run. The message continues to later handlers.
Example
observe ->(msg, _) { msg.leaf_reset? || msg.panel_reset? },
->(_, model) { model.with(total_resets: model.total_resets + 1) }
443 444 445 |
# File 'lib/rooibos/router.rb', line 443 def observe(...) observes.add_custom(...) end |
#observe_all ⇒ Object
Observes any message. Does not stop further processing.
Matches every message. Useful for metrics, debugging, or global state updates that apply regardless of message type.
Example
observe_all ->(msg, model) {
model.with(message_count: model. + 1)
}
429 430 431 |
# File 'lib/rooibos/router.rb', line 429 def observe_all(...) observes.add_all(...) end |
#observe_events ⇒ Object
Observes matching key events. Does not stop further processing.
Matches raw RatatuiRuby events by to_sym. All matching observers run in declaration order. The message continues to later handlers. Use observe for side effects that should not block other handlers: logging, counting, updating derived state.
Example
observe_events :enter,
->(_, model) { [model, Rooibos::Command.custom(Logger.log("Enter pressed"))] }
389 390 391 |
# File 'lib/rooibos/router.rb', line 389 def observe_events(...) observes.add_events(...) end |
#observe_instances_of ⇒ Object
Observes matching class instances. Does not stop further processing.
Matches messages by class. Use it to react to custom message types while allowing them to continue to other handlers.
Example
observe_instances_of LeafReset,
->(_, model) { model.with(nested_resets: model.nested_resets + 1) }
415 416 417 |
# File 'lib/rooibos/router.rb', line 415 def observe_instances_of(...) observes.add_instances_of(...) end |
#observe_routed ⇒ Object
Observes matching routed messages. Does not stop further processing.
Matches Message::Routed by envelope. The message continues to later handlers after this observer runs.
Example
observe_routed :submit,
->(_, model) { model.with(submissions: model.submissions + 1) }
402 403 404 |
# File 'lib/rooibos/router.rb', line 402 def observe_routed(...) observes.add_routed(...) end |
#otherwise ⇒ Object
Catches unhandled messages as a router-level fallback.
Messages not handled by receive, intercept, or forward fall through to otherwise. The route_to: parameter accepts the same three forms as to: in the forward family. Multiple otherwise declarations with guards create a conditional fallthrough chain.
This keeps outer fragments minimal. Declare what you handle; everything else flows to the nested fragment.
Example
otherwise route_to: :counter_tab,
when: ->(_, model) { model.active_tab == :counter }
otherwise route_to: :color_tab,
when: ->(_, model) { model.active_tab == :color }
otherwise route_to: :dashboard
465 466 467 |
# File 'lib/rooibos/router.rb', line 465 def otherwise(...) otherwises.add(...) end |
#receive ⇒ Object Also known as: intercept
Handles messages matching a custom predicate. Stops further processing.
The predicate lambda receives (message, model). If it returns a truthy value, the handler runs and no later handlers execute.
intercept is an alias.
Example
receive ->(msg, _) { msg.key? && msg.text? },
->(msg, model) { model.with(buffer: model.buffer + msg.char) }
278 279 280 |
# File 'lib/rooibos/router.rb', line 278 def receive(...) receives.add_custom(...) end |
#receive_all ⇒ Object Also known as: intercept_all
Handles any message. Stops further processing.
Matches every message. Combine with guards to create conditional catch-alls. For example, block all input when a fragment is inactive.
intercept_all is an alias.
Example
receive_all ->(msg, model) { [model, nil] },
unless: ->(_, model) { model.active }
263 264 265 |
# File 'lib/rooibos/router.rb', line 263 def receive_all(...) receives.add_all(...) end |
#receive_events ⇒ Object Also known as: intercept_events
Handles matching key events directly. Stops further processing.
Matches raw RatatuiRuby events by their to_sym value. The second argument is an action name (Symbol) or a handler lambda. The first matching receive wins; later handlers do not run.
intercept_events is an alias. Use receive when the message is addressed to you. Use intercept when stopping a bubbled message mid-chain.
Example
receive_events :ctrl_c, :quit
receive_events :q, :quit
receive_events :enter, ->(_, model) { model.with(submitted: true) }
216 217 218 |
# File 'lib/rooibos/router.rb', line 216 def receive_events(...) receives.add_events(...) end |
#receive_instances_of ⇒ Object Also known as: intercept_instances_of
Handles matching class instances. Stops further processing.
Matches messages by class. Use receive for messages addressed to you. Use intercept to stop a bubbled message mid-chain.
intercept_instances_of is an alias.
Example
receive_instances_of FatalError,
->(msg, model) { [model.with(error: msg), Rooibos::Command.exit] }
248 249 250 |
# File 'lib/rooibos/router.rb', line 248 def receive_instances_of(...) receives.add_instances_of(...) end |
#receive_routed ⇒ Object Also known as: intercept_routed
Handles matching routed messages. Stops further processing.
Matches Message::Routed messages by their envelope symbol. Use this when an outer fragment has forwarded a message with as: and your fragment handles it.
intercept_routed is an alias.
Example
receive_routed :panel_self,
->(_, model) { model.with(count: model.count + 1) }
232 233 234 |
# File 'lib/rooibos/router.rb', line 232 def receive_routed(...) receives.add_routed(...) end |
#route(prefix = nil, to:, read: nil, write: nil) ⇒ Object
Declares a child route binding a nested fragment to a model slice.
The simplest form names a model attribute. :sidebar means “read from model.sidebar, write back with model.with(sidebar: ...).”
When your model stores fragments in hashes or other structures, pass read: and write: lambdas for custom extraction and merging. A route with lambdas has no prefix symbol.
Returns the Route object. Capture it when neither the prefix symbol nor the fragment module can unambiguously identify the route.
- prefix
-
Symbol or String naming the model attribute. Optional when using
read:/write:. - to
-
The fragment module whose
Updatehandles messages. - read
-
Lambda
->(model) -> nested_model. Overrides prefix-based extraction. - write
-
Lambda
->(model, value) -> model. Overrides prefix-based merging.
Example
# Named attribute (most common)
route :sidebar, to: Sidebar
# Custom accessors for hash-stored fragments
route read: ->(model) { model.panels[:sidebar] },
write: ->(model, value) { model.with(panels: model.panels.merge(sidebar: value)) },
to: Sidebar
# Capture for disambiguation
ACTIVE = route read: ->(m) { m.tabs[m.active_tab] },
write: ->(m, v) { m.with(tabs: m.tabs.merge(m.active_tab => v)) },
to: TabContent
forward_events :enter, to: ACTIVE, as: :submit
164 165 166 |
# File 'lib/rooibos/router.rb', line 164 def route(prefix = nil, to:, read: nil, write: nil, **) routes.add(Route.new(prefix: prefix&.to_s&.to_sym, fragment: to, read:, write:)) end |