Class: Ruact::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/ruact/configuration.rb

Overview

Holds gem-wide configuration. Instantiated once via Ruact.config. Configure via ‘Ruact.configure { |c| c.attr = value }` in an initializer.

Frozen after ‘Ruact.configure` returns (Story 7.3) — direct post-boot mutation (`Ruact.config.attr = value` outside the block) raises `Ruact::ConfigurationError` with the offending attribute, the caller’s file:line, and the suggested fix. Re-calling ‘Ruact.configure` after boot replaces the configuration atomically and emits a `[ruact]` warning.

Constant Summary collapse

ATTRIBUTES =

The set of public attributes; new attributes added here automatically inherit the freeze contract via the ‘define_method` writer below.

%i[
  manifest_path
  strict_serialization
  suspense_timeout
  vite_dev_server
  current_user_resolver
  dev_error_payload_enabled
  max_upload_bytes
  query_route_prefix
  query_parent_controller
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(template: nil) ⇒ Configuration

Build a fresh Configuration. When template is given, dup every public attribute from it so the draft is mutable — used by ‘Ruact.configure` for atomic-replacement cloning. The dup is required because the template is always a published (frozen) Configuration with deep-frozen attribute values, and AC1 requires the DSL inside the configure block to behave identically regardless of whether this is the first call or a later one (including idiomatic in-place mutation of inherited values).

‘dup` is safe for every supported attribute type: Strings produce an unfrozen copy; nil/true/false/Numerics/Symbols dup to themselves (they are inherently immutable, so the dup is a no-op).

Parameters:



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/ruact/configuration.rb', line 134

def initialize(template: nil)
  if template
    ATTRIBUTES.each do |attr|
      value = template.public_send(attr)
      # Procs are immutable from the outside (Story 8.3 — current_user_resolver).
      # Duping creates a different Proc instance, breaking identity comparisons
      # across re-configurations. Procs are inherently re-entrant safe (no
      # mutable internal state surface) so the dup is unnecessary; the freeze
      # at seal! time is enough.
      cloned = value.is_a?(Proc) ? value : value.dup
      instance_variable_set("@#{attr}", cloned)
    end
  else
    @manifest_path        = nil
    @strict_serialization = begin
      Rails.env.production?
    rescue StandardError
      false
    end
    @suspense_timeout     = 5.0
    @vite_dev_server      = "http://localhost:5173"
    @current_user_resolver = nil
    @dev_error_payload_enabled = nil
    @max_upload_bytes = 10 * 1024 * 1024
    @query_route_prefix = "/q"
    @query_parent_controller = "ApplicationController"
  end
end

Instance Attribute Details

#current_user_resolverProc? (readonly)

Returns Story 8.3 — Lambda invoked by the standalone server-action dispatcher when a block reads ‘current_user`. Receives `request.env` (Hash) and returns the authenticated user (or nil). Memoized per-dispatch; left nil by default so apps that don’t use standalone actions never get a phantom ‘current_user` resolver.

Examples:

Devise

Ruact.configure { |c| c.current_user_resolver = ->(env) { env['warden']&.user } }

Hand-rolled session

Ruact.configure { |c| c.current_user_resolver = ->(env) { User.find_by(id: env['rack.session'][:user_id]) } }

Returns:

  • (Proc, nil)

    Story 8.3 — Lambda invoked by the standalone server-action dispatcher when a block reads ‘current_user`. Receives `request.env` (Hash) and returns the authenticated user (or nil). Memoized per-dispatch; left nil by default so apps that don’t use standalone actions never get a phantom ‘current_user` resolver.



108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/ruact/configuration.rb', line 108

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#dev_error_payload_enabledBoolean? (readonly)

Returns Story 8.4 — When true, server-action failures respond with a verbose JSON payload (action name, error class, message, split backtrace, contextual suggestion, validation errors). When false, the wire body carries only the four baseline fields (‘_ruact_server_action_error`, `action_name`, `error_class`, `message`) so React components can render their own UI without accidental backtrace leakage. Default `nil` — the endpoint controller resolves nil to `Rails.env.development? || Rails.env.test?`, keeping the Configuration trivially constructible in non-Rails specs.

Examples:

Force production-shape errors in development

Ruact.configure { |c| c.dev_error_payload_enabled = false }

Returns:

  • (Boolean, nil)

    Story 8.4 — When true, server-action failures respond with a verbose JSON payload (action name, error class, message, split backtrace, contextual suggestion, validation errors). When false, the wire body carries only the four baseline fields (‘_ruact_server_action_error`, `action_name`, `error_class`, `message`) so React components can render their own UI without accidental backtrace leakage. Default `nil` — the endpoint controller resolves nil to `Rails.env.development? || Rails.env.test?`, keeping the Configuration trivially constructible in non-Rails specs.



108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/ruact/configuration.rb', line 108

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#manifest_pathString? (readonly)

Returns Path to react-client-manifest.json. Defaults to Rails.root.join(“public/react-client-manifest.json”) when nil.

Returns:

  • (String, nil)

    Path to react-client-manifest.json. Defaults to Rails.root.join(“public/react-client-manifest.json”) when nil.



108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/ruact/configuration.rb', line 108

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#max_upload_bytesInteger? (readonly)

Note:

This is a controller-level “fail fast at the boundary” knob, not a stream-safety guarantee — Rack’s multipart parser will still buffer bodies up to its own limits before the guard rejects. For very large uploads route through Active Storage Direct Upload or a presigned S3 URL; see ‘website/docs/api/server-actions.md` “File uploads” section.

Returns Story 8.5 — upper bound (in bytes) on the ‘Content-Length` of `multipart/form-data` and `application/x-www-form-urlencoded` requests dispatched through `POST /__ruact/fn/:name`. When the inbound `Content-Length` exceeds this value, the endpoint controller raises `Ruact::UploadTooLargeError` BEFORE Rack’s multipart parser runs, producing a 413 with the Story 8.4 structured error body. Default: ‘10 * 1024 * 1024` (10 MB). Set to `nil` to disable the gem-side guard — typical when a reverse proxy (`client_max_body_size`) or host middleware already owns the operational cap. Chunked-transfer requests (no `Content-Length` header) bypass the guard regardless of this setting; the action body is responsible for any belt-and-suspenders check via `params.size` / `params.byte_size` in that case.

Examples:

Raise the limit to 25 MB

Ruact.configure { |c| c.max_upload_bytes = 25 * 1024 * 1024 }

Disable the gem-side guard (reverse proxy owns the cap)

Ruact.configure { |c| c.max_upload_bytes = nil }

Returns:

  • (Integer, nil)

    Story 8.5 — upper bound (in bytes) on the ‘Content-Length` of `multipart/form-data` and `application/x-www-form-urlencoded` requests dispatched through `POST /__ruact/fn/:name`. When the inbound `Content-Length` exceeds this value, the endpoint controller raises `Ruact::UploadTooLargeError` BEFORE Rack’s multipart parser runs, producing a 413 with the Story 8.4 structured error body. Default: ‘10 * 1024 * 1024` (10 MB). Set to `nil` to disable the gem-side guard — typical when a reverse proxy (`client_max_body_size`) or host middleware already owns the operational cap. Chunked-transfer requests (no `Content-Length` header) bypass the guard regardless of this setting; the action body is responsible for any belt-and-suspenders check via `params.size` / `params.byte_size` in that case.



108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/ruact/configuration.rb', line 108

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#query_parent_controllerString (readonly)

Returns Story 9.4 — class NAME of the controller the gem’s internal query dispatch controller inherits from (default ‘“ApplicationController”` — the Devise `parent_controller` pattern). Kept as a String and constantized lazily at route-draw time, NOT at configure time: `ApplicationController` does not exist when the gem loads. The host’s REAL callback chain (‘authenticate_user!`, tenant scoping, Pundit) runs before any query class is instantiated (FR89).

Examples:

Dispatch queries through an API base controller

Ruact.configure { |c| c.query_parent_controller = "Api::BaseController" }

Returns:

  • (String)

    Story 9.4 — class NAME of the controller the gem’s internal query dispatch controller inherits from (default ‘“ApplicationController”` — the Devise `parent_controller` pattern). Kept as a String and constantized lazily at route-draw time, NOT at configure time: `ApplicationController` does not exist when the gem loads. The host’s REAL callback chain (‘authenticate_user!`, tenant scoping, Pundit) runs before any query class is instantiated (FR89).



108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/ruact/configuration.rb', line 108

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#query_route_prefixString (readonly)

Returns Story 9.4 — URL prefix under which the ‘ruact_queries` routing macro draws one named GET route per public query method (default `“/q”` → `GET /q/<jsIdentifier>`). Must be a String starting with `/` and without a trailing slash (the macro joins prefix and identifier with `/`). Changing the prefix is configuration, never code.

Examples:

Mount queries under /api/queries

Ruact.configure { |c| c.query_route_prefix = "/api/queries" }

Returns:

  • (String)

    Story 9.4 — URL prefix under which the ‘ruact_queries` routing macro draws one named GET route per public query method (default `“/q”` → `GET /q/<jsIdentifier>`). Must be a String starting with `/` and without a trailing slash (the macro joins prefix and identifier with `/`). Changing the prefix is configuration, never code.



108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/ruact/configuration.rb', line 108

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#strict_serializationBoolean (readonly)

Returns When true, objects without explicit ruact_props declaration raise Ruact::SerializationError. Defaults to false in development, true in production.

Returns:

  • (Boolean)

    When true, objects without explicit ruact_props declaration raise Ruact::SerializationError. Defaults to false in development, true in production.



108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/ruact/configuration.rb', line 108

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#suspense_timeoutFloat (readonly)

Returns Seconds before a deferred Suspense chunk times out. Default: 5.0.

Returns:

  • (Float)

    Seconds before a deferred Suspense chunk times out. Default: 5.0.



108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/ruact/configuration.rb', line 108

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#vite_dev_serverString (readonly)

Returns Base URL of the Vite dev server. Default: “localhost:5173”.

Returns:

  • (String)

    Base URL of the Vite dev server. Default: “localhost:5173”.



108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/ruact/configuration.rb', line 108

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end