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,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require 'rack/test'
5
+ require 'json'
6
+
7
+ RSpec.configure do |config|
8
+ config.include Rack::Test::Methods
9
+
10
+ config.expect_with :rspec do |expectations|
11
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
12
+ end
13
+
14
+ config.mock_with :rspec do |mocks|
15
+ mocks.verify_partial_doubles = true
16
+ end
17
+
18
+ config.shared_context_metadata_behavior = :apply_to_host_groups
19
+ config.filter_run_when_matching :focus
20
+ config.disable_monkey_patching!
21
+ config.warnings = true
22
+
23
+ if config.files_to_run.one?
24
+ config.default_formatter = "doc"
25
+ end
26
+
27
+ config.profile_examples = 10
28
+ config.order = :random
29
+ Kernel.srand config.seed
30
+ end
31
+
32
+ # Helper methods for testing serverless functions
33
+ module ServerlessTestHelpers
34
+ def parse_json_response
35
+ JSON.parse(last_response.body)
36
+ end
37
+
38
+ def expect_json_response(expected_keys = [])
39
+ expect(last_response.content_type).to include('application/json')
40
+ response_body = parse_json_response
41
+ expected_keys.each do |key|
42
+ expect(response_body).to have_key(key.to_s)
43
+ end
44
+ response_body
45
+ end
46
+
47
+ def mock_aws_context(request_id: 'test-request-123')
48
+ OpenStruct.new(
49
+ aws_request_id: request_id,
50
+ function_name: 'test-function',
51
+ function_version: '$LATEST',
52
+ memory_limit_in_mb: '512'
53
+ )
54
+ end
55
+
56
+ def mock_gcp_request(method: 'GET', path: '/', body: nil, headers: {})
57
+ request = double('request')
58
+ allow(request).to receive(:request_method).and_return(method)
59
+ allow(request).to receive(:path).and_return(path)
60
+ allow(request).to receive(:query_string).and_return('')
61
+ allow(request).to receive(:body).and_return(StringIO.new(body || ''))
62
+ allow(request).to receive(:content_type).and_return(headers['Content-Type'])
63
+ allow(request).to receive(:headers).and_return(headers)
64
+ allow(request).to receive(:host).and_return('localhost')
65
+ allow(request).to receive(:port).and_return(8080)
66
+ request
67
+ end
68
+
69
+ def mock_azure_context(invocation_id: 'test-invocation-123')
70
+ {
71
+ invocation_id: invocation_id,
72
+ function_name: 'test-function',
73
+ function_directory: '/tmp'
74
+ }
75
+ end
76
+
77
+ def mock_vercel_request(method: 'GET', url: 'https://localhost/', body: nil, headers: {})
78
+ request = double('request')
79
+ allow(request).to receive(:method).and_return(method)
80
+ allow(request).to receive(:url).and_return(url)
81
+ allow(request).to receive(:body).and_return(body)
82
+ allow(request).to receive(:headers).and_return(headers)
83
+ request
84
+ end
85
+ end
86
+
87
+ RSpec.configure do |config|
88
+ config.include ServerlessTestHelpers
89
+ end
@@ -0,0 +1,31 @@
1
+ {
2
+ "version": 2,
3
+ "name": "rapitapir-vercel-api",
4
+ "builds": [
5
+ {
6
+ "src": "vercel_example.rb",
7
+ "use": "@vercel/ruby"
8
+ }
9
+ ],
10
+ "routes": [
11
+ {
12
+ "src": "/api/(.*)",
13
+ "dest": "/vercel_example.rb"
14
+ },
15
+ {
16
+ "src": "/(.*)",
17
+ "dest": "/vercel_example.rb"
18
+ }
19
+ ],
20
+ "env": {
21
+ "RACK_ENV": "production",
22
+ "RAPITAPIR_ENV": "serverless"
23
+ },
24
+ "regions": ["all"],
25
+ "functions": {
26
+ "vercel_example.rb": {
27
+ "memory": 512,
28
+ "maxDuration": 30
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,404 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rapitapir'
4
+ require 'json'
5
+
6
+ # Vercel serverless function for SinatraRapiTapir
7
+ # This example shows how to deploy a RapiTapir API on Vercel
8
+ class BookAPIVercel < SinatraRapiTapir
9
+ # Configure for Vercel serverless
10
+ rapitapir do
11
+ info(
12
+ title: 'Serverless Book API - Vercel',
13
+ description: 'A book management API deployed on Vercel Edge Functions',
14
+ version: '1.0.0'
15
+ )
16
+
17
+ # Vercel 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
+ # Vercel-specific optimizations
25
+ set :sessions, false
26
+ set :static, false
27
+ set :show_exceptions, false
28
+ end
29
+
30
+ development_defaults!
31
+ end
32
+
33
+ # Book schema optimized for edge computing
34
+ BOOK_SCHEMA = T.hash({
35
+ "id" => T.string,
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
+ "tags" => T.optional(T.array(T.string)),
42
+ "rating" => T.optional(T.float(minimum: 0, maximum: 5)),
43
+ "created_at" => T.datetime,
44
+ "updated_at" => T.datetime
45
+ })
46
+
47
+ # In-memory storage for demo (use Vercel KV or external DB in production)
48
+ @@books = [
49
+ {
50
+ id: "vercel_book_1",
51
+ title: "Serverless Ruby at the Edge",
52
+ author: "Edge Developer",
53
+ isbn: "9781111111111",
54
+ published_year: 2024,
55
+ available: true,
56
+ tags: ["ruby", "serverless", "edge"],
57
+ rating: 4.8,
58
+ created_at: Time.now - 86400,
59
+ updated_at: Time.now - 86400
60
+ },
61
+ {
62
+ id: "vercel_book_2",
63
+ title: "Building APIs with RapiTapir",
64
+ author: "API Expert",
65
+ isbn: "9782222222222",
66
+ published_year: 2024,
67
+ available: true,
68
+ tags: ["api", "ruby", "rapitapir"],
69
+ rating: 4.9,
70
+ created_at: Time.now - 43200,
71
+ updated_at: Time.now - 43200
72
+ }
73
+ ]
74
+
75
+ # Health check with Vercel-specific info
76
+ endpoint(
77
+ GET('/health')
78
+ .summary('Health check for Vercel function')
79
+ .description('Returns health status with Vercel deployment info')
80
+ .tags('Health', 'Vercel')
81
+ .ok(T.hash({
82
+ "status" => T.string,
83
+ "timestamp" => T.datetime,
84
+ "vercel_info" => T.hash({
85
+ "region" => T.optional(T.string),
86
+ "deployment_id" => T.optional(T.string),
87
+ "environment" => T.optional(T.string),
88
+ "branch" => T.optional(T.string),
89
+ "commit_sha" => T.optional(T.string)
90
+ }),
91
+ "performance" => T.hash({
92
+ "cold_start" => T.boolean,
93
+ "edge_location" => T.optional(T.string)
94
+ })
95
+ }))
96
+ .build
97
+ ) do
98
+ {
99
+ status: 'healthy',
100
+ timestamp: Time.now,
101
+ vercel_info: {
102
+ region: ENV['VERCEL_REGION'],
103
+ deployment_id: ENV['VERCEL_DEPLOYMENT_ID'],
104
+ environment: ENV['VERCEL_ENV'],
105
+ branch: ENV['VERCEL_GIT_COMMIT_REF'],
106
+ commit_sha: ENV['VERCEL_GIT_COMMIT_SHA']
107
+ },
108
+ performance: {
109
+ cold_start: Thread.current[:cold_start] || false,
110
+ edge_location: ENV['VERCEL_REGION']
111
+ }
112
+ }
113
+ end
114
+
115
+ # Fast book listing for edge performance
116
+ endpoint(
117
+ GET('/books')
118
+ .summary('List books (edge optimized)')
119
+ .description('Fast book listing optimized for edge deployment')
120
+ .query(:limit, T.optional(T.integer(minimum: 1, maximum: 50)), description: 'Limit results')
121
+ .query(:tag, T.optional(T.string), description: 'Filter by tag')
122
+ .query(:min_rating, T.optional(T.float(minimum: 0, maximum: 5)), description: 'Minimum rating')
123
+ .tags('Books')
124
+ .ok(T.hash({
125
+ "books" => T.array(BOOK_SCHEMA),
126
+ "total" => T.integer,
127
+ "edge_cached" => T.boolean,
128
+ "response_time_ms" => T.float
129
+ }))
130
+ .build
131
+ ) do |inputs|
132
+ start_time = Time.now
133
+ books = @@books.dup
134
+
135
+ # Apply tag filter
136
+ books = books.select { |book| book[:tags]&.include?(inputs[:tag]) } if inputs[:tag]
137
+
138
+ # Apply rating filter
139
+ books = books.select { |book| (book[:rating] || 0) >= inputs[:min_rating] } if inputs[:min_rating]
140
+
141
+ # Apply limit
142
+ limit = inputs[:limit] || 20
143
+ books = books.first(limit)
144
+
145
+ response_time = ((Time.now - start_time) * 1000).round(2)
146
+
147
+ {
148
+ books: books,
149
+ total: books.length,
150
+ edge_cached: false, # Could be true if using Vercel Edge Config
151
+ response_time_ms: response_time
152
+ }
153
+ end
154
+
155
+ # Get book with edge caching headers
156
+ endpoint(
157
+ GET('/books/:id')
158
+ .path_param(:id, T.string, description: 'Book ID')
159
+ .summary('Get book by ID (edge cached)')
160
+ .description('Retrieve a book with aggressive edge caching')
161
+ .tags('Books')
162
+ .ok(BOOK_SCHEMA)
163
+ .error_response(404, T.hash({ "error" => T.string, "book_id" => T.string }))
164
+ .build
165
+ ) do |inputs|
166
+ book = @@books.find { |b| b[:id] == inputs[:id] }
167
+
168
+ if book
169
+ # Set cache headers for Vercel edge caching
170
+ cache_control 'public, max-age=300, s-maxage=3600' # 5min browser, 1hr edge
171
+ headers 'X-Vercel-Cache' => 'MISS' # Would be set by Vercel
172
+
173
+ book
174
+ else
175
+ halt 404, { error: 'Book not found', book_id: inputs[:id] }.to_json
176
+ end
177
+ end
178
+
179
+ # Fast book creation
180
+ endpoint(
181
+ POST('/books')
182
+ .summary('Create book (edge optimized)')
183
+ .description('Quickly create a new book with edge processing')
184
+ .body(T.hash({
185
+ "title" => T.string(min_length: 1, max_length: 255),
186
+ "author" => T.string(min_length: 1, max_length: 255),
187
+ "isbn" => T.optional(T.string(pattern: /^\d{10}(\d{3})?$/)),
188
+ "published_year" => T.optional(T.integer(minimum: 1000, maximum: 3000)),
189
+ "available" => T.optional(T.boolean),
190
+ "tags" => T.optional(T.array(T.string)),
191
+ "rating" => T.optional(T.float(minimum: 0, maximum: 5))
192
+ }))
193
+ .tags('Books')
194
+ .ok(BOOK_SCHEMA)
195
+ .error_response(400, T.hash({ "error" => T.string }))
196
+ .build
197
+ ) do |inputs|
198
+ book_data = inputs[:body]
199
+
200
+ # Generate edge-optimized ID
201
+ new_id = "vercel_#{Time.now.to_i}_#{rand(1000)}"
202
+
203
+ new_book = {
204
+ id: new_id,
205
+ title: book_data[:title] || book_data['title'],
206
+ author: book_data[:author] || book_data['author'],
207
+ isbn: book_data[:isbn] || book_data['isbn'],
208
+ published_year: book_data[:published_year] || book_data['published_year'],
209
+ available: book_data.key?(:available) ? book_data[:available] : (book_data.key?('available') ? book_data['available'] : true),
210
+ tags: book_data[:tags] || book_data['tags'] || [],
211
+ rating: book_data[:rating] || book_data['rating'],
212
+ created_at: Time.now,
213
+ updated_at: Time.now
214
+ }
215
+
216
+ @@books << new_book
217
+
218
+ # Set location header
219
+ headers 'Location' => "/books/#{new_id}"
220
+
221
+ status 201
222
+ new_book
223
+ end
224
+
225
+ # Vercel-specific analytics endpoint
226
+ endpoint(
227
+ GET('/vercel/analytics')
228
+ .summary('Vercel deployment analytics')
229
+ .description('Get Vercel-specific deployment and performance data')
230
+ .tags('Vercel', 'Analytics')
231
+ .ok(T.hash({
232
+ "deployment" => T.hash({
233
+ "id" => T.optional(T.string),
234
+ "url" => T.optional(T.string),
235
+ "environment" => T.optional(T.string),
236
+ "created_at" => T.optional(T.datetime)
237
+ }),
238
+ "git_info" => T.hash({
239
+ "branch" => T.optional(T.string),
240
+ "commit_sha" => T.optional(T.string),
241
+ "commit_message" => T.optional(T.string),
242
+ "repo_url" => T.optional(T.string)
243
+ }),
244
+ "edge_performance" => T.hash({
245
+ "region" => T.optional(T.string),
246
+ "cold_starts" => T.integer,
247
+ "avg_response_time" => T.float
248
+ })
249
+ }))
250
+ .build
251
+ ) do
252
+ {
253
+ deployment: {
254
+ id: ENV['VERCEL_DEPLOYMENT_ID'],
255
+ url: ENV['VERCEL_URL'],
256
+ environment: ENV['VERCEL_ENV'],
257
+ created_at: ENV['VERCEL_DEPLOYMENT_ID'] ? Time.now : nil # Mock timestamp
258
+ },
259
+ git_info: {
260
+ branch: ENV['VERCEL_GIT_COMMIT_REF'],
261
+ commit_sha: ENV['VERCEL_GIT_COMMIT_SHA'],
262
+ commit_message: ENV['VERCEL_GIT_COMMIT_MESSAGE'],
263
+ repo_url: ENV['VERCEL_GIT_REPO_SLUG'] ? "https://github.com/#{ENV['VERCEL_GIT_REPO_SLUG']}" : nil
264
+ },
265
+ edge_performance: {
266
+ region: ENV['VERCEL_REGION'],
267
+ cold_starts: 0, # Mock data
268
+ avg_response_time: 45.2 # Mock data
269
+ }
270
+ }
271
+ end
272
+
273
+ # Search with edge optimization
274
+ endpoint(
275
+ GET('/books/search')
276
+ .query(:q, T.string(min_length: 1), description: 'Search query')
277
+ .query(:fuzzy, T.optional(T.boolean), description: 'Enable fuzzy search')
278
+ .summary('Search books (edge optimized)')
279
+ .description('Fast search optimized for edge computing')
280
+ .tags('Books', 'Search')
281
+ .ok(T.hash({
282
+ "results" => T.array(BOOK_SCHEMA),
283
+ "query" => T.string,
284
+ "fuzzy_enabled" => T.boolean,
285
+ "search_time_ms" => T.float,
286
+ "edge_region" => T.optional(T.string)
287
+ }))
288
+ .build
289
+ ) do |inputs|
290
+ start_time = Time.now
291
+ query = inputs[:q].downcase
292
+ fuzzy = inputs[:fuzzy] || false
293
+
294
+ results = @@books.select do |book|
295
+ # Simple text search (could be enhanced with fuzzy matching)
296
+ [book[:title], book[:author], book[:tags]&.join(' ')].compact.any? do |field|
297
+ if fuzzy
298
+ # Simple fuzzy matching (could use more sophisticated algorithms)
299
+ field.downcase.include?(query) ||
300
+ query.chars.all? { |c| field.downcase.include?(c) }
301
+ else
302
+ field.downcase.include?(query)
303
+ end
304
+ end
305
+ end
306
+
307
+ search_time = ((Time.now - start_time) * 1000).round(2)
308
+
309
+ # Set edge cache headers
310
+ cache_control 'public, max-age=60, s-maxage=300' # 1min browser, 5min edge
311
+
312
+ {
313
+ results: results,
314
+ query: inputs[:q],
315
+ fuzzy_enabled: fuzzy,
316
+ search_time_ms: search_time,
317
+ edge_region: ENV['VERCEL_REGION']
318
+ }
319
+ end
320
+ end
321
+
322
+ # Vercel handler function
323
+ def handler(request:, response:)
324
+ # Mark as cold start on first execution
325
+ Thread.current[:cold_start] = !defined?(@@vercel_app_initialized)
326
+
327
+ # Initialize app (cached after first execution)
328
+ @@vercel_app ||= BookAPIVercel.new
329
+ @@vercel_app_initialized = true
330
+
331
+ # Convert Vercel request to Rack environment
332
+ rack_env = build_rack_env_from_vercel(request)
333
+
334
+ # Process request
335
+ status, headers, body = @@vercel_app.call(rack_env)
336
+
337
+ # Convert response for Vercel
338
+ body_content = ''
339
+ body.each { |part| body_content += part }
340
+
341
+ # Set Vercel response
342
+ response.status = status
343
+ headers.each { |key, value| response[key] = value }
344
+ response.write(body_content)
345
+
346
+ rescue => e
347
+ # Error handling for Vercel
348
+ response.status = 500
349
+ response['Content-Type'] = 'application/json'
350
+ response.write({
351
+ error: 'Internal server error',
352
+ message: e.message,
353
+ timestamp: Time.now.iso8601,
354
+ region: ENV['VERCEL_REGION']
355
+ }.to_json)
356
+ ensure
357
+ Thread.current[:cold_start] = nil
358
+ end
359
+
360
+ # Convert Vercel request to Rack environment
361
+ def build_rack_env_from_vercel(request)
362
+ method = request.method
363
+ url = request.url
364
+ uri = URI.parse(url)
365
+
366
+ # Get request body
367
+ body = request.body || ''
368
+
369
+ rack_env = {
370
+ 'REQUEST_METHOD' => method,
371
+ 'PATH_INFO' => uri.path,
372
+ 'QUERY_STRING' => uri.query || '',
373
+ 'CONTENT_TYPE' => request.headers['content-type'],
374
+ 'CONTENT_LENGTH' => body.bytesize.to_s,
375
+ 'rack.input' => StringIO.new(body),
376
+ 'rack.errors' => $stderr,
377
+ 'rack.version' => [1, 3],
378
+ 'rack.url_scheme' => uri.scheme || 'https',
379
+ 'rack.multithread' => false,
380
+ 'rack.multiprocess' => true,
381
+ 'rack.run_once' => true,
382
+ 'SERVER_NAME' => uri.host,
383
+ 'SERVER_PORT' => (uri.port || 443).to_s,
384
+ 'HTTP_HOST' => uri.host
385
+ }
386
+
387
+ # Add HTTP headers
388
+ request.headers.each do |key, value|
389
+ key = key.upcase.gsub('-', '_')
390
+ key = "HTTP_#{key}" unless %w[CONTENT_TYPE CONTENT_LENGTH].include?(key)
391
+ rack_env[key] = value
392
+ end
393
+
394
+ # Add Vercel-specific headers
395
+ rack_env['HTTP_X_VERCEL_DEPLOYMENT_ID'] = ENV['VERCEL_DEPLOYMENT_ID']
396
+ rack_env['HTTP_X_VERCEL_REGION'] = ENV['VERCEL_REGION']
397
+
398
+ rack_env
399
+ end
400
+
401
+ # For local development
402
+ if __FILE__ == $0
403
+ BookAPIVercel.run!
404
+ end
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # RapiTapir Strict Validation Examples
5
+ #
6
+ # This file demonstrates the strict validation behavior in RapiTapir v2.0
7
+ # By default, hash schemas now reject unexpected fields for better security.
8
+
9
+ require_relative '../lib/rapitapir'
10
+
11
+ puts "šŸ”’ RapiTapir v2.0 - Strict Validation by Default"
12
+ puts "=" * 55
13
+
14
+ # Define strict schema (default behavior)
15
+ STRICT_USER_SCHEMA = RapiTapir::Types.hash({
16
+ 'name' => RapiTapir::Types.string,
17
+ 'email' => RapiTapir::Types.email,
18
+ 'age' => RapiTapir::Types.integer
19
+ })
20
+
21
+ # Define open schema (allows additional properties)
22
+ OPEN_USER_SCHEMA = RapiTapir::Types.open_hash({
23
+ 'name' => RapiTapir::Types.string,
24
+ 'email' => RapiTapir::Types.email,
25
+ 'age' => RapiTapir::Types.integer
26
+ })
27
+
28
+ def test_validation(title, data, schema)
29
+ puts "\n#{title}"
30
+ puts "-" * 40
31
+ puts "šŸ“„ Input: #{data.inspect}"
32
+
33
+ begin
34
+ result = schema.coerce(data)
35
+ puts "āœ… Success: #{result.inspect}"
36
+ rescue RapiTapir::Types::CoercionError => e
37
+ puts "āŒ Error: #{e.message}"
38
+ if e.reason =~ /Unexpected fields/
39
+ puts "šŸ”’ Security: Rejecting unexpected data"
40
+ end
41
+ end
42
+ end
43
+
44
+ # Test data with extra field
45
+ test_data_with_extra = {
46
+ 'name' => 'John Doe',
47
+ 'email' => 'john@example.com',
48
+ 'age' => 25,
49
+ 'extra_field' => 'unexpected_value',
50
+ 'another_field' => 42
51
+ }
52
+
53
+ # Test data without extra fields
54
+ test_data_clean = {
55
+ 'name' => 'Jane Smith',
56
+ 'email' => 'jane@example.com',
57
+ 'age' => 30
58
+ }
59
+
60
+ puts "\nšŸ“‹ 1. STRICT VALIDATION (Default Behavior)"
61
+ puts " šŸ”’ Rejects unexpected fields for security"
62
+
63
+ test_validation(
64
+ "1ļøāƒ£ Strict Schema with Extra Fields",
65
+ test_data_with_extra,
66
+ STRICT_USER_SCHEMA
67
+ )
68
+
69
+ test_validation(
70
+ "2ļøāƒ£ Strict Schema with Clean Data",
71
+ test_data_clean,
72
+ STRICT_USER_SCHEMA
73
+ )
74
+
75
+ puts "\nšŸ“‹ 2. OPEN VALIDATION (Explicit opt-in)"
76
+ puts " 🌐 Allows additional properties when needed"
77
+
78
+ test_validation(
79
+ "3ļøāƒ£ Open Schema with Extra Fields",
80
+ test_data_with_extra,
81
+ OPEN_USER_SCHEMA
82
+ )
83
+
84
+ test_validation(
85
+ "4ļøāƒ£ Open Schema with Clean Data",
86
+ test_data_clean,
87
+ OPEN_USER_SCHEMA
88
+ )
89
+
90
+ puts "\nšŸŽ‰ Strict Validation Benefits:"
91
+ puts " āœ… Enhanced security by rejecting unexpected data"
92
+ puts " āœ… Clear error messages showing allowed fields"
93
+ puts " āœ… Prevents data leakage and injection attacks"
94
+ puts " āœ… Enforces API contract compliance"
95
+ puts " āœ… Explicit opt-in for flexible schemas when needed"
96
+
97
+ puts "\nšŸ’” Usage Guidelines:"
98
+ puts " • Use RapiTapir::Types.hash() for strict validation (default)"
99
+ puts " • Use RapiTapir::Types.open_hash() when you need flexibility"
100
+ puts " • Most production APIs should use strict validation"
101
+ puts " • Consider open validation only for specific use cases like:"
102
+ puts " - Webhook payloads with variable structures"
103
+ puts " - Configuration objects with user-defined fields"
104
+ puts " - Migration endpoints that need backward compatibility"