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.,
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 validationApiRegulator::UnexpectedParams
: Raised when unexpected parameters are providedApiRegulator::ValidationError
: Base class for validation errors
Development and Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/new-feature
) - Write tests for your changes
- Ensure all tests pass (
bundle exec rspec
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin feature/new-feature
) - Open a pull request
Running Tests
bundle install
bundle exec rspec
Releasing New Versions
ApiRegulator is published to RubyGems. To release:
- Update version in
lib/api_regulator/version.rb
- Run
bundle install
to updateGemfile.lock
- Create PR and merge after review
- Trigger the Release Gem workflow
- 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.