rapitapir 0.1.2 ā 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
- metadata +74 -2
@@ -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"
|