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,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RapiTapir Sinatra Working Example
4
+ # Using the original SinatraAdapter approach that we know works
5
+
6
+ require_relative '../lib/rapitapir'
7
+
8
+ # Check for Sinatra availability
9
+ begin
10
+ require 'sinatra/base'
11
+ require_relative '../lib/rapitapir/server/sinatra_adapter'
12
+ require_relative '../lib/rapitapir/openapi/schema_generator'
13
+ require_relative '../lib/rapitapir/sinatra/swagger_ui_generator'
14
+ SINATRA_AVAILABLE = true
15
+ rescue LoadError
16
+ SINATRA_AVAILABLE = false
17
+ puts '⚠️ Sinatra not available. Install with: gem install sinatra'
18
+ puts '🔄 Running in demo mode instead...'
19
+ end
20
+
21
+ # Simple in-memory data store
22
+ class BookStore
23
+ @@books = [
24
+ { id: 1, title: 'The Ruby Programming Language', author: 'Matz', isbn: '978-0596516178', published: true },
25
+ { id: 2, title: 'Metaprogramming Ruby', author: 'Paolo Perrotta', isbn: '978-1934356470', published: true }
26
+ ]
27
+ @@next_id = 3
28
+
29
+ def self.all
30
+ @@books
31
+ end
32
+
33
+ def self.find(id)
34
+ @@books.find { |book| book[:id] == id.to_i }
35
+ end
36
+
37
+ def self.create(attrs)
38
+ book = attrs.merge(id: @@next_id)
39
+ @@next_id += 1
40
+ @@books << book
41
+ book
42
+ end
43
+
44
+ def self.published
45
+ @@books.select { |book| book[:published] }
46
+ end
47
+ end
48
+
49
+ # Define the book schema
50
+ BOOK_SCHEMA = RapiTapir::Types.hash({
51
+ 'id' => RapiTapir::Types.integer,
52
+ 'title' => RapiTapir::Types.string,
53
+ 'author' => RapiTapir::Types.string,
54
+ 'isbn' => RapiTapir::Types.string,
55
+ 'published' => RapiTapir::Types.boolean
56
+ })
57
+
58
+ # Working Bookstore API using direct SinatraAdapter
59
+ if SINATRA_AVAILABLE
60
+ class WorkingBookstoreAPI < Sinatra::Base
61
+ configure do
62
+ # Create the RapiTapir adapter
63
+ set :rapitapir, RapiTapir::Server::SinatraAdapter.new(self)
64
+ end
65
+
66
+ # OpenAPI JSON endpoint - clean and simple
67
+ get '/openapi.json' do
68
+ content_type :json
69
+ endpoints = settings.rapitapir.endpoints.map { |ep_data| ep_data[:endpoint] }
70
+
71
+ generator = RapiTapir::OpenAPI::SchemaGenerator.new(
72
+ endpoints: endpoints,
73
+ info: {
74
+ title: 'Working Bookstore API',
75
+ description: 'A simple bookstore API built with RapiTapir and Sinatra',
76
+ version: '1.0.0'
77
+ },
78
+ servers: [{ url: 'http://localhost:4567', description: 'Development server' }]
79
+ )
80
+
81
+ generator.to_json
82
+ end
83
+
84
+ # Swagger UI documentation endpoint - clean and simple
85
+ get '/docs' do
86
+ content_type :html
87
+ api_info = {
88
+ title: 'Working Bookstore API',
89
+ description: 'A simple bookstore API built with RapiTapir and Sinatra'
90
+ }
91
+
92
+ RapiTapir::Sinatra::SwaggerUIGenerator.new('/openapi.json', api_info).generate
93
+ end
94
+
95
+ # Health endpoint
96
+ health_endpoint = RapiTapir.get('/health')
97
+ .summary('Health check')
98
+ .ok(RapiTapir::Types.hash({ 'status' => RapiTapir::Types.string }))
99
+ .build
100
+
101
+ settings.rapitapir.register_endpoint(health_endpoint) do |_inputs|
102
+ { status: 'healthy' }
103
+ end
104
+
105
+ # List books endpoint
106
+ list_books_endpoint = RapiTapir.get('/books')
107
+ .summary('List all books')
108
+ .ok(RapiTapir::Types.array(BOOK_SCHEMA))
109
+ .build
110
+
111
+ settings.rapitapir.register_endpoint(list_books_endpoint) do |_inputs|
112
+ BookStore.all
113
+ end
114
+
115
+ # Get book by ID endpoint
116
+ get_book_endpoint = RapiTapir.get('/books/:id')
117
+ .summary('Get book by ID')
118
+ .path_param(:id, RapiTapir::Types.integer, description: 'Book ID')
119
+ .ok(BOOK_SCHEMA)
120
+ .build
121
+
122
+ settings.rapitapir.register_endpoint(get_book_endpoint) do |inputs|
123
+ book = BookStore.find(inputs[:id])
124
+ halt 404, { error: 'Book not found' }.to_json unless book
125
+ book
126
+ end
127
+
128
+ # Create book endpoint
129
+ create_book_endpoint = RapiTapir.post('/books')
130
+ .summary('Create new book')
131
+ .json_body(BOOK_SCHEMA)
132
+ .created(BOOK_SCHEMA)
133
+ .build
134
+
135
+ settings.rapitapir.register_endpoint(create_book_endpoint) do |inputs|
136
+ BookStore.create(inputs[:body].transform_keys(&:to_sym))
137
+ end
138
+
139
+ # Published books endpoint
140
+ published_books_endpoint = RapiTapir.get('/books/published')
141
+ .summary('Get published books only')
142
+ .ok(RapiTapir::Types.array(BOOK_SCHEMA))
143
+ .build
144
+
145
+ settings.rapitapir.register_endpoint(published_books_endpoint) do |_inputs|
146
+ BookStore.published
147
+ end
148
+
149
+ configure :development do
150
+ puts "\n📚 Working Bookstore API"
151
+ puts '🌐 Health: http://localhost:4567/health'
152
+ puts '📖 Books: http://localhost:4567/books'
153
+ puts '📋 Published: http://localhost:4567/books/published'
154
+ puts '📚 Documentation: http://localhost:4567/docs'
155
+ puts '📋 OpenAPI: http://localhost:4567/openapi.json'
156
+ puts "\n✅ Using direct SinatraAdapter integration"
157
+ puts '📖 Full OpenAPI 3.0 documentation auto-generated!'
158
+ end
159
+ end
160
+
161
+ WorkingBookstoreAPI.run! if __FILE__ == $PROGRAM_NAME
162
+ else
163
+ # Demo mode when Sinatra is not available
164
+ puts "\n📚 Working Bookstore API - Demo Mode"
165
+ puts '=' * 45
166
+
167
+ puts "\n✅ Successfully loaded:"
168
+ puts ' • RapiTapir core'
169
+ puts ' • Type system'
170
+ puts ' • Book schema and store'
171
+
172
+ puts "\n📊 This API provides:"
173
+ puts ' GET /health - Health check'
174
+ puts ' GET /books - List all books'
175
+ puts ' GET /books/:id - Get book by ID'
176
+ puts ' POST /books - Create new book'
177
+ puts ' GET /books/published - Published books only'
178
+
179
+ puts "\n🔧 Direct SinatraAdapter integration:"
180
+ puts ' • Uses settings.rapitapir.register_endpoint()'
181
+ puts ' • Standard RapiTapir endpoint definitions'
182
+ puts ' • No complex extension magic'
183
+ puts ' • Just works!'
184
+
185
+ puts "\n💡 To run the actual server:"
186
+ puts ' gem install sinatra'
187
+ puts " ruby #{__FILE__}"
188
+
189
+ puts "\n📖 Sample usage with curl:"
190
+ puts ' curl http://localhost:4567/books'
191
+ puts ' curl http://localhost:4567/books/1'
192
+ puts ' curl -X POST http://localhost:4567/books \\'
193
+ puts " -H 'Content-Type: application/json' \\"
194
+ puts " -d '{\"title\":\"New Book\",\"author\":\"Author\",\"isbn\":\"123\",\"published\":true}'"
195
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RapiTapir
4
+ module Auth
5
+ # Configuration class for authentication and security settings
6
+ #
7
+ # Provides centralized configuration for authentication schemes, OAuth2,
8
+ # session management, rate limiting, and CORS settings.
9
+ #
10
+ # @example Configure authentication
11
+ # RapiTapir::Auth.configure do |config|
12
+ # config.jwt_secret = 'your-secret-key'
13
+ # config.oauth2.client_id = 'your-client-id'
14
+ # config.rate_limiting.requests_per_minute = 100
15
+ # end
16
+ class Configuration
17
+ attr_accessor :default_realm, :jwt_secret, :jwt_algorithm
18
+ attr_reader :oauth2, :session, :rate_limiting, :cors
19
+
20
+ def initialize
21
+ @default_realm = 'RapiTapir API'
22
+ @jwt_secret = nil
23
+ @jwt_algorithm = 'HS256'
24
+ @oauth2 = OAuth2Config.new
25
+ @session = SessionConfig.new
26
+ @rate_limiting = RateLimitingConfig.new
27
+ @cors = CorsConfig.new
28
+ end
29
+
30
+ # OAuth2 configuration settings
31
+ #
32
+ # Provides configuration for OAuth2 authentication flow including
33
+ # authorization URLs, token endpoints, and client credentials.
34
+ class OAuth2Config
35
+ attr_accessor :authorization_url, :token_url, :refresh_url, :scopes, :client_id, :client_secret
36
+
37
+ def initialize
38
+ @authorization_url = nil
39
+ @token_url = nil
40
+ @refresh_url = nil
41
+ @client_id = nil
42
+ @client_secret = nil
43
+ @scopes = {}
44
+ end
45
+
46
+ def add_scope(name, description)
47
+ @scopes[name.to_s] = description
48
+ end
49
+ end
50
+
51
+ # Session management configuration
52
+ #
53
+ # Provides settings for session-based authentication including
54
+ # session keys, security settings, and cookie configuration.
55
+ class SessionConfig
56
+ attr_accessor :enabled, :key, :secret, :domain, :path, :secure, :http_only, :same_site
57
+
58
+ def initialize
59
+ @enabled = false
60
+ @key = '_rapitapir_session'
61
+ @secret = nil
62
+ @domain = nil
63
+ @path = '/'
64
+ @secure = false
65
+ @http_only = true
66
+ @same_site = 'Lax'
67
+ end
68
+ end
69
+
70
+ # Rate limiting configuration
71
+ #
72
+ # Provides settings for rate limiting including request limits per time period,
73
+ # burst limits, and client identification methods.
74
+ class RateLimitingConfig
75
+ attr_accessor :enabled, :requests_per_minute, :requests_per_hour,
76
+ :requests_per_day, :burst_limit, :identifier
77
+
78
+ def initialize
79
+ @enabled = false
80
+ @requests_per_minute = 60
81
+ @requests_per_hour = 1000
82
+ @requests_per_day = 10_000
83
+ @burst_limit = 10
84
+ @identifier = :ip_address
85
+ end
86
+
87
+ def enable(requests_per_minute: 60, requests_per_hour: 1000,
88
+ requests_per_day: 10_000, burst_limit: 10, identifier: :ip_address)
89
+ @enabled = true
90
+ @requests_per_minute = requests_per_minute
91
+ @requests_per_hour = requests_per_hour
92
+ @requests_per_day = requests_per_day
93
+ @burst_limit = burst_limit
94
+ @identifier = identifier
95
+ end
96
+ end
97
+
98
+ # CORS (Cross-Origin Resource Sharing) configuration
99
+ #
100
+ # Provides settings for CORS headers including allowed origins, methods,
101
+ # headers, and credentials handling.
102
+ class CorsConfig
103
+ attr_accessor :enabled, :allowed_origins, :allowed_methods, :allowed_headers,
104
+ :exposed_headers, :allow_credentials, :max_age, :preflight_continue
105
+
106
+ def initialize
107
+ @enabled = false
108
+ @allowed_origins = ['*']
109
+ @allowed_methods = %w[GET POST PUT PATCH DELETE OPTIONS HEAD]
110
+ @allowed_headers = %w[Content-Type Authorization Accept X-Requested-With]
111
+ @exposed_headers = []
112
+ @allow_credentials = false
113
+ @max_age = 86_400 # 24 hours
114
+ @preflight_continue = false
115
+ end
116
+
117
+ def enable(allowed_origins: ['*'], allowed_methods: nil, allowed_headers: nil,
118
+ allow_credentials: false, max_age: 86_400)
119
+ @enabled = true
120
+ @allowed_origins = allowed_origins
121
+ @allowed_methods = allowed_methods if allowed_methods
122
+ @allowed_headers = allowed_headers if allowed_headers
123
+ @allow_credentials = allow_credentials
124
+ @max_age = max_age
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RapiTapir
4
+ module Auth
5
+ # Authentication context containing user information and authorization data
6
+ #
7
+ # Represents the authenticated state of a request, including user details,
8
+ # granted scopes, authentication tokens, and session information.
9
+ #
10
+ # @example Create a context
11
+ # context = RapiTapir::Auth::Context.new(
12
+ # user: { id: 123, name: 'John' },
13
+ # scopes: ['read', 'write'],
14
+ # token: 'bearer-token-123'
15
+ # )
16
+ #
17
+ # @example Check permissions
18
+ # context.authenticated? # => true
19
+ # context.has_scope?('read') # => true
20
+ class Context
21
+ attr_reader :user, :scopes, :token, :session, :metadata
22
+
23
+ def initialize(user: nil, scopes: [], token: nil, session: {}, metadata: {})
24
+ @user = user
25
+ @scopes = Array(scopes)
26
+ @token = token
27
+ @session = session || {}
28
+ @metadata = metadata || {}
29
+ end
30
+
31
+ def authenticated?
32
+ !@user.nil?
33
+ end
34
+
35
+ def scope?(scope)
36
+ @scopes.include?(scope.to_s)
37
+ end
38
+ alias has_scope? scope?
39
+
40
+ def any_scope?(*scopes)
41
+ scopes.any? { |scope| scope?(scope) }
42
+ end
43
+ alias has_any_scope? any_scope?
44
+
45
+ def all_scopes?(*scopes)
46
+ scopes.all? { |scope| scope?(scope) }
47
+ end
48
+ alias has_all_scopes? all_scopes?
49
+
50
+ def user_id
51
+ return nil unless @user
52
+
53
+ case @user
54
+ when Hash
55
+ @user[:id] || @user['id']
56
+ when Numeric, String
57
+ @user
58
+ else
59
+ @user.respond_to?(:id) ? @user.id : @user.to_s
60
+ end
61
+ end
62
+
63
+ def add_scope(scope)
64
+ @scopes << scope.to_s unless scope?(scope)
65
+ end
66
+
67
+ def remove_scope(scope)
68
+ @scopes.delete(scope.to_s)
69
+ end
70
+
71
+ def merge(other_context)
72
+ Context.new(
73
+ user: other_context.user || @user,
74
+ scopes: (@scopes + other_context.scopes).uniq,
75
+ token: other_context.token || @token,
76
+ session: @session.merge(other_context.session),
77
+ metadata: @metadata.merge(other_context.metadata)
78
+ )
79
+ end
80
+
81
+ def to_hash
82
+ {
83
+ user: @user,
84
+ scopes: @scopes,
85
+ token: @token,
86
+ session: @session,
87
+ metadata: @metadata,
88
+ authenticated: authenticated?,
89
+ user_id: user_id
90
+ }
91
+ end
92
+
93
+ def inspect
94
+ "#<RapiTapir::Auth::Context user_id=#{user_id.inspect} " \
95
+ "scopes=#{@scopes.inspect} authenticated=#{authenticated?}>"
96
+ end
97
+ end
98
+
99
+ # Thread-local context storage
100
+ class ContextStore
101
+ def self.current
102
+ Thread.current[:rapitapir_auth_context]
103
+ end
104
+
105
+ def self.current=(context)
106
+ Thread.current[:rapitapir_auth_context] = context
107
+ end
108
+
109
+ def self.with_context(context)
110
+ old_context = current
111
+ self.current = context
112
+ yield
113
+ ensure
114
+ self.current = old_context
115
+ end
116
+
117
+ def self.clear
118
+ Thread.current[:rapitapir_auth_context] = nil
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RapiTapir
4
+ module Auth
5
+ # Base authentication error for all authentication-related exceptions
6
+ # This is the parent class for all authentication errors in RapiTapir
7
+ class AuthenticationError < StandardError
8
+ attr_reader :status, :error_code, :error_description
9
+
10
+ def initialize(message, status: 401, error_code: 'authentication_failed', error_description: nil)
11
+ super(message)
12
+ @status = status
13
+ @error_code = error_code
14
+ @error_description = error_description || message
15
+ end
16
+
17
+ def to_hash
18
+ {
19
+ error: @error_code,
20
+ error_description: @error_description,
21
+ status: @status
22
+ }
23
+ end
24
+ end
25
+
26
+ # Authorization error for insufficient permissions
27
+ # Raised when a user lacks required permissions to access a resource
28
+ class AuthorizationError < AuthenticationError
29
+ def initialize(message, error_code: 'insufficient_scope', error_description: nil)
30
+ super(message, status: 403, error_code: error_code, error_description: error_description)
31
+ end
32
+ end
33
+
34
+ # Error for invalid or expired authentication tokens
35
+ # Raised when a provided token is malformed, expired, or otherwise invalid
36
+ class InvalidTokenError < AuthenticationError
37
+ def initialize(message = 'Invalid or expired token', error_code: 'invalid_token')
38
+ super
39
+ end
40
+ end
41
+
42
+ # Error for missing authentication tokens
43
+ # Raised when authentication is required but no token was provided
44
+ class MissingTokenError < AuthenticationError
45
+ def initialize(message = 'Authentication token required', error_code: 'missing_token')
46
+ super
47
+ end
48
+ end
49
+
50
+ # Error for invalid user credentials
51
+ # Raised when provided username/password credentials are incorrect
52
+ class InvalidCredentialsError < AuthenticationError
53
+ def initialize(message = 'Invalid credentials', error_code: 'invalid_credentials')
54
+ super
55
+ end
56
+ end
57
+
58
+ # Error for exceeded rate limits
59
+ # Raised when a client exceeds the configured rate limit for requests
60
+ class RateLimitExceededError < AuthenticationError
61
+ attr_reader :retry_after, :limit, :remaining, :reset_time
62
+
63
+ def initialize(message = 'Rate limit exceeded', retry_after: nil, limit: nil, remaining: 0, reset_time: nil)
64
+ super(message, status: 429, error_code: 'rate_limit_exceeded')
65
+ @retry_after = retry_after
66
+ @limit = limit
67
+ @remaining = remaining
68
+ @reset_time = reset_time
69
+ end
70
+
71
+ def to_hash
72
+ super.merge({
73
+ retry_after: @retry_after,
74
+ limit: @limit,
75
+ remaining: @remaining,
76
+ reset_time: @reset_time&.to_i
77
+ }.compact)
78
+ end
79
+ end
80
+
81
+ # Error for insufficient OAuth scopes
82
+ # Raised when a token lacks the required scopes for an operation
83
+ class ScopeError < AuthorizationError
84
+ attr_reader :required_scopes, :provided_scopes
85
+
86
+ def initialize(required_scopes, provided_scopes = [])
87
+ @required_scopes = Array(required_scopes)
88
+ @provided_scopes = Array(provided_scopes)
89
+
90
+ message = "Insufficient scope. Required: #{@required_scopes.join(', ')}"
91
+ message += ". Provided: #{@provided_scopes.join(', ')}" unless @provided_scopes.empty?
92
+
93
+ super(message, error_code: 'insufficient_scope')
94
+ end
95
+
96
+ def to_hash
97
+ super.merge({
98
+ required_scopes: @required_scopes,
99
+ provided_scopes: @provided_scopes
100
+ })
101
+ end
102
+ end
103
+ end
104
+ end