Skip to content

hammadxcm/petstore-api-client

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

30 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿพ Petstore API Client

Gem Version Ruby Tests Coverage License GitHub


โš ๏ธ AI USAGE DISCLOSURE

NO AI was used for CORE CODE

AI was ONLY used for:

  • ๐Ÿ“ Documentation (README, guides)
  • ๐Ÿ“š YARD documentation comments
  • ๐Ÿ”ง Minor code refactoring

ALL core functionality, architecture, business logic, implementation, and test coverage were developed by me from scratch.


Production-ready Ruby client for the Swagger Petstore API with OAuth2 support, automatic retries, and comprehensive validation.

Author: Hammad Khan (@hammadxcm)

๐Ÿ“‘ Table of Contents

๐Ÿš€ 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"
)

โšก Quick Copy-Paste Test Commands

๐Ÿ”น One-Liner Installation Test

# Install and verify in one command
gem install petstore_api_client && ruby -e "require 'petstore_api_client'; puts 'โœ… Gem installed! Version: ' + PetstoreApiClient::VERSION"

๐Ÿ”น Quick Ruby Test (Copy entire block)

# Paste this entire block into IRB or Ruby console
require 'petstore_api_client'
client = PetstoreApiClient::ApiClient.new
pet = client.create_pet(name: "QuickTest", photo_urls: ["https://example.com/test.jpg"], status: "available")
puts "โœ… Created pet: #{pet['name']} (ID: #{pet['id']})"
client.delete_pet(pet['id'])
puts "โœ… Cleanup complete!"

๐Ÿ”น Rails Console Quick Test (Copy entire block)

# Paste this entire block into Rails console
require 'petstore_api_client'
client = PetstoreApiClient::ApiClient.new
client.configure { |c| c.timeout = 30 }
pet = client.create_pet(name: "RailsTest-#{Time.now.to_i}", photo_urls: ["https://example.com/rails.jpg"], status: "available")
puts "โœ… Pet created! ID: #{pet['id']}, Name: #{pet['name']}"
fetched = client.get_pet(pet['id'])
puts "โœ… Pet fetched! Status: #{fetched['status']}"
client.delete_pet(pet['id'])
puts "โœ… Test complete!"

๐Ÿ”น Full CRUD Test (Copy entire block)

# Complete CRUD operations test - paste this entire block
require 'petstore_api_client'
client = PetstoreApiClient::ApiClient.new

# CREATE
puts "1๏ธโƒฃ  CREATE..."
pet = client.create_pet(name: "TestPet-#{rand(1000)}", photo_urls: ["https://example.com/photo.jpg"], status: "available")
pet_id = pet['id']
puts "   โœ… Created: #{pet['name']} (ID: #{pet_id})"

# READ
puts "2๏ธโƒฃ  READ..."
fetched = client.get_pet(pet_id)
puts "   โœ… Fetched: #{fetched['name']}"

# UPDATE
puts "3๏ธโƒฃ  UPDATE..."
updated = client.update_pet(pet_id, status: 'sold')
puts "   โœ… Updated status to: #{updated['status']}"

# DELETE
puts "4๏ธโƒฃ  DELETE..."
client.delete_pet(pet_id)
puts "   โœ… Deleted pet #{pet_id}"

puts "๐ŸŽ‰ All CRUD operations successful!"

๐Ÿ”น Authentication Test (OAuth2)

# Test OAuth2 authentication - paste this entire block
require 'petstore_api_client'
client = PetstoreApiClient::ApiClient.new
client.configure do |config|
  config.auth_strategy = :oauth2
  config.oauth2_client_id = ENV['PETSTORE_OAUTH2_CLIENT_ID'] || 'test-client'
  config.oauth2_client_secret = ENV['PETSTORE_OAUTH2_CLIENT_SECRET'] || 'test-secret'
end
puts "โœ… OAuth2 configured"
puts "   Strategy: #{client.config.auth_strategy}"
puts "   Client ID: #{client.config.oauth2_client_id}"

๐Ÿ”น Error Handling Test

# Test error handling - paste this entire block
require 'petstore_api_client'
client = PetstoreApiClient::ApiClient.new

# Test NotFoundError
begin
  client.get_pet(999999999)
rescue PetstoreApiClient::NotFoundError => e
  puts "โœ… NotFoundError caught correctly"
end

# Test ValidationError
begin
  client.create_pet(name: "", photo_urls: [])
rescue PetstoreApiClient::ValidationError => e
  puts "โœ… ValidationError caught correctly"
end

puts "๐ŸŽ‰ Error handling works!"

โœจ 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

๐Ÿ“ฅ From RubyGems (Recommended)
# Gemfile
gem 'petstore_api_client', '~> 0.1.0'

Then install:

bundle install
๐Ÿ“ฅ From Source
# Gemfile
gem 'petstore_api_client', github: 'hammadxcm/petstore-api-client'
๐Ÿ“ฅ Direct Installation
gem install petstore_api_client

๐Ÿ” 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
Loading
๐Ÿ”‘ 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
Loading

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

๐Ÿš‚ Rails Integration

graph TB
    subgraph "Rails Application"
        A[config/initializers/<br/>petstore_api_client.rb] -->|Global Config| B[PetstoreApiClient]
        C[.env / credentials] -->|Env Vars| A

        B --> D[Controllers]
        B --> E[Services/Models]
        B --> F[Background Jobs]

        D --> G[PetsController]
        E --> H[PetSyncService]
        F --> I[PetSyncJob]

        G -->|create_pet| J[ApiClient]
        H -->|sync_available_pets| J
        I -->|Async sync| J
    end

    subgraph "Gem Layer"
        J --> K[Authentication<br/>Strategy]
        J --> L[Retry<br/>Middleware]
        J --> M[Rate Limit<br/>Handler]
    end

    subgraph "External API"
        K --> N[Petstore API]
        L --> N
        M --> N
    end

    style A fill:#e1f5ff
    style B fill:#c8e6c9
    style J fill:#fff3e0
    style N fill:#f3e5f5
Loading

Installation in Rails

Add to your Gemfile:

gem 'petstore_api_client', '~> 0.1.0'

Install:

bundle install
๐Ÿ“‹ Configuration with Initializer

Create config/initializers/petstore_api_client.rb:

# config/initializers/petstore_api_client.rb

PetstoreApiClient.configure do |config|
  # Base configuration
  config.base_url = ENV.fetch('PETSTORE_API_URL', 'https://petstore.swagger.io/v2')
  config.timeout = 30
  config.open_timeout = 10

  # Authentication - choose one strategy
  config.auth_strategy = :oauth2  # or :api_key, :both, :none

  # OAuth2 configuration (if using oauth2 or both)
  config.oauth2_client_id = ENV['PETSTORE_OAUTH2_CLIENT_ID']
  config.oauth2_client_secret = ENV['PETSTORE_OAUTH2_CLIENT_SECRET']
  config.oauth2_token_url = ENV['PETSTORE_OAUTH2_TOKEN_URL']
  config.oauth2_scope = 'read:pets write:pets'

  # API Key configuration (if using api_key or both)
  config.api_key = ENV['PETSTORE_API_KEY']

  # Retry configuration
  config.max_retries = 3
  config.retry_statuses = [503, 429]

  # Pagination defaults
  config.default_page_size = 25
end
๐Ÿ“‹ Usage in Rails Controllers
# app/controllers/pets_controller.rb
class PetsController < ApplicationController
  before_action :initialize_client

  def index
    @pets = @client.find_pets_by_status('available')
    render json: @pets
  rescue PetstoreApiClient::ApiError => e
    render json: { error: e.message }, status: :bad_gateway
  end

  def show
    @pet = @client.get_pet(params[:id])
    render json: @pet
  rescue PetstoreApiClient::NotFoundError => e
    render json: { error: 'Pet not found' }, status: :not_found
  end

  def create
    @pet = @client.create_pet(pet_params)
    render json: @pet, status: :created
  rescue PetstoreApiClient::ValidationError => e
    render json: { errors: e.message }, status: :unprocessable_entity
  end

  def update
    @pet = @client.update_pet(params[:id], pet_params)
    render json: @pet
  rescue PetstoreApiClient::NotFoundError => e
    render json: { error: 'Pet not found' }, status: :not_found
  end

  def destroy
    @client.delete_pet(params[:id])
    head :no_content
  rescue PetstoreApiClient::NotFoundError => e
    render json: { error: 'Pet not found' }, status: :not_found
  end

  private

  def initialize_client
    @client = PetstoreApiClient::ApiClient.new
  end

  def pet_params
    params.require(:pet).permit(:name, :status, photo_urls: [], tags: [:id, :name])
  end
end
๐Ÿ“‹ Usage in Rails Models/Services
# app/services/pet_sync_service.rb
class PetSyncService
  def initialize
    @client = PetstoreApiClient::ApiClient.new
  end

  def sync_available_pets
    pets = @client.find_pets_by_status('available')

    pets.each do |pet_data|
      Pet.find_or_create_by(external_id: pet_data['id']) do |pet|
        pet.name = pet_data['name']
        pet.status = pet_data['status']
        pet.photo_urls = pet_data['photoUrls']
      end
    end

    pets.count
  rescue PetstoreApiClient::ApiError => e
    Rails.logger.error("Pet sync failed: #{e.message}")
    raise
  end

  def create_remote_pet(local_pet)
    @client.create_pet(
      name: local_pet.name,
      photo_urls: local_pet.photo_urls,
      status: local_pet.status,
      tags: local_pet.tags.map { |tag| { name: tag.name } }
    )
  end
end
๐Ÿ“‹ Background Jobs (Sidekiq/ActiveJob)
# app/jobs/pet_sync_job.rb
class PetSyncJob < ApplicationJob
  queue_as :default
  retry_on PetstoreApiClient::RateLimitError, wait: :polynomially_longer
  discard_on PetstoreApiClient::AuthenticationError

  def perform
    client = PetstoreApiClient::ApiClient.new
    pets = client.find_pets_by_status('available')

    Rails.logger.info "Synced #{pets.count} pets from Petstore API"
  end
end
๐Ÿ“‹ Environment Variables (.env)
# .env or .env.local
PETSTORE_API_URL=https://petstore.swagger.io/v2
PETSTORE_OAUTH2_CLIENT_ID=your-client-id
PETSTORE_OAUTH2_CLIENT_SECRET=your-client-secret
PETSTORE_OAUTH2_TOKEN_URL=https://petstore.swagger.io/oauth/token
PETSTORE_API_KEY=special-key
๐Ÿ“‹ Rails Credentials (Encrypted)
# Edit credentials
EDITOR=vim rails credentials:edit
# config/credentials.yml.enc
petstore:
  oauth2_client_id: your-client-id
  oauth2_client_secret: your-secret
  api_key: special-key

Access in initializer:

# config/initializers/petstore_api_client.rb
PetstoreApiClient.configure do |config|
  credentials = Rails.application.credentials.petstore

  config.oauth2_client_id = credentials[:oauth2_client_id]
  config.oauth2_client_secret = credentials[:oauth2_client_secret]
  config.api_key = credentials[:api_key]
end
๐Ÿ“‹ Testing with RSpec
# spec/services/pet_sync_service_spec.rb
require 'rails_helper'

RSpec.describe PetSyncService do
  let(:client) { instance_double(PetstoreApiClient::ApiClient) }
  let(:service) { described_class.new }

  before do
    allow(PetstoreApiClient::ApiClient).to receive(:new).and_return(client)
  end

  describe '#sync_available_pets' do
    it 'syncs pets from API' do
      pets_data = [
        { 'id' => 1, 'name' => 'Fluffy', 'status' => 'available', 'photoUrls' => [] }
      ]

      allow(client).to receive(:find_pets_by_status)
        .with('available')
        .and_return(pets_data)

      expect { service.sync_available_pets }.to change(Pet, :count).by(1)
    end

    it 'handles API errors gracefully' do
      allow(client).to receive(:find_pets_by_status)
        .and_raise(PetstoreApiClient::ApiError, 'API is down')

      expect { service.sync_available_pets }.to raise_error(PetstoreApiClient::ApiError)
    end
  end
end

๐Ÿงช Testing Gem Installation

Quick Verification

After installing the gem, verify it's working correctly:

# Install the gem
gem install petstore_api_client

# Verify installation
gem list petstore_api_client

# Check version
ruby -e "require 'petstore_api_client'; puts PetstoreApiClient::VERSION"

Expected output:

petstore_api_client (0.1.0)
0.1.0
๐Ÿ“‹ Rails Console Testing (Detailed Examples)

Rails Console Testing

1. Add Gem to Rails App

# Gemfile
gem 'petstore_api_client', '~> 0.1.0'
bundle install

2. Test in Rails Console

rails console

Basic Connectivity Test:

# Load the gem
require 'petstore_api_client'

# Create a client (using default configuration)
client = PetstoreApiClient::ApiClient.new

# Test basic functionality - get a pet by ID
# Note: Petstore API has some demo pets available
pet = client.get_pet(1)
puts "Pet Name: #{pet['name']}"
puts "Status: #{pet['status']}"

# Success! โœ… If you see pet data, the gem is working

Configuration Test:

# Test with custom configuration
PetstoreApiClient.configure do |config|
  config.timeout = 60
  config.auth_strategy = :api_key
  config.api_key = 'special-key'  # Petstore demo API key
end

# Create client with configuration
client = PetstoreApiClient::ApiClient.new

# Verify configuration was applied
puts "Timeout: #{client.config.timeout}"
puts "Auth Strategy: #{client.config.auth_strategy}"

# Test authenticated request
client.get_pet(1)
# Success! โœ… Configuration is working

CRUD Operations Test:

# CREATE - Add a new pet
new_pet = client.create_pet(
  name: "Rails Test Pet",
  photo_urls: ["https://example.com/photo.jpg"],
  status: "available",
  tags: [{ name: "test" }]
)

puts "Created Pet ID: #{new_pet['id']}"
pet_id = new_pet['id']

# READ - Get the pet we just created
pet = client.get_pet(pet_id)
puts "Retrieved Pet: #{pet['name']}"

# UPDATE - Change the pet's status
updated_pet = client.update_pet(pet_id, status: 'sold')
puts "Updated Status: #{updated_pet['status']}"

# DELETE - Remove the pet
client.delete_pet(pet_id)
puts "Pet deleted successfully!"

# Success! โœ… All CRUD operations working

Error Handling Test:

# Test error handling with invalid ID
begin
  client.get_pet(999999999)
rescue PetstoreApiClient::NotFoundError => e
  puts "โœ… NotFoundError caught correctly: #{e.message}"
end

# Test validation error
begin
  client.create_pet(name: "", photo_urls: [])  # Invalid data
rescue PetstoreApiClient::ValidationError => e
  puts "โœ… ValidationError caught correctly: #{e.message}"
end

# Success! โœ… Error handling is working

Pagination Test:

# Test finding pets by status with pagination
available_pets = client.find_pets_by_status('available')
puts "Found #{available_pets.count} available pets"

# Show first few pets
available_pets.first(3).each do |pet|
  puts "- #{pet['name']} (ID: #{pet['id']})"
end

# Success! โœ… Pagination working
๐Ÿ“‹ Rails App Integration Test (Detailed Example)

Rails App Integration Test

Create a test controller to verify full integration:

1. Generate Test Controller

rails generate controller PetTest index create

2. Update Controller

# app/controllers/pet_test_controller.rb
class PetTestController < ApplicationController
  before_action :initialize_client

  # GET /pet_test
  def index
    @pets = @client.find_pets_by_status('available')
    render json: {
      success: true,
      count: @pets.count,
      pets: @pets.first(5)
    }
  rescue PetstoreApiClient::ApiError => e
    render json: { success: false, error: e.message }, status: :bad_gateway
  end

  # POST /pet_test
  def create
    @pet = @client.create_pet(
      name: params[:name] || "Test Pet #{Time.now.to_i}",
      photo_urls: [params[:photo_url] || "https://example.com/pet.jpg"],
      status: params[:status] || "available"
    )

    render json: {
      success: true,
      message: "Pet created successfully",
      pet: @pet
    }, status: :created
  rescue PetstoreApiClient::ValidationError => e
    render json: { success: false, errors: e.message }, status: :unprocessable_entity
  rescue PetstoreApiClient::ApiError => e
    render json: { success: false, error: e.message }, status: :bad_gateway
  end

  private

  def initialize_client
    @client = PetstoreApiClient::ApiClient.new
  end
end

3. Add Routes

# config/routes.rb
Rails.application.routes.draw do
  get 'pet_test', to: 'pet_test#index'
  post 'pet_test', to: 'pet_test#create'
end

4. Start Rails Server

rails server

5. Test Endpoints

Test GET request:

curl http://localhost:3000/pet_test

Expected response:

{
  "success": true,
  "count": 10,
  "pets": [...]
}

Test POST request:

curl -X POST http://localhost:3000/pet_test \
  -H "Content-Type: application/json" \
  -d '{"name":"Fluffy","status":"available"}'

Expected response:

{
  "success": true,
  "message": "Pet created successfully",
  "pet": {
    "id": 12345,
    "name": "Fluffy",
    "status": "available"
  }
}

โœ… Success! If both requests work, the gem is fully integrated with your Rails app!

๐Ÿ“‹ Troubleshooting & Performance Testing

Troubleshooting

Gem not found:

# Verify gem is installed
bundle list | grep petstore_api_client

# Reinstall if needed
bundle install

Configuration not loading:

# Check if initializer was loaded
rails runner "puts PetstoreApiClient.configuration.inspect"

Connection errors:

# Test network connectivity
require 'net/http'
uri = URI('https://petstore.swagger.io/v2/pet/1')
response = Net::HTTP.get_response(uri)
puts response.code  # Should be "200"

SSL certificate errors:

# Temporarily disable SSL verification (development only!)
require 'openssl'
OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE

Performance Testing

Test gem performance in Rails console:

require 'benchmark'

client = PetstoreApiClient::ApiClient.new

# Single request benchmark
time = Benchmark.realtime do
  client.get_pet(1)
end
puts "Single request: #{(time * 1000).round(2)}ms"

# Multiple requests benchmark
time = Benchmark.realtime do
  10.times { client.get_pet(1) }
end
puts "10 requests: #{(time * 1000).round(2)}ms"
puts "Average: #{(time * 100).round(2)}ms per request"

Integration Test Checklist

  • โœ… Gem installs without errors
  • โœ… Basic client initialization works
  • โœ… Configuration can be set globally
  • โœ… CRUD operations function correctly
  • โœ… Error handling catches exceptions
  • โœ… Pagination returns results
  • โœ… Rails controller integration works
  • โœ… API requests complete successfully
  • โœ… Performance is acceptable

โš™๏ธ 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

๐Ÿ”„ Request Lifecycle

sequenceDiagram
    participant App as Your Application
    participant Client as ApiClient
    participant Auth as Auth Strategy
    participant Retry as Retry Middleware
    participant API as Petstore API

    App->>Client: create_pet(data)

    Note over Client: 1. Validate Input
    Client->>Client: Validate required fields

    Note over Client,Auth: 2. Authentication
    Client->>Auth: apply(request)

    alt OAuth2 Strategy
        Auth->>Auth: Check token expiry
        alt Token expired/missing
            Auth->>API: POST /oauth/token
            API-->>Auth: access_token
            Auth->>Auth: Cache token
        end
        Auth->>Auth: Add Authorization header
    else API Key Strategy
        Auth->>Auth: Add api_key header
    end

    Note over Client,Retry: 3. Send Request
    Client->>Retry: execute(request)
    Retry->>API: POST /pet

    alt Success (2xx)
        API-->>Retry: 200 OK + Pet data
        Retry-->>Client: Pet response
        Client-->>App: Pet object
    else Rate Limited (429)
        API-->>Retry: 429 Too Many Requests
        Note over Retry: Wait + exponential backoff
        Retry->>API: Retry request
        API-->>Retry: 200 OK
        Retry-->>Client: Pet response
        Client-->>App: Pet object
    else Server Error (5xx)
        API-->>Retry: 503 Service Unavailable
        Note over Retry: Retry with backoff
        Retry->>API: Retry request
        API-->>Retry: 200 OK
        Retry-->>Client: Pet response
        Client-->>App: Pet object
    else Client Error (4xx)
        API-->>Retry: 404 Not Found
        Retry-->>Client: Error response
        Client->>Client: Raise NotFoundError
        Client-->>App: Exception
    end
Loading

๐Ÿ”„ 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
Loading

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 (CRUD Operations)
# 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.find_by_tags(["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 Examples
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
Loading
begin
  pet = client.get_pet(999999)
rescue PetstoreApiClient::NotFoundError => e
  puts "Not found: #{e.message}"
rescue PetstoreApiClient::AuthenticationError => e
  puts "Auth failed: #{e.message}"
rescue PetstoreApiClient::ValidationError => e
  puts "Validation: #{e.message}"
rescue PetstoreApiClient::ApiError => e
  puts "API error: #{e.message} (#{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
Loading

๐Ÿงช 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 & Development Setup

๐ŸŽฎ 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

Test with all authentication strategies:

# =====================================
# Strategy 1: No Authentication (:none)
# =====================================
client = PetstoreApiClient::ApiClient.new
client.configure do |config|
  config.auth_strategy = :none
end
pet = client.create_pet(
  name: "RailsPet-NoAuth-#{Time.now.to_i}",
  photo_urls: ["https://example.com/rails-pet.jpg"]
)
puts "โœ… Created with :none auth: #{pet['name']}"

# =====================================
# Strategy 2: API Key Authentication
# =====================================
client = PetstoreApiClient::ApiClient.new
client.configure do |config|
  config.auth_strategy = :api_key
  config.api_key = ENV['PETSTORE_API_KEY']  # or 'special-key'
  # Alternative: config.api_key = :from_env (reads from PETSTORE_API_KEY)
end
pet = client.create_pet(
  name: "RailsPet-ApiKey-#{Time.now.to_i}",
  photo_urls: ["https://example.com/rails-pet.jpg"]
)
puts "โœ… Created with :api_key auth: #{pet['name']}"

# =====================================
# Strategy 3: OAuth2 Authentication
# =====================================
client = PetstoreApiClient::ApiClient.new
client.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.oauth2_scope = 'read:pets write:pets'  # Optional
end
pet = client.create_pet(
  name: "RailsPet-OAuth2-#{Time.now.to_i}",
  photo_urls: ["https://example.com/rails-pet.jpg"]
)
puts "โœ… Created with :oauth2 auth: #{pet['name']}"

# =====================================
# Strategy 4: Dual Authentication (:both)
# =====================================
client = PetstoreApiClient::ApiClient.new
client.configure do |config|
  config.auth_strategy = :both
  config.api_key = ENV['PETSTORE_API_KEY']
  config.oauth2_client_id = ENV['PETSTORE_OAUTH2_CLIENT_ID']
  config.oauth2_client_secret = ENV['PETSTORE_OAUTH2_CLIENT_SECRET']
end
pet = client.create_pet(
  name: "RailsPet-Both-#{Time.now.to_i}",
  photo_urls: ["https://example.com/rails-pet.jpg"]
)
puts "โœ… Created with :both auth: #{pet['name']}"

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.message}")
    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:

  • โŒ .env files
  • โŒ credentials.json
  • โŒ *.pem, *.key files
  • โŒ 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:

[![CI](https://github.com/hammadxcm/petstore-api-client/workflows/CI/badge.svg)](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 client
  • faraday-retry (~> 2.0) - Auto-retry middleware
  • oauth2 (~> 2.0) - OAuth2 client
  • activemodel (>= 6.0) - Validations

Development:

  • rspec (~> 3.12) - Testing
  • vcr (~> 6.0) - HTTP recording
  • simplecov (~> 0.22) - Coverage
๐Ÿค Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

How to contribute:

  1. ๐Ÿด Fork it (https://github.com/hammadxcm/petstore-api-client/fork)
  2. ๐ŸŒฟ Create feature branch (git checkout -b feature/amazing-feature)
  3. โœ… Add tests for your changes
  4. ๐Ÿงช Run tests (bundle exec rspec)
  5. ๐Ÿ” Run linter (bundle exec rubocop)
  6. ๐Ÿ’พ Commit (git commit -m 'Add amazing feature')
  7. ๐Ÿ“ค Push (git push origin feature/amazing-feature)
  8. ๐ŸŽ‰ 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


๐Ÿพ Made with โค๏ธ for the Ruby community by @hammadxcm

Ruby OAuth2 License GitHub

Quick Start โ€ข Authentication โ€ข Examples โ€ข Contributing โ€ข Issues

Repository: github.com/hammadxcm/petstore-api-client

About

Production-ready Ruby client for Swagger Petstore API with OAuth2 support

Resources

License

Stars

Watchers

Forks

Packages

No packages published