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