rapitapir 0.1.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 (157) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +57 -0
  4. data/CHANGELOG.md +94 -0
  5. data/CLEANUP_SUMMARY.md +155 -0
  6. data/CONTRIBUTING.md +280 -0
  7. data/LICENSE +21 -0
  8. data/README.md +485 -0
  9. data/debug_hash.rb +20 -0
  10. data/docs/EXTENSION_COMPARISON.md +388 -0
  11. data/docs/SINATRA_EXTENSION.md +467 -0
  12. data/docs/archive/PHASE_1_2_COMPLETE.md +77 -0
  13. data/docs/archive/PHASE_1_3_COMPLETE.md +152 -0
  14. data/docs/archive/PHASE_2_1_OBSERVABILITY_COMPLETED.md +203 -0
  15. data/docs/archive/PHASE_2_SUMMARY.md +209 -0
  16. data/docs/archive/REFACTORING_SUMMARY.md +184 -0
  17. data/docs/archive/phase_1_3_plan.md +136 -0
  18. data/docs/archive/sinatra_extension_summary.md +188 -0
  19. data/docs/archive/sinatra_working_solution.md +113 -0
  20. data/docs/archive/typescript-client-generator-summary.md +259 -0
  21. data/docs/auto-derivation.md +146 -0
  22. data/docs/blueprint.md +1091 -0
  23. data/docs/endpoint-definition.md +211 -0
  24. data/docs/github_pages_fix.md +52 -0
  25. data/docs/github_pages_setup.md +49 -0
  26. data/docs/implementation-status.md +357 -0
  27. data/docs/observability.md +647 -0
  28. data/docs/phase3-plan.md +108 -0
  29. data/docs/sinatra_rapitapir.md +87 -0
  30. data/docs/type_shortcuts.md +146 -0
  31. data/examples/README_ENTERPRISE.md +202 -0
  32. data/examples/authentication_example.rb +192 -0
  33. data/examples/auto_derivation_ruby_friendly.rb +163 -0
  34. data/examples/cli/user_api_endpoints.rb +56 -0
  35. data/examples/client/typescript_client_example.rb +102 -0
  36. data/examples/client/user-api-client.ts +193 -0
  37. data/examples/demo_api.rb +41 -0
  38. data/examples/docs/documentation_example.rb +112 -0
  39. data/examples/docs/user-api-docs.html +789 -0
  40. data/examples/docs/user-api-docs.md +403 -0
  41. data/examples/enhanced_auto_derivation_test.rb +83 -0
  42. data/examples/enterprise_extension_demo.rb +417 -0
  43. data/examples/enterprise_rapitapir_api.rb +662 -0
  44. data/examples/getting_started_extension.rb +218 -0
  45. data/examples/hello_world.rb +74 -0
  46. data/examples/oauth2/.env.example +19 -0
  47. data/examples/oauth2/README.md +205 -0
  48. data/examples/oauth2/generic_oauth2_api.rb +226 -0
  49. data/examples/oauth2/get_token.rb +72 -0
  50. data/examples/oauth2/songs_api_with_auth0.rb +320 -0
  51. data/examples/oauth2/test_api.sh +16 -0
  52. data/examples/oauth2/test_songs_api.sh +110 -0
  53. data/examples/observability/.env.example +35 -0
  54. data/examples/observability/README.md +230 -0
  55. data/examples/observability/README_HONEYCOMB.md +332 -0
  56. data/examples/observability/advanced_setup.rb +384 -0
  57. data/examples/observability/basic_setup.rb +192 -0
  58. data/examples/observability/complete_test.rb +121 -0
  59. data/examples/observability/honeycomb_example.rb +523 -0
  60. data/examples/observability/honeycomb_rapitapir_clean.rb +488 -0
  61. data/examples/observability/honeycomb_rapitapir_example.rb +523 -0
  62. data/examples/observability/honeycomb_working_example.rb +489 -0
  63. data/examples/observability/quick_test.rb +78 -0
  64. data/examples/observability/simple_test.rb +14 -0
  65. data/examples/observability/test_honeycomb_demo.rb +354 -0
  66. data/examples/observability/test_live_honeycomb.rb +111 -0
  67. data/examples/observability/test_validation.rb +78 -0
  68. data/examples/observability/test_working_validation.rb +66 -0
  69. data/examples/openapi/user_api_schema.rb +132 -0
  70. data/examples/production_ready_example.rb +105 -0
  71. data/examples/rails/users_controller.rb +146 -0
  72. data/examples/readme/basic_sinatra_example.rb +128 -0
  73. data/examples/server/user_api.rb +179 -0
  74. data/examples/simple_auto_derivation_demo.rb +44 -0
  75. data/examples/simple_demo_api.rb +18 -0
  76. data/examples/sinatra/user_app.rb +127 -0
  77. data/examples/t_shortcut_demo.rb +59 -0
  78. data/examples/user_api.rb +190 -0
  79. data/examples/working_getting_started.rb +184 -0
  80. data/examples/working_simple_example.rb +195 -0
  81. data/lib/rapitapir/auth/configuration.rb +129 -0
  82. data/lib/rapitapir/auth/context.rb +122 -0
  83. data/lib/rapitapir/auth/errors.rb +104 -0
  84. data/lib/rapitapir/auth/middleware.rb +324 -0
  85. data/lib/rapitapir/auth/oauth2.rb +350 -0
  86. data/lib/rapitapir/auth/schemes.rb +420 -0
  87. data/lib/rapitapir/auth.rb +113 -0
  88. data/lib/rapitapir/cli/command.rb +535 -0
  89. data/lib/rapitapir/cli/server.rb +243 -0
  90. data/lib/rapitapir/cli/validator.rb +373 -0
  91. data/lib/rapitapir/client/generator_base.rb +272 -0
  92. data/lib/rapitapir/client/typescript_generator.rb +350 -0
  93. data/lib/rapitapir/core/endpoint.rb +158 -0
  94. data/lib/rapitapir/core/enhanced_endpoint.rb +235 -0
  95. data/lib/rapitapir/core/input.rb +182 -0
  96. data/lib/rapitapir/core/output.rb +164 -0
  97. data/lib/rapitapir/core/request.rb +19 -0
  98. data/lib/rapitapir/core/response.rb +17 -0
  99. data/lib/rapitapir/docs/html_generator.rb +780 -0
  100. data/lib/rapitapir/docs/markdown_generator.rb +464 -0
  101. data/lib/rapitapir/dsl/endpoint_dsl.rb +116 -0
  102. data/lib/rapitapir/dsl/enhanced_endpoint_dsl.rb +62 -0
  103. data/lib/rapitapir/dsl/enhanced_input.rb +73 -0
  104. data/lib/rapitapir/dsl/enhanced_output.rb +63 -0
  105. data/lib/rapitapir/dsl/enhanced_structures.rb +393 -0
  106. data/lib/rapitapir/dsl/fluent_dsl.rb +72 -0
  107. data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +316 -0
  108. data/lib/rapitapir/dsl/http_verbs.rb +77 -0
  109. data/lib/rapitapir/dsl/input_methods.rb +47 -0
  110. data/lib/rapitapir/dsl/observability_methods.rb +81 -0
  111. data/lib/rapitapir/dsl/output_methods.rb +43 -0
  112. data/lib/rapitapir/dsl/type_resolution.rb +43 -0
  113. data/lib/rapitapir/observability/configuration.rb +108 -0
  114. data/lib/rapitapir/observability/health_check.rb +236 -0
  115. data/lib/rapitapir/observability/logging.rb +270 -0
  116. data/lib/rapitapir/observability/metrics.rb +203 -0
  117. data/lib/rapitapir/observability/middleware.rb +243 -0
  118. data/lib/rapitapir/observability/tracing.rb +143 -0
  119. data/lib/rapitapir/observability.rb +28 -0
  120. data/lib/rapitapir/openapi/schema_generator.rb +403 -0
  121. data/lib/rapitapir/schema.rb +136 -0
  122. data/lib/rapitapir/server/enhanced_rack_adapter.rb +379 -0
  123. data/lib/rapitapir/server/middleware.rb +120 -0
  124. data/lib/rapitapir/server/path_matcher.rb +45 -0
  125. data/lib/rapitapir/server/rack_adapter.rb +215 -0
  126. data/lib/rapitapir/server/rails_adapter.rb +17 -0
  127. data/lib/rapitapir/server/rails_adapter_class.rb +53 -0
  128. data/lib/rapitapir/server/rails_controller.rb +72 -0
  129. data/lib/rapitapir/server/rails_input_processor.rb +73 -0
  130. data/lib/rapitapir/server/rails_response_handler.rb +29 -0
  131. data/lib/rapitapir/server/sinatra_adapter.rb +200 -0
  132. data/lib/rapitapir/server/sinatra_integration.rb +93 -0
  133. data/lib/rapitapir/sinatra/configuration.rb +91 -0
  134. data/lib/rapitapir/sinatra/extension.rb +214 -0
  135. data/lib/rapitapir/sinatra/oauth2_helpers.rb +236 -0
  136. data/lib/rapitapir/sinatra/resource_builder.rb +152 -0
  137. data/lib/rapitapir/sinatra/swagger_ui_generator.rb +166 -0
  138. data/lib/rapitapir/sinatra_rapitapir.rb +40 -0
  139. data/lib/rapitapir/types/array.rb +163 -0
  140. data/lib/rapitapir/types/auto_derivation.rb +265 -0
  141. data/lib/rapitapir/types/base.rb +146 -0
  142. data/lib/rapitapir/types/boolean.rb +46 -0
  143. data/lib/rapitapir/types/date.rb +92 -0
  144. data/lib/rapitapir/types/datetime.rb +98 -0
  145. data/lib/rapitapir/types/email.rb +32 -0
  146. data/lib/rapitapir/types/float.rb +134 -0
  147. data/lib/rapitapir/types/hash.rb +161 -0
  148. data/lib/rapitapir/types/integer.rb +143 -0
  149. data/lib/rapitapir/types/object.rb +156 -0
  150. data/lib/rapitapir/types/optional.rb +65 -0
  151. data/lib/rapitapir/types/string.rb +185 -0
  152. data/lib/rapitapir/types/uuid.rb +32 -0
  153. data/lib/rapitapir/types.rb +155 -0
  154. data/lib/rapitapir/version.rb +5 -0
  155. data/lib/rapitapir.rb +173 -0
  156. data/rapitapir.gemspec +66 -0
  157. metadata +387 -0
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'enhanced_rack_adapter'
4
+
5
+ module RapiTapir
6
+ module Server
7
+ # Sinatra integration for enhanced endpoints
8
+ module SinatraIntegration
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ base.class_eval do
12
+ @rapitapir_adapter = EnhancedRackAdapter.new
13
+ end
14
+ end
15
+
16
+ # Class methods for Sinatra integration
17
+ #
18
+ # Provides methods to define RapiTapir endpoints and generate documentation
19
+ # within Sinatra applications.
20
+ module ClassMethods
21
+ def rapitapir_adapter
22
+ @rapitapir_adapter ||= EnhancedRackAdapter.new
23
+ end
24
+
25
+ # Mount an endpoint in Sinatra
26
+ def mount_endpoint(endpoint, &handler)
27
+ rapitapir_adapter.mount(endpoint, &handler)
28
+
29
+ # Register the route with Sinatra
30
+ method_name = endpoint.method.to_s.downcase
31
+ path_pattern = convert_path_to_sinatra(endpoint.path)
32
+
33
+ send(method_name, path_pattern) do
34
+ # Delegate to RapiTapir adapter
35
+ rapitapir_adapter.call(env)
36
+ end
37
+ end
38
+
39
+ # Use middleware with RapiTapir
40
+ def rapitapir_use(middleware_class, ...)
41
+ rapitapir_adapter.use(middleware_class, ...)
42
+ end
43
+
44
+ # Register error handlers
45
+ def rapitapir_error(error_class, &handler)
46
+ rapitapir_adapter.on_error(error_class, &handler)
47
+ end
48
+
49
+ # Generate OpenAPI spec for all mounted endpoints
50
+ def to_openapi_spec(info = {})
51
+ spec = build_base_openapi_spec(info)
52
+ add_endpoints_to_spec(spec)
53
+ spec
54
+ end
55
+
56
+ private
57
+
58
+ def build_base_openapi_spec(info)
59
+ {
60
+ openapi: '3.0.3',
61
+ info: build_openapi_info(info),
62
+ paths: {}
63
+ }
64
+ end
65
+
66
+ def build_openapi_info(info)
67
+ {
68
+ title: info[:title] || 'API',
69
+ version: info[:version] || '1.0.0',
70
+ description: info[:description]
71
+ }.compact
72
+ end
73
+
74
+ def add_endpoints_to_spec(spec)
75
+ rapitapir_adapter.endpoints.each do |endpoint_data|
76
+ endpoint = endpoint_data[:endpoint]
77
+ path = endpoint.path
78
+ method = endpoint.method.to_s.downcase
79
+
80
+ spec[:paths][path] ||= {}
81
+ spec[:paths][path][method] = endpoint.to_openapi_spec
82
+ end
83
+ end
84
+
85
+ def convert_path_to_sinatra(path)
86
+ # Convert RapiTapir path format to Sinatra path format
87
+ # e.g., "/users/{id}" -> "/users/:id"
88
+ path.gsub(/\{([^}]+)\}/, ':\1')
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RapiTapir
4
+ module Sinatra
5
+ # Configuration class for RapiTapir Sinatra integration
6
+ # Follows Single Responsibility Principle - manages configuration only
7
+ class Configuration
8
+ attr_accessor :docs_path, :openapi_path, :health_check_enabled, :health_check_path
9
+ attr_reader :api_info, :servers, :public_paths, :auth_schemes
10
+
11
+ def initialize
12
+ @api_info = {
13
+ title: 'API Documentation',
14
+ description: 'Generated with RapiTapir',
15
+ version: '1.0.0'
16
+ }
17
+ @servers = []
18
+ @public_paths = []
19
+ @auth_schemes = {}
20
+ @docs_path = '/docs'
21
+ @openapi_path = '/openapi.json'
22
+ @health_check_enabled = false
23
+ @health_check_path = '/health'
24
+ end
25
+
26
+ # API Information configuration
27
+ def info(title: nil, description: nil, version: nil, **options)
28
+ @api_info[:title] = title if title
29
+ @api_info[:description] = description if description
30
+ @api_info[:version] = version if version
31
+ @api_info.merge!(options)
32
+ end
33
+
34
+ # Server configuration
35
+ def server(url:, description: nil)
36
+ @servers << { url: url, description: description }.compact
37
+ end
38
+
39
+ # Add paths that don't require authentication
40
+ def add_public_paths(*paths)
41
+ @public_paths.concat(paths.flatten.map(&:to_s))
42
+ end
43
+
44
+ # Authentication scheme management
45
+ def add_auth_scheme(name, scheme)
46
+ @auth_schemes[name] = scheme
47
+ end
48
+
49
+ def get_auth_scheme(name)
50
+ @auth_schemes[name]
51
+ end
52
+
53
+ # Check if documentation is enabled
54
+ def docs_enabled?
55
+ !@docs_path.nil?
56
+ end
57
+
58
+ # Environment-specific configurations
59
+ def development_defaults!
60
+ # Enable health check endpoint
61
+ enable_health_check
62
+ # Enable docs by default in development
63
+ enable_docs unless @docs_path.nil? && @openapi_path.nil?
64
+ # Add health check to public paths (no auth required)
65
+ add_public_paths(@health_check_path)
66
+ puts '📝 Applied development defaults for RapiTapir (includes health check at /health)'
67
+ end
68
+
69
+ def production_defaults!
70
+ # Basic production settings
71
+ puts '🔒 Applied production defaults for RapiTapir'
72
+ end
73
+
74
+ # Health check configuration
75
+ def enable_health_check(path: '/health')
76
+ @health_check_enabled = true
77
+ @health_check_path = path
78
+ end
79
+
80
+ def health_check_enabled?
81
+ @health_check_enabled
82
+ end
83
+
84
+ # Documentation configuration
85
+ def enable_docs(path: '/docs', openapi_path: '/openapi.json')
86
+ @docs_path = path
87
+ @openapi_path = openapi_path
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra/base'
4
+ require_relative '../server/sinatra_adapter'
5
+ require_relative '../auth'
6
+ require_relative '../openapi/schema_generator'
7
+ require_relative 'configuration'
8
+ require_relative 'resource_builder'
9
+ require_relative 'swagger_ui_generator'
10
+ require_relative 'oauth2_helpers'
11
+ require_relative '../dsl/http_verbs'
12
+
13
+ module RapiTapir
14
+ module Sinatra
15
+ # Main Sinatra Extension for RapiTapir integration
16
+ # Provides a seamless, ergonomic experience for building enterprise-grade APIs
17
+ module Extension
18
+ # Extension registration hook
19
+ def self.registered(app)
20
+ app.helpers Helpers
21
+ app.extend ClassMethods
22
+ app.extend DSL::HTTPVerbs # Automatically include enhanced HTTP verb DSL
23
+ app.extend OAuth2Helpers # Include OAuth2 authentication helpers
24
+ app.set :rapitapir_config, Configuration.new
25
+ app.set :rapitapir_endpoints, []
26
+ app.set :rapitapir_adapter, nil
27
+ end
28
+
29
+ # Class methods added to the Sinatra application
30
+ module ClassMethods
31
+ # Configure RapiTapir integration
32
+ def rapitapir(&block)
33
+ config = settings.rapitapir_config
34
+ config.instance_eval(&block) if block_given?
35
+ setup_rapitapir_integration
36
+ end
37
+
38
+ # Register an endpoint with automatic route creation
39
+ def endpoint(definition, &handler)
40
+ endpoint_obj = case definition
41
+ when RapiTapir::Core::Endpoint, RapiTapir::Core::EnhancedEndpoint
42
+ definition
43
+ when Proc
44
+ definition.call
45
+ else
46
+ raise ArgumentError, 'Invalid endpoint definition'
47
+ end
48
+
49
+ # Store the endpoint
50
+ settings.rapitapir_endpoints << { endpoint: endpoint_obj, handler: handler }
51
+
52
+ # Register with adapter if available
53
+ settings.rapitapir_adapter&.register_endpoint(endpoint_obj, handler)
54
+
55
+ endpoint_obj
56
+ end
57
+
58
+ # DSL for common endpoint patterns
59
+ def api_resource(path, schema:, **options, &block)
60
+ ResourceBuilder.new(self, path, schema, **options).instance_eval(&block)
61
+ end
62
+
63
+ # Authentication configuration
64
+ def auth_scheme(name, type, **config)
65
+ settings.rapitapir_config.add_auth_scheme(name, type, **config)
66
+ end
67
+
68
+ # Middleware configuration
69
+ def use_rapitapir_middleware(*middleware_types)
70
+ middleware_types.each { |type| settings.rapitapir_config.enable_middleware(type) }
71
+ end
72
+
73
+ # OpenAPI documentation endpoints
74
+ def enable_docs(path: '/docs', openapi_path: '/openapi.json')
75
+ settings.rapitapir_config.docs_path = path
76
+ settings.rapitapir_config.openapi_path = openapi_path
77
+ end
78
+
79
+ private
80
+
81
+ def setup_rapitapir_integration
82
+ return if settings.rapitapir_adapter
83
+
84
+ config = settings.rapitapir_config
85
+
86
+ # Create and configure adapter
87
+ adapter = RapiTapir::Server::SinatraAdapter.new(self)
88
+ set :rapitapir_adapter, adapter
89
+
90
+ # Register existing endpoints
91
+ settings.rapitapir_endpoints.each do |ep_data|
92
+ adapter.register_endpoint(ep_data[:endpoint], ep_data[:handler])
93
+ end
94
+
95
+ # Setup documentation endpoints
96
+ setup_documentation_endpoints(config) if config.docs_enabled?
97
+
98
+ # Setup health check endpoint automatically
99
+ setup_health_check_endpoint(config) if config.health_check_enabled?
100
+ end
101
+
102
+ def setup_documentation_endpoints(config)
103
+ openapi_path = config.openapi_path
104
+ docs_path = config.docs_path
105
+
106
+ # OpenAPI spec endpoint
107
+ get openapi_path do
108
+ content_type :json
109
+ JSON.pretty_generate(generate_openapi_spec)
110
+ end
111
+
112
+ # Swagger UI endpoint
113
+ get docs_path do
114
+ generate_swagger_ui(openapi_path, config.api_info)
115
+ end
116
+ end
117
+
118
+ def setup_health_check_endpoint(config)
119
+ health_path = config.health_check_path
120
+ health_endpoint = build_health_check_endpoint(health_path)
121
+ health_handler = build_health_check_handler(config)
122
+
123
+ # Register the health check endpoint
124
+ settings.rapitapir_endpoints << { endpoint: health_endpoint, handler: health_handler }
125
+
126
+ # Register with adapter
127
+ settings.rapitapir_adapter&.register_endpoint(health_endpoint, health_handler)
128
+ end
129
+
130
+ def build_health_check_endpoint(health_path)
131
+ RapiTapir.get(health_path)
132
+ .summary('Health check')
133
+ .description('Returns the health status of the API')
134
+ .tags('Health')
135
+ .ok(build_health_check_schema)
136
+ .build
137
+ end
138
+
139
+ def build_health_check_schema
140
+ RapiTapir::Types.hash({
141
+ 'status' => RapiTapir::Types.string,
142
+ 'timestamp' => RapiTapir::Types.string,
143
+ 'version' => RapiTapir::Types.optional(RapiTapir::Types.string)
144
+ })
145
+ end
146
+
147
+ def build_health_check_handler(config)
148
+ proc do |_inputs|
149
+ {
150
+ status: 'healthy',
151
+ timestamp: Time.now.iso8601,
152
+ version: config.api_info[:version]
153
+ }
154
+ end
155
+ end
156
+ end
157
+
158
+ # Instance methods added to the Sinatra application
159
+ module Helpers
160
+ # Generate OpenAPI specification from registered endpoints
161
+ def generate_openapi_spec
162
+ config = settings.rapitapir_config
163
+ endpoints = settings.rapitapir_endpoints.map { |ep| ep[:endpoint] }
164
+
165
+ generator = RapiTapir::OpenAPI::SchemaGenerator.new(
166
+ endpoints: endpoints,
167
+ info: config.api_info,
168
+ servers: config.servers
169
+ )
170
+
171
+ generator.generate
172
+ end
173
+
174
+ # Generate Swagger UI HTML
175
+ def generate_swagger_ui(openapi_path, api_info)
176
+ SwaggerUIGenerator.new(openapi_path, api_info).generate
177
+ end
178
+
179
+ # Authentication helpers
180
+ def current_user
181
+ @current_user
182
+ end
183
+
184
+ def authenticated?
185
+ !current_user.nil?
186
+ end
187
+
188
+ def scope?(scope)
189
+ return false unless authenticated?
190
+
191
+ user_scopes = current_user.is_a?(Hash) ? current_user[:scopes] || [] : []
192
+ user_scopes.include?(scope.to_s)
193
+ end
194
+ alias has_scope? scope?
195
+
196
+ def require_authentication!
197
+ halt 401, { error: 'Authentication required' }.to_json unless authenticated?
198
+ end
199
+
200
+ def require_scope!(scope)
201
+ require_authentication!
202
+ halt 403, { error: "#{scope.capitalize} permission required" }.to_json unless has_scope?(scope)
203
+ end
204
+
205
+ private
206
+
207
+ # Simple placeholder for future security scheme building
208
+ def build_security_schemes(_config)
209
+ {}
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../auth/oauth2'
4
+
5
+ module RapiTapir
6
+ module Sinatra
7
+ # OAuth2 integration helpers for Sinatra applications
8
+ # Provides convenient methods to secure endpoints with OAuth2/Auth0
9
+ module OAuth2Helpers
10
+ # Configure Auth0 OAuth2 authentication
11
+ def auth0_oauth2(scheme_name = :oauth2_auth0, domain:, audience:, **options)
12
+ auth_scheme = RapiTapir::Auth::OAuth2::Auth0Scheme.new(scheme_name, {
13
+ domain: domain,
14
+ audience: audience,
15
+ **options
16
+ })
17
+
18
+ # Store the auth scheme for use in endpoint protection
19
+ settings.rapitapir_config.add_auth_scheme(scheme_name, auth_scheme)
20
+
21
+ # Add authentication helper methods
22
+ helpers do
23
+ include OAuth2HelperMethods
24
+ end
25
+
26
+ auth_scheme
27
+ end
28
+
29
+ # Configure generic OAuth2 authentication with token introspection
30
+ def oauth2_introspection(scheme_name = :oauth2, introspection_endpoint:, client_id:, client_secret:, **options)
31
+ auth_scheme = RapiTapir::Auth::OAuth2::GenericScheme.new(scheme_name, {
32
+ introspection_endpoint: introspection_endpoint,
33
+ client_id: client_id,
34
+ client_secret: client_secret,
35
+ **options
36
+ })
37
+
38
+ settings.rapitapir_config.add_auth_scheme(scheme_name, auth_scheme)
39
+
40
+ helpers do
41
+ include OAuth2HelperMethods
42
+ end
43
+
44
+ auth_scheme
45
+ end
46
+
47
+ # Protect specific routes with OAuth2 authentication
48
+ def protect_with_oauth2(*paths, scopes: [], scheme: :oauth2_auth0)
49
+ paths.each do |path|
50
+ before path do
51
+ authorize_oauth2!(required_scopes: scopes, scheme: scheme)
52
+ end
53
+ end
54
+ end
55
+
56
+ # Protect all routes with OAuth2 authentication
57
+ def protect_all_routes_with_oauth2(scopes: [], scheme: :oauth2_auth0, except: [])
58
+ before do
59
+ # Skip protection for excluded paths
60
+ next if except.any? { |pattern| request.path_info.match?(pattern) }
61
+
62
+ authorize_oauth2!(required_scopes: scopes, scheme: scheme)
63
+ end
64
+ end
65
+ end
66
+
67
+ # Helper methods available in route handlers
68
+ module OAuth2HelperMethods
69
+ # Authenticate and authorize with OAuth2
70
+ def authorize_oauth2!(required_scopes: [], scheme: :oauth2_auth0)
71
+ auth_scheme = settings.rapitapir_config.auth_schemes[scheme]
72
+
73
+ unless auth_scheme
74
+ halt 500, { error: 'OAuth2 authentication not configured' }.to_json
75
+ end
76
+
77
+ begin
78
+ context = auth_scheme.authenticate(request)
79
+
80
+ unless context
81
+ challenge = auth_scheme.challenge
82
+ headers 'WWW-Authenticate' => challenge
83
+ halt 401, {
84
+ error: 'unauthorized',
85
+ error_description: 'Access token required'
86
+ }.to_json
87
+ end
88
+
89
+ # Store context for use in route handlers
90
+ request.env['rapitapir.auth.context'] = context
91
+
92
+ # Check required scopes if specified
93
+ if required_scopes.any?
94
+ missing_scopes = required_scopes - context.scopes
95
+
96
+ if missing_scopes.any?
97
+ halt 403, {
98
+ error: 'insufficient_scope',
99
+ error_description: "Missing required scopes: #{missing_scopes.join(', ')}"
100
+ }.to_json
101
+ end
102
+ end
103
+
104
+ context
105
+ rescue RapiTapir::Auth::InvalidTokenError => e
106
+ challenge = auth_scheme.challenge
107
+ headers 'WWW-Authenticate' => challenge
108
+ halt 401, {
109
+ error: 'invalid_token',
110
+ error_description: e.message
111
+ }.to_json
112
+ rescue RapiTapir::Auth::AuthenticationError => e
113
+ halt 500, {
114
+ error: 'authentication_failed',
115
+ error_description: e.message
116
+ }.to_json
117
+ end
118
+ end
119
+
120
+ # Get current authentication context
121
+ def current_auth_context
122
+ request.env['rapitapir.auth.context']
123
+ end
124
+
125
+ # Get current authenticated user
126
+ def current_user
127
+ current_auth_context&.user
128
+ end
129
+
130
+ # Check if user is authenticated
131
+ def authenticated?
132
+ !current_auth_context.nil?
133
+ end
134
+
135
+ # Check if user has specific scope
136
+ def has_scope?(scope)
137
+ return false unless current_auth_context
138
+
139
+ current_auth_context.scopes.include?(scope.to_s)
140
+ end
141
+
142
+ # Check if user has all required scopes
143
+ def has_scopes?(*scopes)
144
+ return false unless current_auth_context
145
+
146
+ scopes.all? { |scope| has_scope?(scope) }
147
+ end
148
+
149
+ # Check if user has any of the specified scopes
150
+ def has_any_scope?(*scopes)
151
+ return false unless current_auth_context
152
+
153
+ scopes.any? { |scope| has_scope?(scope) }
154
+ end
155
+
156
+ # Require specific scopes for the current request
157
+ def require_scopes!(*scopes)
158
+ missing_scopes = scopes.reject { |scope| has_scope?(scope) }
159
+
160
+ if missing_scopes.any?
161
+ halt 403, {
162
+ error: 'insufficient_scope',
163
+ error_description: "Missing required scopes: #{missing_scopes.join(', ')}"
164
+ }.to_json
165
+ end
166
+ end
167
+
168
+ # Extract token from Authorization header
169
+ def extract_bearer_token
170
+ auth_header = request.env['HTTP_AUTHORIZATION']
171
+ return nil unless auth_header
172
+
173
+ match = auth_header.match(/\ABearer\s+(.+)\z/i)
174
+ match ? match[1] : nil
175
+ end
176
+
177
+ # Validate token directly (useful for custom logic)
178
+ def validate_oauth2_token(token, scheme: :oauth2_auth0)
179
+ auth_scheme = settings.rapitapir_config.auth_schemes[scheme]
180
+ return nil unless auth_scheme
181
+
182
+ begin
183
+ auth_scheme.authenticate(
184
+ OpenStruct.new(env: { 'HTTP_AUTHORIZATION' => "Bearer #{token}" })
185
+ )
186
+ rescue StandardError
187
+ nil
188
+ end
189
+ end
190
+ end
191
+
192
+ # DSL extensions for endpoint definitions with OAuth2
193
+ module OAuth2EndpointExtensions
194
+ # Add OAuth2 authentication to an endpoint
195
+ def with_oauth2_auth(scopes: [], scheme: :oauth2_auth0, description: nil)
196
+ auth_scheme_obj = case scheme
197
+ when Symbol
198
+ # Will be resolved at runtime
199
+ OpenStruct.new(name: scheme, scopes: scopes)
200
+ else
201
+ scheme
202
+ end
203
+
204
+ security_in(auth_scheme_obj).tap do |endpoint|
205
+ # Add to endpoint metadata for OpenAPI generation
206
+ endpoint.metadata[:security] ||= []
207
+ endpoint.metadata[:security] << {
208
+ scheme: scheme,
209
+ scopes: scopes,
210
+ description: description || "OAuth2 authentication with scopes: #{scopes.join(', ')}"
211
+ }
212
+ end
213
+ end
214
+
215
+ # Add Auth0 OAuth2 authentication to an endpoint
216
+ def with_auth0(scopes: [], description: nil)
217
+ with_oauth2_auth(scopes: scopes, scheme: :oauth2_auth0, description: description)
218
+ end
219
+
220
+ # Add scope requirement to an endpoint
221
+ def require_scopes(*scopes)
222
+ with_oauth2_auth(scopes: scopes.flatten)
223
+ end
224
+ end
225
+ end
226
+ end
227
+
228
+ # Extend the Sinatra extension with OAuth2 helpers if it exists
229
+ if defined?(RapiTapir::Sinatra::Extension)
230
+ RapiTapir::Sinatra::Extension::ClassMethods.include(RapiTapir::Sinatra::OAuth2Helpers)
231
+ end
232
+
233
+ # Extend endpoint building with OAuth2 methods
234
+ if defined?(RapiTapir::DSL::FluentEndpointBuilder)
235
+ RapiTapir::DSL::FluentEndpointBuilder.include(RapiTapir::Sinatra::OAuth2EndpointExtensions)
236
+ end