Skip to content

Latest commit

 

History

History
500 lines (389 loc) · 8.94 KB

File metadata and controls

500 lines (389 loc) · 8.94 KB

Defining Contracts

← Back to Documentation

Overview

Learn how to define Treaty contracts, including request and response definitions, and service delegation. This guide covers the complete structure of a contract and best practices for organizing your API definitions.

Contract Structure

A Treaty contract consists of:

  1. Class definition - inheriting from Treaty::Action::Base
  2. Version blocks - one or more version definitions
  3. Request definition - what data comes in
  4. Response definition(s) - what data goes out
  5. Delegation - where to process the request

Basic Contract

module Gate
  module API
    module Posts
      class CreateTreaty < ApplicationTreaty
        version 1 do

          request do
            object :post do
              string :title
              string :content
            end
          end

          response 201 do
            object :post do
              string :id
              string :title
              string :content
              time :created_at
            end
          end

          delegate_to Posts::CreateService
        end
      end
    end
  end
end

Version Definition

Simple Version

version 1 do
  # Version configuration
end

Default Version

version 2, default: true do
  # This version is used when no version specified
end

Version with Metadata

version 3 do
  summary "Added support for post categories and tags"

  deprecated(lambda do
    Gem::Version.new(ENV.fetch("RELEASE_VERSION", "0.0.0")) >=
      Gem::Version.new("4.0.0")
  end)


  # ... rest of definition
end

Request Definition

Single Object

request do
  object :post do
    string :title
    string :content
  end
end

Multiple Objects

request do
  object :post do
    string :title
    string :content
    string :summary, :optional
  end

  object :filters do
    string :category, :optional
    array :tags, :optional do
      string :_self
    end
  end
end

With Format Validation

request do
  object :user do
    string :email, format: :email
    string :password, format: {
      is: :password,
      message: "Password must be 8-16 characters with digit, lowercase, and uppercase"
    }
    string :birth_date, :optional, format: :date
    string :external_id, :optional, format: :uuid
  end
end

With Conditional Attributes

request do
  object :post do
    string :title
    string :content
    string :status, in: %w[draft published]

    # Published date only for published posts
    datetime :published_at, :optional,
             if: ->(post:) { post[:status] == "published" }

    # Draft notes only for draft posts
    string :draft_notes, :optional,
           unless: ->(post:) { post[:status] == "published" }
  end
end

With Computed Attributes

request do
  object :post do
    string :title
    string :content

    # Computed: derive slug from title
    string :slug, :optional,
           computed: ->(**attributes) { attributes.dig(:post, :title).to_s.downcase.gsub(/\s+/, "-") }

    # Computed: calculate word count from content
    integer :word_count, :optional,
            computed: ->(**attributes) { attributes.dig(:post, :content).to_s.split.size }

    object :author do
      string :first_name
      string :last_name

      # Computed: combine first and last name
      string :full_name, :optional,
             computed: (lambda do |**attributes|
               "#{attributes.dig(:post, :author, :first_name)} #{attributes.dig(:post, :author, :last_name)}"
             end)
    end
  end
end

Root Level Attributes (:_self object)

request do
  # These attributes go to root level
  object :_self do
    string :signature
    string :timestamp
  end

  # These go under 'post' key
  object :post do
    string :title
  end
end

# Expects data like:
# {
#   signature: "abc123",
#   timestamp: "2024-01-01",
#   post: { title: "Hello" }
# }

Empty Object (Declaration Only)

request do
  object :post
  object :filters
end

# Just declares that these objects exist

Response Definition

Single Status Code

response 200 do
  array :posts do
    string :id
    string :title
  end
end

Multiple Status Codes

response 200 do
  array :posts do
    string :id
    string :title
  end

  object :meta do
    integer :count
    integer :page
  end
end

response 201 do
  object :post do
    string :id
    string :title
    time :created_at
  end
end

response 422 do
  object :errors do
    string :message
  end
end

Delegation

Service Class

# Constant
delegate_to Posts::CreateService

# String (constant)
delegate_to "Posts::CreateService"

# String (path, will be constantized)
delegate_to "posts/create_service"

Lambda Function

delegate_to(lambda do |params:|
  # Process request directly
  post = Post.create!(params[:post])
  { post: post.as_json }
end)

With Options

# Specify method and return processing
delegate_to Posts::CreateService => :call, return: lambda(&:data)

# Service will be called as:
# service = Posts::CreateService.call(params: validated_params)
# result = return_lambda.call(service)

Inventory

Provide controller-specific data to services:

class PostsController < ApplicationController
  treaty :index do
    provide :current_user
    provide :posts, from: :load_posts
    provide :meta, from: -> { { timestamp: Time.current } }
  end

  private

  def load_posts
    Post.where(user: current_user).published
  end
end

Services receive inventory as a parameter:

class Posts::IndexService
  def self.call(inventory:, params:)
    current_user = inventory.current_user
    posts = inventory.posts
    # ...
  end
end

See Inventory System for detailed documentation.

Multiple Requests

You can define multiple request blocks that will be merged:

request do
  # Query parameters
  object :_self do
    string :signature
  end
end

request do
  # Body parameters
  object :post do
    string :title
    string :content
  end
end

Best Practices

1. One Contract Per Action

# Good
class Posts::CreateTreaty < ApplicationTreaty
  # Handles posts#create
end

class Posts::UpdateTreaty < ApplicationTreaty
  # Handles posts#update
end

# Bad - don't handle multiple actions in one treaty

2. Meaningful Version Numbers

# Good
version 1 do
  summary "Initial release"
end

version 2 do
  summary "Added author support"
end

version 3 do
  summary "Added categories and tags"
end

# Bad - no context about changes
version 1 do; end
version 2 do; end

3. Always Set a Default Version

version 3, default: true do
  # This is the current production version
end

4. Document Deprecation

version 1 do
  summary "Initial release - DEPRECATED"

  deprecated(lambda do
    # Will be removed in version 4.0.0
    Gem::Version.new(ENV["RELEASE_VERSION"]) >= Gem::Version.new("4.0.0")
  end)

  # ... rest of definition
end

Complete Example

Here's a complete example from the sandbox:

module Gate
  module API
    module Posts
      class CreateTreaty < ApplicationTreaty
        version 3 do
          summary "Added author and socials to expand post data"


          request do
            object :_self do
              string :signature
            end
          end

          request do
            object :post do
              string :title
              string :summary
              string :description, :optional
              string :content

              array :tags, :optional do
                string :_self
              end

              object :author do
                string :name
                string :bio

                array :socials, :optional do
                  string :provider, in: %w[twitter linkedin github]
                  string :handle, as: :value
                end
              end
            end
          end

          response 201 do
            object :post do
              string :id
              string :title
              string :summary
              string :description
              string :content

              array :tags do
                string :_self
              end

              object :author do
                string :name
                string :bio

                array :socials do
                  string :provider
                  string :value, as: :handle
                end
              end

              integer :rating
              integer :views

              time :created_at
              time :updated_at
            end
          end

          delegate_to "posts/stable/create_service"
        end
      end
    end
  end
end

Next Steps

  • Attributes - Learn about attribute types and options
  • Objects - Understand object organization
  • Versioning - Manage multiple API versions

← Back to Documentation