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,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/otto/route_handlers/class_method.rb
4
+
5
+ require 'json'
6
+ require 'securerandom'
7
+
8
+ require_relative 'base'
9
+
10
+ class Otto
11
+ module RouteHandlers
12
+ # Handler for class methods (existing Otto pattern)
13
+ # Maintains backward compatibility for Controller.action patterns
14
+ class ClassMethodHandler < BaseHandler
15
+ def call(env, extra_params = {})
16
+ req = Rack::Request.new(env)
17
+ res = Rack::Response.new
18
+
19
+ begin
20
+ # Apply the same extensions and processing as original Route#call
21
+ setup_request_response(req, res, env, extra_params)
22
+
23
+ # Call class method directly (existing Otto behavior)
24
+ result = target_class.send(route_definition.method_name, req, res)
25
+
26
+ # Only handle response if response_type is not default
27
+ if route_definition.response_type != 'default'
28
+ handle_response(result, res, {
29
+ class: target_class,
30
+ request: req,
31
+ })
32
+ end
33
+ rescue StandardError => e
34
+ # Check if we're being called through Otto's integrated context (vs direct handler testing)
35
+ # In integrated context, let Otto's centralized error handler manage the response
36
+ # In direct testing context, handle errors locally for unit testing
37
+ if otto_instance
38
+ # Log error for handler-specific context but let Otto's centralized error handler manage the response
39
+ Otto.logger.error "[ClassMethodHandler] #{e.class}: #{e.message}"
40
+ Otto.logger.debug "[ClassMethodHandler] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
41
+ raise e # Re-raise to let Otto's centralized error handler manage the response
42
+ else
43
+ # Direct handler testing context - handle errors locally with security improvements
44
+ error_id = SecureRandom.hex(8)
45
+ Otto.logger.error "[#{error_id}] #{e.class}: #{e.message}"
46
+ Otto.logger.debug "[#{error_id}] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
47
+
48
+ res.status = 500
49
+
50
+ # Content negotiation for error response
51
+ accept_header = env['HTTP_ACCEPT'].to_s
52
+ if accept_header.include?('application/json')
53
+ res.headers['content-type'] = 'application/json'
54
+ error_data = if Otto.env?(:dev, :development)
55
+ {
56
+ error: 'Internal Server Error',
57
+ message: 'Server error occurred. Check logs for details.',
58
+ error_id: error_id,
59
+ }
60
+ else
61
+ {
62
+ error: 'Internal Server Error',
63
+ message: 'An error occurred. Please try again later.',
64
+ }
65
+ end
66
+ res.write JSON.generate(error_data)
67
+ else
68
+ res.headers['content-type'] = 'text/plain'
69
+ if Otto.env?(:dev, :development)
70
+ res.write "Server error (ID: #{error_id}). Check logs for details."
71
+ else
72
+ res.write 'An error occurred. Please try again later.'
73
+ end
74
+ end
75
+
76
+ # Add security headers if available
77
+ if otto_instance.respond_to?(:security_config) && otto_instance.security_config
78
+ otto_instance.security_config.security_headers.each do |header, value|
79
+ res.headers[header] = value
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ finalize_response(res)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/otto/route_handlers/factory.rb
4
+
5
+ require_relative 'base'
6
+
7
+ class Otto
8
+ module RouteHandlers
9
+ # Factory for creating appropriate handlers based on route definitions
10
+ class HandlerFactory
11
+ # Create a handler for the given route definition
12
+ # @param route_definition [Otto::RouteDefinition] The route definition
13
+ # @param otto_instance [Otto] The Otto instance for configuration access
14
+ # @return [BaseHandler] Appropriate handler for the route
15
+ def self.create_handler(route_definition, otto_instance = nil)
16
+ case route_definition.kind
17
+ when :logic
18
+ LogicClassHandler.new(route_definition, otto_instance)
19
+ when :instance
20
+ InstanceMethodHandler.new(route_definition, otto_instance)
21
+ when :class
22
+ ClassMethodHandler.new(route_definition, otto_instance)
23
+ else
24
+ raise ArgumentError, "Unknown handler kind: #{route_definition.kind}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/otto/route_handlers/instance_method.rb
4
+ require 'securerandom'
5
+
6
+ require_relative 'base'
7
+
8
+ class Otto
9
+ module RouteHandlers
10
+ # Handler for instance methods (existing Otto pattern)
11
+ # Maintains backward compatibility for Controller#action patterns
12
+ class InstanceMethodHandler < BaseHandler
13
+ def call(env, extra_params = {})
14
+ req = Rack::Request.new(env)
15
+ res = Rack::Response.new
16
+
17
+ begin
18
+ # Apply the same extensions and processing as original Route#call
19
+ setup_request_response(req, res, env, extra_params)
20
+
21
+ # Create instance and call method (existing Otto behavior)
22
+ instance = target_class.new(req, res)
23
+ result = instance.send(route_definition.method_name)
24
+
25
+ # Only handle response if response_type is not default
26
+ if route_definition.response_type != 'default'
27
+ handle_response(result, res, {
28
+ instance: instance,
29
+ request: req,
30
+ })
31
+ end
32
+ rescue StandardError => e
33
+ # Check if we're being called through Otto's integrated context (vs direct handler testing)
34
+ # In integrated context, let Otto's centralized error handler manage the response
35
+ # In direct testing context, handle errors locally for unit testing
36
+ if otto_instance
37
+ # Log error for handler-specific context but let the centralized error handler manage the response
38
+ Otto.logger.error "[InstanceMethodHandler] #{e.class}: #{e.message}"
39
+ Otto.logger.debug "[InstanceMethodHandler] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
40
+ raise e # Re-raise to let Otto's centralized error handler manage the response
41
+ else
42
+ # Direct handler testing context - handle errors locally with security improvements
43
+ error_id = SecureRandom.hex(8)
44
+ Otto.logger.error "[#{error_id}] #{e.class}: #{e.message}"
45
+ Otto.logger.debug "[#{error_id}] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
46
+
47
+ res.status = 500
48
+ res.headers['content-type'] = 'text/plain'
49
+
50
+ if Otto.env?(:dev, :development)
51
+ res.write "Server error (ID: #{error_id}). Check logs for details."
52
+ else
53
+ res.write 'An error occurred. Please try again later.'
54
+ end
55
+
56
+ # Add security headers if available
57
+ if otto_instance.respond_to?(:security_config) && otto_instance.security_config
58
+ otto_instance.security_config.security_headers.each do |header, value|
59
+ res.headers[header] = value
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ finalize_response(res)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/otto/route_handlers/lambda.rb
4
+ require 'securerandom'
5
+
6
+ require_relative 'base'
7
+
8
+ class Otto
9
+ module RouteHandlers
10
+ # Custom handler for lambda/proc definitions (future extension)
11
+ class LambdaHandler < BaseHandler
12
+ def call(env, extra_params = {})
13
+ req = Rack::Request.new(env)
14
+ res = Rack::Response.new
15
+
16
+ begin
17
+ # Security: Lambda handlers require pre-configured procs from Otto instance
18
+ # This prevents code injection via eval and maintains security
19
+ handler_name = route_definition.klass_name
20
+ lambda_registry = otto_instance&.config&.dig(:lambda_handlers) || {}
21
+
22
+ lambda_proc = lambda_registry[handler_name]
23
+ unless lambda_proc.respond_to?(:call)
24
+ raise ArgumentError, "Lambda handler '#{handler_name}' not found in registry or not callable"
25
+ end
26
+
27
+ result = lambda_proc.call(req, res, extra_params)
28
+
29
+ handle_response(result, res, {
30
+ lambda: lambda_proc,
31
+ request: req,
32
+ })
33
+ rescue StandardError => e
34
+ error_id = SecureRandom.hex(8)
35
+ Otto.logger.error "[#{error_id}] #{e.class}: #{e.message}"
36
+ Otto.logger.debug "[#{error_id}] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
37
+
38
+ res.status = 500
39
+ res.headers['content-type'] = 'text/plain'
40
+
41
+ if Otto.env?(:dev, :development)
42
+ res.write "Lambda handler error (ID: #{error_id}). Check logs for details."
43
+ else
44
+ res.write 'An error occurred. Please try again later.'
45
+ end
46
+
47
+ # Add security headers if available
48
+ if otto_instance.respond_to?(:security_config) && otto_instance.security_config
49
+ otto_instance.security_config.security_headers.each do |header, value|
50
+ res.headers[header] = value
51
+ end
52
+ end
53
+ end
54
+
55
+ res.finish
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/otto/route_handlers/logic_class.rb
4
+ require 'json'
5
+ require 'securerandom'
6
+
7
+ require_relative 'base'
8
+
9
+ class Otto
10
+ module RouteHandlers
11
+ # Handler for Logic classes (new in Otto Framework Enhancement)
12
+ # Handles Logic class routes with the modern RequestContext pattern
13
+ # Logic classes use signature: initialize(context, params, locale)
14
+ class LogicClassHandler < BaseHandler
15
+ def call(env, extra_params = {})
16
+ req = Rack::Request.new(env)
17
+ res = Rack::Response.new
18
+
19
+ begin
20
+ # Get strategy result (guaranteed to exist from auth middleware)
21
+ strategy_result = env['otto.strategy_result'] || Otto::Security::Authentication::StrategyResult.anonymous
22
+
23
+ # Initialize Logic class with new signature: context, params, locale
24
+ logic_params = req.params.merge(extra_params)
25
+
26
+ # Handle JSON request bodies
27
+ if req.content_type&.include?('application/json') && req.body.size.positive?
28
+ begin
29
+ req.body.rewind
30
+ json_data = JSON.parse(req.body.read)
31
+ logic_params = logic_params.merge(json_data) if json_data.is_a?(Hash)
32
+ rescue JSON::ParserError => e
33
+ Otto.logger.error "[LogicClassHandler] JSON parsing error: #{e.message}"
34
+ end
35
+ end
36
+
37
+ locale = env['otto.locale'] || 'en'
38
+
39
+ logic = target_class.new(strategy_result, logic_params, locale)
40
+
41
+ # Execute standard Logic class lifecycle
42
+ logic.raise_concerns if logic.respond_to?(:raise_concerns)
43
+
44
+ result = if logic.respond_to?(:process)
45
+ logic.process
46
+ else
47
+ logic.call || logic
48
+ end
49
+
50
+ # Handle response with Logic instance context
51
+ handle_response(result, res, {
52
+ logic_instance: logic,
53
+ request: req,
54
+ status_code: logic.respond_to?(:status_code) ? logic.status_code : nil,
55
+ })
56
+ rescue StandardError => e
57
+ # Check if we're being called through Otto's integrated context (vs direct handler testing)
58
+ # In integrated context, let Otto's centralized error handler manage the response
59
+ # In direct testing context, handle errors locally for unit testing
60
+ if otto_instance
61
+ # Log error for handler-specific context but let Otto's centralized error handler manage the response
62
+ Otto.logger.error "[LogicClassHandler] #{e.class}: #{e.message}"
63
+ Otto.logger.debug "[LogicClassHandler] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
64
+ raise e # Re-raise to let Otto's centralized error handler manage the response
65
+ else
66
+ # Direct handler testing context - handle errors locally with security improvements
67
+ error_id = SecureRandom.hex(8)
68
+ Otto.logger.error "[#{error_id}] #{e.class}: #{e.message}"
69
+ Otto.logger.debug "[#{error_id}] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
70
+
71
+ res.status = 500
72
+ res.headers['content-type'] = 'text/plain'
73
+
74
+ if Otto.env?(:dev, :development)
75
+ res.write "Server error (ID: #{error_id}). Check logs for details."
76
+ else
77
+ res.write 'An error occurred. Please try again later.'
78
+ end
79
+
80
+ # Add security headers if available
81
+ if otto_instance.respond_to?(:security_config) && otto_instance.security_config
82
+ otto_instance.security_config.security_headers.each do |header, value|
83
+ res.headers[header] = value
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ res.finish
90
+ end
91
+ end
92
+ end
93
+ end