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,192 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Example: Authentication and Security in RapiTapir
|
4
|
+
#
|
5
|
+
# This example demonstrates how to use RapiTapir's Phase 2.2 Authentication & Security features,
|
6
|
+
# including various authentication schemes, authorization middleware, and security features.
|
7
|
+
|
8
|
+
require_relative '../lib/rapitapir'
|
9
|
+
require 'ostruct'
|
10
|
+
|
11
|
+
# Helper method to create simple JWT (for demonstration only)
|
12
|
+
def create_simple_jwt(payload, secret)
|
13
|
+
require 'base64'
|
14
|
+
require 'json'
|
15
|
+
require 'openssl'
|
16
|
+
|
17
|
+
header = { 'alg' => 'HS256', 'typ' => 'JWT' }
|
18
|
+
|
19
|
+
encoded_header = Base64.urlsafe_encode64(JSON.generate(header)).tr('=', '')
|
20
|
+
encoded_payload = Base64.urlsafe_encode64(JSON.generate(payload)).tr('=', '')
|
21
|
+
|
22
|
+
signature = Base64.urlsafe_encode64(
|
23
|
+
OpenSSL::HMAC.digest('SHA256', secret, "#{encoded_header}.#{encoded_payload}")
|
24
|
+
).tr('=', '')
|
25
|
+
|
26
|
+
"#{encoded_header}.#{encoded_payload}.#{signature}"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Configure authentication globally
|
30
|
+
RapiTapir::Auth.configure do |config|
|
31
|
+
config.default_realm = 'My API'
|
32
|
+
config.jwt_secret = 'your-secret-key'
|
33
|
+
config.oauth2.client_id = 'your-client-id'
|
34
|
+
config.oauth2.client_secret = 'your-client-secret'
|
35
|
+
config.rate_limiting.requests_per_minute = 100
|
36
|
+
end
|
37
|
+
|
38
|
+
# Create authentication schemes
|
39
|
+
bearer_auth = RapiTapir::Auth.bearer_token(:bearer, {
|
40
|
+
token_validator: proc do |token|
|
41
|
+
# Validate token against your database or service
|
42
|
+
case token
|
43
|
+
when 'valid-token-123'
|
44
|
+
{
|
45
|
+
user: { id: 123, name: 'John Doe', email: 'john@example.com' },
|
46
|
+
scopes: %w[read write]
|
47
|
+
}
|
48
|
+
when 'admin-token-456'
|
49
|
+
{
|
50
|
+
user: { id: 456, name: 'Admin User', email: 'admin@example.com' },
|
51
|
+
scopes: %w[read write admin]
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
})
|
56
|
+
|
57
|
+
api_key_auth = RapiTapir::Auth.api_key(:api_key, {
|
58
|
+
header_name: 'X-API-Key',
|
59
|
+
key_validator: proc do |key|
|
60
|
+
# Validate API key
|
61
|
+
case key
|
62
|
+
when 'api-key-789'
|
63
|
+
{
|
64
|
+
user: { id: 'api-user', name: 'API Client' },
|
65
|
+
scopes: ['read']
|
66
|
+
}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
})
|
70
|
+
|
71
|
+
jwt_auth = RapiTapir::Auth.jwt(:jwt, {
|
72
|
+
secret: 'your-jwt-secret',
|
73
|
+
algorithm: 'HS256'
|
74
|
+
})
|
75
|
+
|
76
|
+
# Usage examples:
|
77
|
+
|
78
|
+
# 1. Test authentication manually
|
79
|
+
puts '=== Authentication Examples ==='
|
80
|
+
|
81
|
+
# Create a mock request for Bearer token
|
82
|
+
bearer_request = Struct.new(:env, :params, :headers).new(
|
83
|
+
{ 'HTTP_AUTHORIZATION' => 'Bearer valid-token-123' },
|
84
|
+
{},
|
85
|
+
{ 'authorization' => 'Bearer valid-token-123' }
|
86
|
+
)
|
87
|
+
|
88
|
+
context = bearer_auth.authenticate(bearer_request)
|
89
|
+
puts "Bearer auth result: #{context&.authenticated?} - User: #{context&.user}"
|
90
|
+
|
91
|
+
# Create a mock request for API key
|
92
|
+
api_key_request = Struct.new(:env, :params, :headers).new(
|
93
|
+
{ 'HTTP_X_API_KEY' => 'api-key-789' },
|
94
|
+
{},
|
95
|
+
{ 'x-api-key' => 'api-key-789' }
|
96
|
+
)
|
97
|
+
|
98
|
+
context = api_key_auth.authenticate(api_key_request)
|
99
|
+
puts "API key auth result: #{context&.authenticated?} - User: #{context&.user}"
|
100
|
+
|
101
|
+
# 2. Test JWT authentication
|
102
|
+
puts "\n=== JWT Authentication Example ==="
|
103
|
+
|
104
|
+
# Create a simple JWT token
|
105
|
+
jwt_payload = {
|
106
|
+
'sub' => 'user123',
|
107
|
+
'name' => 'JWT User',
|
108
|
+
'scopes' => %w[read write],
|
109
|
+
'exp' => Time.now.to_i + 3600
|
110
|
+
}
|
111
|
+
|
112
|
+
jwt_token = create_simple_jwt(jwt_payload, 'your-jwt-secret')
|
113
|
+
puts "Generated JWT: #{jwt_token[0..20]}..."
|
114
|
+
|
115
|
+
jwt_request = Struct.new(:env, :params, :headers).new(
|
116
|
+
{ 'HTTP_AUTHORIZATION' => "Bearer #{jwt_token}" },
|
117
|
+
{},
|
118
|
+
{ 'authorization' => "Bearer #{jwt_token}" }
|
119
|
+
)
|
120
|
+
|
121
|
+
context = jwt_auth.authenticate(jwt_request)
|
122
|
+
puts "JWT auth result: #{context&.authenticated?} - User: #{context&.user}"
|
123
|
+
|
124
|
+
# 3. Demonstrate context store
|
125
|
+
puts "\n=== Context Store Example ==="
|
126
|
+
|
127
|
+
test_context = RapiTapir::Auth::Context.new(
|
128
|
+
user: { id: 999, name: 'Test User' },
|
129
|
+
scopes: %w[read write],
|
130
|
+
token: 'test-token'
|
131
|
+
)
|
132
|
+
|
133
|
+
RapiTapir::Auth::ContextStore.with_context(test_context) do
|
134
|
+
puts "Current user: #{RapiTapir::Auth.current_user}"
|
135
|
+
puts "Authenticated: #{RapiTapir::Auth.authenticated?}"
|
136
|
+
puts "Has 'read' scope: #{RapiTapir::Auth.has_scope?('read')}"
|
137
|
+
puts "Has 'admin' scope: #{RapiTapir::Auth.has_scope?('admin')}"
|
138
|
+
end
|
139
|
+
|
140
|
+
puts "Context after block: #{RapiTapir::Auth.current_context}"
|
141
|
+
|
142
|
+
# 4. Test middleware functionality
|
143
|
+
puts "\n=== Middleware Examples ==="
|
144
|
+
|
145
|
+
# Test rate limiting storage
|
146
|
+
storage = RapiTapir::Auth::Middleware::RateLimitingMiddleware::MemoryStorage.new
|
147
|
+
storage.increment('test_key')
|
148
|
+
storage.increment('test_key')
|
149
|
+
puts "Rate limit count: #{storage.get('test_key')}"
|
150
|
+
|
151
|
+
# Test CORS functionality
|
152
|
+
cors_middleware = RapiTapir::Auth.cors_middleware({
|
153
|
+
allowed_origins: ['https://example.com'],
|
154
|
+
allowed_methods: %w[GET POST],
|
155
|
+
allow_credentials: true
|
156
|
+
})
|
157
|
+
|
158
|
+
puts "CORS middleware created: #{cors_middleware.class}"
|
159
|
+
|
160
|
+
# 5. Test authorization
|
161
|
+
puts "\n=== Authorization Examples ==="
|
162
|
+
|
163
|
+
admin_context = RapiTapir::Auth::Context.new(
|
164
|
+
user: { id: 123, name: 'Admin' },
|
165
|
+
scopes: %w[read write admin]
|
166
|
+
)
|
167
|
+
|
168
|
+
regular_context = RapiTapir::Auth::Context.new(
|
169
|
+
user: { id: 456, name: 'User' },
|
170
|
+
scopes: ['read']
|
171
|
+
)
|
172
|
+
|
173
|
+
RapiTapir::Auth::ContextStore.with_context(admin_context) do
|
174
|
+
puts "Admin user has admin scope: #{RapiTapir::Auth.has_scope?('admin')}"
|
175
|
+
end
|
176
|
+
|
177
|
+
RapiTapir::Auth::ContextStore.with_context(regular_context) do
|
178
|
+
puts "Regular user has admin scope: #{RapiTapir::Auth.has_scope?('admin')}"
|
179
|
+
end
|
180
|
+
|
181
|
+
puts "\n=== Phase 2.2 Authentication & Security System Complete ==="
|
182
|
+
puts '✅ Bearer Token Authentication'
|
183
|
+
puts '✅ API Key Authentication'
|
184
|
+
puts '✅ Basic Authentication'
|
185
|
+
puts '✅ OAuth2 Authentication'
|
186
|
+
puts '✅ JWT Authentication'
|
187
|
+
puts '✅ Authorization Middleware'
|
188
|
+
puts '✅ Rate Limiting'
|
189
|
+
puts '✅ CORS Support'
|
190
|
+
puts '✅ Security Headers'
|
191
|
+
puts '✅ Context Management'
|
192
|
+
puts '✅ Comprehensive Test Suite (91 tests)'
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This file demonstrates the improved auto-derivation capabilities that work
|
4
|
+
# well with Ruby's dynamic nature and lack of built-in type declarations
|
5
|
+
|
6
|
+
require_relative '../lib/rapitapir/types'
|
7
|
+
|
8
|
+
puts "=== RapiTapir Auto-Derivation Examples ==="
|
9
|
+
puts
|
10
|
+
|
11
|
+
# 1. From Hash (most reliable for Ruby)
|
12
|
+
puts "1. From Hash (recommended for Ruby)"
|
13
|
+
hash_data = {
|
14
|
+
name: "John Doe",
|
15
|
+
age: 30,
|
16
|
+
email: "john@example.com",
|
17
|
+
active: true,
|
18
|
+
score: 95.5,
|
19
|
+
tags: ["developer", "ruby"],
|
20
|
+
metadata: { role: "senior", team: "backend" }
|
21
|
+
}
|
22
|
+
|
23
|
+
schema = RapiTapir::Types.from_hash(hash_data)
|
24
|
+
puts "Schema from hash:"
|
25
|
+
puts schema.field_types.inspect
|
26
|
+
puts
|
27
|
+
|
28
|
+
# 2. From Hash with filtering
|
29
|
+
filtered_schema = RapiTapir::Types.from_hash(hash_data, except: [:metadata])
|
30
|
+
puts "Schema with 'except' filtering:"
|
31
|
+
puts filtered_schema.field_types.inspect
|
32
|
+
puts
|
33
|
+
|
34
|
+
# 3. From explicit types (recommended for classes)
|
35
|
+
puts "2. From Class with Explicit Types (recommended)"
|
36
|
+
class Person
|
37
|
+
attr_accessor :name, :age, :email, :active
|
38
|
+
end
|
39
|
+
|
40
|
+
# Method 1: Via types parameter
|
41
|
+
person_schema = RapiTapir::Types.from_object(Person, types: {
|
42
|
+
name: :string,
|
43
|
+
age: :integer,
|
44
|
+
email: :string,
|
45
|
+
active: :boolean
|
46
|
+
})
|
47
|
+
puts "Person schema with explicit types:"
|
48
|
+
puts person_schema.field_types.inspect
|
49
|
+
puts
|
50
|
+
|
51
|
+
# 4. From instance (less reliable but convenient)
|
52
|
+
puts "3. From Instance (use with well-populated data)"
|
53
|
+
person = Person.new
|
54
|
+
person.name = "Jane Smith"
|
55
|
+
person.age = 28
|
56
|
+
person.email = "jane@example.com"
|
57
|
+
person.active = true
|
58
|
+
|
59
|
+
instance_schema = RapiTapir::Types.from_object(person)
|
60
|
+
puts "Schema from instance:"
|
61
|
+
puts instance_schema.field_types.inspect
|
62
|
+
puts
|
63
|
+
|
64
|
+
# 5. From Struct (good compromise)
|
65
|
+
puts "4. From Struct (good Ruby pattern)"
|
66
|
+
Point = Struct.new(:x, :y, :z) do
|
67
|
+
def distance_from_origin
|
68
|
+
Math.sqrt(x**2 + y**2 + z**2)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
sample_point = Point.new(1.0, 2.0, 3.0)
|
73
|
+
point_schema = RapiTapir::Types.from_object(Point, sample: sample_point)
|
74
|
+
puts "Point schema from Struct:"
|
75
|
+
puts point_schema.field_types.inspect
|
76
|
+
puts
|
77
|
+
|
78
|
+
# 6. DSL approach (most Ruby-like)
|
79
|
+
puts "5. DSL Approach (Ruby-idiomatic)"
|
80
|
+
class User
|
81
|
+
include RapiTapir::Types::AutoDerivation::Annotated
|
82
|
+
|
83
|
+
attr_accessor :username, :email, :age, :admin
|
84
|
+
|
85
|
+
rapitapir_schema do
|
86
|
+
field :username, :string
|
87
|
+
field :email, :string
|
88
|
+
field :age, :integer
|
89
|
+
field :admin, :boolean
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
user_schema = RapiTapir::Types.from_object(User)
|
94
|
+
puts "User schema with DSL:"
|
95
|
+
puts user_schema.field_types.inspect
|
96
|
+
puts
|
97
|
+
|
98
|
+
# 7. OpenStruct (convenient for prototyping)
|
99
|
+
puts "6. From OpenStruct"
|
100
|
+
require 'ostruct'
|
101
|
+
|
102
|
+
config = OpenStruct.new(
|
103
|
+
host: "localhost",
|
104
|
+
port: 3000,
|
105
|
+
ssl: false,
|
106
|
+
timeout: 30.5
|
107
|
+
)
|
108
|
+
|
109
|
+
config_schema = RapiTapir::Types.from_open_struct(config)
|
110
|
+
puts "Config schema from OpenStruct:"
|
111
|
+
puts config_schema.field_types.inspect
|
112
|
+
puts
|
113
|
+
|
114
|
+
# 8. JSON Schema (for external APIs)
|
115
|
+
puts "7. From JSON Schema"
|
116
|
+
json_schema = {
|
117
|
+
"type" => "object",
|
118
|
+
"properties" => {
|
119
|
+
"id" => { "type" => "integer" },
|
120
|
+
"name" => { "type" => "string", "maxLength" => 100 },
|
121
|
+
"email" => { "type" => "string", "format" => "email" },
|
122
|
+
"created_at" => { "type" => "string", "format" => "date-time" },
|
123
|
+
"tags" => {
|
124
|
+
"type" => "array",
|
125
|
+
"items" => { "type" => "string" }
|
126
|
+
}
|
127
|
+
},
|
128
|
+
"required" => ["id", "name", "email"]
|
129
|
+
}
|
130
|
+
|
131
|
+
json_derived_schema = RapiTapir::Types.from_json_schema(json_schema)
|
132
|
+
puts "Schema from JSON Schema:"
|
133
|
+
puts json_derived_schema.field_types.inspect
|
134
|
+
puts
|
135
|
+
|
136
|
+
# 9. Demonstrate the limitations
|
137
|
+
puts "8. Limitations and Error Handling"
|
138
|
+
|
139
|
+
class EmptyClass
|
140
|
+
end
|
141
|
+
|
142
|
+
begin
|
143
|
+
# This should fail gracefully
|
144
|
+
RapiTapir::Types.from_object(EmptyClass)
|
145
|
+
rescue ArgumentError => e
|
146
|
+
puts "Expected error for empty class: #{e.message}"
|
147
|
+
end
|
148
|
+
|
149
|
+
begin
|
150
|
+
# This should also fail
|
151
|
+
RapiTapir::Types.from_object(Point) # Struct without sample
|
152
|
+
rescue ArgumentError => e
|
153
|
+
puts "Expected error for Struct without sample: #{e.message}"
|
154
|
+
end
|
155
|
+
|
156
|
+
puts
|
157
|
+
puts "=== Recommendations ==="
|
158
|
+
puts "1. Use from_hash() for parsed JSON or config data"
|
159
|
+
puts "2. Use explicit types parameter for Ruby classes"
|
160
|
+
puts "3. Use DSL annotations for reusable schemas"
|
161
|
+
puts "4. Use Structs for value objects with samples"
|
162
|
+
puts "5. Use from_json_schema() for external API integration"
|
163
|
+
puts "6. Avoid deriving from empty classes or nil values"
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Add the lib directory to the load path
|
4
|
+
lib_path = File.expand_path('../lib', __dir__)
|
5
|
+
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
|
6
|
+
|
7
|
+
require 'rapitapir'
|
8
|
+
|
9
|
+
# Include DSL to use helper methods
|
10
|
+
include RapiTapir::DSL
|
11
|
+
|
12
|
+
# Define endpoints for CLI usage
|
13
|
+
[
|
14
|
+
# Get all users
|
15
|
+
RapiTapir.get('/users')
|
16
|
+
.out(json_body([{ id: :integer, name: :string, email: :string }]))
|
17
|
+
.summary('Get all users')
|
18
|
+
.description('Retrieve a list of all users'),
|
19
|
+
|
20
|
+
# Get user by ID
|
21
|
+
RapiTapir.get('/users/:id')
|
22
|
+
.in(path_param(:id, :integer))
|
23
|
+
.out(json_body({ id: :integer, name: :string, email: :string }))
|
24
|
+
.summary('Get user by ID')
|
25
|
+
.description('Retrieve a specific user by their ID'),
|
26
|
+
|
27
|
+
# Create new user
|
28
|
+
RapiTapir.post('/users')
|
29
|
+
.in(body({ name: :string, email: :string }))
|
30
|
+
.out(json_body({ id: :integer, name: :string, email: :string }))
|
31
|
+
.summary('Create new user')
|
32
|
+
.description('Create a new user with the provided information'),
|
33
|
+
|
34
|
+
# Update user
|
35
|
+
RapiTapir.put('/users/:id')
|
36
|
+
.in(path_param(:id, :integer))
|
37
|
+
.in(body({ name: :string, email: :string }))
|
38
|
+
.out(json_body({ id: :integer, name: :string, email: :string }))
|
39
|
+
.summary('Update user')
|
40
|
+
.description('Update an existing user'),
|
41
|
+
|
42
|
+
# Delete user
|
43
|
+
RapiTapir.delete('/users/:id')
|
44
|
+
.in(path_param(:id, :integer))
|
45
|
+
.out(json_body({ success: :boolean }))
|
46
|
+
.summary('Delete user')
|
47
|
+
.description('Delete a user by their ID'),
|
48
|
+
|
49
|
+
# Search users
|
50
|
+
RapiTapir.get('/users/search')
|
51
|
+
.in(query(:q, :string))
|
52
|
+
.in(query(:limit, :integer, optional: true))
|
53
|
+
.out(json_body([{ id: :integer, name: :string, email: :string }]))
|
54
|
+
.summary('Search users')
|
55
|
+
.description('Search for users by name or email')
|
56
|
+
]
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../lib/rapitapir'
|
4
|
+
|
5
|
+
# Include DSL to use helper methods
|
6
|
+
include RapiTapir::DSL
|
7
|
+
|
8
|
+
# Define the same User API as before
|
9
|
+
user_api = [
|
10
|
+
# Get all users
|
11
|
+
RapiTapir.get('/users')
|
12
|
+
.out(json_body([{ id: :integer, name: :string, email: :string }]))
|
13
|
+
.summary('Get all users')
|
14
|
+
.description('Retrieve a list of all users'),
|
15
|
+
|
16
|
+
# Get user by ID
|
17
|
+
RapiTapir.get('/users/:id')
|
18
|
+
.in(path_param(:id, :integer))
|
19
|
+
.out(json_body({ id: :integer, name: :string, email: :string }))
|
20
|
+
.summary('Get user by ID')
|
21
|
+
.description('Retrieve a specific user by their ID'),
|
22
|
+
|
23
|
+
# Create new user
|
24
|
+
RapiTapir.post('/users')
|
25
|
+
.in(body({ name: :string, email: :string }))
|
26
|
+
.out(json_body({ id: :integer, name: :string, email: :string }))
|
27
|
+
.summary('Create new user')
|
28
|
+
.description('Create a new user with the provided information'),
|
29
|
+
|
30
|
+
# Update user
|
31
|
+
RapiTapir.put('/users/:id')
|
32
|
+
.in(path_param(:id, :integer))
|
33
|
+
.in(body({ name: :string, email: :string }))
|
34
|
+
.out(json_body({ id: :integer, name: :string, email: :string }))
|
35
|
+
.summary('Update user')
|
36
|
+
.description('Update an existing user'),
|
37
|
+
|
38
|
+
# Delete user
|
39
|
+
RapiTapir.delete('/users/:id')
|
40
|
+
.in(path_param(:id, :integer))
|
41
|
+
.out(json_body({ success: :boolean }))
|
42
|
+
.summary('Delete user')
|
43
|
+
.description('Delete a user by their ID'),
|
44
|
+
|
45
|
+
# Search users
|
46
|
+
RapiTapir.get('/users/search')
|
47
|
+
.in(query(:q, :string))
|
48
|
+
.in(query(:limit, :integer, optional: true))
|
49
|
+
.out(json_body([{ id: :integer, name: :string, email: :string }]))
|
50
|
+
.summary('Search users')
|
51
|
+
.description('Search for users by name or email')
|
52
|
+
]
|
53
|
+
|
54
|
+
# Generate TypeScript client
|
55
|
+
puts 'Generating TypeScript client...'
|
56
|
+
|
57
|
+
generator = RapiTapir::Client::TypescriptGenerator.new(
|
58
|
+
endpoints: user_api,
|
59
|
+
config: {
|
60
|
+
base_url: 'https://api.example.com',
|
61
|
+
client_name: 'UserApiClient',
|
62
|
+
package_name: '@mycompany/user-api-client',
|
63
|
+
version: '1.2.0'
|
64
|
+
}
|
65
|
+
)
|
66
|
+
|
67
|
+
# Save to file
|
68
|
+
output_file = File.join(__dir__, 'user-api-client.ts')
|
69
|
+
generator.save_to_file(output_file)
|
70
|
+
|
71
|
+
puts "\nTypeScript client generated successfully!"
|
72
|
+
puts "File: #{output_file}"
|
73
|
+
puts "\nTo use the client in your TypeScript project:"
|
74
|
+
puts '1. Copy the generated file to your project'
|
75
|
+
puts '2. Install dependencies: npm install'
|
76
|
+
puts '3. Import and use the client:'
|
77
|
+
puts ''
|
78
|
+
puts '```typescript'
|
79
|
+
puts "import UserApiClient from './user-api-client';"
|
80
|
+
puts ''
|
81
|
+
puts 'const client = new UserApiClient({'
|
82
|
+
puts " baseUrl: 'https://api.example.com',"
|
83
|
+
puts " headers: { 'Authorization': 'Bearer your-token' }"
|
84
|
+
puts '});'
|
85
|
+
puts ''
|
86
|
+
puts '// Get all users'
|
87
|
+
puts 'const users = await client.getUsers();'
|
88
|
+
puts ''
|
89
|
+
puts '// Get user by ID'
|
90
|
+
puts 'const user = await client.getUsersById({ id: 123 });'
|
91
|
+
puts ''
|
92
|
+
puts '// Create new user'
|
93
|
+
puts 'const newUser = await client.createUsers({'
|
94
|
+
puts " body: { name: 'John Doe', email: 'john@example.com' }"
|
95
|
+
puts '});'
|
96
|
+
puts ''
|
97
|
+
puts '// Search users'
|
98
|
+
puts 'const searchResults = await client.getUsersSearch({'
|
99
|
+
puts " q: 'john',"
|
100
|
+
puts ' limit: 10'
|
101
|
+
puts '});'
|
102
|
+
puts '```'
|
@@ -0,0 +1,193 @@
|
|
1
|
+
// Generated by RapiTapir TypeScript Client Generator
|
2
|
+
// Package: @mycompany/user-api-client
|
3
|
+
// Version: 1.2.0
|
4
|
+
// Base URL: https://api.example.com
|
5
|
+
|
6
|
+
// Response type for all API calls
|
7
|
+
export interface ApiResponse<T> {
|
8
|
+
data: T;
|
9
|
+
status: number;
|
10
|
+
headers: Record<string, string>;
|
11
|
+
}
|
12
|
+
|
13
|
+
// Error type for API errors
|
14
|
+
export interface ApiError {
|
15
|
+
message: string;
|
16
|
+
status: number;
|
17
|
+
details?: any;
|
18
|
+
}
|
19
|
+
|
20
|
+
// HTTP client configuration
|
21
|
+
export interface ClientConfig {
|
22
|
+
baseUrl?: string;
|
23
|
+
headers?: Record<string, string>;
|
24
|
+
timeout?: number;
|
25
|
+
}
|
26
|
+
|
27
|
+
|
28
|
+
// Generated types
|
29
|
+
export type GetusersResponse = { id: number; name: string; email: string }[];
|
30
|
+
|
31
|
+
export interface GetusersbyidRequest {
|
32
|
+
id: number;
|
33
|
+
}
|
34
|
+
|
35
|
+
|
36
|
+
export type GetusersbyidResponse = { id: number; name: string; email: string };
|
37
|
+
|
38
|
+
export interface CreateuserRequest {
|
39
|
+
body: { name: string; email: string };
|
40
|
+
}
|
41
|
+
|
42
|
+
|
43
|
+
export type CreateuserResponse = { id: number; name: string; email: string };
|
44
|
+
|
45
|
+
export interface UpdateuserRequest {
|
46
|
+
id: number;
|
47
|
+
body: { name: string; email: string };
|
48
|
+
}
|
49
|
+
|
50
|
+
|
51
|
+
export type UpdateuserResponse = { id: number; name: string; email: string };
|
52
|
+
|
53
|
+
export interface DeleteuserRequest {
|
54
|
+
id: number;
|
55
|
+
}
|
56
|
+
|
57
|
+
|
58
|
+
export type DeleteuserResponse = { success: boolean };
|
59
|
+
|
60
|
+
export interface GetuserssearchRequest {
|
61
|
+
q: string;
|
62
|
+
limit?: number;
|
63
|
+
}
|
64
|
+
|
65
|
+
|
66
|
+
export type GetuserssearchResponse = { id: number; name: string; email: string }[];
|
67
|
+
|
68
|
+
export class UserApiClient {
|
69
|
+
private baseUrl: string;
|
70
|
+
private headers: Record<string, string>;
|
71
|
+
private timeout: number;
|
72
|
+
|
73
|
+
constructor(config: ClientConfig = {}) {
|
74
|
+
this.baseUrl = config.baseUrl || 'https://api.example.com';
|
75
|
+
this.headers = config.headers || {};
|
76
|
+
this.timeout = config.timeout || 10000;
|
77
|
+
}
|
78
|
+
|
79
|
+
private async request<T>(
|
80
|
+
method: string,
|
81
|
+
path: string,
|
82
|
+
options: {
|
83
|
+
params?: Record<string, any>;
|
84
|
+
body?: any;
|
85
|
+
headers?: Record<string, string>;
|
86
|
+
} = {}
|
87
|
+
): Promise<ApiResponse<T>> {
|
88
|
+
const url = new URL(path, this.baseUrl);
|
89
|
+
|
90
|
+
// Add query parameters
|
91
|
+
if (options.params) {
|
92
|
+
Object.entries(options.params).forEach(([key, value]) => {
|
93
|
+
if (value !== undefined && value !== null) {
|
94
|
+
url.searchParams.append(key, String(value));
|
95
|
+
}
|
96
|
+
});
|
97
|
+
}
|
98
|
+
|
99
|
+
const requestHeaders = {
|
100
|
+
'Content-Type': 'application/json',
|
101
|
+
...this.headers,
|
102
|
+
...options.headers,
|
103
|
+
};
|
104
|
+
|
105
|
+
const requestInit: RequestInit = {
|
106
|
+
method,
|
107
|
+
headers: requestHeaders,
|
108
|
+
};
|
109
|
+
|
110
|
+
if (options.body) {
|
111
|
+
requestInit.body = JSON.stringify(options.body);
|
112
|
+
}
|
113
|
+
|
114
|
+
try {
|
115
|
+
const response = await fetch(url.toString(), requestInit);
|
116
|
+
|
117
|
+
const responseHeaders: Record<string, string> = {};
|
118
|
+
response.headers.forEach((value, key) => {
|
119
|
+
responseHeaders[key] = value;
|
120
|
+
});
|
121
|
+
|
122
|
+
let data: T;
|
123
|
+
const contentType = response.headers.get('content-type');
|
124
|
+
if (contentType && contentType.includes('application/json')) {
|
125
|
+
data = await response.json();
|
126
|
+
} else {
|
127
|
+
data = (await response.text()) as unknown as T;
|
128
|
+
}
|
129
|
+
|
130
|
+
if (!response.ok) {
|
131
|
+
const error: ApiError = {
|
132
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
133
|
+
status: response.status,
|
134
|
+
details: data,
|
135
|
+
};
|
136
|
+
throw error;
|
137
|
+
}
|
138
|
+
|
139
|
+
return {
|
140
|
+
data,
|
141
|
+
status: response.status,
|
142
|
+
headers: responseHeaders,
|
143
|
+
};
|
144
|
+
} catch (error) {
|
145
|
+
if (error && typeof error === 'object' && 'status' in error) {
|
146
|
+
throw error; // Re-throw ApiError
|
147
|
+
}
|
148
|
+
|
149
|
+
const apiError: ApiError = {
|
150
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
151
|
+
status: 0,
|
152
|
+
details: error,
|
153
|
+
};
|
154
|
+
throw apiError;
|
155
|
+
}
|
156
|
+
}
|
157
|
+
|
158
|
+
async getUsers(): Promise<ApiResponse<GetusersResponse>> {
|
159
|
+
return this.request<GetusersResponse>('GET', '/users');
|
160
|
+
}
|
161
|
+
|
162
|
+
async getUsersById(request: GetusersbyidRequest): Promise<ApiResponse<GetusersbyidResponse>> {
|
163
|
+
return this.request<GetusersbyidResponse>('GET', `/users/${request.id}`, {
|
164
|
+
});
|
165
|
+
}
|
166
|
+
|
167
|
+
async createUser(request: CreateuserRequest): Promise<ApiResponse<CreateuserResponse>> {
|
168
|
+
return this.request<CreateuserResponse>('POST', '/users', {
|
169
|
+
body: request.body
|
170
|
+
});
|
171
|
+
}
|
172
|
+
|
173
|
+
async updateUser(request: UpdateuserRequest): Promise<ApiResponse<UpdateuserResponse>> {
|
174
|
+
return this.request<UpdateuserResponse>('PUT', `/users/${request.id}`, {
|
175
|
+
body: request.body
|
176
|
+
});
|
177
|
+
}
|
178
|
+
|
179
|
+
async deleteUser(request: DeleteuserRequest): Promise<ApiResponse<DeleteuserResponse>> {
|
180
|
+
return this.request<DeleteuserResponse>('DELETE', `/users/${request.id}`, {
|
181
|
+
});
|
182
|
+
}
|
183
|
+
|
184
|
+
async getUsersSearch(request: GetuserssearchRequest): Promise<ApiResponse<GetuserssearchResponse>> {
|
185
|
+
return this.request<GetuserssearchResponse>('GET', '/users/search', {
|
186
|
+
params: { q: request.q, limit: request.limit }
|
187
|
+
});
|
188
|
+
}
|
189
|
+
}
|
190
|
+
|
191
|
+
|
192
|
+
// Default export
|
193
|
+
export default UserApiClient;
|