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