๐พ Petstore API Client
Production-ready Ruby client for the Swagger Petstore API with OAuth2 support, automatic retries, and comprehensive validation.
Author: Hammad Khan (@hammadxcm)
Note: All architecture, business logic, implementation, and test coverage were developed by me from scratch. AI tools were used solely to enhance documentation and code comments.
๐ Quick Start
gem install petstore_api_client
require 'petstore_api_client'
# Create client
client = PetstoreApiClient::ApiClient.new
# Create a pet
pet = client.create_pet(
name: "Fluffy",
photo_urls: ["https://example.com/fluffy.jpg"],
status: "available"
)
โจ Features
| Feature | Description |
|---|---|
| ๐ Dual Authentication | API Key & OAuth2 (Client Credentials) |
| ๐ Auto Retry | Exponential backoff for transient failures |
| โก Rate Limiting | Smart handling with retry-after support |
| ๐ Pagination | Flexible page/offset navigation |
| โ Validation | Pre-request data validation |
| ๐ก๏ธ Error Handling | 7 custom exception types |
| ๐ Test Coverage | 96.91% coverage, 454 passing tests |
| ๐ฏ SOLID Design | Production-ready architecture |
๐ฆ Installation
# Gemfile
gem 'petstore_api_client'
# Install
bundle install
๐ Authentication
The client supports multiple authentication strategies with feature flags:
graph TD
A[Configure Auth Strategy] --> B{Which Strategy?}
B -->|:none| C[No Authentication]
B -->|:api_key| D[API Key Only]
B -->|:oauth2| E[OAuth2 Only]
B -->|:both| F[API Key + OAuth2]
D --> G[Add api_key Header]
E --> H[Fetch OAuth2 Token]
H --> I[Add Authorization: Bearer]
F --> J[Add Both Headers]
style B fill:#e3f2fd
style D fill:#fff3e0
style E fill:#c8e6c9
style F fill:#f3e5f5
๐ API Key Authentication
client = PetstoreApiClient::ApiClient.new
client.configure do |config|
config.auth_strategy = :api_key # Default
config.api_key = "special-key"
end
From Environment:
export PETSTORE_API_KEY="your-key"
config.api_key = :from_env # Loads from PETSTORE_API_KEY
๐ซ OAuth2 Authentication
client.configure do |config|
config.auth_strategy = :oauth2
config.oauth2_client_id = "my-client-id"
config.oauth2_client_secret = "my-secret"
config.oauth2_scope = "read:pets write:pets" # Optional
end
OAuth2 Flow:
sequenceDiagram
participant App as Your App
participant Client as API Client
participant Auth as OAuth2 Strategy
participant Token as Token Server
participant API as Petstore API
App->>Client: create_pet(data)
Client->>Auth: apply(request)
alt Token Missing/Expired
Auth->>Token: POST /oauth/token
Token-->>Auth: access_token + expires_in
Auth->>Auth: Cache token
end
Auth->>Auth: Add Authorization: Bearer {token}
Client->>API: POST /pet (with Bearer token)
API-->>Client: 200 OK + Pet data
Client-->>App: Pet object
Environment Variables:
export PETSTORE_OAUTH2_CLIENT_ID="my-client-id"
export PETSTORE_OAUTH2_CLIENT_SECRET="my-secret"
export PETSTORE_OAUTH2_TOKEN_URL="https://custom.com/token" # Optional
export PETSTORE_OAUTH2_SCOPE="read:pets write:pets" # Optional
๐ Dual Authentication (Both)
Send both API Key and OAuth2 headers simultaneously:
client.configure do |config|
config.auth_strategy = :both
config.api_key = "special-key"
config.oauth2_client_id = "client-id"
config.oauth2_client_secret = "secret"
end
# Requests will include:
# - api_key: special-key
# - Authorization: Bearer {access_token}
๐ซ No Authentication
config.auth_strategy = :none # No auth headers
๐ Security Features
| Feature | Description |
|---|---|
| โ Credential Validation | Format & length checks |
| โ HTTPS Warnings | Alerts for insecure connections |
| โ Secure Logging | API keys masked in output (e.g., spec****) |
| โ Token Auto-Refresh | OAuth2 tokens refreshed 60s before expiry |
| โ Thread-Safe | Mutex-protected token management |
โ๏ธ Configuration
๐ All Configuration Options
| Option | Type | Default | Description | |------------------------|---------|-------------------------------------------|-----------------------------------------| | `base_url` | String | `https://petstore.swagger.io/v2` | API endpoint | | `auth_strategy` | Symbol | `:api_key` | `:none`, `:api_key`, `:oauth2`, `:both` | | `api_key` | String | `nil` | API key for authentication | | `oauth2_client_id` | String | `nil` | OAuth2 client ID | | `oauth2_client_secret` | String | `nil` | OAuth2 client secret | | `oauth2_token_url` | String | `https://petstore.swagger.io/oauth/token` | OAuth2 token endpoint | | `oauth2_scope` | String | `nil` | OAuth2 scope | | `timeout` | Integer | `30` | Request timeout (seconds) | | `open_timeout` | Integer | `10` | Connection timeout (seconds) | | `retry_enabled` | Boolean | `true` | Enable auto-retry | | `max_retries` | Integer | `2` | Retry attempts | | `default_page_size` | Integer | `25` | Pagination page size | | `max_page_size` | Integer | `100` | Max pagination size |client = PetstoreApiClient::ApiClient.new
client.configure do |config|
config.timeout = 60
config.retry_enabled = true
config.max_retries = 3
end
๐ Auto-Retry & Rate Limiting
flowchart LR
A[Request] --> B{Success?}
B -->|2xx| C[โ
Return]
B -->|429/5xx| D{Retries<br/>Left?}
B -->|4xx| E[โ Error]
D -->|Yes| F[Wait +<br/>Backoff]
D -->|No| G[โ Raise<br/>Error]
F --> A
style C fill:#c8e6c9
style E fill:#ffcdd2
style G fill:#ffcdd2
style F fill:#fff9c4
Handles:
- ๐ Network failures
- โฑ๏ธ Timeouts
- ๐ฆ Rate limits (429)
- ๐ง Server errors (500, 502, 503, 504)
begin
pet = client.get_pet(123)
rescue PetstoreApiClient::RateLimitError => e
puts "Retry after: #{e.retry_after}s"
end
๐ Usage Examples
๐ Pet Management
# Create
pet = client.create_pet(
name: "Max",
photo_urls: ["https://example.com/max.jpg"],
category: { id: 1, name: "Dogs" },
tags: [{ id: 1, name: "friendly" }],
status: "available" # available | pending | sold
)
# Read
pet = client.get_pet(123)
# Update
updated = client.update_pet(
id: 123,
name: "Max Updated",
photo_urls: ["https://example.com/max-new.jpg"],
status: "sold"
)
# Delete
client.delete_pet(123)
# Find by status (with pagination)
pets = client.pets.find_by_status("available", page: 1, per_page: 10)
# Find by tags
pets = client.pets.(["friendly", "vaccinated"])
๐ Store Orders
# Create order
order = client.create_order(
pet_id: 123,
quantity: 2,
status: "placed", # placed | approved | delivered
ship_date: DateTime.now + 7
)
# Get order
order = client.get_order(987)
# Delete order
client.delete_order(987)
๐ Pagination
pets = client.pets.find_by_status("available", page: 1, per_page: 25)
# Navigation
puts "Page #{pets.page} of #{pets.total_pages}"
puts "Showing #{pets.count} of #{pets.total_count}"
pets.next_page? # => true
pets.prev_page? # => false
pets.first_page? # => true
pets.last_page? # => false
# Iterate
pets.each { |pet| puts pet.name }
pets.map(&:id)
๐ก๏ธ Error Handling
graph TD
A[PetstoreApiClient::Error] --> B[ValidationError<br/>โ ๏ธ Pre-request]
A --> C[ConfigurationError<br/>โ๏ธ Config invalid]
A --> D[AuthenticationError<br/>๐ Auth failed]
A --> E[NotFoundError<br/>โ 404]
A --> F[InvalidInputError<br/>โ ๏ธ 400/405]
A --> G[InvalidOrderError<br/>๐ฆ 400 order]
A --> H[RateLimitError<br/>โฑ๏ธ 429]
A --> I[ConnectionError<br/>๐ Network]
A --> J[ApiError<br/>๐ง 5xx]
style A fill:#ffebee
style D fill:#fff3e0
style H fill:#fff9c4
begin
pet = client.get_pet(999999)
rescue PetstoreApiClient::NotFoundError => e
puts "Not found: #{e.}"
rescue PetstoreApiClient::AuthenticationError => e
puts "Auth failed: #{e.}"
rescue PetstoreApiClient::ValidationError => e
puts "Validation: #{e.}"
rescue PetstoreApiClient::ApiError => e
puts "API error: #{e.} (#{e.status_code})"
end
๐๏ธ Architecture
graph TB
A[Your App] --> B[ApiClient]
B --> C[PetClient]
B --> D[StoreClient]
C --> E[Request Module]
D --> E
E --> F[Connection]
F --> G[Middleware Stack]
G --> H[AuthMiddleware<br/>๐ Add auth headers]
H --> I[RetryMiddleware<br/>๐ Auto-retry]
I --> J[RateLimitMiddleware<br/>โฑ๏ธ Handle 429]
J --> K[JSON Parser]
K --> L[Petstore API]
M[Configuration] -.-> B
M -.-> F
N[Authentication<br/>Strategy] --> O[ApiKey]
N --> P[OAuth2]
N --> Q[Composite]
N --> R[None]
H -.uses.-> N
style B fill:#e1f5ff
style C fill:#fff3e0
style D fill:#fff3e0
style H fill:#c8e6c9
style I fill:#fff9c4
style J fill:#ffcdd2
๐งช Testing
| Metric | Value |
|---|---|
| โ Total Tests | 454 passing |
| ๐ Line Coverage | 96.91% |
| ๐ Branch Coverage | 86.21% |
| ๐ฏ RuboCop | 0 offenses |
๐ Quick Test (From Project Root)
# One-command test
./bin/test
# Or manually
bundle install
bundle exec rspec
# With detailed output
bundle exec rspec --format documentation
# Lint check
bundle exec rubocop
๐ Coverage Report
bundle exec rspec
open coverage/index.html # Mac
xdg-open coverage/index.html # Linux
๐ฎ Interactive Console
IRB Console (Pre-configured):
bin/console
The console automatically loads the gem and creates a client instance:
# Client is ready to use!
pet = client.create_pet(
name: "TestDog",
photo_urls: ["http://example.com/dog.jpg"],
status: "available"
)
puts "Created: #{pet.name} (ID: #{pet.id})"
# Test OAuth2 authentication
client.configure do |config|
config.auth_strategy = :oauth2
config.oauth2_client_id = "test-client"
config.oauth2_client_secret = "test-secret"
end
# Clean up
client.delete_pet(pet.id)
๐ Rails Console Integration
Option 1: Gemfile Installation
Add to your Rails Gemfile:
gem 'petstore_api_client'
Then in Rails console:
rails console
# Create client with API Key
client = PetstoreApiClient::ApiClient.new
client.configure do |config|
config.api_key = ENV['PETSTORE_API_KEY']
# or
config.api_key = :from_env
end
# Test it
pet = client.create_pet(
name: "RailsPet",
photo_urls: ["https://example.com/rails-pet.jpg"]
)
Option 2: Load from Local Path
In Rails console:
# Load from local gem directory
$LOAD_PATH.unshift('/path/to/petstore-api-client/lib')
require 'petstore_api_client'
# Use it
client = PetstoreApiClient::ApiClient.new
Option 3: Rails Initializer
Create config/initializers/petstore.rb:
# config/initializers/petstore.rb
PetstoreApiClient.configure do |config|
config.auth_strategy = :oauth2
config.oauth2_client_id = ENV['PETSTORE_OAUTH2_CLIENT_ID']
config.oauth2_client_secret = ENV['PETSTORE_OAUTH2_CLIENT_SECRET']
config.timeout = 60
end
Then in your Rails app:
# app/services/pet_service.rb
class PetService
def self.create_pet(name:, photo_urls:)
client = PetstoreApiClient::ApiClient.new
client.create_pet(
name: name,
photo_urls: photo_urls,
status: 'available'
)
rescue PetstoreApiClient::ValidationError => e
Rails.logger.error("Validation failed: #{e.}")
nil
end
end
๐ Environment Setup
1. Copy environment template:
cp .env.example .env
2. Edit .env with your credentials:
# Choose your auth strategy
PETSTORE_API_KEY=special-key
# OR for OAuth2
PETSTORE_OAUTH2_CLIENT_ID=my-client-id
PETSTORE_OAUTH2_CLIENT_SECRET=my-secret
3. Load in Rails:
# Gemfile
gem 'dotenv-rails', groups: [:development, :test]
# .env is automatically loaded
โ ๏ธ Security Checklist
Before committing:
# 1. Check .gitignore includes sensitive files
cat .gitignore | grep -E '\.env|credentials|secrets|\.pem|\.key'
# 2. Verify no secrets in git
git status
git diff
# 3. Check for hardcoded secrets
grep -r "client_secret\|api_key" lib/ --exclude-dir=spec
# 4. Ensure .env is not staged
git ls-files | grep "\.env$" && echo "โ ๏ธ WARNING: .env is tracked!"
Never commit:
- โ
.envfiles - โ
credentials.json - โ
*.pem,*.keyfiles - โ OAuth2 client secrets
- โ API keys in code
๐ CI/CD Pipeline
GitHub Actions automatically runs on push/PR:
| Step | Command | Purpose |
|---|---|---|
| ๐งช Tests | bundle exec rspec |
Run 454 tests |
| ๐ Lint | bundle exec rubocop |
Code quality |
| ๐ Security | bundle audit |
Dependency vulnerabilities |
| ๐ฆ Build | gem build |
Build gem package |
| ๐ Coverage | Check 95%+ threshold | Ensure quality |
View CI status:
https://github.com/hammadxcm/petstore-api-client/actions
CI/CD Badge:
[](https://github.com/hammadxcm/petstore-api-client/actions)
๐ API Coverage
| Endpoint | Method | Client Method |
|---|---|---|
/pet |
POST | create_pet(data) |
/pet |
PUT | update_pet(data) |
/pet/{id} |
GET | get_pet(id) |
/pet/{id} |
DELETE | delete_pet(id) |
/pet/findByStatus |
GET | find_by_status(status, opts) |
/pet/findByTags |
GET | find_by_tags(tags, opts) |
/store/order |
POST | create_order(data) |
/store/order/{id} |
GET | get_order(id) |
/store/order/{id} |
DELETE | delete_order(id) |
๐ Documentation
- ๐ง YARD Docs - Full API reference
- ๐ Authentication guide is included above (see Authentication section)
- ๐ฉ Feature flags documented above (see Auth Strategies)
๐๏ธ Design Principles
โ SOLID - Single Responsibility, Open/Closed, Liskov, Interface Segregation, Dependency Inversion โ Strategy Pattern - Swappable authentication strategies โ Middleware Pattern - Composable Faraday middleware โ Factory Pattern - Configuration builds authenticators โ Composite Pattern - Combine multiple auth strategies โ Null Object - None authenticator for consistent interface
๐ฆ Dependencies
Runtime:
faraday(~> 2.0) - HTTP clientfaraday-retry(~> 2.0) - Auto-retry middlewareoauth2(~> 2.0) - OAuth2 clientactivemodel(>= 6.0) - Validations
Development:
rspec(~> 3.12) - Testingvcr(~> 6.0) - HTTP recordingsimplecov(~> 0.22) - Coverage
๐ค Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
How to contribute:
- ๐ด Fork it (https://github.com/hammadxcm/petstore-api-client/fork)
- ๐ฟ Create feature branch (
git checkout -b feature/amazing-feature) - โ Add tests for your changes
- ๐งช Run tests (
bundle exec rspec) - ๐ Run linter (
bundle exec rubocop) - ๐พ Commit (
git commit -m 'Add amazing feature') - ๐ค Push (
git push origin feature/amazing-feature) - ๐ Create Pull Request
Code owners: Changes will be automatically reviewed by @hammadxcm
Guidelines:
- Write tests for new features
- Follow existing code style
- Update documentation
- Keep commits focused and atomic
๐ License
MIT License - see LICENSE
๐ฌ Support & Contact
- ๐ค Author: Hammad Khan (@hammadxcm)
- ๐ Issues: GitHub Issues
- ๐ก Feature Requests: GitHub Discussions
- ๐ง Contact: Open an issue
- โญ Star the repo: github.com/hammadxcm/petstore-api-client