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,218 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RapiTapir Sinatra Extension - Getting Started Example
|
4
|
+
#
|
5
|
+
# This minimal example shows how easy it is to create an enterprise-grade
|
6
|
+
# API with the RapiTapir Sinatra Extension - zero boilerplate!
|
7
|
+
|
8
|
+
# Check for Sinatra availability
|
9
|
+
begin
|
10
|
+
require 'sinatra/base'
|
11
|
+
SINATRA_AVAILABLE = true
|
12
|
+
rescue LoadError
|
13
|
+
SINATRA_AVAILABLE = false
|
14
|
+
puts '⚠️ Sinatra not available. Install with: gem install sinatra'
|
15
|
+
puts '🔄 Running in demo mode instead...'
|
16
|
+
end
|
17
|
+
|
18
|
+
require_relative '../lib/rapitapir'
|
19
|
+
|
20
|
+
# Only require extension if Sinatra is available
|
21
|
+
# Note: Extension and SinatraRapiTapir are auto-loaded when requiring rapitapir
|
22
|
+
|
23
|
+
# Simple in-memory data store
|
24
|
+
class BookStore
|
25
|
+
@@books = [
|
26
|
+
{ id: 1, title: 'The Ruby Programming Language', author: 'Matz', isbn: '978-0596516178', published: true },
|
27
|
+
{ id: 2, title: 'Metaprogramming Ruby', author: 'Paolo Perrotta', isbn: '978-1934356470', published: true }
|
28
|
+
]
|
29
|
+
@@next_id = 3
|
30
|
+
|
31
|
+
def self.all
|
32
|
+
@@books
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.find(id)
|
36
|
+
@@books.find { |book| book[:id] == id.to_i }
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.create(attrs)
|
40
|
+
book = attrs.merge(id: @@next_id)
|
41
|
+
@@next_id += 1
|
42
|
+
@@books << book
|
43
|
+
book
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.update(id, attrs)
|
47
|
+
book = find(id)
|
48
|
+
return nil unless book
|
49
|
+
|
50
|
+
attrs.each { |key, value| book[key] = value }
|
51
|
+
book
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.delete(id)
|
55
|
+
@@books.reject! { |book| book[:id] == id.to_i }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Define the book schema
|
60
|
+
BOOK_SCHEMA = RapiTapir::Types.hash({
|
61
|
+
'id' => RapiTapir::Types.integer,
|
62
|
+
'title' => RapiTapir::Types.string,
|
63
|
+
'author' => RapiTapir::Types.string,
|
64
|
+
'isbn' => RapiTapir::Types.string,
|
65
|
+
'published' => RapiTapir::Types.boolean
|
66
|
+
})
|
67
|
+
|
68
|
+
# Schema for creating books (without ID)
|
69
|
+
BOOK_CREATE_SCHEMA = RapiTapir::Types.hash({
|
70
|
+
'title' => RapiTapir::Types.string,
|
71
|
+
'author' => RapiTapir::Types.string,
|
72
|
+
'isbn' => RapiTapir::Types.string,
|
73
|
+
'published' => RapiTapir::Types.boolean
|
74
|
+
})
|
75
|
+
|
76
|
+
# Your API application - incredibly simple!
|
77
|
+
if SINATRA_AVAILABLE
|
78
|
+
class BookstoreAPI < SinatraRapiTapir
|
79
|
+
|
80
|
+
# One-line configuration for the entire API
|
81
|
+
rapitapir do
|
82
|
+
info(
|
83
|
+
title: 'Bookstore API',
|
84
|
+
description: 'A simple bookstore API built with SinatraRapiTapir',
|
85
|
+
version: '1.0.0'
|
86
|
+
)
|
87
|
+
|
88
|
+
development_defaults! # Sets up CORS, rate limiting, docs, health check, etc.
|
89
|
+
add_public_paths('/books') # No auth required for books (health check is auto-public)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Full RESTful books resource - individual endpoints for better control
|
93
|
+
|
94
|
+
# List all books
|
95
|
+
endpoint(
|
96
|
+
GET('/books')
|
97
|
+
.summary('List all books')
|
98
|
+
.ok(RapiTapir::Types.array(BOOK_SCHEMA))
|
99
|
+
.build
|
100
|
+
) { BookStore.all }
|
101
|
+
|
102
|
+
# Get published books only (MUST come before /books/:id)
|
103
|
+
endpoint(
|
104
|
+
GET('/books/published')
|
105
|
+
.summary('Get published books')
|
106
|
+
.ok(RapiTapir::Types.array(BOOK_SCHEMA))
|
107
|
+
.build
|
108
|
+
) { BookStore.all.select { |book| book[:published] } }
|
109
|
+
|
110
|
+
# Get book by ID
|
111
|
+
endpoint(
|
112
|
+
GET('/books/:id')
|
113
|
+
.path_param(:id, RapiTapir::Types.integer)
|
114
|
+
.summary('Get book by ID')
|
115
|
+
.ok(BOOK_SCHEMA)
|
116
|
+
.not_found(RapiTapir::Types.hash({ 'error' => RapiTapir::Types.string }))
|
117
|
+
.build
|
118
|
+
) do |inputs|
|
119
|
+
book = BookStore.find(inputs[:id])
|
120
|
+
raise ArgumentError, 'Book not found' unless book
|
121
|
+
book
|
122
|
+
end
|
123
|
+
|
124
|
+
# Create new book
|
125
|
+
endpoint(
|
126
|
+
POST('/books')
|
127
|
+
.summary('Create new book')
|
128
|
+
.json_body(BOOK_CREATE_SCHEMA)
|
129
|
+
.created(BOOK_SCHEMA)
|
130
|
+
.build
|
131
|
+
) do |inputs|
|
132
|
+
attrs = inputs[:body].transform_keys(&:to_sym)
|
133
|
+
attrs[:published] = true if attrs[:published].nil? # Default to published
|
134
|
+
BookStore.create(attrs)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Update book
|
138
|
+
endpoint(
|
139
|
+
PUT('/books/:id')
|
140
|
+
.path_param(:id, RapiTapir::Types.integer)
|
141
|
+
.json_body(BOOK_CREATE_SCHEMA)
|
142
|
+
.summary('Update book')
|
143
|
+
.ok(BOOK_SCHEMA)
|
144
|
+
.not_found(RapiTapir::Types.hash({ 'error' => RapiTapir::Types.string }))
|
145
|
+
.build
|
146
|
+
) do |inputs|
|
147
|
+
book = BookStore.find(inputs[:id])
|
148
|
+
raise ArgumentError, 'Book not found' unless book
|
149
|
+
attrs = inputs[:body].transform_keys(&:to_sym)
|
150
|
+
BookStore.update(inputs[:id], attrs)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Delete book
|
154
|
+
endpoint(
|
155
|
+
DELETE('/books/:id')
|
156
|
+
.path_param(:id, RapiTapir::Types.integer)
|
157
|
+
.summary('Delete book')
|
158
|
+
.no_content
|
159
|
+
.not_found(RapiTapir::Types.hash({ 'error' => RapiTapir::Types.string }))
|
160
|
+
.build
|
161
|
+
) do |inputs|
|
162
|
+
book = BookStore.find(inputs[:id])
|
163
|
+
raise ArgumentError, 'Book not found' unless book
|
164
|
+
BookStore.delete(inputs[:id])
|
165
|
+
'' # Empty content for 204
|
166
|
+
end
|
167
|
+
|
168
|
+
configure :development do
|
169
|
+
puts "\n📚 Bookstore API with SinatraRapiTapir"
|
170
|
+
puts "🚀 Clean syntax: class BookstoreAPI < SinatraRapiTapir"
|
171
|
+
puts '🌐 Documentation: http://localhost:4567/docs'
|
172
|
+
puts '📋 OpenAPI: http://localhost:4567/openapi.json'
|
173
|
+
puts '❤️ Health: http://localhost:4567/health'
|
174
|
+
puts '📖 Books: http://localhost:4567/books'
|
175
|
+
puts "\n✨ Zero boilerplate, full enterprise features!"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
BookstoreAPI.run! if __FILE__ == $PROGRAM_NAME
|
180
|
+
else
|
181
|
+
# Demo mode when Sinatra is not available
|
182
|
+
puts "\n📚 RapiTapir Sinatra Extension - Demo Mode"
|
183
|
+
puts '=' * 50
|
184
|
+
|
185
|
+
puts "\n✅ Successfully loaded:"
|
186
|
+
puts ' • RapiTapir core'
|
187
|
+
puts ' • Type system'
|
188
|
+
puts ' • Book schema'
|
189
|
+
|
190
|
+
puts "\n📊 This API would provide:"
|
191
|
+
puts ' GET /health - Health check'
|
192
|
+
puts ' GET /books - List all books'
|
193
|
+
puts ' GET /books/:id - Get book by ID'
|
194
|
+
puts ' POST /books - Create new book'
|
195
|
+
puts ' PUT /books/:id - Update book'
|
196
|
+
puts ' GET /books/published - Published books only'
|
197
|
+
puts ' GET /docs - Swagger UI documentation'
|
198
|
+
puts ' GET /openapi.json - OpenAPI 3.0 specification'
|
199
|
+
|
200
|
+
puts "\n🎯 Extension features:"
|
201
|
+
puts ' • Zero boilerplate configuration'
|
202
|
+
puts ' • RESTful resource builder (crud block)'
|
203
|
+
puts ' • Built-in authentication helpers'
|
204
|
+
puts ' • Auto-generated OpenAPI documentation'
|
205
|
+
puts ' • Production middleware (CORS, rate limiting, security)'
|
206
|
+
puts ' • Custom endpoints with configure block'
|
207
|
+
|
208
|
+
puts "\n💡 To run the actual server:"
|
209
|
+
puts ' gem install sinatra'
|
210
|
+
puts " ruby #{__FILE__}"
|
211
|
+
|
212
|
+
puts "\n📖 Sample usage with curl:"
|
213
|
+
puts ' curl http://localhost:4567/books'
|
214
|
+
puts ' curl http://localhost:4567/books/1'
|
215
|
+
puts ' curl -X POST http://localhost:4567/books \\'
|
216
|
+
puts " -H 'Content-Type: application/json' \\"
|
217
|
+
puts " -d '{\"title\":\"New Book\",\"author\":\"Author\",\"isbn\":\"123\",\"published\":true}'"
|
218
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RapiTapir Sinatra Extension - Hello World Example
|
4
|
+
#
|
5
|
+
# The most minimal example showing how to create a beautiful, type-safe API
|
6
|
+
# with automatic OpenAPI documentation in just a few lines of code!
|
7
|
+
|
8
|
+
require 'sinatra/base'
|
9
|
+
require_relative '../lib/rapitapir'
|
10
|
+
|
11
|
+
# Your entire API in under 20 lines! 🚀
|
12
|
+
class HelloWorldAPI < SinatraRapiTapir
|
13
|
+
|
14
|
+
# One-line API configuration
|
15
|
+
rapitapir do
|
16
|
+
info(title: 'Hello World API', version: '1.0.0')
|
17
|
+
development_defaults! # Auto CORS, docs, health checks, etc.
|
18
|
+
end
|
19
|
+
|
20
|
+
# Hello World endpoint - beautifully typed and documented using enhanced DSL
|
21
|
+
endpoint(
|
22
|
+
GET('/hello')
|
23
|
+
.query(:name, RapiTapir::Types.optional(RapiTapir::Types.string))
|
24
|
+
.summary('Say hello to someone')
|
25
|
+
.description('Returns a personalized greeting')
|
26
|
+
.tags('Greetings')
|
27
|
+
.ok(RapiTapir::Types.hash({
|
28
|
+
'message' => RapiTapir::Types.string,
|
29
|
+
'timestamp' => RapiTapir::Types.string
|
30
|
+
}))
|
31
|
+
.build
|
32
|
+
) do |inputs|
|
33
|
+
name = inputs[:name] || 'World'
|
34
|
+
{
|
35
|
+
message: "Hello, #{name}!",
|
36
|
+
timestamp: Time.now.iso8601
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
# Another endpoint showing path parameters with enhanced DSL
|
41
|
+
endpoint(
|
42
|
+
GET('/greet/:language')
|
43
|
+
.path_param(:language, RapiTapir::Types.string)
|
44
|
+
.summary('Multilingual greeting')
|
45
|
+
.tags('Greetings')
|
46
|
+
.ok(RapiTapir::Types.hash({ 'greeting' => RapiTapir::Types.string }))
|
47
|
+
.build
|
48
|
+
) do |inputs|
|
49
|
+
greetings = {
|
50
|
+
'english' => 'Hello!',
|
51
|
+
'spanish' => '¡Hola!',
|
52
|
+
'french' => 'Bonjour!',
|
53
|
+
'italian' => 'Ciao!',
|
54
|
+
'german' => 'Hallo!',
|
55
|
+
'japanese' => 'こんにちは!'
|
56
|
+
}
|
57
|
+
|
58
|
+
greeting = greetings[inputs[:language].downcase] || 'Hello!'
|
59
|
+
{ greeting: greeting }
|
60
|
+
end
|
61
|
+
|
62
|
+
configure :development do
|
63
|
+
puts "\n🌟 Hello World API with SinatraRapiTapir base class"
|
64
|
+
puts "🚀 Clean syntax: class HelloWorldAPI < SinatraRapiTapir"
|
65
|
+
puts "🌐 Swagger UI: http://localhost:4567/docs"
|
66
|
+
puts "📋 OpenAPI: http://localhost:4567/openapi.json"
|
67
|
+
puts "👋 Try it: http://localhost:4567/hello?name=Developer"
|
68
|
+
puts "🌍 Languages: http://localhost:4567/greet/spanish"
|
69
|
+
puts "❤️ Health: http://localhost:4567/health"
|
70
|
+
puts "\n✨ Beautiful, type-safe API in under 15 lines of code!"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
HelloWorldAPI.run! if __FILE__ == $PROGRAM_NAME
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# Auth0 Configuration for Generic OAuth2 API Testing
|
2
|
+
# Copy this to .env and fill in your Auth0 details
|
3
|
+
|
4
|
+
# Auth0 Domain (e.g., your-tenant.auth0.com)
|
5
|
+
AUTH0_DOMAIN=your-tenant.auth0.com
|
6
|
+
|
7
|
+
# Auth0 API Audience (configured in your Auth0 API settings)
|
8
|
+
AUTH0_AUDIENCE=https://your-api-identifier
|
9
|
+
|
10
|
+
# Auth0 Client ID (for testing with Machine-to-Machine app)
|
11
|
+
AUTH0_CLIENT_ID=your-m2m-client-id
|
12
|
+
|
13
|
+
# Auth0 Client Secret (for testing with Machine-to-Machine app)
|
14
|
+
AUTH0_CLIENT_SECRET=your-m2m-client-secret
|
15
|
+
|
16
|
+
# Alternative: OAuth2 Generic Introspection (if not using Auth0)
|
17
|
+
# OAUTH2_INTROSPECTION_ENDPOINT=https://your-oauth-server/introspect
|
18
|
+
# OAUTH2_CLIENT_ID=your-client-id
|
19
|
+
# OAUTH2_CLIENT_SECRET=your-client-secret
|
@@ -0,0 +1,205 @@
|
|
1
|
+
# OAuth2 Examples
|
2
|
+
|
3
|
+
This directory contains examples demonstrating OAuth2 integration with RapiTapir.
|
4
|
+
|
5
|
+
## Examples
|
6
|
+
|
7
|
+
### 1. Songs API with Auth0 (`songs_api_with_auth0.rb`)
|
8
|
+
|
9
|
+
A complete example using Auth0 for OAuth2 authentication. Demonstrates:
|
10
|
+
|
11
|
+
- Auth0-specific JWT validation with JWKS
|
12
|
+
- Scope-based authorization
|
13
|
+
- Protected endpoints with different permission levels
|
14
|
+
- Comprehensive error handling
|
15
|
+
- Rate limiting integration
|
16
|
+
|
17
|
+
**Features:**
|
18
|
+
- Public endpoints (health check, song list)
|
19
|
+
- Protected endpoints requiring authentication
|
20
|
+
- Admin endpoints requiring special scopes
|
21
|
+
- Token introspection and validation
|
22
|
+
- JWKS caching for performance
|
23
|
+
|
24
|
+
**Setup:**
|
25
|
+
```bash
|
26
|
+
# Set Auth0 environment variables
|
27
|
+
export AUTH0_DOMAIN="your-tenant.auth0.com"
|
28
|
+
export AUTH0_CLIENT_ID="your-client-id"
|
29
|
+
export AUTH0_CLIENT_SECRET="your-client-secret"
|
30
|
+
export AUTH0_AUDIENCE="your-api-identifier"
|
31
|
+
|
32
|
+
# Run the example
|
33
|
+
ruby songs_api_with_auth0.rb
|
34
|
+
```
|
35
|
+
|
36
|
+
### 2. Generic OAuth2 API (`generic_oauth2_api.rb`)
|
37
|
+
|
38
|
+
A simpler example using generic OAuth2 token introspection. Demonstrates:
|
39
|
+
|
40
|
+
- Token introspection with any OAuth2 provider
|
41
|
+
- Basic scope validation
|
42
|
+
- User information retrieval
|
43
|
+
- Authentication status checking
|
44
|
+
|
45
|
+
**Features:**
|
46
|
+
- Generic OAuth2 provider support
|
47
|
+
- Token introspection endpoint
|
48
|
+
- Basic scope-based authorization
|
49
|
+
- User context access
|
50
|
+
|
51
|
+
**Setup:**
|
52
|
+
```bash
|
53
|
+
# Set OAuth2 provider environment variables
|
54
|
+
export OAUTH2_INTROSPECTION_ENDPOINT="https://your-oauth-server/introspect"
|
55
|
+
export OAUTH2_CLIENT_ID="your-client-id"
|
56
|
+
export OAUTH2_CLIENT_SECRET="your-client-secret"
|
57
|
+
|
58
|
+
# Run the example
|
59
|
+
ruby generic_oauth2_api.rb
|
60
|
+
```
|
61
|
+
|
62
|
+
## Authentication Flow
|
63
|
+
|
64
|
+
### Auth0 Example
|
65
|
+
1. Client obtains JWT token from Auth0
|
66
|
+
2. Client includes token in `Authorization: Bearer <token>` header
|
67
|
+
3. RapiTapir validates JWT signature using JWKS
|
68
|
+
4. Endpoint handler receives authenticated user context
|
69
|
+
|
70
|
+
### Generic OAuth2 Example
|
71
|
+
1. Client obtains access token from OAuth2 provider
|
72
|
+
2. Client includes token in `Authorization: Bearer <token>` header
|
73
|
+
3. RapiTapir introspects token with OAuth2 provider
|
74
|
+
4. Endpoint handler receives authenticated user context
|
75
|
+
|
76
|
+
## Testing the APIs
|
77
|
+
|
78
|
+
### Using curl with Auth0
|
79
|
+
|
80
|
+
```bash
|
81
|
+
# Get a token (example with Auth0 Client Credentials flow)
|
82
|
+
TOKEN=$(curl -s -X POST "https://YOUR_DOMAIN.auth0.com/oauth/token" \
|
83
|
+
-H "Content-Type: application/json" \
|
84
|
+
-d '{
|
85
|
+
"client_id": "YOUR_CLIENT_ID",
|
86
|
+
"client_secret": "YOUR_CLIENT_SECRET",
|
87
|
+
"audience": "YOUR_API_IDENTIFIER",
|
88
|
+
"grant_type": "client_credentials"
|
89
|
+
}' | jq -r '.access_token')
|
90
|
+
|
91
|
+
# Test protected endpoint
|
92
|
+
curl -H "Authorization: Bearer $TOKEN" \
|
93
|
+
http://localhost:4567/songs
|
94
|
+
```
|
95
|
+
|
96
|
+
### Using curl with Generic OAuth2
|
97
|
+
|
98
|
+
```bash
|
99
|
+
# Assuming you have an access token from your OAuth2 provider
|
100
|
+
TOKEN="your-access-token"
|
101
|
+
|
102
|
+
# Test protected endpoint
|
103
|
+
curl -H "Authorization: Bearer $TOKEN" \
|
104
|
+
http://localhost:4567/tasks
|
105
|
+
```
|
106
|
+
|
107
|
+
## Available Endpoints
|
108
|
+
|
109
|
+
### Songs API (Auth0)
|
110
|
+
- `GET /` - Public welcome message
|
111
|
+
- `GET /health` - Health check with auth status
|
112
|
+
- `GET /songs` - List songs (requires authentication)
|
113
|
+
- `POST /songs` - Create song (requires `write:songs` scope)
|
114
|
+
- `PUT /songs/:id` - Update song (requires `write:songs` scope)
|
115
|
+
- `DELETE /songs/:id` - Delete song (requires `admin:songs` scope)
|
116
|
+
- `GET /admin/stats` - Admin statistics (requires `admin:songs` scope)
|
117
|
+
|
118
|
+
### Generic OAuth2 API
|
119
|
+
- `GET /tasks` - List tasks (public)
|
120
|
+
- `GET /health` - Health check with auth status
|
121
|
+
- `POST /tasks` - Create task (requires `write` scope)
|
122
|
+
- `PUT /tasks/:id` - Update task (requires `write` scope)
|
123
|
+
- `DELETE /tasks/:id` - Delete task (requires `admin` scope)
|
124
|
+
- `GET /me` - Get user information (requires authentication)
|
125
|
+
|
126
|
+
## Documentation
|
127
|
+
|
128
|
+
Both examples include automatic OpenAPI documentation:
|
129
|
+
- Auth0 Example: http://localhost:4567/docs
|
130
|
+
- Generic OAuth2 Example: http://localhost:4567/docs
|
131
|
+
|
132
|
+
## Error Handling
|
133
|
+
|
134
|
+
Both examples demonstrate comprehensive error handling:
|
135
|
+
|
136
|
+
- **401 Unauthorized**: Missing or invalid token
|
137
|
+
- **403 Forbidden**: Token valid but insufficient scopes
|
138
|
+
- **404 Not Found**: Resource not found
|
139
|
+
- **422 Unprocessable Entity**: Invalid request data
|
140
|
+
|
141
|
+
## Security Features
|
142
|
+
|
143
|
+
- **JWT Validation**: Signature verification and claims validation
|
144
|
+
- **Scope Authorization**: Granular permission control
|
145
|
+
- **Token Caching**: JWKS and token introspection caching
|
146
|
+
- **Rate Limiting**: Built-in rate limiting support
|
147
|
+
- **Secure Headers**: Automatic security headers
|
148
|
+
- **CORS Support**: Cross-origin resource sharing
|
149
|
+
|
150
|
+
## Production Considerations
|
151
|
+
|
152
|
+
1. **Environment Variables**: Never hardcode credentials
|
153
|
+
2. **HTTPS Only**: Always use HTTPS in production
|
154
|
+
3. **Token Expiration**: Handle token refresh properly
|
155
|
+
4. **Error Logging**: Log authentication failures securely
|
156
|
+
5. **Rate Limiting**: Implement appropriate rate limits
|
157
|
+
6. **Monitoring**: Monitor authentication metrics
|
158
|
+
|
159
|
+
## Integration Patterns
|
160
|
+
|
161
|
+
### Middleware Integration
|
162
|
+
```ruby
|
163
|
+
# Automatic authentication for all endpoints
|
164
|
+
rapitapir do
|
165
|
+
default_auth oauth2_auth(scopes: ['read'])
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
### Conditional Authentication
|
170
|
+
```ruby
|
171
|
+
# Different auth requirements per endpoint
|
172
|
+
endpoint(
|
173
|
+
GET('/public').build
|
174
|
+
) { "Public data" }
|
175
|
+
|
176
|
+
endpoint(
|
177
|
+
GET('/private').with_oauth2_auth.build
|
178
|
+
) { "Private data" }
|
179
|
+
```
|
180
|
+
|
181
|
+
### Custom Scope Validation
|
182
|
+
```ruby
|
183
|
+
# Custom authorization logic
|
184
|
+
def admin_required!
|
185
|
+
context = authorize_oauth2!(required_scopes: ['admin'])
|
186
|
+
halt 403 unless context.metadata[:role] == 'admin'
|
187
|
+
end
|
188
|
+
```
|
189
|
+
|
190
|
+
## Troubleshooting
|
191
|
+
|
192
|
+
### Common Issues
|
193
|
+
|
194
|
+
1. **JWKS Fetch Errors**: Check Auth0 domain and network connectivity
|
195
|
+
2. **Token Validation Failures**: Verify audience and issuer claims
|
196
|
+
3. **Scope Mismatches**: Ensure client has required scopes
|
197
|
+
4. **Environment Variables**: Double-check all required variables are set
|
198
|
+
|
199
|
+
### Debug Mode
|
200
|
+
|
201
|
+
Set `RACK_ENV=development` to enable detailed error messages and logging.
|
202
|
+
|
203
|
+
### Testing Without OAuth2
|
204
|
+
|
205
|
+
Both examples include public endpoints that don't require authentication, useful for testing basic functionality.
|