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
data/docs/using-mcp.md
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
# Using RapiTapir APIs as LLM Tools (MCP)
|
2
|
+
|
3
|
+
RapiTapir supports the Model Context Protocol (MCP) to make your API endpoints discoverable and consumable by LLMs and AI agents.
|
4
|
+
|
5
|
+
### How to Mark Endpoints for MCP Export
|
6
|
+
|
7
|
+
Use the `.mcp_export` method in your endpoint DSL chain:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
endpoint(
|
11
|
+
GET('/hello')
|
12
|
+
.query(:name, T.string, description: 'Name to greet')
|
13
|
+
.ok(T.hash({ "message" => T.string }))
|
14
|
+
.enable_mcp # Mark for MCP export
|
15
|
+
.build
|
16
|
+
) do |inputs|
|
17
|
+
{ message: "Hello, #{inputs[:name]}!" }
|
18
|
+
end
|
19
|
+
```
|
20
|
+
|
21
|
+
### CLI Usage
|
22
|
+
|
23
|
+
Generate MCP context using the command line:
|
24
|
+
|
25
|
+
```bash
|
26
|
+
# Generate MCP context for all marked endpoints
|
27
|
+
rapitapir generate mcp --endpoints api.rb --output mcp-context.json
|
28
|
+
|
29
|
+
# Alternative export command
|
30
|
+
rapitapir export mcp --endpoints api.rb --output mcp-context.json
|
31
|
+
```
|
32
|
+
|
33
|
+
### Ruby API Usage
|
34
|
+
|
35
|
+
Use the MCP exporter directly in Ruby code:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
require 'rapitapir/ai/mcp'
|
39
|
+
|
40
|
+
# Load your endpoints
|
41
|
+
endpoints = [your_endpoint_list]
|
42
|
+
|
43
|
+
# Create exporter and generate context
|
44
|
+
exporter = RapiTapir::AI::MCP::Exporter.new(endpoints)
|
45
|
+
mcp_context = exporter.as_mcp_context
|
46
|
+
|
47
|
+
# Save to file
|
48
|
+
File.write('mcp-context.json', JSON.pretty_generate(mcp_context))
|
49
|
+
```
|
50
|
+
|
51
|
+
### Example Output
|
52
|
+
|
53
|
+
The generated MCP context includes:
|
54
|
+
|
55
|
+
```json
|
56
|
+
{
|
57
|
+
"name": "RapiTapir API Context",
|
58
|
+
"version": "1.0.0",
|
59
|
+
"endpoints": [
|
60
|
+
{
|
61
|
+
"name": "get__users__id_",
|
62
|
+
"method": "GET",
|
63
|
+
"path": "/users/{id}",
|
64
|
+
"summary": "Retrieve a user by ID",
|
65
|
+
"description": "Fetches a single user record by their unique identifier",
|
66
|
+
"input_schema": {
|
67
|
+
"id": {
|
68
|
+
"type": "integer",
|
69
|
+
"kind": "path",
|
70
|
+
"required": true
|
71
|
+
}
|
72
|
+
},
|
73
|
+
"output_schema": {
|
74
|
+
"json": {
|
75
|
+
"type": {
|
76
|
+
"id": "integer",
|
77
|
+
"name": "string",
|
78
|
+
"email": "string"
|
79
|
+
}
|
80
|
+
}
|
81
|
+
},
|
82
|
+
"examples": []
|
83
|
+
}
|
84
|
+
]
|
85
|
+
}
|
86
|
+
```
|
87
|
+
|
88
|
+
### Use Cases
|
89
|
+
- LLM/agent tool discovery
|
90
|
+
- Automated API documentation for AI
|
91
|
+
- Integration with agent frameworks (LangChain, OpenAI, etc.)
|
92
|
+
|
93
|
+
See the AI integration plan for more details.
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
|
5
|
+
# Add the project root to the load path for development
|
6
|
+
project_root = File.expand_path('../../', __dir__)
|
7
|
+
$LOAD_PATH.unshift(File.join(project_root, 'lib')) unless $LOAD_PATH.include?(File.join(project_root, 'lib'))
|
8
|
+
|
9
|
+
require 'rapitapir'
|
10
|
+
require 'rapitapir/ai/rag'
|
11
|
+
require 'json'
|
12
|
+
|
13
|
+
# Example: Knowledge Base API with RAG Support
|
14
|
+
module KnowledgeBaseAPI
|
15
|
+
extend RapiTapir::DSL
|
16
|
+
|
17
|
+
# Type alias for convenience
|
18
|
+
T = RapiTapir::Types
|
19
|
+
|
20
|
+
# Define schemas
|
21
|
+
ASK_SCHEMA = T.hash({
|
22
|
+
'question' => T.string,
|
23
|
+
'user_id' => T.string
|
24
|
+
}).freeze
|
25
|
+
|
26
|
+
ANSWER_SCHEMA = T.hash({
|
27
|
+
'answer' => T.string,
|
28
|
+
'sources' => T.array(T.string),
|
29
|
+
'timestamp' => T.string
|
30
|
+
}).freeze
|
31
|
+
|
32
|
+
CHAT_INPUT_SCHEMA = T.hash({
|
33
|
+
'message' => T.string
|
34
|
+
}).freeze
|
35
|
+
|
36
|
+
CHAT_OUTPUT_SCHEMA = T.hash({
|
37
|
+
'response' => T.string
|
38
|
+
}).freeze
|
39
|
+
|
40
|
+
# Ask a question using RAG
|
41
|
+
ASK_QUESTION = RapiTapir.post('/ask')
|
42
|
+
.json_body(ASK_SCHEMA)
|
43
|
+
.ok(ANSWER_SCHEMA)
|
44
|
+
.rag_inference(
|
45
|
+
llm: :openai,
|
46
|
+
retrieval: :memory,
|
47
|
+
context_fields: [:user_id],
|
48
|
+
config: {
|
49
|
+
llm: { api_key: 'mock' },
|
50
|
+
retrieval: {
|
51
|
+
documents: [
|
52
|
+
{ content: 'RapiTapir is a Ruby API framework that provides type-safe endpoints.' },
|
53
|
+
{ content: 'RAG combines document retrieval with LLM text generation.' },
|
54
|
+
{ content: 'This API demonstrates AI-powered question answering.' }
|
55
|
+
]
|
56
|
+
}
|
57
|
+
}
|
58
|
+
)
|
59
|
+
.summary('Ask a question about the knowledge base')
|
60
|
+
.mcp_export
|
61
|
+
|
62
|
+
# Simple chat
|
63
|
+
CHAT = RapiTapir.post('/chat')
|
64
|
+
.json_body(CHAT_INPUT_SCHEMA)
|
65
|
+
.ok(CHAT_OUTPUT_SCHEMA)
|
66
|
+
.rag_inference(
|
67
|
+
llm: :openai,
|
68
|
+
retrieval: :memory,
|
69
|
+
config: {
|
70
|
+
llm: { api_key: 'mock' },
|
71
|
+
retrieval: {
|
72
|
+
documents: [
|
73
|
+
{ content: 'RapiTapir supports MCP for AI agent integration.' }
|
74
|
+
]
|
75
|
+
}
|
76
|
+
}
|
77
|
+
)
|
78
|
+
.summary('Chat with AI')
|
79
|
+
.mcp_export
|
80
|
+
end
|
81
|
+
|
82
|
+
# Endpoints for CLI
|
83
|
+
knowledge_base_api = [KnowledgeBaseAPI::ASK_QUESTION, KnowledgeBaseAPI::CHAT]
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../lib/rapitapir'
|
4
|
+
require_relative '../../lib/rapitapir/endpoint_registry'
|
5
|
+
|
6
|
+
# Example: User Management API with MCP Export
|
7
|
+
# This demonstrates how to mark endpoints for MCP export for AI/LLM consumption
|
8
|
+
|
9
|
+
class UserManagementAPI
|
10
|
+
include RapiTapir::DSL
|
11
|
+
|
12
|
+
# User schema for consistent typing
|
13
|
+
USER_SCHEMA = {
|
14
|
+
id: :integer,
|
15
|
+
name: :string,
|
16
|
+
email: :string,
|
17
|
+
created_at: :datetime
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
# Endpoint 1: Get user by ID - marked for MCP export
|
21
|
+
GET_USER = RapiTapir.get('/users/{id}')
|
22
|
+
.in(path_param(:id, :integer))
|
23
|
+
.out(json_body(USER_SCHEMA))
|
24
|
+
.summary('Retrieve a user by ID')
|
25
|
+
.description('Fetches a single user record by their unique identifier')
|
26
|
+
.mcp_export # Mark for MCP export
|
27
|
+
|
28
|
+
# Endpoint 2: Create new user - marked for MCP export
|
29
|
+
CREATE_USER = RapiTapir.post('/users')
|
30
|
+
.in(json_body({
|
31
|
+
name: :string,
|
32
|
+
email: :string
|
33
|
+
}))
|
34
|
+
.out(status_code(201))
|
35
|
+
.out(json_body(USER_SCHEMA))
|
36
|
+
.summary('Create a new user')
|
37
|
+
.description('Creates a new user account with name and email')
|
38
|
+
.mcp_export # Mark for MCP export
|
39
|
+
|
40
|
+
# Endpoint 3: Update user - NOT marked for MCP export
|
41
|
+
UPDATE_USER = RapiTapir.put('/users/{id}')
|
42
|
+
.in(path_param(:id, :integer))
|
43
|
+
.in(json_body({
|
44
|
+
name: :string,
|
45
|
+
email: :string
|
46
|
+
}))
|
47
|
+
.out(json_body(USER_SCHEMA))
|
48
|
+
.summary('Update an existing user')
|
49
|
+
.description('Updates user information')
|
50
|
+
# No .mcp_export - this endpoint won't be included in MCP context
|
51
|
+
|
52
|
+
# Endpoint 4: List users with pagination - marked for MCP export
|
53
|
+
LIST_USERS = RapiTapir.get('/users')
|
54
|
+
.in(query(:page, :integer, required: false))
|
55
|
+
.in(query(:limit, :integer, required: false))
|
56
|
+
.out(json_body({
|
57
|
+
users: [:array, USER_SCHEMA],
|
58
|
+
total: :integer,
|
59
|
+
page: :integer,
|
60
|
+
limit: :integer
|
61
|
+
}))
|
62
|
+
.summary('List all users')
|
63
|
+
.description('Retrieves a paginated list of all users')
|
64
|
+
.mcp_export # Mark for MCP export
|
65
|
+
|
66
|
+
# Store endpoints for export
|
67
|
+
ALL_ENDPOINTS = [GET_USER, CREATE_USER, UPDATE_USER, LIST_USERS].freeze
|
68
|
+
|
69
|
+
# Register endpoints in global registry
|
70
|
+
RapiTapir::EndpointRegistry.register_all(ALL_ENDPOINTS)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Usage demonstration
|
74
|
+
if __FILE__ == $PROGRAM_NAME
|
75
|
+
puts "User Management API Example"
|
76
|
+
puts "==========================="
|
77
|
+
|
78
|
+
# Show which endpoints are marked for MCP export
|
79
|
+
mcp_endpoints = UserManagementAPI::ALL_ENDPOINTS.select(&:mcp_export?)
|
80
|
+
puts "\nEndpoints marked for MCP export:"
|
81
|
+
mcp_endpoints.each do |ep|
|
82
|
+
puts "- #{ep.method.upcase} #{ep.path} (#{ep.metadata[:summary]})"
|
83
|
+
end
|
84
|
+
|
85
|
+
# Generate MCP context
|
86
|
+
require_relative '../../lib/rapitapir/ai/mcp'
|
87
|
+
exporter = RapiTapir::AI::MCP::Exporter.new(UserManagementAPI::ALL_ENDPOINTS)
|
88
|
+
mcp_context = exporter.as_mcp_context
|
89
|
+
|
90
|
+
puts "\nGenerated MCP Context:"
|
91
|
+
puts JSON.pretty_generate(mcp_context)
|
92
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
|
5
|
+
# Add the project root to the load path for development
|
6
|
+
project_root = File.expand_path('../../', __dir__)
|
7
|
+
$LOAD_PATH.unshift(File.join(project_root, 'lib')) unless $LOAD_PATH.include?(File.join(project_root, 'lib'))
|
8
|
+
|
9
|
+
require 'rapitapir'
|
10
|
+
require 'rapitapir/ai/llm_instruction'
|
11
|
+
require 'json'
|
12
|
+
|
13
|
+
# Type alias for convenience (avoid warning if already defined)
|
14
|
+
T = RapiTapir::Types unless defined?(T)
|
15
|
+
|
16
|
+
module UserValidationAPI
|
17
|
+
# User creation validation schema
|
18
|
+
USER_INPUT_SCHEMA = T.hash({
|
19
|
+
'name' => T.string(min_length: 2, max_length: 50),
|
20
|
+
'email' => T.email,
|
21
|
+
'age' => T.integer(minimum: 18, maximum: 120),
|
22
|
+
'preferences' => T.optional(T.hash({
|
23
|
+
'newsletter' => T.boolean,
|
24
|
+
'notifications' => T.boolean
|
25
|
+
}))
|
26
|
+
}).freeze
|
27
|
+
|
28
|
+
USER_OUTPUT_SCHEMA = T.hash({
|
29
|
+
'id' => T.integer,
|
30
|
+
'name' => T.string,
|
31
|
+
'email' => T.string,
|
32
|
+
'status' => T.string,
|
33
|
+
'created_at' => T.datetime
|
34
|
+
}).freeze
|
35
|
+
|
36
|
+
VALIDATION_ERROR_SCHEMA = T.hash({
|
37
|
+
'error' => T.string,
|
38
|
+
'field_errors' => T.array(T.hash({
|
39
|
+
'field' => T.string,
|
40
|
+
'message' => T.string,
|
41
|
+
'code' => T.string
|
42
|
+
}))
|
43
|
+
}).freeze
|
44
|
+
|
45
|
+
# Data transformation schemas
|
46
|
+
LEGACY_USER_INPUT = T.hash({
|
47
|
+
'full_name' => T.string,
|
48
|
+
'email_address' => T.string,
|
49
|
+
'birth_year' => T.integer,
|
50
|
+
'settings' => T.optional(T.string) # JSON string in legacy format
|
51
|
+
}).freeze
|
52
|
+
|
53
|
+
# Analytics schema
|
54
|
+
USER_ANALYTICS_SCHEMA = T.hash({
|
55
|
+
'user_count' => T.integer,
|
56
|
+
'active_users' => T.integer,
|
57
|
+
'signup_rate' => T.float,
|
58
|
+
'demographics' => T.hash({
|
59
|
+
'avg_age' => T.float,
|
60
|
+
'age_distribution' => T.array(T.hash({
|
61
|
+
'range' => T.string,
|
62
|
+
'count' => T.integer
|
63
|
+
}))
|
64
|
+
})
|
65
|
+
}).freeze
|
66
|
+
|
67
|
+
# Validation endpoint with LLM instruction for input validation
|
68
|
+
CREATE_USER = RapiTapir.post('/users')
|
69
|
+
.json_body(USER_INPUT_SCHEMA)
|
70
|
+
.ok(USER_OUTPUT_SCHEMA)
|
71
|
+
.error_out(400, VALIDATION_ERROR_SCHEMA)
|
72
|
+
.llm_instruction(
|
73
|
+
purpose: :validation,
|
74
|
+
fields: [:body] # Focus on request body validation
|
75
|
+
)
|
76
|
+
.summary('Create a new user with validation')
|
77
|
+
.description('Creates a new user account with comprehensive input validation and business rule checking')
|
78
|
+
.mcp_export
|
79
|
+
|
80
|
+
# Transformation endpoint for legacy data migration
|
81
|
+
MIGRATE_LEGACY_USER = RapiTapir.post('/users/migrate')
|
82
|
+
.json_body(LEGACY_USER_INPUT)
|
83
|
+
.ok(USER_OUTPUT_SCHEMA)
|
84
|
+
.llm_instruction(
|
85
|
+
purpose: :transformation,
|
86
|
+
fields: :all
|
87
|
+
)
|
88
|
+
.summary('Migrate legacy user data to new format')
|
89
|
+
.description('Transforms legacy user data format to current schema with proper field mapping')
|
90
|
+
.mcp_export
|
91
|
+
|
92
|
+
# Analysis endpoint for user analytics
|
93
|
+
USER_ANALYTICS = RapiTapir.get('/users/analytics')
|
94
|
+
.query(:start_date, :date)
|
95
|
+
.query(:end_date, :date)
|
96
|
+
.query(:segment, T.optional(T.string))
|
97
|
+
.ok(USER_ANALYTICS_SCHEMA)
|
98
|
+
.llm_instruction(
|
99
|
+
purpose: :analysis,
|
100
|
+
fields: [:json] # Focus on analyzing the response data
|
101
|
+
)
|
102
|
+
.summary('Get user analytics and insights')
|
103
|
+
.description('Provides comprehensive user analytics including demographics and engagement metrics')
|
104
|
+
.mcp_export
|
105
|
+
|
106
|
+
# Documentation endpoint for API reference
|
107
|
+
USER_SEARCH = RapiTapir.get('/users/search')
|
108
|
+
.query(:q, T.string(min_length: 1))
|
109
|
+
.query(:limit, T.optional(T.integer(minimum: 1, maximum: 100)))
|
110
|
+
.query(:offset, T.optional(T.integer(minimum: 0)))
|
111
|
+
.ok(T.hash({
|
112
|
+
'users' => T.array(USER_OUTPUT_SCHEMA),
|
113
|
+
'total_count' => T.integer,
|
114
|
+
'has_more' => T.boolean
|
115
|
+
}))
|
116
|
+
.llm_instruction(
|
117
|
+
purpose: :documentation,
|
118
|
+
fields: :all
|
119
|
+
)
|
120
|
+
.summary('Search for users')
|
121
|
+
.description('Full-text search across user profiles with pagination support')
|
122
|
+
.mcp_export
|
123
|
+
|
124
|
+
# Testing endpoint for comprehensive test generation
|
125
|
+
UPDATE_USER = RapiTapir.put('/users/:id')
|
126
|
+
.path_param(:id, :integer)
|
127
|
+
.json_body(T.hash({
|
128
|
+
'name' => T.optional(T.string(min_length: 2, max_length: 50)),
|
129
|
+
'email' => T.optional(T.email),
|
130
|
+
'preferences' => T.optional(T.hash({
|
131
|
+
'newsletter' => T.boolean,
|
132
|
+
'notifications' => T.boolean
|
133
|
+
}))
|
134
|
+
}))
|
135
|
+
.ok(USER_OUTPUT_SCHEMA)
|
136
|
+
.error_out(404, T.hash({'error' => T.string}))
|
137
|
+
.error_out(400, VALIDATION_ERROR_SCHEMA)
|
138
|
+
.llm_instruction(
|
139
|
+
purpose: :testing,
|
140
|
+
fields: :all
|
141
|
+
)
|
142
|
+
.summary('Update user information')
|
143
|
+
.description('Updates user profile with partial data and validation')
|
144
|
+
.mcp_export
|
145
|
+
|
146
|
+
# Completion endpoint for smart field suggestions
|
147
|
+
USER_PROFILE_COMPLETION = RapiTapir.post('/users/complete')
|
148
|
+
.json_body(T.hash({
|
149
|
+
'partial_profile' => T.hash({
|
150
|
+
'name' => T.optional(T.string),
|
151
|
+
'email' => T.optional(T.string),
|
152
|
+
'location' => T.optional(T.string),
|
153
|
+
'interests' => T.optional(T.array(T.string))
|
154
|
+
})
|
155
|
+
}))
|
156
|
+
.ok(T.hash({
|
157
|
+
'suggestions' => T.hash({
|
158
|
+
'name' => T.optional(T.array(T.string)),
|
159
|
+
'email' => T.optional(T.string),
|
160
|
+
'location' => T.optional(T.array(T.string)),
|
161
|
+
'interests' => T.optional(T.array(T.string))
|
162
|
+
}),
|
163
|
+
'confidence_scores' => T.hash({
|
164
|
+
'name' => T.optional(T.float),
|
165
|
+
'email' => T.optional(T.float),
|
166
|
+
'location' => T.optional(T.float),
|
167
|
+
'interests' => T.optional(T.float)
|
168
|
+
})
|
169
|
+
}))
|
170
|
+
.llm_instruction(
|
171
|
+
purpose: :completion,
|
172
|
+
fields: [:body]
|
173
|
+
)
|
174
|
+
.summary('Get smart profile completion suggestions')
|
175
|
+
.description('Provides intelligent suggestions for completing user profile data')
|
176
|
+
.mcp_export
|
177
|
+
end
|
178
|
+
|
179
|
+
# Endpoints for CLI
|
180
|
+
user_validation_api = [
|
181
|
+
UserValidationAPI::CREATE_USER,
|
182
|
+
UserValidationAPI::MIGRATE_LEGACY_USER,
|
183
|
+
UserValidationAPI::USER_ANALYTICS,
|
184
|
+
UserValidationAPI::USER_SEARCH,
|
185
|
+
UserValidationAPI::UPDATE_USER,
|
186
|
+
UserValidationAPI::USER_PROFILE_COMPLETION
|
187
|
+
]
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# Rails 8 + RapiTapir Quick Start Guide
|
2
|
+
|
3
|
+
## 🚀 How to Fix the `uninitialized constant RapiTapir::Server::Rails::ControllerBase` Error
|
4
|
+
|
5
|
+
The error you're encountering is due to the **loading order**. RapiTapir's Rails integration requires Rails to be loaded first.
|
6
|
+
|
7
|
+
### ✅ **Solution: Correct Loading Order**
|
8
|
+
|
9
|
+
In your Rails application, ensure this loading order:
|
10
|
+
|
11
|
+
#### 1. In your `Gemfile`:
|
12
|
+
```ruby
|
13
|
+
source 'https://rubygems.org'
|
14
|
+
ruby '3.2.0'
|
15
|
+
|
16
|
+
gem 'rails', '~> 8.0.0' # Rails 8
|
17
|
+
gem 'rapitapir', '~> 1.0' # RapiTapir after Rails
|
18
|
+
|
19
|
+
# Other gems...
|
20
|
+
gem 'sqlite3'
|
21
|
+
gem 'puma'
|
22
|
+
```
|
23
|
+
|
24
|
+
#### 2. In your application (or initializer):
|
25
|
+
```ruby
|
26
|
+
# This is already handled by Rails bundler, but if you need manual loading:
|
27
|
+
|
28
|
+
# 1. Rails loads first (via bundle/require in application.rb)
|
29
|
+
require 'rails/all'
|
30
|
+
|
31
|
+
# 2. RapiTapir loads after Rails
|
32
|
+
require 'rapitapir' # Usually handled by bundler
|
33
|
+
|
34
|
+
# 3. Now you can use RapiTapir classes
|
35
|
+
class ApplicationController < RapiTapir::Server::Rails::ControllerBase
|
36
|
+
# This works!
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
### 🧪 **Test with Our Runnable Example**
|
41
|
+
|
42
|
+
The easiest way to verify everything works:
|
43
|
+
|
44
|
+
```bash
|
45
|
+
cd examples/rails
|
46
|
+
ruby traditional_app_runnable.rb
|
47
|
+
```
|
48
|
+
|
49
|
+
This should start successfully and show:
|
50
|
+
```
|
51
|
+
🚀 Starting Traditional Rails App with RapiTapir on http://localhost:3000
|
52
|
+
📚 API Documentation: http://localhost:3000/docs
|
53
|
+
📋 OpenAPI Spec: http://localhost:3000/openapi.json
|
54
|
+
🏥 Health Check: http://localhost:3000/health
|
55
|
+
```
|
56
|
+
|
57
|
+
### 🔧 **For New Rails 8 Applications**
|
58
|
+
|
59
|
+
#### 1. Create Rails App
|
60
|
+
```bash
|
61
|
+
rails new my_api --api
|
62
|
+
cd my_api
|
63
|
+
```
|
64
|
+
|
65
|
+
#### 2. Add RapiTapir to Gemfile
|
66
|
+
```ruby
|
67
|
+
# Gemfile
|
68
|
+
gem 'rapitapir', '~> 1.0'
|
69
|
+
```
|
70
|
+
|
71
|
+
#### 3. Install
|
72
|
+
```bash
|
73
|
+
bundle install
|
74
|
+
```
|
75
|
+
|
76
|
+
#### 4. Update ApplicationController
|
77
|
+
```ruby
|
78
|
+
# app/controllers/application_controller.rb
|
79
|
+
class ApplicationController < RapiTapir::Server::Rails::ControllerBase
|
80
|
+
rapitapir do
|
81
|
+
development_defaults! if Rails.env.development?
|
82
|
+
|
83
|
+
# Global error responses
|
84
|
+
error_out(json_body(error: T.string), 404)
|
85
|
+
error_out(json_body(error: T.string, errors: T.array(T.string).optional), 422)
|
86
|
+
|
87
|
+
# Health endpoint
|
88
|
+
GET('/health')
|
89
|
+
.out(json_body(status: T.string, timestamp: T.string))
|
90
|
+
.summary("Health check")
|
91
|
+
end
|
92
|
+
|
93
|
+
def health_check
|
94
|
+
{ status: 'ok', timestamp: Time.current.iso8601 }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
#### 5. Create API Controllers
|
100
|
+
```ruby
|
101
|
+
# app/controllers/api/v1/users_controller.rb
|
102
|
+
class Api::V1::UsersController < ApplicationController
|
103
|
+
rapitapir do
|
104
|
+
user_type = T.hash(id: T.integer, name: T.string, email: T.string)
|
105
|
+
|
106
|
+
GET('/api/v1/users')
|
107
|
+
.out(json_body(users: T.array(user_type)))
|
108
|
+
.summary("List users")
|
109
|
+
.tag("Users")
|
110
|
+
end
|
111
|
+
|
112
|
+
def list_users
|
113
|
+
{ users: [{ id: 1, name: "Test User", email: "test@example.com" }] }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
#### 6. Update Routes
|
119
|
+
```ruby
|
120
|
+
# config/routes.rb
|
121
|
+
Rails.application.routes.draw do
|
122
|
+
rapitapir_routes_for ApplicationController
|
123
|
+
|
124
|
+
namespace :api do
|
125
|
+
namespace :v1 do
|
126
|
+
rapitapir_routes_for 'Api::V1::UsersController'
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
#### 7. Start Server
|
133
|
+
```bash
|
134
|
+
rails server
|
135
|
+
```
|
136
|
+
|
137
|
+
Visit:
|
138
|
+
- 🏥 Health: http://localhost:3000/health
|
139
|
+
- 📚 Docs: http://localhost:3000/docs
|
140
|
+
- 👥 Users: http://localhost:3000/api/v1/users
|
141
|
+
|
142
|
+
### ⚠️ **Common Issues & Solutions**
|
143
|
+
|
144
|
+
#### Issue: `uninitialized constant RapiTapir::Server::Rails::ControllerBase`
|
145
|
+
**Solution**: Make sure Rails is loaded before RapiTapir. In most Rails apps, this happens automatically via bundler.
|
146
|
+
|
147
|
+
#### Issue: `NoMethodError: undefined method 'rapitapir_routes_for'`
|
148
|
+
**Solution**: Ensure RapiTapir's Rails integration is loaded. Add to `config/application.rb`:
|
149
|
+
```ruby
|
150
|
+
require 'rapitapir'
|
151
|
+
```
|
152
|
+
|
153
|
+
#### Issue: Documentation not showing up
|
154
|
+
**Solution**: Make sure you have `development_defaults!` in your rapitapir block.
|
155
|
+
|
156
|
+
### 🎉 **Benefits of Rails 8 + RapiTapir**
|
157
|
+
|
158
|
+
- ✅ **Modern Rails**: Latest Rails 8 features and performance
|
159
|
+
- ✅ **Type Safety**: Automatic input validation and output schemas
|
160
|
+
- ✅ **Auto Documentation**: Swagger UI and OpenAPI 3.0 specs
|
161
|
+
- ✅ **Clean Syntax**: Sinatra-like DSL in Rails controllers
|
162
|
+
- ✅ **Zero Config**: Works out of the box with `development_defaults!`
|
163
|
+
- ✅ **Production Ready**: Error handling, health checks, monitoring
|
164
|
+
|
165
|
+
The combination of Rails 8 and RapiTapir gives you the best of both worlds: Rails' maturity and ecosystem with RapiTapir's elegant API design! 🚀
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Instructions for using RapiTapir with Rails
|
4
|
+
#
|
5
|
+
# To fix the "uninitialized constant RapiTapir::Server::Rails::ControllerBase" error:
|
6
|
+
#
|
7
|
+
# 1. In your Rails application, make sure to require 'rapitapir' AFTER Rails is loaded
|
8
|
+
# 2. In your Gemfile, add:
|
9
|
+
# gem 'rapitapir'
|
10
|
+
# 3. In your ApplicationController or in an initializer, add:
|
11
|
+
# require 'rapitapir' (only if needed - bundler usually handles this)
|
12
|
+
#
|
13
|
+
# The error occurs because RapiTapir's Rails integration requires Rails and ActiveSupport
|
14
|
+
# to be loaded first.
|
15
|
+
|
16
|
+
# For the traditional_app_runnable.rb example, the require order should be:
|
17
|
+
|
18
|
+
require 'bundler/inline'
|
19
|
+
|
20
|
+
gemfile do
|
21
|
+
source 'https://rubygems.org'
|
22
|
+
gem 'rails', '~> 8.0'
|
23
|
+
gem 'sqlite3'
|
24
|
+
gem 'puma'
|
25
|
+
end
|
26
|
+
|
27
|
+
# Load Rails first
|
28
|
+
require 'rails/all'
|
29
|
+
|
30
|
+
# THEN load RapiTapir (this is the key!)
|
31
|
+
require_relative '../../../lib/rapitapir'
|
32
|
+
|
33
|
+
# Now you can use RapiTapir::Server::Rails::ControllerBase
|
34
|
+
|
35
|
+
puts "✅ This order works correctly!"
|