SkillAgentSearch skills...

Explicit

Explicit is a validation and documentation library for REST APIs that enforces documented types at runtime

Install / Use

/learn @luizpvas/Explicit
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Explicit

Explicit is a validation and documentation library for REST APIs that enforces documented types at runtime.

| Documentation example screenshot | | :-----------------------------------------------------------------------------------------------------------------------------------: | | Click here to visit the example documentation page |

  1. Installation
  2. Defining requests
  3. Reusing types
  4. Reusing requests
  5. Writing tests
  6. Publishing documentation
  7. MCP
  8. Types
  9. Configuration

Installation

Add the following line to your Gemfile and then run bundle install:

gem "explicit", "~> 0.2"

Defining requests

Call Explicit::Request.new to define a request. The following methods are available:

  • get(path) - Adds a route to the request. Use the syntax /:param for path params.
    • There is also head, post, put, delete, options and patch for other HTTP verbs.
  • title(text) - Adds a title to the request. Displayed in documentation.
  • description(text) - Adds a description to the endpoint. Displayed in documentation. Markdown supported.
  • header(name, type, options = {}) - Adds a type to the request header.
    • The following options are available:
      • auth: :bearer - Sets the authentication to bearer.
      • auth: :basic - Sets the authentication to basic.
  • param(name, type, options = {}) - Adds a type to the request param. It works for params in the request body, query string and path params.
    • The following options are available:
      • optional: true - Makes the param nilable.
      • default: value - Sets a default value to the param, which makes it optional.
      • description: "text" - Adds a documentation to the param. Markdown supported.
  • response(status, type) - Adds a response type. You can add multiple responses with different formats.
  • example(params:, headers:, response:) - Adds an example to the documentation. See more details here.
  • base_url(url) - Sets the host for this API. For example: "https://api.myapp.com". Meant to be used with request composition.
  • base_path(prefix) - Sets a prefix for the routes. For example: "/api/v1". Meant to be used with request composition.

For example:

class RegistrationsController < ActionController::API
  Request = Explicit::Request.new do
    post "/api/registrations"

    description <<~MD
    Attempts to register a new user in the system. If `payment_type` is not
    specified a trial period of 30 days is started.
    MD

    param :name, [:string, empty: false]
    param :email, [:string, format: URI::MailTo::EMAIL_REGEXP, strip: true]
    param :payment_type, [:enum, ["free_trial", "credit_card"]], default: "free_trial"
    param :terms_of_use, :agreement

    response 200, { user: { id: :integer, email: :string } }
    response 422, { error: "email_already_taken" }
  end

  def create
    Request.validate!(params) => { name:, email:, payment_type: }

    user = User.create!(name:, email:, payment_type:)

    render json: { user: user.as_json(only: %[id email]) }
  rescue ActiveRecord::RecordNotUnique
    render json: { error: "email_already_taken" }, status: 422
  end
end

Reusing types

Types are just data. You can share types the same way you reuse constants or configs in your app. For example:

module MyApp::Type
  UUID = [:string, format: /^\h{8}-(\h{4}-){3}\h{12}$/].freeze
  EMAIL = [:string, format: URI::MailTo::EMAIL_REGEXP, strip: true].freeze

  ADDRESS = {
    country_name: [:string, empty: false],
    zipcode: [:string, format: /\d{6}-\d{3}/]
  }.freeze
end

# ... and then reference the shared types when needed
Request = Explicit::Request.new do
  param :customer_uuid, MyApp::Type::UUID
  param :email, MyApp::Type::EMAIL
  param :address, MyApp::Type::ADDRESS
end

Reusing requests

Sometimes it is useful to share a group of params, headers or responses between requests. You can achieve this by instantiating requests from an existing request instead of Explicit::Request. For example:

AuthenticatedRequest = Explicit::Request.new do
  base_url "https://my-app.com"
  base_path "/api/v1"

  header "Authorization", [:string, format: /Bearer [a-zA-Z0-9]{20}/], auth: :bearer

  response 403, { error: "unauthorized" }
end

Request = AuthenticatedRequest.new do
  # Request inherits all definitions from AuthenticatedRequest.
  # Any change you make to params, headers, responses or examples will add to
  # existing definitions.
end

Writing tests

Include Explicit::TestHelper in your test/test_helper.rb or spec/rails_helper.rb. This module provides the method fetch(request, **options) that let's you verify the endpoint works as expected and that it responds with a valid response according to the docs.

<details open> <summary>For Minitest users, add the following line to your <code>test/test_helper.rb</code></summary>
module ActiveSupport
  class TestCase
    fixtures :all

    # Run tests in parallel with specified workers
    parallelize(workers: :number_of_processors)

+   include Explicit::TestHelper
  end
end
</details> <details open> <summary>For RSpec users, add the following line to your <code>spec/rails_helper.rb</code></summary>
RSpec.configure do |config|
+  config.include Explicit::TestHelper
end
</details>

To test your controller, call fetch(request, **options) and write assertions against the response. If the response is invalid the test fails with Explicit::Request::InvalidResponseError.

The response object has a status, an integer value for the http status, and data, a hash with the response data. It also provides dig for a slighly shorter syntax when accessing nested attributes.

Path params are matched by name, so if you have an endpoint configured with put "/customers/:customer_id" you must call as fetch(CustomerController::UpdateRequest, { customer_id: 123 }).

Note: Response types are only verified in test environment with no performance penalty when running in production.

<details open> <summary>Minitest example</summary>
class API::V1::RegistrationsControllerTest < ActionDispatch::IntegrationTest
  test "successful registration" do
    response = fetch(RegistrationsController::Request, params: {
      name: "Bilbo Baggins",
      email: "bilbo@shire.com",
      payment_type: "free_trial",
      terms_of_use: true
    })

    assert_equal 200, response.status
    assert_equal "bilbo@shire.com", response.dig(:user, :email)
  end
end
</details> <details open> <summary>RSpec example</summary>
describe RegistrationsController::Request, type: :request do
  context "when request params are valid" do
    it "successfully registers a new user" do
      response = fetch(described_class, params: {
        name: "Bilbo Baggins",
        email: "bilbo@shire.com",
        payment_type: "free_trial",
        terms_of_use: true
      })

      expect(response.status).to eql(200)
      expect(response.dig(:user, :email)).to eql("bilbo@shire.com")
    end
  end
end
</details>

Publishing documentation

Call Explicit::Documentation.new to group, organize and publish the documentation for your API. The following methods are available:

  • page_title(text) - Sets the web page title.
  • company_logo_url(url) - Shows the company logo in the navigation menu. The url can also be a lambda that returns the url, useful for referencing assets at runtime.
  • favicon_url(url) - Adds a favicon to the web page.
  • version(semver) - Sets the version of the API. Default: "1.0"
  • section(name, &block) - Adds a section to the navigation menu.
  • add(request) - Adds a request to the section
  • add(title:, partial:) - Adds a partial to the section

For example:

module MyApp::API::V1
  Documentation = Explicit::Documentation.new do
    page_title "Acme API Docs"
    company_logo_url "https://my-app.com/logo.png"
    favicon_url "https://my-app.com/favicon.ico"
    version "1.0.5"

    section "Introduction" do
      add title: "About", partial: "api/v1/introduction/about"
    end

    section "Auth" do
      add RegistrationsController::CreateRequest
      add SessionsController::CreateRequest
      add SessionsController::DestroyRequest
    end

    section "Articles" do
      add ArticlesController::CreateRequest
      add ArticlesController:
View on GitHub
GitHub Stars8
CategoryDevelopment
Updated5mo ago
Forks0

Languages

Ruby

Security Score

82/100

Audited on Oct 14, 2025

No findings