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
data/docs/endpoint-definition.md
CHANGED
@@ -1,211 +1,618 @@
|
|
1
|
-
# RapiTapir Endpoint
|
1
|
+
# RapiTapir Endpoint Definition Guide
|
2
2
|
|
3
|
-
This
|
3
|
+
This comprehensive guide covers the RapiTapir DSL for defining HTTP API endpoints with type safety, validation, and automatic documentation generation.
|
4
4
|
|
5
|
-
##
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
- [Overview](#overview)
|
8
|
+
- [HTTP Verb Methods](#http-verb-methods)
|
9
|
+
- [Input Definitions](#input-definitions)
|
10
|
+
- [Output Definitions](#output-definitions)
|
11
|
+
- [Metadata and Documentation](#metadata-and-documentation)
|
12
|
+
- [Advanced Features](#advanced-features)
|
13
|
+
- [Type System Integration](#type-system-integration)
|
14
|
+
- [Complete Examples](#complete-examples)
|
15
|
+
|
16
|
+
## Overview
|
17
|
+
|
18
|
+
RapiTapir provides a fluent, chainable DSL for defining HTTP endpoints. The modern DSL uses HTTP verb methods (`GET()`, `POST()`, etc.) combined with the global `T` shortcut for clean, readable endpoint definitions.
|
19
|
+
|
20
|
+
### Basic Structure
|
6
21
|
|
7
22
|
```ruby
|
8
|
-
|
23
|
+
endpoint(
|
24
|
+
HTTP_VERB('/path')
|
25
|
+
.input_definitions() # Query params, path params, headers, body
|
26
|
+
.output_definitions() # Success and error responses
|
27
|
+
.metadata() # Summary, description, tags, etc.
|
28
|
+
.build
|
29
|
+
) do |inputs|
|
30
|
+
# Your endpoint implementation
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
34
|
+
## HTTP Verb Methods
|
9
35
|
|
10
|
-
|
11
|
-
|
12
|
-
|
36
|
+
All standard HTTP methods are available as chainable builders:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
# All HTTP verbs supported
|
40
|
+
GET('/users') # Retrieve resources
|
41
|
+
POST('/users') # Create resources
|
42
|
+
PUT('/users/:id') # Update/replace resources
|
43
|
+
PATCH('/users/:id') # Partial updates
|
44
|
+
DELETE('/users/:id') # Delete resources
|
45
|
+
OPTIONS('/users') # CORS preflight
|
46
|
+
HEAD('/users') # Headers only
|
13
47
|
```
|
14
48
|
|
15
|
-
|
49
|
+
### Path Parameters
|
16
50
|
|
17
|
-
|
51
|
+
Define path parameters directly in the URL pattern:
|
18
52
|
|
19
53
|
```ruby
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
RapiTapir.patch('/users/:id') # PATCH endpoint
|
24
|
-
RapiTapir.delete('/users/:id') # DELETE endpoint
|
25
|
-
RapiTapir.options('/users') # OPTIONS endpoint
|
26
|
-
RapiTapir.head('/users') # HEAD endpoint
|
54
|
+
GET('/users/:id') # Single parameter
|
55
|
+
GET('/users/:id/posts/:post_id') # Multiple parameters
|
56
|
+
GET('/files/*path') # Splat parameters
|
27
57
|
```
|
28
58
|
|
29
|
-
## Input
|
59
|
+
## Input Definitions
|
30
60
|
|
31
61
|
### Query Parameters
|
62
|
+
|
32
63
|
```ruby
|
33
|
-
|
34
|
-
.
|
35
|
-
.
|
64
|
+
GET('/search')
|
65
|
+
.query(:q, T.string(min_length: 1), description: 'Search query')
|
66
|
+
.query(:limit, T.optional(T.integer(minimum: 1, maximum: 100)), description: 'Results limit')
|
67
|
+
.query(:sort, T.optional(T.string(enum: %w[name date relevance])), description: 'Sort order')
|
68
|
+
.query(:tags, T.optional(T.array(T.string)), description: 'Filter tags')
|
36
69
|
```
|
37
70
|
|
38
71
|
### Path Parameters
|
72
|
+
|
39
73
|
```ruby
|
40
|
-
|
41
|
-
.
|
74
|
+
GET('/users/:id')
|
75
|
+
.path_param(:id, T.integer(minimum: 1), description: 'User ID')
|
76
|
+
|
77
|
+
GET('/files/:category/:filename')
|
78
|
+
.path_param(:category, T.string(enum: %w[images documents videos]), description: 'File category')
|
79
|
+
.path_param(:filename, T.string(pattern: /^[\w\-_.]+$/), description: 'Filename')
|
42
80
|
```
|
43
81
|
|
44
|
-
### Headers
|
82
|
+
### Request Headers
|
83
|
+
|
45
84
|
```ruby
|
46
|
-
|
47
|
-
.
|
85
|
+
POST('/upload')
|
86
|
+
.header('Content-Type', T.string, description: 'MIME type of uploaded content')
|
87
|
+
.header('X-Request-ID', T.optional(T.string), description: 'Request tracking ID')
|
88
|
+
.header('Authorization', T.string, description: 'Bearer token')
|
48
89
|
```
|
49
90
|
|
50
91
|
### Request Body
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
# JSON body with schema
|
95
|
+
POST('/users')
|
96
|
+
.body(T.hash({
|
97
|
+
"name" => T.string(min_length: 1, max_length: 100),
|
98
|
+
"email" => T.email,
|
99
|
+
"age" => T.optional(T.integer(minimum: 18, maximum: 120)),
|
100
|
+
"preferences" => T.optional(T.hash({
|
101
|
+
"theme" => T.string(enum: %w[light dark]),
|
102
|
+
"notifications" => T.boolean
|
103
|
+
}))
|
104
|
+
}), description: 'User data to create')
|
105
|
+
|
106
|
+
# File upload
|
107
|
+
POST('/files')
|
108
|
+
.body(T.string, content_type: 'multipart/form-data', description: 'File content')
|
109
|
+
|
110
|
+
# Raw binary data
|
111
|
+
POST('/images')
|
112
|
+
.body(T.string, content_type: 'image/jpeg', description: 'JPEG image data')
|
113
|
+
```
|
114
|
+
|
115
|
+
## Output Definitions
|
116
|
+
|
117
|
+
### Success Responses
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
GET('/users')
|
121
|
+
.ok(T.array(USER_SCHEMA), description: 'List of users')
|
122
|
+
|
123
|
+
POST('/users')
|
124
|
+
.created(USER_SCHEMA, description: 'Created user')
|
125
|
+
|
126
|
+
PUT('/users/:id')
|
127
|
+
.ok(USER_SCHEMA, description: 'Updated user')
|
128
|
+
|
129
|
+
DELETE('/users/:id')
|
130
|
+
.no_content(description: 'User deleted successfully')
|
131
|
+
```
|
132
|
+
|
133
|
+
### Error Responses
|
134
|
+
|
51
135
|
```ruby
|
52
|
-
|
53
|
-
.
|
136
|
+
GET('/users/:id')
|
137
|
+
.ok(USER_SCHEMA)
|
138
|
+
.error_response(404, T.hash({
|
139
|
+
"error" => T.string,
|
140
|
+
"user_id" => T.integer
|
141
|
+
}), description: 'User not found')
|
142
|
+
.error_response(500, T.hash({
|
143
|
+
"error" => T.string,
|
144
|
+
"trace_id" => T.string
|
145
|
+
}), description: 'Internal server error')
|
146
|
+
|
147
|
+
POST('/users')
|
148
|
+
.created(USER_SCHEMA)
|
149
|
+
.error_response(400, T.hash({
|
150
|
+
"error" => T.string,
|
151
|
+
"validation_errors" => T.array(T.hash({
|
152
|
+
"field" => T.string,
|
153
|
+
"message" => T.string,
|
154
|
+
"code" => T.string
|
155
|
+
}))
|
156
|
+
}), description: 'Validation failed')
|
157
|
+
.error_response(409, T.hash({
|
158
|
+
"error" => T.string,
|
159
|
+
"existing_user_id" => T.integer
|
160
|
+
}), description: 'User already exists')
|
54
161
|
```
|
55
162
|
|
56
|
-
|
163
|
+
### Multiple Response Types
|
57
164
|
|
58
|
-
### JSON Response
|
59
165
|
```ruby
|
60
|
-
|
61
|
-
.
|
166
|
+
GET('/content/:id')
|
167
|
+
.path_param(:id, T.integer)
|
168
|
+
.query(:format, T.optional(T.string(enum: %w[json xml])), description: 'Response format')
|
169
|
+
.ok(CONTENT_SCHEMA, content_type: 'application/json', description: 'JSON response')
|
170
|
+
.ok(T.string, content_type: 'application/xml', description: 'XML response')
|
171
|
+
.error_response(404, ERROR_SCHEMA)
|
62
172
|
```
|
63
173
|
|
64
|
-
|
174
|
+
## Metadata and Documentation
|
175
|
+
|
176
|
+
### Basic Metadata
|
177
|
+
|
65
178
|
```ruby
|
66
|
-
|
179
|
+
GET('/users')
|
180
|
+
.summary('List all users')
|
181
|
+
.description('Retrieves a paginated list of all users in the system with optional filtering')
|
182
|
+
.tags('Users', 'CRUD')
|
183
|
+
.deprecated(false)
|
67
184
|
```
|
68
185
|
|
69
|
-
###
|
186
|
+
### Rich Documentation
|
187
|
+
|
70
188
|
```ruby
|
71
|
-
|
72
|
-
.
|
73
|
-
.
|
189
|
+
POST('/users')
|
190
|
+
.summary('Create a new user')
|
191
|
+
.description('''
|
192
|
+
Creates a new user account with the provided information.
|
193
|
+
|
194
|
+
The email address must be unique across the system.
|
195
|
+
Password requirements:
|
196
|
+
- Minimum 8 characters
|
197
|
+
- At least one uppercase letter
|
198
|
+
- At least one number
|
199
|
+
- At least one special character
|
200
|
+
''')
|
201
|
+
.tags('Users', 'Registration')
|
202
|
+
.external_docs(
|
203
|
+
description: 'User Management Guide',
|
204
|
+
url: 'https://docs.example.com/users'
|
205
|
+
)
|
74
206
|
```
|
75
207
|
|
76
|
-
###
|
208
|
+
### OpenAPI Extensions
|
209
|
+
|
77
210
|
```ruby
|
78
|
-
|
79
|
-
.
|
211
|
+
GET('/users')
|
212
|
+
.openapi_extensions({
|
213
|
+
'x-rate-limit' => '100/minute',
|
214
|
+
'x-permissions' => ['users:read'],
|
215
|
+
'x-cache-ttl' => 300
|
216
|
+
})
|
80
217
|
```
|
81
218
|
|
82
|
-
##
|
219
|
+
## Advanced Features
|
220
|
+
|
221
|
+
### Authentication Requirements
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
GET('/profile')
|
225
|
+
.bearer_auth(scopes: ['profile:read'])
|
226
|
+
.ok(USER_SCHEMA)
|
227
|
+
|
228
|
+
DELETE('/admin/users/:id')
|
229
|
+
.bearer_auth(scopes: ['admin', 'users:delete'])
|
230
|
+
.no_content()
|
231
|
+
```
|
232
|
+
|
233
|
+
### AI Integration
|
83
234
|
|
84
|
-
### Documentation
|
85
235
|
```ruby
|
86
|
-
|
87
|
-
.
|
88
|
-
.
|
236
|
+
GET('/search/semantic')
|
237
|
+
.query(:query, T.string, description: 'Natural language search query')
|
238
|
+
.enable_rag(
|
239
|
+
retrieval_backend: :memory,
|
240
|
+
llm_provider: :openai,
|
241
|
+
context_window: 4000
|
242
|
+
)
|
243
|
+
.enable_mcp # Export for AI agents
|
244
|
+
.enable_llm_instructions(purpose: :completion)
|
245
|
+
.ok(T.hash({
|
246
|
+
"results" => T.array(SEARCH_RESULT_SCHEMA),
|
247
|
+
"reasoning" => T.string,
|
248
|
+
"confidence" => T.float
|
249
|
+
}))
|
89
250
|
```
|
90
251
|
|
91
|
-
###
|
252
|
+
### Observability
|
253
|
+
|
92
254
|
```ruby
|
93
|
-
|
94
|
-
.
|
255
|
+
GET('/users/:id')
|
256
|
+
.path_param(:id, T.integer)
|
257
|
+
.with_metrics('user_requests', labels: { operation: 'get_by_id' })
|
258
|
+
.with_tracing('fetch_user')
|
259
|
+
.ok(USER_SCHEMA)
|
95
260
|
```
|
96
261
|
|
97
|
-
###
|
262
|
+
### Caching
|
263
|
+
|
98
264
|
```ruby
|
99
|
-
|
265
|
+
GET('/users/:id')
|
266
|
+
.cache(ttl: 300, vary: ['Authorization'])
|
267
|
+
.ok(USER_SCHEMA)
|
100
268
|
```
|
101
269
|
|
102
|
-
##
|
270
|
+
## Type System Integration
|
103
271
|
|
104
|
-
|
105
|
-
- `:integer` - Integer values
|
106
|
-
- `:float` - Float values (accepts integers too)
|
107
|
-
- `:boolean` - Boolean values (true/false)
|
108
|
-
- `:date` - Date objects or ISO date strings
|
109
|
-
- `:datetime` - DateTime objects or ISO datetime strings
|
110
|
-
- `Hash` - Hash schemas with typed keys
|
111
|
-
- `Class` - Custom class types
|
272
|
+
### Using the T Shortcut
|
112
273
|
|
113
|
-
|
274
|
+
The global `T` constant provides clean access to all RapiTapir types:
|
114
275
|
|
115
276
|
```ruby
|
116
|
-
|
277
|
+
# Primitive types
|
278
|
+
T.string # String type
|
279
|
+
T.integer # Integer type
|
280
|
+
T.float # Float type
|
281
|
+
T.boolean # Boolean type
|
282
|
+
T.date # Date type
|
283
|
+
T.datetime # DateTime type
|
284
|
+
|
285
|
+
# Constrained types
|
286
|
+
T.string(min_length: 1, max_length: 100)
|
287
|
+
T.integer(minimum: 0, maximum: 999)
|
288
|
+
T.float(minimum: 0.0)
|
289
|
+
|
290
|
+
# Special types
|
291
|
+
T.email # Email validation
|
292
|
+
T.uuid # UUID format
|
293
|
+
T.url # URL format
|
294
|
+
|
295
|
+
# Collections
|
296
|
+
T.array(T.string) # Array of strings
|
297
|
+
T.hash({ "key" => T.value }) # Object schema
|
298
|
+
|
299
|
+
# Optional types
|
300
|
+
T.optional(T.string) # Nullable string
|
301
|
+
```
|
117
302
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
age: :integer,
|
126
|
-
active: :boolean
|
127
|
-
}))
|
128
|
-
.out(status_code(201))
|
129
|
-
.out(json_body({
|
130
|
-
id: :integer,
|
131
|
-
name: :string,
|
132
|
-
email: :string,
|
133
|
-
created_at: :datetime
|
134
|
-
}))
|
135
|
-
.error_out(400, json_body({ error: :string, details: :string }))
|
136
|
-
.error_out(422, json_body({
|
137
|
-
error: :string,
|
138
|
-
validation_errors: [{ field: :string, message: :string }]
|
139
|
-
}))
|
140
|
-
.description('Create a new user account')
|
141
|
-
.summary('Create user')
|
142
|
-
.tag('users')
|
143
|
-
.example({ name: 'John Doe', email: 'john@example.com', age: 30, active: true })
|
144
|
-
|
145
|
-
# Validate inputs and outputs
|
146
|
-
input_data = {
|
147
|
-
body: { name: 'John', email: 'john@example.com', age: 30, active: true }
|
148
|
-
}
|
149
|
-
output_data = {
|
303
|
+
### Auto-Derivation
|
304
|
+
|
305
|
+
Generate schemas from existing data:
|
306
|
+
|
307
|
+
```ruby
|
308
|
+
# From hash
|
309
|
+
USER_SCHEMA = T.from_hash({
|
150
310
|
id: 1,
|
151
|
-
name:
|
152
|
-
|
153
|
-
|
154
|
-
}
|
311
|
+
name: "John Doe",
|
312
|
+
active: true,
|
313
|
+
tags: ["admin"]
|
314
|
+
})
|
155
315
|
|
156
|
-
|
316
|
+
# From JSON Schema
|
317
|
+
API_SCHEMA = T.from_json_schema(json_schema_object)
|
157
318
|
|
158
|
-
#
|
159
|
-
|
160
|
-
puts user_endpoint.to_h # Complete hash representation
|
319
|
+
# From OpenStruct
|
320
|
+
CONFIG_SCHEMA = T.from_open_struct(config_object)
|
161
321
|
```
|
162
322
|
|
163
|
-
|
323
|
+
### Schema Composition
|
164
324
|
|
165
|
-
|
325
|
+
```ruby
|
326
|
+
# Base schemas
|
327
|
+
ADDRESS_SCHEMA = T.hash({
|
328
|
+
"street" => T.string,
|
329
|
+
"city" => T.string,
|
330
|
+
"country" => T.string,
|
331
|
+
"postal_code" => T.string
|
332
|
+
})
|
333
|
+
|
334
|
+
# Composed schemas
|
335
|
+
USER_SCHEMA = T.hash({
|
336
|
+
"id" => T.integer,
|
337
|
+
"name" => T.string,
|
338
|
+
"email" => T.email,
|
339
|
+
"address" => T.optional(ADDRESS_SCHEMA),
|
340
|
+
"billing_address" => T.optional(ADDRESS_SCHEMA)
|
341
|
+
})
|
342
|
+
```
|
343
|
+
|
344
|
+
## Complete Examples
|
345
|
+
|
346
|
+
### Simple CRUD Endpoint
|
166
347
|
|
167
348
|
```ruby
|
168
|
-
|
169
|
-
.
|
170
|
-
|
349
|
+
class UserAPI < SinatraRapiTapir
|
350
|
+
USER_SCHEMA = T.hash({
|
351
|
+
"id" => T.integer,
|
352
|
+
"name" => T.string(min_length: 1, max_length: 100),
|
353
|
+
"email" => T.email,
|
354
|
+
"active" => T.boolean,
|
355
|
+
"created_at" => T.datetime
|
356
|
+
})
|
357
|
+
|
358
|
+
# List users
|
359
|
+
endpoint(
|
360
|
+
GET('/users')
|
361
|
+
.summary('List users')
|
362
|
+
.query(:active, T.optional(T.boolean), description: 'Filter by active status')
|
363
|
+
.query(:limit, T.optional(T.integer(minimum: 1, maximum: 100)), description: 'Page size')
|
364
|
+
.query(:offset, T.optional(T.integer(minimum: 0)), description: 'Page offset')
|
365
|
+
.tags('Users')
|
366
|
+
.ok(T.hash({
|
367
|
+
"users" => T.array(USER_SCHEMA),
|
368
|
+
"total" => T.integer,
|
369
|
+
"limit" => T.integer,
|
370
|
+
"offset" => T.integer
|
371
|
+
}))
|
372
|
+
.build
|
373
|
+
) do |inputs|
|
374
|
+
users = User.all
|
375
|
+
users = users.where(active: inputs[:active]) if inputs[:active]
|
376
|
+
|
377
|
+
limit = inputs[:limit] || 50
|
378
|
+
offset = inputs[:offset] || 0
|
379
|
+
|
380
|
+
paginated_users = users.limit(limit).offset(offset)
|
381
|
+
|
382
|
+
{
|
383
|
+
users: paginated_users.map(&:to_h),
|
384
|
+
total: users.count,
|
385
|
+
limit: limit,
|
386
|
+
offset: offset
|
387
|
+
}
|
388
|
+
end
|
389
|
+
|
390
|
+
# Get user by ID
|
391
|
+
endpoint(
|
392
|
+
GET('/users/:id')
|
393
|
+
.summary('Get user by ID')
|
394
|
+
.path_param(:id, T.integer(minimum: 1), description: 'User ID')
|
395
|
+
.tags('Users')
|
396
|
+
.ok(USER_SCHEMA, description: 'User details')
|
397
|
+
.error_response(404, T.hash({
|
398
|
+
"error" => T.string,
|
399
|
+
"user_id" => T.integer
|
400
|
+
}), description: 'User not found')
|
401
|
+
.build
|
402
|
+
) do |inputs|
|
403
|
+
user = User.find(inputs[:id])
|
404
|
+
halt 404, { error: 'User not found', user_id: inputs[:id] }.to_json unless user
|
405
|
+
user.to_h
|
406
|
+
end
|
407
|
+
|
408
|
+
# Create user
|
409
|
+
endpoint(
|
410
|
+
POST('/users')
|
411
|
+
.summary('Create a new user')
|
412
|
+
.body(T.hash({
|
413
|
+
"name" => T.string(min_length: 1, max_length: 100),
|
414
|
+
"email" => T.email,
|
415
|
+
"active" => T.optional(T.boolean)
|
416
|
+
}), description: 'User data')
|
417
|
+
.tags('Users')
|
418
|
+
.created(USER_SCHEMA, description: 'Created user')
|
419
|
+
.error_response(400, T.hash({
|
420
|
+
"error" => T.string,
|
421
|
+
"validation_errors" => T.array(T.string)
|
422
|
+
}), description: 'Validation failed')
|
423
|
+
.error_response(409, T.hash({
|
424
|
+
"error" => T.string,
|
425
|
+
"existing_email" => T.string
|
426
|
+
}), description: 'Email already exists')
|
427
|
+
.build
|
428
|
+
) do |inputs|
|
429
|
+
begin
|
430
|
+
user = User.create!(inputs[:body])
|
431
|
+
status 201
|
432
|
+
user.to_h
|
433
|
+
rescue ValidationError => e
|
434
|
+
halt 400, {
|
435
|
+
error: 'Validation failed',
|
436
|
+
validation_errors: e.messages
|
437
|
+
}.to_json
|
438
|
+
rescue UniqueConstraintError => e
|
439
|
+
halt 409, {
|
440
|
+
error: 'Email already exists',
|
441
|
+
existing_email: inputs[:body]['email']
|
442
|
+
}.to_json
|
443
|
+
end
|
444
|
+
end
|
445
|
+
end
|
446
|
+
```
|
171
447
|
|
172
|
-
|
173
|
-
endpoint.validate!({ id: 123 }, { name: 'John' })
|
448
|
+
### Advanced API with Authentication and AI
|
174
449
|
|
175
|
-
|
176
|
-
|
177
|
-
|
450
|
+
```ruby
|
451
|
+
class AdvancedAPI < SinatraRapiTapir
|
452
|
+
rapitapir do
|
453
|
+
info(title: 'Advanced API', version: '2.0.0')
|
454
|
+
|
455
|
+
oauth2_auth0 :auth0,
|
456
|
+
domain: ENV['AUTH0_DOMAIN'],
|
457
|
+
audience: ENV['AUTH0_AUDIENCE']
|
458
|
+
|
459
|
+
production_defaults!
|
460
|
+
end
|
461
|
+
|
462
|
+
# AI-powered search endpoint
|
463
|
+
endpoint(
|
464
|
+
GET('/search/intelligent')
|
465
|
+
.summary('AI-powered intelligent search')
|
466
|
+
.description('Uses machine learning to understand natural language queries and return relevant results')
|
467
|
+
.query(:q, T.string(min_length: 1), description: 'Natural language search query')
|
468
|
+
.query(:limit, T.optional(T.integer(minimum: 1, maximum: 50)), description: 'Maximum results')
|
469
|
+
.query(:include_reasoning, T.optional(T.boolean), description: 'Include AI reasoning in response')
|
470
|
+
.bearer_auth(scopes: ['search:enhanced'])
|
471
|
+
.tags('Search', 'AI')
|
472
|
+
.ok(T.hash({
|
473
|
+
"results" => T.array(T.hash({
|
474
|
+
"id" => T.string,
|
475
|
+
"title" => T.string,
|
476
|
+
"content" => T.string,
|
477
|
+
"relevance_score" => T.float(minimum: 0, maximum: 1),
|
478
|
+
"metadata" => T.hash({})
|
479
|
+
})),
|
480
|
+
"query_analysis" => T.hash({
|
481
|
+
"intent" => T.string,
|
482
|
+
"entities" => T.array(T.string),
|
483
|
+
"confidence" => T.float(minimum: 0, maximum: 1)
|
484
|
+
}),
|
485
|
+
"reasoning" => T.optional(T.string),
|
486
|
+
"total_results" => T.integer,
|
487
|
+
"processing_time_ms" => T.float
|
488
|
+
}))
|
489
|
+
.error_response(400, T.hash({
|
490
|
+
"error" => T.string,
|
491
|
+
"suggestion" => T.string
|
492
|
+
}), description: 'Invalid query')
|
493
|
+
.error_response(401, T.hash({ "error" => T.string }), description: 'Authentication required')
|
494
|
+
.error_response(403, T.hash({ "error" => T.string }), description: 'Insufficient permissions')
|
495
|
+
.enable_rag(
|
496
|
+
retrieval_backend: :elasticsearch,
|
497
|
+
llm_provider: :openai,
|
498
|
+
context_window: 8000
|
499
|
+
)
|
500
|
+
.enable_llm_instructions(purpose: :analysis)
|
501
|
+
.with_metrics('ai_search_requests', labels: { model: 'gpt-4' })
|
502
|
+
.with_tracing('intelligent_search')
|
503
|
+
.build
|
504
|
+
) do |inputs|
|
505
|
+
start_time = Time.now
|
506
|
+
|
507
|
+
begin
|
508
|
+
# Verify enhanced search permissions
|
509
|
+
require_scope!('search:enhanced')
|
510
|
+
|
511
|
+
# Perform AI-enhanced search
|
512
|
+
search_results = AISearchService.intelligent_search(
|
513
|
+
query: inputs[:q],
|
514
|
+
limit: inputs[:limit] || 20,
|
515
|
+
user_context: current_auth_context[:user_info],
|
516
|
+
rag_context: rag_context
|
517
|
+
)
|
518
|
+
|
519
|
+
response = {
|
520
|
+
results: search_results[:items],
|
521
|
+
query_analysis: search_results[:analysis],
|
522
|
+
total_results: search_results[:total],
|
523
|
+
processing_time_ms: ((Time.now - start_time) * 1000).round(2)
|
524
|
+
}
|
525
|
+
|
526
|
+
# Include reasoning if requested
|
527
|
+
if inputs[:include_reasoning]
|
528
|
+
response[:reasoning] = search_results[:reasoning]
|
529
|
+
end
|
530
|
+
|
531
|
+
response
|
532
|
+
|
533
|
+
rescue InvalidQueryError => e
|
534
|
+
halt 400, {
|
535
|
+
error: e.message,
|
536
|
+
suggestion: e.suggestion
|
537
|
+
}.to_json
|
538
|
+
end
|
539
|
+
end
|
540
|
+
end
|
178
541
|
```
|
179
542
|
|
180
|
-
##
|
543
|
+
## Best Practices
|
544
|
+
|
545
|
+
### 1. Schema Organization
|
546
|
+
```ruby
|
547
|
+
# Define schemas as constants for reuse
|
548
|
+
USER_SCHEMA = T.hash({
|
549
|
+
"id" => T.integer,
|
550
|
+
"name" => T.string,
|
551
|
+
"email" => T.email
|
552
|
+
})
|
553
|
+
|
554
|
+
# Use schema composition
|
555
|
+
FULL_USER_SCHEMA = T.hash({
|
556
|
+
**USER_SCHEMA.definition,
|
557
|
+
"profile" => T.optional(PROFILE_SCHEMA),
|
558
|
+
"preferences" => T.optional(PREFERENCES_SCHEMA)
|
559
|
+
})
|
560
|
+
```
|
181
561
|
|
182
|
-
|
562
|
+
### 2. Error Handling
|
563
|
+
```ruby
|
564
|
+
# Define consistent error schemas
|
565
|
+
ERROR_SCHEMA = T.hash({
|
566
|
+
"error" => T.string,
|
567
|
+
"code" => T.string,
|
568
|
+
"details" => T.optional(T.hash({}))
|
569
|
+
})
|
570
|
+
|
571
|
+
# Use specific error responses
|
572
|
+
.error_response(400, VALIDATION_ERROR_SCHEMA, description: 'Validation failed')
|
573
|
+
.error_response(404, NOT_FOUND_ERROR_SCHEMA, description: 'Resource not found')
|
574
|
+
.error_response(500, INTERNAL_ERROR_SCHEMA, description: 'Internal server error')
|
575
|
+
```
|
183
576
|
|
577
|
+
### 3. Documentation
|
184
578
|
```ruby
|
185
|
-
|
186
|
-
|
187
|
-
|
579
|
+
# Always provide clear summaries and descriptions
|
580
|
+
.summary('Create user account')
|
581
|
+
.description('Creates a new user account with email verification')
|
582
|
+
|
583
|
+
# Use meaningful parameter descriptions
|
584
|
+
.query(:filter, T.optional(T.string), description: 'Filter users by name or email')
|
188
585
|
|
189
|
-
#
|
190
|
-
|
191
|
-
puts with_auth.inputs.length # 1
|
192
|
-
puts with_output.inputs.length # 1
|
193
|
-
puts with_output.outputs.length # 1
|
586
|
+
# Tag endpoints for logical grouping
|
587
|
+
.tags('Users', 'Account Management')
|
194
588
|
```
|
195
589
|
|
196
|
-
|
590
|
+
### 4. Type Safety
|
591
|
+
```ruby
|
592
|
+
# Use specific constraints
|
593
|
+
T.string(min_length: 1, max_length: 255)
|
594
|
+
T.integer(minimum: 1)
|
595
|
+
T.array(T.string, min_items: 1)
|
596
|
+
|
597
|
+
# Prefer enums for known values
|
598
|
+
T.string(enum: %w[active inactive pending])
|
197
599
|
|
198
|
-
|
600
|
+
# Use optional for nullable fields
|
601
|
+
T.optional(T.string)
|
602
|
+
```
|
199
603
|
|
604
|
+
### 5. Performance
|
200
605
|
```ruby
|
201
|
-
#
|
202
|
-
|
203
|
-
|
606
|
+
# Add caching for expensive operations
|
607
|
+
.cache(ttl: 300, vary: ['Authorization'])
|
608
|
+
|
609
|
+
# Use metrics for monitoring
|
610
|
+
.with_metrics('endpoint_requests', labels: { operation: 'list' })
|
204
611
|
|
205
|
-
#
|
206
|
-
|
612
|
+
# Add tracing for debugging
|
613
|
+
.with_tracing('database_query')
|
207
614
|
```
|
208
615
|
|
209
616
|
---
|
210
617
|
|
211
|
-
|
618
|
+
This guide covers the comprehensive RapiTapir endpoint definition DSL. For more examples, see the [examples directory](../examples/) and the [Sinatra extension guide](SINATRA_EXTENSION.md).
|