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,44 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
puts "=== RapiTapir Auto-Derivation Examples ==="
|
5
|
+
|
6
|
+
require_relative '../lib/rapitapir/types'
|
7
|
+
|
8
|
+
# 1. JSON Schema Example
|
9
|
+
puts "\n1. JSON Schema Auto-Derivation"
|
10
|
+
puts "-" * 30
|
11
|
+
|
12
|
+
user_schema = {
|
13
|
+
"type" => "object",
|
14
|
+
"properties" => {
|
15
|
+
"name" => { "type" => "string" },
|
16
|
+
"age" => { "type" => "integer" },
|
17
|
+
"active" => { "type" => "boolean" }
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
result = RapiTapir::Types.from_json_schema(user_schema)
|
22
|
+
puts "ā
Derived schema from JSON Schema"
|
23
|
+
puts "Fields: #{result.field_types.keys.join(', ')}"
|
24
|
+
|
25
|
+
# 2. OpenStruct Example
|
26
|
+
puts "\n2. OpenStruct Auto-Derivation"
|
27
|
+
puts "-" * 30
|
28
|
+
|
29
|
+
require 'ostruct'
|
30
|
+
config = OpenStruct.new(
|
31
|
+
host: "api.example.com",
|
32
|
+
port: 443,
|
33
|
+
ssl: true
|
34
|
+
)
|
35
|
+
|
36
|
+
result = RapiTapir::Types.from_open_struct(config)
|
37
|
+
puts "ā
Derived schema from OpenStruct"
|
38
|
+
puts "Fields: #{result.field_types.keys.join(', ')}"
|
39
|
+
|
40
|
+
puts "\nš Auto-derivation working for structured data sources!"
|
41
|
+
puts "š” Focus on sources with explicit type information:"
|
42
|
+
puts " - JSON Schema (API contracts)"
|
43
|
+
puts " - OpenStruct (configuration objects)"
|
44
|
+
puts " - Protobuf (when available)"
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/rapitapir'
|
4
|
+
include RapiTapir::DSL
|
5
|
+
|
6
|
+
# Simple API endpoints for demonstration
|
7
|
+
RapiTapir.get('/users')
|
8
|
+
.out(json_body([{ id: :integer, name: :string, email: :string }]))
|
9
|
+
.summary('List all users')
|
10
|
+
.description('Retrieve a paginated list of all users in the system')
|
11
|
+
|
12
|
+
RapiTapir.post('/users')
|
13
|
+
.in(body({ name: :string, email: :string, password: :string }))
|
14
|
+
.out(status_code(201), json_body({ id: :integer, name: :string, email: :string }))
|
15
|
+
.error_out(400, json_body({ error: :string, validation_errors: [:string] }))
|
16
|
+
.error_out(422, json_body({ error: :string, message: :string }))
|
17
|
+
.summary('Create new user')
|
18
|
+
.description('Create a new user account with the provided information')
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sinatra'
|
4
|
+
require 'json'
|
5
|
+
require_relative '../../lib/rapitapir'
|
6
|
+
require_relative '../../lib/rapitapir/server/sinatra_adapter'
|
7
|
+
|
8
|
+
# Example Sinatra app using RapiTapir
|
9
|
+
class UserSinatraApp < Sinatra::Base
|
10
|
+
def initialize
|
11
|
+
super
|
12
|
+
@users = {
|
13
|
+
1 => { id: 1, name: 'John Doe', email: 'john@example.com' },
|
14
|
+
2 => { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
|
15
|
+
}
|
16
|
+
|
17
|
+
setup_rapitapir_endpoints
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def setup_rapitapir_endpoints
|
23
|
+
adapter = RapiTapir::Server::SinatraAdapter.new(self)
|
24
|
+
|
25
|
+
# List users endpoint
|
26
|
+
list_endpoint = RapiTapir.get('/users')
|
27
|
+
.summary('List all users')
|
28
|
+
.out(RapiTapir::Core::Output.new(kind: :json, type: { users: Array }))
|
29
|
+
|
30
|
+
adapter.register_endpoint(list_endpoint) do |_inputs|
|
31
|
+
{ users: @users.values }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get user by ID endpoint
|
35
|
+
get_endpoint = RapiTapir.get('/users/:id')
|
36
|
+
.summary('Get user by ID')
|
37
|
+
.in(RapiTapir::Core::Input.new(kind: :path, name: :id, type: :integer))
|
38
|
+
.out(RapiTapir::Core::Output.new(kind: :json,
|
39
|
+
type: {
|
40
|
+
id: :integer, name: :string, email: :string
|
41
|
+
}))
|
42
|
+
|
43
|
+
adapter.register_endpoint(get_endpoint) do |inputs|
|
44
|
+
user_id = inputs[:id]
|
45
|
+
user = @users[user_id]
|
46
|
+
|
47
|
+
raise ArgumentError, 'User not found' unless user
|
48
|
+
|
49
|
+
user
|
50
|
+
end
|
51
|
+
|
52
|
+
# Create user endpoint
|
53
|
+
create_endpoint = RapiTapir.post('/users')
|
54
|
+
.summary('Create a new user')
|
55
|
+
.in(RapiTapir::Core::Input.new(kind: :body, name: :user_data, type: Hash))
|
56
|
+
.out(RapiTapir::Core::Output.new(kind: :json,
|
57
|
+
type: {
|
58
|
+
id: :integer, name: :string, email: :string
|
59
|
+
}))
|
60
|
+
|
61
|
+
adapter.register_endpoint(create_endpoint) do |inputs|
|
62
|
+
user_data = inputs[:user_data]
|
63
|
+
new_id = (@users.keys.max || 0) + 1
|
64
|
+
|
65
|
+
new_user = {
|
66
|
+
id: new_id,
|
67
|
+
name: user_data['name'],
|
68
|
+
email: user_data['email']
|
69
|
+
}
|
70
|
+
|
71
|
+
@users[new_id] = new_user
|
72
|
+
new_user
|
73
|
+
end
|
74
|
+
|
75
|
+
# Update user endpoint
|
76
|
+
update_endpoint = RapiTapir.put('/users/:id')
|
77
|
+
.summary('Update an existing user')
|
78
|
+
.in(RapiTapir::Core::Input.new(kind: :path, name: :id, type: :integer))
|
79
|
+
.in(RapiTapir::Core::Input.new(kind: :body, name: :user_data, type: Hash))
|
80
|
+
.out(RapiTapir::Core::Output.new(kind: :json,
|
81
|
+
type: {
|
82
|
+
id: :integer, name: :string, email: :string
|
83
|
+
}))
|
84
|
+
|
85
|
+
adapter.register_endpoint(update_endpoint) do |inputs|
|
86
|
+
user_id = inputs[:id]
|
87
|
+
user_data = inputs[:user_data]
|
88
|
+
|
89
|
+
raise ArgumentError, 'User not found' unless @users[user_id]
|
90
|
+
|
91
|
+
@users[user_id].merge!(
|
92
|
+
name: user_data['name'] || @users[user_id][:name],
|
93
|
+
email: user_data['email'] || @users[user_id][:email]
|
94
|
+
)
|
95
|
+
|
96
|
+
@users[user_id]
|
97
|
+
end
|
98
|
+
|
99
|
+
# Delete user endpoint
|
100
|
+
delete_endpoint = RapiTapir.delete('/users/:id')
|
101
|
+
.summary('Delete a user')
|
102
|
+
.in(RapiTapir::Core::Input.new(kind: :path, name: :id, type: :integer))
|
103
|
+
.out(RapiTapir::Core::Output.new(kind: :json, type: { message: :string }))
|
104
|
+
|
105
|
+
adapter.register_endpoint(delete_endpoint) do |inputs|
|
106
|
+
user_id = inputs[:id]
|
107
|
+
|
108
|
+
raise ArgumentError, 'User not found' unless @users[user_id]
|
109
|
+
|
110
|
+
@users.delete(user_id)
|
111
|
+
{ message: "User #{user_id} deleted successfully" }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Run the app if this file is executed directly
|
117
|
+
if __FILE__ == $PROGRAM_NAME
|
118
|
+
puts 'Starting RapiTapir Sinatra User API server on http://localhost:4567'
|
119
|
+
puts 'Available endpoints:'
|
120
|
+
puts ' GET /users - List all users'
|
121
|
+
puts ' GET /users/:id - Get user by ID'
|
122
|
+
puts ' POST /users - Create new user'
|
123
|
+
puts ' PUT /users/:id - Update user'
|
124
|
+
puts ' DELETE /users/:id - Delete user'
|
125
|
+
|
126
|
+
UserSinatraApp.run!
|
127
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Demonstration: T shortcut works immediately without any setup
|
5
|
+
|
6
|
+
require_relative '../lib/rapitapir'
|
7
|
+
require_relative '../lib/rapitapir/sinatra_rapitapir'
|
8
|
+
|
9
|
+
puts "šÆ Testing automatic T shortcut availability..."
|
10
|
+
puts
|
11
|
+
|
12
|
+
# Test 1: Top-level usage
|
13
|
+
puts "ā
Top-level usage:"
|
14
|
+
BOOK_SCHEMA = T.hash({
|
15
|
+
"id" => T.integer,
|
16
|
+
"title" => T.string(min_length: 1),
|
17
|
+
"published" => T.boolean
|
18
|
+
})
|
19
|
+
puts " BOOK_SCHEMA = T.hash(...) ā"
|
20
|
+
puts " Schema type: #{BOOK_SCHEMA.class}"
|
21
|
+
puts
|
22
|
+
|
23
|
+
# Test 2: Inside a class
|
24
|
+
puts "ā
Inside a class:"
|
25
|
+
class DemoAPI < SinatraRapiTapir
|
26
|
+
USER_SCHEMA = T.hash({
|
27
|
+
"name" => T.string,
|
28
|
+
"email" => T.email,
|
29
|
+
"age" => T.optional(T.integer(min: 0))
|
30
|
+
})
|
31
|
+
|
32
|
+
puts " USER_SCHEMA = T.hash(...) ā"
|
33
|
+
puts " Schema type: #{USER_SCHEMA.class}"
|
34
|
+
end
|
35
|
+
puts
|
36
|
+
|
37
|
+
# Test 3: Complex nested types
|
38
|
+
puts "ā
Complex nested types:"
|
39
|
+
COMPLEX_SCHEMA = T.hash({
|
40
|
+
"user" => T.hash({
|
41
|
+
"profile" => T.optional(T.hash({
|
42
|
+
"preferences" => T.array(T.string),
|
43
|
+
"settings" => T.hash({
|
44
|
+
"theme" => T.string,
|
45
|
+
"notifications" => T.boolean
|
46
|
+
})
|
47
|
+
}))
|
48
|
+
}),
|
49
|
+
"metadata" => T.hash({
|
50
|
+
"version" => T.string,
|
51
|
+
"timestamp" => T.datetime
|
52
|
+
})
|
53
|
+
})
|
54
|
+
puts " Complex nested schema created ā"
|
55
|
+
puts " Schema type: #{COMPLEX_SCHEMA.class}"
|
56
|
+
puts
|
57
|
+
|
58
|
+
puts "š All tests passed! T shortcut works everywhere automatically!"
|
59
|
+
puts "š No manual setup required - just `require 'rapitapir'` and use T.* anywhere!"
|
@@ -0,0 +1,190 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
require_relative '../lib/rapitapir'
|
5
|
+
|
6
|
+
# Example: User Management API
|
7
|
+
module UserAPI
|
8
|
+
extend RapiTapir::DSL
|
9
|
+
|
10
|
+
# GET /users - List all users
|
11
|
+
LIST_USERS = RapiTapir.get('/users')
|
12
|
+
.in(query(:page, :integer, optional: true))
|
13
|
+
.in(query(:limit, :integer, optional: true))
|
14
|
+
.in(query(:search, :string, optional: true))
|
15
|
+
.in(header(:authorization, :string))
|
16
|
+
.out(status_code(200))
|
17
|
+
.out(json_body({
|
18
|
+
users: [{ id: :integer, name: :string, email: :string }],
|
19
|
+
total: :integer,
|
20
|
+
page: :integer,
|
21
|
+
limit: :integer
|
22
|
+
}))
|
23
|
+
.error_out(401, json_body({ error: :string }))
|
24
|
+
.error_out(500, json_body({ error: :string }))
|
25
|
+
.description('Retrieve a paginated list of users')
|
26
|
+
.summary('List users')
|
27
|
+
.tag('users')
|
28
|
+
|
29
|
+
# GET /users/:id - Get specific user
|
30
|
+
GET_USER = RapiTapir.get('/users/:id')
|
31
|
+
.in(path_param(:id, :integer))
|
32
|
+
.in(header(:authorization, :string))
|
33
|
+
.out(status_code(200))
|
34
|
+
.out(json_body({ id: :integer, name: :string, email: :string, created_at: :datetime }))
|
35
|
+
.error_out(401, json_body({ error: :string }))
|
36
|
+
.error_out(404, json_body({ error: :string }))
|
37
|
+
.error_out(500, json_body({ error: :string }))
|
38
|
+
.description('Retrieve a specific user by ID')
|
39
|
+
.summary('Get user')
|
40
|
+
.tag('users')
|
41
|
+
|
42
|
+
# POST /users - Create new user
|
43
|
+
CREATE_USER = RapiTapir.post('/users')
|
44
|
+
.in(header(:authorization, :string))
|
45
|
+
.in(header(:'content-type', :string))
|
46
|
+
.in(body({ name: :string, email: :string, password: :string }))
|
47
|
+
.out(status_code(201))
|
48
|
+
.out(json_body({ id: :integer, name: :string, email: :string, created_at: :datetime }))
|
49
|
+
.error_out(400, json_body({ error: :string, details: :string }))
|
50
|
+
.error_out(401, json_body({ error: :string }))
|
51
|
+
.error_out(422, json_body({ error: :string,
|
52
|
+
validation_errors: [{ field: :string, message: :string }] }))
|
53
|
+
.error_out(500, json_body({ error: :string }))
|
54
|
+
.description('Create a new user account')
|
55
|
+
.summary('Create user')
|
56
|
+
.tag('users')
|
57
|
+
|
58
|
+
# PUT /users/:id - Update user
|
59
|
+
UPDATE_USER = RapiTapir.put('/users/:id')
|
60
|
+
.in(path_param(:id, :integer))
|
61
|
+
.in(header(:authorization, :string))
|
62
|
+
.in(header(:'content-type', :string))
|
63
|
+
.in(body({ name: :string, email: :string }))
|
64
|
+
.out(status_code(200))
|
65
|
+
.out(json_body({ id: :integer, name: :string, email: :string, updated_at: :datetime }))
|
66
|
+
.error_out(400, json_body({ error: :string, details: :string }))
|
67
|
+
.error_out(401, json_body({ error: :string }))
|
68
|
+
.error_out(404, json_body({ error: :string }))
|
69
|
+
.error_out(422, json_body({ error: :string,
|
70
|
+
validation_errors: [{ field: :string, message: :string }] }))
|
71
|
+
.error_out(500, json_body({ error: :string }))
|
72
|
+
.description('Update an existing user')
|
73
|
+
.summary('Update user')
|
74
|
+
.tag('users')
|
75
|
+
|
76
|
+
# DELETE /users/:id - Delete user
|
77
|
+
DELETE_USER = RapiTapir.delete('/users/:id')
|
78
|
+
.in(path_param(:id, :integer))
|
79
|
+
.in(header(:authorization, :string))
|
80
|
+
.out(status_code(204))
|
81
|
+
.error_out(401, json_body({ error: :string }))
|
82
|
+
.error_out(404, json_body({ error: :string }))
|
83
|
+
.error_out(500, json_body({ error: :string }))
|
84
|
+
.description('Delete a user account')
|
85
|
+
.summary('Delete user')
|
86
|
+
.tag('users')
|
87
|
+
|
88
|
+
# POST /users/login - User authentication
|
89
|
+
LOGIN_USER = RapiTapir.post('/users/login')
|
90
|
+
.in(header(:'content-type', :string))
|
91
|
+
.in(body({ email: :string, password: :string }))
|
92
|
+
.out(status_code(200))
|
93
|
+
.out(json_body({
|
94
|
+
user: { id: :integer, name: :string, email: :string },
|
95
|
+
token: :string,
|
96
|
+
expires_at: :datetime
|
97
|
+
}))
|
98
|
+
.error_out(400, json_body({ error: :string }))
|
99
|
+
.error_out(401, json_body({ error: :string }))
|
100
|
+
.error_out(500, json_body({ error: :string }))
|
101
|
+
.description('Authenticate user and return access token')
|
102
|
+
.summary('User login')
|
103
|
+
.tag('authentication')
|
104
|
+
|
105
|
+
# Example usage and validation
|
106
|
+
def self.demo
|
107
|
+
puts '=== RapiTapir User API Demo ==='
|
108
|
+
puts
|
109
|
+
|
110
|
+
# Example 1: Valid user creation
|
111
|
+
puts '1. Valid user creation:'
|
112
|
+
input_data = {
|
113
|
+
body: { name: 'John Doe', email: 'john@example.com', password: 'secret123' }
|
114
|
+
}
|
115
|
+
output_data = {
|
116
|
+
id: 1,
|
117
|
+
name: 'John Doe',
|
118
|
+
email: 'john@example.com',
|
119
|
+
created_at: DateTime.now
|
120
|
+
}
|
121
|
+
|
122
|
+
begin
|
123
|
+
CREATE_USER.validate!(input_data, output_data)
|
124
|
+
puts 'ā Validation passed'
|
125
|
+
rescue StandardError => e
|
126
|
+
puts "ā Validation failed: #{e.message}"
|
127
|
+
end
|
128
|
+
puts
|
129
|
+
|
130
|
+
# Example 2: Invalid user creation (missing required field)
|
131
|
+
puts '2. Invalid user creation (missing password):'
|
132
|
+
invalid_input = {
|
133
|
+
body: { name: 'John Doe', email: 'john@example.com' }
|
134
|
+
}
|
135
|
+
|
136
|
+
begin
|
137
|
+
CREATE_USER.validate!(invalid_input, output_data)
|
138
|
+
puts 'ā Validation passed'
|
139
|
+
rescue StandardError => e
|
140
|
+
puts "ā Validation failed: #{e.message}"
|
141
|
+
end
|
142
|
+
puts
|
143
|
+
|
144
|
+
# Example 3: Valid user listing
|
145
|
+
puts '3. Valid user listing:'
|
146
|
+
list_input = {}
|
147
|
+
list_output = {
|
148
|
+
users: [
|
149
|
+
{ id: 1, name: 'John Doe', email: 'john@example.com' },
|
150
|
+
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
|
151
|
+
],
|
152
|
+
total: 2,
|
153
|
+
page: 1,
|
154
|
+
limit: 10
|
155
|
+
}
|
156
|
+
|
157
|
+
begin
|
158
|
+
LIST_USERS.validate!(list_input, list_output)
|
159
|
+
puts 'ā Validation passed'
|
160
|
+
rescue StandardError => e
|
161
|
+
puts "ā Validation failed: #{e.message}"
|
162
|
+
end
|
163
|
+
puts
|
164
|
+
|
165
|
+
# Example 4: Endpoint metadata
|
166
|
+
puts '4. Endpoint metadata:'
|
167
|
+
puts 'GET /users:'
|
168
|
+
puts " Description: #{GET_USER.metadata[:description]}"
|
169
|
+
puts " Summary: #{GET_USER.metadata[:summary]}"
|
170
|
+
puts " Tag: #{GET_USER.metadata[:tag]}"
|
171
|
+
puts " Inputs: #{GET_USER.inputs.length}"
|
172
|
+
puts " Outputs: #{GET_USER.outputs.length}"
|
173
|
+
puts " Error responses: #{GET_USER.errors.length}"
|
174
|
+
puts
|
175
|
+
|
176
|
+
# Example 5: Serialization
|
177
|
+
puts '5. Output serialization:'
|
178
|
+
json_output = CREATE_USER.outputs.find { |o| o.kind == :json }
|
179
|
+
if json_output
|
180
|
+
serialized = json_output.serialize(output_data)
|
181
|
+
puts "JSON: #{serialized}"
|
182
|
+
end
|
183
|
+
puts
|
184
|
+
|
185
|
+
puts '=== Demo Complete ==='
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Run the demo if this file is executed directly
|
190
|
+
UserAPI.demo if __FILE__ == $PROGRAM_NAME
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RapiTapir Sinatra Extension - WORKING Getting Started Example
|
4
|
+
#
|
5
|
+
# This is a simplified version that actually works with Sinatra!
|
6
|
+
|
7
|
+
# Check for Sinatra availability
|
8
|
+
begin
|
9
|
+
require 'sinatra/base'
|
10
|
+
SINATRA_AVAILABLE = true
|
11
|
+
rescue LoadError
|
12
|
+
SINATRA_AVAILABLE = false
|
13
|
+
puts 'ā ļø Sinatra not available. Install with: gem install sinatra'
|
14
|
+
puts 'š Running in demo mode instead...'
|
15
|
+
end
|
16
|
+
|
17
|
+
require_relative '../lib/rapitapir'
|
18
|
+
|
19
|
+
# Only require SinatraAdapter if Sinatra is available
|
20
|
+
if SINATRA_AVAILABLE
|
21
|
+
require_relative '../lib/rapitapir/server/sinatra_adapter'
|
22
|
+
require_relative '../lib/rapitapir/openapi/schema_generator'
|
23
|
+
require_relative '../lib/rapitapir/sinatra/swagger_ui_generator'
|
24
|
+
end
|
25
|
+
|
26
|
+
# Simple in-memory data store
|
27
|
+
class BookStore
|
28
|
+
@@books = [
|
29
|
+
{ id: 1, title: 'The Ruby Programming Language', author: 'Matz', isbn: '978-0596516178', published: true },
|
30
|
+
{ id: 2, title: 'Metaprogramming Ruby', author: 'Paolo Perrotta', isbn: '978-1934356470', published: true }
|
31
|
+
]
|
32
|
+
@@next_id = 3
|
33
|
+
|
34
|
+
def self.all
|
35
|
+
@@books
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.find(id)
|
39
|
+
@@books.find { |book| book[:id] == id.to_i }
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.create(attrs)
|
43
|
+
book = attrs.merge(id: @@next_id)
|
44
|
+
@@next_id += 1
|
45
|
+
@@books << book
|
46
|
+
book
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.published
|
50
|
+
@@books.select { |book| book[:published] }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Define the book schema
|
55
|
+
BOOK_SCHEMA = RapiTapir::Types.hash({
|
56
|
+
'id' => RapiTapir::Types.integer,
|
57
|
+
'title' => RapiTapir::Types.string,
|
58
|
+
'author' => RapiTapir::Types.string,
|
59
|
+
'isbn' => RapiTapir::Types.string,
|
60
|
+
'published' => RapiTapir::Types.boolean
|
61
|
+
})
|
62
|
+
|
63
|
+
# WORKING Bookstore API - simplified but effective!
|
64
|
+
if SINATRA_AVAILABLE
|
65
|
+
class WorkingBookstoreAPI < Sinatra::Base
|
66
|
+
configure do
|
67
|
+
# Create the RapiTapir adapter - this is the key!
|
68
|
+
set :rapitapir, RapiTapir::Server::SinatraAdapter.new(self)
|
69
|
+
puts 'š RapiTapir integration enabled'
|
70
|
+
end
|
71
|
+
|
72
|
+
# OpenAPI JSON endpoint - clean and simple
|
73
|
+
get '/openapi.json' do
|
74
|
+
content_type :json
|
75
|
+
endpoints = settings.rapitapir.endpoints.map { |ep_data| ep_data[:endpoint] }
|
76
|
+
|
77
|
+
generator = RapiTapir::OpenAPI::SchemaGenerator.new(
|
78
|
+
endpoints: endpoints,
|
79
|
+
info: {
|
80
|
+
title: 'WORKING Bookstore API',
|
81
|
+
description: 'A simple bookstore API built with RapiTapir and Sinatra - WORKING VERSION!',
|
82
|
+
version: '1.0.0'
|
83
|
+
},
|
84
|
+
servers: [{ url: 'http://localhost:4567', description: 'Development server' }]
|
85
|
+
)
|
86
|
+
|
87
|
+
generator.to_json
|
88
|
+
end
|
89
|
+
|
90
|
+
# Swagger UI documentation endpoint - clean and simple
|
91
|
+
get '/docs' do
|
92
|
+
content_type :html
|
93
|
+
api_info = {
|
94
|
+
title: 'WORKING Bookstore API',
|
95
|
+
description: 'A simple bookstore API built with RapiTapir and Sinatra - WORKING VERSION!'
|
96
|
+
}
|
97
|
+
|
98
|
+
RapiTapir::Sinatra::SwaggerUIGenerator.new('/openapi.json', api_info).generate
|
99
|
+
end
|
100
|
+
|
101
|
+
# Health check - super simple
|
102
|
+
health_endpoint = RapiTapir.get('/health')
|
103
|
+
.summary('Health check')
|
104
|
+
.ok(RapiTapir::Types.hash({ 'status' => RapiTapir::Types.string }))
|
105
|
+
.build
|
106
|
+
|
107
|
+
settings.rapitapir.register_endpoint(health_endpoint) { { status: 'healthy' } }
|
108
|
+
|
109
|
+
# List all books
|
110
|
+
list_books_endpoint = RapiTapir.get('/books')
|
111
|
+
.summary('List all books')
|
112
|
+
.ok(RapiTapir::Types.array(BOOK_SCHEMA))
|
113
|
+
.build
|
114
|
+
|
115
|
+
settings.rapitapir.register_endpoint(list_books_endpoint) { BookStore.all }
|
116
|
+
|
117
|
+
# Published books only - custom endpoint (define BEFORE :id route)
|
118
|
+
published_books_endpoint = RapiTapir.get('/books/published')
|
119
|
+
.summary('Get published books only')
|
120
|
+
.ok(RapiTapir::Types.array(BOOK_SCHEMA))
|
121
|
+
.build
|
122
|
+
|
123
|
+
settings.rapitapir.register_endpoint(published_books_endpoint) { BookStore.published }
|
124
|
+
|
125
|
+
# Get book by ID (must come AFTER specific routes like /books/published)
|
126
|
+
get_book_endpoint = RapiTapir.get('/books/:id')
|
127
|
+
.summary('Get book by ID')
|
128
|
+
.path_param(:id, RapiTapir::Types.integer, description: 'Book ID')
|
129
|
+
.ok(BOOK_SCHEMA)
|
130
|
+
.build
|
131
|
+
|
132
|
+
settings.rapitapir.register_endpoint(get_book_endpoint) do |inputs|
|
133
|
+
book = BookStore.find(inputs[:id])
|
134
|
+
halt 404, { error: 'Book not found' }.to_json unless book
|
135
|
+
book
|
136
|
+
end
|
137
|
+
|
138
|
+
configure :development do
|
139
|
+
puts "\nš WORKING Bookstore API with RapiTapir"
|
140
|
+
puts 'š Health: http://localhost:4567/health'
|
141
|
+
puts 'š Books: http://localhost:4567/books'
|
142
|
+
puts 'š Published: http://localhost:4567/books/published'
|
143
|
+
puts 'š Book by ID: http://localhost:4567/books/1'
|
144
|
+
puts 'š Documentation: http://localhost:4567/docs'
|
145
|
+
puts 'š OpenAPI: http://localhost:4567/openapi.json'
|
146
|
+
puts "\nā
Zero boilerplate, working integration!"
|
147
|
+
puts 'š” Key: Direct use of SinatraAdapter.register_endpoint()'
|
148
|
+
puts 'š Full OpenAPI 3.0 documentation auto-generated!'
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
WorkingBookstoreAPI.run! if __FILE__ == $PROGRAM_NAME
|
153
|
+
else
|
154
|
+
# Demo mode when Sinatra is not available
|
155
|
+
puts "\nš RapiTapir Bookstore API - Demo Mode"
|
156
|
+
puts '=' * 50
|
157
|
+
|
158
|
+
puts "\nā
Successfully loaded:"
|
159
|
+
puts ' ⢠RapiTapir core'
|
160
|
+
puts ' ⢠Type system'
|
161
|
+
puts ' ⢠Book schema and store'
|
162
|
+
|
163
|
+
puts "\nš This working API provides:"
|
164
|
+
puts ' GET /health - Health check'
|
165
|
+
puts ' GET /books - List all books'
|
166
|
+
puts ' GET /books/:id - Get book by ID'
|
167
|
+
puts ' GET /books/published - Published books only'
|
168
|
+
|
169
|
+
puts "\nš§ Working pattern:"
|
170
|
+
puts ' ⢠Create SinatraAdapter: RapiTapir::Server::SinatraAdapter.new(self)'
|
171
|
+
puts ' ⢠Register endpoints: settings.rapitapir.register_endpoint(endpoint, &handler)'
|
172
|
+
puts " ⢠Build endpoints: RapiTapir.get('/path').summary('...').build"
|
173
|
+
puts ' ⢠Simple and effective!'
|
174
|
+
|
175
|
+
puts "\nš” To run the working server:"
|
176
|
+
puts ' gem install sinatra rack'
|
177
|
+
puts " ruby #{__FILE__}"
|
178
|
+
|
179
|
+
puts "\nš Sample usage with curl:"
|
180
|
+
puts ' curl http://localhost:4567/health'
|
181
|
+
puts ' curl http://localhost:4567/books'
|
182
|
+
puts ' curl http://localhost:4567/books/1'
|
183
|
+
puts ' curl http://localhost:4567/books/published'
|
184
|
+
end
|