Skip to content

Latest commit

 

History

History
321 lines (238 loc) · 7.3 KB

README.md

File metadata and controls

321 lines (238 loc) · 7.3 KB

ApiVerifier

An Elixir library for defining and verifying API response contracts.

Features

  • DSL for contract definition: Define API contracts using an intuitive macro-based DSL
  • Pure validation logic: All validation is side-effect free and testable
  • Error accumulation: Collects all validation errors at once, not just the first one
  • Type checking: Supports integer, string, boolean, float, array, and object types
  • Optional fields: Define required and optional response fields
  • Array validation: Validate arrays with element type checking
  • No external dependencies: No Phoenix, no HTTP clients, just pure Elixir logic

Installation

Add api_verifier to your mix.exs:

def deps do
  [
    {:api_verifier, path: "."} # or from hex when published
  ]
end

Then run:

mix deps.get

Quick Start

1. Define a Contract

Create a module that uses the ApiVerifier DSL:

defmodule MyAPI.Contracts do
  use ApiVerifier.DSL

  contract "GetUser" do
    response 200 do
      field :id, :integer
      field :name, :string
      field :email, :string
      field :active, :boolean
      field :tags, :array, array_type: :string
      field :metadata, :object, required: false
    end

    response 404 do
      field :error, :string
      field :message, :string
    end
  end
end

2. Get the Contract

contract = ApiVerifier.DSL.get_contract(MyAPI.Contracts, "GetUser")

3. Validate a Response

response_data = %{
  id: 123,
  name: "Alice",
  email: "alice@example.com",
  active: true,
  tags: ["admin", "user"]
}

case ApiVerifier.validate(contract, 200, response_data) do
  {:ok, data} ->
    IO.puts("Response is valid!")
    IO.inspect(data)

  {:error, errors} ->
    IO.puts("Response validation failed:")
    Enum.each(errors, fn error ->
      IO.inspect(error)
    end)
end

Supported Types

  • :string - Expects a binary value
  • :integer - Expects an integer
  • :float - Expects a float or integer
  • :boolean - Expects true or false
  • :array - Expects a list (optionally with element type checking via array_type: option)
  • :object - Expects a map

Field Options

required (default: true)

Mark a field as optional:

field :description, :string, required: false

array_type

Specify the type of elements in an array:

field :tags, :array, array_type: :string
field :ids, :array, array_type: :integer

Error Types

The validator returns structured errors with the following error types:

  • :missing_field - A required field is missing from the response
  • :type_mismatch - A field has the wrong type
  • :unexpected_field - The response contains a field not in the contract
  • :no_response_spec - No specification exists for the given status code

Error Structure

Each error is a ApiVerifier.Validator.ValidationError struct with:

  • field - The field name (or nil for status/spec-level errors)
  • error - The error type (atom)
  • expected - What was expected
  • actual - What was actually received

Examples

Example 1: Basic Contract

defmodule ProductAPI do
  use ApiVerifier.DSL

  contract "GetProduct" do
    response 200 do
      field :id, :integer
      field :name, :string
      field :price, :float
      field :in_stock, :boolean
    end
  end
end

contract = ApiVerifier.DSL.get_contract(ProductAPI, "GetProduct")
response = %{id: 1, name: "Widget", price: 9.99, in_stock: true}

{:ok, _} = ApiVerifier.validate(contract, 200, response)

Example 2: Contract with Optional Fields

defmodule UserAPI do
  use ApiVerifier.DSL

  contract "CreateUser" do
    response 201 do
      field :id, :integer
      field :name, :string
      field :email, :string
      field :bio, :string, required: false
    end
  end
end

Example 3: Contract with Arrays

defmodule BlogAPI do
  use ApiVerifier.DSL

  contract "ListPosts" do
    response 200 do
      field :posts, :array, array_type: :object
      field :total, :integer
    end
  end
end

Example 4: Multiple Contracts in One Module

defmodule PaymentAPI do
  use ApiVerifier.DSL

  contract "ProcessPayment" do
    response 200 do
      field :transaction_id, :string
      field :amount, :float
      field :status, :string
    end

    response 400 do
      field :error, :string
      field :code, :string
    end

    response 402 do
      field :error, :string
      field :message, :string
    end
  end
end

# Get contract
contract = ApiVerifier.DSL.get_contract(PaymentAPI, "ProcessPayment")

# Validate different responses
ApiVerifier.validate(contract, 200, success_response)
ApiVerifier.validate(contract, 400, error_response)
ApiVerifier.validate(contract, 402, payment_required_response)

Architecture

ApiVerifier is built with a focus on:

  1. Pure functions: All validation logic is pure and testable
  2. Compile-time DSL: Contracts are compiled at compile time into immutable data structures
  3. Separation of concerns: Macros handle the DSL, pure functions handle validation
  4. No side effects in core: All side effects live at the edges (CLI, HTTP handlers, etc.)
  5. Structured errors: All validation errors are structured data, never exceptions

Project Structure

lib/
  api_verifier.ex           # Main public API
  api_verifier/
    contract.ex             # Data structures for Contract, ResponseSpec, FieldSpec
    validator.ex            # Pure validation logic
    dsl.ex                  # Macro-based DSL for defining contracts

test/
  api_verifier_test.exs     # Integration tests
  test_helper.exs           # Test setup
  api_verifier/
    contract_test.exs       # Contract data structure tests
    validator_test.exs      # Validator logic tests
    dsl_test.exs            # DSL macro tests

Testing

Run the test suite:

mix test

Run with verbose output:

mix test --trace

Run a specific test file:

mix test test/api_verifier/validator_test.exs

Design Principles

1. Compile-Time Contracts

Contracts are defined at compile time using macros and compiled into immutable data structures. No runtime DSL evaluation.

2. Pure Validation

The Validator.validate/3 function is pure:

  • No side effects
  • Deterministic
  • Fully testable
  • Easy to compose

3. Comprehensive Error Reporting

The validator doesn't stop at the first error. It accumulates all errors and reports them together, making it easier to see all problems at once.

4. Type Safety

The library uses Elixir's type system to define contracts and validate against them. All inputs and outputs are clearly typed.

5. Extensibility

The architecture makes it easy to extend with new features:

  • New field types can be added by extending the validator
  • New error types can be added to the error structure
  • The pure validation logic can be composed with other functions

Future Enhancements

While the current milestone focuses on basic functionality, future versions could add:

  • Nested object validation (detailed validation of nested structures)
  • String pattern validation (regex, format)
  • Number range validation
  • Custom validation functions
  • Response header validation
  • HTTP status-specific error responses
  • Integration with HTTP clients
  • CLI tool for validating real API responses
  • Swagger/OpenAPI integration

License

MIT