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
  dev_error_payload_enabled
  max_upload_bytes
  query_route_prefix
  query_parent_controller
  signed_global_id_default_purpose
  signed_global_id_default_expires_in
  shadcn_compatible_versions
].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:



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/ruact/configuration.rb', line 164

def initialize(template: nil)
  if template
    ATTRIBUTES.each do |attr|
      value = template.public_send(attr)
      # Procs are immutable from the outside. 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"
    @dev_error_payload_enabled = nil
    @max_upload_bytes = 10 * 1024 * 1024
    @query_route_prefix = "/q"
    @query_parent_controller = "ApplicationController"
    @signed_global_id_default_purpose = nil
    @signed_global_id_default_expires_in = nil
    @shadcn_compatible_versions = [1, 2]
  end
end

Instance Attribute Details

#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 error-rendering layer 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 error-rendering layer resolves nil to Rails.env.development? || Rails.env.test?, keeping the Configuration trivially constructible in non-Rails specs.



138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/ruact/configuration.rb', line 138

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.



138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/ruact/configuration.rb', line 138

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 to a Ruact::Server mutation route. When the inbound Content-Length exceeds this value, the server concern 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[:file].size / params[:file].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 to a Ruact::Server mutation route. When the inbound Content-Length exceeds this value, the server concern 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[:file].size / params[:file].byte_size in that case.



138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/ruact/configuration.rb', line 138

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).



138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/ruact/configuration.rb', line 138

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.



138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/ruact/configuration.rb', line 138

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

#shadcn_compatible_versionsArray<Integer> (readonly)

Returns Story 10.5 — the shadcn/ui MAJOR versions the ruact:scaffold generator is regression-tested against. When the generator detects an installed shadcn major (best-effort, from the host package.json) that is NOT in this list, it emits a warning (never a hard stop) that the generated components may import from outdated @/components/ui/* paths. Must be a non-empty Array of Integers. Default [1, 2] (the majors tested at gem-release time). A dev who has manually verified a newer major adds it here to suppress the warning — the documented "override" path.

Examples:

Allow shadcn v3 once you have verified it

Ruact.configure { |c| c.shadcn_compatible_versions = [1, 2, 3] }

Returns:

  • (Array<Integer>)

    Story 10.5 — the shadcn/ui MAJOR versions the ruact:scaffold generator is regression-tested against. When the generator detects an installed shadcn major (best-effort, from the host package.json) that is NOT in this list, it emits a warning (never a hard stop) that the generated components may import from outdated @/components/ui/* paths. Must be a non-empty Array of Integers. Default [1, 2] (the majors tested at gem-release time). A dev who has manually verified a newer major adds it here to suppress the warning — the documented "override" path.



138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/ruact/configuration.rb', line 138

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

#signed_global_id_default_expires_inActiveSupport::Duration? (readonly)

Returns Story 13.2 (FR96) — the default expires_in: Ruact.signed_global_id uses when the call omits expires_in:. Must be an ActiveSupport::Duration (e.g. 15.minutes) — globalid calls #from_now on it. Default nil — when neither the call nor this config supplies an expiry, the helper raises Ruact::Error rather than mint a non-expiring token. To deliberately mint a non-expiring token, pass an explicit expires_in: nil at the call site (a reviewed per-call choice), never via this default.

Examples:

Set an app-wide default expiry

Ruact.configure { |c| c.signed_global_id_default_expires_in = 1.hour }

Returns:

  • (ActiveSupport::Duration, nil)

    Story 13.2 (FR96) — the default expires_in: Ruact.signed_global_id uses when the call omits expires_in:. Must be an ActiveSupport::Duration (e.g. 15.minutes) — globalid calls #from_now on it. Default nil — when neither the call nor this config supplies an expiry, the helper raises Ruact::Error rather than mint a non-expiring token. To deliberately mint a non-expiring token, pass an explicit expires_in: nil at the call site (a reviewed per-call choice), never via this default.



138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/ruact/configuration.rb', line 138

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

#signed_global_id_default_purposeSymbol, ... (readonly)

Returns Story 13.2 (FR96) — the default for: purpose Ruact.signed_global_id / Ruact.locate_signed use when the call omits for:. A purpose scopes a signed reference to one use-site so a token minted for editing a post cannot be replayed against, say, a delete endpoint. Default nil — when neither the call nor this config supplies a purpose, the helper raises Ruact::Error rather than sign an unscoped token (the "developer forgot" path is a loud error, never a silent insecure default). Prefer a per-call for: when use-sites differ; set this only for an app-wide default purpose.

Examples:

Set an app-wide default purpose

Ruact.configure { |c| c.signed_global_id_default_purpose = :ruact_ref }

Returns:

  • (Symbol, String, nil)

    Story 13.2 (FR96) — the default for: purpose Ruact.signed_global_id / Ruact.locate_signed use when the call omits for:. A purpose scopes a signed reference to one use-site so a token minted for editing a post cannot be replayed against, say, a delete endpoint. Default nil — when neither the call nor this config supplies a purpose, the helper raises Ruact::Error rather than sign an unscoped token (the "developer forgot" path is a loud error, never a silent insecure default). Prefer a per-call for: when use-sites differ; set this only for an app-wide default purpose.



138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/ruact/configuration.rb', line 138

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.



138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/ruact/configuration.rb', line 138

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.



138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/ruact/configuration.rb', line 138

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: "http://localhost:5173&quot;.

Returns:



138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/ruact/configuration.rb', line 138

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