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



112
113
114
115
116
117
118
119
120
121
# File 'lib/otto/core/configuration.rb', line 112

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



152
153
154
155
156
157
# File 'lib/otto/core/configuration.rb', line 152

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



78
79
80
81
82
83
84
85
# File 'lib/otto/core/configuration.rb', line 78

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



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/otto/core/configuration.rb', line 87

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



136
137
138
139
# File 'lib/otto/core/configuration.rb', line 136

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
65
66
67
68
69
70
71
72
73
74
75
76
# 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 count-based trusted-proxy depth if provided (mutually exclusive
  # with trusted_proxies; conflict validated at configuration freeze).
  # Guard on presence (`unless nil?`), not truthiness, so an explicitly
  # provided invalid value (e.g. `false`) reaches the validating setter
  # and fails loud instead of being silently dropped.
  @security_config.trusted_proxy_depth = opts[:trusted_proxy_depth] unless opts[:trusted_proxy_depth].nil?

  # Select the forwarded header depth mode reads from ('X-Forwarded-For',
  # 'Forwarded', or 'Both'). Only consulted in depth mode. Same presence
  # guard: a provided-but-invalid value is validated, not ignored.
  @security_config.trusted_proxy_header = opts[:trusted_proxy_header] unless opts[:trusted_proxy_header].nil?

  # 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



219
220
221
# File 'lib/otto/core/configuration.rb', line 219

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



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
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/otto/core/configuration.rb', line 168

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



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

def frozen_configuration?
  @configuration_frozen == true
end

#middleware_enabled?(middleware_class) ⇒ Boolean

Returns:

  • (Boolean)


223
224
225
226
# File 'lib/otto/core/configuration.rb', line 223

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.



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/otto/core/configuration.rb', line 239

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