rapitapir 0.1.1 ā 2.0.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 +4 -4
- data/.rubocop.yml +7 -7
- data/.rubocop_todo.yml +83 -0
- data/README.md +1319 -235
- data/RUBY_WEEKLY_LAUNCH_POST.md +219 -0
- data/docs/RAILS_INTEGRATION_IMPLEMENTATION.md +209 -0
- data/docs/SINATRA_EXTENSION.md +399 -348
- data/docs/STRICT_VALIDATION.md +229 -0
- data/docs/VALIDATION_IMPROVEMENTS.md +218 -0
- data/docs/ai-integration-plan.md +112 -0
- data/docs/auto-derivation.md +505 -92
- data/docs/endpoint-definition.md +536 -129
- data/docs/n8n-integration.md +212 -0
- data/docs/observability.md +810 -500
- data/docs/using-mcp.md +93 -0
- data/examples/ai/knowledge_base_rag.rb +83 -0
- data/examples/ai/user_management_mcp.rb +92 -0
- data/examples/ai/user_validation_llm.rb +187 -0
- data/examples/rails/RAILS_8_GUIDE.md +165 -0
- data/examples/rails/RAILS_LOADING_FIX.rb +35 -0
- data/examples/rails/README.md +497 -0
- data/examples/rails/comprehensive_test.rb +91 -0
- data/examples/rails/config/routes.rb +48 -0
- data/examples/rails/debug_controller.rb +63 -0
- data/examples/rails/detailed_test.rb +46 -0
- data/examples/rails/enhanced_users_controller.rb +278 -0
- data/examples/rails/final_server_test.rb +50 -0
- data/examples/rails/hello_world_app.rb +116 -0
- data/examples/rails/hello_world_controller.rb +186 -0
- data/examples/rails/hello_world_routes.rb +28 -0
- data/examples/rails/rails8_minimal_demo.rb +132 -0
- data/examples/rails/rails8_simple_demo.rb +140 -0
- data/examples/rails/rails8_working_demo.rb +255 -0
- data/examples/rails/real_world_blog_api.rb +510 -0
- data/examples/rails/server_test.rb +46 -0
- data/examples/rails/test_direct_processing.rb +41 -0
- data/examples/rails/test_hello_world.rb +80 -0
- data/examples/rails/test_rails_integration.rb +54 -0
- data/examples/rails/traditional_app/Gemfile +37 -0
- data/examples/rails/traditional_app/README.md +265 -0
- data/examples/rails/traditional_app/app/controllers/api/v1/posts_controller.rb +254 -0
- data/examples/rails/traditional_app/app/controllers/api/v1/users_controller.rb +220 -0
- data/examples/rails/traditional_app/app/controllers/application_controller.rb +86 -0
- data/examples/rails/traditional_app/app/controllers/application_controller_simplified.rb +87 -0
- data/examples/rails/traditional_app/app/controllers/documentation_controller.rb +149 -0
- data/examples/rails/traditional_app/app/controllers/health_controller.rb +42 -0
- data/examples/rails/traditional_app/config/routes.rb +25 -0
- data/examples/rails/traditional_app/config/routes_best_practice.rb +25 -0
- data/examples/rails/traditional_app/config/routes_simplified.rb +36 -0
- data/examples/rails/traditional_app_runnable.rb +406 -0
- data/examples/rails/users_controller.rb +4 -1
- data/examples/serverless/Gemfile +43 -0
- data/examples/serverless/QUICKSTART.md +331 -0
- data/examples/serverless/README.md +520 -0
- data/examples/serverless/aws_lambda_example.rb +307 -0
- data/examples/serverless/aws_sam_template.yaml +215 -0
- data/examples/serverless/azure_functions_example.rb +407 -0
- data/examples/serverless/deploy.rb +204 -0
- data/examples/serverless/gcp_cloud_functions_example.rb +367 -0
- data/examples/serverless/gcp_function.yaml +23 -0
- data/examples/serverless/host.json +24 -0
- data/examples/serverless/package.json +32 -0
- data/examples/serverless/spec/aws_lambda_spec.rb +196 -0
- data/examples/serverless/spec/spec_helper.rb +89 -0
- data/examples/serverless/vercel.json +31 -0
- data/examples/serverless/vercel_example.rb +404 -0
- data/examples/strict_validation_examples.rb +104 -0
- data/examples/validation_error_examples.rb +173 -0
- data/lib/rapitapir/ai/llm_instruction.rb +456 -0
- data/lib/rapitapir/ai/mcp.rb +134 -0
- data/lib/rapitapir/ai/rag.rb +287 -0
- data/lib/rapitapir/ai/rag_middleware.rb +147 -0
- data/lib/rapitapir/auth/oauth2.rb +43 -57
- data/lib/rapitapir/cli/command.rb +362 -2
- data/lib/rapitapir/cli/mcp_export.rb +18 -0
- data/lib/rapitapir/cli/validator.rb +2 -6
- data/lib/rapitapir/core/endpoint.rb +59 -6
- data/lib/rapitapir/core/enhanced_endpoint.rb +2 -6
- data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +53 -0
- data/lib/rapitapir/endpoint_registry.rb +47 -0
- data/lib/rapitapir/observability/health_check.rb +4 -4
- data/lib/rapitapir/observability/logging.rb +10 -10
- data/lib/rapitapir/schema.rb +2 -2
- data/lib/rapitapir/server/rack_adapter.rb +1 -3
- data/lib/rapitapir/server/rails/configuration.rb +77 -0
- data/lib/rapitapir/server/rails/controller_base.rb +185 -0
- data/lib/rapitapir/server/rails/documentation_helpers.rb +76 -0
- data/lib/rapitapir/server/rails/resource_builder.rb +181 -0
- data/lib/rapitapir/server/rails/routes.rb +114 -0
- data/lib/rapitapir/server/rails_adapter.rb +10 -3
- data/lib/rapitapir/server/rails_adapter_class.rb +1 -3
- data/lib/rapitapir/server/rails_controller.rb +1 -3
- data/lib/rapitapir/server/rails_integration.rb +67 -0
- data/lib/rapitapir/server/rails_response_handler.rb +16 -3
- data/lib/rapitapir/server/sinatra_adapter.rb +29 -5
- data/lib/rapitapir/server/sinatra_integration.rb +4 -4
- data/lib/rapitapir/sinatra/extension.rb +2 -2
- data/lib/rapitapir/sinatra/oauth2_helpers.rb +34 -40
- data/lib/rapitapir/types/array.rb +4 -0
- data/lib/rapitapir/types/auto_derivation.rb +4 -18
- data/lib/rapitapir/types/datetime.rb +1 -3
- data/lib/rapitapir/types/float.rb +2 -6
- data/lib/rapitapir/types/hash.rb +40 -2
- data/lib/rapitapir/types/integer.rb +4 -12
- data/lib/rapitapir/types/object.rb +6 -2
- data/lib/rapitapir/types.rb +6 -2
- data/lib/rapitapir/version.rb +1 -1
- data/lib/rapitapir.rb +5 -3
- data/rapitapir.gemspec +7 -5
- metadata +116 -16
@@ -0,0 +1,407 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rapitapir'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
# Azure Functions handler for SinatraRapiTapir
|
7
|
+
# This example shows how to deploy a RapiTapir API as an Azure Function
|
8
|
+
class BookAPIAzureFunction < SinatraRapiTapir
|
9
|
+
# Configure for Azure Functions
|
10
|
+
rapitapir do
|
11
|
+
info(
|
12
|
+
title: 'Serverless Book API - Azure Functions',
|
13
|
+
description: 'A book management API deployed on Azure Functions',
|
14
|
+
version: '1.0.0'
|
15
|
+
)
|
16
|
+
|
17
|
+
# Azure Functions optimized configuration
|
18
|
+
configure do
|
19
|
+
set :environment, :production
|
20
|
+
set :logging, true
|
21
|
+
set :dump_errors, false
|
22
|
+
set :raise_errors, true
|
23
|
+
|
24
|
+
# Azure Functions specific settings
|
25
|
+
set :sessions, false
|
26
|
+
set :static, false
|
27
|
+
set :protection, except: [:json_csrf]
|
28
|
+
end
|
29
|
+
|
30
|
+
development_defaults!
|
31
|
+
end
|
32
|
+
|
33
|
+
# Book schema for Azure Cosmos DB compatibility
|
34
|
+
BOOK_SCHEMA = T.hash({
|
35
|
+
"id" => T.string, # Azure Cosmos DB uses string IDs
|
36
|
+
"title" => T.string(min_length: 1, max_length: 255),
|
37
|
+
"author" => T.string(min_length: 1, max_length: 255),
|
38
|
+
"isbn" => T.optional(T.string(pattern: /^\d{10}(\d{3})?$/)),
|
39
|
+
"published_year" => T.optional(T.integer(minimum: 1000, maximum: 3000)),
|
40
|
+
"available" => T.boolean,
|
41
|
+
"category" => T.optional(T.string),
|
42
|
+
"created_at" => T.datetime,
|
43
|
+
"updated_at" => T.datetime,
|
44
|
+
"_rid" => T.optional(T.string), # Cosmos DB resource ID
|
45
|
+
"_etag" => T.optional(T.string) # Cosmos DB etag for optimistic concurrency
|
46
|
+
})
|
47
|
+
|
48
|
+
# Mock data (in production, use Azure Cosmos DB or SQL Database)
|
49
|
+
@@books = [
|
50
|
+
{
|
51
|
+
id: "azure_book_1",
|
52
|
+
title: "Ruby on Azure",
|
53
|
+
author: "Cloud Developer",
|
54
|
+
isbn: "9781234567890",
|
55
|
+
published_year: 2023,
|
56
|
+
available: true,
|
57
|
+
category: "cloud",
|
58
|
+
created_at: Time.now - 86400,
|
59
|
+
updated_at: Time.now - 86400
|
60
|
+
},
|
61
|
+
{
|
62
|
+
id: "azure_book_2",
|
63
|
+
title: "Serverless Ruby Applications",
|
64
|
+
author: "Function Expert",
|
65
|
+
isbn: "9789876543210",
|
66
|
+
published_year: 2024,
|
67
|
+
available: true,
|
68
|
+
category: "serverless",
|
69
|
+
created_at: Time.now - 43200,
|
70
|
+
updated_at: Time.now - 43200
|
71
|
+
}
|
72
|
+
]
|
73
|
+
|
74
|
+
# Health check with Azure Functions info
|
75
|
+
endpoint(
|
76
|
+
GET('/health')
|
77
|
+
.summary('Health check for Azure Function')
|
78
|
+
.description('Returns the health status of the Azure Function')
|
79
|
+
.tags('Health', 'Azure')
|
80
|
+
.ok(T.hash({
|
81
|
+
"status" => T.string,
|
82
|
+
"timestamp" => T.datetime,
|
83
|
+
"azure_info" => T.hash({
|
84
|
+
"function_app_name" => T.optional(T.string),
|
85
|
+
"function_name" => T.optional(T.string),
|
86
|
+
"resource_group" => T.optional(T.string),
|
87
|
+
"subscription_id" => T.optional(T.string),
|
88
|
+
"region" => T.optional(T.string),
|
89
|
+
"plan_type" => T.optional(T.string)
|
90
|
+
})
|
91
|
+
}))
|
92
|
+
.build
|
93
|
+
) do
|
94
|
+
{
|
95
|
+
status: 'healthy',
|
96
|
+
timestamp: Time.now,
|
97
|
+
azure_info: {
|
98
|
+
function_app_name: ENV['WEBSITE_SITE_NAME'],
|
99
|
+
function_name: ENV['REQ_HEADERS_FUNCTION_NAME'],
|
100
|
+
resource_group: ENV['WEBSITE_RESOURCE_GROUP'],
|
101
|
+
subscription_id: ENV['WEBSITE_OWNER_NAME']&.split('+')&.first,
|
102
|
+
region: ENV['REGION_NAME'],
|
103
|
+
plan_type: ENV['WEBSITE_SKU']
|
104
|
+
}
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
# List books with Azure-specific features
|
109
|
+
endpoint(
|
110
|
+
GET('/books')
|
111
|
+
.summary('List all books')
|
112
|
+
.description('Retrieve books with Azure Cosmos DB style querying')
|
113
|
+
.query(:limit, T.optional(T.integer(minimum: 1, maximum: 100)), description: 'Limit results')
|
114
|
+
.query(:category, T.optional(T.string), description: 'Filter by category')
|
115
|
+
.query(:continuation_token, T.optional(T.string), description: 'Pagination token')
|
116
|
+
.tags('Books')
|
117
|
+
.ok(T.hash({
|
118
|
+
"books" => T.array(BOOK_SCHEMA),
|
119
|
+
"count" => T.integer,
|
120
|
+
"continuation_token" => T.optional(T.string),
|
121
|
+
"request_charge" => T.optional(T.float)
|
122
|
+
}))
|
123
|
+
.build
|
124
|
+
) do |inputs|
|
125
|
+
books = @@books.dup
|
126
|
+
|
127
|
+
# Apply category filter
|
128
|
+
books = books.select { |book| book[:category] == inputs[:category] } if inputs[:category]
|
129
|
+
|
130
|
+
# Simple pagination simulation
|
131
|
+
limit = inputs[:limit] || 50
|
132
|
+
offset = inputs[:continuation_token] ? inputs[:continuation_token].to_i : 0
|
133
|
+
|
134
|
+
paginated_books = books[offset, limit] || []
|
135
|
+
next_token = (offset + limit < books.length) ? (offset + limit).to_s : nil
|
136
|
+
|
137
|
+
{
|
138
|
+
books: paginated_books,
|
139
|
+
count: paginated_books.length,
|
140
|
+
continuation_token: next_token,
|
141
|
+
request_charge: 2.5 # Mock Cosmos DB RU charge
|
142
|
+
}
|
143
|
+
end
|
144
|
+
|
145
|
+
# Get book by ID
|
146
|
+
endpoint(
|
147
|
+
GET('/books/:id')
|
148
|
+
.path_param(:id, T.string, description: 'Book ID')
|
149
|
+
.summary('Get book by ID')
|
150
|
+
.description('Retrieve a specific book from Azure storage')
|
151
|
+
.tags('Books')
|
152
|
+
.ok(BOOK_SCHEMA)
|
153
|
+
.error_response(404, T.hash({ "error" => T.string, "book_id" => T.string }))
|
154
|
+
.build
|
155
|
+
) do |inputs|
|
156
|
+
book = @@books.find { |b| b[:id] == inputs[:id] }
|
157
|
+
|
158
|
+
if book
|
159
|
+
book
|
160
|
+
else
|
161
|
+
halt 404, { error: 'Book not found', book_id: inputs[:id] }.to_json
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Create book with Azure optimizations
|
166
|
+
endpoint(
|
167
|
+
POST('/books')
|
168
|
+
.summary('Create a new book')
|
169
|
+
.description('Add a book to Azure Cosmos DB')
|
170
|
+
.body(T.hash({
|
171
|
+
"title" => T.string(min_length: 1, max_length: 255),
|
172
|
+
"author" => T.string(min_length: 1, max_length: 255),
|
173
|
+
"isbn" => T.optional(T.string(pattern: /^\d{10}(\d{3})?$/)),
|
174
|
+
"published_year" => T.optional(T.integer(minimum: 1000, maximum: 3000)),
|
175
|
+
"available" => T.optional(T.boolean),
|
176
|
+
"category" => T.optional(T.string)
|
177
|
+
}))
|
178
|
+
.tags('Books')
|
179
|
+
.ok(BOOK_SCHEMA)
|
180
|
+
.error_response(400, T.hash({ "error" => T.string }))
|
181
|
+
.build
|
182
|
+
) do |inputs|
|
183
|
+
book_data = inputs[:body]
|
184
|
+
|
185
|
+
# Generate Azure-style ID
|
186
|
+
new_id = "azure_book_#{SecureRandom.uuid}"
|
187
|
+
|
188
|
+
new_book = {
|
189
|
+
id: new_id,
|
190
|
+
title: book_data[:title] || book_data['title'],
|
191
|
+
author: book_data[:author] || book_data['author'],
|
192
|
+
isbn: book_data[:isbn] || book_data['isbn'],
|
193
|
+
published_year: book_data[:published_year] || book_data['published_year'],
|
194
|
+
available: book_data.key?(:available) ? book_data[:available] : (book_data.key?('available') ? book_data['available'] : true),
|
195
|
+
category: book_data[:category] || book_data['category'],
|
196
|
+
created_at: Time.now,
|
197
|
+
updated_at: Time.now,
|
198
|
+
_rid: "rid_#{SecureRandom.hex(8)}",
|
199
|
+
_etag: "etag_#{SecureRandom.hex(16)}"
|
200
|
+
}
|
201
|
+
|
202
|
+
@@books << new_book
|
203
|
+
|
204
|
+
status 201
|
205
|
+
new_book
|
206
|
+
end
|
207
|
+
|
208
|
+
# Azure-specific monitoring endpoint
|
209
|
+
endpoint(
|
210
|
+
GET('/azure/function-metrics')
|
211
|
+
.summary('Azure Function performance metrics')
|
212
|
+
.description('Get Azure-specific runtime and performance information')
|
213
|
+
.tags('Azure', 'Monitoring')
|
214
|
+
.ok(T.hash({
|
215
|
+
"function_execution" => T.hash({
|
216
|
+
"execution_id" => T.optional(T.string),
|
217
|
+
"invocation_id" => T.optional(T.string),
|
218
|
+
"execution_context" => T.optional(T.string)
|
219
|
+
}),
|
220
|
+
"azure_environment" => T.hash({
|
221
|
+
"app_service_plan" => T.optional(T.string),
|
222
|
+
"website_sku" => T.optional(T.string),
|
223
|
+
"instance_id" => T.optional(T.string),
|
224
|
+
"worker_runtime" => T.optional(T.string)
|
225
|
+
}),
|
226
|
+
"performance" => T.hash({
|
227
|
+
"memory_usage_mb" => T.optional(T.integer),
|
228
|
+
"cpu_time_ms" => T.optional(T.integer),
|
229
|
+
"cold_start" => T.optional(T.boolean)
|
230
|
+
})
|
231
|
+
}))
|
232
|
+
.build
|
233
|
+
) do
|
234
|
+
{
|
235
|
+
function_execution: {
|
236
|
+
execution_id: ENV['REQ_HEADERS_FUNCTION_EXECUTION_ID'],
|
237
|
+
invocation_id: ENV['REQ_HEADERS_FUNCTION_INVOCATION_ID'],
|
238
|
+
execution_context: Thread.current[:azure_execution_context]
|
239
|
+
},
|
240
|
+
azure_environment: {
|
241
|
+
app_service_plan: ENV['WEBSITE_SKU'],
|
242
|
+
website_sku: ENV['WEBSITE_SKU'],
|
243
|
+
instance_id: ENV['WEBSITE_INSTANCE_ID'],
|
244
|
+
worker_runtime: ENV['FUNCTIONS_WORKER_RUNTIME']
|
245
|
+
},
|
246
|
+
performance: {
|
247
|
+
memory_usage_mb: get_memory_usage,
|
248
|
+
cpu_time_ms: get_cpu_time,
|
249
|
+
cold_start: Thread.current[:cold_start]
|
250
|
+
}
|
251
|
+
}
|
252
|
+
end
|
253
|
+
|
254
|
+
# Azure Service Bus integration example
|
255
|
+
endpoint(
|
256
|
+
POST('/books/:id/notify')
|
257
|
+
.path_param(:id, T.string, description: 'Book ID')
|
258
|
+
.body(T.hash({
|
259
|
+
"event_type" => T.string(enum: %w[borrowed returned reserved cancelled]),
|
260
|
+
"user_id" => T.string,
|
261
|
+
"message" => T.optional(T.string)
|
262
|
+
}))
|
263
|
+
.summary('Send book notification')
|
264
|
+
.description('Send notification via Azure Service Bus')
|
265
|
+
.tags('Books', 'Notifications', 'Azure')
|
266
|
+
.ok(T.hash({
|
267
|
+
"notification_sent" => T.boolean,
|
268
|
+
"message_id" => T.string,
|
269
|
+
"queue_name" => T.string
|
270
|
+
}))
|
271
|
+
.build
|
272
|
+
) do |inputs|
|
273
|
+
book = @@books.find { |b| b[:id] == inputs[:id] }
|
274
|
+
halt 404, { error: 'Book not found' }.to_json unless book
|
275
|
+
|
276
|
+
event_data = inputs[:body]
|
277
|
+
|
278
|
+
# Simulate Azure Service Bus message
|
279
|
+
message_id = "msg_#{SecureRandom.uuid}"
|
280
|
+
queue_name = "book-notifications"
|
281
|
+
|
282
|
+
# In production, send to Azure Service Bus
|
283
|
+
notification_payload = {
|
284
|
+
book_id: inputs[:id],
|
285
|
+
book_title: book[:title],
|
286
|
+
event_type: event_data[:event_type],
|
287
|
+
user_id: event_data[:user_id],
|
288
|
+
message: event_data[:message],
|
289
|
+
timestamp: Time.now.iso8601
|
290
|
+
}
|
291
|
+
|
292
|
+
# Mock sending notification
|
293
|
+
puts "Sending to Azure Service Bus: #{notification_payload.to_json}"
|
294
|
+
|
295
|
+
{
|
296
|
+
notification_sent: true,
|
297
|
+
message_id: message_id,
|
298
|
+
queue_name: queue_name
|
299
|
+
}
|
300
|
+
end
|
301
|
+
|
302
|
+
private
|
303
|
+
|
304
|
+
def get_memory_usage
|
305
|
+
# Mock memory usage (in production, use Azure monitoring)
|
306
|
+
rand(100..500)
|
307
|
+
end
|
308
|
+
|
309
|
+
def get_cpu_time
|
310
|
+
# Mock CPU time (in production, use Azure monitoring)
|
311
|
+
rand(50..200)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# Azure Functions entry point
|
316
|
+
def main(context, req)
|
317
|
+
# Set Azure execution context
|
318
|
+
Thread.current[:azure_execution_context] = context
|
319
|
+
Thread.current[:cold_start] = !defined?(@@app_initialized)
|
320
|
+
|
321
|
+
# Initialize app (cached after first execution)
|
322
|
+
@@app ||= BookAPIAzureFunction.new
|
323
|
+
@@app_initialized = true
|
324
|
+
|
325
|
+
# Convert Azure Functions request to Rack environment
|
326
|
+
rack_env = build_rack_env_from_azure(req, context)
|
327
|
+
|
328
|
+
# Process request
|
329
|
+
status, headers, body = @@app.call(rack_env)
|
330
|
+
|
331
|
+
# Convert response for Azure Functions
|
332
|
+
body_content = ''
|
333
|
+
body.each { |part| body_content += part }
|
334
|
+
|
335
|
+
# Azure Functions response format
|
336
|
+
{
|
337
|
+
status: status,
|
338
|
+
headers: headers,
|
339
|
+
body: body_content
|
340
|
+
}
|
341
|
+
rescue => e
|
342
|
+
# Error handling for Azure Functions
|
343
|
+
{
|
344
|
+
status: 500,
|
345
|
+
headers: { 'Content-Type' => 'application/json' },
|
346
|
+
body: {
|
347
|
+
error: 'Internal server error',
|
348
|
+
message: e.message,
|
349
|
+
timestamp: Time.now.iso8601,
|
350
|
+
execution_id: context[:invocation_id]
|
351
|
+
}.to_json
|
352
|
+
}
|
353
|
+
ensure
|
354
|
+
# Clean up thread variables
|
355
|
+
Thread.current[:azure_execution_context] = nil
|
356
|
+
Thread.current[:cold_start] = nil
|
357
|
+
end
|
358
|
+
|
359
|
+
# Convert Azure Functions request to Rack environment
|
360
|
+
def build_rack_env_from_azure(req, context)
|
361
|
+
method = req[:method]
|
362
|
+
url = req[:url]
|
363
|
+
uri = URI.parse(url)
|
364
|
+
|
365
|
+
query_string = uri.query || ''
|
366
|
+
path = uri.path
|
367
|
+
|
368
|
+
# Get request body
|
369
|
+
body = req[:body] || ''
|
370
|
+
headers = req[:headers] || {}
|
371
|
+
|
372
|
+
rack_env = {
|
373
|
+
'REQUEST_METHOD' => method,
|
374
|
+
'PATH_INFO' => path,
|
375
|
+
'QUERY_STRING' => query_string,
|
376
|
+
'CONTENT_TYPE' => headers['content-type'] || headers['Content-Type'],
|
377
|
+
'CONTENT_LENGTH' => body.bytesize.to_s,
|
378
|
+
'rack.input' => StringIO.new(body),
|
379
|
+
'rack.errors' => $stderr,
|
380
|
+
'rack.version' => [1, 3],
|
381
|
+
'rack.url_scheme' => uri.scheme || 'https',
|
382
|
+
'rack.multithread' => false,
|
383
|
+
'rack.multiprocess' => true,
|
384
|
+
'rack.run_once' => true,
|
385
|
+
'SERVER_NAME' => uri.host || 'localhost',
|
386
|
+
'SERVER_PORT' => (uri.port || 443).to_s,
|
387
|
+
'HTTP_HOST' => uri.host || 'localhost'
|
388
|
+
}
|
389
|
+
|
390
|
+
# Add HTTP headers
|
391
|
+
headers.each do |key, value|
|
392
|
+
key = key.upcase.gsub('-', '_')
|
393
|
+
key = "HTTP_#{key}" unless %w[CONTENT_TYPE CONTENT_LENGTH].include?(key)
|
394
|
+
rack_env[key] = value
|
395
|
+
end
|
396
|
+
|
397
|
+
# Add Azure-specific headers
|
398
|
+
rack_env['HTTP_X_AZURE_EXECUTION_ID'] = context[:invocation_id] if context[:invocation_id]
|
399
|
+
rack_env['HTTP_X_AZURE_FUNCTION_NAME'] = context[:function_name] if context[:function_name]
|
400
|
+
|
401
|
+
rack_env
|
402
|
+
end
|
403
|
+
|
404
|
+
# For local development
|
405
|
+
if __FILE__ == $0
|
406
|
+
BookAPIAzureFunction.run!
|
407
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# RapiTapir Serverless Deployment Helper
|
5
|
+
# This script helps deploy RapiTapir APIs to various serverless platforms
|
6
|
+
|
7
|
+
require 'optparse'
|
8
|
+
require 'json'
|
9
|
+
require 'fileutils'
|
10
|
+
|
11
|
+
class ServerlessDeployer
|
12
|
+
PLATFORMS = %w[aws gcp azure vercel all].freeze
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@options = {}
|
16
|
+
parse_options
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
case @options[:platform]
|
21
|
+
when 'aws'
|
22
|
+
deploy_aws
|
23
|
+
when 'gcp'
|
24
|
+
deploy_gcp
|
25
|
+
when 'azure'
|
26
|
+
deploy_azure
|
27
|
+
when 'vercel'
|
28
|
+
deploy_vercel
|
29
|
+
when 'all'
|
30
|
+
deploy_all
|
31
|
+
else
|
32
|
+
puts "Error: Invalid platform. Use: #{PLATFORMS.join(', ')}"
|
33
|
+
exit 1
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def parse_options
|
40
|
+
OptionParser.new do |opts|
|
41
|
+
opts.banner = "Usage: #{$0} [options]"
|
42
|
+
|
43
|
+
opts.on('-p', '--platform PLATFORM', PLATFORMS,
|
44
|
+
'Platform to deploy to (aws, gcp, azure, vercel, all)') do |platform|
|
45
|
+
@options[:platform] = platform
|
46
|
+
end
|
47
|
+
|
48
|
+
opts.on('-n', '--name NAME', 'Function/API name') do |name|
|
49
|
+
@options[:name] = name
|
50
|
+
end
|
51
|
+
|
52
|
+
opts.on('-r', '--region REGION', 'Deployment region') do |region|
|
53
|
+
@options[:region] = region
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on('--dry-run', 'Show commands without executing') do
|
57
|
+
@options[:dry_run] = true
|
58
|
+
end
|
59
|
+
|
60
|
+
opts.on('-h', '--help', 'Show this help') do
|
61
|
+
puts opts
|
62
|
+
exit
|
63
|
+
end
|
64
|
+
end.parse!
|
65
|
+
|
66
|
+
if @options[:platform].nil?
|
67
|
+
puts "Error: Platform is required. Use -p to specify."
|
68
|
+
exit 1
|
69
|
+
end
|
70
|
+
|
71
|
+
@options[:name] ||= 'rapitapir-api'
|
72
|
+
@options[:region] ||= 'us-east-1'
|
73
|
+
end
|
74
|
+
|
75
|
+
def deploy_aws
|
76
|
+
puts "š Deploying to AWS Lambda..."
|
77
|
+
|
78
|
+
commands = [
|
79
|
+
'bundle install --deployment',
|
80
|
+
'sam build',
|
81
|
+
"sam deploy --stack-name #{@options[:name]} --region #{@options[:region]} --capabilities CAPABILITY_IAM --confirm-changeset"
|
82
|
+
]
|
83
|
+
|
84
|
+
execute_commands(commands)
|
85
|
+
|
86
|
+
puts "ā
AWS Lambda deployment complete!"
|
87
|
+
puts "š Check AWS Console for function URL"
|
88
|
+
end
|
89
|
+
|
90
|
+
def deploy_gcp
|
91
|
+
puts "š Deploying to Google Cloud Functions..."
|
92
|
+
|
93
|
+
commands = [
|
94
|
+
'bundle install',
|
95
|
+
"gcloud functions deploy #{@options[:name]} --runtime ruby32 --trigger-http --allow-unauthenticated --region #{@options[:region]}"
|
96
|
+
]
|
97
|
+
|
98
|
+
execute_commands(commands)
|
99
|
+
|
100
|
+
puts "ā
Google Cloud Functions deployment complete!"
|
101
|
+
puts "š Function URL: https://#{@options[:region]}-PROJECT-ID.cloudfunctions.net/#{@options[:name]}"
|
102
|
+
end
|
103
|
+
|
104
|
+
def deploy_azure
|
105
|
+
puts "š Deploying to Azure Functions..."
|
106
|
+
|
107
|
+
commands = [
|
108
|
+
'bundle install',
|
109
|
+
'func init --worker-runtime custom',
|
110
|
+
"func azure functionapp create #{@options[:name]} --consumption-plan-location #{@options[:region]}",
|
111
|
+
"func azure functionapp publish #{@options[:name]}"
|
112
|
+
]
|
113
|
+
|
114
|
+
execute_commands(commands)
|
115
|
+
|
116
|
+
puts "ā
Azure Functions deployment complete!"
|
117
|
+
puts "š Check Azure Portal for function URL"
|
118
|
+
end
|
119
|
+
|
120
|
+
def deploy_vercel
|
121
|
+
puts "š Deploying to Vercel..."
|
122
|
+
|
123
|
+
# Check if vercel.json exists
|
124
|
+
unless File.exist?('vercel.json')
|
125
|
+
puts "ā ļø Creating vercel.json configuration..."
|
126
|
+
create_vercel_config
|
127
|
+
end
|
128
|
+
|
129
|
+
commands = [
|
130
|
+
'bundle install',
|
131
|
+
'vercel --prod'
|
132
|
+
]
|
133
|
+
|
134
|
+
execute_commands(commands)
|
135
|
+
|
136
|
+
puts "ā
Vercel deployment complete!"
|
137
|
+
puts "š Your API is live at the URL shown above"
|
138
|
+
end
|
139
|
+
|
140
|
+
def deploy_all
|
141
|
+
puts "š Deploying to all platforms..."
|
142
|
+
|
143
|
+
%w[aws gcp azure vercel].each do |platform|
|
144
|
+
puts "\n" + "="*50
|
145
|
+
@options[:platform] = platform
|
146
|
+
send("deploy_#{platform}")
|
147
|
+
end
|
148
|
+
|
149
|
+
puts "\n" + "="*50
|
150
|
+
puts "ā
All deployments complete!"
|
151
|
+
end
|
152
|
+
|
153
|
+
def execute_commands(commands)
|
154
|
+
commands.each do |command|
|
155
|
+
puts "$ #{command}"
|
156
|
+
|
157
|
+
if @options[:dry_run]
|
158
|
+
puts " (dry run - not executed)"
|
159
|
+
else
|
160
|
+
success = system(command)
|
161
|
+
unless success
|
162
|
+
puts "ā Command failed: #{command}"
|
163
|
+
exit 1
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def create_vercel_config
|
170
|
+
config = {
|
171
|
+
version: 2,
|
172
|
+
name: @options[:name],
|
173
|
+
builds: [
|
174
|
+
{
|
175
|
+
src: "*.rb",
|
176
|
+
use: "@vercel/ruby"
|
177
|
+
}
|
178
|
+
],
|
179
|
+
routes: [
|
180
|
+
{
|
181
|
+
src: "/(.*)",
|
182
|
+
dest: "/vercel_example.rb"
|
183
|
+
}
|
184
|
+
]
|
185
|
+
}
|
186
|
+
|
187
|
+
File.write('vercel.json', JSON.pretty_generate(config))
|
188
|
+
puts "š Created vercel.json"
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Run if called directly
|
193
|
+
if __FILE__ == $0
|
194
|
+
begin
|
195
|
+
deployer = ServerlessDeployer.new
|
196
|
+
deployer.run
|
197
|
+
rescue Interrupt
|
198
|
+
puts "\nā Deployment cancelled"
|
199
|
+
exit 1
|
200
|
+
rescue => e
|
201
|
+
puts "ā Error: #{e.message}"
|
202
|
+
exit 1
|
203
|
+
end
|
204
|
+
end
|