otto 1.5.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +44 -5
- data/.github/workflows/claude-code-review.yml +53 -0
- data/.github/workflows/claude.yml +49 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +24 -345
- data/CHANGELOG.rst +83 -0
- data/CLAUDE.md +56 -0
- data/Gemfile +21 -5
- data/Gemfile.lock +69 -31
- data/README.md +2 -0
- data/bin/rspec +16 -0
- data/changelog.d/20250911_235619_delano_next.rst +28 -0
- data/changelog.d/20250912_123055_delano_remove_ostruct.rst +21 -0
- data/changelog.d/20250912_175625_claude_delano_remove_ostruct.rst +21 -0
- data/changelog.d/README.md +120 -0
- data/changelog.d/scriv.ini +5 -0
- data/docs/.gitignore +1 -0
- data/docs/migrating/v2.0.0-pre1.md +276 -0
- data/examples/.gitignore +1 -0
- data/examples/advanced_routes/README.md +33 -0
- data/examples/advanced_routes/app/controllers/handlers/async.rb +9 -0
- data/examples/advanced_routes/app/controllers/handlers/dynamic.rb +9 -0
- data/examples/advanced_routes/app/controllers/handlers/static.rb +9 -0
- data/examples/advanced_routes/app/controllers/modules/auth.rb +9 -0
- data/examples/advanced_routes/app/controllers/modules/transformer.rb +9 -0
- data/examples/advanced_routes/app/controllers/modules/validator.rb +9 -0
- data/examples/advanced_routes/app/controllers/routes_app.rb +232 -0
- data/examples/advanced_routes/app/controllers/v2/admin.rb +9 -0
- data/examples/advanced_routes/app/controllers/v2/config.rb +9 -0
- data/examples/advanced_routes/app/controllers/v2/settings.rb +9 -0
- data/examples/advanced_routes/app/logic/admin/logic/manager.rb +27 -0
- data/examples/advanced_routes/app/logic/admin/panel.rb +27 -0
- data/examples/advanced_routes/app/logic/analytics_processor.rb +25 -0
- data/examples/advanced_routes/app/logic/complex/business/handler.rb +27 -0
- data/examples/advanced_routes/app/logic/data_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/data_processor.rb +25 -0
- data/examples/advanced_routes/app/logic/input_validator.rb +24 -0
- data/examples/advanced_routes/app/logic/nested/feature/logic.rb +27 -0
- data/examples/advanced_routes/app/logic/reports_generator.rb +27 -0
- data/examples/advanced_routes/app/logic/simple_logic.rb +25 -0
- data/examples/advanced_routes/app/logic/system/config/manager.rb +27 -0
- data/examples/advanced_routes/app/logic/test_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/transform_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/upload_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/v2/logic/dashboard.rb +27 -0
- data/examples/advanced_routes/app/logic/v2/logic/processor.rb +27 -0
- data/examples/advanced_routes/app.rb +33 -0
- data/examples/advanced_routes/config.rb +23 -0
- data/examples/advanced_routes/config.ru +7 -0
- data/examples/advanced_routes/puma.rb +20 -0
- data/examples/advanced_routes/routes +167 -0
- data/examples/advanced_routes/run.rb +39 -0
- data/examples/advanced_routes/test.rb +58 -0
- data/examples/authentication_strategies/README.md +32 -0
- data/examples/authentication_strategies/app/auth.rb +68 -0
- data/examples/authentication_strategies/app/controllers/auth_controller.rb +29 -0
- data/examples/authentication_strategies/app/controllers/main_controller.rb +28 -0
- data/examples/authentication_strategies/config.ru +24 -0
- data/examples/authentication_strategies/routes +37 -0
- data/examples/basic/README.md +29 -0
- data/examples/basic/app.rb +7 -35
- data/examples/basic/routes +0 -9
- data/examples/mcp_demo/README.md +87 -0
- data/examples/mcp_demo/app.rb +51 -0
- data/examples/mcp_demo/config.ru +17 -0
- data/examples/mcp_demo/routes +9 -0
- data/examples/security_features/README.md +46 -0
- data/examples/security_features/app.rb +23 -24
- data/examples/security_features/config.ru +8 -10
- data/lib/otto/core/configuration.rb +167 -0
- data/lib/otto/core/error_handler.rb +86 -0
- data/lib/otto/core/file_safety.rb +61 -0
- data/lib/otto/core/middleware_stack.rb +157 -0
- data/lib/otto/core/router.rb +183 -0
- data/lib/otto/core/uri_generator.rb +44 -0
- data/lib/otto/design_system.rb +7 -5
- data/lib/otto/helpers/base.rb +3 -0
- data/lib/otto/helpers/request.rb +10 -8
- data/lib/otto/helpers/response.rb +5 -4
- data/lib/otto/helpers/validation.rb +85 -0
- data/lib/otto/mcp/auth/token.rb +77 -0
- data/lib/otto/mcp/protocol.rb +164 -0
- data/lib/otto/mcp/rate_limiting.rb +155 -0
- data/lib/otto/mcp/registry.rb +100 -0
- data/lib/otto/mcp/route_parser.rb +77 -0
- data/lib/otto/mcp/server.rb +206 -0
- data/lib/otto/mcp/validation.rb +123 -0
- data/lib/otto/response_handlers/auto.rb +39 -0
- data/lib/otto/response_handlers/base.rb +16 -0
- data/lib/otto/response_handlers/default.rb +16 -0
- data/lib/otto/response_handlers/factory.rb +39 -0
- data/lib/otto/response_handlers/json.rb +28 -0
- data/lib/otto/response_handlers/redirect.rb +25 -0
- data/lib/otto/response_handlers/view.rb +24 -0
- data/lib/otto/response_handlers.rb +9 -135
- data/lib/otto/route.rb +9 -9
- data/lib/otto/route_definition.rb +30 -33
- data/lib/otto/route_handlers/base.rb +121 -0
- data/lib/otto/route_handlers/class_method.rb +89 -0
- data/lib/otto/route_handlers/factory.rb +29 -0
- data/lib/otto/route_handlers/instance_method.rb +69 -0
- data/lib/otto/route_handlers/lambda.rb +59 -0
- data/lib/otto/route_handlers/logic_class.rb +93 -0
- data/lib/otto/route_handlers.rb +10 -376
- data/lib/otto/security/authentication/auth_strategy.rb +44 -0
- data/lib/otto/security/authentication/authentication_middleware.rb +123 -0
- data/lib/otto/security/authentication/failure_result.rb +36 -0
- data/lib/otto/security/authentication/strategies/api_key_strategy.rb +40 -0
- data/lib/otto/security/authentication/strategies/permission_strategy.rb +47 -0
- data/lib/otto/security/authentication/strategies/public_strategy.rb +19 -0
- data/lib/otto/security/authentication/strategies/role_strategy.rb +57 -0
- data/lib/otto/security/authentication/strategies/session_strategy.rb +41 -0
- data/lib/otto/security/authentication/strategy_result.rb +223 -0
- data/lib/otto/security/authentication.rb +28 -282
- data/lib/otto/security/config.rb +15 -11
- data/lib/otto/security/configurator.rb +219 -0
- data/lib/otto/security/csrf.rb +8 -143
- data/lib/otto/security/middleware/csrf_middleware.rb +151 -0
- data/lib/otto/security/middleware/rate_limit_middleware.rb +38 -0
- data/lib/otto/security/middleware/validation_middleware.rb +252 -0
- data/lib/otto/security/rate_limiter.rb +86 -0
- data/lib/otto/security/rate_limiting.rb +16 -0
- data/lib/otto/security/validator.rb +8 -292
- data/lib/otto/static.rb +3 -0
- data/lib/otto/utils.rb +14 -0
- data/lib/otto/version.rb +3 -1
- data/lib/otto.rb +184 -414
- data/otto.gemspec +11 -6
- metadata +134 -25
- data/examples/dynamic_pages/app.rb +0 -115
- data/examples/dynamic_pages/config.ru +0 -30
- data/examples/dynamic_pages/routes +0 -21
- data/examples/helpers_demo/app.rb +0 -244
- data/examples/helpers_demo/config.ru +0 -26
- data/examples/helpers_demo/routes +0 -7
@@ -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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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 =>
|
124
|
-
raise ArgumentError, "Class not found: #{class_name} - #{
|
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
|
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
|
-
|
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
|
@@ -35,22 +37,22 @@ class Otto
|
|
35
37
|
attr_reader :keys
|
36
38
|
|
37
39
|
def initialize(verb, path, definition, pattern: nil, keys: nil)
|
38
|
-
@verb
|
39
|
-
@path
|
40
|
+
@verb = verb.to_s.upcase.to_sym
|
41
|
+
@path = path
|
40
42
|
@definition = definition
|
41
|
-
@pattern
|
42
|
-
@keys
|
43
|
+
@pattern = pattern
|
44
|
+
@keys = keys || []
|
43
45
|
|
44
46
|
# Parse the definition into target and options
|
45
|
-
parsed
|
46
|
-
@target
|
47
|
+
parsed = parse_definition(definition)
|
48
|
+
@target = parsed[:target]
|
47
49
|
@options = parsed[:options].freeze
|
48
50
|
|
49
51
|
# Parse the target into class, method, and kind
|
50
52
|
target_parsed = parse_target(@target)
|
51
|
-
@klass_name
|
52
|
-
@method_name
|
53
|
-
@kind
|
53
|
+
@klass_name = target_parsed[:klass_name]
|
54
|
+
@method_name = target_parsed[:method_name]
|
55
|
+
@kind = target_parsed[:kind]
|
54
56
|
|
55
57
|
# Freeze for immutability
|
56
58
|
freeze
|
@@ -118,7 +120,7 @@ class Otto
|
|
118
120
|
kind: @kind,
|
119
121
|
options: @options,
|
120
122
|
pattern: @pattern,
|
121
|
-
keys: @keys
|
123
|
+
keys: @keys,
|
122
124
|
}
|
123
125
|
end
|
124
126
|
|
@@ -131,7 +133,7 @@ class Otto
|
|
131
133
|
# Detailed inspection
|
132
134
|
# @return [String]
|
133
135
|
def inspect
|
134
|
-
"#<Otto::RouteDefinition #{
|
136
|
+
"#<Otto::RouteDefinition #{self} options=#{@options.inspect}>"
|
135
137
|
end
|
136
138
|
|
137
139
|
private
|
@@ -140,17 +142,17 @@ class Otto
|
|
140
142
|
# @param definition [String] The route definition
|
141
143
|
# @return [Hash] Hash with :target and :options keys
|
142
144
|
def parse_definition(definition)
|
143
|
-
parts
|
144
|
-
target
|
145
|
+
parts = definition.split(/\s+/)
|
146
|
+
target = parts.shift
|
145
147
|
options = {}
|
146
148
|
|
147
149
|
parts.each do |part|
|
148
150
|
key, value = part.split('=', 2)
|
149
151
|
if key && value
|
150
152
|
options[key.to_sym] = value
|
151
|
-
|
153
|
+
elsif Otto.debug
|
152
154
|
# Malformed parameter, log warning if debug enabled
|
153
|
-
Otto.logger.warn "Ignoring malformed route parameter: #{part}"
|
155
|
+
Otto.logger.warn "Ignoring malformed route parameter: #{part}"
|
154
156
|
end
|
155
157
|
end
|
156
158
|
|
@@ -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
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# lib/otto/route_handlers/base.rb
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
class Otto
|
7
|
+
module RouteHandlers
|
8
|
+
# Base class for all route handlers
|
9
|
+
# Provides common functionality and interface
|
10
|
+
class BaseHandler
|
11
|
+
attr_reader :route_definition, :otto_instance
|
12
|
+
|
13
|
+
def initialize(route_definition, otto_instance = nil)
|
14
|
+
@route_definition = route_definition
|
15
|
+
@otto_instance = otto_instance
|
16
|
+
end
|
17
|
+
|
18
|
+
# Execute the route handler
|
19
|
+
# @param env [Hash] Rack environment
|
20
|
+
# @param extra_params [Hash] Additional parameters
|
21
|
+
# @return [Array] Rack response array
|
22
|
+
def call(env, extra_params = {})
|
23
|
+
raise NotImplementedError, 'Subclasses must implement #call'
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
# Get the target class, loading it safely
|
29
|
+
# @return [Class] The target class
|
30
|
+
def target_class
|
31
|
+
@target_class ||= safe_const_get(route_definition.klass_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Setup request and response with the same extensions and processing as Route#call
|
35
|
+
# @param req [Rack::Request] Request object
|
36
|
+
# @param res [Rack::Response] Response object
|
37
|
+
# @param env [Hash] Rack environment
|
38
|
+
# @param extra_params [Hash] Additional parameters
|
39
|
+
def setup_request_response(req, res, env, extra_params)
|
40
|
+
# Apply the same extensions as original Route#call
|
41
|
+
req.extend Otto::RequestHelpers
|
42
|
+
res.extend Otto::ResponseHelpers
|
43
|
+
res.request = req
|
44
|
+
|
45
|
+
# Make security config available to response helpers
|
46
|
+
if otto_instance.respond_to?(:security_config) && otto_instance.security_config
|
47
|
+
env['otto.security_config'] = otto_instance.security_config
|
48
|
+
end
|
49
|
+
|
50
|
+
# Make route definition and options available to middleware and handlers
|
51
|
+
env['otto.route_definition'] = route_definition
|
52
|
+
env['otto.route_options'] = route_definition.options
|
53
|
+
|
54
|
+
# Process parameters through security layer
|
55
|
+
req.params.merge! extra_params
|
56
|
+
req.params.replace Otto::Static.indifferent_params(req.params)
|
57
|
+
|
58
|
+
# Add security headers
|
59
|
+
if otto_instance.respond_to?(:security_config) && otto_instance.security_config
|
60
|
+
otto_instance.security_config.security_headers.each do |header, value|
|
61
|
+
res.headers[header] = value
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Setup class extensions if target_class is available
|
66
|
+
if target_class
|
67
|
+
target_class.extend Otto::Route::ClassMethods
|
68
|
+
target_class.otto = otto_instance if otto_instance
|
69
|
+
end
|
70
|
+
|
71
|
+
# Add security helpers if CSRF is enabled
|
72
|
+
if otto_instance.respond_to?(:security_config) && otto_instance.security_config&.csrf_enabled?
|
73
|
+
res.extend Otto::Security::CSRFHelpers
|
74
|
+
end
|
75
|
+
|
76
|
+
# Add validation helpers
|
77
|
+
res.extend Otto::Security::ValidationHelpers
|
78
|
+
end
|
79
|
+
|
80
|
+
# Finalize response with the same processing as Route#call
|
81
|
+
# @param res [Rack::Response] Response object
|
82
|
+
# @return [Array] Rack response array
|
83
|
+
def finalize_response(res)
|
84
|
+
res.body = [res.body] unless res.body.respond_to?(:each)
|
85
|
+
res.finish
|
86
|
+
end
|
87
|
+
|
88
|
+
# Handle response using appropriate response handler
|
89
|
+
# @param result [Object] Result from route execution
|
90
|
+
# @param response [Rack::Response] Response object
|
91
|
+
# @param context [Hash] Additional context for response handling
|
92
|
+
def handle_response(result, response, context = {})
|
93
|
+
response_type = route_definition.response_type
|
94
|
+
|
95
|
+
# Get the appropriate response handler
|
96
|
+
handler_class = case response_type
|
97
|
+
in 'json' then Otto::ResponseHandlers::JSONHandler
|
98
|
+
in 'redirect' then Otto::ResponseHandlers::RedirectHandler
|
99
|
+
in 'view' then Otto::ResponseHandlers::ViewHandler
|
100
|
+
in 'auto' then Otto::ResponseHandlers::AutoHandler
|
101
|
+
else Otto::ResponseHandlers::DefaultHandler
|
102
|
+
end
|
103
|
+
|
104
|
+
handler_class.handle(result, response, context)
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
# Safely get a constant from a string name
|
110
|
+
# @param name [String] Class name
|
111
|
+
# @return [Class] The class
|
112
|
+
def safe_const_get(name)
|
113
|
+
name.split('::').inject(Object) do |scope, const_name|
|
114
|
+
scope.const_get(const_name)
|
115
|
+
end
|
116
|
+
rescue NameError => e
|
117
|
+
raise NameError, "Unknown class: #{name} (#{e})"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|