rooibos 0.6.2 → 0.7.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.
- checksums.yaml +4 -4
- data/LICENSES/BSD-2-Clause.txt +9 -0
- data/REUSE.toml +5 -0
- data/exe/.gitkeep +0 -0
- data/lib/rooibos/cli/commands/new.rb +24 -0
- data/lib/rooibos/command/batch.rb +10 -0
- data/lib/rooibos/command/bubble.rb +34 -0
- data/lib/rooibos/command/custom.rb +3 -2
- data/lib/rooibos/command/deliver.rb +50 -0
- data/lib/rooibos/command/http.rb +1 -1
- data/lib/rooibos/command/lifecycle.rb +3 -1
- data/lib/rooibos/command/outlet.rb +19 -9
- data/lib/rooibos/command.rb +107 -3
- data/lib/rooibos/configuration.rb +29 -0
- data/lib/rooibos/message/bubbled.rb +29 -0
- data/lib/rooibos/message.rb +24 -6
- data/lib/rooibos/router/action.rb +36 -0
- data/lib/rooibos/router/flow/dispatch.rb +39 -0
- data/lib/rooibos/router/flow/inward.rb +41 -0
- data/lib/rooibos/router/flow/outward.rb +44 -0
- data/lib/rooibos/router/guard.rb +56 -0
- data/lib/rooibos/router/predicate.rb +65 -0
- data/lib/rooibos/router/registry/actions.rb +41 -0
- data/lib/rooibos/router/registry/forwards.rb +58 -0
- data/lib/rooibos/router/registry/observes.rb +57 -0
- data/lib/rooibos/router/registry/otherwises.rb +29 -0
- data/lib/rooibos/router/registry/receives.rb +57 -0
- data/lib/rooibos/router/registry/routes.rb +59 -0
- data/lib/rooibos/router/registry.rb +26 -0
- data/lib/rooibos/router/route.rb +42 -0
- data/lib/rooibos/router/router_update.rb +53 -0
- data/lib/rooibos/router/rule/forward.rb +39 -0
- data/lib/rooibos/router/rule/observe.rb +22 -0
- data/lib/rooibos/router/rule/otherwise.rb +26 -0
- data/lib/rooibos/router/rule/receive.rb +22 -0
- data/lib/rooibos/router/rule.rb +40 -0
- data/lib/rooibos/router.rb +424 -438
- data/lib/rooibos/runtime.rb +37 -52
- data/lib/rooibos/test_helper.rb +22 -0
- data/lib/rooibos/transition.rb +92 -0
- data/lib/rooibos/version.rb +1 -1
- data/lib/rooibos.rb +2 -57
- data/sig/rooibos/cli.rbs +1 -0
- data/sig/rooibos/command.rbs +44 -0
- data/sig/rooibos/configuration.rbs +20 -0
- data/sig/rooibos/message.rbs +12 -0
- data/sig/rooibos/router/action.rbs +33 -0
- data/sig/rooibos/router/actions.rbs +27 -0
- data/sig/rooibos/router/flow/dispatch.rbs +29 -0
- data/sig/rooibos/router/flow/inward.rbs +37 -0
- data/sig/rooibos/router/flow/outward.rbs +36 -0
- data/sig/rooibos/router/forward.rbs +35 -0
- data/sig/rooibos/router/forwards.rbs +34 -0
- data/sig/rooibos/router/guard.rbs +21 -0
- data/sig/rooibos/router/observe.rbs +20 -0
- data/sig/rooibos/router/observes.rbs +38 -0
- data/sig/rooibos/router/otherwise.rbs +22 -0
- data/sig/rooibos/router/otherwises.rbs +20 -0
- data/sig/rooibos/router/predicate.rbs +51 -0
- data/sig/rooibos/router/receive.rbs +20 -0
- data/sig/rooibos/router/receives.rbs +38 -0
- data/sig/rooibos/router/registry.rbs +24 -0
- data/sig/rooibos/router/route.rbs +46 -0
- data/sig/rooibos/router/router_update.rbs +33 -0
- data/sig/rooibos/router/routes.rbs +41 -0
- data/sig/rooibos/router/rule.rbs +36 -0
- data/sig/rooibos/router.rbs +216 -161
- data/sig/rooibos/runtime.rbs +0 -1
- data/sig/rooibos/test_helper.rbs +6 -0
- data/sig/rooibos/transition.rbs +33 -0
- data/sig/rooibos.rbs +0 -10
- metadata +144 -198
- data/.builds/ruby-3.2.yml +0 -55
- data/.builds/ruby-3.3.yml +0 -55
- data/.builds/ruby-3.4.yml +0 -55
- data/.builds/ruby-4.0.0.yml +0 -55
- data/.pre-commit-config.yaml +0 -16
- data/.rubocop.yml +0 -8
- data/AGENTS.md +0 -108
- data/CHANGELOG.md +0 -308
- data/README.md +0 -183
- data/README.rdoc +0 -374
- data/Rakefile +0 -16
- data/Steepfile +0 -13
- data/doc/best_practices/forms_and_validation.md +0 -20
- data/doc/best_practices/http_workflows.md +0 -20
- data/doc/best_practices/index.md +0 -26
- data/doc/best_practices/lists_and_tables.md +0 -20
- data/doc/best_practices/modal_dialogs.md +0 -20
- data/doc/best_practices/no_stateful_widgets.md +0 -184
- data/doc/best_practices/orchestration.md +0 -20
- data/doc/best_practices/streaming_data.md +0 -20
- data/doc/contributors/design/commands_and_outlets.md +0 -214
- data/doc/contributors/design/mvu_tea_implementations_research.md +0 -373
- data/doc/contributors/documentation_plan.md +0 -616
- data/doc/contributors/documentation_stub_audit.md +0 -112
- data/doc/contributors/documentation_style.md +0 -275
- data/doc/contributors/e2e_pty.md +0 -168
- data/doc/contributors/maybe_stateful_router.md +0 -56
- data/doc/contributors/specs/earliest_tutorial_steps_per_story.md +0 -70
- data/doc/contributors/specs/file_browser.md +0 -789
- data/doc/contributors/specs/file_browser_stories.md +0 -784
- data/doc/contributors/specs/tutorials_to_stories.rb +0 -167
- data/doc/contributors/todo/scrollbar.md +0 -118
- data/doc/contributors/tutorial_old/01_project_setup.md +0 -20
- data/doc/contributors/tutorial_old/02_hello_world.md +0 -24
- data/doc/contributors/tutorial_old/03_adding_state.md +0 -26
- data/doc/contributors/tutorial_old/06_organizing_your_code.md +0 -20
- data/doc/contributors/tutorial_old/07_your_first_command.md +0 -21
- data/doc/contributors/tutorial_old/08_the_preview_pane.md +0 -20
- data/doc/contributors/tutorial_old/09_loading_states.md +0 -20
- data/doc/contributors/tutorial_old/10_testing_your_app.md +0 -20
- data/doc/contributors/tutorial_old/11_polish_and_refine.md +0 -20
- data/doc/contributors/tutorial_old/12_going_further.md +0 -20
- data/doc/contributors/tutorial_old/index.md +0 -20
- data/doc/custom.css +0 -22
- data/doc/essentials/commands.md +0 -20
- data/doc/essentials/index.md +0 -31
- data/doc/essentials/messages.md +0 -21
- data/doc/essentials/models.md +0 -21
- data/doc/essentials/shortcuts.md +0 -19
- data/doc/essentials/the_elm_architecture.md +0 -24
- data/doc/essentials/the_runtime.md +0 -21
- data/doc/essentials/update_functions.md +0 -20
- data/doc/essentials/views.md +0 -22
- data/doc/getting_started/for_go_developers.md +0 -16
- data/doc/getting_started/for_python_developers.md +0 -16
- data/doc/getting_started/for_rails_developers.md +0 -17
- data/doc/getting_started/for_ratatui_ruby_developers.md +0 -17
- data/doc/getting_started/for_react_developers.md +0 -17
- data/doc/getting_started/index.md +0 -52
- data/doc/getting_started/install.md +0 -20
- data/doc/getting_started/quickstart.md +0 -20
- data/doc/getting_started/ruby_primer.md +0 -19
- data/doc/getting_started/why_rooibos.md +0 -20
- data/doc/images/verify_readme_usage.png +0 -0
- data/doc/images/widget_cmd_exec.png +0 -0
- data/doc/index.md +0 -93
- data/doc/scaling_up/async_patterns.md +0 -20
- data/doc/scaling_up/command_composition.md +0 -20
- data/doc/scaling_up/custom_commands.md +0 -21
- data/doc/scaling_up/fractal_architecture.md +0 -20
- data/doc/scaling_up/index.md +0 -30
- data/doc/scaling_up/message_routing.md +0 -20
- data/doc/scaling_up/ractor_safety.md +0 -20
- data/doc/scaling_up/testing.md +0 -21
- data/doc/troubleshooting/common_errors.md +0 -20
- data/doc/troubleshooting/debugging.md +0 -21
- data/doc/troubleshooting/index.md +0 -23
- data/doc/troubleshooting/performance.md +0 -20
- data/doc/tutorial/01_project_setup.md +0 -44
- data/doc/tutorial/02_hello_world.md +0 -45
- data/doc/tutorial/03_static_file_list.md +0 -44
- data/doc/tutorial/04_arrow_navigation.md +0 -47
- data/doc/tutorial/05_real_files.md +0 -45
- data/doc/tutorial/06_safe_refactoring.md +0 -21
- data/doc/tutorial/07_red_first_tdd.md +0 -26
- data/doc/tutorial/08_file_metadata.md +0 -42
- data/doc/tutorial/09_text_preview.md +0 -44
- data/doc/tutorial/10_directory_tree.md +0 -42
- data/doc/tutorial/11_pane_focus.md +0 -40
- data/doc/tutorial/12_sorting.md +0 -41
- data/doc/tutorial/13_filtering.md +0 -43
- data/doc/tutorial/14_toggle_hidden.md +0 -41
- data/doc/tutorial/15_text_input_widget.md +0 -43
- data/doc/tutorial/16_rename_files.md +0 -42
- data/doc/tutorial/17_confirmation_dialogs.md +0 -43
- data/doc/tutorial/18_progress_indicators.md +0 -43
- data/doc/tutorial/19_atomic_operations.md +0 -42
- data/doc/tutorial/20_external_editor.md +0 -42
- data/doc/tutorial/21_modal_overlays.md +0 -41
- data/doc/tutorial/22_error_handling.md +0 -43
- data/doc/tutorial/23_terminal_capabilities.md +0 -53
- data/doc/tutorial/24_mouse_events.md +0 -43
- data/doc/tutorial/25_resize_events.md +0 -43
- data/doc/tutorial/26_loading_states.md +0 -42
- data/doc/tutorial/27_performance.md +0 -43
- data/doc/tutorial/28_color_schemes.md +0 -47
- data/doc/tutorial/29_configuration.md +0 -124
- data/doc/tutorial/30_going_further.md +0 -17
- data/doc/tutorial/index.md +0 -17
- data/examples/app_fractal_dashboard/README.md +0 -60
- data/examples/app_fractal_dashboard/app.rb +0 -63
- data/examples/app_fractal_dashboard/dashboard/base.rb +0 -73
- data/examples/app_fractal_dashboard/dashboard/update_helpers.rb +0 -86
- data/examples/app_fractal_dashboard/dashboard/update_manual.rb +0 -87
- data/examples/app_fractal_dashboard/dashboard/update_router.rb +0 -43
- data/examples/app_fractal_dashboard/fragments/custom_shell_input.rb +0 -81
- data/examples/app_fractal_dashboard/fragments/custom_shell_modal.rb +0 -82
- data/examples/app_fractal_dashboard/fragments/custom_shell_output.rb +0 -90
- data/examples/app_fractal_dashboard/fragments/disk_usage.rb +0 -47
- data/examples/app_fractal_dashboard/fragments/network_panel.rb +0 -45
- data/examples/app_fractal_dashboard/fragments/ping.rb +0 -47
- data/examples/app_fractal_dashboard/fragments/stats_panel.rb +0 -45
- data/examples/app_fractal_dashboard/fragments/system_info.rb +0 -47
- data/examples/app_fractal_dashboard/fragments/uptime.rb +0 -47
- data/examples/tutorial/01/app.rb +0 -50
- data/examples/tutorial/02/app.rb +0 -64
- data/examples/tutorial/03/app.rb +0 -91
- data/examples/tutorial/06_safe_refactoring/app.rb +0 -124
- data/examples/verify_readme_usage/README.md +0 -54
- data/examples/verify_readme_usage/app.rb +0 -47
- data/examples/verify_website_first_app/app.rb +0 -85
- data/examples/verify_website_hello_mvu/app.rb +0 -31
- data/examples/widget_command_system/README.md +0 -70
- data/examples/widget_command_system/app.rb +0 -134
- data/generate_tutorial_stubs.rb +0 -126
- data/mise.toml +0 -8
- data/rbs_collection.lock.yaml +0 -108
- data/rbs_collection.yaml +0 -15
- data/tasks/example_viewer.html.erb +0 -172
- data/tasks/install.rake +0 -29
- data/tasks/resources/build.yml.erb +0 -55
- data/tasks/resources/index.html.erb +0 -44
- data/tasks/resources/rubies.yml +0 -7
- data/tasks/steep.rake +0 -11
- /data/{vendor/goodcop/base.yml → lib/rooibos/rubocop.yml} +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module Rooibos
|
|
10
|
+
module Router
|
|
11
|
+
# :stopdoc:
|
|
12
|
+
module Flow
|
|
13
|
+
# Outward flow: messages traveling toward the root.
|
|
14
|
+
#
|
|
15
|
+
# Observe → intercept.
|
|
16
|
+
class Outward < Data.define(:observes, :receives, :routes)
|
|
17
|
+
include Dispatch
|
|
18
|
+
|
|
19
|
+
# Sentinel: intercept consumed the bubble. Non-nil so
|
|
20
|
+
# Transition#with_command replaces the original Command::Bubble
|
|
21
|
+
# instead of preserving it via the nil no-op.
|
|
22
|
+
INTERCEPTED = Object.new.freeze
|
|
23
|
+
|
|
24
|
+
def call(message, model)
|
|
25
|
+
transition = run_all(observes, message, model)
|
|
26
|
+
config = Configuration.new(message:, model: transition.model)
|
|
27
|
+
intercept_first_matching(receives, config, transition) ||
|
|
28
|
+
transition
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private def intercept_first_matching(rules, config, transition)
|
|
32
|
+
rules.each do |rule|
|
|
33
|
+
new_transition = rule.apply_if_matches(config, routes) or next
|
|
34
|
+
return transition
|
|
35
|
+
.with_model(new_transition.model)
|
|
36
|
+
.with_command(new_transition.command || INTERCEPTED)
|
|
37
|
+
end
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
private_constant :Flow
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module Rooibos
|
|
10
|
+
module Router
|
|
11
|
+
# :stopdoc:
|
|
12
|
+
# Normalizes guard options into a callable.
|
|
13
|
+
class Guard < Data.define(:callable)
|
|
14
|
+
@scope = []
|
|
15
|
+
|
|
16
|
+
def self.scoped(callable)
|
|
17
|
+
@scope.push(callable)
|
|
18
|
+
yield
|
|
19
|
+
ensure
|
|
20
|
+
@scope.pop
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.from(guard: nil, when: nil, unless: nil, if: nil, only: nil, except: nil, skip: nil)
|
|
24
|
+
when_guard = binding.local_variable_get(:when) ||
|
|
25
|
+
binding.local_variable_get(:if) ||
|
|
26
|
+
binding.local_variable_get(:only)
|
|
27
|
+
unless_guard = binding.local_variable_get(:unless) ||
|
|
28
|
+
binding.local_variable_get(:except) ||
|
|
29
|
+
binding.local_variable_get(:skip)
|
|
30
|
+
normalized = guard || when_guard
|
|
31
|
+
normalized = -> (msg, model) { !unless_guard.call(msg, model) } if unless_guard
|
|
32
|
+
new(callable: apply_scope(normalized))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private_class_method def self.apply_scope(callable)
|
|
36
|
+
@scope.inject(callable) do |inner, outer|
|
|
37
|
+
if inner
|
|
38
|
+
-> (msg, model) { outer.call(msg, model) && inner.call(msg, model) }
|
|
39
|
+
else
|
|
40
|
+
outer
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def arity
|
|
46
|
+
callable&.arity || 2
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def call(message, model)
|
|
50
|
+
return true unless callable
|
|
51
|
+
callable.arity.zero? ? callable.call : callable.call(message, model)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
private_constant :Guard
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module Rooibos
|
|
10
|
+
module Router
|
|
11
|
+
# :stopdoc:
|
|
12
|
+
module Predicate
|
|
13
|
+
# Matches key events by symbol.
|
|
14
|
+
class Events < Data.define(:keys)
|
|
15
|
+
def initialize(keys:)
|
|
16
|
+
super(keys: Array(keys))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def arity = 2
|
|
20
|
+
|
|
21
|
+
def call(message, _model)
|
|
22
|
+
message.respond_to?(:to_sym) && keys.include?(message.to_sym)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Matches routed messages by envelope.
|
|
27
|
+
class Routed < Data.define(:envelope)
|
|
28
|
+
def arity = 2
|
|
29
|
+
|
|
30
|
+
def call(message, _model)
|
|
31
|
+
message.is_a?(Message::Routed) && message.envelope == envelope
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Matches routed messages by any of the given envelopes.
|
|
36
|
+
class RoutedEnvelopes < Data.define(:envelopes)
|
|
37
|
+
def initialize(envelopes:)
|
|
38
|
+
super(envelopes: Array(envelopes))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def arity = 2
|
|
42
|
+
|
|
43
|
+
def call(message, _model)
|
|
44
|
+
message.routed? && envelopes.include?(message.envelope)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Matches messages by class.
|
|
49
|
+
class InstancesOf < Data.define(:klass)
|
|
50
|
+
def arity = 2
|
|
51
|
+
|
|
52
|
+
def call(message, _model)
|
|
53
|
+
message.is_a?(klass)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Matches all messages.
|
|
58
|
+
class Always < Data.define
|
|
59
|
+
def arity = 2
|
|
60
|
+
def call(_message, _model) = true
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
private_constant :Predicate
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module Rooibos
|
|
10
|
+
module Router
|
|
11
|
+
# :stopdoc:
|
|
12
|
+
# Collection of named actions.
|
|
13
|
+
class Actions < Data.define(:rules)
|
|
14
|
+
include Enumerable
|
|
15
|
+
|
|
16
|
+
def initialize(rules: [])
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add(name, handler)
|
|
21
|
+
action = if handler.is_a?(Module)
|
|
22
|
+
RoutedAction.new(name: name.to_sym, fragment: handler)
|
|
23
|
+
else
|
|
24
|
+
LambdaAction.new(name: name.to_sym, handler:)
|
|
25
|
+
end
|
|
26
|
+
rules << action
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def [](name)
|
|
30
|
+
find { |action| action.name == name.to_sym } or
|
|
31
|
+
raise ArgumentError, "Unknown action: #{name}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def each(&block)
|
|
35
|
+
return rules.each unless block
|
|
36
|
+
rules.each { |action| block.call(action) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
private_constant :Actions
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module Rooibos
|
|
10
|
+
module Router
|
|
11
|
+
# :stopdoc:
|
|
12
|
+
# Collection of forwards
|
|
13
|
+
class Forwards < Data.define(:rules)
|
|
14
|
+
include Registry
|
|
15
|
+
|
|
16
|
+
def initialize(rules: [])
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add_instances_of(klass, to: nil, as: nil, broadcast: false, broadcast_to: nil) = add(Forward.new(
|
|
21
|
+
predicate: Predicate::InstancesOf.new(klass:),
|
|
22
|
+
targets: resolve_targets(to:, broadcast:, broadcast_to:),
|
|
23
|
+
envelope: as
|
|
24
|
+
))
|
|
25
|
+
|
|
26
|
+
def add_events(keys, to:, as: nil, guard: nil, when: nil, unless: nil) = add(Forward.new(
|
|
27
|
+
predicate: Predicate::Events.new(keys:),
|
|
28
|
+
targets: to,
|
|
29
|
+
envelope: as,
|
|
30
|
+
guard: Guard.from(guard:, when: binding.local_variable_get(:when), unless: binding.local_variable_get(:unless))
|
|
31
|
+
))
|
|
32
|
+
|
|
33
|
+
def add_routed(envelopes, to:, as: nil) = add(Forward.new(
|
|
34
|
+
predicate: Predicate::RoutedEnvelopes.new(envelopes:),
|
|
35
|
+
targets: to,
|
|
36
|
+
envelope: as
|
|
37
|
+
))
|
|
38
|
+
|
|
39
|
+
def add_custom(predicate, to:, as: nil, **guard_opts) = add(Forward.new(
|
|
40
|
+
predicate:,
|
|
41
|
+
targets: to,
|
|
42
|
+
envelope: as,
|
|
43
|
+
guard: Guard.from(**guard_opts)
|
|
44
|
+
))
|
|
45
|
+
|
|
46
|
+
private def resolve_targets(to:, broadcast:, broadcast_to:)
|
|
47
|
+
if broadcast
|
|
48
|
+
ALL_ROUTES
|
|
49
|
+
elsif broadcast_to
|
|
50
|
+
broadcast_to
|
|
51
|
+
else
|
|
52
|
+
to
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
private_constant :Forwards
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module Rooibos
|
|
10
|
+
module Router
|
|
11
|
+
# :stopdoc:
|
|
12
|
+
# Collection of observe rules.
|
|
13
|
+
class Observes < Data.define(:rules, :actions)
|
|
14
|
+
include Registry
|
|
15
|
+
|
|
16
|
+
def initialize(rules: [], actions:)
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add_events(keys, handler, **guard_opts) = add(Observe.new(
|
|
21
|
+
predicate: Predicate::Events.new(keys:),
|
|
22
|
+
action: resolve(handler),
|
|
23
|
+
guard: Guard.from(**guard_opts)
|
|
24
|
+
))
|
|
25
|
+
|
|
26
|
+
def add_routed(envelope, handler, guard: nil) = add(Observe.new(
|
|
27
|
+
predicate: Predicate::Routed.new(envelope:),
|
|
28
|
+
action: resolve(handler),
|
|
29
|
+
guard: Guard.from(guard:)
|
|
30
|
+
))
|
|
31
|
+
|
|
32
|
+
def add_instances_of(klass, handler, guard: nil) = add(Observe.new(
|
|
33
|
+
predicate: Predicate::InstancesOf.new(klass:),
|
|
34
|
+
action: resolve(handler),
|
|
35
|
+
guard: Guard.from(guard:)
|
|
36
|
+
))
|
|
37
|
+
|
|
38
|
+
def add_all(handler, **guard_opts) = add(Observe.new(
|
|
39
|
+
predicate: Predicate::Always.new,
|
|
40
|
+
action: resolve(handler),
|
|
41
|
+
guard: Guard.from(**guard_opts)
|
|
42
|
+
))
|
|
43
|
+
|
|
44
|
+
def add_custom(predicate, handler, **guard_opts) = add(Observe.new(
|
|
45
|
+
predicate:,
|
|
46
|
+
action: resolve(handler),
|
|
47
|
+
guard: Guard.from(**guard_opts)
|
|
48
|
+
))
|
|
49
|
+
|
|
50
|
+
private def resolve(handler)
|
|
51
|
+
return actions[handler] if handler.is_a?(Symbol)
|
|
52
|
+
LambdaAction.new(name: nil, handler:)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
private_constant :Observes
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module Rooibos
|
|
10
|
+
module Router
|
|
11
|
+
# :stopdoc:
|
|
12
|
+
# Collection of otherwise rules.
|
|
13
|
+
class Otherwises < Data.define(:rules)
|
|
14
|
+
include Registry
|
|
15
|
+
|
|
16
|
+
def initialize(rules: [])
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add(route_to:, **guard_opts)
|
|
21
|
+
super(Otherwise.new(
|
|
22
|
+
target: route_to,
|
|
23
|
+
guard: Guard.from(**guard_opts)
|
|
24
|
+
))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
private_constant :Otherwises
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module Rooibos
|
|
10
|
+
module Router
|
|
11
|
+
# :stopdoc:
|
|
12
|
+
# Collection of receive rules.
|
|
13
|
+
class Receives < Data.define(:rules, :actions)
|
|
14
|
+
include Registry
|
|
15
|
+
|
|
16
|
+
def initialize(rules: [], actions:)
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add_events(keys, handler, **guard_opts) = add(Receive.new(
|
|
21
|
+
predicate: Predicate::Events.new(keys:),
|
|
22
|
+
action: resolve(handler),
|
|
23
|
+
guard: Guard.from(**guard_opts)
|
|
24
|
+
))
|
|
25
|
+
|
|
26
|
+
def add_routed(envelope, handler, guard: nil) = add(Receive.new(
|
|
27
|
+
predicate: Predicate::Routed.new(envelope:),
|
|
28
|
+
action: resolve(handler),
|
|
29
|
+
guard: Guard.from(guard:)
|
|
30
|
+
))
|
|
31
|
+
|
|
32
|
+
def add_instances_of(klass, handler, guard: nil) = add(Receive.new(
|
|
33
|
+
predicate: Predicate::InstancesOf.new(klass:),
|
|
34
|
+
action: resolve(handler),
|
|
35
|
+
guard: Guard.from(guard:)
|
|
36
|
+
))
|
|
37
|
+
|
|
38
|
+
def add_all(handler, **guard_opts) = add(Receive.new(
|
|
39
|
+
predicate: Predicate::Always.new,
|
|
40
|
+
action: resolve(handler),
|
|
41
|
+
guard: Guard.from(**guard_opts)
|
|
42
|
+
))
|
|
43
|
+
|
|
44
|
+
def add_custom(predicate, handler, guard: nil) = add(Receive.new(
|
|
45
|
+
predicate:,
|
|
46
|
+
action: resolve(handler),
|
|
47
|
+
guard: Guard.from(guard:)
|
|
48
|
+
))
|
|
49
|
+
|
|
50
|
+
private def resolve(handler)
|
|
51
|
+
return actions[handler] if handler.is_a?(Symbol)
|
|
52
|
+
LambdaAction.new(name: nil, handler:)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
private_constant :Receives
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module Rooibos
|
|
10
|
+
module Router
|
|
11
|
+
# :stopdoc:
|
|
12
|
+
class Routes
|
|
13
|
+
include Enumerable
|
|
14
|
+
|
|
15
|
+
def initialize(routes = Set.new) = @routes = routes
|
|
16
|
+
|
|
17
|
+
def add(route)
|
|
18
|
+
@routes << route
|
|
19
|
+
route
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def for(identifier)
|
|
23
|
+
case identifier
|
|
24
|
+
when Module then unique(identifier) { |r| r.fragment == identifier }
|
|
25
|
+
when Symbol then unique(identifier) { |r| r.prefix == identifier }
|
|
26
|
+
else identifier
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def each(&block)
|
|
31
|
+
return @routes.each unless block
|
|
32
|
+
@routes.each { |route| block.call(route) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def subset(targets)
|
|
36
|
+
resolved = targets.filter_map { |t| self.for(t) }
|
|
37
|
+
Routes.new(resolved.to_set)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def broadcast(message, model)
|
|
41
|
+
reduce(Transition.initial(model)) do |transition, route|
|
|
42
|
+
new_model, command = route.delegate(message, transition.model)
|
|
43
|
+
transition.with_model(new_model).with_added_command(command)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def broadcast_to(prefixes, message, model)
|
|
48
|
+
subset(prefixes).broadcast(message, model)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private def unique(identifier, &)
|
|
52
|
+
matches = @routes.select(&)
|
|
53
|
+
raise Rooibos::Error::Invariant, "Ambiguous route: #{identifier} matches #{matches.size} routes. Capture the Route returned by `route` and pass it to `to:` to disambiguate." if matches.size > 1
|
|
54
|
+
matches.first
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
private_constant :Routes
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module Rooibos
|
|
10
|
+
module Router
|
|
11
|
+
# :stopdoc:
|
|
12
|
+
# Common behavior for Rule collections.
|
|
13
|
+
# Classes including this must define :rules as a Data member.
|
|
14
|
+
module Registry
|
|
15
|
+
include Enumerable
|
|
16
|
+
|
|
17
|
+
def add(rule) = rules << rule
|
|
18
|
+
|
|
19
|
+
def each(&block)
|
|
20
|
+
return rules.each unless block
|
|
21
|
+
rules.each { |rule| block.call(rule) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
private_constant :Registry
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module Rooibos
|
|
10
|
+
module Router
|
|
11
|
+
# :stopdoc:
|
|
12
|
+
Route = Data.define(:prefix, :fragment, :read, :write) do
|
|
13
|
+
def initialize(prefix:, fragment:, read: nil, write: nil)
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def delegate(message, model)
|
|
18
|
+
nested_model = extract(model)
|
|
19
|
+
result = fragment::Update.call(message, nested_model)
|
|
20
|
+
transition = Transition.from(result, nested_model)
|
|
21
|
+
Transition.new(model: merge(model, transition.model), command: transition.command)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private def extract(model)
|
|
25
|
+
if read
|
|
26
|
+
read.call(model)
|
|
27
|
+
else
|
|
28
|
+
model.public_send(prefix)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private def merge(model, new_nested_model)
|
|
33
|
+
if write
|
|
34
|
+
write.call(model, new_nested_model)
|
|
35
|
+
else
|
|
36
|
+
model.with(prefix => new_nested_model)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
private_constant :Route
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module Rooibos
|
|
10
|
+
module Router
|
|
11
|
+
# :stopdoc:
|
|
12
|
+
# Encapsulates dispatch logic - given frozen rule sets, processes messages.
|
|
13
|
+
class RouterUpdate < Data.define(:inward, :outward)
|
|
14
|
+
def call(message, model)
|
|
15
|
+
case message
|
|
16
|
+
when Message::Bubbled then dispatch_outward(message.message, model)
|
|
17
|
+
else dispatch_inward(message, model)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private def dispatch_inward(message, model)
|
|
22
|
+
transition = inward.call(message, model)
|
|
23
|
+
case transition.command
|
|
24
|
+
when Command::Bubble # Sentinel; not a real Command to be handled by the runtime
|
|
25
|
+
bubble_model, bubble_cmd = dispatch_outward(transition.command.message, transition.model)
|
|
26
|
+
transition = transition.with_model(bubble_model).with_command(bubble_cmd)
|
|
27
|
+
when Command::Batch
|
|
28
|
+
transition = extract_bubbles_from_batch(transition)
|
|
29
|
+
end
|
|
30
|
+
transition = transition.with(command: nil) if transition.command.equal?(Flow::Outward::INTERCEPTED)
|
|
31
|
+
transition.to_a
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private def extract_bubbles_from_batch(transition)
|
|
35
|
+
bubbles, remaining = transition.command.extract_bubbles
|
|
36
|
+
return transition if bubbles.empty?
|
|
37
|
+
|
|
38
|
+
result = Transition.new(model: transition.model, command: remaining)
|
|
39
|
+
bubbles.each do |bubble|
|
|
40
|
+
model, cmd = dispatch_outward(bubble.message, result.model)
|
|
41
|
+
result = Transition.new(model:, command: result.command)
|
|
42
|
+
result = result.with_added_command(cmd) unless cmd.nil? || cmd.equal?(Flow::Outward::INTERCEPTED)
|
|
43
|
+
end
|
|
44
|
+
result
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private def dispatch_outward(message, model)
|
|
48
|
+
outward.call(message, model).to_a
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
private_constant :RouterUpdate
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module Rooibos
|
|
10
|
+
module Router
|
|
11
|
+
# :stopdoc:
|
|
12
|
+
# Forward rule - matches messages and delegates to route(s)
|
|
13
|
+
class Forward < Data.define(:predicate, :targets, :envelope, :guard)
|
|
14
|
+
include Rule
|
|
15
|
+
|
|
16
|
+
def initialize(predicate:, targets: ALL_ROUTES, envelope: nil, guard: nil)
|
|
17
|
+
targets = Array(targets) unless targets == ALL_ROUTES
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def apply(message, model, routes)
|
|
22
|
+
if targets == ALL_ROUTES
|
|
23
|
+
routes
|
|
24
|
+
else
|
|
25
|
+
routes.subset(targets)
|
|
26
|
+
end.broadcast(envelop(message), model)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private def envelop(message)
|
|
30
|
+
if envelope
|
|
31
|
+
Message::Routed.new(envelope:, event: message)
|
|
32
|
+
else
|
|
33
|
+
message
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
private_constant :Forward
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module Rooibos
|
|
10
|
+
module Router
|
|
11
|
+
# :stopdoc:
|
|
12
|
+
# Observe rule - matches messages, handles, and continues processing.
|
|
13
|
+
class Observe < Data.define(:predicate, :action, :guard)
|
|
14
|
+
include Rule
|
|
15
|
+
|
|
16
|
+
def apply(message, model, routes)
|
|
17
|
+
action.apply(message, model, routes)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
private_constant :Observe
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module Rooibos
|
|
10
|
+
module Router
|
|
11
|
+
# :stopdoc:
|
|
12
|
+
# Rule for otherwise routing - routes unhandled messages to a fragment.
|
|
13
|
+
Otherwise = Data.define(:target, :guard, :predicate) do
|
|
14
|
+
include Rule
|
|
15
|
+
|
|
16
|
+
def initialize(target:, guard: nil, predicate: Predicate::Always.new)
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def apply(message, model, routes)
|
|
21
|
+
routes.subset([target]).broadcast(message, model)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
private_constant :Otherwise
|
|
25
|
+
end
|
|
26
|
+
end
|