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.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +44 -5
  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 +21 -5
  10. data/Gemfile.lock +69 -31
  11. data/README.md +2 -0
  12. data/bin/rspec +16 -0
  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 +51 -0
  66. data/examples/mcp_demo/config.ru +17 -0
  67. data/examples/mcp_demo/routes +9 -0
  68. data/examples/security_features/README.md +46 -0
  69. data/examples/security_features/app.rb +23 -24
  70. data/examples/security_features/config.ru +8 -10
  71. data/lib/otto/core/configuration.rb +167 -0
  72. data/lib/otto/core/error_handler.rb +86 -0
  73. data/lib/otto/core/file_safety.rb +61 -0
  74. data/lib/otto/core/middleware_stack.rb +157 -0
  75. data/lib/otto/core/router.rb +183 -0
  76. data/lib/otto/core/uri_generator.rb +44 -0
  77. data/lib/otto/design_system.rb +7 -5
  78. data/lib/otto/helpers/base.rb +3 -0
  79. data/lib/otto/helpers/request.rb +10 -8
  80. data/lib/otto/helpers/response.rb +5 -4
  81. data/lib/otto/helpers/validation.rb +85 -0
  82. data/lib/otto/mcp/auth/token.rb +77 -0
  83. data/lib/otto/mcp/protocol.rb +164 -0
  84. data/lib/otto/mcp/rate_limiting.rb +155 -0
  85. data/lib/otto/mcp/registry.rb +100 -0
  86. data/lib/otto/mcp/route_parser.rb +77 -0
  87. data/lib/otto/mcp/server.rb +206 -0
  88. data/lib/otto/mcp/validation.rb +123 -0
  89. data/lib/otto/response_handlers/auto.rb +39 -0
  90. data/lib/otto/response_handlers/base.rb +16 -0
  91. data/lib/otto/response_handlers/default.rb +16 -0
  92. data/lib/otto/response_handlers/factory.rb +39 -0
  93. data/lib/otto/response_handlers/json.rb +28 -0
  94. data/lib/otto/response_handlers/redirect.rb +25 -0
  95. data/lib/otto/response_handlers/view.rb +24 -0
  96. data/lib/otto/response_handlers.rb +9 -135
  97. data/lib/otto/route.rb +9 -9
  98. data/lib/otto/route_definition.rb +30 -33
  99. data/lib/otto/route_handlers/base.rb +121 -0
  100. data/lib/otto/route_handlers/class_method.rb +89 -0
  101. data/lib/otto/route_handlers/factory.rb +29 -0
  102. data/lib/otto/route_handlers/instance_method.rb +69 -0
  103. data/lib/otto/route_handlers/lambda.rb +59 -0
  104. data/lib/otto/route_handlers/logic_class.rb +93 -0
  105. data/lib/otto/route_handlers.rb +10 -376
  106. data/lib/otto/security/authentication/auth_strategy.rb +44 -0
  107. data/lib/otto/security/authentication/authentication_middleware.rb +123 -0
  108. data/lib/otto/security/authentication/failure_result.rb +36 -0
  109. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +40 -0
  110. data/lib/otto/security/authentication/strategies/permission_strategy.rb +47 -0
  111. data/lib/otto/security/authentication/strategies/public_strategy.rb +19 -0
  112. data/lib/otto/security/authentication/strategies/role_strategy.rb +57 -0
  113. data/lib/otto/security/authentication/strategies/session_strategy.rb +41 -0
  114. data/lib/otto/security/authentication/strategy_result.rb +223 -0
  115. data/lib/otto/security/authentication.rb +28 -282
  116. data/lib/otto/security/config.rb +15 -11
  117. data/lib/otto/security/configurator.rb +219 -0
  118. data/lib/otto/security/csrf.rb +8 -143
  119. data/lib/otto/security/middleware/csrf_middleware.rb +151 -0
  120. data/lib/otto/security/middleware/rate_limit_middleware.rb +38 -0
  121. data/lib/otto/security/middleware/validation_middleware.rb +252 -0
  122. data/lib/otto/security/rate_limiter.rb +86 -0
  123. data/lib/otto/security/rate_limiting.rb +16 -0
  124. data/lib/otto/security/validator.rb +8 -292
  125. data/lib/otto/static.rb +3 -0
  126. data/lib/otto/utils.rb +14 -0
  127. data/lib/otto/version.rb +3 -1
  128. data/lib/otto.rb +184 -414
  129. data/otto.gemspec +11 -6
  130. metadata +134 -25
  131. data/examples/dynamic_pages/app.rb +0 -115
  132. data/examples/dynamic_pages/config.ru +0 -30
  133. data/examples/dynamic_pages/routes +0 -21
  134. data/examples/helpers_demo/app.rb +0 -244
  135. data/examples/helpers_demo/config.ru +0 -26
  136. 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
- # 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
@@ -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 = verb.to_s.upcase.to_sym
39
- @path = path
40
+ @verb = verb.to_s.upcase.to_sym
41
+ @path = path
40
42
  @definition = definition
41
- @pattern = pattern
42
- @keys = keys || []
43
+ @pattern = pattern
44
+ @keys = keys || []
43
45
 
44
46
  # Parse the definition into target and options
45
- parsed = parse_definition(definition)
46
- @target = parsed[: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 = target_parsed[:klass_name]
52
- @method_name = target_parsed[:method_name]
53
- @kind = target_parsed[: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 #{to_s} options=#{@options.inspect}>"
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 = definition.split(/\s+/)
144
- target = parts.shift
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
- else
153
+ elsif Otto.debug
152
154
  # Malformed parameter, log warning if debug enabled
153
- Otto.logger.warn "Ignoring malformed route parameter: #{part}" if Otto.debug
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
- 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
@@ -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