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,367 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rapitapir'
4
+ require 'functions_framework'
5
+ require 'json'
6
+
7
+ # Google Cloud Functions handler for SinatraRapiTapir
8
+ # This example shows how to deploy a RapiTapir API as a Google Cloud Function
9
+ class BookAPICloudFunction < SinatraRapiTapir
10
+ # Configure for Google Cloud Functions
11
+ rapitapir do
12
+ info(
13
+ title: 'Serverless Book API - Google Cloud Functions',
14
+ description: 'A book management API deployed on Google Cloud Functions',
15
+ version: '1.0.0'
16
+ )
17
+
18
+ # Cloud Functions optimized configuration
19
+ configure do
20
+ set :environment, :production
21
+ set :logging, true
22
+ set :dump_errors, false
23
+ set :raise_errors, true
24
+
25
+ # Disable features that don't work well in Cloud Functions
26
+ set :sessions, false
27
+ set :static, false
28
+ set :protection, except: [:json_csrf] # Cloud Functions handles CSRF
29
+ end
30
+
31
+ # Enable essential features for serverless
32
+ development_defaults!
33
+ end
34
+
35
+ # Book schema
36
+ BOOK_SCHEMA = T.hash({
37
+ "id" => T.string, # Using string IDs for Firestore compatibility
38
+ "title" => T.string(min_length: 1, max_length: 255),
39
+ "author" => T.string(min_length: 1, max_length: 255),
40
+ "isbn" => T.optional(T.string(pattern: /^\d{10}(\d{3})?$/)),
41
+ "published_year" => T.optional(T.integer(minimum: 1000, maximum: 3000)),
42
+ "available" => T.boolean,
43
+ "created_at" => T.datetime,
44
+ "updated_at" => T.datetime,
45
+ "metadata" => T.optional(T.hash({}))
46
+ })
47
+
48
+ # Mock data (in production, use Firestore or Cloud SQL)
49
+ @@books = [
50
+ {
51
+ id: "book_1",
52
+ title: "The Ruby Programming Language",
53
+ author: "Matz, Flanagan",
54
+ isbn: "9780596516178",
55
+ published_year: 2008,
56
+ available: true,
57
+ created_at: Time.now - 86400,
58
+ updated_at: Time.now - 86400,
59
+ metadata: { category: "programming", difficulty: "intermediate" }
60
+ },
61
+ {
62
+ id: "book_2",
63
+ title: "Effective Ruby",
64
+ author: "Peter J. Jones",
65
+ isbn: "9780134456478",
66
+ published_year: 2014,
67
+ available: true,
68
+ created_at: Time.now - 43200,
69
+ updated_at: Time.now - 43200,
70
+ metadata: { category: "programming", difficulty: "advanced" }
71
+ }
72
+ ]
73
+
74
+ # Health check with Cloud Functions specific info
75
+ endpoint(
76
+ GET('/health')
77
+ .summary('Health check for Cloud Function')
78
+ .description('Returns the health status of the Google Cloud Function')
79
+ .tags('Health', 'Cloud Functions')
80
+ .ok(T.hash({
81
+ "status" => T.string,
82
+ "timestamp" => T.datetime,
83
+ "function_info" => T.hash({
84
+ "name" => T.optional(T.string),
85
+ "region" => T.optional(T.string),
86
+ "project" => T.optional(T.string),
87
+ "memory" => T.optional(T.string),
88
+ "timeout" => T.optional(T.string)
89
+ })
90
+ }))
91
+ .build
92
+ ) do
93
+ {
94
+ status: 'healthy',
95
+ timestamp: Time.now,
96
+ function_info: {
97
+ name: ENV['FUNCTION_NAME'],
98
+ region: ENV['FUNCTION_REGION'],
99
+ project: ENV['GCP_PROJECT'] || ENV['GOOGLE_CLOUD_PROJECT'],
100
+ memory: ENV['FUNCTION_MEMORY_MB'],
101
+ timeout: ENV['FUNCTION_TIMEOUT_SEC']
102
+ }
103
+ }
104
+ end
105
+
106
+ # List books with Cloud Functions optimizations
107
+ endpoint(
108
+ GET('/books')
109
+ .summary('List all books')
110
+ .description('Retrieve all books with optional filtering')
111
+ .query(:limit, T.optional(T.integer(minimum: 1, maximum: 100)), description: 'Limit results')
112
+ .query(:category, T.optional(T.string), description: 'Filter by category')
113
+ .query(:available_only, T.optional(T.boolean), description: 'Show only available books')
114
+ .tags('Books')
115
+ .ok(T.hash({
116
+ "books" => T.array(BOOK_SCHEMA),
117
+ "total" => T.integer,
118
+ "limit" => T.optional(T.integer),
119
+ "filters_applied" => T.hash({})
120
+ }))
121
+ .build
122
+ ) do |inputs|
123
+ books = @@books.dup
124
+ filters_applied = {}
125
+
126
+ # Apply category filter
127
+ if inputs[:category]
128
+ books = books.select { |book| book[:metadata]&.dig(:category) == inputs[:category] }
129
+ filters_applied[:category] = inputs[:category]
130
+ end
131
+
132
+ # Apply availability filter
133
+ if inputs[:available_only]
134
+ books = books.select { |book| book[:available] }
135
+ filters_applied[:available_only] = true
136
+ end
137
+
138
+ # Apply limit
139
+ limit = inputs[:limit]
140
+ books = books.first(limit) if limit
141
+
142
+ {
143
+ books: books,
144
+ total: books.length,
145
+ limit: limit,
146
+ filters_applied: filters_applied
147
+ }
148
+ end
149
+
150
+ # Get specific book
151
+ endpoint(
152
+ GET('/books/:id')
153
+ .path_param(:id, T.string, description: 'Book ID')
154
+ .summary('Get book by ID')
155
+ .description('Retrieve a specific book')
156
+ .tags('Books')
157
+ .ok(BOOK_SCHEMA)
158
+ .error_response(404, T.hash({ "error" => T.string, "book_id" => T.string }))
159
+ .build
160
+ ) do |inputs|
161
+ book = @@books.find { |b| b[:id] == inputs[:id] }
162
+
163
+ if book
164
+ book
165
+ else
166
+ halt 404, { error: 'Book not found', book_id: inputs[:id] }.to_json
167
+ end
168
+ end
169
+
170
+ # Create book
171
+ endpoint(
172
+ POST('/books')
173
+ .summary('Create a new book')
174
+ .description('Add a new book to the collection')
175
+ .body(T.hash({
176
+ "title" => T.string(min_length: 1, max_length: 255),
177
+ "author" => T.string(min_length: 1, max_length: 255),
178
+ "isbn" => T.optional(T.string(pattern: /^\d{10}(\d{3})?$/)),
179
+ "published_year" => T.optional(T.integer(minimum: 1000, maximum: 3000)),
180
+ "available" => T.optional(T.boolean),
181
+ "metadata" => T.optional(T.hash({}))
182
+ }))
183
+ .tags('Books')
184
+ .ok(BOOK_SCHEMA)
185
+ .error_response(400, T.hash({ "error" => T.string, "details" => T.optional(T.array(T.string)) }))
186
+ .build
187
+ ) do |inputs|
188
+ book_data = inputs[:body]
189
+
190
+ # Generate new ID (in production, use Firestore auto-generated IDs)
191
+ new_id = "book_#{Time.now.to_i}_#{rand(1000)}"
192
+
193
+ new_book = {
194
+ id: new_id,
195
+ title: book_data[:title] || book_data['title'],
196
+ author: book_data[:author] || book_data['author'],
197
+ isbn: book_data[:isbn] || book_data['isbn'],
198
+ published_year: book_data[:published_year] || book_data['published_year'],
199
+ available: book_data.key?(:available) ? book_data[:available] : (book_data.key?('available') ? book_data['available'] : true),
200
+ metadata: book_data[:metadata] || book_data['metadata'] || {},
201
+ created_at: Time.now,
202
+ updated_at: Time.now
203
+ }
204
+
205
+ @@books << new_book
206
+
207
+ status 201
208
+ new_book
209
+ end
210
+
211
+ # Cloud Functions specific endpoints
212
+ endpoint(
213
+ GET('/gcp/function-info')
214
+ .summary('Google Cloud Function runtime info')
215
+ .description('Get detailed information about the Cloud Function environment')
216
+ .tags('GCP', 'Info')
217
+ .ok(T.hash({
218
+ "function" => T.hash({
219
+ "name" => T.optional(T.string),
220
+ "region" => T.optional(T.string),
221
+ "project" => T.optional(T.string),
222
+ "memory_mb" => T.optional(T.string),
223
+ "timeout_sec" => T.optional(T.string),
224
+ "runtime" => T.optional(T.string)
225
+ }),
226
+ "request" => T.hash({
227
+ "execution_id" => T.optional(T.string),
228
+ "trace_id" => T.optional(T.string)
229
+ }),
230
+ "environment" => T.hash({
231
+ "ruby_version" => T.string,
232
+ "rack_env" => T.optional(T.string)
233
+ })
234
+ }))
235
+ .build
236
+ ) do
237
+ {
238
+ function: {
239
+ name: ENV['FUNCTION_NAME'],
240
+ region: ENV['FUNCTION_REGION'],
241
+ project: ENV['GCP_PROJECT'] || ENV['GOOGLE_CLOUD_PROJECT'],
242
+ memory_mb: ENV['FUNCTION_MEMORY_MB'],
243
+ timeout_sec: ENV['FUNCTION_TIMEOUT_SEC'],
244
+ runtime: ENV['FUNCTION_RUNTIME']
245
+ },
246
+ request: {
247
+ execution_id: ENV['FUNCTION_EXECUTION_ID'],
248
+ trace_id: request.env['HTTP_X_CLOUD_TRACE_CONTEXT']&.split('/')&.first
249
+ },
250
+ environment: {
251
+ ruby_version: RUBY_VERSION,
252
+ rack_env: ENV['RACK_ENV']
253
+ }
254
+ }
255
+ end
256
+
257
+ # Search with Cloud Functions optimizations
258
+ endpoint(
259
+ GET('/books/search')
260
+ .query(:q, T.string(min_length: 1), description: 'Search query')
261
+ .query(:fields, T.optional(T.array(T.string(enum: %w[title author isbn]))), description: 'Fields to search')
262
+ .summary('Search books')
263
+ .description('Search books by title, author, or ISBN')
264
+ .tags('Books', 'Search')
265
+ .ok(T.hash({
266
+ "results" => T.array(BOOK_SCHEMA),
267
+ "query" => T.string,
268
+ "fields_searched" => T.array(T.string),
269
+ "total_matches" => T.integer
270
+ }))
271
+ .build
272
+ ) do |inputs|
273
+ query = inputs[:q].downcase
274
+ fields = inputs[:fields] || %w[title author isbn]
275
+
276
+ results = @@books.select do |book|
277
+ fields.any? do |field|
278
+ book[field.to_sym]&.to_s&.downcase&.include?(query)
279
+ end
280
+ end
281
+
282
+ {
283
+ results: results,
284
+ query: inputs[:q],
285
+ fields_searched: fields,
286
+ total_matches: results.length
287
+ }
288
+ end
289
+ end
290
+
291
+ # Initialize the Sinatra app instance
292
+ APP = BookAPICloudFunction.new
293
+
294
+ # Google Cloud Functions entry point using Functions Framework
295
+ FunctionsFramework.http('rapitapir_book_api') do |request|
296
+ # Convert Cloud Functions request to Rack environment
297
+ rack_env = build_rack_env_from_cloud_functions(request)
298
+
299
+ # Process through Sinatra app
300
+ status, headers, body = APP.call(rack_env)
301
+
302
+ # Convert response for Cloud Functions
303
+ body_content = ''
304
+ body.each { |part| body_content += part }
305
+
306
+ # Create Cloud Functions response
307
+ response = Rack::Response.new(body_content, status, headers)
308
+ response.finish
309
+ rescue => e
310
+ # Error handling
311
+ error_response = {
312
+ error: 'Internal server error',
313
+ message: e.message,
314
+ timestamp: Time.now.iso8601
315
+ }
316
+
317
+ [500, { 'Content-Type' => 'application/json' }, [error_response.to_json]]
318
+ end
319
+
320
+ # Convert Cloud Functions request to Rack environment
321
+ def build_rack_env_from_cloud_functions(request)
322
+ # Extract method and path
323
+ method = request.request_method
324
+ path = request.path
325
+ query_string = request.query_string
326
+
327
+ # Get request body
328
+ body = request.body.read if request.body
329
+ request.body.rewind if request.body&.respond_to?(:rewind)
330
+
331
+ rack_env = {
332
+ 'REQUEST_METHOD' => method,
333
+ 'PATH_INFO' => path,
334
+ 'QUERY_STRING' => query_string || '',
335
+ 'CONTENT_TYPE' => request.content_type,
336
+ 'CONTENT_LENGTH' => body ? body.bytesize.to_s : '0',
337
+ 'rack.input' => StringIO.new(body || ''),
338
+ 'rack.errors' => $stderr,
339
+ 'rack.version' => [1, 3],
340
+ 'rack.url_scheme' => 'https',
341
+ 'rack.multithread' => false,
342
+ 'rack.multiprocess' => true,
343
+ 'rack.run_once' => true,
344
+ 'SERVER_NAME' => request.host,
345
+ 'SERVER_PORT' => request.port.to_s,
346
+ 'HTTP_HOST' => request.host
347
+ }
348
+
349
+ # Add HTTP headers
350
+ request.headers.each do |key, value|
351
+ key = key.upcase.gsub('-', '_')
352
+ key = "HTTP_#{key}" unless %w[CONTENT_TYPE CONTENT_LENGTH].include?(key)
353
+ rack_env[key] = value.is_a?(Array) ? value.join(',') : value
354
+ end
355
+
356
+ # Add Cloud Functions specific headers
357
+ rack_env['HTTP_X_FORWARDED_FOR'] = request.headers['x-forwarded-for'] if request.headers['x-forwarded-for']
358
+ rack_env['HTTP_X_CLOUD_TRACE_CONTEXT'] = request.headers['x-cloud-trace-context'] if request.headers['x-cloud-trace-context']
359
+
360
+ rack_env
361
+ end
362
+
363
+ # For local development
364
+ if __FILE__ == $0
365
+ # Run locally for testing
366
+ BookAPICloudFunction.run!
367
+ end
@@ -0,0 +1,23 @@
1
+ name: rapitapir-book-api
2
+ runtime: ruby32
3
+ entry_point: rapitapir_book_api
4
+
5
+ # Environment variables
6
+ env_variables:
7
+ RACK_ENV: production
8
+ RAPITAPIR_ENV: serverless
9
+ GCP_PROJECT: your-project-id
10
+
11
+ # Function configuration
12
+ available_memory_mb: 512
13
+ timeout: 60s
14
+ max_instances: 100
15
+
16
+ # VPC configuration (optional)
17
+ # vpc_connector: projects/PROJECT_ID/locations/REGION/connectors/CONNECTOR_NAME
18
+
19
+ # Security
20
+ ingress_settings: ALLOW_ALL # Change to ALLOW_INTERNAL_ONLY for internal APIs
21
+
22
+ # Source code deployment
23
+ source_archive_url: gs://your-bucket/source.zip
@@ -0,0 +1,24 @@
1
+ {
2
+ "version": "2.0",
3
+ "logging": {
4
+ "applicationInsights": {
5
+ "samplingSettings": {
6
+ "isEnabled": true,
7
+ "excludedTypes": "Request"
8
+ }
9
+ }
10
+ },
11
+ "extensionBundle": {
12
+ "id": "Microsoft.Azure.Functions.ExtensionBundle",
13
+ "version": "[2.*, 3.0.0)"
14
+ },
15
+ "functionTimeout": "00:05:00",
16
+ "customHandler": {
17
+ "description": {
18
+ "defaultExecutablePath": "ruby",
19
+ "workingDirectory": "",
20
+ "arguments": ["azure_functions_example.rb"]
21
+ },
22
+ "enableForwardingHttpRequest": true
23
+ }
24
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "rapitapir-serverless-examples",
3
+ "version": "1.0.0",
4
+ "description": "Serverless deployment examples for RapiTapir APIs",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "deploy:aws": "sam build && sam deploy",
8
+ "deploy:gcp": "gcloud functions deploy rapitapir-api --runtime ruby32 --trigger-http --source .",
9
+ "deploy:azure": "func azure functionapp publish rapitapir-function-app",
10
+ "deploy:vercel": "vercel --prod",
11
+ "local:aws": "sam local start-api",
12
+ "local:gcp": "functions-framework-ruby --target=rapitapir_book_api",
13
+ "local:azure": "func start",
14
+ "local:vercel": "vercel dev",
15
+ "test": "bundle exec rspec",
16
+ "test:integration": "bundle exec rspec spec/integration/"
17
+ },
18
+ "dependencies": {},
19
+ "devDependencies": {},
20
+ "keywords": [
21
+ "rapitapir",
22
+ "serverless",
23
+ "api",
24
+ "ruby",
25
+ "aws-lambda",
26
+ "google-cloud-functions",
27
+ "azure-functions",
28
+ "vercel"
29
+ ],
30
+ "author": "RapiTapir Team",
31
+ "license": "MIT"
32
+ }
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../spec_helper'
4
+ require_relative '../../aws_lambda_example'
5
+
6
+ RSpec.describe 'AWS Lambda Example' do
7
+ def app
8
+ BookAPILambda.new
9
+ end
10
+
11
+ describe 'HTTP endpoints' do
12
+ describe 'GET /health' do
13
+ it 'returns health status' do
14
+ get '/health'
15
+
16
+ expect(last_response).to be_ok
17
+ response = expect_json_response(%w[status timestamp lambda_context])
18
+ expect(response['status']).to eq('healthy')
19
+ end
20
+ end
21
+
22
+ describe 'GET /books' do
23
+ it 'returns list of books' do
24
+ get '/books'
25
+
26
+ expect(last_response).to be_ok
27
+ response = expect_json_response(%w[books total])
28
+ expect(response['books']).to be_an(Array)
29
+ expect(response['total']).to be_an(Integer)
30
+ end
31
+
32
+ it 'applies limit parameter' do
33
+ get '/books?limit=1'
34
+
35
+ expect(last_response).to be_ok
36
+ response = expect_json_response(%w[books total limit])
37
+ expect(response['books'].length).to eq(1)
38
+ expect(response['limit']).to eq(1)
39
+ end
40
+
41
+ it 'filters available books only' do
42
+ get '/books?available_only=true'
43
+
44
+ expect(last_response).to be_ok
45
+ response = expect_json_response(%w[books total])
46
+ response['books'].each do |book|
47
+ expect(book['available']).to be true
48
+ end
49
+ end
50
+ end
51
+
52
+ describe 'GET /books/:id' do
53
+ it 'returns specific book' do
54
+ get '/books/1'
55
+
56
+ expect(last_response).to be_ok
57
+ response = expect_json_response(%w[id title author])
58
+ expect(response['id']).to eq(1)
59
+ end
60
+
61
+ it 'returns 404 for non-existent book' do
62
+ get '/books/999'
63
+
64
+ expect(last_response.status).to eq(404)
65
+ response = expect_json_response(%w[error book_id])
66
+ expect(response['book_id']).to eq(999)
67
+ end
68
+ end
69
+
70
+ describe 'POST /books' do
71
+ let(:valid_book) do
72
+ {
73
+ title: 'Test Book',
74
+ author: 'Test Author',
75
+ isbn: '9781234567890',
76
+ published_year: 2024,
77
+ available: true
78
+ }
79
+ end
80
+
81
+ it 'creates a new book' do
82
+ post '/books', valid_book.to_json, 'CONTENT_TYPE' => 'application/json'
83
+
84
+ expect(last_response.status).to eq(201)
85
+ response = expect_json_response(%w[id title author created_at updated_at])
86
+ expect(response['title']).to eq('Test Book')
87
+ expect(response['author']).to eq('Test Author')
88
+ end
89
+
90
+ it 'validates required fields' do
91
+ invalid_book = { title: '' }
92
+ post '/books', invalid_book.to_json, 'CONTENT_TYPE' => 'application/json'
93
+
94
+ expect(last_response.status).to eq(400)
95
+ end
96
+ end
97
+
98
+ describe 'GET /lambda/info' do
99
+ it 'returns Lambda runtime information' do
100
+ get '/lambda/info'
101
+
102
+ expect(last_response).to be_ok
103
+ response = expect_json_response(%w[runtime handler memory_size])
104
+ end
105
+ end
106
+ end
107
+
108
+ describe 'Lambda handler function' do
109
+ it 'processes API Gateway event' do
110
+ event = {
111
+ 'httpMethod' => 'GET',
112
+ 'path' => '/health',
113
+ 'queryStringParameters' => nil,
114
+ 'headers' => {
115
+ 'Content-Type' => 'application/json'
116
+ },
117
+ 'body' => nil
118
+ }
119
+
120
+ context = mock_aws_context
121
+
122
+ response = lambda_handler(event: event, context: context)
123
+
124
+ expect(response[:statusCode]).to eq(200)
125
+ expect(response[:headers]).to include('Content-Type')
126
+
127
+ body = JSON.parse(response[:body])
128
+ expect(body['status']).to eq('healthy')
129
+ end
130
+
131
+ it 'handles POST requests with body' do
132
+ book_data = {
133
+ title: 'Lambda Book',
134
+ author: 'Lambda Author'
135
+ }
136
+
137
+ event = {
138
+ 'httpMethod' => 'POST',
139
+ 'path' => '/books',
140
+ 'queryStringParameters' => nil,
141
+ 'headers' => {
142
+ 'Content-Type' => 'application/json'
143
+ },
144
+ 'body' => book_data.to_json
145
+ }
146
+
147
+ context = mock_aws_context
148
+
149
+ response = lambda_handler(event: event, context: context)
150
+
151
+ expect(response[:statusCode]).to eq(201)
152
+
153
+ body = JSON.parse(response[:body])
154
+ expect(body['title']).to eq('Lambda Book')
155
+ end
156
+
157
+ it 'handles errors gracefully' do
158
+ # Simulate an error by passing invalid event
159
+ event = {}
160
+ context = mock_aws_context
161
+
162
+ response = lambda_handler(event: event, context: context)
163
+
164
+ expect(response[:statusCode]).to eq(500)
165
+ expect(response[:headers]).to include('Content-Type' => 'application/json')
166
+
167
+ body = JSON.parse(response[:body])
168
+ expect(body).to have_key('error')
169
+ end
170
+ end
171
+
172
+ describe 'Rack environment conversion' do
173
+ it 'converts API Gateway event to Rack env' do
174
+ event = {
175
+ 'httpMethod' => 'GET',
176
+ 'path' => '/test',
177
+ 'queryStringParameters' => { 'param' => 'value' },
178
+ 'headers' => {
179
+ 'Host' => 'api.example.com',
180
+ 'User-Agent' => 'Test'
181
+ },
182
+ 'body' => 'test body'
183
+ }
184
+
185
+ context = mock_aws_context
186
+ rack_env = build_rack_env_from_api_gateway(event, context)
187
+
188
+ expect(rack_env['REQUEST_METHOD']).to eq('GET')
189
+ expect(rack_env['PATH_INFO']).to eq('/test')
190
+ expect(rack_env['QUERY_STRING']).to eq('param=value')
191
+ expect(rack_env['HTTP_HOST']).to eq('api.example.com')
192
+ expect(rack_env['HTTP_USER_AGENT']).to eq('Test')
193
+ expect(rack_env['CONTENT_LENGTH']).to eq('9')
194
+ end
195
+ end
196
+ end