Module: Ruact::ServerFunctions::RouteSource

Defined in:
lib/ruact/server_functions/route_source.rb

Overview

Story 9.3 — derives v2 server-function entries from the Rails route table.

The route table is the single source of truth (FR61): every non-GET routed action on a controller that includes Ruact::Server is a callable server function. This module is the route-driven replacement for the v1 registry source consumed by Snapshot — it reads routes, not ‘ruact_action` declarations.

Pure by construction: RouteSource.collect takes the route set and two resolver callables (host predicate + override lookup). The railtie passes the real constant-resolving implementations; unit specs inject lambdas so the derivation table is testable without booting controllers.

## Derivation table (locked, ADR addendum 2026-06-09)

‘js_identifier = lowerCamel(action) + Namespace*(Pascal) + Resource(Pascal)`

  • **Resource word** — singular for the RESTful writes (‘create`/`update`/ `destroy`) and for any member route (path carries `:id`); plural for a custom collection route. Examples: `posts#create` → `createPost`, `posts#publish` (member) → `publishPost`, `posts#publish_all` (collection) → `publishAllPosts`, `resource :session` `#create` →`createSession`.

  • Namespace — PascalCased and inserted between verb and resource (prefix, NOT flat): ‘admin/posts#create` → `createAdminPost`, `admin/reports/posts#create` → `createAdminReportsPost`. Prefixing keeps the merged JS namespace collision-free by construction (a flat scheme would force `admin/posts#create` and `posts#create` to collide).

  • PATCH/PUT — ‘resources` emits both verbs for `update`; they collapse to one entry with `http_method: “PATCH”` (Rails’ primary verb).

See Also:

  • "Story 9.3"

Constant Summary collapse

MUTATION_VERBS =

Verbs that expose a callable server function. GET/HEAD are pages.

%w[POST PUT PATCH DELETE].freeze
RESTFUL_WRITES =

The RESTful writes whose JS name uses the SINGULAR resource even though ‘create` is technically a collection route.

%w[create update destroy].freeze
VERB_PRIORITY =

When the same controller#action is routed under several verbs (the ‘update` PATCH/PUT pair), keep the first by this priority.

{ "PATCH" => 0, "PUT" => 1, "POST" => 2, "DELETE" => 3 }.freeze

Class Method Summary collapse

Class Method Details

.collect(route_set, host_predicate: nil, overrides_for: nil) ⇒ Array<Hash>

Collects v2 mutation entries from route_set.

Parameters:

  • route_set (#routes)

    anything exposing ‘#routes` (a `ActionDispatch::Routing::RouteSet`, or `Rails.application.routes`).

  • host_predicate (#call) (defaults to: nil)

    ‘controller_path(String) -> Boolean` —true when that controller includes Ruact::Server. Defaults to real constant resolution.

  • overrides_for (#call) (defaults to: nil)

    ‘controller_path(String) -> HashString=>String` — the `ruact_function_name` override map (action name → js identifier) for that controller. Defaults to real constant resolution.

Returns:

  • (Array<Hash>)

    entries (string keys) sorted by ‘js_identifier`; shape: `js_identifier`, `kind` (always `“action”`), `http_method`, `path`, `segments` (Array<String>), `controller`, `action`.



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/ruact/server_functions/route_source.rb', line 66

def collect(route_set, host_predicate: nil, overrides_for: nil)
  host_predicate ||= method(:default_host?)
  overrides_for  ||= method(:default_overrides_for)

  by_key = {}
  route_set.routes.each do |route|
    verb = route.verb.to_s
    next unless MUTATION_VERBS.include?(verb)

    controller = route.defaults[:controller]
    action = route.defaults[:action]
    next if controller.nil? || action.nil?
    next unless host_predicate.call(controller)

    key = [controller, action]
    existing = by_key[key]
    # PATCH/PUT collapse: keep the higher-priority verb only.
    next if existing && verb_rank(verb) >= verb_rank(existing["http_method"])

    by_key[key] = build_entry(route, verb, controller, action, overrides_for)
  end

  entries = by_key.values.sort_by { |entry| entry["js_identifier"] }
  detect_collisions!(entries)
  entries
end