rapitapir 0.1.2 → 2.0.0

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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -7
  3. data/.rubocop_todo.yml +83 -0
  4. data/README.md +1319 -235
  5. data/RUBY_WEEKLY_LAUNCH_POST.md +219 -0
  6. data/docs/RAILS_INTEGRATION_IMPLEMENTATION.md +209 -0
  7. data/docs/SINATRA_EXTENSION.md +399 -348
  8. data/docs/STRICT_VALIDATION.md +229 -0
  9. data/docs/VALIDATION_IMPROVEMENTS.md +218 -0
  10. data/docs/ai-integration-plan.md +112 -0
  11. data/docs/auto-derivation.md +505 -92
  12. data/docs/endpoint-definition.md +536 -129
  13. data/docs/n8n-integration.md +212 -0
  14. data/docs/observability.md +810 -500
  15. data/docs/using-mcp.md +93 -0
  16. data/examples/ai/knowledge_base_rag.rb +83 -0
  17. data/examples/ai/user_management_mcp.rb +92 -0
  18. data/examples/ai/user_validation_llm.rb +187 -0
  19. data/examples/rails/RAILS_8_GUIDE.md +165 -0
  20. data/examples/rails/RAILS_LOADING_FIX.rb +35 -0
  21. data/examples/rails/README.md +497 -0
  22. data/examples/rails/comprehensive_test.rb +91 -0
  23. data/examples/rails/config/routes.rb +48 -0
  24. data/examples/rails/debug_controller.rb +63 -0
  25. data/examples/rails/detailed_test.rb +46 -0
  26. data/examples/rails/enhanced_users_controller.rb +278 -0
  27. data/examples/rails/final_server_test.rb +50 -0
  28. data/examples/rails/hello_world_app.rb +116 -0
  29. data/examples/rails/hello_world_controller.rb +186 -0
  30. data/examples/rails/hello_world_routes.rb +28 -0
  31. data/examples/rails/rails8_minimal_demo.rb +132 -0
  32. data/examples/rails/rails8_simple_demo.rb +140 -0
  33. data/examples/rails/rails8_working_demo.rb +255 -0
  34. data/examples/rails/real_world_blog_api.rb +510 -0
  35. data/examples/rails/server_test.rb +46 -0
  36. data/examples/rails/test_direct_processing.rb +41 -0
  37. data/examples/rails/test_hello_world.rb +80 -0
  38. data/examples/rails/test_rails_integration.rb +54 -0
  39. data/examples/rails/traditional_app/Gemfile +37 -0
  40. data/examples/rails/traditional_app/README.md +265 -0
  41. data/examples/rails/traditional_app/app/controllers/api/v1/posts_controller.rb +254 -0
  42. data/examples/rails/traditional_app/app/controllers/api/v1/users_controller.rb +220 -0
  43. data/examples/rails/traditional_app/app/controllers/application_controller.rb +86 -0
  44. data/examples/rails/traditional_app/app/controllers/application_controller_simplified.rb +87 -0
  45. data/examples/rails/traditional_app/app/controllers/documentation_controller.rb +149 -0
  46. data/examples/rails/traditional_app/app/controllers/health_controller.rb +42 -0
  47. data/examples/rails/traditional_app/config/routes.rb +25 -0
  48. data/examples/rails/traditional_app/config/routes_best_practice.rb +25 -0
  49. data/examples/rails/traditional_app/config/routes_simplified.rb +36 -0
  50. data/examples/rails/traditional_app_runnable.rb +406 -0
  51. data/examples/rails/users_controller.rb +4 -1
  52. data/examples/serverless/Gemfile +43 -0
  53. data/examples/serverless/QUICKSTART.md +331 -0
  54. data/examples/serverless/README.md +520 -0
  55. data/examples/serverless/aws_lambda_example.rb +307 -0
  56. data/examples/serverless/aws_sam_template.yaml +215 -0
  57. data/examples/serverless/azure_functions_example.rb +407 -0
  58. data/examples/serverless/deploy.rb +204 -0
  59. data/examples/serverless/gcp_cloud_functions_example.rb +367 -0
  60. data/examples/serverless/gcp_function.yaml +23 -0
  61. data/examples/serverless/host.json +24 -0
  62. data/examples/serverless/package.json +32 -0
  63. data/examples/serverless/spec/aws_lambda_spec.rb +196 -0
  64. data/examples/serverless/spec/spec_helper.rb +89 -0
  65. data/examples/serverless/vercel.json +31 -0
  66. data/examples/serverless/vercel_example.rb +404 -0
  67. data/examples/strict_validation_examples.rb +104 -0
  68. data/examples/validation_error_examples.rb +173 -0
  69. data/lib/rapitapir/ai/llm_instruction.rb +456 -0
  70. data/lib/rapitapir/ai/mcp.rb +134 -0
  71. data/lib/rapitapir/ai/rag.rb +287 -0
  72. data/lib/rapitapir/ai/rag_middleware.rb +147 -0
  73. data/lib/rapitapir/auth/oauth2.rb +43 -57
  74. data/lib/rapitapir/cli/command.rb +362 -2
  75. data/lib/rapitapir/cli/mcp_export.rb +18 -0
  76. data/lib/rapitapir/cli/validator.rb +2 -6
  77. data/lib/rapitapir/core/endpoint.rb +59 -6
  78. data/lib/rapitapir/core/enhanced_endpoint.rb +2 -6
  79. data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +53 -0
  80. data/lib/rapitapir/endpoint_registry.rb +47 -0
  81. data/lib/rapitapir/observability/health_check.rb +4 -4
  82. data/lib/rapitapir/observability/logging.rb +10 -10
  83. data/lib/rapitapir/schema.rb +2 -2
  84. data/lib/rapitapir/server/rack_adapter.rb +1 -3
  85. data/lib/rapitapir/server/rails/configuration.rb +77 -0
  86. data/lib/rapitapir/server/rails/controller_base.rb +185 -0
  87. data/lib/rapitapir/server/rails/documentation_helpers.rb +76 -0
  88. data/lib/rapitapir/server/rails/resource_builder.rb +181 -0
  89. data/lib/rapitapir/server/rails/routes.rb +114 -0
  90. data/lib/rapitapir/server/rails_adapter.rb +10 -3
  91. data/lib/rapitapir/server/rails_adapter_class.rb +1 -3
  92. data/lib/rapitapir/server/rails_controller.rb +1 -3
  93. data/lib/rapitapir/server/rails_integration.rb +67 -0
  94. data/lib/rapitapir/server/rails_response_handler.rb +16 -3
  95. data/lib/rapitapir/server/sinatra_adapter.rb +29 -5
  96. data/lib/rapitapir/server/sinatra_integration.rb +4 -4
  97. data/lib/rapitapir/sinatra/extension.rb +2 -2
  98. data/lib/rapitapir/sinatra/oauth2_helpers.rb +34 -40
  99. data/lib/rapitapir/types/array.rb +4 -0
  100. data/lib/rapitapir/types/auto_derivation.rb +4 -18
  101. data/lib/rapitapir/types/datetime.rb +1 -3
  102. data/lib/rapitapir/types/float.rb +2 -6
  103. data/lib/rapitapir/types/hash.rb +40 -2
  104. data/lib/rapitapir/types/integer.rb +4 -12
  105. data/lib/rapitapir/types/object.rb +6 -2
  106. data/lib/rapitapir/types.rb +6 -2
  107. data/lib/rapitapir/version.rb +1 -1
  108. data/lib/rapitapir.rb +5 -3
  109. metadata +74 -2
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RapiTapir
4
+ module Server
5
+ module Rails
6
+ # Resource builder for creating RESTful API endpoints in Rails controllers
7
+ # Provides the same functionality as Sinatra::ResourceBuilder but adapted for Rails
8
+ class ResourceBuilder
9
+ attr_reader :endpoints
10
+
11
+ def initialize(controller_class, base_path, schema, **options)
12
+ @controller_class = controller_class
13
+ @base_path = base_path.chomp('/')
14
+ @schema = schema
15
+ @options = options
16
+ @endpoints = []
17
+ end
18
+
19
+ # Enable standard CRUD operations
20
+ # @param except [Array<Symbol>] Operations to exclude
21
+ # @param only [Array<Symbol>] Only include these operations
22
+ # @param block [Proc] Block defining the CRUD handlers
23
+ def crud(except: [], only: nil, **handlers, &block)
24
+ operations = only || %i[index show create update destroy]
25
+ operations -= except if except.any?
26
+
27
+ # If a block is given, evaluate it in the context of this ResourceBuilder
28
+ if block_given?
29
+ instance_eval(&block)
30
+ else
31
+ # Legacy style with handlers hash
32
+ operations.each do |operation|
33
+ send(operation, &handlers[operation]) if respond_to?(operation, true)
34
+ end
35
+ end
36
+ end
37
+
38
+ # List all resources (GET /resources)
39
+ # @param options [Hash] Endpoint options
40
+ # @param handler [Proc] Request handler
41
+ def index(**options, &handler)
42
+ handler ||= proc { [] }
43
+
44
+ endpoint_def = @controller_class.GET(@base_path)
45
+ .summary(options[:summary] || "List all #{resource_name}")
46
+ .description(options[:description] || "Retrieve a list of #{resource_name}")
47
+ .then { |ep| add_pagination_params(ep) }
48
+ .ok(RapiTapir::Types.array(@schema))
49
+ .build
50
+
51
+ @endpoints << [endpoint_def, handler]
52
+ end
53
+
54
+ # Get specific resource (GET /resources/:id)
55
+ # @param options [Hash] Endpoint options
56
+ # @param handler [Proc] Request handler
57
+ def show(**options, &handler)
58
+ handler ||= proc { {} }
59
+
60
+ endpoint_def = @controller_class.GET("#{@base_path}/:id")
61
+ .summary(options[:summary] || "Get #{resource_name}")
62
+ .description(options[:description] || "Retrieve a specific #{resource_name} by ID")
63
+ .path_param(:id, :integer, description: "#{resource_name.capitalize} ID")
64
+ .ok(@schema)
65
+ .not_found(error_schema, description: "#{resource_name.capitalize} not found")
66
+ .build
67
+
68
+ @endpoints << [endpoint_def, handler]
69
+ end
70
+
71
+ # Create new resource (POST /resources)
72
+ # @param options [Hash] Endpoint options
73
+ # @param handler [Proc] Request handler
74
+ def create(**options, &handler)
75
+ handler ||= proc { {} }
76
+
77
+ endpoint_def = @controller_class.POST(@base_path)
78
+ .summary(options[:summary] || "Create #{resource_name}")
79
+ .description(options[:description] || "Create a new #{resource_name}")
80
+ .json_body(@schema)
81
+ .created(@schema)
82
+ .bad_request(validation_error_schema, description: 'Validation error')
83
+ .build
84
+
85
+ @endpoints << [endpoint_def, handler]
86
+ end
87
+
88
+ # Update resource (PUT /resources/:id)
89
+ # @param options [Hash] Endpoint options
90
+ # @param handler [Proc] Request handler
91
+ def update(**options, &handler)
92
+ handler ||= proc { {} }
93
+
94
+ endpoint_def = @controller_class.PUT("#{@base_path}/:id")
95
+ .summary(options[:summary] || "Update #{resource_name}")
96
+ .description(options[:description] || "Update an existing #{resource_name}")
97
+ .path_param(:id, RapiTapir::Types.integer, description: "#{resource_name.capitalize} ID")
98
+ .json_body(@schema)
99
+ .ok(@schema)
100
+ .not_found(error_schema, description: "#{resource_name.capitalize} not found")
101
+ .bad_request(validation_error_schema, description: 'Validation error')
102
+ .build
103
+
104
+ @endpoints << [endpoint_def, handler]
105
+ end
106
+
107
+ # Delete resource (DELETE /resources/:id)
108
+ # @param options [Hash] Endpoint options
109
+ # @param handler [Proc] Request handler
110
+ def destroy(**options, &handler)
111
+ handler ||= proc { head :no_content }
112
+
113
+ endpoint_def = @controller_class.DELETE("#{@base_path}/:id")
114
+ .summary(options[:summary] || "Delete #{resource_name}")
115
+ .description(options[:description] || "Delete a #{resource_name}")
116
+ .path_param(:id, :integer, description: "#{resource_name.capitalize} ID")
117
+ .no_content
118
+ .not_found(error_schema, description: "#{resource_name.capitalize} not found")
119
+ .build
120
+
121
+ @endpoints << [endpoint_def, handler]
122
+ end
123
+
124
+ # Add custom endpoint to the resource
125
+ # @param method [Symbol] HTTP method
126
+ # @param path_suffix [String] Path suffix to append to base path
127
+ # @param options [Hash] Endpoint options
128
+ # @param handler [Proc] Request handler
129
+ def custom(method, path_suffix, **options, &handler)
130
+ full_path = path_suffix.start_with?('/') ? path_suffix : "#{@base_path}/#{path_suffix}"
131
+
132
+ endpoint_def = @controller_class.public_send(method.to_s.upcase, full_path)
133
+ .summary(options[:summary] || "Custom #{method.upcase} #{path_suffix}")
134
+ .description(options[:description] || "Custom endpoint for #{resource_name}")
135
+
136
+ # Apply any custom configuration
137
+ endpoint_def = options[:configure].call(endpoint_def) if options[:configure]
138
+
139
+ endpoint_def = endpoint_def.build
140
+
141
+ @endpoints << [endpoint_def, handler]
142
+ end
143
+
144
+ private
145
+
146
+ # Add pagination query parameters to an endpoint
147
+ # @param endpoint [RapiTapir::DSL::FluentEndpointBuilder] The endpoint builder
148
+ # @return [RapiTapir::DSL::FluentEndpointBuilder] The modified endpoint builder
149
+ def add_pagination_params(endpoint)
150
+ endpoint.query(:limit, RapiTapir::Types.optional(RapiTapir::Types.integer(minimum: 1, maximum: 100)),
151
+ description: 'Maximum number of results')
152
+ .query(:offset, RapiTapir::Types.optional(RapiTapir::Types.integer(minimum: 0)),
153
+ description: 'Number of results to skip')
154
+ end
155
+
156
+ # Get the singular resource name from the base path
157
+ # @return [String] The singular resource name
158
+ def resource_name
159
+ @resource_name ||= @base_path.split('/').last.chomp('s')
160
+ end
161
+
162
+ # Standard error schema for 404 responses
163
+ # @return [RapiTapir::Types::Hash] Error schema
164
+ def error_schema
165
+ RapiTapir::Types.hash({
166
+ 'error' => RapiTapir::Types.string
167
+ })
168
+ end
169
+
170
+ # Validation error schema for 400 responses
171
+ # @return [RapiTapir::Types::Hash] Validation error schema
172
+ def validation_error_schema
173
+ RapiTapir::Types.hash({
174
+ 'error' => RapiTapir::Types.string,
175
+ 'details' => RapiTapir::Types.optional(RapiTapir::Types.array(RapiTapir::Types.string))
176
+ })
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RapiTapir
4
+ module Server
5
+ module Rails
6
+ # Rails routes integration module
7
+ # Provides helpers for automatically generating routes from RapiTapir endpoint definitions
8
+ module Routes
9
+ # Generate routes for a RapiTapir Rails controller
10
+ # @param controller_class [Class] The controller class with RapiTapir endpoints
11
+ # @example
12
+ # Rails.application.routes.draw do
13
+ # rapitapir_routes_for UsersController
14
+ # end
15
+ def rapitapir_routes_for(controller_class)
16
+ raise ArgumentError, "#{controller_class} must include RapiTapir::Server::Rails::Controller" unless controller_class.respond_to?(:rapitapir_endpoints)
17
+
18
+ controller_name = controller_class.controller_name
19
+
20
+ controller_class.rapitapir_endpoints.each do |action, endpoint_config|
21
+ endpoint_def = endpoint_config[:endpoint]
22
+ method = endpoint_def.method.downcase
23
+ path = convert_rapitapir_path_to_rails(endpoint_def.path)
24
+
25
+ # Generate the Rails route
26
+ public_send(method, path, to: "#{controller_name}##{action}",
27
+ as: route_name(controller_name, action, method))
28
+ end
29
+ end
30
+
31
+ # Generate all routes for multiple controllers
32
+ # @param controllers [Array<Class>] Array of controller classes
33
+ # @example
34
+ # Rails.application.routes.draw do
35
+ # rapitapir_routes_for_all [UsersController, BooksController, OrdersController]
36
+ # end
37
+ def rapitapir_routes_for_all(controllers)
38
+ controllers.each do |controller_class|
39
+ rapitapir_routes_for(controller_class)
40
+ end
41
+ end
42
+
43
+ # Auto-discover and register all RapiTapir controllers in the application
44
+ # @example
45
+ # Rails.application.routes.draw do
46
+ # rapitapir_auto_routes
47
+ # end
48
+ def rapitapir_auto_routes
49
+ # Find all controllers that include RapiTapir
50
+ controllers = ApplicationController.descendants.select do |klass|
51
+ klass.included_modules.include?(RapiTapir::Server::Rails::Controller) ||
52
+ klass < RapiTapir::Server::Rails::ControllerBase
53
+ end
54
+
55
+ rapitapir_routes_for_all(controllers)
56
+ end
57
+
58
+ private
59
+
60
+ # Convert RapiTapir path format to Rails route format
61
+ # @param rapitapir_path [String] Path with RapiTapir parameter syntax
62
+ # @return [String] Path with Rails parameter syntax
63
+ # @example
64
+ # convert_rapitapir_path_to_rails('/users/:id') # => '/users/:id'
65
+ # convert_rapitapir_path_to_rails('/users/{id}') # => '/users/:id'
66
+ def convert_rapitapir_path_to_rails(rapitapir_path)
67
+ # Convert {id} format to :id format for Rails
68
+ rapitapir_path.gsub(/\{(\w+)\}/, ':\1')
69
+ end
70
+
71
+ # Generate a route name for the endpoint
72
+ # @param controller_name [String] The controller name
73
+ # @param action [Symbol] The action name
74
+ # @param method [String] The HTTP method
75
+ # @return [Symbol] The route name
76
+ def route_name(controller_name, action, method)
77
+ base_name = controller_name.chomp('_controller')
78
+
79
+ case action
80
+ when :index
81
+ base_name.to_sym
82
+ when :show
83
+ singularize_name(base_name).to_sym
84
+ when :create
85
+ base_name.to_sym
86
+ when :update
87
+ singularize_name(base_name).to_sym
88
+ when :destroy
89
+ singularize_name(base_name).to_sym
90
+ else
91
+ :"#{method}_#{base_name}_#{action}"
92
+ end
93
+ end
94
+
95
+ # Simple singularization for route names
96
+ # @param name [String] The plural name
97
+ # @return [String] The singular name
98
+ def singularize_name(name)
99
+ # Simple singularization logic
100
+ if name.end_with?('ies')
101
+ "#{name.chomp('ies')}y"
102
+ elsif name.end_with?('s')
103
+ name.chomp('s')
104
+ else
105
+ name
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ # NOTE: Rails routes extension is handled in the rails_integration.rb file
114
+ # to ensure proper timing with Rails application initialization
@@ -3,15 +3,22 @@
3
3
  require_relative 'rails_controller'
4
4
  require_relative 'rails_adapter_class'
5
5
 
6
+ # Load enhanced Rails integration components
7
+ require_relative 'rails/controller_base'
8
+ require_relative 'rails/configuration'
9
+ require_relative 'rails/routes'
10
+ require_relative 'rails/resource_builder'
11
+ require_relative 'rails/documentation_helpers'
12
+
6
13
  module RapiTapir
7
14
  module Server
8
15
  # Rails integration module for RapiTapir
9
16
  #
10
17
  # Provides controller concerns and adapters for seamless integration with Rails applications.
11
- # Split into separate files for better organization while maintaining compatibility.
18
+ # Includes the enhanced Rails integration with ControllerBase and auto-routing.
12
19
  module Rails
13
- # Main module that includes both Controller concern and RailsAdapter class
14
- # This maintains backward compatibility while organizing code better
20
+ # Main module that includes both legacy Controller concern and enhanced ControllerBase
21
+ # This maintains backward compatibility while providing the new enhanced features
15
22
  end
16
23
  end
17
24
  end
@@ -13,9 +13,7 @@ module RapiTapir
13
13
 
14
14
  # Register an endpoint and generate Rails routes
15
15
  def register_endpoint(endpoint, controller_class, action_name)
16
- unless endpoint.is_a?(RapiTapir::Core::Endpoint)
17
- raise ArgumentError, 'Endpoint must be a RapiTapir::Core::Endpoint'
18
- end
16
+ raise ArgumentError, 'Endpoint must be a RapiTapir::Core::Endpoint' unless endpoint.is_a?(RapiTapir::Core::Endpoint)
19
17
 
20
18
  endpoint.validate!
21
19
 
@@ -19,9 +19,7 @@ module RapiTapir
19
19
  class_methods do
20
20
  # Register an endpoint for this controller
21
21
  def rapitapir_endpoint(action_name, endpoint, &handler)
22
- unless endpoint.is_a?(RapiTapir::Core::Endpoint)
23
- raise ArgumentError, 'Endpoint must be a RapiTapir::Core::Endpoint'
24
- end
22
+ raise ArgumentError, 'Endpoint must be a RapiTapir::Core::Endpoint' unless endpoint.is_a?(RapiTapir::Core::Endpoint)
25
23
 
26
24
  endpoint.validate!
27
25
 
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Rails integration loader - ensures proper module initialization
4
+ # Only loads if Rails and ActiveSupport are available
5
+
6
+ # First, ensure the module structure exists
7
+ module RapiTapir
8
+ module Server
9
+ module Rails
10
+ # Rails integration module for RapiTapir
11
+ end
12
+ end
13
+
14
+ # Define a method to load Rails integration when Rails becomes available
15
+ def self.load_rails_integration!
16
+ return if @rails_integration_loaded
17
+ return unless defined?(Rails) && defined?(ActiveSupport)
18
+
19
+ # Remove fallback ControllerBase if it exists
20
+ RapiTapir::Server::Rails.send(:remove_const, :ControllerBase) if RapiTapir::Server::Rails.const_defined?(:ControllerBase)
21
+
22
+ # Load Rails integration components in the correct order
23
+ require_relative 'rails_controller'
24
+ require_relative 'rails_adapter_class'
25
+ require_relative 'rails/configuration'
26
+ require_relative 'rails/routes'
27
+ require_relative 'rails/resource_builder'
28
+ require_relative 'rails/documentation_helpers'
29
+ require_relative 'rails/controller_base'
30
+
31
+ # Create Railtie for Rails integration
32
+ if defined?(Rails::Railtie) && !Object.const_defined?(:RapiTapirRailtie)
33
+ Object.const_set(:RapiTapirRailtie, Class.new(Rails::Railtie) do
34
+ initializer 'rapitapir.extend_routes', after: :initialize_routes do |app|
35
+ app.routes&.extend(RapiTapir::Server::Rails::Routes)
36
+ end
37
+ end)
38
+ end
39
+
40
+ @rails_integration_loaded = true
41
+ end
42
+ end
43
+
44
+ # Load immediately if Rails is already available
45
+ if defined?(Rails) && defined?(ActiveSupport)
46
+ RapiTapir.load_rails_integration!
47
+ else
48
+ # Rails not available - provide minimal fallback
49
+ module RapiTapir
50
+ module Server
51
+ module Rails
52
+ class ControllerBase
53
+ def self.method_missing(method, ...)
54
+ # Try to load Rails integration when accessed
55
+ RapiTapir.load_rails_integration!
56
+ if defined?(RapiTapir::Server::Rails::ControllerBase) && self.class != RapiTapir::Server::Rails::ControllerBase
57
+ return RapiTapir::Server::Rails::ControllerBase.send(method, ...)
58
+ end
59
+
60
+ raise NameError,
61
+ 'Rails integration not available. Please ensure Rails is loaded before requiring RapiTapir.'
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -8,14 +8,27 @@ module RapiTapir
8
8
  private
9
9
 
10
10
  def render_rapitapir_response(result, endpoint)
11
+ # Extract custom status code if provided
12
+ status_code = 200
13
+ response_data = result
14
+
15
+ if result.is_a?(Hash) && result.key?(:_status)
16
+ status_code = result[:_status]
17
+ response_data = result.except(:_status)
18
+ elsif result.is_a?(Hash) && result.key?('_status')
19
+ status_code = result['_status']
20
+ response_data = result.except('_status')
21
+ else
22
+ status_code = determine_rails_status_code(endpoint)
23
+ end
24
+
11
25
  output = endpoint.outputs.find { |o| o.kind == :json } || endpoint.outputs.first
12
- status_code = determine_rails_status_code(endpoint)
13
26
 
14
27
  if output&.kind == :xml
15
- render xml: result, status: status_code
28
+ render xml: response_data, status: status_code
16
29
  else
17
30
  # Default to JSON for nil output, :json kind, or unknown kinds
18
- render json: result, status: status_code
31
+ render json: response_data, status: status_code
19
32
  end
20
33
  end
21
34
 
@@ -24,9 +24,7 @@ module RapiTapir
24
24
 
25
25
  # Register an endpoint with automatic Sinatra route creation
26
26
  def register_endpoint(endpoint, handler = nil, &block)
27
- unless endpoint.is_a?(RapiTapir::Core::Endpoint)
28
- raise ArgumentError, 'Endpoint must be a RapiTapir::Core::Endpoint'
29
- end
27
+ raise ArgumentError, 'Endpoint must be a RapiTapir::Core::Endpoint' unless endpoint.is_a?(RapiTapir::Core::Endpoint)
30
28
 
31
29
  endpoint.validate!
32
30
 
@@ -43,7 +41,6 @@ module RapiTapir
43
41
 
44
42
  private
45
43
 
46
- # rubocop:disable Metrics/AbcSize
47
44
  def register_sinatra_route(endpoint_info)
48
45
  method_name = endpoint_info[:endpoint].method.to_s.downcase.to_sym
49
46
  path_pattern = convert_path_to_sinatra(endpoint_info[:endpoint].path)
@@ -61,7 +58,6 @@ module RapiTapir
61
58
  end
62
59
  end
63
60
  end
64
- # rubocop:enable Metrics/AbcSize
65
61
 
66
62
  public
67
63
 
@@ -72,6 +68,18 @@ module RapiTapir
72
68
  processed_inputs = extract_sinatra_inputs(request, params, endpoint_data[:endpoint])
73
69
  result = @app.instance_exec(processed_inputs, &endpoint_data[:handler])
74
70
  send_successful_response(endpoint_data, result)
71
+ rescue RapiTapir::Types::CoercionError => e
72
+ detailed_error_response(400, 'Validation Error', e.reason, {
73
+ field: extract_field_from_error(e),
74
+ value: e.value,
75
+ expected_type: e.type
76
+ })
77
+ rescue RapiTapir::Types::ValidationError => e
78
+ detailed_error_response(400, 'Validation Error', e.message, {
79
+ errors: e.errors,
80
+ value: e.value,
81
+ expected_type: e.type.to_s
82
+ })
75
83
  rescue ArgumentError => e
76
84
  error_response(400, e.message)
77
85
  rescue StandardError => e
@@ -85,6 +93,22 @@ module RapiTapir
85
93
  [status_code, { 'Content-Type' => 'application/json' }, [error_data.to_json]]
86
94
  end
87
95
 
96
+ def detailed_error_response(status_code, error, message, details = {})
97
+ error_data = {
98
+ error: error,
99
+ message: message
100
+ }
101
+ error_data.merge!(details) if details.any?
102
+ [status_code, { 'Content-Type' => 'application/json' }, [error_data.to_json]]
103
+ end
104
+
105
+ def extract_field_from_error(error)
106
+ # Try to extract field name from error message
107
+ # Match both "Field 'field_name':" and "Required field 'field_name'"
108
+ match = error.reason.match(/(?:Field|Required field) '([^']+)'/)
109
+ match[1] if match
110
+ end
111
+
88
112
  def execute_endpoint_handler(endpoint_data, processed_inputs)
89
113
  case endpoint_data[:handler]
90
114
  when Proc
@@ -23,8 +23,8 @@ module RapiTapir
23
23
  end
24
24
 
25
25
  # Mount an endpoint in Sinatra
26
- def mount_endpoint(endpoint, &handler)
27
- rapitapir_adapter.mount(endpoint, &handler)
26
+ def mount_endpoint(endpoint, &)
27
+ rapitapir_adapter.mount(endpoint, &)
28
28
 
29
29
  # Register the route with Sinatra
30
30
  method_name = endpoint.method.to_s.downcase
@@ -42,8 +42,8 @@ module RapiTapir
42
42
  end
43
43
 
44
44
  # Register error handlers
45
- def rapitapir_error(error_class, &handler)
46
- rapitapir_adapter.on_error(error_class, &handler)
45
+ def rapitapir_error(error_class, &)
46
+ rapitapir_adapter.on_error(error_class, &)
47
47
  end
48
48
 
49
49
  # Generate OpenAPI spec for all mounted endpoints
@@ -29,9 +29,9 @@ module RapiTapir
29
29
  # Class methods added to the Sinatra application
30
30
  module ClassMethods
31
31
  # Configure RapiTapir integration
32
- def rapitapir(&block)
32
+ def rapitapir(&)
33
33
  config = settings.rapitapir_config
34
- config.instance_eval(&block) if block_given?
34
+ config.instance_eval(&) if block_given?
35
35
  setup_rapitapir_integration
36
36
  end
37
37