otto 2.0.0.pre8 → 2.0.0.pre10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/.github/workflows/claude-code-review.yml +1 -1
  4. data/.github/workflows/claude.yml +1 -1
  5. data/.github/workflows/code-smells.yml +2 -2
  6. data/CHANGELOG.rst +73 -35
  7. data/CLAUDE.md +44 -0
  8. data/Gemfile.lock +7 -7
  9. data/README.md +20 -0
  10. data/docs/.gitignore +2 -0
  11. data/docs/modern-authentication-authorization-landscape.md +558 -0
  12. data/docs/multi-strategy-authentication-design.md +1401 -0
  13. data/lib/otto/core/error_handler.rb +104 -10
  14. data/lib/otto/core/freezable.rb +0 -2
  15. data/lib/otto/core/helper_registry.rb +135 -0
  16. data/lib/otto/core/lifecycle_hooks.rb +63 -0
  17. data/lib/otto/core/middleware_management.rb +70 -0
  18. data/lib/otto/core/middleware_stack.rb +12 -8
  19. data/lib/otto/core/router.rb +25 -31
  20. data/lib/otto/core.rb +3 -0
  21. data/lib/otto/errors.rb +92 -0
  22. data/lib/otto/helpers.rb +2 -2
  23. data/lib/otto/locale/middleware.rb +1 -1
  24. data/lib/otto/mcp/core.rb +33 -0
  25. data/lib/otto/mcp/protocol.rb +1 -1
  26. data/lib/otto/mcp/rate_limiting.rb +6 -2
  27. data/lib/otto/mcp/schema_validation.rb +2 -2
  28. data/lib/otto/mcp.rb +1 -0
  29. data/lib/otto/privacy/core.rb +82 -0
  30. data/lib/otto/privacy.rb +1 -0
  31. data/lib/otto/{helpers/request.rb → request.rb} +17 -6
  32. data/lib/otto/{helpers/response.rb → response.rb} +20 -7
  33. data/lib/otto/response_handlers/json.rb +1 -3
  34. data/lib/otto/response_handlers/view.rb +1 -1
  35. data/lib/otto/route.rb +2 -4
  36. data/lib/otto/route_handlers/base.rb +88 -5
  37. data/lib/otto/route_handlers/class_method.rb +9 -67
  38. data/lib/otto/route_handlers/instance_method.rb +10 -57
  39. data/lib/otto/route_handlers/lambda.rb +2 -2
  40. data/lib/otto/route_handlers/logic_class.rb +85 -90
  41. data/lib/otto/security/authentication/auth_strategy.rb +2 -2
  42. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +1 -1
  43. data/lib/otto/security/authentication/strategy_result.rb +9 -9
  44. data/lib/otto/security/authorization_error.rb +1 -1
  45. data/lib/otto/security/config.rb +3 -3
  46. data/lib/otto/security/core.rb +167 -0
  47. data/lib/otto/security/middleware/csrf_middleware.rb +1 -1
  48. data/lib/otto/security/middleware/validation_middleware.rb +1 -1
  49. data/lib/otto/security/rate_limiter.rb +7 -3
  50. data/lib/otto/security.rb +1 -0
  51. data/lib/otto/version.rb +1 -1
  52. data/lib/otto.rb +29 -404
  53. metadata +12 -3
@@ -26,7 +26,7 @@ class Otto
26
26
  log_context = base_context.merge(
27
27
  error: error.message,
28
28
  error_class: error.class.name,
29
- error_id: error_id
29
+ error_id: error_id,
30
30
  )
31
31
  log_context[:handler] = env['otto.handler'] if env['otto.handler']
32
32
  log_context[:duration] = env['otto.handler_duration'] if env['otto.handler_duration']
@@ -38,7 +38,7 @@ class Otto
38
38
 
39
39
  # Parse request for content negotiation
40
40
  begin
41
- Rack::Request.new(env)
41
+ Otto::Request.new(env)
42
42
  rescue StandardError
43
43
  nil
44
44
  end
@@ -69,13 +69,95 @@ class Otto
69
69
  end
70
70
 
71
71
  # Content negotiation for built-in error response
72
- accept_header = env['HTTP_ACCEPT'].to_s
73
- return json_error_response(error_id) if accept_header.include?('application/json')
72
+ return json_error_response(error_id) if wants_json_response?(env)
74
73
 
75
74
  # Fallback to built-in error response
76
75
  @server_error || secure_error_response(error_id)
77
76
  end
78
77
 
78
+ # Register an error handler for expected business logic errors
79
+ #
80
+ # This allows you to handle known error conditions (like missing resources,
81
+ # expired data, rate limits) without logging them as unhandled 500 errors.
82
+ #
83
+ # @param error_class [Class, String] The exception class or class name to handle
84
+ # @param status [Integer] HTTP status code to return (default: 500)
85
+ # @param log_level [Symbol] Log level for expected errors (:info, :warn, :error)
86
+ # @param handler [Proc] Optional block to customize error response
87
+ #
88
+ # @example Basic usage with status code
89
+ # otto.register_error_handler(Onetime::MissingSecret, status: 404, log_level: :info)
90
+ # otto.register_error_handler(Onetime::SecretExpired, status: 410, log_level: :info)
91
+ #
92
+ # @example With custom response handler
93
+ # otto.register_error_handler(Onetime::RateLimited, status: 429, log_level: :warn) do |error, req|
94
+ # {
95
+ # error: 'Rate limit exceeded',
96
+ # retry_after: error.retry_after,
97
+ # message: error.message
98
+ # }
99
+ # end
100
+ #
101
+ # @example Using string class names (for lazy loading)
102
+ # otto.register_error_handler('Onetime::MissingSecret', status: 404, log_level: :info)
103
+ #
104
+ def register_error_handler(error_class, status: 500, log_level: :info, &handler)
105
+ ensure_not_frozen!
106
+
107
+ # Normalize error class to string for consistent lookup
108
+ error_class_name = error_class.is_a?(String) ? error_class : error_class.name
109
+
110
+ @error_handlers[error_class_name] = {
111
+ status: status,
112
+ log_level: log_level,
113
+ handler: handler
114
+ }
115
+ end
116
+
117
+ private
118
+
119
+ # Register all Otto framework error classes with appropriate status codes
120
+ #
121
+ # This method auto-registers base HTTP error classes and all framework-specific
122
+ # error classes (Security, MCP) so that raising them automatically returns the
123
+ # correct HTTP status code instead of 500.
124
+ #
125
+ # Users can override these registrations by calling register_error_handler
126
+ # after Otto.new with custom status codes or log levels.
127
+ #
128
+ # @return [void]
129
+ # @api private
130
+ def register_framework_errors
131
+ # Base HTTP errors (for direct use or subclassing by implementing projects)
132
+ register_error_from_class(Otto::NotFoundError)
133
+ register_error_from_class(Otto::BadRequestError)
134
+ register_error_from_class(Otto::UnauthorizedError)
135
+ register_error_from_class(Otto::ForbiddenError)
136
+ register_error_from_class(Otto::PayloadTooLargeError)
137
+
138
+ # Security module errors
139
+ register_error_from_class(Otto::Security::AuthorizationError)
140
+ register_error_from_class(Otto::Security::CSRFError)
141
+ register_error_from_class(Otto::Security::RequestTooLargeError)
142
+ register_error_from_class(Otto::Security::ValidationError)
143
+
144
+ # MCP module errors
145
+ register_error_from_class(Otto::MCP::ValidationError)
146
+ end
147
+
148
+ # Register an error handler using the error class as the single source of truth
149
+ #
150
+ # @param error_class [Class] Error class that responds to default_status and default_log_level
151
+ # @return [void]
152
+ # @api private
153
+ def register_error_from_class(error_class)
154
+ register_error_handler(
155
+ error_class,
156
+ status: error_class.default_status,
157
+ log_level: error_class.default_log_level
158
+ )
159
+ end
160
+
79
161
  private
80
162
 
81
163
  # Handle expected business logic errors with custom status codes and logging
@@ -96,7 +178,7 @@ class Otto
96
178
  error: error.message,
97
179
  error_class: error.class.name,
98
180
  error_id: error_id,
99
- expected: true # Mark as expected error
181
+ expected: true # Mark as expected error
100
182
  )
101
183
  log_context[:handler] = env['otto.handler'] if env['otto.handler']
102
184
  log_context[:duration] = env['otto.handler_duration'] if env['otto.handler_duration']
@@ -109,7 +191,7 @@ class Otto
109
191
  response_body = if handler_config[:handler]
110
192
  # Use custom handler block if provided
111
193
  begin
112
- req = Rack::Request.new(env)
194
+ req = @request_class.new(env)
113
195
  result = handler_config[:handler].call(error, req)
114
196
 
115
197
  # Validate that custom handler returned a Hash
@@ -146,14 +228,13 @@ class Otto
146
228
  response_body[:error_id] = error_id if Otto.env?(:dev, :development)
147
229
 
148
230
  # Content negotiation
149
- accept_header = env['HTTP_ACCEPT'].to_s
150
231
  status = handler_config[:status] || 500
151
232
 
152
- if accept_header.include?('application/json')
233
+ if wants_json_response?(env)
153
234
  body = JSON.generate(response_body)
154
235
  headers = {
155
236
  'content-type' => 'application/json',
156
- 'content-length' => body.bytesize.to_s
237
+ 'content-length' => body.bytesize.to_s,
157
238
  }.merge(@security_config.security_headers)
158
239
 
159
240
  [status, headers, [body]]
@@ -167,7 +248,7 @@ class Otto
167
248
 
168
249
  headers = {
169
250
  'content-type' => 'text/plain',
170
- 'content-length' => body.bytesize.to_s
251
+ 'content-length' => body.bytesize.to_s,
171
252
  }.merge(@security_config.security_headers)
172
253
 
173
254
  [status, headers, [body]]
@@ -211,6 +292,19 @@ class Otto
211
292
 
212
293
  [500, headers, [body]]
213
294
  end
295
+
296
+ private
297
+
298
+ # Determine if the client wants a JSON response
299
+ # Route's response_type declaration takes precedence over Accept header
300
+ #
301
+ # @param env [Hash] Rack environment
302
+ # @return [Boolean] true if JSON response is preferred
303
+ def wants_json_response?(env)
304
+ route_definition = env['otto.route_definition']
305
+ (route_definition&.response_type == 'json') ||
306
+ env['HTTP_ACCEPT'].to_s.include?('application/json')
307
+ end
214
308
  end
215
309
  end
216
310
  end
@@ -2,8 +2,6 @@
2
2
  #
3
3
  # frozen_string_literal: true
4
4
 
5
- require 'set'
6
-
7
5
  class Otto
8
6
  module Core
9
7
  # Provides deep freezing capability for configuration objects
@@ -0,0 +1,135 @@
1
+ # lib/otto/core/helper_registry.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ class Otto
6
+ module Core
7
+ # Helper registration module for extending Otto's Request and Response classes.
8
+ # Provides the public API for registering custom helper modules.
9
+ module HelperRegistry
10
+ # Register request helper modules
11
+ #
12
+ # Registered modules are included in Otto::Request at the class level,
13
+ # making custom helpers available alongside Otto's built-in helpers.
14
+ # Must be called before first request (before configuration freezing).
15
+ #
16
+ # This is the official integration point for application-specific helpers
17
+ # that work with Otto internals (strategy_result, privacy features, etc.).
18
+ #
19
+ # @param modules [Module, Array<Module>] Module(s) containing helper methods
20
+ # @example
21
+ # module Onetime::RequestHelpers
22
+ # def current_customer
23
+ # user.is_a?(Onetime::Customer) ? user : Onetime::Customer.anonymous
24
+ # end
25
+ #
26
+ # def organization
27
+ # @organization ||= strategy_result&.metadata.dig(:organization_context, :organization)
28
+ # end
29
+ # end
30
+ #
31
+ # otto.register_request_helpers(Onetime::RequestHelpers)
32
+ #
33
+ # @raise [ArgumentError] if module is not a Module
34
+ # @raise [FrozenError] if called after configuration is frozen
35
+ def register_request_helpers(*modules)
36
+ begin
37
+ ensure_not_frozen!
38
+ rescue FrozenError
39
+ raise FrozenError, 'Cannot register request helpers after first request'
40
+ end
41
+
42
+ modules.each do |mod|
43
+ unless mod.is_a?(Module)
44
+ raise ArgumentError, "Expected Module, got #{mod.class}"
45
+ end
46
+ @request_helper_modules << mod unless @request_helper_modules.include?(mod)
47
+ end
48
+
49
+ # Re-finalize to include newly registered helpers
50
+ finalize_request_response_classes
51
+ end
52
+
53
+ # Register response helper modules
54
+ #
55
+ # Registered modules are included in Otto::Response at the class level,
56
+ # making custom helpers available alongside Otto's built-in helpers.
57
+ # Must be called before first request (before configuration freezing).
58
+ #
59
+ # @param modules [Module, Array<Module>] Module(s) containing helper methods
60
+ # @example
61
+ # module Onetime::ResponseHelpers
62
+ # def json_success(data, status: 200)
63
+ # headers['content-type'] = 'application/json'
64
+ # self.status = status
65
+ # write JSON.generate({ success: true, data: data })
66
+ # end
67
+ # end
68
+ #
69
+ # otto.register_response_helpers(Onetime::ResponseHelpers)
70
+ #
71
+ # @raise [ArgumentError] if module is not a Module
72
+ # @raise [FrozenError] if called after configuration is frozen
73
+ def register_response_helpers(*modules)
74
+ begin
75
+ ensure_not_frozen!
76
+ rescue FrozenError
77
+ raise FrozenError, 'Cannot register response helpers after first request'
78
+ end
79
+
80
+ modules.each do |mod|
81
+ unless mod.is_a?(Module)
82
+ raise ArgumentError, "Expected Module, got #{mod.class}"
83
+ end
84
+ @response_helper_modules << mod unless @response_helper_modules.include?(mod)
85
+ end
86
+
87
+ # Re-finalize to include newly registered helpers
88
+ finalize_request_response_classes
89
+ end
90
+
91
+ # Get registered request helper modules (for debugging)
92
+ #
93
+ # @return [Array<Module>] Array of registered request helper modules
94
+ # @api private
95
+ def registered_request_helpers
96
+ @request_helper_modules.dup
97
+ end
98
+
99
+ # Get registered response helper modules (for debugging)
100
+ #
101
+ # @return [Array<Module>] Array of registered response helper modules
102
+ # @api private
103
+ def registered_response_helpers
104
+ @response_helper_modules.dup
105
+ end
106
+
107
+ private
108
+
109
+ # Finalize request and response classes with framework and custom helpers
110
+ #
111
+ # This method creates Otto's request and response classes by:
112
+ # 1. Subclassing Otto::Request/Response (which have framework helpers built-in)
113
+ # 2. Including any registered custom helper modules
114
+ #
115
+ # Called during initialization and can be called again if helpers are registered
116
+ # after initialization (before first request).
117
+ #
118
+ # @return [void]
119
+ # @api private
120
+ def finalize_request_response_classes
121
+ # Create request class with framework helpers
122
+ # Otto::Request has all framework helpers as instance methods
123
+ @request_class = Class.new(Otto::Request)
124
+
125
+ # Create response class with framework helpers
126
+ # Otto::Response has all framework helpers as instance methods
127
+ @response_class = Class.new(Otto::Response)
128
+
129
+ # Apply registered custom helpers (framework helpers always come first)
130
+ @request_helper_modules&.each { |mod| @request_class.include(mod) }
131
+ @response_helper_modules&.each { |mod| @response_class.include(mod) }
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,63 @@
1
+ # lib/otto/core/lifecycle_hooks.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ class Otto
6
+ module Core
7
+ # Lifecycle hooks module for registering callbacks at various points in request processing.
8
+ # Provides the public API for request completion callbacks.
9
+ module LifecycleHooks
10
+ # Register a callback to be executed after each request completes
11
+ #
12
+ # Instance-level request completion callbacks allow each Otto instance
13
+ # to have its own isolated set of callbacks, preventing duplicate
14
+ # invocations in multi-app architectures (e.g., Rack::URLMap).
15
+ #
16
+ # The callback receives three arguments:
17
+ # - request: Rack::Request object
18
+ # - response: Rack::Response object (wrapping the response tuple)
19
+ # - duration: Request processing duration in microseconds
20
+ #
21
+ # @example Basic usage
22
+ # otto = Otto.new(routes_file)
23
+ # otto.on_request_complete do |req, res, duration|
24
+ # logger.info "Request completed", path: req.path, duration: duration
25
+ # end
26
+ #
27
+ # @example Multi-app architecture
28
+ # # App 1: Core Web Application
29
+ # core_router = Otto.new
30
+ # core_router.on_request_complete do |req, res, duration|
31
+ # logger.info "Core app request", path: req.path
32
+ # end
33
+ #
34
+ # # App 2: API Application
35
+ # api_router = Otto.new
36
+ # api_router.on_request_complete do |req, res, duration|
37
+ # logger.info "API request", path: req.path
38
+ # end
39
+ #
40
+ # # Each callback only fires for its respective Otto instance
41
+ #
42
+ # @yield [request, response, duration] Block to execute after each request
43
+ # @yieldparam request [Rack::Request] The request object
44
+ # @yieldparam response [Rack::Response] The response object
45
+ # @yieldparam duration [Integer] Duration in microseconds
46
+ # @return [self] Returns self for method chaining
47
+ # @raise [FrozenError] if called after configuration is frozen
48
+ def on_request_complete(&block)
49
+ ensure_not_frozen!
50
+ @request_complete_callbacks << block if block_given?
51
+ self
52
+ end
53
+
54
+ # Get registered request completion callbacks (for internal use)
55
+ #
56
+ # @api private
57
+ # @return [Array<Proc>] Array of registered callback blocks
58
+ def request_complete_callbacks
59
+ @request_complete_callbacks
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,70 @@
1
+ # lib/otto/core/middleware_management.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ class Otto
6
+ module Core
7
+ # Middleware management module for building and configuring the Rack middleware stack.
8
+ # Provides the public API for adding middleware and building the application.
9
+ module MiddlewareManagement
10
+ # Builds the middleware application chain
11
+ # Called once at initialization and whenever middleware stack changes
12
+ #
13
+ # IMPORTANT: If you have routes with auth requirements, you MUST add session
14
+ # middleware to your middleware stack BEFORE Otto processes requests.
15
+ #
16
+ # Session middleware is required for RouteAuthWrapper to correctly persist
17
+ # session changes during authentication. Common options include:
18
+ # - Rack::Session::Cookie (requires rack-session gem)
19
+ # - Rack::Session::Pool
20
+ # - Rack::Session::Memcache
21
+ # - Any Rack-compatible session middleware
22
+ #
23
+ # Example:
24
+ # use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
25
+ # otto = Otto.new('routes.txt')
26
+ #
27
+ def build_app!
28
+ base_app = method(:handle_request)
29
+ @app = @middleware.wrap(base_app, @security_config)
30
+ end
31
+
32
+ # Add middleware to the stack
33
+ #
34
+ # @param middleware [Class] Middleware class to add
35
+ # @param args Additional arguments passed to middleware constructor
36
+ def use(middleware, ...)
37
+ ensure_not_frozen!
38
+ @middleware.add(middleware, ...)
39
+
40
+ # NOTE: If build_app! is triggered during a request (via use() or
41
+ # middleware_stack=), the @app instance variable could be swapped
42
+ # mid-request in a multi-threaded environment.
43
+
44
+ build_app! if @app # Rebuild app if already initialized
45
+ end
46
+
47
+ # Compatibility method for existing tests
48
+ # @return [Array] List of middleware classes
49
+ def middleware_stack
50
+ @middleware.middleware_list
51
+ end
52
+
53
+ # Compatibility method for existing tests
54
+ # @param stack [Array] Array of middleware classes
55
+ def middleware_stack=(stack)
56
+ @middleware.clear!
57
+ Array(stack).each { |middleware| @middleware.add(middleware) }
58
+ build_app! if @app # Rebuild app if already initialized
59
+ end
60
+
61
+ # Check if a specific middleware is enabled
62
+ #
63
+ # @param middleware_class [Class] Middleware class to check
64
+ # @return [Boolean] true if middleware is in the stack
65
+ def middleware_enabled?(middleware_class)
66
+ @middleware.includes?(middleware_class)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -73,7 +73,7 @@ class Otto
73
73
  when :last
74
74
  @stack << entry
75
75
  else
76
- @stack << entry # Default append
76
+ @stack << entry # Default append
77
77
  end
78
78
 
79
79
  @middleware_set.add(middleware_class)
@@ -114,15 +114,21 @@ class Otto
114
114
 
115
115
  # Check optimal order: rate_limit < auth < validation
116
116
  if rate_limit_pos && auth_pos && rate_limit_pos > auth_pos
117
- warnings << '[MCP Middleware] RateLimitMiddleware should come before TokenMiddleware for optimal performance'
117
+ warnings << <<~MSG.chomp
118
+ [MCP Middleware] RateLimitMiddleware should come before TokenMiddleware
119
+ MSG
118
120
  end
119
121
 
120
122
  if auth_pos && validation_pos && auth_pos > validation_pos
121
- warnings << '[MCP Middleware] TokenMiddleware should come before SchemaValidationMiddleware for optimal performance'
123
+ warnings << <<~MSG.chomp
124
+ [MCP Middleware] TokenMiddleware should come before SchemaValidationMiddleware
125
+ MSG
122
126
  end
123
127
 
124
128
  if rate_limit_pos && validation_pos && rate_limit_pos > validation_pos
125
- warnings << '[MCP Middleware] RateLimitMiddleware should come before SchemaValidationMiddleware for optimal performance'
129
+ warnings << <<~MSG.chomp
130
+ [MCP Middleware] RateLimitMiddleware should come before SchemaValidationMiddleware
131
+ MSG
126
132
  end
127
133
 
128
134
  warnings
@@ -198,8 +204,8 @@ class Otto
198
204
  @stack.map do |entry|
199
205
  {
200
206
  middleware: entry[:middleware],
201
- args: entry[:args],
202
- options: entry[:options],
207
+ args: entry[:args],
208
+ options: entry[:options],
203
209
  }
204
210
  end
205
211
  end
@@ -225,8 +231,6 @@ class Otto
225
231
  @stack.reverse_each(&)
226
232
  end
227
233
 
228
-
229
-
230
234
  private
231
235
 
232
236
  def middleware_needs_config?(middleware_class)
@@ -36,28 +36,28 @@ class Otto
36
36
  route.otto = self
37
37
  path_clean = path.gsub(%r{/$}, '')
38
38
  @route_definitions[route.definition] = route
39
- Otto.structured_log(:debug, "Route loaded",
40
- {
41
- pattern: route.pattern.source,
42
- verb: route.verb,
43
- definition: route.definition,
44
- type: 'pattern'
45
- }
46
- ) if Otto.debug
39
+ if Otto.debug
40
+ Otto.structured_log(:debug, 'Route loaded',
41
+ {
42
+ pattern: route.pattern.source,
43
+ verb: route.verb,
44
+ definition: route.definition,
45
+ type: 'pattern',
46
+ })
47
+ end
47
48
  @routes[route.verb] ||= []
48
49
  @routes[route.verb] << route
49
50
  @routes_literal[route.verb] ||= {}
50
51
  @routes_literal[route.verb][path_clean] = route
51
52
  rescue StandardError => e
52
- Otto.structured_log(:error, "Route load failed",
53
+ Otto.structured_log(:error, 'Route load failed',
53
54
  {
54
- path: path,
55
+ path: path,
55
56
  verb: verb,
56
57
  definition: definition,
57
58
  error: e.message,
58
- error_class: e.class.name
59
- }
60
- )
59
+ error_class: e.class.name,
60
+ })
61
61
  Otto.logger.debug e.backtrace.join("\n") if Otto.debug
62
62
  end
63
63
  self
@@ -96,30 +96,27 @@ class Otto
96
96
  literal_routes.merge! routes_literal[:GET] if http_verb == :HEAD
97
97
 
98
98
  if static_route && http_verb == :GET && routes_static[:GET].member?(base_path)
99
- Otto.structured_log(:debug, "Route matched",
99
+ Otto.structured_log(:debug, 'Route matched',
100
100
  Otto::LoggingHelpers.request_context(env).merge(
101
101
  type: 'static_cached',
102
102
  base_path: base_path
103
- )
104
- )
103
+ ))
105
104
  static_route.call(env)
106
105
  elsif literal_routes.has_key?(path_info_clean)
107
106
  route = literal_routes[path_info_clean]
108
- Otto.structured_log(:debug, "Route matched",
107
+ Otto.structured_log(:debug, 'Route matched',
109
108
  Otto::LoggingHelpers.request_context(env).merge(
110
109
  type: 'literal',
111
110
  handler: route.route_definition.definition,
112
111
  auth_strategy: route.route_definition.auth_requirement || 'none'
113
- )
114
- )
112
+ ))
115
113
  route.call(env)
116
114
  elsif static_route && http_verb == :GET && safe_file?(path_info)
117
- Otto.structured_log(:debug, "Route matched",
115
+ Otto.structured_log(:debug, 'Route matched',
118
116
  Otto::LoggingHelpers.request_context(env).merge(
119
117
  type: 'static_new',
120
118
  base_path: base_path
121
- )
122
- )
119
+ ))
123
120
  routes_static[:GET][base_path] = base_path
124
121
  static_route.call(env)
125
122
  else
@@ -165,14 +162,13 @@ class Otto
165
162
  found_route = route
166
163
 
167
164
  # Log successful route match
168
- Otto.structured_log(:debug, "Route matched",
165
+ Otto.structured_log(:debug, 'Route matched',
169
166
  Otto::LoggingHelpers.request_context(env).merge(
170
167
  pattern: route.pattern.source,
171
168
  handler: route.route_definition.definition,
172
169
  auth_strategy: route.route_definition.auth_requirement || 'none',
173
170
  route_params: extra_params
174
- )
175
- )
171
+ ))
176
172
  break
177
173
  end
178
174
 
@@ -180,19 +176,17 @@ class Otto
180
176
  if found_route
181
177
  # Log 404 route usage if we fell back to it
182
178
  if found_route == literal_routes['/404']
183
- Otto.structured_log(:info, "Route not found",
179
+ Otto.structured_log(:info, 'Route not found',
184
180
  Otto::LoggingHelpers.request_context(env).merge(
185
181
  fallback_to: '404_route'
186
- )
187
- )
182
+ ))
188
183
  end
189
184
  found_route.call env, extra_params
190
185
  else
191
- Otto.structured_log(:info, "Route not found",
186
+ Otto.structured_log(:info, 'Route not found',
192
187
  Otto::LoggingHelpers.request_context(env).merge(
193
188
  fallback_to: 'default_not_found'
194
- )
195
- )
189
+ ))
196
190
  @not_found || Otto::Static.not_found
197
191
  end
198
192
  end
data/lib/otto/core.rb CHANGED
@@ -8,3 +8,6 @@ require_relative 'core/configuration'
8
8
  require_relative 'core/error_handler'
9
9
  require_relative 'core/uri_generator'
10
10
  require_relative 'core/middleware_stack'
11
+ require_relative 'core/helper_registry'
12
+ require_relative 'core/middleware_management'
13
+ require_relative 'core/lifecycle_hooks'