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
@@ -1,211 +1,618 @@
1
- # RapiTapir Endpoint DSL Reference
1
+ # RapiTapir Endpoint Definition Guide
2
2
 
3
- This document describes the RapiTapir DSL for defining HTTP API endpoints in Ruby with type safety and validation.
3
+ This comprehensive guide covers the RapiTapir DSL for defining HTTP API endpoints with type safety, validation, and automatic documentation generation.
4
4
 
5
- ## Basic Usage
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
- require 'rapitapir'
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
- endpoint = RapiTapir.get('/hello')
11
- .in(RapiTapir.query(:name, :string))
12
- .out(RapiTapir.json_body(message: :string))
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
- ## HTTP Methods
49
+ ### Path Parameters
16
50
 
17
- RapiTapir supports all standard HTTP methods:
51
+ Define path parameters directly in the URL pattern:
18
52
 
19
53
  ```ruby
20
- RapiTapir.get('/users') # GET endpoint
21
- RapiTapir.post('/users') # POST endpoint
22
- RapiTapir.put('/users/:id') # PUT endpoint
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 DSL Helpers
59
+ ## Input Definitions
30
60
 
31
61
  ### Query Parameters
62
+
32
63
  ```ruby
33
- .in(query(:name, :string))
34
- .in(query(:age, :integer, optional: true))
35
- .in(query(:active, :boolean))
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
- .in(path_param(:id, :integer))
41
- .in(path_param(:slug, :string))
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
- .in(header(:authorization, :string))
47
- .in(header(:'content-type', :string))
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
- .in(body({ name: :string, email: :string }))
53
- .in(body(User)) # Custom class
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
- ## Output DSL Helpers
163
+ ### Multiple Response Types
57
164
 
58
- ### JSON Response
59
165
  ```ruby
60
- .out(json_body({ id: :integer, name: :string }))
61
- .out(json_body([{ id: :integer, name: :string }])) # Array
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
- ### XML Response
174
+ ## Metadata and Documentation
175
+
176
+ ### Basic Metadata
177
+
65
178
  ```ruby
66
- .out(xml_body({ message: :string }))
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
- ### Status Codes
186
+ ### Rich Documentation
187
+
70
188
  ```ruby
71
- .out(status_code(200))
72
- .out(status_code(201))
73
- .out(status_code(204))
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
- ### Error Responses
208
+ ### OpenAPI Extensions
209
+
77
210
  ```ruby
78
- .error_out(404, json_body({ error: :string }))
79
- .error_out(422, json_body({ error: :string, details: :string }))
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
- ## Metadata Helpers
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
- .description('Retrieve user by ID')
87
- .summary('Get user')
88
- .tag('users')
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
- ### Lifecycle
252
+ ### Observability
253
+
92
254
  ```ruby
93
- .deprecated(true) # Mark as deprecated
94
- .deprecated(false) # Not deprecated
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
- ### Examples
262
+ ### Caching
263
+
98
264
  ```ruby
99
- .example({ name: 'John', email: 'john@example.com' })
265
+ GET('/users/:id')
266
+ .cache(ttl: 300, vary: ['Authorization'])
267
+ .ok(USER_SCHEMA)
100
268
  ```
101
269
 
102
- ## Supported Types
270
+ ## Type System Integration
103
271
 
104
- - `:string` - String values
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
- ## Complete Example
274
+ The global `T` constant provides clean access to all RapiTapir types:
114
275
 
115
276
  ```ruby
116
- require 'rapitapir'
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
- # Define a complete CRUD endpoint
119
- user_endpoint = RapiTapir.post('/users')
120
- .in(header(:authorization, :string))
121
- .in(header(:'content-type', :string))
122
- .in(body({
123
- name: :string,
124
- email: :string,
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: 'John',
152
- email: 'john@example.com',
153
- created_at: DateTime.now
154
- }
311
+ name: "John Doe",
312
+ active: true,
313
+ tags: ["admin"]
314
+ })
155
315
 
156
- user_endpoint.validate!(input_data, output_data) # Returns true or raises TypeError
316
+ # From JSON Schema
317
+ API_SCHEMA = T.from_json_schema(json_schema_object)
157
318
 
158
- # Get endpoint metadata
159
- puts user_endpoint.metadata[:description] # "Create a new user account"
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
- ## Type Validation
323
+ ### Schema Composition
164
324
 
165
- RapiTapir performs runtime type validation:
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
- endpoint = RapiTapir.get('/users/:id')
169
- .in(path_param(:id, :integer))
170
- .out(json_body({ name: :string }))
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
- # This will pass
173
- endpoint.validate!({ id: 123 }, { name: 'John' })
448
+ ### Advanced API with Authentication and AI
174
449
 
175
- # This will raise TypeError
176
- endpoint.validate!({ id: 'abc' }, { name: 'John' })
177
- endpoint.validate!({ id: 123 }, { name: 123 })
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
- ## Immutability
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
- All endpoint operations return new endpoint instances, preserving immutability:
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
- base = RapiTapir.get('/users')
186
- with_auth = base.in(header(:authorization, :string))
187
- with_output = with_auth.out(json_body({ users: [:string] }))
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
- # base, with_auth, and with_output are all different objects
190
- puts base.inputs.length # 0
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
- ## Error Handling
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
- The DSL provides comprehensive error handling with detailed messages:
600
+ # Use optional for nullable fields
601
+ T.optional(T.string)
602
+ ```
199
603
 
604
+ ### 5. Performance
200
605
  ```ruby
201
- # ArgumentError for invalid parameters
202
- query(nil, :string) # "Input name cannot be nil"
203
- status_code(999) # "Invalid status code: 999"
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
- # TypeError for validation failures
206
- endpoint.validate!({ name: 123 }, {}) # "Invalid type for input 'name': expected string, got Integer"
612
+ # Add tracing for debugging
613
+ .with_tracing('database_query')
207
614
  ```
208
615
 
209
616
  ---
210
617
 
211
- For more examples and advanced usage, see the files in the `examples/` directory and the implementation plan in `docs/blueprint.md`.
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).