Module: Otto::Core::Configuration

Includes:
Freezable
Included in:
Otto
Defined in:
lib/otto/core/configuration.rb

Overview

Configuration module providing locale and application configuration methods

Instance Method Summary collapse

Methods included from Freezable

#deep_freeze!

Instance Method Details

#configure(available_locales: nil, default_locale: nil) ⇒ Object

Configure locale settings for the application

Examples:

otto.configure(
  available_locales: { 'en' => 'English', 'es' => 'Spanish', 'fr' => 'French' },
  default_locale: 'en'
)

Parameters:

  • available_locales (Hash) (defaults to: nil)

    Hash of available locales (e.g., { ‘en’ => ‘English’, ‘es’ => ‘Spanish’ })

  • default_locale (String) (defaults to: nil)

    Default locale to use as fallback



100
101
102
103
104
105
106
107
108
109
# File 'lib/otto/core/configuration.rb', line 100

def configure(available_locales: nil, default_locale: nil)
  ensure_not_frozen!

  # Initialize locale_config if not already set
  @locale_config ||= Otto::Locale::Config.new

  # Update configuration
  @locale_config.available_locales = available_locales if available_locales
  @locale_config.default_locale = default_locale if default_locale
end

#configure_auth_strategies(strategies, default_strategy: 'noauth') ⇒ Object

Configure authentication strategies for route-level access control.

Examples:

otto.configure_auth_strategies({
  'noauth' => Otto::Security::Authentication::Strategies::NoAuthStrategy.new,
  'authenticated' => Otto::Security::Authentication::Strategies::SessionStrategy.new(session_key: 'user_id'),
  'role:admin' => Otto::Security::Authentication::Strategies::RoleStrategy.new(['admin']),
  'api_key' => Otto::Security::Authentication::Strategies::APIKeyStrategy.new(api_keys: ['secret123'])
})

Parameters:

  • strategies (Hash)

    Hash mapping strategy names to strategy instances

  • default_strategy (String) (defaults to: 'noauth')

    Default strategy to use when none specified



140
141
142
143
144
145
# File 'lib/otto/core/configuration.rb', line 140

def configure_auth_strategies(strategies, default_strategy: 'noauth')
  ensure_not_frozen!
  # Update existing @auth_config rather than creating a new one
  @auth_config[:auth_strategies] = strategies
  @auth_config[:default_auth_strategy] = default_strategy
end

#configure_authentication(opts) ⇒ Object



66
67
68
69
70
71
72
73
# File 'lib/otto/core/configuration.rb', line 66

def configure_authentication(opts)
  # Update existing @auth_config rather than creating a new one
  # to maintain synchronization with the configurator
  @auth_config[:auth_strategies] = opts[:auth_strategies] if opts[:auth_strategies]
  @auth_config[:default_auth_strategy] = opts[:default_auth_strategy] if opts[:default_auth_strategy]

  # No-op: authentication strategies are configured via @auth_config above
end

#configure_locale(opts) ⇒ Object



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/otto/core/configuration.rb', line 18

def configure_locale(opts)
  # Check if we have any locale configuration
  has_direct_options = opts[:available_locales] || opts[:default_locale]
  has_legacy_config = opts[:locale_config]

  # Only create locale_config if we have configuration
  return unless has_direct_options || has_legacy_config

  # Initialize with direct options
  available_locales = opts[:available_locales]
  default_locale = opts[:default_locale]

  # Legacy support: Configure locale if provided via locale_config hash
  if opts[:locale_config]
    locale_opts = opts[:locale_config]
    available_locales ||= locale_opts[:available_locales] || locale_opts[:available]
    default_locale ||= locale_opts[:default_locale] || locale_opts[:default]
  end

  # Create Otto::Locale::Config instance
  @locale_config = Otto::Locale::Config.new(
    available_locales: available_locales,
    default_locale: default_locale
  )
end

#configure_mcp(opts) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/otto/core/configuration.rb', line 75

def configure_mcp(opts)
  @mcp_server = nil

  # Enable MCP if requested in options
  return unless opts[:mcp_enabled] || opts[:mcp_http] || opts[:mcp_stdio]

  @mcp_server = Otto::MCP::Server.new(self)

  mcp_options = {}
  mcp_options[:http_endpoint] = opts[:mcp_endpoint] if opts[:mcp_endpoint]

  return unless opts[:mcp_http] != false # Default to true unless explicitly disabled

  @mcp_server.enable!(mcp_options)
end

#configure_rate_limiting(config) ⇒ Object

Configure rate limiting settings.

Examples:

otto.configure_rate_limiting({
  requests_per_minute: 50,
  custom_rules: {
    'api_calls' => { limit: 30, period: 60, condition: ->(req) { req.path.start_with?('/api') }}
  }
})

Parameters:

  • config (Hash)

    Rate limiting configuration

Options Hash (config):

  • :requests_per_minute (Integer)

    Maximum requests per minute per IP

  • :custom_rules (Hash)

    Hash of custom rate limiting rules

  • :cache_store (Object)

    Custom cache store for rate limiting



124
125
126
127
# File 'lib/otto/core/configuration.rb', line 124

def configure_rate_limiting(config)
  ensure_not_frozen!
  @security_config.rate_limiting_config.merge!(config)
end

#configure_security(opts) ⇒ Object



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/otto/core/configuration.rb', line 44

def configure_security(opts)
  # Enable CSRF protection if requested
  enable_csrf_protection! if opts[:csrf_protection]

  # Enable request validation if requested
  enable_request_validation! if opts[:request_validation]

  # Enable rate limiting if requested
  if opts[:rate_limiting]
    rate_limiting_opts = opts[:rate_limiting].is_a?(Hash) ? opts[:rate_limiting] : {}
    enable_rate_limiting!(rate_limiting_opts)
  end

  # Add trusted proxies if provided
  Array(opts[:trusted_proxies]).each { |proxy| add_trusted_proxy(proxy) } if opts[:trusted_proxies]

  # Set custom security headers
  return unless opts[:security_headers]

  set_security_headers(opts[:security_headers])
end

#ensure_not_frozen!Object

Ensure configuration is not frozen before allowing mutations

Raises:

  • (FrozenError)

    if configuration is frozen



207
208
209
# File 'lib/otto/core/configuration.rb', line 207

def ensure_not_frozen!
  raise FrozenError, 'Cannot modify frozen configuration' if frozen_configuration?
end

#freeze_configuration!self

Freeze the application configuration to prevent runtime modifications. Called automatically at the end of initialization to ensure immutability.

This prevents security-critical configuration from being modified after the application begins handling requests. Uses deep freezing to prevent both direct modification and modification through nested structures.

Returns:

  • (self)

Raises:

  • (RuntimeError)

    if configuration is already frozen



156
157
158
159
160
161
162
163
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
193
194
195
# File 'lib/otto/core/configuration.rb', line 156

def freeze_configuration!
  if frozen_configuration?
    Otto.structured_log(:debug, 'Configuration already frozen', { status: 'skipped' }) if Otto.debug
    return self
  end

  start_time = Otto::Utils.now_in_μs

  # Deep freeze configuration objects with memoization support
  @security_config.deep_freeze! if @security_config.respond_to?(:deep_freeze!)
  @locale_config.deep_freeze! if @locale_config.respond_to?(:deep_freeze!)
  @middleware.deep_freeze! if @middleware.respond_to?(:deep_freeze!)

  # Deep freeze configuration hashes (recursively freezes nested structures)
  deep_freeze_value(@auth_config) if @auth_config
  deep_freeze_value(@option) if @option

  # Validate registered handler-wrapper factories against every loaded
  # route before locking the config. Surfaces TypeError / factory bugs
  # at boot instead of on the first request that happens to match.
  validate_handler_wrappers!

  # Deep freeze route structures (prevent modification of nested hashes/arrays)
  deep_freeze_value(@routes) if @routes
  deep_freeze_value(@routes_literal) if @routes_literal
  deep_freeze_value(@routes_static) if @routes_static
  deep_freeze_value(@route_definitions) if @route_definitions

  @configuration_frozen = true

  duration = Otto::Utils.now_in_μs - start_time
  frozen_objects = %w[security_config locale_config middleware auth_config option routes]
  Otto.structured_log(:info, 'Freezing completed',
    {
            duration: duration,
      frozen_objects: frozen_objects.join(','),
    })

  self
end

#frozen_configuration?Boolean

Check if configuration is frozen

Returns:

  • (Boolean)

    true if configuration is frozen



200
201
202
# File 'lib/otto/core/configuration.rb', line 200

def frozen_configuration?
  @configuration_frozen == true
end

#middleware_enabled?(middleware_class) ⇒ Boolean

Returns:

  • (Boolean)


211
212
213
214
# File 'lib/otto/core/configuration.rb', line 211

def middleware_enabled?(middleware_class)
  # Only check the new middleware stack as the single source of truth
  @middleware&.includes?(middleware_class)
end

#validate_handler_wrappers!void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Walk every loaded route and exercise the registered handler-wrapper factories against a sentinel inner handler. Each factory must return a callable; HandlerFactory.apply_handler_wrappers raises TypeError otherwise. The constructed chain is discarded — this is a fail-fast validation pass, not memoization.

Iterates @routes (covers MCP routes added directly) uniquified by identity. No-op if no wrappers are registered or no routes are loaded.



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/otto/core/configuration.rb', line 227

def validate_handler_wrappers!
  return unless @routes && @route_handler_factory
  return if @handler_wrappers.nil? || @handler_wrappers.empty?

  sentinel = ->(_env, _extra = {}) { [200, {}, []] }
  seen = {}.compare_by_identity
  @routes.each_value do |routes_for_verb|
    routes_for_verb.each do |route|
      next if seen[route]

      seen[route] = true
      Otto::RouteHandlers::HandlerFactory.apply_handler_wrappers(
        sentinel, route.route_definition, self
      )
    end
  end
end