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,662 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Enterprise-grade Sinatra API with RapiTapir - Using SinatraAdapter
4
+ #
5
+ # This example demonstrates a production-ready Sinatra application with:
6
+ # - Bearer Token Authentication
7
+ # - Auto-generated OpenAPI 3.0 documentation from RapiTapir endpoint definitions
8
+ # - Request/Response validation with SinatraAdapter
9
+ # - Error handling
10
+ # - Rate limiting
11
+ # - CORS support
12
+ # - Security headers
13
+
14
+ require 'sinatra/base'
15
+ require 'json'
16
+ require_relative '../lib/rapitapir'
17
+ require_relative '../lib/rapitapir/server/sinatra_adapter'
18
+
19
+ # Sample User Database (In production, use a real database)
20
+ class UserDatabase
21
+ USERS = {
22
+ 'user-token-123' => {
23
+ id: 1,
24
+ name: 'John Doe',
25
+ email: 'john.doe@example.com',
26
+ role: 'user',
27
+ scopes: %w[read write]
28
+ },
29
+ 'admin-token-456' => {
30
+ id: 2,
31
+ name: 'Jane Admin',
32
+ email: 'jane.admin@example.com',
33
+ role: 'admin',
34
+ scopes: %w[read write admin delete]
35
+ },
36
+ 'readonly-token-789' => {
37
+ id: 3,
38
+ name: 'Bob Reader',
39
+ email: 'bob.reader@example.com',
40
+ role: 'readonly',
41
+ scopes: ['read']
42
+ }
43
+ }.freeze
44
+
45
+ def self.find_by_token(token)
46
+ USERS[token]
47
+ end
48
+
49
+ def self.all_users
50
+ USERS.values
51
+ end
52
+
53
+ def self.find_by_id(id)
54
+ USERS.values.find { |user| user[:id] == id.to_i }
55
+ end
56
+ end
57
+
58
+ # Task Database (In production, use a real database)
59
+ class TaskDatabase
60
+ @@tasks = [
61
+ { id: 1, title: 'Setup CI/CD Pipeline', description: 'Configure automated testing and deployment',
62
+ status: 'in_progress', assignee_id: 1, created_at: Time.now - 86_400 },
63
+ { id: 2, title: 'Review Security Audit', description: 'Complete quarterly security review', status: 'pending',
64
+ assignee_id: 2, created_at: Time.now - 3600 },
65
+ { id: 3, title: 'Update Documentation', description: 'Refresh API documentation', status: 'completed',
66
+ assignee_id: 1, created_at: Time.now - 172_800 }
67
+ ]
68
+ @@next_id = 4
69
+
70
+ def self.all
71
+ @@tasks
72
+ end
73
+
74
+ def self.find(id)
75
+ @@tasks.find { |task| task[:id] == id.to_i }
76
+ end
77
+
78
+ def self.create(attrs)
79
+ task = attrs.merge(id: @@next_id, created_at: Time.now)
80
+ @@next_id += 1
81
+ @@tasks << task
82
+ task
83
+ end
84
+
85
+ def self.update(id, attrs)
86
+ task = find(id)
87
+ return nil unless task
88
+
89
+ attrs.each { |key, value| task[key] = value }
90
+ task[:updated_at] = Time.now
91
+ task
92
+ end
93
+
94
+ def self.delete(id)
95
+ @@tasks.reject! { |task| task[:id] == id.to_i }
96
+ end
97
+
98
+ def self.by_assignee(assignee_id)
99
+ @@tasks.select { |task| task[:assignee_id] == assignee_id.to_i }
100
+ end
101
+ end
102
+
103
+ # RapiTapir Endpoint Definitions
104
+ module TaskAPI
105
+ extend RapiTapir::DSL
106
+
107
+ # Define schemas using RapiTapir types
108
+ TASK_SCHEMA = RapiTapir::Types.hash({
109
+ 'id' => RapiTapir::Types.integer,
110
+ 'title' => RapiTapir::Types.string,
111
+ 'description' => RapiTapir::Types.string,
112
+ 'status' => RapiTapir::Types.string,
113
+ 'assignee_id' => RapiTapir::Types.integer,
114
+ 'created_at' => RapiTapir::Types.string,
115
+ 'updated_at' => RapiTapir::Types.optional(RapiTapir::Types.string)
116
+ })
117
+
118
+ TASK_CREATE_SCHEMA = RapiTapir::Types.hash({
119
+ 'title' => RapiTapir::Types.string,
120
+ 'description' => RapiTapir::Types.string,
121
+ 'status' => RapiTapir::Types.optional(RapiTapir::Types.string),
122
+ 'assignee_id' => RapiTapir::Types.integer
123
+ })
124
+
125
+ TASK_UPDATE_SCHEMA = RapiTapir::Types.hash({
126
+ 'title' => RapiTapir::Types.optional(RapiTapir::Types.string),
127
+ 'description' => RapiTapir::Types.optional(RapiTapir::Types.string),
128
+ 'status' => RapiTapir::Types.optional(RapiTapir::Types.string),
129
+ 'assignee_id' => RapiTapir::Types.optional(RapiTapir::Types.integer)
130
+ })
131
+
132
+ USER_SCHEMA = RapiTapir::Types.hash({
133
+ 'id' => RapiTapir::Types.integer,
134
+ 'name' => RapiTapir::Types.string,
135
+ 'email' => RapiTapir::Types.string,
136
+ 'role' => RapiTapir::Types.string,
137
+ 'scopes' => RapiTapir::Types.array(RapiTapir::Types.string)
138
+ })
139
+
140
+ ERROR_SCHEMA = RapiTapir::Types.hash({
141
+ 'error' => RapiTapir::Types.string
142
+ })
143
+
144
+ HEALTH_SCHEMA = RapiTapir::Types.hash({
145
+ 'status' => RapiTapir::Types.string,
146
+ 'timestamp' => RapiTapir::Types.string,
147
+ 'version' => RapiTapir::Types.string,
148
+ 'uptime' => RapiTapir::Types.integer,
149
+ 'authentication' => RapiTapir::Types.string,
150
+ 'features' => RapiTapir::Types.array(RapiTapir::Types.string)
151
+ })
152
+
153
+ # Define all API endpoints using RapiTapir DSL
154
+ def self.endpoints
155
+ @endpoints ||= [
156
+ # Health check endpoint (public)
157
+ RapiTapir.get('/health')
158
+ .summary('Health check')
159
+ .description('Returns the health status of the API')
160
+ .ok(HEALTH_SCHEMA)
161
+ .build,
162
+
163
+ # List tasks endpoint
164
+ RapiTapir.get('/api/v1/tasks')
165
+ .summary('List all tasks')
166
+ .description('Retrieve a list of all tasks in the system. Requires read permission.')
167
+ .query(:status, RapiTapir::Types.optional(RapiTapir::Types.string),
168
+ description: 'Filter by task status')
169
+ .query(:assignee_id, RapiTapir::Types.optional(RapiTapir::Types.integer),
170
+ description: 'Filter by assignee ID')
171
+ .query(:limit, RapiTapir::Types.optional(RapiTapir::Types.integer),
172
+ description: 'Maximum number of results')
173
+ .query(:offset, RapiTapir::Types.optional(RapiTapir::Types.integer),
174
+ description: 'Number of results to skip')
175
+ .ok(RapiTapir::Types.array(TASK_SCHEMA))
176
+ .error_response(401, ERROR_SCHEMA, description: 'Authentication required')
177
+ .error_response(403, ERROR_SCHEMA, description: 'Insufficient permissions')
178
+ .build,
179
+
180
+ # Get specific task endpoint
181
+ RapiTapir.get('/api/v1/tasks/:id')
182
+ .summary('Get a specific task')
183
+ .description('Retrieve details of a specific task by ID. Requires read permission.')
184
+ .path_param(:id, RapiTapir::Types.integer, description: 'Task ID')
185
+ .ok(TASK_SCHEMA)
186
+ .error_response(404, ERROR_SCHEMA, description: 'Task not found')
187
+ .error_response(401, ERROR_SCHEMA, description: 'Authentication required')
188
+ .build,
189
+
190
+ # Create task endpoint
191
+ RapiTapir.post('/api/v1/tasks')
192
+ .summary('Create a new task')
193
+ .description('Create a new task in the system. Requires write permission.')
194
+ .json_body(TASK_CREATE_SCHEMA)
195
+ .created(TASK_SCHEMA)
196
+ .error_response(400, ERROR_SCHEMA, description: 'Validation error')
197
+ .error_response(401, ERROR_SCHEMA, description: 'Authentication required')
198
+ .error_response(403, ERROR_SCHEMA, description: 'Insufficient permissions')
199
+ .build,
200
+
201
+ # Update task endpoint
202
+ RapiTapir.put('/api/v1/tasks/:id')
203
+ .summary('Update a task')
204
+ .description('Update an existing task. Requires write permission.')
205
+ .path_param(:id, RapiTapir::Types.integer, description: 'Task ID')
206
+ .json_body(TASK_UPDATE_SCHEMA)
207
+ .ok(TASK_SCHEMA)
208
+ .error_response(404, ERROR_SCHEMA, description: 'Task not found')
209
+ .error_response(400, ERROR_SCHEMA, description: 'Validation error')
210
+ .error_response(401, ERROR_SCHEMA, description: 'Authentication required')
211
+ .error_response(403, ERROR_SCHEMA, description: 'Insufficient permissions')
212
+ .build,
213
+
214
+ # Delete task endpoint
215
+ RapiTapir.delete('/api/v1/tasks/:id')
216
+ .summary('Delete a task')
217
+ .description('Delete a task from the system. Requires admin permission.')
218
+ .path_param(:id, RapiTapir::Types.integer, description: 'Task ID')
219
+ .no_content(description: 'Task deleted successfully')
220
+ .error_response(404, ERROR_SCHEMA, description: 'Task not found')
221
+ .error_response(403, ERROR_SCHEMA, description: 'Admin permission required')
222
+ .error_response(401, ERROR_SCHEMA, description: 'Authentication required')
223
+ .build,
224
+
225
+ # User profile endpoint
226
+ RapiTapir.get('/api/v1/profile')
227
+ .summary('Get current user profile')
228
+ .description('Retrieve the profile of the authenticated user')
229
+ .ok(RapiTapir::Types.hash({
230
+ 'id' => RapiTapir::Types.integer,
231
+ 'name' => RapiTapir::Types.string,
232
+ 'email' => RapiTapir::Types.string,
233
+ 'role' => RapiTapir::Types.string,
234
+ 'scopes' => RapiTapir::Types.array(RapiTapir::Types.string),
235
+ 'tasks' => RapiTapir::Types.array(RapiTapir::Types.hash({
236
+ 'id' => RapiTapir::Types.integer,
237
+ 'title' => RapiTapir::Types.string,
238
+ 'status' => RapiTapir::Types.string
239
+ }))
240
+ }))
241
+ .error_response(401, ERROR_SCHEMA, description: 'Authentication required')
242
+ .build,
243
+
244
+ # Admin users endpoint
245
+ RapiTapir.get('/api/v1/admin/users')
246
+ .summary('List all users (admin only)')
247
+ .description('Retrieve a list of all users in the system. Requires admin permission.')
248
+ .ok(RapiTapir::Types.array(USER_SCHEMA))
249
+ .error_response(401, ERROR_SCHEMA, description: 'Authentication required')
250
+ .error_response(403, ERROR_SCHEMA, description: 'Admin permission required')
251
+ .build
252
+ ]
253
+ end
254
+
255
+ # Generate OpenAPI specification from RapiTapir endpoints
256
+ def self.openapi_spec
257
+ @openapi_spec ||= begin
258
+ require_relative '../lib/rapitapir/openapi/schema_generator'
259
+
260
+ generator = RapiTapir::OpenAPI::SchemaGenerator.new(
261
+ endpoints: endpoints,
262
+ info: {
263
+ title: 'Enterprise Task Management API',
264
+ description: 'A production-ready task management API with authentication and authorization',
265
+ version: '1.0.0',
266
+ contact: {
267
+ name: 'API Support',
268
+ email: 'api-support@example.com',
269
+ url: 'https://example.com/support'
270
+ },
271
+ license: {
272
+ name: 'MIT',
273
+ url: 'https://opensource.org/licenses/MIT'
274
+ }
275
+ },
276
+ servers: [
277
+ {
278
+ url: 'http://localhost:4567',
279
+ description: 'Development server'
280
+ },
281
+ {
282
+ url: 'https://api.example.com',
283
+ description: 'Production server'
284
+ }
285
+ ]
286
+ )
287
+
288
+ # Add security schemes to the spec
289
+ spec = generator.generate
290
+ spec[:components] ||= {}
291
+ spec[:components][:securitySchemes] = {
292
+ bearerAuth: {
293
+ type: 'http',
294
+ scheme: 'bearer',
295
+ bearerFormat: 'Token',
296
+ description: 'Enter your bearer token (e.g., user-token-123)'
297
+ }
298
+ }
299
+
300
+ # Add security requirement to all endpoints except health
301
+ spec[:paths].each do |path, methods|
302
+ next if path == '/health'
303
+
304
+ methods.each_value do |operation|
305
+ operation[:security] = [{ bearerAuth: [] }]
306
+ end
307
+ end
308
+
309
+ spec
310
+ end
311
+ end
312
+ end
313
+
314
+ # Main Sinatra Application
315
+ class EnterpriseTaskAPI < Sinatra::Base
316
+ def initialize
317
+ super
318
+
319
+ configure do
320
+ set :show_exceptions, false
321
+ set :raise_errors, false
322
+ set :dump_errors, false
323
+ end
324
+
325
+ # Setup authentication scheme
326
+ bearer_auth = RapiTapir::Auth.bearer_token(:bearer, {
327
+ realm: 'Enterprise Task Management API',
328
+ token_validator: proc do |token|
329
+ user = UserDatabase.find_by_token(token)
330
+ next nil unless user
331
+
332
+ {
333
+ user: user,
334
+ scopes: user[:scopes]
335
+ }
336
+ end
337
+ })
338
+
339
+ auth_schemes = { bearer: bearer_auth }
340
+
341
+ # Setup middleware stack
342
+ use RapiTapir::Auth::Middleware::SecurityHeadersMiddleware
343
+ use RapiTapir::Auth::Middleware::CorsMiddleware, {
344
+ allowed_origins: ['http://localhost:3000', 'https://app.example.com'],
345
+ allowed_methods: %w[GET POST PUT DELETE PATCH OPTIONS],
346
+ allowed_headers: %w[Authorization Content-Type Accept],
347
+ allow_credentials: true
348
+ }
349
+ use RapiTapir::Auth::Middleware::RateLimitingMiddleware, {
350
+ requests_per_minute: 100,
351
+ requests_per_hour: 2000
352
+ }
353
+ use RapiTapir::Auth::Middleware::AuthenticationMiddleware, auth_schemes
354
+
355
+ # Setup RapiTapir adapter and register endpoints
356
+ setup_rapitapir_endpoints
357
+ end
358
+
359
+ # Helper methods
360
+ def json_response(status, data)
361
+ content_type :json
362
+ halt status, JSON.generate(data)
363
+ end
364
+
365
+ def require_scope(scope)
366
+ return if RapiTapir::Auth.has_scope?(scope)
367
+
368
+ json_response(403, { error: "#{scope.capitalize} permission required" })
369
+ end
370
+
371
+ def require_authenticated
372
+ return if RapiTapir::Auth.authenticated?
373
+
374
+ json_response(401, { error: 'Authentication required' })
375
+ end
376
+
377
+ def parse_json_body
378
+ if request.content_type&.include?('application/json') && request.body.read.length.positive?
379
+ request.body.rewind
380
+ JSON.parse(request.body.read, symbolize_names: true)
381
+ else
382
+ {}
383
+ end
384
+ rescue JSON::ParserError
385
+ json_response(400, { error: 'Invalid JSON' })
386
+ end
387
+
388
+ def format_task(task)
389
+ task_copy = task.dup
390
+ task_copy[:created_at] = task_copy[:created_at].iso8601 if task_copy[:created_at]
391
+ task_copy[:updated_at] = task_copy[:updated_at].iso8601 if task_copy[:updated_at]
392
+ task_copy
393
+ end
394
+
395
+ private
396
+
397
+ def setup_rapitapir_endpoints
398
+ adapter = RapiTapir::Server::SinatraAdapter.new(self)
399
+
400
+ # Register all endpoints using the adapter
401
+ TaskAPI.endpoints.each do |endpoint|
402
+ adapter.register_endpoint(endpoint, get_endpoint_handler(endpoint))
403
+ end
404
+ end
405
+
406
+ def get_endpoint_handler(endpoint)
407
+ case endpoint.path
408
+ when '/health'
409
+ proc do |_inputs|
410
+ {
411
+ status: 'healthy',
412
+ timestamp: Time.now.iso8601,
413
+ version: '1.0.0',
414
+ uptime: Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i,
415
+ authentication: 'Bearer Token',
416
+ features: ['Rate Limiting', 'CORS', 'Security Headers', 'Auto-generated OpenAPI 3.0', 'RapiTapir DSL']
417
+ }
418
+ end
419
+
420
+ when '/api/v1/tasks'
421
+ if endpoint.method == :get
422
+ proc do |inputs|
423
+ require_authenticated
424
+ require_scope('read')
425
+
426
+ tasks = TaskDatabase.all
427
+
428
+ # Apply filters
429
+ tasks = tasks.select { |task| task[:status] == inputs[:status] } if inputs[:status]
430
+
431
+ tasks = tasks.select { |task| task[:assignee_id] == inputs[:assignee_id] } if inputs[:assignee_id]
432
+
433
+ # Apply pagination
434
+ limit = inputs[:limit] || 50
435
+ offset = inputs[:offset] || 0
436
+ tasks = tasks.drop(offset).take(limit)
437
+
438
+ # Format timestamps
439
+ tasks.map { |task| format_task(task) }
440
+ end
441
+ else # POST
442
+ proc do |inputs|
443
+ require_authenticated
444
+ require_scope('write')
445
+
446
+ body = inputs[:body] || {}
447
+
448
+ # Validate required fields - now handled by RapiTapir type validation
449
+ # Create task
450
+ task_data = {
451
+ title: body['title'],
452
+ description: body['description'],
453
+ status: body['status'] || 'pending',
454
+ assignee_id: body['assignee_id']
455
+ }
456
+
457
+ task = TaskDatabase.create(task_data)
458
+ format_task(task)
459
+ end
460
+ end
461
+
462
+ when '/api/v1/tasks/:id'
463
+ case endpoint.method
464
+ when :get
465
+ proc do |inputs|
466
+ require_authenticated
467
+ require_scope('read')
468
+
469
+ task = TaskDatabase.find(inputs[:id])
470
+ halt 404, { error: 'Task not found' }.to_json unless task
471
+
472
+ # Enrich with assignee details
473
+ assignee = UserDatabase.find_by_id(task[:assignee_id])
474
+ task_with_assignee = format_task(task)
475
+ task_with_assignee[:assignee] = if assignee
476
+ {
477
+ id: assignee[:id],
478
+ name: assignee[:name],
479
+ email: assignee[:email]
480
+ }
481
+ end
482
+
483
+ task_with_assignee
484
+ end
485
+ when :put
486
+ proc do |inputs|
487
+ require_authenticated
488
+ require_scope('write')
489
+
490
+ task = TaskDatabase.find(inputs[:id])
491
+ halt 404, { error: 'Task not found' }.to_json unless task
492
+
493
+ body = inputs[:body] || {}
494
+ update_data = {}
495
+
496
+ # Prepare update data - validation handled by RapiTapir
497
+ update_data[:title] = body['title'] if body['title']
498
+ update_data[:description] = body['description'] if body['description']
499
+ update_data[:status] = body['status'] if body['status']
500
+ update_data[:assignee_id] = body['assignee_id'] if body['assignee_id']
501
+
502
+ # Update task
503
+ updated_task = TaskDatabase.update(inputs[:id], update_data)
504
+ format_task(updated_task)
505
+ end
506
+ when :delete
507
+ proc do |inputs|
508
+ require_authenticated
509
+ require_scope('admin')
510
+
511
+ task = TaskDatabase.find(inputs[:id])
512
+ halt 404, { error: 'Task not found' }.to_json unless task
513
+
514
+ TaskDatabase.delete(inputs[:id])
515
+ status 204
516
+ nil # Return nothing for 204 No Content
517
+ end
518
+ end
519
+
520
+ when '/api/v1/profile'
521
+ proc do |_inputs|
522
+ require_authenticated
523
+
524
+ current_user = RapiTapir::Auth.current_user
525
+
526
+ # Get user's assigned tasks
527
+ user_tasks = TaskDatabase.by_assignee(current_user[:id]).map do |task|
528
+ {
529
+ id: task[:id],
530
+ title: task[:title],
531
+ status: task[:status]
532
+ }
533
+ end
534
+
535
+ profile = current_user.dup
536
+ profile[:tasks] = user_tasks
537
+ profile
538
+ end
539
+
540
+ when '/api/v1/admin/users'
541
+ proc do |_inputs|
542
+ require_authenticated
543
+ require_scope('admin')
544
+
545
+ UserDatabase.all_users
546
+ end
547
+
548
+ else
549
+ proc do |_inputs|
550
+ halt 404, { error: 'Endpoint not implemented' }.to_json
551
+ end
552
+ end
553
+ end
554
+
555
+ # OpenAPI Documentation endpoint - Auto-generated from RapiTapir endpoints
556
+ get '/openapi.json' do
557
+ content_type :json
558
+ JSON.pretty_generate(TaskAPI.openapi_spec)
559
+ end
560
+
561
+ # Swagger UI endpoint
562
+ get '/docs' do
563
+ <<~HTML
564
+ <!DOCTYPE html>
565
+ <html>
566
+ <head>
567
+ <title>Enterprise Task Management API - Documentation</title>
568
+ <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css" />
569
+ <style>
570
+ html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
571
+ *, *:before, *:after { box-sizing: inherit; }
572
+ body { margin:0; background: #fafafa; }
573
+ .swagger-ui .topbar { display: none; }
574
+ .info-banner {
575
+ background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
576
+ color: white;
577
+ padding: 20px;
578
+ text-align: center;
579
+ margin-bottom: 20px;
580
+ }
581
+ .info-banner h1 { margin: 0; font-size: 24px; }
582
+ .info-banner p { margin: 10px 0 0 0; opacity: 0.9; }
583
+ </style>
584
+ </head>
585
+ <body>
586
+ <div class="info-banner">
587
+ <h1>šŸš€ Enterprise Task Management API</h1>
588
+ <p>Auto-generated from RapiTapir endpoint definitions with SinatraAdapter integration</p>
589
+ </div>
590
+ <div id="swagger-ui"></div>
591
+ <script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js"></script>
592
+ <script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-standalone-preset.js"></script>
593
+ <script>
594
+ window.onload = function() {
595
+ const ui = SwaggerUIBundle({
596
+ url: '/openapi.json',
597
+ dom_id: '#swagger-ui',
598
+ deepLinking: true,
599
+ presets: [
600
+ SwaggerUIBundle.presets.apis,
601
+ SwaggerUIStandalonePreset
602
+ ],
603
+ plugins: [
604
+ SwaggerUIBundle.plugins.DownloadUrl
605
+ ],
606
+ layout: "StandaloneLayout",
607
+ tryItOutEnabled: true,
608
+ supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch'],
609
+ onComplete: function() {
610
+ console.log('Swagger UI loaded successfully');
611
+ console.log('OpenAPI spec auto-generated from RapiTapir endpoints');
612
+ console.log('Endpoints handled by SinatraAdapter with full type validation');
613
+ }
614
+ });
615
+ };
616
+ </script>
617
+ </body>
618
+ </html>
619
+ HTML
620
+ end
621
+
622
+ # Global error handler
623
+ error do |e|
624
+ content_type :json
625
+ status 500
626
+ JSON.generate({
627
+ error: 'Internal server error',
628
+ message: development? ? e.message : 'Something went wrong'
629
+ })
630
+ end
631
+
632
+ # 404 handler
633
+ not_found do
634
+ content_type :json
635
+ JSON.generate({ error: 'Endpoint not found' })
636
+ end
637
+
638
+ # Start server info
639
+ configure :development do
640
+ puts "\nšŸš€ Enterprise Task Management API Starting..."
641
+ puts 'šŸ“š API Documentation: http://localhost:4567/docs'
642
+ puts 'šŸ“‹ OpenAPI Spec (Auto-generated): http://localhost:4567/openapi.json'
643
+ puts 'ā¤ļø Health Check: http://localhost:4567/health'
644
+ puts "\nšŸ”‘ Available Bearer Tokens:"
645
+ puts ' User Token: user-token-123 (scopes: read, write)'
646
+ puts ' Admin Token: admin-token-456 (scopes: read, write, admin, delete)'
647
+ puts ' Read-only Token: readonly-token-789 (scopes: read)'
648
+ puts "\nšŸ“– Example API Calls:"
649
+ puts " curl -H 'Authorization: Bearer user-token-123' http://localhost:4567/api/v1/tasks"
650
+ puts " curl -H 'Authorization: Bearer admin-token-456' http://localhost:4567/api/v1/admin/users"
651
+ puts " curl -X POST -H 'Authorization: Bearer user-token-123' -H 'Content-Type: application/json' \\"
652
+ puts " -d '{\"title\":\"New Task\",\"description\":\"Test task\",\"assignee_id\":1}' \\"
653
+ puts ' http://localhost:4567/api/v1/tasks'
654
+ puts "\n✨ Features: SinatraAdapter, Bearer Auth, Rate Limiting, CORS, Security Headers"
655
+ puts "šŸŽÆ RapiTapir: #{TaskAPI.endpoints.size} endpoints auto-registered with full type safety"
656
+ puts 'šŸ”§ Architecture: Routes handled by SinatraAdapter with automatic input/output validation'
657
+ puts ''
658
+ end
659
+ end
660
+
661
+ # Start the server if this file is run directly
662
+ EnterpriseTaskAPI.run! if __FILE__ == $PROGRAM_NAME