module Rooibos::Router::ClassMethods
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.
Public Instance Methods
Source
# File lib/rooibos/router.rb, line 207 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
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
Source
# File lib/rooibos/router.rb, line 368 def forward(predicate, to: @_scoped_target, **) forwards.add_custom(predicate, to:, **) end
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
Source
# File lib/rooibos/router.rb, line 354 def forward_all(to: @_scoped_target, **guard_opts) forwards.add_custom(Predicate::Always.new, to:, **guard_opts) end
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
Source
# File lib/rooibos/router.rb, line 321 def forward_events(keys, to: @_scoped_target, **) forwards.add_events(keys, to:, **) end
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.
Example
forward_events :enter, to: :active_form, as: :submit forward_events [:up, :k], to: :list, as: :move_up
Source
# File lib/rooibos/router.rb, line 180 def forward_instances_of(klass, ...) forwards.add_instances_of(klass, ...) end
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
Source
# File lib/rooibos/router.rb, line 339 def forward_routed(envelopes, to: @_scoped_target, **) forwards.add_routed(envelopes, to:, **) end
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.
Example
forward_routed :leaf_1, to: :top_leaf, as: :increment forward_routed :leaf_2, to: :bottom_leaf, as: :increment
Source
# 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:, routes:) ) end
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
Source
# File lib/rooibos/router.rb, line 437 def observe(...) observes.add_custom(...) end
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) }
Source
# File lib/rooibos/router.rb, line 423 def observe_all(...) observes.add_all(...) end
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.message_count + 1) }
Source
# File lib/rooibos/router.rb, line 383 def observe_events(...) observes.add_events(...) end
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"))] }
Source
# File lib/rooibos/router.rb, line 409 def observe_instances_of(...) observes.add_instances_of(...) end
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) }
Source
# File lib/rooibos/router.rb, line 396 def observe_routed(...) observes.add_routed(...) end
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) }
Source
# File lib/rooibos/router.rb, line 459 def otherwise(...) otherwises.add(...) end
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
Source
# File lib/rooibos/router.rb, line 294 def receive(...) receives.add_custom(...) end
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) }
Source
# File lib/rooibos/router.rb, line 279 def receive_all(...) receives.add_all(...) end
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 }
Source
# File lib/rooibos/router.rb, line 232 def receive_events(...) receives.add_events(...) end
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) }
Source
# File lib/rooibos/router.rb, line 264 def receive_instances_of(...) receives.add_instances_of(...) end
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] }
Source
# File lib/rooibos/router.rb, line 248 def receive_routed(...) receives.add_routed(...) end
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) }
Source
# 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
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