Project

terrain

0.0
No commit activity in last 3 years
No release in over 3 years
Terrain
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

 Project Readme

Terrain

Build Status

Opinionated toolkit for building CRUD APIs with Rails

Install

Add Terrain to your Gemfile:

gem 'terrain'

Usage

  • Error handling
  • Resources
    • Authorization
    • Serialization
    • Querying
      • Filtering
      • Ordering
      • Pagination
      • Relationships
    • CRUD operations
  • Config

Error handling

class ExampleController < ApplicationController
  include Terrain::Errors
end

Rescues the following errors:

  • ActiveRecord::AssociationNotFoundError (400)
  • Pundit::NotAuthorizedError (403)
  • ActiveRecord::RecordNotFound (404)
  • ActionController::RoutingError (404)
  • ActiveRecord::RecordInvalid (422)

JSON responses are of the form:

{
  "error": {
    "key": "type_of_error",
    "message": "Localized error message",
    "details": "Optional details"
  }
}

To rescue a custom error with a similar response:

class ExampleController < ApplicationController
  include Terrain::Errors

  rescue_from MyError, with: :my_error

  private

  def my_error
    error_response(:type_of_error, 500, { some: :details })
  end
end

Resources

Suppose you have an Example model with foo, bar, and baz columns.

class ExampleController < ApplicationController
  include Terrain::Resource

  resource Example, permit: [:foo, :bar, :baz]
end

This sets up the typical resourceful Rails controller actions. Note that you'll still need to setup corresponding routes.

Authorization

Authorization is handled by Pundit. If the policy class for a given resource exists, each controller action calls the policy before proceeding with the operation. Authorization expects a current_user controller method to exist (otherwise nil is used as the pundit_user).

Serialization

via ActiveModelSerializers

Querying

Records of a given resource are queried by requesting the index action.

Filtering

Queries are scoped to the results returned from the resource_scope method. By default this returns all records, however, you can override it to further filter the results (i.e. based on query params, nested route params, etc.):

class ExampleController < ApplicationController
  include Terrain::Resource

  resource Example, permit: [:foo, :bar, :baz]

  private

  def resource_scope
    scope = super
    scope = scope.where(foo: params[:foo]) if params[:foo].present?
    scope
  end
end
Ordering

You can pass an order param to reorder the response records. Specify a comma-separated list of fields and prefix the field with a - for descending order:

# corresponds to Example.order('foo', 'bar desc')
get :index, order: 'foo,-bar'
Pagination

To request a range of records, specify the range in an HTTP header:

# Request the first 10 records
get :index, {}, { 'Range' => '0-9' }

All responses include a Content-Range header that specifies the exact range returned as well as a total count of records. i.e.

Content-Range: 0-9/100

You can also pass open ended ranges such as 10- (i.e. skip the first 10 records).

Relationships

No model relationships are serialized in the response by default. To specify the set of relationships to be embedded in the response, pass a comma-separated list of relationships in the include param.

As an example, suppose we're querying for posts which each have many tags and belong to an author. We could embed those relationships with the following include param:

get :index, include: 'author,tags'

Suppose now that the author also has a profile relationship. We could include the author, author profile and tags by passing:

get :index, include: 'author.profile,tags'

Included relationships are automatically preloaded via the ActiveRecord includes method. The include param is also supported in show actions.

CRUD operations

You may need an action to perform additional steps beyond simple persistence. There are methods for performing CRUD operations that can be overridden (shown below with their default implementation):

class ExampleController < ApplicationController
  include Terrain::Resource

  resource Example, permit: [:foo, :bar, :baz]

  private

  def create_record
    resource.create!(permitted_params)
  end

  def update_record(record)
    record.update_attributes!(permitted_params)
    record
  end

  def destroy_record(record)
    record.delete
  end
end

Config

Terrain.configure do |config|
  # Maximum number of records returned
  config.max_records = Float::INFINITY
end