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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +57 -0
- data/CHANGELOG.md +94 -0
- data/CLEANUP_SUMMARY.md +155 -0
- data/CONTRIBUTING.md +280 -0
- data/LICENSE +21 -0
- data/README.md +485 -0
- data/debug_hash.rb +20 -0
- data/docs/EXTENSION_COMPARISON.md +388 -0
- data/docs/SINATRA_EXTENSION.md +467 -0
- data/docs/archive/PHASE_1_2_COMPLETE.md +77 -0
- data/docs/archive/PHASE_1_3_COMPLETE.md +152 -0
- data/docs/archive/PHASE_2_1_OBSERVABILITY_COMPLETED.md +203 -0
- data/docs/archive/PHASE_2_SUMMARY.md +209 -0
- data/docs/archive/REFACTORING_SUMMARY.md +184 -0
- data/docs/archive/phase_1_3_plan.md +136 -0
- data/docs/archive/sinatra_extension_summary.md +188 -0
- data/docs/archive/sinatra_working_solution.md +113 -0
- data/docs/archive/typescript-client-generator-summary.md +259 -0
- data/docs/auto-derivation.md +146 -0
- data/docs/blueprint.md +1091 -0
- data/docs/endpoint-definition.md +211 -0
- data/docs/github_pages_fix.md +52 -0
- data/docs/github_pages_setup.md +49 -0
- data/docs/implementation-status.md +357 -0
- data/docs/observability.md +647 -0
- data/docs/phase3-plan.md +108 -0
- data/docs/sinatra_rapitapir.md +87 -0
- data/docs/type_shortcuts.md +146 -0
- data/examples/README_ENTERPRISE.md +202 -0
- data/examples/authentication_example.rb +192 -0
- data/examples/auto_derivation_ruby_friendly.rb +163 -0
- data/examples/cli/user_api_endpoints.rb +56 -0
- data/examples/client/typescript_client_example.rb +102 -0
- data/examples/client/user-api-client.ts +193 -0
- data/examples/demo_api.rb +41 -0
- data/examples/docs/documentation_example.rb +112 -0
- data/examples/docs/user-api-docs.html +789 -0
- data/examples/docs/user-api-docs.md +403 -0
- data/examples/enhanced_auto_derivation_test.rb +83 -0
- data/examples/enterprise_extension_demo.rb +417 -0
- data/examples/enterprise_rapitapir_api.rb +662 -0
- data/examples/getting_started_extension.rb +218 -0
- data/examples/hello_world.rb +74 -0
- data/examples/oauth2/.env.example +19 -0
- data/examples/oauth2/README.md +205 -0
- data/examples/oauth2/generic_oauth2_api.rb +226 -0
- data/examples/oauth2/get_token.rb +72 -0
- data/examples/oauth2/songs_api_with_auth0.rb +320 -0
- data/examples/oauth2/test_api.sh +16 -0
- data/examples/oauth2/test_songs_api.sh +110 -0
- data/examples/observability/.env.example +35 -0
- data/examples/observability/README.md +230 -0
- data/examples/observability/README_HONEYCOMB.md +332 -0
- data/examples/observability/advanced_setup.rb +384 -0
- data/examples/observability/basic_setup.rb +192 -0
- data/examples/observability/complete_test.rb +121 -0
- data/examples/observability/honeycomb_example.rb +523 -0
- data/examples/observability/honeycomb_rapitapir_clean.rb +488 -0
- data/examples/observability/honeycomb_rapitapir_example.rb +523 -0
- data/examples/observability/honeycomb_working_example.rb +489 -0
- data/examples/observability/quick_test.rb +78 -0
- data/examples/observability/simple_test.rb +14 -0
- data/examples/observability/test_honeycomb_demo.rb +354 -0
- data/examples/observability/test_live_honeycomb.rb +111 -0
- data/examples/observability/test_validation.rb +78 -0
- data/examples/observability/test_working_validation.rb +66 -0
- data/examples/openapi/user_api_schema.rb +132 -0
- data/examples/production_ready_example.rb +105 -0
- data/examples/rails/users_controller.rb +146 -0
- data/examples/readme/basic_sinatra_example.rb +128 -0
- data/examples/server/user_api.rb +179 -0
- data/examples/simple_auto_derivation_demo.rb +44 -0
- data/examples/simple_demo_api.rb +18 -0
- data/examples/sinatra/user_app.rb +127 -0
- data/examples/t_shortcut_demo.rb +59 -0
- data/examples/user_api.rb +190 -0
- data/examples/working_getting_started.rb +184 -0
- data/examples/working_simple_example.rb +195 -0
- data/lib/rapitapir/auth/configuration.rb +129 -0
- data/lib/rapitapir/auth/context.rb +122 -0
- data/lib/rapitapir/auth/errors.rb +104 -0
- data/lib/rapitapir/auth/middleware.rb +324 -0
- data/lib/rapitapir/auth/oauth2.rb +350 -0
- data/lib/rapitapir/auth/schemes.rb +420 -0
- data/lib/rapitapir/auth.rb +113 -0
- data/lib/rapitapir/cli/command.rb +535 -0
- data/lib/rapitapir/cli/server.rb +243 -0
- data/lib/rapitapir/cli/validator.rb +373 -0
- data/lib/rapitapir/client/generator_base.rb +272 -0
- data/lib/rapitapir/client/typescript_generator.rb +350 -0
- data/lib/rapitapir/core/endpoint.rb +158 -0
- data/lib/rapitapir/core/enhanced_endpoint.rb +235 -0
- data/lib/rapitapir/core/input.rb +182 -0
- data/lib/rapitapir/core/output.rb +164 -0
- data/lib/rapitapir/core/request.rb +19 -0
- data/lib/rapitapir/core/response.rb +17 -0
- data/lib/rapitapir/docs/html_generator.rb +780 -0
- data/lib/rapitapir/docs/markdown_generator.rb +464 -0
- data/lib/rapitapir/dsl/endpoint_dsl.rb +116 -0
- data/lib/rapitapir/dsl/enhanced_endpoint_dsl.rb +62 -0
- data/lib/rapitapir/dsl/enhanced_input.rb +73 -0
- data/lib/rapitapir/dsl/enhanced_output.rb +63 -0
- data/lib/rapitapir/dsl/enhanced_structures.rb +393 -0
- data/lib/rapitapir/dsl/fluent_dsl.rb +72 -0
- data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +316 -0
- data/lib/rapitapir/dsl/http_verbs.rb +77 -0
- data/lib/rapitapir/dsl/input_methods.rb +47 -0
- data/lib/rapitapir/dsl/observability_methods.rb +81 -0
- data/lib/rapitapir/dsl/output_methods.rb +43 -0
- data/lib/rapitapir/dsl/type_resolution.rb +43 -0
- data/lib/rapitapir/observability/configuration.rb +108 -0
- data/lib/rapitapir/observability/health_check.rb +236 -0
- data/lib/rapitapir/observability/logging.rb +270 -0
- data/lib/rapitapir/observability/metrics.rb +203 -0
- data/lib/rapitapir/observability/middleware.rb +243 -0
- data/lib/rapitapir/observability/tracing.rb +143 -0
- data/lib/rapitapir/observability.rb +28 -0
- data/lib/rapitapir/openapi/schema_generator.rb +403 -0
- data/lib/rapitapir/schema.rb +136 -0
- data/lib/rapitapir/server/enhanced_rack_adapter.rb +379 -0
- data/lib/rapitapir/server/middleware.rb +120 -0
- data/lib/rapitapir/server/path_matcher.rb +45 -0
- data/lib/rapitapir/server/rack_adapter.rb +215 -0
- data/lib/rapitapir/server/rails_adapter.rb +17 -0
- data/lib/rapitapir/server/rails_adapter_class.rb +53 -0
- data/lib/rapitapir/server/rails_controller.rb +72 -0
- data/lib/rapitapir/server/rails_input_processor.rb +73 -0
- data/lib/rapitapir/server/rails_response_handler.rb +29 -0
- data/lib/rapitapir/server/sinatra_adapter.rb +200 -0
- data/lib/rapitapir/server/sinatra_integration.rb +93 -0
- data/lib/rapitapir/sinatra/configuration.rb +91 -0
- data/lib/rapitapir/sinatra/extension.rb +214 -0
- data/lib/rapitapir/sinatra/oauth2_helpers.rb +236 -0
- data/lib/rapitapir/sinatra/resource_builder.rb +152 -0
- data/lib/rapitapir/sinatra/swagger_ui_generator.rb +166 -0
- data/lib/rapitapir/sinatra_rapitapir.rb +40 -0
- data/lib/rapitapir/types/array.rb +163 -0
- data/lib/rapitapir/types/auto_derivation.rb +265 -0
- data/lib/rapitapir/types/base.rb +146 -0
- data/lib/rapitapir/types/boolean.rb +46 -0
- data/lib/rapitapir/types/date.rb +92 -0
- data/lib/rapitapir/types/datetime.rb +98 -0
- data/lib/rapitapir/types/email.rb +32 -0
- data/lib/rapitapir/types/float.rb +134 -0
- data/lib/rapitapir/types/hash.rb +161 -0
- data/lib/rapitapir/types/integer.rb +143 -0
- data/lib/rapitapir/types/object.rb +156 -0
- data/lib/rapitapir/types/optional.rb +65 -0
- data/lib/rapitapir/types/string.rb +185 -0
- data/lib/rapitapir/types/uuid.rb +32 -0
- data/lib/rapitapir/types.rb +155 -0
- data/lib/rapitapir/version.rb +5 -0
- data/lib/rapitapir.rb +173 -0
- data/rapitapir.gemspec +66 -0
- 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
|