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

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_routerObject

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

#observeObject

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_allObject

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)
}


429
430
431
# File 'lib/rooibos/router.rb', line 429

def observe_all(...)
  observes.add_all(...)
end

#observe_eventsObject

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_ofObject

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_routedObject

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

#otherwiseObject

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

#receiveObject 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_allObject 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_eventsObject 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_ofObject 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_routedObject 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 Update handles 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