Git Product home page Git Product logo

fastapi's Introduction

FastAPI Gem Version Build Status

Easily create robust, standardized API endpoints using lightning-fast database queries

FastAPI is a Rails library for querying interdependent datasets quickly and returning a human-readable, standard API output.

It works by constructing complex SQL queries that make efficient use of JOINs and subqueries based upon model dependencies (namely belongs_to, has_one, and has_many).

In only a few lines of code you can decide which fields you wish to expose to your endpoint, any filters you wish to run the data through, and create your controller.

Preview

You can preview a live example of FastAPI at http://fastapi.herokuapp.com/

The repository is located at thestorefront/fastapi_example

Requirements

This gem requires Oj >= 2.9.9 for JSONification, ActiveRecord >= 3.2.0, and ActiveSupport >= 3.2.0.

FastAPI currently supports PostegreSQL as a data layer.

Installation

FastAPI is available via RubyGems using:

$ gem install fastapi

Otherwise, in any Gemfile in a rails project, use:

require 'fastapi'

Examples

Let's say we have three models. Person, Bucket, and Marble. Each Bucket belongs to a Person and can have many Marbles.

Your model for Bucket might look something like this:

class Bucket < ActiveRecord::Base

  belongs_to :person
  has_many :marbles

end

Assume Bucket also has the fields :color, and :material.

Each Marble can have :color and :radius.

Every Person has a :name, :gender and :age.

We want to expose a list of Buckets as a JSONified API endpoint that contains records that look like the following:

  {
    'id': 1,
    'color': 'blue',
    'material': 'plastic',
    'person': {
      'id': 107,
      'name': 'Mary-anne',
      'gender': 'Female',
      'age': 27
    },
    'marbles': [
      {
        'id': 22,
        'color': 'red',
        'radius': 5
      },
      {
        'id': 76,
        'color': 'green',
        'radius': 7
      }
    ]
  }

In order to do that we first look at our Bucket model and add the following:

class Bucket < ActiveRecord::Base

  belongs_to :person
  has_many :marbles

  # A "standard interface" is a list of user-exposed fields for the endpoint
  fastapi_standard_interface [
    :id,
    :color,
    :material,
    :person,
    :marbles
  ]

end

We then modify our Person model:

class Person < ActiveRecord::Base

  # Person is not top-level in the case of the "buckets"
  #   endpoint... we use a special setting indicating
  #   which fields to use if Person happens to be nested.

  # You can NOT include dependent fields here. (belongs_to, has_many)
  #   This is a hard-and-fast FastAPI rule that prevents overly
  #   complex nesting scenarios.

  fastapi_standard_interface_nested [
    :id,
    :name,
    :gender,
    :age
  ]

end

Keep in mind that this will only affect the cases where Person is a nested object.

If we wanted to expose a top-level Person API endpoint, we would use fastapi_standard_interface as well.

Finally, we must modify our Marble model in the same way:

class Marble < ActiveRecord::Base

  fastapi_standard_interface_nested [
    :id,
    :color,
    :radius
  ]

end

Hmm... let's say we only want to list the Marbles that have a radius less than or equal to (<=) 10. Easy! We go back and modify our Bucket model. Add the following to class Bucket < ActiveRecord::Base:

  # top level filters affect the data that is shown,
  #   while filters on "has_many" fields affect which rows are shown per
  #   record
  fastapi_default_filters({
    marbles: {
      radius__lte: 10
    }
  })

Phew! We're almost done. Now to create the endpoint.

First open config/routes.rb and add the following:

namespace :api do
  namespace :v1, defaults: {format: :json} do
    resource :buckets
  end
end

We now create a route to an API controller for Bucket in app/controllers/api/v1/buckets_controller.rb

(Can also use rails generate controller Api::V1::Buckets in the terminal):

class Api::V1::BucketsController < ApplicationController

  def index

    filters = request.query_parameters

    render json: Bucket.fastapi.filter(filters).response

  end

end

Boom! Run your server with rails server and hop your way over to http://yourserver[:port]/api/v1/buckets to see your beautiful list of Buckets in the FastAPI standard JSON format. :)

Try to filter your datasets as well:

http://yourserver[:port]/api/v1/buckets/?color=red or

http://yourserver[:port]/api/v1/buckets/?color__in[]=red&color__in[]=blue

There are many to play with, go nuts!

Documentation

FastAPI has four core components:

  1. ActiveRecord::Base extension that adds necessary class and instance methods.
  2. class FastAPI which is instantiated by an ActiveRecord::Base instance.
  3. Filters, which provide a way of easily interfacing with your data.
  4. FastAPI standard output, a strict way of displaying all FastAPI responses.

ActiveRecord::Base (Extension)

ClassMethods

fastapi_standard_interface

fastapi_standard_interface( fields [Array] )

Sets the standard interface for the top level of a fastapi response. Can use any available fields for the model, or belongs_to and has_many associations. Be sure to use the correct word form (singular vs. plural).

fastapi_standard_interface_nested

fastapi_standard_interface_nested( fields [Array] )

Sets the standard interface for the second level of a fastapi response (nested). Will be referred to whenever this model is found nested in another API response. Can use any available fields for the model, does not support associations.

fastapi_default_filters

fastapi_default_filters( filters [Hash] )

Sets any default filters for the top level fastapi response. Will be overridden if the same filter keys are provided when calling .filter on a FastAPI instance. See Filters section for more information on available filters.

fastapi_safe_field

fastapi_safe_fields( fields [Array] )

Sets safe fields for FastAPIInstance.safe_filter. These safe fields are a whitelist for filters, meaning safe_filter will only allow filtering by these fields.

fastapi

fastapi

Shorthand for the FastAPI constructor. Equivalent to FastAPI.new(MyModel). Recommended usage is MyModel.fastapi.


class FastAPI

FastAPI instances provide a way to interface with your datasets and obtain necessary information (for an API response or otherwise).

InstanceMethods

initialize

initialize( model [Model < ActiveRecord::Base] )

Constructor. Automatically called using Model.fastapi, but can be used as FastAPI.new(Model). Binds the provided Model to the FastAPI instance.

filter

filter( filters [Hash] = {} , meta [Hash] = {} )

Compiles and executes an SQL query based on the supplied filters (see Filters section for more details). Can add additional fields to the expected meta response in the output, as keys in the meta Hash.

safe_filter

safe_filter( filters [Hash] = {} , meta [Hash] = {} )

Compiles and executes an SQL query based on the supplied filters (see Filters section for more details). Will only allow filtering by fields set in fastapi_safe_fields, or fastapi_standard_interface if not set. Can add additional fields to the expected meta response in the output, as keys in the meta Hash. Intended for use with filters = request.query_parameters.

fetch

fetch( id [Integer] , meta [Hash] = {} )

Similar to filter, but will retrieve a single object based on a single id. Ideal for show on a resource, as FastAPI will still format the response appropriately (and give a customized error for id not found).

data

data

Returns a Hash containing the data from the most recently executed filter or fetch call.

data_json

data_json

Returns a JSONified string containing the information in data

meta

meta

Returns a Hash containing the metadata from the most recently executed filter or fetch call.

meta_json

meta_json

Returns a JSONified string containing the information in meta

to_hash

to_hash

Returns a Hash containing both the data and metadata from the most recently executed filter or fetch call.

response

response

Intended to return the final API response. Returns a JSONified string containing the information available in the to_hash method.

reject

reject( message [String] = 'Access Denied' )

Returns a JSONified string representing a standardized empty API response, with a provided error message. For example, if a user is not allowed to access a resource, you would call render json: Model.fastapi.reject.


Filters

Filters are a powerful tool in FastAPI that allow for granular control of your API responses. FastAPIInstance.filter accepts them, and they are also used in ActiveRecord::Base::fastapi_default_filters.

Filters work in the following way:

Model.fastapi.filter({
  key1: 2,
  key2: 'three'
})

Will grab a subset of all Models where :key1 is 2 and :key2 is 'three'.


Filter Comparators

What if we want to find a subset of Models where :key1 is greater than or equal to (>=) 5?

Model.fastapi.filter({
  key1__gte: 5
})

It's that easy. The double underscore indicates you're using a filter comparator, and gte stands for greater than or equal to.

The available comparators are as follows: (Descriptions marked with * indicate scalar inputs will be converted to arrays)

Scalar Fields

'is'              # Field == Value
'not'             # Field != Value
'gt'              # Field > Value
'gte'             # Field >= Value
'lt'              # Field < Value
'lte'             # Field <= Value
'like'            # Field contains Value (string)
'not_like'        # Field does not contain Value (string)
'ilike'           # Field contains Value (case ins.)
'not_ilike'       # Field does not contain Value (case ins.)
'null'            # Field is NULL
'not_null'        # Field is not NULL
'in'              # * Field is in Value
'not_in'          # * Field is not in Value

Array Fields

'subset'          # * Field is a subset of Value
'not_subset'      # * Field is not a subset of Value
'contains'        # * Value is a subset of Field
'not_contains'    # * Value is not a subset of Field
'intersects'      # * Field and Value have shared elements
'not_intersects'  # * Field and Value have no shared elements

If your key contains a double underscore, make sure to use the __is comparator if you look for a specific value.


Filters in HTTP Requests

If you'd like to allow for client-side data filtration (highly recommended), simply use the following in your API endpoint controller:

filters = request.query_parameters
render json: Model.fastapi.filter(filters).response

This will allow you to use filters (and their comparators) in the HTTP query parameters.

For example, http://yourapp/api/v1/users/?active=t&age__gte=19&age__gte=35 could return all active users between 19 and 35 years old.


Data Types in HTTP Requests

While using FastAPI, boolean fields are automatically detected, and the strings 't' and 'f' are converted to true and false, respectively. The same goes for integers. (Converted from string to int.)


Sorting

In FastAPI, sorting is accomplished using a special filter: :__order

:__order Can be in the format of 'key', 'key,DIRECTION' or [:key, 'DIRECTION'] where DIRECTION is ASC or DESC. (Default ASC.)

An example, order users by age (ascending):

render json: User.fastapi.filter({__order: [:age, ASC]}).response

Or perhaps via HTTP (hitting an endpoint with request.query_parameters as the filter):

http://yourapp/api/v1/users/?__order=age,ASC

Pagination

In FastAPI we opted for very robust, granular control of API responses. "Pages" do not exist in a strict sense, but rather by :__offset and :__count, much like you'd expect in a traditional database query.

For example,

render json: Model.fastapi.filter({__offset: 100, __count: 100}).response

Would return (up to) 100 results from Model, beginning at result number 100. (Page 2 at 100 results per page.)

Standard Output

FastAPI has a very strict, standard way of outputting data in the form of a response.

Responses will always look like the following:

{
  'meta': {
    'total': 0,
    'count': 0,
    'offset': 0,
    'error': null,
  },
  'data': []
}

Where meta.total is the total number of records in the entire dataset, meta.count is the number of records in the response, meta.offset is the offset of the first record of the response, and meta.error is null if there was no error, or a string containing an error message if there was an error.

data will always be an Array of Objects. If there was an error with the response, data will be empty. If the response was formed by a FastAPIInstance.fetch call and a record was retrieved, data will be a length-1 Array.

Credits

Thanks for reading! We welcome contributors with good ideas, and we're always looking for new talent.

FastAPI was created by Keith Horwood and Trevor Strieber of Storefront, Inc. in 2014 and is (happily!) MIT licensed.

Twitter: @keithwhor, @TrevorStrieber, @Storefront

Github: keithwhor, TrevorS, thestorefront

Most recent version of the gem is available at RubyGems.org: fastapi

fastapi's People

Contributors

arthurthornton avatar keithwhor avatar rschaefli avatar trevors avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fastapi's Issues

Possible regression related to filters in version 0.1.15.

Since commit 741a0dc, the example default filter no longer works:

# example filter
fastapi_default_filters({
  marbles: {
    radius__lte: 10
  }
})

Errors out with:

Started GET "/api/v1/buckets" for ::1 at 2015-02-09 14:10:51 -0500
Processing by Api::V1::BucketsController#index as JSON
   (0.3ms)  SELECT COUNT(id) FROM buckets
   (0.7ms)  SELECT buckets.id as id, buckets.color as color, buckets.material as material, people.id as person__id, people.name as person__name, people.gender as person__gender, people.age as person__age, ARRAY_TO_STRING(ARRAY( SELECT ROW( __marbles.id, __marbles.color, __marbles.radius ) FROM marbles as __marbles WHERE __marbles.bucket_id IS NOT NULL AND __marbles.bucket_id = buckets.id AND  ORDER BY __marbles.created_at DESC ), ',') as __many__marbles FROM buckets LEFT JOIN people AS people ON people.id = buckets.person_id  ORDER BY buckets.created_at DESC LIMIT 500 OFFSET 0
PG::SyntaxError: ERROR:  syntax error at or near "ORDER"
LINE 1: ...OT NULL AND __marbles.bucket_id = buckets.id AND  ORDER BY _...
                                                             ^
: SELECT buckets.id as id, buckets.color as color, buckets.material as material, people.id as person__id, people.name as person__name, people.gender as person__gender, people.age as person__age, ARRAY_TO_STRING(ARRAY( SELECT ROW( __marbles.id, __marbles.color, __marbles.radius ) FROM marbles as __marbles WHERE __marbles.bucket_id IS NOT NULL AND __marbles.bucket_id = buckets.id AND  ORDER BY __marbles.created_at DESC ), ',') as __many__marbles FROM buckets LEFT JOIN people AS people ON people.id = buckets.person_id  ORDER BY buckets.created_at DESC LIMIT 500 OFFSET 0
Completed 200 OK in 24ms (Views: 0.2ms | ActiveRecord: 2.8ms)

This is the SQL that was generated at commit d9fa7b2 for the same default filter:

Started GET "/api/v1/buckets" for ::1 at 2015-02-09 14:14:34 -0500
Processing by Api::V1::BucketsController#index as JSON
   (0.2ms)  SELECT COUNT(id) FROM buckets
   (0.7ms)  SELECT buckets.id as id, buckets.color as color, buckets.material as material, people.id as person__id, people.name as person__name, people.gender as person__gender, people.age as person__age, ARRAY_TO_STRING(ARRAY( SELECT ROW( __marbles.id, __marbles.color, __marbles.radius ) FROM marbles as __marbles WHERE __marbles.bucket_id IS NOT NULL AND __marbles.bucket_id = buckets.id AND __marbles.radius <= '10' ORDER BY __marbles.created_at DESC ), ',') as __many__marbles FROM buckets LEFT JOIN people AS people ON people.id = buckets.person_id  ORDER BY buckets.created_at DESC LIMIT 500 OFFSET 0
Completed 200 OK in 4ms (Views: 0.1ms | ActiveRecord: 0.9ms)

Filters specified in the URL continue to work, for example:

➜  fast-api-project git:(master) ✗ curl -i http://localhost:3000/api/v1/buckets/\?color\=red
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
Etag: W/"93b094ff833a9f1699fde1d7ba52999f"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 3028c5c9-2cd5-4fc5-9479-8555de2b41a6
X-Runtime: 0.005019
Server: WEBrick/1.3.1 (Ruby/2.1.5/2014-11-13)
Date: Mon, 09 Feb 2015 19:13:02 GMT
Content-Length: 365
Connection: Keep-Alive

{"meta":{"total":1,"offset":0,"count":1,"error":null},"data":[{"id":7,"color":"red","material":"wood","person":{"id":4,"name":"Shannon","gender":"female","age":16},"marbles":[{"id":31,"color":"black","radius":2},{"id":32,"color":"yellow","radius":3},{"id":33,"color":"black","radius":1},{"id":34,"color":"blue","radius":5},{"id":35,"color":"orange","radius":10}]}]}

I've tested this using Rails 3.2.17 and Rails 4.2.0.

table_name overrides don't carry over properly

Because the code uses the tableize method provided by activesupport, it is not possible for FastAPI to work with tables that do not follow the traditional Rails naming conventions. Instead of generating the table name from the class name, the table name should be accessed from the table_name attribute of the class, which is available by default and can be overridden very easily in any given model.

# current table name code used 6 times
self_string_table = @model.to_s.tableize

# calls this rails code
# File activesupport/lib/active_support/inflector/methods.rb, line 143
def tableize(class_name)
  pluralize(underscore(class_name))
end

# which does not pay attention to active_record configs
config.active_record.pluralize_table_names = false

# using the table_name attribute yields a more consistent result and can be overridden in models as necessary
model.table_name

Example on line 760

I have already fixed this in my project, but am hesitant to issue a pull request until I have tested this more thoroughly. Let me know what you think.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.