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,307 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rapitapir'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
# AWS Lambda handler for SinatraRapiTapir
|
7
|
+
# This example shows how to deploy a RapiTapir API as an AWS Lambda function
|
8
|
+
class BookAPILambda < SinatraRapiTapir
|
9
|
+
# Configure for serverless deployment
|
10
|
+
rapitapir do
|
11
|
+
info(
|
12
|
+
title: 'Serverless Book API',
|
13
|
+
description: 'A book management API deployed on AWS Lambda',
|
14
|
+
version: '1.0.0'
|
15
|
+
)
|
16
|
+
|
17
|
+
# Serverless-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
|
+
# Disable features that don't work well in Lambda
|
25
|
+
set :sessions, false
|
26
|
+
set :static, false
|
27
|
+
end
|
28
|
+
|
29
|
+
# Enable essential features
|
30
|
+
development_defaults!
|
31
|
+
end
|
32
|
+
|
33
|
+
# Book schema optimized for serverless
|
34
|
+
BOOK_SCHEMA = T.hash({
|
35
|
+
"id" => T.integer,
|
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
|
+
"created_at" => T.optional(T.datetime),
|
42
|
+
"updated_at" => T.optional(T.datetime)
|
43
|
+
})
|
44
|
+
|
45
|
+
# Mock data for demonstration (in production, use DynamoDB or RDS)
|
46
|
+
@@books = [
|
47
|
+
{
|
48
|
+
id: 1,
|
49
|
+
title: "The Ruby Programming Language",
|
50
|
+
author: "Matz, Flanagan",
|
51
|
+
isbn: "9780596516178",
|
52
|
+
published_year: 2008,
|
53
|
+
available: true,
|
54
|
+
created_at: Time.now - 86400,
|
55
|
+
updated_at: Time.now - 86400
|
56
|
+
},
|
57
|
+
{
|
58
|
+
id: 2,
|
59
|
+
title: "Effective Ruby",
|
60
|
+
author: "Peter J. Jones",
|
61
|
+
isbn: "9780134456478",
|
62
|
+
published_year: 2014,
|
63
|
+
available: true,
|
64
|
+
created_at: Time.now - 43200,
|
65
|
+
updated_at: Time.now - 43200
|
66
|
+
}
|
67
|
+
]
|
68
|
+
|
69
|
+
# Health check endpoint
|
70
|
+
endpoint(
|
71
|
+
GET('/health')
|
72
|
+
.summary('Health check for Lambda function')
|
73
|
+
.description('Returns the health status of the serverless function')
|
74
|
+
.tags('Health')
|
75
|
+
.ok(T.hash({
|
76
|
+
"status" => T.string,
|
77
|
+
"timestamp" => T.datetime,
|
78
|
+
"lambda_context" => T.optional(T.hash({}))
|
79
|
+
}))
|
80
|
+
.build
|
81
|
+
) do
|
82
|
+
{
|
83
|
+
status: 'healthy',
|
84
|
+
timestamp: Time.now,
|
85
|
+
lambda_context: {
|
86
|
+
function_name: ENV['AWS_LAMBDA_FUNCTION_NAME'],
|
87
|
+
function_version: ENV['AWS_LAMBDA_FUNCTION_VERSION'],
|
88
|
+
memory_limit: ENV['AWS_LAMBDA_FUNCTION_MEMORY_SIZE']
|
89
|
+
}
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
# List all books
|
94
|
+
endpoint(
|
95
|
+
GET('/books')
|
96
|
+
.summary('List all books')
|
97
|
+
.description('Retrieve all books from the serverless database')
|
98
|
+
.query(:limit, T.optional(T.integer(minimum: 1, maximum: 100)), description: 'Limit number of results')
|
99
|
+
.query(:available_only, T.optional(T.boolean), description: 'Filter only available books')
|
100
|
+
.tags('Books')
|
101
|
+
.ok(T.hash({
|
102
|
+
"books" => T.array(BOOK_SCHEMA),
|
103
|
+
"total" => T.integer,
|
104
|
+
"limit" => T.optional(T.integer)
|
105
|
+
}))
|
106
|
+
.build
|
107
|
+
) do |inputs|
|
108
|
+
books = @@books.dup
|
109
|
+
|
110
|
+
# Filter by availability if requested
|
111
|
+
books = books.select { |book| book[:available] } if inputs[:available_only]
|
112
|
+
|
113
|
+
# Apply limit
|
114
|
+
limit = inputs[:limit]
|
115
|
+
books = books.first(limit) if limit
|
116
|
+
|
117
|
+
{
|
118
|
+
books: books,
|
119
|
+
total: books.length,
|
120
|
+
limit: limit
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
# Get a specific book
|
125
|
+
endpoint(
|
126
|
+
GET('/books/:id')
|
127
|
+
.path_param(:id, T.integer, description: 'Book ID')
|
128
|
+
.summary('Get book by ID')
|
129
|
+
.description('Retrieve a specific book by its ID')
|
130
|
+
.tags('Books')
|
131
|
+
.ok(BOOK_SCHEMA)
|
132
|
+
.error_response(404, T.hash({ "error" => T.string, "book_id" => T.integer }))
|
133
|
+
.build
|
134
|
+
) do |inputs|
|
135
|
+
book = @@books.find { |b| b[:id] == inputs[:id] }
|
136
|
+
|
137
|
+
if book
|
138
|
+
book
|
139
|
+
else
|
140
|
+
halt 404, { error: 'Book not found', book_id: inputs[:id] }.to_json
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Create a new book
|
145
|
+
endpoint(
|
146
|
+
POST('/books')
|
147
|
+
.summary('Create a new book')
|
148
|
+
.description('Add a new book to the collection')
|
149
|
+
.body(T.hash({
|
150
|
+
"title" => T.string(min_length: 1, max_length: 255),
|
151
|
+
"author" => T.string(min_length: 1, max_length: 255),
|
152
|
+
"isbn" => T.optional(T.string(pattern: /^\d{10}(\d{3})?$/)),
|
153
|
+
"published_year" => T.optional(T.integer(minimum: 1000, maximum: 3000)),
|
154
|
+
"available" => T.optional(T.boolean)
|
155
|
+
}))
|
156
|
+
.tags('Books')
|
157
|
+
.ok(BOOK_SCHEMA)
|
158
|
+
.error_response(400, T.hash({ "error" => T.string, "details" => T.optional(T.array(T.string)) }))
|
159
|
+
.build
|
160
|
+
) do |inputs|
|
161
|
+
book_data = inputs[:body]
|
162
|
+
|
163
|
+
# Generate new ID
|
164
|
+
new_id = (@@books.map { |b| b[:id] }.max || 0) + 1
|
165
|
+
|
166
|
+
# Create new book with timestamps
|
167
|
+
new_book = {
|
168
|
+
id: new_id,
|
169
|
+
title: book_data[:title] || book_data['title'],
|
170
|
+
author: book_data[:author] || book_data['author'],
|
171
|
+
isbn: book_data[:isbn] || book_data['isbn'],
|
172
|
+
published_year: book_data[:published_year] || book_data['published_year'],
|
173
|
+
available: book_data.key?(:available) ? book_data[:available] : (book_data.key?('available') ? book_data['available'] : true),
|
174
|
+
created_at: Time.now,
|
175
|
+
updated_at: Time.now
|
176
|
+
}
|
177
|
+
|
178
|
+
@@books << new_book
|
179
|
+
|
180
|
+
status 201
|
181
|
+
new_book
|
182
|
+
end
|
183
|
+
|
184
|
+
# Lambda-specific endpoints
|
185
|
+
endpoint(
|
186
|
+
GET('/lambda/info')
|
187
|
+
.summary('AWS Lambda runtime information')
|
188
|
+
.description('Get information about the Lambda runtime environment')
|
189
|
+
.tags('Lambda', 'Info')
|
190
|
+
.ok(T.hash({
|
191
|
+
"runtime" => T.string,
|
192
|
+
"handler" => T.string,
|
193
|
+
"memory_size" => T.string,
|
194
|
+
"timeout" => T.string,
|
195
|
+
"version" => T.string,
|
196
|
+
"log_group" => T.string,
|
197
|
+
"request_id" => T.optional(T.string)
|
198
|
+
}))
|
199
|
+
.build
|
200
|
+
) do
|
201
|
+
{
|
202
|
+
runtime: ENV['AWS_EXECUTION_ENV'] || 'Unknown',
|
203
|
+
handler: ENV['_HANDLER'] || 'Unknown',
|
204
|
+
memory_size: ENV['AWS_LAMBDA_FUNCTION_MEMORY_SIZE'] || 'Unknown',
|
205
|
+
timeout: ENV['AWS_LAMBDA_FUNCTION_TIMEOUT'] || 'Unknown',
|
206
|
+
version: ENV['AWS_LAMBDA_FUNCTION_VERSION'] || 'Unknown',
|
207
|
+
log_group: ENV['AWS_LAMBDA_LOG_GROUP_NAME'] || 'Unknown',
|
208
|
+
request_id: Thread.current[:lambda_request_id]
|
209
|
+
}
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# AWS Lambda handler function
|
214
|
+
def lambda_handler(event:, context:)
|
215
|
+
# Store Lambda context for access in endpoints
|
216
|
+
Thread.current[:lambda_context] = context
|
217
|
+
Thread.current[:lambda_request_id] = context.aws_request_id
|
218
|
+
|
219
|
+
# Convert API Gateway event to Rack environment
|
220
|
+
rack_env = build_rack_env_from_api_gateway(event, context)
|
221
|
+
|
222
|
+
# Process request through Sinatra app
|
223
|
+
app = BookAPILambda.new
|
224
|
+
status, headers, body = app.call(rack_env)
|
225
|
+
|
226
|
+
# Convert Rack response to API Gateway format
|
227
|
+
build_api_gateway_response(status, headers, body)
|
228
|
+
rescue => e
|
229
|
+
# Error handling for Lambda
|
230
|
+
{
|
231
|
+
statusCode: 500,
|
232
|
+
headers: { 'Content-Type' => 'application/json' },
|
233
|
+
body: {
|
234
|
+
error: 'Internal server error',
|
235
|
+
message: e.message,
|
236
|
+
request_id: context&.aws_request_id
|
237
|
+
}.to_json
|
238
|
+
}
|
239
|
+
ensure
|
240
|
+
# Clean up thread variables
|
241
|
+
Thread.current[:lambda_context] = nil
|
242
|
+
Thread.current[:lambda_request_id] = nil
|
243
|
+
end
|
244
|
+
|
245
|
+
# Convert API Gateway event to Rack environment
|
246
|
+
def build_rack_env_from_api_gateway(event, context)
|
247
|
+
method = event['httpMethod'] || event['requestContext']['http']['method']
|
248
|
+
path = event['path'] || event['rawPath']
|
249
|
+
query_string = event['queryStringParameters'] || {}
|
250
|
+
headers = event['headers'] || {}
|
251
|
+
body = event['body']
|
252
|
+
|
253
|
+
# Build query string
|
254
|
+
query_string_formatted = query_string.map { |k, v| "#{k}=#{v}" }.join('&') if query_string.any?
|
255
|
+
|
256
|
+
rack_env = {
|
257
|
+
'REQUEST_METHOD' => method,
|
258
|
+
'PATH_INFO' => path,
|
259
|
+
'QUERY_STRING' => query_string_formatted || '',
|
260
|
+
'CONTENT_TYPE' => headers['content-type'] || headers['Content-Type'],
|
261
|
+
'CONTENT_LENGTH' => body ? body.bytesize.to_s : '0',
|
262
|
+
'rack.input' => StringIO.new(body || ''),
|
263
|
+
'rack.errors' => $stderr,
|
264
|
+
'rack.version' => [1, 3],
|
265
|
+
'rack.url_scheme' => 'https',
|
266
|
+
'rack.multithread' => false,
|
267
|
+
'rack.multiprocess' => true,
|
268
|
+
'rack.run_once' => true,
|
269
|
+
'SERVER_NAME' => headers['host'] || 'localhost',
|
270
|
+
'SERVER_PORT' => '443',
|
271
|
+
'HTTP_HOST' => headers['host'] || 'localhost'
|
272
|
+
}
|
273
|
+
|
274
|
+
# Add HTTP headers
|
275
|
+
headers.each do |key, value|
|
276
|
+
key = key.upcase.gsub('-', '_')
|
277
|
+
key = "HTTP_#{key}" unless %w[CONTENT_TYPE CONTENT_LENGTH].include?(key)
|
278
|
+
rack_env[key] = value
|
279
|
+
end
|
280
|
+
|
281
|
+
rack_env
|
282
|
+
end
|
283
|
+
|
284
|
+
# Convert Rack response to API Gateway format
|
285
|
+
def build_api_gateway_response(status, headers, body)
|
286
|
+
# Combine body parts if it's an array
|
287
|
+
body_content = ''
|
288
|
+
body.each { |part| body_content += part }
|
289
|
+
|
290
|
+
# Determine if response should be base64 encoded
|
291
|
+
is_base64 = !body_content.encoding.ascii_compatible? ||
|
292
|
+
headers['Content-Type']&.include?('image/') ||
|
293
|
+
headers['Content-Type']&.include?('application/pdf')
|
294
|
+
|
295
|
+
{
|
296
|
+
statusCode: status,
|
297
|
+
headers: headers,
|
298
|
+
body: is_base64 ? Base64.encode64(body_content) : body_content,
|
299
|
+
isBase64Encoded: is_base64
|
300
|
+
}
|
301
|
+
end
|
302
|
+
|
303
|
+
# For local development
|
304
|
+
if __FILE__ == $0
|
305
|
+
# Run locally for testing
|
306
|
+
BookAPILambda.run!
|
307
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
# AWS Lambda deployment configuration for RapiTapir
|
2
|
+
# This template.yaml file defines the AWS resources needed to deploy
|
3
|
+
# a SinatraRapiTapir API as a serverless Lambda function
|
4
|
+
|
5
|
+
AWSTemplateFormatVersion: '2010-09-09'
|
6
|
+
Transform: AWS::Serverless-2016-10-31
|
7
|
+
Description: 'RapiTapir Book API deployed on AWS Lambda with API Gateway'
|
8
|
+
|
9
|
+
Globals:
|
10
|
+
Function:
|
11
|
+
Timeout: 30
|
12
|
+
MemorySize: 512
|
13
|
+
Runtime: ruby3.2
|
14
|
+
Environment:
|
15
|
+
Variables:
|
16
|
+
RACK_ENV: production
|
17
|
+
RAPITAPIR_ENV: serverless
|
18
|
+
|
19
|
+
Parameters:
|
20
|
+
Environment:
|
21
|
+
Type: String
|
22
|
+
Default: dev
|
23
|
+
AllowedValues: [dev, staging, prod]
|
24
|
+
Description: Environment name
|
25
|
+
|
26
|
+
DomainName:
|
27
|
+
Type: String
|
28
|
+
Default: ''
|
29
|
+
Description: Custom domain name (optional)
|
30
|
+
|
31
|
+
Resources:
|
32
|
+
# Lambda function
|
33
|
+
BookAPIFunction:
|
34
|
+
Type: AWS::Serverless::Function
|
35
|
+
Properties:
|
36
|
+
FunctionName: !Sub 'rapitapir-book-api-${Environment}'
|
37
|
+
CodeUri: ./
|
38
|
+
Handler: aws_lambda_example.lambda_handler
|
39
|
+
Description: 'RapiTapir Book API serverless function'
|
40
|
+
|
41
|
+
# API Gateway Event
|
42
|
+
Events:
|
43
|
+
BookAPIGateway:
|
44
|
+
Type: Api
|
45
|
+
Properties:
|
46
|
+
Path: /{proxy+}
|
47
|
+
Method: ANY
|
48
|
+
RestApiId: !Ref BookAPIGateway
|
49
|
+
|
50
|
+
BookAPIGatewayRoot:
|
51
|
+
Type: Api
|
52
|
+
Properties:
|
53
|
+
Path: /
|
54
|
+
Method: ANY
|
55
|
+
RestApiId: !Ref BookAPIGateway
|
56
|
+
|
57
|
+
# Permissions
|
58
|
+
Policies:
|
59
|
+
- Version: '2012-10-17'
|
60
|
+
Statement:
|
61
|
+
- Effect: Allow
|
62
|
+
Action:
|
63
|
+
- logs:CreateLogGroup
|
64
|
+
- logs:CreateLogStream
|
65
|
+
- logs:PutLogEvents
|
66
|
+
Resource: '*'
|
67
|
+
- Effect: Allow
|
68
|
+
Action:
|
69
|
+
- dynamodb:GetItem
|
70
|
+
- dynamodb:PutItem
|
71
|
+
- dynamodb:UpdateItem
|
72
|
+
- dynamodb:DeleteItem
|
73
|
+
- dynamodb:Query
|
74
|
+
- dynamodb:Scan
|
75
|
+
Resource: !GetAtt BooksTable.Arn
|
76
|
+
|
77
|
+
Environment:
|
78
|
+
Variables:
|
79
|
+
BOOKS_TABLE_NAME: !Ref BooksTable
|
80
|
+
ENVIRONMENT: !Ref Environment
|
81
|
+
|
82
|
+
# API Gateway
|
83
|
+
BookAPIGateway:
|
84
|
+
Type: AWS::Serverless::Api
|
85
|
+
Properties:
|
86
|
+
Name: !Sub 'rapitapir-book-api-${Environment}'
|
87
|
+
StageName: !Ref Environment
|
88
|
+
Description: 'API Gateway for RapiTapir Book API'
|
89
|
+
|
90
|
+
# CORS Configuration
|
91
|
+
Cors:
|
92
|
+
AllowMethods: "'GET,POST,PUT,DELETE,HEAD,OPTIONS'"
|
93
|
+
AllowHeaders: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
|
94
|
+
AllowOrigin: "'*'"
|
95
|
+
MaxAge: "'600'"
|
96
|
+
|
97
|
+
# Gateway Responses for better error handling
|
98
|
+
GatewayResponses:
|
99
|
+
DEFAULT_4XX:
|
100
|
+
ResponseTemplates:
|
101
|
+
application/json: '{"error": "Client Error", "message": "$context.error.message"}'
|
102
|
+
ResponseParameters:
|
103
|
+
Headers:
|
104
|
+
Access-Control-Allow-Origin: "'*'"
|
105
|
+
DEFAULT_5XX:
|
106
|
+
ResponseTemplates:
|
107
|
+
application/json: '{"error": "Server Error", "message": "$context.error.message"}'
|
108
|
+
ResponseParameters:
|
109
|
+
Headers:
|
110
|
+
Access-Control-Allow-Origin: "'*'"
|
111
|
+
|
112
|
+
# Request/Response transformations
|
113
|
+
DefinitionBody:
|
114
|
+
swagger: '2.0'
|
115
|
+
info:
|
116
|
+
title: 'RapiTapir Book API'
|
117
|
+
version: '1.0.0'
|
118
|
+
paths:
|
119
|
+
/{proxy+}:
|
120
|
+
x-amazon-apigateway-any-method:
|
121
|
+
x-amazon-apigateway-integration:
|
122
|
+
type: aws_proxy
|
123
|
+
httpMethod: POST
|
124
|
+
uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${BookAPIFunction.Arn}/invocations'
|
125
|
+
passthroughBehavior: when_no_match
|
126
|
+
/:
|
127
|
+
x-amazon-apigateway-any-method:
|
128
|
+
x-amazon-apigateway-integration:
|
129
|
+
type: aws_proxy
|
130
|
+
httpMethod: POST
|
131
|
+
uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${BookAPIFunction.Arn}/invocations'
|
132
|
+
passthroughBehavior: when_no_match
|
133
|
+
|
134
|
+
# DynamoDB Table for persistent storage (optional)
|
135
|
+
BooksTable:
|
136
|
+
Type: AWS::DynamoDB::Table
|
137
|
+
Properties:
|
138
|
+
TableName: !Sub 'rapitapir-books-${Environment}'
|
139
|
+
BillingMode: PAY_PER_REQUEST
|
140
|
+
AttributeDefinitions:
|
141
|
+
- AttributeName: id
|
142
|
+
AttributeType: S
|
143
|
+
KeySchema:
|
144
|
+
- AttributeName: id
|
145
|
+
KeyType: HASH
|
146
|
+
StreamSpecification:
|
147
|
+
StreamViewType: NEW_AND_OLD_IMAGES
|
148
|
+
PointInTimeRecoverySpecification:
|
149
|
+
PointInTimeRecoveryEnabled: true
|
150
|
+
Tags:
|
151
|
+
- Key: Environment
|
152
|
+
Value: !Ref Environment
|
153
|
+
- Key: Application
|
154
|
+
Value: RapiTapir-BookAPI
|
155
|
+
|
156
|
+
# CloudWatch Log Group
|
157
|
+
BookAPILogGroup:
|
158
|
+
Type: AWS::Logs::LogGroup
|
159
|
+
Properties:
|
160
|
+
LogGroupName: !Sub '/aws/lambda/rapitapir-book-api-${Environment}'
|
161
|
+
RetentionInDays: 14
|
162
|
+
|
163
|
+
# Custom Domain (optional)
|
164
|
+
CustomDomain:
|
165
|
+
Type: AWS::ApiGateway::DomainName
|
166
|
+
Condition: HasDomainName
|
167
|
+
Properties:
|
168
|
+
DomainName: !Ref DomainName
|
169
|
+
SecurityPolicy: TLS_1_2
|
170
|
+
CertificateArn: !Ref SSLCertificate
|
171
|
+
|
172
|
+
# Route53 Record for custom domain
|
173
|
+
DomainRecord:
|
174
|
+
Type: AWS::Route53::RecordSet
|
175
|
+
Condition: HasDomainName
|
176
|
+
Properties:
|
177
|
+
HostedZoneId: !Ref HostedZone
|
178
|
+
Name: !Ref DomainName
|
179
|
+
Type: A
|
180
|
+
AliasTarget:
|
181
|
+
DNSName: !GetAtt CustomDomain.DistributionDomainName
|
182
|
+
HostedZoneId: !GetAtt CustomDomain.DistributionHostedZoneId
|
183
|
+
|
184
|
+
Conditions:
|
185
|
+
HasDomainName: !Not [!Equals [!Ref DomainName, '']]
|
186
|
+
|
187
|
+
Outputs:
|
188
|
+
# API Gateway URL
|
189
|
+
BookAPIGatewayUrl:
|
190
|
+
Description: 'API Gateway endpoint URL for Book API'
|
191
|
+
Value: !Sub 'https://${BookAPIGateway}.execute-api.${AWS::Region}.amazonaws.com/${Environment}/'
|
192
|
+
Export:
|
193
|
+
Name: !Sub '${AWS::StackName}-api-url'
|
194
|
+
|
195
|
+
# Lambda Function ARN
|
196
|
+
BookAPIFunctionArn:
|
197
|
+
Description: 'Book API Lambda Function ARN'
|
198
|
+
Value: !GetAtt BookAPIFunction.Arn
|
199
|
+
Export:
|
200
|
+
Name: !Sub '${AWS::StackName}-function-arn'
|
201
|
+
|
202
|
+
# DynamoDB Table
|
203
|
+
BooksTableName:
|
204
|
+
Description: 'DynamoDB table name for books'
|
205
|
+
Value: !Ref BooksTable
|
206
|
+
Export:
|
207
|
+
Name: !Sub '${AWS::StackName}-table-name'
|
208
|
+
|
209
|
+
# Custom Domain URL (if configured)
|
210
|
+
CustomDomainUrl:
|
211
|
+
Condition: HasDomainName
|
212
|
+
Description: 'Custom domain URL for the API'
|
213
|
+
Value: !Sub 'https://${DomainName}/'
|
214
|
+
Export:
|
215
|
+
Name: !Sub '${AWS::StackName}-custom-domain-url'
|