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.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -7
  3. data/.rubocop_todo.yml +83 -0
  4. data/README.md +1319 -235
  5. data/RUBY_WEEKLY_LAUNCH_POST.md +219 -0
  6. data/docs/RAILS_INTEGRATION_IMPLEMENTATION.md +209 -0
  7. data/docs/SINATRA_EXTENSION.md +399 -348
  8. data/docs/STRICT_VALIDATION.md +229 -0
  9. data/docs/VALIDATION_IMPROVEMENTS.md +218 -0
  10. data/docs/ai-integration-plan.md +112 -0
  11. data/docs/auto-derivation.md +505 -92
  12. data/docs/endpoint-definition.md +536 -129
  13. data/docs/n8n-integration.md +212 -0
  14. data/docs/observability.md +810 -500
  15. data/docs/using-mcp.md +93 -0
  16. data/examples/ai/knowledge_base_rag.rb +83 -0
  17. data/examples/ai/user_management_mcp.rb +92 -0
  18. data/examples/ai/user_validation_llm.rb +187 -0
  19. data/examples/rails/RAILS_8_GUIDE.md +165 -0
  20. data/examples/rails/RAILS_LOADING_FIX.rb +35 -0
  21. data/examples/rails/README.md +497 -0
  22. data/examples/rails/comprehensive_test.rb +91 -0
  23. data/examples/rails/config/routes.rb +48 -0
  24. data/examples/rails/debug_controller.rb +63 -0
  25. data/examples/rails/detailed_test.rb +46 -0
  26. data/examples/rails/enhanced_users_controller.rb +278 -0
  27. data/examples/rails/final_server_test.rb +50 -0
  28. data/examples/rails/hello_world_app.rb +116 -0
  29. data/examples/rails/hello_world_controller.rb +186 -0
  30. data/examples/rails/hello_world_routes.rb +28 -0
  31. data/examples/rails/rails8_minimal_demo.rb +132 -0
  32. data/examples/rails/rails8_simple_demo.rb +140 -0
  33. data/examples/rails/rails8_working_demo.rb +255 -0
  34. data/examples/rails/real_world_blog_api.rb +510 -0
  35. data/examples/rails/server_test.rb +46 -0
  36. data/examples/rails/test_direct_processing.rb +41 -0
  37. data/examples/rails/test_hello_world.rb +80 -0
  38. data/examples/rails/test_rails_integration.rb +54 -0
  39. data/examples/rails/traditional_app/Gemfile +37 -0
  40. data/examples/rails/traditional_app/README.md +265 -0
  41. data/examples/rails/traditional_app/app/controllers/api/v1/posts_controller.rb +254 -0
  42. data/examples/rails/traditional_app/app/controllers/api/v1/users_controller.rb +220 -0
  43. data/examples/rails/traditional_app/app/controllers/application_controller.rb +86 -0
  44. data/examples/rails/traditional_app/app/controllers/application_controller_simplified.rb +87 -0
  45. data/examples/rails/traditional_app/app/controllers/documentation_controller.rb +149 -0
  46. data/examples/rails/traditional_app/app/controllers/health_controller.rb +42 -0
  47. data/examples/rails/traditional_app/config/routes.rb +25 -0
  48. data/examples/rails/traditional_app/config/routes_best_practice.rb +25 -0
  49. data/examples/rails/traditional_app/config/routes_simplified.rb +36 -0
  50. data/examples/rails/traditional_app_runnable.rb +406 -0
  51. data/examples/rails/users_controller.rb +4 -1
  52. data/examples/serverless/Gemfile +43 -0
  53. data/examples/serverless/QUICKSTART.md +331 -0
  54. data/examples/serverless/README.md +520 -0
  55. data/examples/serverless/aws_lambda_example.rb +307 -0
  56. data/examples/serverless/aws_sam_template.yaml +215 -0
  57. data/examples/serverless/azure_functions_example.rb +407 -0
  58. data/examples/serverless/deploy.rb +204 -0
  59. data/examples/serverless/gcp_cloud_functions_example.rb +367 -0
  60. data/examples/serverless/gcp_function.yaml +23 -0
  61. data/examples/serverless/host.json +24 -0
  62. data/examples/serverless/package.json +32 -0
  63. data/examples/serverless/spec/aws_lambda_spec.rb +196 -0
  64. data/examples/serverless/spec/spec_helper.rb +89 -0
  65. data/examples/serverless/vercel.json +31 -0
  66. data/examples/serverless/vercel_example.rb +404 -0
  67. data/examples/strict_validation_examples.rb +104 -0
  68. data/examples/validation_error_examples.rb +173 -0
  69. data/lib/rapitapir/ai/llm_instruction.rb +456 -0
  70. data/lib/rapitapir/ai/mcp.rb +134 -0
  71. data/lib/rapitapir/ai/rag.rb +287 -0
  72. data/lib/rapitapir/ai/rag_middleware.rb +147 -0
  73. data/lib/rapitapir/auth/oauth2.rb +43 -57
  74. data/lib/rapitapir/cli/command.rb +362 -2
  75. data/lib/rapitapir/cli/mcp_export.rb +18 -0
  76. data/lib/rapitapir/cli/validator.rb +2 -6
  77. data/lib/rapitapir/core/endpoint.rb +59 -6
  78. data/lib/rapitapir/core/enhanced_endpoint.rb +2 -6
  79. data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +53 -0
  80. data/lib/rapitapir/endpoint_registry.rb +47 -0
  81. data/lib/rapitapir/observability/health_check.rb +4 -4
  82. data/lib/rapitapir/observability/logging.rb +10 -10
  83. data/lib/rapitapir/schema.rb +2 -2
  84. data/lib/rapitapir/server/rack_adapter.rb +1 -3
  85. data/lib/rapitapir/server/rails/configuration.rb +77 -0
  86. data/lib/rapitapir/server/rails/controller_base.rb +185 -0
  87. data/lib/rapitapir/server/rails/documentation_helpers.rb +76 -0
  88. data/lib/rapitapir/server/rails/resource_builder.rb +181 -0
  89. data/lib/rapitapir/server/rails/routes.rb +114 -0
  90. data/lib/rapitapir/server/rails_adapter.rb +10 -3
  91. data/lib/rapitapir/server/rails_adapter_class.rb +1 -3
  92. data/lib/rapitapir/server/rails_controller.rb +1 -3
  93. data/lib/rapitapir/server/rails_integration.rb +67 -0
  94. data/lib/rapitapir/server/rails_response_handler.rb +16 -3
  95. data/lib/rapitapir/server/sinatra_adapter.rb +29 -5
  96. data/lib/rapitapir/server/sinatra_integration.rb +4 -4
  97. data/lib/rapitapir/sinatra/extension.rb +2 -2
  98. data/lib/rapitapir/sinatra/oauth2_helpers.rb +34 -40
  99. data/lib/rapitapir/types/array.rb +4 -0
  100. data/lib/rapitapir/types/auto_derivation.rb +4 -18
  101. data/lib/rapitapir/types/datetime.rb +1 -3
  102. data/lib/rapitapir/types/float.rb +2 -6
  103. data/lib/rapitapir/types/hash.rb +40 -2
  104. data/lib/rapitapir/types/integer.rb +4 -12
  105. data/lib/rapitapir/types/object.rb +6 -2
  106. data/lib/rapitapir/types.rb +6 -2
  107. data/lib/rapitapir/version.rb +1 -1
  108. data/lib/rapitapir.rb +5 -3
  109. data/rapitapir.gemspec +7 -5
  110. 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