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