otto 1.6.0 → 2.0.0.pre1

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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/.github/workflows/claude-code-review.yml +53 -0
  4. data/.github/workflows/claude.yml +49 -0
  5. data/.gitignore +3 -0
  6. data/.rubocop.yml +24 -345
  7. data/CHANGELOG.rst +83 -0
  8. data/CLAUDE.md +56 -0
  9. data/Gemfile +10 -3
  10. data/Gemfile.lock +23 -28
  11. data/README.md +2 -0
  12. data/bin/rspec +4 -4
  13. data/changelog.d/20250911_235619_delano_next.rst +28 -0
  14. data/changelog.d/20250912_123055_delano_remove_ostruct.rst +21 -0
  15. data/changelog.d/20250912_175625_claude_delano_remove_ostruct.rst +21 -0
  16. data/changelog.d/README.md +120 -0
  17. data/changelog.d/scriv.ini +5 -0
  18. data/docs/.gitignore +1 -0
  19. data/docs/migrating/v2.0.0-pre1.md +276 -0
  20. data/examples/.gitignore +1 -0
  21. data/examples/advanced_routes/README.md +33 -0
  22. data/examples/advanced_routes/app/controllers/handlers/async.rb +9 -0
  23. data/examples/advanced_routes/app/controllers/handlers/dynamic.rb +9 -0
  24. data/examples/advanced_routes/app/controllers/handlers/static.rb +9 -0
  25. data/examples/advanced_routes/app/controllers/modules/auth.rb +9 -0
  26. data/examples/advanced_routes/app/controllers/modules/transformer.rb +9 -0
  27. data/examples/advanced_routes/app/controllers/modules/validator.rb +9 -0
  28. data/examples/advanced_routes/app/controllers/routes_app.rb +232 -0
  29. data/examples/advanced_routes/app/controllers/v2/admin.rb +9 -0
  30. data/examples/advanced_routes/app/controllers/v2/config.rb +9 -0
  31. data/examples/advanced_routes/app/controllers/v2/settings.rb +9 -0
  32. data/examples/advanced_routes/app/logic/admin/logic/manager.rb +27 -0
  33. data/examples/advanced_routes/app/logic/admin/panel.rb +27 -0
  34. data/examples/advanced_routes/app/logic/analytics_processor.rb +25 -0
  35. data/examples/advanced_routes/app/logic/complex/business/handler.rb +27 -0
  36. data/examples/advanced_routes/app/logic/data_logic.rb +23 -0
  37. data/examples/advanced_routes/app/logic/data_processor.rb +25 -0
  38. data/examples/advanced_routes/app/logic/input_validator.rb +24 -0
  39. data/examples/advanced_routes/app/logic/nested/feature/logic.rb +27 -0
  40. data/examples/advanced_routes/app/logic/reports_generator.rb +27 -0
  41. data/examples/advanced_routes/app/logic/simple_logic.rb +25 -0
  42. data/examples/advanced_routes/app/logic/system/config/manager.rb +27 -0
  43. data/examples/advanced_routes/app/logic/test_logic.rb +23 -0
  44. data/examples/advanced_routes/app/logic/transform_logic.rb +23 -0
  45. data/examples/advanced_routes/app/logic/upload_logic.rb +23 -0
  46. data/examples/advanced_routes/app/logic/v2/logic/dashboard.rb +27 -0
  47. data/examples/advanced_routes/app/logic/v2/logic/processor.rb +27 -0
  48. data/examples/advanced_routes/app.rb +33 -0
  49. data/examples/advanced_routes/config.rb +23 -0
  50. data/examples/advanced_routes/config.ru +7 -0
  51. data/examples/advanced_routes/puma.rb +20 -0
  52. data/examples/advanced_routes/routes +167 -0
  53. data/examples/advanced_routes/run.rb +39 -0
  54. data/examples/advanced_routes/test.rb +58 -0
  55. data/examples/authentication_strategies/README.md +32 -0
  56. data/examples/authentication_strategies/app/auth.rb +68 -0
  57. data/examples/authentication_strategies/app/controllers/auth_controller.rb +29 -0
  58. data/examples/authentication_strategies/app/controllers/main_controller.rb +28 -0
  59. data/examples/authentication_strategies/config.ru +24 -0
  60. data/examples/authentication_strategies/routes +37 -0
  61. data/examples/basic/README.md +29 -0
  62. data/examples/basic/app.rb +7 -35
  63. data/examples/basic/routes +0 -9
  64. data/examples/mcp_demo/README.md +87 -0
  65. data/examples/mcp_demo/app.rb +29 -34
  66. data/examples/mcp_demo/config.ru +9 -60
  67. data/examples/security_features/README.md +46 -0
  68. data/examples/security_features/app.rb +23 -24
  69. data/examples/security_features/config.ru +8 -10
  70. data/lib/otto/core/configuration.rb +167 -0
  71. data/lib/otto/core/error_handler.rb +86 -0
  72. data/lib/otto/core/file_safety.rb +61 -0
  73. data/lib/otto/core/middleware_stack.rb +157 -0
  74. data/lib/otto/core/router.rb +183 -0
  75. data/lib/otto/core/uri_generator.rb +44 -0
  76. data/lib/otto/design_system.rb +7 -5
  77. data/lib/otto/helpers/base.rb +3 -0
  78. data/lib/otto/helpers/request.rb +10 -8
  79. data/lib/otto/helpers/response.rb +5 -4
  80. data/lib/otto/helpers/validation.rb +9 -7
  81. data/lib/otto/mcp/auth/token.rb +10 -9
  82. data/lib/otto/mcp/protocol.rb +24 -27
  83. data/lib/otto/mcp/rate_limiting.rb +8 -3
  84. data/lib/otto/mcp/registry.rb +7 -2
  85. data/lib/otto/mcp/route_parser.rb +10 -15
  86. data/lib/otto/mcp/server.rb +21 -11
  87. data/lib/otto/mcp/validation.rb +14 -10
  88. data/lib/otto/response_handlers/auto.rb +39 -0
  89. data/lib/otto/response_handlers/base.rb +16 -0
  90. data/lib/otto/response_handlers/default.rb +16 -0
  91. data/lib/otto/response_handlers/factory.rb +39 -0
  92. data/lib/otto/response_handlers/json.rb +28 -0
  93. data/lib/otto/response_handlers/redirect.rb +25 -0
  94. data/lib/otto/response_handlers/view.rb +24 -0
  95. data/lib/otto/response_handlers.rb +9 -135
  96. data/lib/otto/route.rb +9 -9
  97. data/lib/otto/route_definition.rb +15 -18
  98. data/lib/otto/route_handlers/base.rb +121 -0
  99. data/lib/otto/route_handlers/class_method.rb +89 -0
  100. data/lib/otto/route_handlers/factory.rb +29 -0
  101. data/lib/otto/route_handlers/instance_method.rb +69 -0
  102. data/lib/otto/route_handlers/lambda.rb +59 -0
  103. data/lib/otto/route_handlers/logic_class.rb +93 -0
  104. data/lib/otto/route_handlers.rb +10 -405
  105. data/lib/otto/security/authentication/auth_strategy.rb +44 -0
  106. data/lib/otto/security/authentication/authentication_middleware.rb +123 -0
  107. data/lib/otto/security/authentication/failure_result.rb +36 -0
  108. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +40 -0
  109. data/lib/otto/security/authentication/strategies/permission_strategy.rb +47 -0
  110. data/lib/otto/security/authentication/strategies/public_strategy.rb +19 -0
  111. data/lib/otto/security/authentication/strategies/role_strategy.rb +57 -0
  112. data/lib/otto/security/authentication/strategies/session_strategy.rb +41 -0
  113. data/lib/otto/security/authentication/strategy_result.rb +223 -0
  114. data/lib/otto/security/authentication.rb +28 -282
  115. data/lib/otto/security/config.rb +14 -12
  116. data/lib/otto/security/configurator.rb +219 -0
  117. data/lib/otto/security/csrf.rb +8 -143
  118. data/lib/otto/security/middleware/csrf_middleware.rb +151 -0
  119. data/lib/otto/security/middleware/rate_limit_middleware.rb +38 -0
  120. data/lib/otto/security/middleware/validation_middleware.rb +252 -0
  121. data/lib/otto/security/rate_limiter.rb +86 -0
  122. data/lib/otto/security/rate_limiting.rb +10 -105
  123. data/lib/otto/security/validator.rb +8 -253
  124. data/lib/otto/static.rb +3 -0
  125. data/lib/otto/utils.rb +14 -0
  126. data/lib/otto/version.rb +3 -1
  127. data/lib/otto.rb +142 -498
  128. data/otto.gemspec +2 -2
  129. metadata +89 -28
  130. data/examples/dynamic_pages/app.rb +0 -115
  131. data/examples/dynamic_pages/config.ru +0 -30
  132. data/examples/dynamic_pages/routes +0 -21
  133. data/examples/helpers_demo/app.rb +0 -244
  134. data/examples/helpers_demo/config.ru +0 -26
  135. data/examples/helpers_demo/routes +0 -7
  136. data/lib/concurrent_cache_store.rb +0 -68
@@ -1,21 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/otto/mcp/route_parser.rb
4
+
1
5
  class Otto
2
6
  module MCP
7
+ # Parser for MCP route definitions and resource URIs
3
8
  class RouteParser
4
9
  def self.parse_mcp_route(_verb, _path, definition)
5
10
  # MCP route format: MCP resource_uri HandlerClass.method_name
6
11
  # Note: The path parameter is ignored for MCP routes - resource_uri comes from definition
7
12
  parts = definition.split(/\s+/, 3)
8
13
 
9
- if parts[0] != 'MCP'
10
- raise ArgumentError, "Expected MCP keyword, got: #{parts[0]}"
11
- end
14
+ raise ArgumentError, "Expected MCP keyword, got: #{parts[0]}" if parts[0] != 'MCP'
12
15
 
13
16
  resource_uri = parts[1]
14
17
  handler_definition = parts[2]
15
18
 
16
- unless resource_uri && handler_definition
17
- raise ArgumentError, "Invalid MCP route format: #{definition}"
18
- end
19
+ raise ArgumentError, "Invalid MCP route format: #{definition}" unless resource_uri && handler_definition
19
20
 
20
21
  # Clean up URI - remove leading slash if present since MCP URIs are relative
21
22
  resource_uri = resource_uri.sub(%r{^/}, '')
@@ -33,16 +34,12 @@ class Otto
33
34
  # Note: The path parameter is ignored for TOOL routes - tool_name comes from definition
34
35
  parts = definition.split(/\s+/, 3)
35
36
 
36
- if parts[0] != 'TOOL'
37
- raise ArgumentError, "Expected TOOL keyword, got: #{parts[0]}"
38
- end
37
+ raise ArgumentError, "Expected TOOL keyword, got: #{parts[0]}" if parts[0] != 'TOOL'
39
38
 
40
39
  tool_name = parts[1]
41
40
  handler_definition = parts[2]
42
41
 
43
- unless tool_name && handler_definition
44
- raise ArgumentError, "Invalid TOOL route format: #{definition}"
45
- end
42
+ raise ArgumentError, "Invalid TOOL route format: #{definition}" unless tool_name && handler_definition
46
43
 
47
44
  # Clean up tool name - remove leading slash if present
48
45
  tool_name = tool_name.sub(%r{^/}, '')
@@ -70,9 +67,7 @@ class Otto
70
67
  # First part is the handler class.method
71
68
  parts[1..-1]&.each do |part|
72
69
  key, value = part.split('=', 2)
73
- if key && value
74
- options[key.to_sym] = value
75
- end
70
+ options[key.to_sym] = value if key && value
76
71
  end
77
72
 
78
73
  options
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/otto/mcp/server.rb
4
+
1
5
  require_relative 'protocol'
2
6
  require_relative 'registry'
3
7
  require_relative 'route_parser'
@@ -7,6 +11,7 @@ require_relative 'rate_limiting'
7
11
 
8
12
  class Otto
9
13
  module MCP
14
+ # MCP server implementation providing Model Context Protocol endpoints
10
15
  class Server
11
16
  attr_reader :protocol, :otto_instance
12
17
 
@@ -68,10 +73,10 @@ class Otto
68
73
  end
69
74
 
70
75
  # Configure validation last (most expensive)
71
- if @enable_validation
72
- @otto_instance.use Otto::MCP::ValidationMiddleware
73
- Otto.logger.debug '[MCP] Request validation enabled' if Otto.debug
74
- end
76
+ return unless @enable_validation
77
+
78
+ @otto_instance.use Otto::MCP::ValidationMiddleware
79
+ Otto.logger.debug '[MCP] Request validation enabled' if Otto.debug
75
80
  end
76
81
 
77
82
  def add_mcp_endpoint_route
@@ -106,11 +111,16 @@ class Otto
106
111
 
107
112
  # Create resource handler
108
113
  handler = lambda do
109
- klass = Object.const_get(klass_name)
110
- klass.public_send(method_name)
111
- rescue StandardError => ex
112
- Otto.logger.error "[MCP] Resource handler error for #{uri}: #{ex.message}"
113
- raise
114
+ klass = Object.const_get(klass_name)
115
+ method = klass.method(method_name)
116
+ if method.arity != 0
117
+ raise ArgumentError, "Handler #{klass_name}.#{method_name} must be a zero-arity method for resource #{uri}"
118
+ end
119
+
120
+ klass.public_send(method_name)
121
+ rescue StandardError => e
122
+ Otto.logger.error "[MCP] Resource handler error for #{uri}: #{e.message}"
123
+ raise
114
124
  end
115
125
 
116
126
  # Register with protocol registry
@@ -119,7 +129,7 @@ class Otto
119
129
  extract_name_from_uri(uri),
120
130
  "Resource: #{uri}",
121
131
  'text/plain',
122
- handler,
132
+ handler
123
133
  )
124
134
 
125
135
  Otto.logger.debug "[MCP] Registered resource: #{uri} -> #{handler_def}" if Otto.debug
@@ -146,7 +156,7 @@ class Otto
146
156
  name,
147
157
  "Tool: #{name}",
148
158
  input_schema,
149
- "#{klass_name}.#{method_name}",
159
+ "#{klass_name}.#{method_name}"
150
160
  )
151
161
 
152
162
  Otto.logger.debug "[MCP] Registered tool: #{name} -> #{handler_def}" if Otto.debug
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/otto/mcp/validation.rb
4
+
1
5
  require 'json'
2
6
 
3
7
  begin
@@ -10,6 +14,7 @@ class Otto
10
14
  module MCP
11
15
  class ValidationError < StandardError; end
12
16
 
17
+ # JSON Schema validator for MCP protocol requests
13
18
  class Validator
14
19
  def initialize
15
20
  @schemas = {}
@@ -48,7 +53,7 @@ class Otto
48
53
 
49
54
  def mcp_request_schema
50
55
  @schemas[:mcp_request] ||= JSONSchemer.schema({
51
- type: 'object',
56
+ type: 'object',
52
57
  required: %w[jsonrpc method id],
53
58
  properties: {
54
59
  jsonrpc: { const: '2.0' },
@@ -57,11 +62,11 @@ class Otto
57
62
  params: { type: 'object' },
58
63
  },
59
64
  additionalProperties: false,
60
- },
61
- )
65
+ })
62
66
  end
63
67
  end
64
68
 
69
+ # Middleware for validating MCP protocol requests using JSON schema
65
70
  class ValidationMiddleware
66
71
  def initialize(app, _security_config = nil)
67
72
  @app = app
@@ -82,10 +87,10 @@ class Otto
82
87
 
83
88
  # Reset body for downstream middleware
84
89
  request.body.rewind if request.body.respond_to?(:rewind)
85
- rescue JSON::ParserError => ex
86
- return validation_error_response(nil, "Invalid JSON: #{ex.message}")
87
- rescue ValidationError => ex
88
- return validation_error_response(data&.dig('id'), ex.message)
90
+ rescue JSON::ParserError => e
91
+ return validation_error_response(nil, "Invalid JSON: #{e.message}")
92
+ rescue ValidationError => e
93
+ return validation_error_response(data&.dig('id'), e.message)
89
94
  end
90
95
  end
91
96
 
@@ -102,15 +107,14 @@ class Otto
102
107
 
103
108
  def validation_error_response(id, message)
104
109
  body = JSON.generate({
105
- jsonrpc: '2.0',
110
+ jsonrpc: '2.0',
106
111
  id: id,
107
112
  error: {
108
113
  code: -32_600,
109
114
  message: 'Invalid Request',
110
115
  data: message,
111
116
  },
112
- },
113
- )
117
+ })
114
118
 
115
119
  [400, { 'content-type' => 'application/json' }, [body]]
116
120
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'json'
5
+ require_relative 'redirect'
6
+ require_relative 'view'
7
+ require_relative 'default'
8
+
9
+ class Otto
10
+ module ResponseHandlers
11
+ # Auto-detection handler that chooses appropriate handler based on context
12
+ class AutoHandler < BaseHandler
13
+ def self.handle(result, response, context = {})
14
+ # Auto-detect based on result type and request context
15
+ handler_class = detect_handler_type(result, response, context)
16
+ handler_class.handle(result, response, context)
17
+ end
18
+
19
+ def self.detect_handler_type(result, response, context)
20
+ # Check if response type was already set by the handler
21
+ content_type = response['Content-Type']
22
+
23
+ if content_type&.include?('application/json')
24
+ JSONHandler
25
+ elsif (context[:logic_instance]&.respond_to?(:redirect_path) && context[:logic_instance].redirect_path) ||
26
+ (result.is_a?(String) && result.match?(%r{^/}))
27
+ # Logic instance has redirect path or result is a string path
28
+ RedirectHandler
29
+ elsif result.is_a?(Hash)
30
+ JSONHandler
31
+ elsif context[:logic_instance]&.respond_to?(:view)
32
+ ViewHandler
33
+ else
34
+ DefaultHandler
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Otto
4
+ module ResponseHandlers
5
+ # Base response handler class
6
+ class BaseHandler
7
+ def self.handle(result, response, context = {})
8
+ raise NotImplementedError, 'Subclasses must implement handle method'
9
+ end
10
+
11
+ def self.ensure_status_set(response, default_status = 200)
12
+ response.status = default_status unless response.status && response.status != 0
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ class Otto
6
+ module ResponseHandlers
7
+ # Default handler that preserves existing Otto behavior
8
+ class DefaultHandler < BaseHandler
9
+ def self.handle(_result, response, _context = {})
10
+ # Otto's default behavior - let the route handler manage the response
11
+ # This handler does nothing, preserving existing behavior
12
+ ensure_status_set(response, 200)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'json'
4
+ require_relative 'redirect'
5
+ require_relative 'view'
6
+ require_relative 'auto'
7
+ require_relative 'default'
8
+
9
+ class Otto
10
+ module ResponseHandlers
11
+ # Factory for creating response handlers
12
+ class HandlerFactory
13
+ # Map of response type names to handler classes
14
+ HANDLER_MAP = {
15
+ 'json' => JSONHandler,
16
+ 'redirect' => RedirectHandler,
17
+ 'view' => ViewHandler,
18
+ 'auto' => AutoHandler,
19
+ 'default' => DefaultHandler,
20
+ }.freeze
21
+
22
+ def self.create_handler(response_type)
23
+ handler_class = HANDLER_MAP[response_type.to_s.downcase]
24
+
25
+ unless handler_class
26
+ Otto.logger.warn "Unknown response type: #{response_type}, falling back to default" if Otto.debug
27
+ handler_class = DefaultHandler
28
+ end
29
+
30
+ handler_class
31
+ end
32
+
33
+ def self.handle_response(result, response, response_type, context = {})
34
+ handler = create_handler(response_type)
35
+ handler.handle(result, response, context)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ class Otto
6
+ module ResponseHandlers
7
+ # Handler for JSON responses
8
+ class JSONHandler < BaseHandler
9
+ def self.handle(result, response, context = {})
10
+ response['Content-Type'] = 'application/json'
11
+
12
+ # Determine the data to serialize
13
+ data = if context[:logic_instance]&.respond_to?(:response_data)
14
+ context[:logic_instance].response_data
15
+ elsif result.is_a?(Hash)
16
+ result
17
+ elsif result.nil?
18
+ { success: true }
19
+ else
20
+ { success: true, data: result }
21
+ end
22
+
23
+ response.body = [JSON.generate(data)]
24
+ ensure_status_set(response, context[:status_code] || 200)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ class Otto
6
+ module ResponseHandlers
7
+ # Handler for redirect responses
8
+ class RedirectHandler < BaseHandler
9
+ def self.handle(result, response, context = {})
10
+ # Determine redirect path
11
+ path = if context[:redirect_path]
12
+ context[:redirect_path]
13
+ elsif context[:logic_instance]&.respond_to?(:redirect_path)
14
+ context[:logic_instance].redirect_path
15
+ elsif result.is_a?(String)
16
+ result
17
+ else
18
+ '/'
19
+ end
20
+
21
+ response.redirect(path)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ class Otto
6
+ module ResponseHandlers
7
+ # Handler for view/template responses
8
+ class ViewHandler < BaseHandler
9
+ def self.handle(result, response, context = {})
10
+ if context[:logic_instance]&.respond_to?(:view)
11
+ response.body = [context[:logic_instance].view.render]
12
+ response['Content-Type'] = 'text/html' unless response['Content-Type']
13
+ elsif result.respond_to?(:to_s)
14
+ response.body = [result.to_s]
15
+ response['Content-Type'] = 'text/html' unless response['Content-Type']
16
+ else
17
+ response.body = ['']
18
+ end
19
+
20
+ ensure_status_set(response, context[:status_code] || 200)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,141 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # lib/otto/response_handlers.rb
2
4
 
3
5
  class Otto
4
6
  module ResponseHandlers
5
- # Base response handler class
6
- class BaseHandler
7
- def self.handle(result, response, context = {})
8
- raise NotImplementedError, "Subclasses must implement handle method"
9
- end
10
-
11
- protected
12
-
13
- def self.ensure_status_set(response, default_status = 200)
14
- response.status = default_status unless response.status && response.status != 0
15
- end
16
- end
17
-
18
- # Handler for JSON responses
19
- class JSONHandler < BaseHandler
20
- def self.handle(result, response, context = {})
21
- response['Content-Type'] = 'application/json'
22
-
23
- # Determine the data to serialize
24
- data = if context[:logic_instance]&.respond_to?(:response_data)
25
- context[:logic_instance].response_data
26
- elsif result.is_a?(Hash)
27
- result
28
- elsif result.nil?
29
- { success: true }
30
- else
31
- { success: true, data: result }
32
- end
33
-
34
- response.body = [JSON.generate(data)]
35
- ensure_status_set(response, context[:status_code] || 200)
36
- end
37
- end
38
-
39
- # Handler for redirect responses
40
- class RedirectHandler < BaseHandler
41
- def self.handle(result, response, context = {})
42
- # Determine redirect path
43
- path = if context[:redirect_path]
44
- context[:redirect_path]
45
- elsif context[:logic_instance]&.respond_to?(:redirect_path)
46
- context[:logic_instance].redirect_path
47
- elsif result.is_a?(String)
48
- result
49
- else
50
- '/'
51
- end
52
-
53
- response.redirect(path)
54
- end
55
- end
56
-
57
- # Handler for view/template responses
58
- class ViewHandler < BaseHandler
59
- def self.handle(result, response, context = {})
60
- if context[:logic_instance]&.respond_to?(:view)
61
- response.body = [context[:logic_instance].view.render]
62
- response['Content-Type'] = 'text/html' unless response['Content-Type']
63
- elsif result.respond_to?(:to_s)
64
- response.body = [result.to_s]
65
- response['Content-Type'] = 'text/html' unless response['Content-Type']
66
- else
67
- response.body = ['']
68
- end
69
-
70
- ensure_status_set(response, context[:status_code] || 200)
71
- end
72
- end
73
-
74
- # Default handler that preserves existing Otto behavior
75
- class DefaultHandler < BaseHandler
76
- def self.handle(result, response, context = {})
77
- # Otto's default behavior - let the route handler manage the response
78
- # This handler does nothing, preserving existing behavior
79
- ensure_status_set(response, 200)
80
- end
81
- end
82
-
83
- # Auto-detection handler that chooses appropriate handler based on context
84
- class AutoHandler < BaseHandler
85
- def self.handle(result, response, context = {})
86
- # Auto-detect based on result type and request context
87
- handler_class = detect_handler_type(result, response, context)
88
- handler_class.handle(result, response, context)
89
- end
90
-
91
- private
92
-
93
- def self.detect_handler_type(result, response, context)
94
- # Check if response type was already set by the handler
95
- content_type = response['Content-Type']
96
-
97
- if content_type&.include?('application/json')
98
- JSONHandler
99
- elsif (context[:logic_instance]&.respond_to?(:redirect_path) && context[:logic_instance]&.redirect_path) ||
100
- (result.is_a?(String) && result.match?(%r{^/}))
101
- # Logic instance has redirect path or result is a string path
102
- RedirectHandler
103
- elsif result.is_a?(Hash)
104
- JSONHandler
105
- elsif context[:logic_instance]&.respond_to?(:view)
106
- ViewHandler
107
- else
108
- DefaultHandler
109
- end
110
- end
111
- end
112
-
113
- # Factory for creating response handlers
114
- class HandlerFactory
115
- # Map of response type names to handler classes
116
- HANDLER_MAP = {
117
- 'json' => JSONHandler,
118
- 'redirect' => RedirectHandler,
119
- 'view' => ViewHandler,
120
- 'auto' => AutoHandler,
121
- 'default' => DefaultHandler
122
- }.freeze
123
-
124
- def self.create_handler(response_type)
125
- handler_class = HANDLER_MAP[response_type.to_s.downcase]
126
-
127
- unless handler_class
128
- Otto.logger.warn "Unknown response type: #{response_type}, falling back to default" if Otto.debug
129
- handler_class = DefaultHandler
130
- end
131
-
132
- handler_class
133
- end
134
-
135
- def self.handle_response(result, response, response_type, context = {})
136
- handler = create_handler(response_type)
137
- handler.handle(result, response, context)
138
- end
139
- end
7
+ require_relative 'response_handlers/base'
8
+ require_relative 'response_handlers/json'
9
+ require_relative 'response_handlers/redirect'
10
+ require_relative 'response_handlers/view'
11
+ require_relative 'response_handlers/default'
12
+ require_relative 'response_handlers/auto'
13
+ require_relative 'response_handlers/factory'
140
14
  end
141
15
  end
data/lib/otto/route.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # lib/otto/route.rb
2
4
 
3
5
  class Otto
@@ -20,6 +22,7 @@ class Otto
20
22
  #
21
23
  #
22
24
  class Route
25
+ # Class methods for Route providing Otto instance access
23
26
  module ClassMethods
24
27
  attr_accessor :otto
25
28
  end
@@ -86,7 +89,6 @@ class Otto
86
89
 
87
90
  private
88
91
 
89
-
90
92
  # Safely resolve a class name using Object.const_get with security validations
91
93
  # This replaces the previous eval() usage to prevent code injection attacks.
92
94
  #
@@ -120,8 +122,8 @@ class Otto
120
122
 
121
123
  begin
122
124
  Object.const_get(class_name)
123
- rescue NameError => ex
124
- raise ArgumentError, "Class not found: #{class_name} - #{ex.message}"
125
+ rescue NameError => e
126
+ raise ArgumentError, "Class not found: #{class_name} - #{e.message}"
125
127
  end
126
128
  end
127
129
 
@@ -148,12 +150,10 @@ class Otto
148
150
  res = Rack::Response.new
149
151
  req.extend Otto::RequestHelpers
150
152
  res.extend Otto::ResponseHelpers
151
- res.request = req
153
+ res.request = req
152
154
 
153
155
  # Make security config available to response helpers
154
- if otto.respond_to?(:security_config) && otto.security_config
155
- env['otto.security_config'] = otto.security_config
156
- end
156
+ env['otto.security_config'] = otto.security_config if otto.respond_to?(:security_config) && otto.security_config
157
157
 
158
158
  # NEW: Make route definition and options available to middleware and handlers
159
159
  env['otto.route_definition'] = @route_definition
@@ -185,7 +185,7 @@ class Otto
185
185
  # This replaces the hardcoded execution pattern with a factory approach
186
186
  if otto&.route_handler_factory
187
187
  handler = otto.route_handler_factory.create_handler(@route_definition, otto)
188
- return handler.call(env, extra_params)
188
+ handler.call(env, extra_params)
189
189
  else
190
190
  # Fallback to legacy behavior for backward compatibility
191
191
  inst = nil
@@ -205,7 +205,7 @@ class Otto
205
205
  context = {
206
206
  logic_instance: (kind == :instance ? inst : nil),
207
207
  status_code: nil,
208
- redirect_path: nil
208
+ redirect_path: nil,
209
209
  }
210
210
 
211
211
  Otto::ResponseHandlers::HandlerFactory.handle_response(result, res, response_type, context)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # lib/otto/route_definition.rb
2
4
 
3
5
  class Otto
@@ -161,24 +163,19 @@ class Otto
161
163
  # @param target [String] The target definition (e.g., "TestApp.index")
162
164
  # @return [Hash] Hash with :klass_name, :method_name, and :kind
163
165
  def parse_target(target)
164
- if target.include?('.')
165
- klass_name, method_name = target.split('.', 2)
166
- { klass_name: klass_name, method_name: method_name, kind: :class }
167
- elsif target.include?('#')
168
- klass_name, method_name = target.split('#', 2)
169
- { klass_name: klass_name, method_name: method_name, kind: :instance }
170
- elsif target.match?(/\A[A-Z][a-zA-Z0-9_]*(?:::[A-Z][a-zA-Z0-9_]*)*\z/)
171
- # Namespaced class with implicit method name (class method with same name as class)
172
- # E.g., "V2::Logic::Admin::Panel" -> Panel.Panel (class method)
173
- # For single word classes like "Logic", it's truly a logic class
174
- method_name = target.split('::').last
175
- if target.include?('::')
176
- # Namespaced class - treat as class method with implicit method name
177
- { klass_name: target, method_name: method_name, kind: :class }
178
- else
179
- # Single word class - treat as logic class
180
- { klass_name: target, method_name: method_name, kind: :logic }
181
- end
166
+ case target
167
+ when /^(.+)\.(.+)$/
168
+ # Class.method - call class method directly
169
+ { klass_name: $1, method_name: $2, kind: :class }
170
+
171
+ when /^(.+)#(.+)$/
172
+ # Class#method - instantiate then call instance method
173
+ { klass_name: $1, method_name: $2, kind: :instance }
174
+
175
+ when /^[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*$/
176
+ # Bare class name - instantiate the class
177
+ { klass_name: target, method_name: target.split('::').last, kind: :logic }
178
+
182
179
  else
183
180
  raise ArgumentError, "Invalid target format: #{target}"
184
181
  end