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,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.