ApiRegulator

ApiRegulator is a Ruby gem designed to document, validate, and generate OpenAPI schemas for Rails APIs. It provides a clean, Rails-friendly DSL for defining API endpoints, parameter validations, response schemas, and webhook definitions directly in your controllers, while automatically generating OpenAPI 3.1.0-compliant documentation.

ApiRegulator leverages Active Model validations for parameter validation, making it familiar and intuitive for Rails developers.

Features

  • 🎯 API Documentation DSL: Define API endpoints, request parameters, and response schemas using an intuitive, Rails-friendly DSL
  • ✅ Dynamic Request Validation: Automatically validate incoming requests against defined parameters using Active Model validations
  • 📖 OpenAPI 3.1.0 Generation: Generate fully compliant OpenAPI documentation ready for Swagger, Postman, or ReadMe
  • 🔗 Webhook Documentation: Define and document webhook payloads with the same DSL
  • 🏷️ API Versioning: Support multiple API versions with selective parameter inclusion
  • 🔒 Security Schemes: Define authentication and authorization schemes
  • 🔄 Shared Schemas: Reuse common response and parameter schemas across multiple endpoints
  • 📚 ReadMe Integration: Built-in tasks for uploading documentation to ReadMe.com
  • 🛡️ Parameter Validation: Strict validation with helpful error messages for unexpected parameters
  • 📄 Custom Page Management: Upload and manage documentation pages alongside API specs

Installation

Add ApiRegulator to your Gemfile:

gem 'api_regulator'

Run bundle install to install the gem.

Setup

1. Create an Initializer

Add the following to config/initializers/api_regulator.rb:

ApiRegulator.configure do |config|
  config.api_base_url = "/api/v1"
  config.docs_path = Rails.root.join("doc").to_s
  config.app_name = "My API"

  # Optional: Configure multiple API versions
  config.versions = {
    "v1.0" => "abc123",           # ReadMe API spec ID
    "v1.0-internal" => "abc345"   # Internal version
  }
  config.default_version = "v1.0-internal"

  # Optional: Define API servers
  config.servers = [
    { url: "https://stg.example.com", description: "Staging", "x-default": true },
    { url: "https://example.com", description: "Production" }
  ]
end

2. Include the DSL in Your Base Controller

class Api::ApplicationController < ActionController::API
  include ApiRegulator::DSL
  include ApiRegulator::ControllerMixin
end

3. Define Security Schemes (Optional)

# In your initializer
ApiRegulator.security_schemes = {
  bearer_auth: {
    type: "http",
    scheme: "bearer",
    bearerFormat: "JWT"
  },
  api_key: {
    type: "apiKey",
    in: "header",
    name: "X-API-Key"
  }
}

Usage

Basic API Definition

class Api::V1::CustomersController < Api::ApplicationController
  api self, :create, desc: "Create a new customer", title: "Create Customer" do
    # Path parameters
    param :organization_id, :string, location: :path, presence: true

    # Query parameters  
    param :expand, :string, location: :query, desc: "Comma-separated list of fields to expand"

    # Body parameters
    param :customer, presence: true do
      param :first_name, :string, presence: true
      param :last_name, :string, presence: true
      param :email, :string, presence: true, format: { with: ApiRegulator::Formats::EMAIL }
      param :ssn, :string, presence: true, length: { is: 9 }
      param :date_of_birth, :string, format: { with: ApiRegulator::Formats::DATE }

      # Nested objects
      param :address, :object do
        param :street, :string, presence: true
        param :city, :string, presence: true
        param :state, :string, presence: true, length: { is: 2 }
        param :zip_code, :string, format: { with: ApiRegulator::Formats::ZIP_CODE }
      end

      # Arrays
      param :phone_numbers, :array, item_type: :string
      param :emergency_contacts, :array do
        param :name, :string, presence: true
        param :relationship, :string, presence: true
        param :phone, :string, presence: true
      end
    end

    # Response definitions
    response 201, "Customer successfully created" do
      param :customer do
        param :id, :string, desc: "Customer UUID"
        param :email, :string, desc: "Customer email address"
        param :created_at, :string, desc: "ISO 8601 timestamp"
      end
    end

    response 422, ref: :validation_errors
    response 401, ref: :unauthorized_error
  end

  def create
    validate_params! # Validates against DSL definition

    customer = Customer.create!(api_params[:customer])
    render json: { customer: customer }, status: :created
  end
end

Advanced Parameter Options

Conditional Requirements

param :ssn, :string, presence: { 
  required_on: [:create, :update],           # Required only for these actions
  required_except_on: [:show]                # Required except for these actions
}

Version-Specific Parameters

param :legacy_field, :string, versions: [:v1], desc: "Only available in v1"
param :new_feature, :boolean, versions: [:v2, :v3], desc: "Available in v2 and v3"

Type Validation and Formatting

param :age, :integer, numericality: { greater_than: 0, less_than: 150 }
param :website, :string, format: { with: ApiRegulator::Formats::URI }
param :score, :number, numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 }
param :is_active, :boolean
param :tags, :array, item_type: :string, inclusion: { in: ["vip", "standard", "premium"] }

Arbitrary Keys Support

param :metadata, :object, allow_arbitrary_keys: true do
  param :created_by, :string  # Known fields can still be defined
end

Nullable Fields

param :middle_name, :string, presence: { allow_nil: true }
param :optional_date, :string, format: { with: ApiRegulator::Formats::DATE, allow_nil: true }

Shared Schemas

Define reusable schemas in your initializer:

# Common error responses
ApiRegulator.register_shared_schema :validation_errors, "Validation error response" do
  param :errors, :array, desc: "Array of validation error messages" do
    param :field, :string, desc: "Field name that failed validation"
    param :message, :string, desc: "Human-readable error message"
    param :code, :string, desc: "Error code for programmatic handling"
  end
end

ApiRegulator.register_shared_schema :unauthorized_error, "Authentication required" do
  param :error, :string, desc: "Error message"
  param :code, :integer, desc: "HTTP status code"
end

# Common response objects
ApiRegulator.register_shared_schema :pagination_meta, "Pagination metadata" do
  param :current_page, :integer, desc: "Current page number"
  param :per_page, :integer, desc: "Items per page"
  param :total_pages, :integer, desc: "Total number of pages"
  param :total_count, :integer, desc: "Total number of items"
end

Use shared schemas in your API definitions:

api self, :index do
  param :page, :integer, location: :query, desc: "Page number"
  param :per_page, :integer, location: :query, desc: "Items per page"

  response 200, "List of customers" do
    param :customers, :array do
      ref :customer_summary  # Reference another shared schema
    end
    ref :pagination_meta
  end
end

Webhook Documentation

Define webhook payloads using the same DSL:

class WebhookDefinitions < Api::ApplicationController
  webhook :customer_created, 
    desc: "Fired when a new customer is created",
    title: "Customer Created",
    tags: ["customers", "webhooks"] do

    param :event, :string, desc: "Event name"
    param :timestamp, :string, desc: "ISO 8601 timestamp"
    param :data do
      param :customer do
        param :id, :string, desc: "Customer UUID"
        param :email, :string, desc: "Customer email"
        param :created_at, :string, desc: "ISO 8601 timestamp"
      end
    end

    example :basic_example, {
      event: "customer.created",
      timestamp: "2024-01-15T10:30:00Z",
      data: {
        customer: {
          id: "cust_abc123",
          email: "[email protected]",
          created_at: "2024-01-15T10:30:00Z"
        }
      }
    }, default: true
  end
end

API Examples

Add examples to your API definitions:

api self, :create do
  param :customer do
    param :name, :string, presence: true
    param :email, :string, presence: true
  end

  example :successful_creation, {
    customer: {
      name: "John Doe",
      email: "[email protected]"
    }
  }, default: true

  example :with_optional_fields, {
    customer: {
      name: "Jane Smith",
      email: "[email protected]",
      phone: "+1-555-0123"
    }
  }
end

Validation and Error Handling

Automatic Parameter Validation

def create
  validate_params! # Raises ApiRegulator::InvalidParams or ApiRegulator::UnexpectedParams

  # Access validated and type-converted parameters
  customer_data = api_params[:customer]
  # Process with confidence that data is valid
end

Custom Error Handling

class Api::ApplicationController < ActionController::API
  include ApiRegulator::DSL
  include ApiRegulator::ControllerMixin

  rescue_from ApiRegulator::InvalidParams do |exception|
    render json: { 
      errors: exception.errors.messages,
      message: "Validation failed" 
    }, status: :unprocessable_entity
  end

  rescue_from ApiRegulator::UnexpectedParams do |exception|
    render json: { 
      errors: exception.details,
      message: "Unexpected parameters provided" 
    }, status: :bad_request
  end
end

OpenAPI Documentation Generation

Generate Documentation

# Generate for default version
rake api_docs:generate

# Generate for specific version
VERSION=v1.0 rake api_docs:generate

# Generate for all configured versions
rake api_docs:generate_all

Upload to ReadMe

Set up your ReadMe credentials:

export RDME_API_KEY="your_readme_api_key"
# Upload API specification
rake api_docs:upload

# Upload custom documentation pages (with YAML frontmatter)
rake api_docs:upload_pages

# Generate and upload everything
rake api_docs:publish

Custom Documentation Pages

Create markdown files in your docs_path with YAML frontmatter:

---
title: "Getting Started"
slug: "getting-started"
category: "documentation"
hidden: false
---

# Getting Started

Your API documentation content here...

ReadMe Integration Features

# Fetch available categories
rake api_docs:fetch_categories

# Upload with specific version
VERSION=v2.0 rake api_docs:upload

Built-in Format Validators

ApiRegulator includes common format validators:

ApiRegulator::Formats::DATE          # ISO 8601 date format
ApiRegulator::Formats::DATETIME      # ISO 8601 datetime format  
ApiRegulator::Formats::EMAIL         # Email address validation
ApiRegulator::Formats::ZIP_CODE      # US ZIP code (12345 or 12345-6789)
ApiRegulator::Formats::URI           # URI format validation

Usage example:

param :email, :string, format: { with: ApiRegulator::Formats::EMAIL }
param :website, :string, format: { with: ApiRegulator::Formats::URI }
param :birth_date, :string, format: { with: ApiRegulator::Formats::DATE }

Testing Your APIs

ApiRegulator integrates seamlessly with RSpec:

RSpec.describe Api::V1::CustomersController do
  describe "POST /api/v1/customers" do
    it "validates required parameters" do
      post "/api/v1/customers", params: {}
      expect(response).to have_http_status(:unprocessable_entity)
    end

    it "creates customer with valid parameters" do
      params = {
        customer: {
          first_name: "John",
          last_name: "Doe", 
          email: "[email protected]"
        }
      }

      post "/api/v1/customers", params: params
      expect(response).to have_http_status(:created)
    end
  end
end

Configuration Reference

ApiRegulator.configure do |config|
  # Base URL for all API endpoints (default: "api/v1")
  config.api_base_url = "/api/v1"

  # Application name shown in documentation (default: "API Documentation")
  config.app_name = "My API"

  # Directory for documentation files (default: "doc")
  config.docs_path = Rails.root.join("doc").to_s

  # Version mapping for ReadMe (optional)
  config.versions = {
    "v1.0" => "readme_spec_id_1",
    "v2.0" => "readme_spec_id_2"
  }

  # Default version when none specified (optional)
  config.default_version = "v1.0"

  # Server definitions for OpenAPI spec (optional)
  config.servers = [
    { url: "https://api.example.com", description: "Production" },
    { url: "https://staging-api.example.com", description: "Staging" }
  ]
end

Advanced Usage

Multiple API Versions

# Define version-specific endpoints
api self, :create, versions: [:v1, :v2] do
  param :name, :string, presence: true
  param :email, :string, presence: true, versions: [:v1, :v2] 
  param :phone, :string, versions: [:v2]  # Only in v2
end

Security Requirements

api self, :create do
  # This endpoint requires authentication
  security [{ bearer_auth: [] }]

  param :customer do
    param :name, :string, presence: true
  end
end

Custom Validation Context

def update
  # Validate with specific context
  validate_params!

  # The validation context is automatically set to the action name (:update)
  # This allows conditional validations based on the action
end

Error Reference

  • ApiRegulator::InvalidParams: Raised when request parameters fail validation
  • ApiRegulator::UnexpectedParams: Raised when unexpected parameters are provided
  • ApiRegulator::ValidationError: Base class for validation errors

Development and Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/new-feature)
  3. Write tests for your changes
  4. Ensure all tests pass (bundle exec rspec)
  5. Commit your changes (git commit -am 'Add some feature')
  6. Push to the branch (git push origin feature/new-feature)
  7. Open a pull request

Running Tests

bundle install
bundle exec rspec

Releasing New Versions

ApiRegulator is published to RubyGems. To release:

  1. Update version in lib/api_regulator/version.rb
  2. Run bundle install to update Gemfile.lock
  3. Create PR and merge after review
  4. Trigger the Release Gem workflow
  5. Verify publication on RubyGems

Requirements

  • Ruby >= 3.0
  • Rails >= 8.0 (ActiveSupport and ActiveModel)

License

This gem is available as open-source software under the MIT License.