Class: Ruact::ServerFunctions::EndpointController

Inherits:
ActionController::Base
  • Object
show all
Includes:
ErrorRendering
Defined in:
lib/ruact/server_functions/endpoint_controller.rb

Overview

Story 8.1 — the single gem-mounted Rails controller backing ‘POST /__ruact/fn/:name`. It resolves the URL `:name` parameter to a registered RegistryEntry, allocates a fresh instance of the entry’s host controller class, and delegates dispatch to that instance via Rails’ standard ‘dispatch(action_name, request, response)` plumbing.

This indirection is what gives ‘ruact_action` blocks access to the host controller’s ‘current_user`, `session`, `before_action` chain, Pundit / ActionPolicy authorization, and `rescue_from` handlers — the block runs inside an honest controller instance, not in some gem-internal context.

The ‘dispatch_action` action below is the ONLY public action on this controller — there is no `:create`, `:update`, etc.; the host’s actions are reached indirectly via the wrapper method ‘_ruact_action<symbol>` that Controller#ruact_action defines.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.standalone_host?(host) ⇒ Boolean

Story 8.3 — positive check for the standalone host shape. A host is standalone iff it’s a Module (and not a Class) that extends ‘Ruact::ServerAction`. The class hierarchy `Class < Module` means `is_a?(Module)` also matches Classes; we exclude Classes explicitly.

Returns:

  • (Boolean)


158
159
160
161
162
163
164
# File 'lib/ruact/server_functions/endpoint_controller.rb', line 158

def self.standalone_host?(host)
  return false if host.nil?
  return false if host.is_a?(Class)
  return false unless host.is_a?(Module)

  host.singleton_class.include?(Ruact::ServerAction)
end

Instance Method Details

#dispatch_actionObject

‘POST /__ruact/fn/:name` (mounted by `Ruact::Railtie`).



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/ruact/server_functions/endpoint_controller.rb', line 94

def dispatch_action
  entry = @__ruact_entry
  return render_unknown(@__ruact_name_sym) unless entry

  host = entry.controller
  if Ruact::ServerFunctions::EndpointController.standalone_host?(host)
    # Call StandaloneDispatcher WITHOUT passing the response so Rails'
    # `ImplicitRender` does not see an uncommitted response (writing
    # directly to `response.body =` would otherwise be silently
    # overwritten by the implicit-render 204). Apply the dispatcher's
    # Result directive via render/head, which Rails recognises as
    # rendered output.
    result = Ruact::ServerFunctions::StandaloneDispatcher.dispatch(entry, request)
    return apply_standalone_result(result)
  end

  unless host.is_a?(Class)
    return render(
      json: { error: "ruact action :#{@__ruact_name_sym} has an invalid host shape — " \
                     "expected a Controller class or a Module that extends Ruact::ServerAction" },
      status: :internal_server_error
    )
  end

  host_class = host

  # Re-run-2 (2026-05-14) — rebuild `request.path_parameters` so that
  # the host action sees `controller`/`action` keys describing ITSELF,
  # not the gem-endpoint route. Without this, `params[:controller]`
  # inside the host's action body returns
  # `"ruact/server_functions/endpoint"` and `params[:action]` returns
  # `"dispatch_action"` — which breaks `controller_name` /
  # `controller_path` / Pundit policy resolution / any code that reads
  # the routing identity. Restore after dispatch so the endpoint
  # response can be rendered with its own identity intact.
  # Re-run-4 (2026-05-15) — DROP `name: raw_name` from the swap.
  # The host action does not need the routing function name (it's
  # already inferable from `action_name`), and keeping it in
  # `path_parameters` made `params[:name]` inside the host action /
  # before_action chain return the route function name instead of
  # a legitimate submitted body field named `:name`. Only
  # `controller`/`action` are swapped — those are required for
  # `controller_name` / `controller_path` / Pundit / instrumentation.
  original_path_parameters = request.path_parameters.dup
  host_path_parameters = {
    controller: host_class.controller_path,
    action: @__ruact_name_sym.to_s
  }
  request.path_parameters = host_path_parameters

  # Thread-local sentinel allows the public action method to be
  # invoked only here, not from a wildcard route the host may have
  # set up — see the guard inside the defined method.
  Thread.current[:__ruact_dispatching] = @__ruact_name_sym
  host_class.dispatch(@__ruact_name_sym.to_s, request, response)
ensure
  Thread.current[:__ruact_dispatching] = nil
  request.path_parameters = original_path_parameters if original_path_parameters
end