rapitapir 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +57 -0
- data/CHANGELOG.md +94 -0
- data/CLEANUP_SUMMARY.md +155 -0
- data/CONTRIBUTING.md +280 -0
- data/LICENSE +21 -0
- data/README.md +485 -0
- data/debug_hash.rb +20 -0
- data/docs/EXTENSION_COMPARISON.md +388 -0
- data/docs/SINATRA_EXTENSION.md +467 -0
- data/docs/archive/PHASE_1_2_COMPLETE.md +77 -0
- data/docs/archive/PHASE_1_3_COMPLETE.md +152 -0
- data/docs/archive/PHASE_2_1_OBSERVABILITY_COMPLETED.md +203 -0
- data/docs/archive/PHASE_2_SUMMARY.md +209 -0
- data/docs/archive/REFACTORING_SUMMARY.md +184 -0
- data/docs/archive/phase_1_3_plan.md +136 -0
- data/docs/archive/sinatra_extension_summary.md +188 -0
- data/docs/archive/sinatra_working_solution.md +113 -0
- data/docs/archive/typescript-client-generator-summary.md +259 -0
- data/docs/auto-derivation.md +146 -0
- data/docs/blueprint.md +1091 -0
- data/docs/endpoint-definition.md +211 -0
- data/docs/github_pages_fix.md +52 -0
- data/docs/github_pages_setup.md +49 -0
- data/docs/implementation-status.md +357 -0
- data/docs/observability.md +647 -0
- data/docs/phase3-plan.md +108 -0
- data/docs/sinatra_rapitapir.md +87 -0
- data/docs/type_shortcuts.md +146 -0
- data/examples/README_ENTERPRISE.md +202 -0
- data/examples/authentication_example.rb +192 -0
- data/examples/auto_derivation_ruby_friendly.rb +163 -0
- data/examples/cli/user_api_endpoints.rb +56 -0
- data/examples/client/typescript_client_example.rb +102 -0
- data/examples/client/user-api-client.ts +193 -0
- data/examples/demo_api.rb +41 -0
- data/examples/docs/documentation_example.rb +112 -0
- data/examples/docs/user-api-docs.html +789 -0
- data/examples/docs/user-api-docs.md +403 -0
- data/examples/enhanced_auto_derivation_test.rb +83 -0
- data/examples/enterprise_extension_demo.rb +417 -0
- data/examples/enterprise_rapitapir_api.rb +662 -0
- data/examples/getting_started_extension.rb +218 -0
- data/examples/hello_world.rb +74 -0
- data/examples/oauth2/.env.example +19 -0
- data/examples/oauth2/README.md +205 -0
- data/examples/oauth2/generic_oauth2_api.rb +226 -0
- data/examples/oauth2/get_token.rb +72 -0
- data/examples/oauth2/songs_api_with_auth0.rb +320 -0
- data/examples/oauth2/test_api.sh +16 -0
- data/examples/oauth2/test_songs_api.sh +110 -0
- data/examples/observability/.env.example +35 -0
- data/examples/observability/README.md +230 -0
- data/examples/observability/README_HONEYCOMB.md +332 -0
- data/examples/observability/advanced_setup.rb +384 -0
- data/examples/observability/basic_setup.rb +192 -0
- data/examples/observability/complete_test.rb +121 -0
- data/examples/observability/honeycomb_example.rb +523 -0
- data/examples/observability/honeycomb_rapitapir_clean.rb +488 -0
- data/examples/observability/honeycomb_rapitapir_example.rb +523 -0
- data/examples/observability/honeycomb_working_example.rb +489 -0
- data/examples/observability/quick_test.rb +78 -0
- data/examples/observability/simple_test.rb +14 -0
- data/examples/observability/test_honeycomb_demo.rb +354 -0
- data/examples/observability/test_live_honeycomb.rb +111 -0
- data/examples/observability/test_validation.rb +78 -0
- data/examples/observability/test_working_validation.rb +66 -0
- data/examples/openapi/user_api_schema.rb +132 -0
- data/examples/production_ready_example.rb +105 -0
- data/examples/rails/users_controller.rb +146 -0
- data/examples/readme/basic_sinatra_example.rb +128 -0
- data/examples/server/user_api.rb +179 -0
- data/examples/simple_auto_derivation_demo.rb +44 -0
- data/examples/simple_demo_api.rb +18 -0
- data/examples/sinatra/user_app.rb +127 -0
- data/examples/t_shortcut_demo.rb +59 -0
- data/examples/user_api.rb +190 -0
- data/examples/working_getting_started.rb +184 -0
- data/examples/working_simple_example.rb +195 -0
- data/lib/rapitapir/auth/configuration.rb +129 -0
- data/lib/rapitapir/auth/context.rb +122 -0
- data/lib/rapitapir/auth/errors.rb +104 -0
- data/lib/rapitapir/auth/middleware.rb +324 -0
- data/lib/rapitapir/auth/oauth2.rb +350 -0
- data/lib/rapitapir/auth/schemes.rb +420 -0
- data/lib/rapitapir/auth.rb +113 -0
- data/lib/rapitapir/cli/command.rb +535 -0
- data/lib/rapitapir/cli/server.rb +243 -0
- data/lib/rapitapir/cli/validator.rb +373 -0
- data/lib/rapitapir/client/generator_base.rb +272 -0
- data/lib/rapitapir/client/typescript_generator.rb +350 -0
- data/lib/rapitapir/core/endpoint.rb +158 -0
- data/lib/rapitapir/core/enhanced_endpoint.rb +235 -0
- data/lib/rapitapir/core/input.rb +182 -0
- data/lib/rapitapir/core/output.rb +164 -0
- data/lib/rapitapir/core/request.rb +19 -0
- data/lib/rapitapir/core/response.rb +17 -0
- data/lib/rapitapir/docs/html_generator.rb +780 -0
- data/lib/rapitapir/docs/markdown_generator.rb +464 -0
- data/lib/rapitapir/dsl/endpoint_dsl.rb +116 -0
- data/lib/rapitapir/dsl/enhanced_endpoint_dsl.rb +62 -0
- data/lib/rapitapir/dsl/enhanced_input.rb +73 -0
- data/lib/rapitapir/dsl/enhanced_output.rb +63 -0
- data/lib/rapitapir/dsl/enhanced_structures.rb +393 -0
- data/lib/rapitapir/dsl/fluent_dsl.rb +72 -0
- data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +316 -0
- data/lib/rapitapir/dsl/http_verbs.rb +77 -0
- data/lib/rapitapir/dsl/input_methods.rb +47 -0
- data/lib/rapitapir/dsl/observability_methods.rb +81 -0
- data/lib/rapitapir/dsl/output_methods.rb +43 -0
- data/lib/rapitapir/dsl/type_resolution.rb +43 -0
- data/lib/rapitapir/observability/configuration.rb +108 -0
- data/lib/rapitapir/observability/health_check.rb +236 -0
- data/lib/rapitapir/observability/logging.rb +270 -0
- data/lib/rapitapir/observability/metrics.rb +203 -0
- data/lib/rapitapir/observability/middleware.rb +243 -0
- data/lib/rapitapir/observability/tracing.rb +143 -0
- data/lib/rapitapir/observability.rb +28 -0
- data/lib/rapitapir/openapi/schema_generator.rb +403 -0
- data/lib/rapitapir/schema.rb +136 -0
- data/lib/rapitapir/server/enhanced_rack_adapter.rb +379 -0
- data/lib/rapitapir/server/middleware.rb +120 -0
- data/lib/rapitapir/server/path_matcher.rb +45 -0
- data/lib/rapitapir/server/rack_adapter.rb +215 -0
- data/lib/rapitapir/server/rails_adapter.rb +17 -0
- data/lib/rapitapir/server/rails_adapter_class.rb +53 -0
- data/lib/rapitapir/server/rails_controller.rb +72 -0
- data/lib/rapitapir/server/rails_input_processor.rb +73 -0
- data/lib/rapitapir/server/rails_response_handler.rb +29 -0
- data/lib/rapitapir/server/sinatra_adapter.rb +200 -0
- data/lib/rapitapir/server/sinatra_integration.rb +93 -0
- data/lib/rapitapir/sinatra/configuration.rb +91 -0
- data/lib/rapitapir/sinatra/extension.rb +214 -0
- data/lib/rapitapir/sinatra/oauth2_helpers.rb +236 -0
- data/lib/rapitapir/sinatra/resource_builder.rb +152 -0
- data/lib/rapitapir/sinatra/swagger_ui_generator.rb +166 -0
- data/lib/rapitapir/sinatra_rapitapir.rb +40 -0
- data/lib/rapitapir/types/array.rb +163 -0
- data/lib/rapitapir/types/auto_derivation.rb +265 -0
- data/lib/rapitapir/types/base.rb +146 -0
- data/lib/rapitapir/types/boolean.rb +46 -0
- data/lib/rapitapir/types/date.rb +92 -0
- data/lib/rapitapir/types/datetime.rb +98 -0
- data/lib/rapitapir/types/email.rb +32 -0
- data/lib/rapitapir/types/float.rb +134 -0
- data/lib/rapitapir/types/hash.rb +161 -0
- data/lib/rapitapir/types/integer.rb +143 -0
- data/lib/rapitapir/types/object.rb +156 -0
- data/lib/rapitapir/types/optional.rb +65 -0
- data/lib/rapitapir/types/string.rb +185 -0
- data/lib/rapitapir/types/uuid.rb +32 -0
- data/lib/rapitapir/types.rb +155 -0
- data/lib/rapitapir/version.rb +5 -0
- data/lib/rapitapir.rb +173 -0
- data/rapitapir.gemspec +66 -0
- metadata +387 -0
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../lib/rapitapir'
|
4
|
+
require_relative '../../lib/rapitapir/openapi/schema_generator'
|
5
|
+
|
6
|
+
# Example script to demonstrate OpenAPI schema generation
|
7
|
+
class OpenAPIExample
|
8
|
+
def initialize
|
9
|
+
@endpoints = setup_user_endpoints
|
10
|
+
end
|
11
|
+
|
12
|
+
def generate_schema
|
13
|
+
generator = RapiTapir::OpenAPI::SchemaGenerator.new(
|
14
|
+
endpoints: @endpoints,
|
15
|
+
info: {
|
16
|
+
title: 'User Management API',
|
17
|
+
version: '1.0.0',
|
18
|
+
description: 'A simple user management API built with RapiTapir'
|
19
|
+
},
|
20
|
+
servers: [
|
21
|
+
{
|
22
|
+
url: 'http://localhost:4567',
|
23
|
+
description: 'Development server (Sinatra)'
|
24
|
+
},
|
25
|
+
{
|
26
|
+
url: 'http://localhost:9292',
|
27
|
+
description: 'Development server (Rack)'
|
28
|
+
}
|
29
|
+
]
|
30
|
+
)
|
31
|
+
|
32
|
+
generator.generate
|
33
|
+
end
|
34
|
+
|
35
|
+
def save_schema(format: :json, pretty: true)
|
36
|
+
schema = generate_schema
|
37
|
+
|
38
|
+
case format
|
39
|
+
when :json
|
40
|
+
content = pretty ? JSON.pretty_generate(schema) : JSON.generate(schema)
|
41
|
+
filename = 'user_api_schema.json'
|
42
|
+
when :yaml
|
43
|
+
require 'yaml'
|
44
|
+
content = YAML.dump(schema)
|
45
|
+
filename = 'user_api_schema.yaml'
|
46
|
+
else
|
47
|
+
raise ArgumentError, "Unsupported format: #{format}"
|
48
|
+
end
|
49
|
+
|
50
|
+
File.write(filename, content)
|
51
|
+
puts "OpenAPI schema saved to #{filename}"
|
52
|
+
puts 'Schema preview:'
|
53
|
+
puts content[0..500] + (content.length > 500 ? '...' : '')
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def setup_user_endpoints
|
59
|
+
endpoints = []
|
60
|
+
|
61
|
+
# List users endpoint
|
62
|
+
list_endpoint = RapiTapir.get('/users')
|
63
|
+
.summary('List all users')
|
64
|
+
.description('Returns a list of all users in the system')
|
65
|
+
.out(RapiTapir::Core::Output.new(kind: :json,
|
66
|
+
type: { users: [{
|
67
|
+
id: :integer, name: :string, email: :string
|
68
|
+
}] }))
|
69
|
+
endpoints << list_endpoint
|
70
|
+
|
71
|
+
# Get user by ID endpoint
|
72
|
+
get_endpoint = RapiTapir.get('/users/:id')
|
73
|
+
.summary('Get user by ID')
|
74
|
+
.description('Returns a single user by their ID')
|
75
|
+
.in(RapiTapir::Core::Input.new(kind: :path, name: :id, type: :integer))
|
76
|
+
.out(RapiTapir::Core::Output.new(kind: :json,
|
77
|
+
type: {
|
78
|
+
id: :integer, name: :string, email: :string
|
79
|
+
}))
|
80
|
+
endpoints << get_endpoint
|
81
|
+
|
82
|
+
# Create user endpoint
|
83
|
+
create_endpoint = RapiTapir.post('/users')
|
84
|
+
.summary('Create a new user')
|
85
|
+
.description('Creates a new user with the provided data')
|
86
|
+
.in(RapiTapir::Core::Input.new(kind: :body, name: :user_data,
|
87
|
+
type: { name: :string, email: :string }))
|
88
|
+
.out(RapiTapir::Core::Output.new(kind: :json,
|
89
|
+
type: {
|
90
|
+
id: :integer, name: :string, email: :string
|
91
|
+
}))
|
92
|
+
endpoints << create_endpoint
|
93
|
+
|
94
|
+
# Update user endpoint
|
95
|
+
update_endpoint = RapiTapir.put('/users/:id')
|
96
|
+
.summary('Update an existing user')
|
97
|
+
.description('Updates an existing user with the provided data')
|
98
|
+
.in(RapiTapir::Core::Input.new(kind: :path, name: :id, type: :integer))
|
99
|
+
.in(RapiTapir::Core::Input.new(kind: :body, name: :user_data,
|
100
|
+
type: { name: :string, email: :string }))
|
101
|
+
.out(RapiTapir::Core::Output.new(kind: :json,
|
102
|
+
type: {
|
103
|
+
id: :integer, name: :string, email: :string
|
104
|
+
}))
|
105
|
+
endpoints << update_endpoint
|
106
|
+
|
107
|
+
# Delete user endpoint
|
108
|
+
delete_endpoint = RapiTapir.delete('/users/:id')
|
109
|
+
.summary('Delete a user')
|
110
|
+
.description('Deletes a user by their ID')
|
111
|
+
.in(RapiTapir::Core::Input.new(kind: :path, name: :id, type: :integer))
|
112
|
+
.out(RapiTapir::Core::Output.new(kind: :json, type: { message: :string }))
|
113
|
+
endpoints << delete_endpoint
|
114
|
+
|
115
|
+
endpoints
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Run the example if this file is executed directly
|
120
|
+
if __FILE__ == $PROGRAM_NAME
|
121
|
+
example = OpenAPIExample.new
|
122
|
+
|
123
|
+
puts 'Generating OpenAPI schema for User Management API...'
|
124
|
+
|
125
|
+
# Generate and save JSON schema
|
126
|
+
example.save_schema(format: :json, pretty: true)
|
127
|
+
|
128
|
+
puts "\n#{'=' * 50}"
|
129
|
+
|
130
|
+
# Generate and save YAML schema
|
131
|
+
example.save_schema(format: :yaml)
|
132
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/rapitapir'
|
4
|
+
|
5
|
+
puts '🌟 RapiTapir Production-Ready API Example'
|
6
|
+
puts '=' * 45
|
7
|
+
|
8
|
+
# Define schemas
|
9
|
+
User = RapiTapir::Schema.define do
|
10
|
+
field :id, :uuid
|
11
|
+
field :name, :string
|
12
|
+
field :email, :email
|
13
|
+
field :created_at, :datetime
|
14
|
+
end
|
15
|
+
|
16
|
+
CreateUser = RapiTapir::Schema.define do
|
17
|
+
field :name, :string
|
18
|
+
field :email, :email
|
19
|
+
end
|
20
|
+
|
21
|
+
Error = RapiTapir::Schema.define do
|
22
|
+
field :error, :string
|
23
|
+
field :message, :string
|
24
|
+
field :code, :integer
|
25
|
+
end
|
26
|
+
|
27
|
+
# Define a complete REST API using the fluent DSL
|
28
|
+
RapiTapir.namespace('/api/v1/users') do
|
29
|
+
bearer_auth('API Token Required')
|
30
|
+
tags('users', 'rest-api')
|
31
|
+
|
32
|
+
error_responses do
|
33
|
+
unauthorized(401, Error, description: 'Invalid or missing token')
|
34
|
+
forbidden(403, Error, description: 'Insufficient permissions')
|
35
|
+
server_error(500, Error, description: 'Internal server error')
|
36
|
+
end
|
37
|
+
|
38
|
+
# GET /api/v1/users - List users with pagination
|
39
|
+
get('/')
|
40
|
+
.summary('List users')
|
41
|
+
.description('Retrieve paginated list of users')
|
42
|
+
.query(:page, :integer, min: 1, default: 1, description: 'Page number')
|
43
|
+
.query(:per_page, :integer, min: 1, max: 100, default: 20, description: 'Items per page')
|
44
|
+
.query(:search, :string, required: false, description: 'Search users by name')
|
45
|
+
.ok(RapiTapir::Types.array(User), description: 'List of users')
|
46
|
+
.bad_request(Error, description: 'Invalid query parameters')
|
47
|
+
|
48
|
+
# GET /api/v1/users/{id} - Get specific user
|
49
|
+
get('/{id}')
|
50
|
+
.summary('Get user by ID')
|
51
|
+
.description('Retrieve a specific user by their unique identifier')
|
52
|
+
.path_param(:id, :uuid, description: 'User unique identifier')
|
53
|
+
.ok(User, description: 'User details')
|
54
|
+
.not_found(Error, description: 'User not found')
|
55
|
+
|
56
|
+
# POST /api/v1/users - Create new user
|
57
|
+
post('/')
|
58
|
+
.summary('Create user')
|
59
|
+
.description('Create a new user account')
|
60
|
+
.json_body(CreateUser, description: 'User data')
|
61
|
+
.requires_scope('users:write')
|
62
|
+
.created(User, description: 'User created successfully')
|
63
|
+
.bad_request(Error, description: 'Invalid user data')
|
64
|
+
.unprocessable_entity(Error, description: 'Validation failed')
|
65
|
+
|
66
|
+
# PUT /api/v1/users/{id} - Update user
|
67
|
+
put('/{id}')
|
68
|
+
.summary('Update user')
|
69
|
+
.description("Update an existing user's information")
|
70
|
+
.path_param(:id, :uuid, description: 'User unique identifier')
|
71
|
+
.json_body(CreateUser, description: 'Updated user data')
|
72
|
+
.requires_scope('users:write')
|
73
|
+
.ok(User, description: 'User updated successfully')
|
74
|
+
.not_found(Error, description: 'User not found')
|
75
|
+
.unprocessable_entity(Error, description: 'Validation failed')
|
76
|
+
|
77
|
+
# DELETE /api/v1/users/{id} - Delete user
|
78
|
+
delete('/{id}')
|
79
|
+
.summary('Delete user')
|
80
|
+
.description('Permanently delete a user account')
|
81
|
+
.path_param(:id, :uuid, description: 'User unique identifier')
|
82
|
+
.requires_scope('users:delete')
|
83
|
+
.no_content(description: 'User deleted successfully')
|
84
|
+
.not_found(Error, description: 'User not found')
|
85
|
+
end
|
86
|
+
|
87
|
+
puts '✅ Defined complete REST API with 5 endpoints'
|
88
|
+
puts ' - GET /api/v1/users (list with pagination)'
|
89
|
+
puts ' - GET /api/v1/users/{id} (get by ID)'
|
90
|
+
puts ' - POST /api/v1/users (create)'
|
91
|
+
puts ' - PUT /api/v1/users/{id} (update)'
|
92
|
+
puts ' - DELETE /api/v1/users/{id} (delete)'
|
93
|
+
|
94
|
+
puts "\n✅ Features implemented:"
|
95
|
+
puts ' - Bearer token authentication'
|
96
|
+
puts ' - Scope-based permissions (read/write/delete)'
|
97
|
+
puts ' - Full CRUD operations'
|
98
|
+
puts ' - Input validation with type constraints'
|
99
|
+
puts ' - Comprehensive error responses'
|
100
|
+
puts ' - OpenAPI 3.0.3 specification ready'
|
101
|
+
puts ' - Production-ready architecture'
|
102
|
+
|
103
|
+
puts "\n🚀 RapiTapir Phase 1 Complete!"
|
104
|
+
puts ' From documentation tool → Production API framework'
|
105
|
+
puts ' Ready for real-world applications! 🎉'
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Example Rails controller using RapiTapir
|
4
|
+
#
|
5
|
+
# To use this in a Rails app:
|
6
|
+
# 1. Add this file to app/controllers/
|
7
|
+
# 2. Include RapiTapir::Server::Rails::Controller
|
8
|
+
# 3. Define endpoints using rapitapir_endpoint
|
9
|
+
# 4. Call process_rapitapir_endpoint in your action methods
|
10
|
+
|
11
|
+
require_relative '../../lib/rapitapir'
|
12
|
+
require_relative '../../lib/rapitapir/server/rails_adapter'
|
13
|
+
|
14
|
+
class UsersController < ApplicationController
|
15
|
+
include RapiTapir::Server::Rails::Controller
|
16
|
+
|
17
|
+
before_action :setup_users_data
|
18
|
+
|
19
|
+
# Define RapiTapir endpoints
|
20
|
+
rapitapir_endpoint :index, RapiTapir.get('/users')
|
21
|
+
.summary('List all users')
|
22
|
+
.description('Returns a list of all users in the system')
|
23
|
+
.out(RapiTapir::Core::Output.new(
|
24
|
+
kind: :json, type: { users: Array }
|
25
|
+
)) do |_inputs|
|
26
|
+
{ users: @users.values }
|
27
|
+
end
|
28
|
+
|
29
|
+
rapitapir_endpoint :show, RapiTapir.get('/users/:id')
|
30
|
+
.summary('Get user by ID')
|
31
|
+
.description('Returns a single user by their ID')
|
32
|
+
.in(RapiTapir::Core::Input.new(kind: :path, name: :id, type: :integer))
|
33
|
+
.out(RapiTapir::Core::Output.new(
|
34
|
+
kind: :json,
|
35
|
+
type: { id: :integer, name: :string, email: :string }
|
36
|
+
)) do |inputs|
|
37
|
+
user_id = inputs[:id]
|
38
|
+
user = @users[user_id]
|
39
|
+
|
40
|
+
raise ArgumentError, 'User not found' unless user
|
41
|
+
|
42
|
+
user
|
43
|
+
end
|
44
|
+
|
45
|
+
rapitapir_endpoint :create, RapiTapir.post('/users')
|
46
|
+
.summary('Create a new user')
|
47
|
+
.description('Creates a new user with the provided data')
|
48
|
+
.in(RapiTapir::Core::Input.new(kind: :body, name: :user_data, type: Hash))
|
49
|
+
.out(RapiTapir::Core::Output.new(
|
50
|
+
kind: :json,
|
51
|
+
type: { id: :integer, name: :string, email: :string }
|
52
|
+
)) do |inputs|
|
53
|
+
user_data = inputs[:user_data]
|
54
|
+
new_id = (@users.keys.max || 0) + 1
|
55
|
+
|
56
|
+
new_user = {
|
57
|
+
id: new_id,
|
58
|
+
name: user_data['name'],
|
59
|
+
email: user_data['email']
|
60
|
+
}
|
61
|
+
|
62
|
+
@users[new_id] = new_user
|
63
|
+
new_user
|
64
|
+
end
|
65
|
+
|
66
|
+
rapitapir_endpoint :update, RapiTapir.put('/users/:id')
|
67
|
+
.summary('Update an existing user')
|
68
|
+
.description('Updates an existing user with the provided data')
|
69
|
+
.in(RapiTapir::Core::Input.new(kind: :path, name: :id, type: :integer))
|
70
|
+
.in(RapiTapir::Core::Input.new(kind: :body, name: :user_data, type: Hash))
|
71
|
+
.out(RapiTapir::Core::Output.new(
|
72
|
+
kind: :json,
|
73
|
+
type: { id: :integer, name: :string, email: :string }
|
74
|
+
)) do |inputs|
|
75
|
+
user_id = inputs[:id]
|
76
|
+
user_data = inputs[:user_data]
|
77
|
+
|
78
|
+
raise ArgumentError, 'User not found' unless @users[user_id]
|
79
|
+
|
80
|
+
@users[user_id].merge!(
|
81
|
+
name: user_data['name'] || @users[user_id][:name],
|
82
|
+
email: user_data['email'] || @users[user_id][:email]
|
83
|
+
)
|
84
|
+
|
85
|
+
@users[user_id]
|
86
|
+
end
|
87
|
+
|
88
|
+
rapitapir_endpoint :destroy, RapiTapir.delete('/users/:id')
|
89
|
+
.summary('Delete a user')
|
90
|
+
.description('Deletes a user by their ID')
|
91
|
+
.in(RapiTapir::Core::Input.new(kind: :path, name: :id, type: :integer))
|
92
|
+
.out(RapiTapir::Core::Output.new(
|
93
|
+
kind: :json, type: { message: :string }
|
94
|
+
)) do |inputs|
|
95
|
+
user_id = inputs[:id]
|
96
|
+
|
97
|
+
raise ArgumentError, 'User not found' unless @users[user_id]
|
98
|
+
|
99
|
+
@users.delete(user_id)
|
100
|
+
{ message: "User #{user_id} deleted successfully" }
|
101
|
+
end
|
102
|
+
|
103
|
+
# Controller actions
|
104
|
+
def index
|
105
|
+
process_rapitapir_endpoint
|
106
|
+
end
|
107
|
+
|
108
|
+
def show
|
109
|
+
process_rapitapir_endpoint
|
110
|
+
end
|
111
|
+
|
112
|
+
def create
|
113
|
+
process_rapitapir_endpoint
|
114
|
+
end
|
115
|
+
|
116
|
+
def update
|
117
|
+
process_rapitapir_endpoint
|
118
|
+
end
|
119
|
+
|
120
|
+
def destroy
|
121
|
+
process_rapitapir_endpoint
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def setup_users_data
|
127
|
+
@users = {
|
128
|
+
1 => { id: 1, name: 'John Doe', email: 'john@example.com' },
|
129
|
+
2 => { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
|
130
|
+
}
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Example config/routes.rb content:
|
135
|
+
#
|
136
|
+
# Rails.application.routes.draw do
|
137
|
+
# resources :users, only: [:index, :show, :create, :update, :destroy]
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# Or using the RailsAdapter for automatic route generation:
|
141
|
+
#
|
142
|
+
# adapter = RapiTapir::Server::Rails::RailsAdapter.new
|
143
|
+
# adapter.register_endpoint(list_endpoint, UsersController, :index)
|
144
|
+
# adapter.register_endpoint(show_endpoint, UsersController, :show)
|
145
|
+
# # ... etc
|
146
|
+
# adapter.generate_routes(Rails.application.routes)
|
@@ -0,0 +1,128 @@
|
|
1
|
+
#require 'rapitapir' # Only one require needed!
|
2
|
+
require 'sinatra/base'
|
3
|
+
require_relative '../../lib/rapitapir'
|
4
|
+
|
5
|
+
# Simple Book model for demonstration
|
6
|
+
class Book
|
7
|
+
attr_reader :id, :title, :author, :published, :isbn, :pages
|
8
|
+
|
9
|
+
@@books = [
|
10
|
+
{ id: 1, title: "The Ruby Programming Language", author: "Matz", published: true, isbn: "978-0596516178", pages: 448 },
|
11
|
+
{ id: 2, title: "Effective Ruby", author: "Peter Jones", published: true, isbn: "978-0134045993", pages: 256 },
|
12
|
+
{ id: 3, title: "Ruby Under a Microscope", author: "Pat Shaughnessy", published: true, isbn: "978-1593275273", pages: 360 }
|
13
|
+
]
|
14
|
+
|
15
|
+
def self.all
|
16
|
+
@@books
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.find(id)
|
20
|
+
@@books.find { |book| book[:id] == id.to_i }
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.create(attributes)
|
24
|
+
new_id = (@@books.map { |b| b[:id] }.max || 0) + 1
|
25
|
+
new_book = attributes.merge(id: new_id)
|
26
|
+
@@books << new_book
|
27
|
+
new_book
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.search(query)
|
31
|
+
results = @@books.select do |book|
|
32
|
+
book[:title].downcase.include?(query.downcase) ||
|
33
|
+
book[:author].downcase.include?(query.downcase)
|
34
|
+
end
|
35
|
+
BookCollection.new(results)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.where(conditions)
|
39
|
+
results = @@books.select do |book|
|
40
|
+
conditions.all? { |key, value| book[key] == value }
|
41
|
+
end
|
42
|
+
BookCollection.new(results)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Simple collection class to handle chaining
|
47
|
+
class BookCollection
|
48
|
+
def initialize(books)
|
49
|
+
@books = books
|
50
|
+
end
|
51
|
+
|
52
|
+
def limit(count)
|
53
|
+
BookCollection.new(@books.first(count))
|
54
|
+
end
|
55
|
+
|
56
|
+
def map(&block)
|
57
|
+
@books.map(&block)
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_a
|
61
|
+
@books
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class BookAPI < SinatraRapiTapir
|
66
|
+
# Configure API information
|
67
|
+
rapitapir do
|
68
|
+
info(
|
69
|
+
title: 'Book API',
|
70
|
+
description: 'A simple book management API',
|
71
|
+
version: '1.0.0'
|
72
|
+
)
|
73
|
+
development_defaults! # Auto CORS, docs, health checks
|
74
|
+
end
|
75
|
+
|
76
|
+
# Define your data schema with T shortcut (globally available!)
|
77
|
+
BOOK_SCHEMA = T.hash({
|
78
|
+
"id" => T.integer,
|
79
|
+
"title" => T.string(min_length: 1, max_length: 255),
|
80
|
+
"author" => T.string(min_length: 1),
|
81
|
+
"published" => T.boolean,
|
82
|
+
"isbn" => T.optional(T.string),
|
83
|
+
"pages" => T.optional(T.integer(minimum: 1))
|
84
|
+
})
|
85
|
+
|
86
|
+
# Define endpoints with the elegant resource DSL and enhanced HTTP verbs
|
87
|
+
api_resource '/books', schema: BOOK_SCHEMA do
|
88
|
+
crud do
|
89
|
+
index { Book.all }
|
90
|
+
|
91
|
+
show do |inputs|
|
92
|
+
book = Book.find(inputs[:id])
|
93
|
+
halt(404, { error: 'Book not found' }.to_json) unless book
|
94
|
+
book
|
95
|
+
end
|
96
|
+
|
97
|
+
create do |inputs|
|
98
|
+
Book.create(inputs[:body])
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Custom endpoint using enhanced DSL
|
103
|
+
custom :get, 'featured' do
|
104
|
+
Book.where(featured: true).to_a
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Alternative endpoint definition using enhanced HTTP verb DSL
|
109
|
+
endpoint(
|
110
|
+
GET('/api/books/search')
|
111
|
+
.query(:q, T.string(min_length: 1), description: 'Search query')
|
112
|
+
.query(:limit, T.optional(T.integer(minimum: 1, maximum: 100)), description: 'Results limit')
|
113
|
+
.summary('Search books')
|
114
|
+
.description('Search books by title or author')
|
115
|
+
.tags('Search')
|
116
|
+
.ok(T.array(BOOK_SCHEMA))
|
117
|
+
.bad_request(T.hash({ "error" => T.string }), description: 'Invalid search parameters')
|
118
|
+
.build
|
119
|
+
) do |inputs|
|
120
|
+
query = inputs[:q]
|
121
|
+
limit = inputs[:limit] || 20
|
122
|
+
|
123
|
+
books = Book.search(query).limit(limit)
|
124
|
+
books.to_a
|
125
|
+
end
|
126
|
+
|
127
|
+
run! if __FILE__ == $0
|
128
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../lib/rapitapir'
|
4
|
+
|
5
|
+
# Example demonstrating RapiTapir server functionality
|
6
|
+
class UserAPI
|
7
|
+
def initialize
|
8
|
+
@adapter = RapiTapir::Server::RackAdapter.new
|
9
|
+
@users = {
|
10
|
+
1 => { id: 1, name: 'John Doe', email: 'john@example.com' },
|
11
|
+
2 => { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
|
12
|
+
}
|
13
|
+
|
14
|
+
setup_middleware
|
15
|
+
setup_endpoints
|
16
|
+
end
|
17
|
+
|
18
|
+
def app
|
19
|
+
@adapter
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def setup_middleware
|
25
|
+
# Add CORS middleware
|
26
|
+
@adapter.use(RapiTapir::Server::Middleware::CORS, {
|
27
|
+
allow_origin: '*',
|
28
|
+
allow_methods: %w[GET POST PUT DELETE OPTIONS],
|
29
|
+
allow_headers: %w[Content-Type Authorization]
|
30
|
+
})
|
31
|
+
|
32
|
+
# Add logging middleware
|
33
|
+
@adapter.use(RapiTapir::Server::Middleware::Logger)
|
34
|
+
end
|
35
|
+
|
36
|
+
def setup_endpoints
|
37
|
+
setup_list_users
|
38
|
+
setup_get_user
|
39
|
+
setup_create_user
|
40
|
+
setup_update_user
|
41
|
+
setup_delete_user
|
42
|
+
end
|
43
|
+
|
44
|
+
def setup_list_users
|
45
|
+
endpoint = RapiTapir.get('/users')
|
46
|
+
.summary('List all users')
|
47
|
+
.description('Returns a list of all users in the system')
|
48
|
+
.out(RapiTapir::Core::Output.new(kind: :json,
|
49
|
+
type: { users: [{
|
50
|
+
id: :integer, name: :string, email: :string
|
51
|
+
}] }))
|
52
|
+
|
53
|
+
@adapter.register_endpoint(endpoint, method(:list_users))
|
54
|
+
end
|
55
|
+
|
56
|
+
def setup_get_user
|
57
|
+
endpoint = RapiTapir.get('/users/:id')
|
58
|
+
.summary('Get user by ID')
|
59
|
+
.description('Returns a single user by their ID')
|
60
|
+
.in(RapiTapir::Core::Input.new(kind: :path, name: :id, type: :integer))
|
61
|
+
.out(RapiTapir::Core::Output.new(kind: :json,
|
62
|
+
type: {
|
63
|
+
id: :integer, name: :string, email: :string
|
64
|
+
}))
|
65
|
+
|
66
|
+
@adapter.register_endpoint(endpoint, method(:get_user))
|
67
|
+
end
|
68
|
+
|
69
|
+
def setup_create_user
|
70
|
+
endpoint = RapiTapir.post('/users')
|
71
|
+
.summary('Create a new user')
|
72
|
+
.description('Creates a new user with the provided data')
|
73
|
+
.in(RapiTapir::Core::Input.new(kind: :body, name: :user_data, type: Hash))
|
74
|
+
.out(RapiTapir::Core::Output.new(kind: :json,
|
75
|
+
type: {
|
76
|
+
id: :integer, name: :string, email: :string
|
77
|
+
}))
|
78
|
+
|
79
|
+
@adapter.register_endpoint(endpoint, method(:create_user))
|
80
|
+
end
|
81
|
+
|
82
|
+
def setup_update_user
|
83
|
+
endpoint = RapiTapir.put('/users/:id')
|
84
|
+
.summary('Update an existing user')
|
85
|
+
.description('Updates an existing user with the provided data')
|
86
|
+
.in(RapiTapir::Core::Input.new(kind: :path, name: :id, type: :integer))
|
87
|
+
.in(RapiTapir::Core::Input.new(kind: :body, name: :user_data, type: Hash))
|
88
|
+
.out(RapiTapir::Core::Output.new(kind: :json,
|
89
|
+
type: {
|
90
|
+
id: :integer, name: :string, email: :string
|
91
|
+
}))
|
92
|
+
|
93
|
+
@adapter.register_endpoint(endpoint, method(:update_user))
|
94
|
+
end
|
95
|
+
|
96
|
+
def setup_delete_user
|
97
|
+
endpoint = RapiTapir.delete('/users/:id')
|
98
|
+
.summary('Delete a user')
|
99
|
+
.description('Deletes a user by their ID')
|
100
|
+
.in(RapiTapir::Core::Input.new(kind: :path, name: :id, type: :integer))
|
101
|
+
.out(RapiTapir::Core::Output.new(kind: :json, type: { message: :string }))
|
102
|
+
|
103
|
+
@adapter.register_endpoint(endpoint, method(:delete_user))
|
104
|
+
end
|
105
|
+
|
106
|
+
# Handler methods
|
107
|
+
def list_users(_inputs)
|
108
|
+
{ users: @users.values }
|
109
|
+
end
|
110
|
+
|
111
|
+
def get_user(inputs)
|
112
|
+
user_id = inputs[:id]
|
113
|
+
user = @users[user_id]
|
114
|
+
|
115
|
+
raise ArgumentError, 'User not found' unless user
|
116
|
+
|
117
|
+
user
|
118
|
+
end
|
119
|
+
|
120
|
+
def create_user(inputs)
|
121
|
+
user_data = inputs[:user_data]
|
122
|
+
new_id = (@users.keys.max || 0) + 1
|
123
|
+
|
124
|
+
new_user = {
|
125
|
+
id: new_id,
|
126
|
+
name: user_data['name'],
|
127
|
+
email: user_data['email']
|
128
|
+
}
|
129
|
+
|
130
|
+
@users[new_id] = new_user
|
131
|
+
new_user
|
132
|
+
end
|
133
|
+
|
134
|
+
def update_user(inputs)
|
135
|
+
user_id = inputs[:id]
|
136
|
+
user_data = inputs[:user_data]
|
137
|
+
|
138
|
+
raise ArgumentError, 'User not found' unless @users[user_id]
|
139
|
+
|
140
|
+
@users[user_id].merge!(
|
141
|
+
name: user_data['name'] || @users[user_id][:name],
|
142
|
+
email: user_data['email'] || @users[user_id][:email]
|
143
|
+
)
|
144
|
+
|
145
|
+
@users[user_id]
|
146
|
+
end
|
147
|
+
|
148
|
+
def delete_user(inputs)
|
149
|
+
user_id = inputs[:id]
|
150
|
+
|
151
|
+
raise ArgumentError, 'User not found' unless @users[user_id]
|
152
|
+
|
153
|
+
@users.delete(user_id)
|
154
|
+
{ message: "User #{user_id} deleted successfully" }
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Example usage:
|
159
|
+
if __FILE__ == $PROGRAM_NAME
|
160
|
+
require 'rack'
|
161
|
+
|
162
|
+
user_api = UserAPI.new
|
163
|
+
|
164
|
+
puts 'Starting RapiTapir User API server on http://localhost:9292'
|
165
|
+
puts 'Available endpoints:'
|
166
|
+
puts ' GET /users - List all users'
|
167
|
+
puts ' GET /users/:id - Get user by ID'
|
168
|
+
puts ' POST /users - Create new user'
|
169
|
+
puts ' PUT /users/:id - Update user'
|
170
|
+
puts ' DELETE /users/:id - Delete user'
|
171
|
+
puts ''
|
172
|
+
puts 'Example requests:'
|
173
|
+
puts ' curl http://localhost:9292/users'
|
174
|
+
puts ' curl http://localhost:9292/users/1'
|
175
|
+
puts " curl -X POST http://localhost:9292/users -H 'Content-Type: application/json' " \
|
176
|
+
"-d '{\"name\":\"Bob\",\"email\":\"bob@example.com\"}'"
|
177
|
+
|
178
|
+
Rack::Handler::WEBrick.run(user_api.app, Port: 9292)
|
179
|
+
end
|