Skip to content

PrecisionHawk/model-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

80 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

model_api

Rails REST API's made easy.

Why use model-api?

Developing REST API's in a conventional manner involves many challenges. Between the parameters, route details, payloads, responses, and API documentation, it's difficult to find an implementation strategy that minimizes repetition and organizes the details in a simple, easy-to-maintain manner. This is where model-api comes in.

With model-api, you can:

  • Consolidate your payload and response metadata for a given resource in the ActiveRecord model.
  • Reduce the lines of code required to implement your Rails-based Rest API dramatically.
  • Generate OpenAPI documentation guaranteed to be in sync with what your API actually supports.
  • Set up easily-configurable filtering, pagination, sorting, and HATEOAS link generation.
  • Render, link, create, search upon, and sort upon your resources' associated objects with ease.
  • Leverage ActiveRecord validation rules in your application's models to validate API calls.
  • Effortlessly support JSON and XML input / output formats together in the same API.
  • Use Java-style camel-case for API content while utilizing Ruby underscore convention internally.

Guides

This README file is intended to provide a brief introduction. For more detailed information, the following guides are available:

General information on the API (including enumerated constants, etc.) can be found here:

Installation

Put this in your Gemfile:

gem 'model-api'

Configuration

The model-api gem doesn't require configuration. However, if you plan to generate OpenAPI documentation, add an open_api.rb file to config/initializers and configure as follows:

OpenApi.configure do |config|

  # Default base path(s), used to scan Rails routes for API endpoints.
  config.base_paths = ['/widget-api/v1']

  # General information about your API.
  config.info = {
      title: 'Acme Widget API',
      description: "Documentation of the Acme's Widget API service",
      version: '1.0.0',
      terms_of_service: 'https://www.acme.com/widget-api/terms_of_service',

      contact: {
          name: 'Acme Corporation API Team',
          url: 'http://www.acme.com/widget-api',
          email: 'widget-api-support@acme.com'
      },

      license: {
          name: 'Apache 2.0',
          url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
      }
  }

  # Default output file path for your generated Open API JSON document.
  config.output_file_path = Rails.root.join('apidoc', 'api-docs.json')
end

Exposing a Resource

To expose a resource, start by defining the underlying model for that resource:

class Book < ActiveRecord::Base
  include ModelApi::Model
  
  # Valication rules are utilized by model-api to validate requests
  validates :name, presence: true, uniqueness: true, length: { maximum: 50 }
  validates :description, length: { maximum: 250 }
  validates :isbn, presence: true, uniqueness: true, length: { maximum: 13 }

  # Define model-level rules and metadata associated with the REST API.
  api_model(
      base_query: ->(opts) { opts[:admin] ? Book.where(public: true) : Book.all }
  )

  # Define the attributes exposed via the REST API.
  api_attributes(
      id: { filter: true, sort: true },
      name: { filter: true, sort: true },
      description: {},
      isbn: { filter: true, sort: true,
          parse: ->(v) { v.to_s.gsub(%r{[^\d]+}, '') },
          render: ->(v) { "#{v[0..2]}-#{v[3]}-#{v[4..5]}-#{v[6..11]}-#{v[12..-1]}" }
      },
      created_at: { read_only: true, filter: true },
      updated_at: { read_only: true, filter: true }
  )

end

An explanation of the options used in the api_model example above:

  • base_query - Limit data exposed by the API based on context (e.g. current user).

An explanation of the options used in the api_attributes example above:

  • filter - Allow filtering by query string parameters (e.g. ?id=123).
  • sort - Allow use of this column in the sort_by parameter.
  • read_only - Disallow column updates (via POST, PUT, or PATCH).
  • parse - Method name (e.g. :to_i) or proc / lambda for pre-processing incoming payload values. (The example lambda removes any non-digit values (such as dashes) from supplied ISBN values.)
  • render - Method name or proc / lambda that formats values returned in response payloads. (The example lambda formats the 13-digit ISBN number using the typical dash convention.)

Next, define the base controller class that all of your API controllers will extend (for this example, in app/controllers/api/v1/base_controller.rb):

  module Api
    module V1
      class BaseController < ActionController::Base
        include ModelApi::BaseController
        include ModelApi::OpenApiExtensions
        include OpenApi::Controller
        
        # OpenAPI documentation metadata shared by all endpoints, including common query string
        #  parameters, HTTP headers, and HTTP response codes.
        open_api_controller \
            query_string: {
                access_token: {
                    type: :string,
                    description: 'OAuth 2 access token query parameter',
                    required: false
                }
            },
            headers: {
                'Authorization' => {
                    type: :string,
                    description: 'Authorization header (format: "bearer &lt;access token&gt;")',
                    required: false
                }
            },
            responses: {
                200 => { description: 'Successful' },
                400 => { description: 'Not found' },
                401 => { description: 'Invalid request' },
                403 => { description: 'Not authorized (typically missing / invalid access token)' }
            }
        
        # OpenAPI documentation for common API endpoint path parameters
        open_api_path_param :book_id, description: 'Book identifier'
        
        # HATEOAS links common to all responses (e.g. a common terms-of-service link)
        def common_response_links(_opts = {})
          { 'terms-of-service' => URI(url_for(controller: '/home', action: :terms_of_service)) }
        end
      end
    end
  end

Add the resource routes to your routes.rb file:

  namespace :api do
    namespace :v1 do
      resource :books, except: [:new, :edit], param: :book_id
    end
  end

Finally, add a controller for your new resource (for this example, in app/controllers/api/v1/base_controller.rb):

  module Api
    module V1
      class BooksController < BaseController
        class << self
        
          # Default model class to use for API endpoints in this controller
          def model_class
            Book
          end
          
          # Default options for model-api helper methods used to process requests to endpoints 
          def base_api_options
            super.merge(id_param: :book_id)
          end
        end
        
        # OpenAPI metadata describing the collective set of endpoints defined in this controller 
        open_api_controller \
            tag: {
            name: 'Books',
            description: 'Comprehensive list of available books'
        }

        # GET /api/v1/books endpoint OpenAPI doc metadata and implementation
        add_open_api_action :index, :index, base_api_options.merge(
            description: 'Retrieve list of available books')
        def index
          render_collection collection_query, base_api_options
        end
        
        # GET /api/v1/books/:book_id endpoint OpenAPI doc metadata and implementation
        add_open_api_action :show, :show, base_api_options.merge(
            description: 'Retrieve details for a specific book')
        def show
          render_object object_query.first, base_api_options
        end
        
        # POST /api/v1/books endpoint OpenAPI doc metadata and implementation
        add_open_api_action :create, :create, base_api_options.merge(
            description: 'Create a new book')
        def create
          do_create base_api_options
        end
        
        # PATCH/PUT api/v1/books/:book_id endpoint OpenAPI doc metadata and implementation
        add_open_api_action :update, :update, base_api_options.merge(
            description: 'Update an existing book')
        def update
          do_update object_query, base_api_options
        end
        
        # DELETE /api/v1/books/:book_id endpoint OpenAPI doc metadata and implementation
        add_open_api_action :destroy, :destroy, base_api_options.merge(
            description: 'Delete an existing book')
        def destroy
          do_destroy object_query, base_api_options
        end

        def object_query(opts = {})
          super(opts.merge(not_found_error: true))
        end
      end
    end
  end

Generating Documentation

To generate OpenAPI documentation:

    rake open_api:docs

Optionally, you may specify a base route path and output file:

    rake open_api:docs[/api/v1,/home/myhome/api-v1.json]

About

Ruby gem allowing Ruby on Rails developers to create REST API’s using metadata defined inside their ActiveRecord models.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages