Module: Plutonium::Routing::WizardRegistration

Defined in:
lib/plutonium/routing/wizard_registration.rb

Overview

Adds register_wizard to the routing mapper, mirroring register_resource (see MapperExtensions). A wizard is portal-hosted (§5.2): the routes are drawn inside the portal engine's routes.draw block, so they inherit the portal's scope/auth/layout, and they dispatch to a portal-namespaced wizard controller that includes Wizard::Controller + the portal's own controller concern.

Draws (portal-relative):

GET  /onboarding(/:token)/:step  → <Portal>::WizardsController#show
POST /onboarding(/:token)/:step  → <Portal>::WizardsController#update

with onboarding_wizard_path / _url helpers.

Examples:

inside a portal engine's routes

AdminPortal::Engine.routes.draw do
  register_wizard OnboardingWizard, at: "onboarding"
  register_resource ::User
end

Constant Summary collapse

WIZARD_CONTROLLER_NAME =
"wizards"

Class Attribute Summary collapse

Instance Method Summary collapse

Class Attribute Details

.appended_public_wizardsObject

Returns the value of attribute appended_public_wizards.



33
34
35
# File 'lib/plutonium/routing/wizard_registration.rb', line 33

def appended_public_wizards
  @appended_public_wizards
end

Instance Method Details

#register_wizard(wizard_class, at:, as: nil, public: nil, layout: nil) ⇒ Object

Parameters:

  • wizard_class (Class)

    a Plutonium::Wizard::Base subclass

  • at (String)

    the portal-relative base path for the wizard's steps

  • as (String, Symbol, nil) (defaults to: nil)

    override the route helper name prefix

  • public (Boolean, nil) (defaults to: nil)

    mount on a PUBLIC (unauthenticated) route outside the portal's auth constraint, for an anonymous (guest) wizard. Defaults to the wizard's own anonymous? flag. A non-anonymous wizard may not be mounted public; an anonymous wizard may not be mounted authenticated (its whole point is pre-login access). See §4.5.

  • layout (Symbol, String, nil) (defaults to: nil)

    the Rails layout this mount renders in — a layout NAME, exactly like the controller layout macro: :basic (the bare BasicLayout, e.g. an onboarding screen), :resource (the standard shell), or any app layout. Only meaningful for register_wizard mounts; resource-defined (wizard macro) wizards are always embedded. Defaults by context (portal → the resource shell, main-app → :basic); turbo-frame requests are always layout-less regardless.



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/plutonium/routing/wizard_registration.rb', line 51

def register_wizard(wizard_class, at:, as: nil, public: nil, layout: nil)
  # The wizard subsystem is opt-in (`config.wizards.enabled`). When disabled,
  # draw no routes — its tables/migrations are skipped too, so a mounted route
  # couldn't work anyway. Warn rather than fail silently, so a
  # registered-but-disabled wizard is discoverable instead of a mystery 404.
  unless Plutonium.configuration.wizards.enabled
    Rails.logger.warn { "[Plutonium::Wizard] not registering routes for #{wizard_class} — config.wizards.enabled is false" }
    return
  end

  # A CONTEXT anchor (`anchored via: :method`) is portal-level: the anchor is
  # resolved by calling a controller method, needs no URL `:id`, and is
  # IDOR-safe (trusted context) — so it CAN mount here. Only a TYPE anchor
  # (`with:`-only, resolved from the URL `:id`) is rejected, because it needs
  # the resource controller's scoped, policy-gated `resource_record!`.
  if wizard_class.anchored? && !wizard_class.anchored_via?
    raise ArgumentError,
      "register_wizard #{wizard_class.name} — `with:`-anchored wizards are not " \
      "mounted portal-level. Register them on the anchored resource's definition " \
      "with the `wizard` macro, which auto-mounts a record action whose anchor is " \
      "resolved through the resource controller's scoped, policy-gated " \
      "`resource_record!`. (A `via:`-anchored wizard mounts here fine.)"
  end

  # Default the mount kind to the wizard's `anonymous?` flag, and reject
  # contradictions (§4.5): an `anonymous` wizard NEEDS a public route
  # (pre-login); a non-`anonymous` wizard MUST stay behind portal auth.
  is_public = public.nil? ? wizard_class.anonymous? : !!public
  if is_public && !wizard_class.anonymous?
    raise ArgumentError,
      "register_wizard #{wizard_class.name}, public: true — only an `anonymous` " \
      "wizard may be mounted public. Add the `anonymous` macro to the wizard, or " \
      "drop `public:`."
  end
  if !is_public && wizard_class.anonymous?
    raise ArgumentError,
      "register_wizard #{wizard_class.name} — an `anonymous` wizard must be mounted " \
      "public (it runs pre-login). Pass `public: true` (it is the default for " \
      "`anonymous` wizards)."
  end

  return register_public_wizard(wizard_class, at:, as:, layout:) if is_public

  ensure_wizard_controller!(wizard_class)

  # The helper name defaults to the mount path (`at:`), so
  # `register_wizard W, at: "onboarding"` yields `onboarding_wizard_path`.
  # `as:` overrides it; the wizard's own route name is the final fallback.
  helper_name = (as || at.presence || wizard_route_name(wizard_class)).to_s.tr("/", "_")
  defaults = wizard_route_defaults(wizard_class, layout)

  scope path: at do
    # Canonical launch: GET the bare mount → resolve/mint the run and PRG to
    # its first (or resumed) step, with the token already in the URL. This is
    # the shareable entry point; `wizard_step_url` builds the stepped URLs.
    get "/", to: "#{WIZARD_CONTROLLER_NAME}#launch",
      as: :"#{helper_name}_wizard_launch", defaults: defaults
    get "(/:token)/:step", to: "#{WIZARD_CONTROLLER_NAME}#show",
      as: :"#{helper_name}_wizard", defaults: defaults
    post "(/:token)/:step", to: "#{WIZARD_CONTROLLER_NAME}#update",
      defaults: defaults
  end
end