rack-autocrud

Rack middleware that works with Sinatra to dynamically create CRUD endpoints and routes based on models. It ain't perfect, but it works.

These generated CRUD routes are assumed to return a Rack response.

It's important to note, that you models and endpoints must be in separate modules (read: namespaces).

Input and Response data are formatted as JSON.

NOTE: While this gem is designed to work with DataMapper, it may be usable with Sequel as well (via Sequel::Plugins::JsonSerializer) since none of the code in rack-autocrud is DataMapper-dependent.

Licensing

This software is licensed under the Simplified BSD License as described in the LICENSE file.

Requirements

  • sinatra
  • json

For DataMapper: * dm-serializer

Installation

gem install rack-autocrud

Usage

Just add something like this to your config.ru:

require 'rack/autocrud'

# Load your models
require 'models'

# Load your endpoints
require 'endpoints'

# Auto-magical CRUD
run Rack::AutoCRUD.new nil, :model_namespace => 'Models', :endpoint_namespace => 'Endpoints'

This would assume you only want CRUD-based routing. You can also use this middleware:

use Rack::AutoCRUD, :model_namespace => 'Models', :endpoint_namespace => 'Endpoints'

Auto-Inclusion of Other Modules

This middleware also takes an option :includes which can be used to automatically include other modules (e.g. helpers) when creating and/or patching endpoints.

For example:

use Rack::AutoCRUD, :model_namespace => 'Models', :endpoint_namespace => 'Endpoints', :includes => [ Your::HelperModule ]

Setting Sinatra Options

You can set Sinatra options for your auto-generated endpoints by passing the :sinatra_opts option to this middleware.

For example:

use Rack::AutoCRUD, :model_namespace => 'Models', :endpoint_namespace => 'Endpoints', :sinatra_opts => { :sessions => true }

Which can be extremely useful if you're handling sessions at a higher level than the auto-generated endpoints.

How Routing Works

The routing is simple. You have a model Models::Person. You've added something like the above to your config.ru. This middleware will dynamically create a Sinatra::Base subclass called Endpoints::Person (if it already exists, these routes are added to it) which will contain the following routes:

Route Action HTTP Response Code
get / List all Person entries 200 / 403
post / Create a new Person 201 / 402
get /:id Retrieve a Person 200
put /:id Update a Person 201 / 402
delete /:id Destroy a Person 204
get /count Get a count of Person entries 200 / 403

The middleware will route based on the URI. Thus, /person would correspond to Endpoints::Person's get / route. The /count route returns the total number of Person entries in the database, if COLLECTABLE is set.

Overriding Generated Routes

You can define your own CRUD routes, which will be called and return a response before the autogenerated routes, as long as they're added after your endpoint is defined.

For example:

require 'sinatra/base'

module Endpoints
  class Person < Sinatra::Base
    get '/'
       Models::Person.all.to_json
    end
  end
end

In this case, if you're using dm-serializer,you'd get back every Models::Person record in the database in a JSON array. By default, the get / route returns "Access Denied."

CRUD Processing Hooks

There are some basic processing hooks you can define in your endpoint:

Hook Description
pre_create(model,request,params) Called before the record is created
post_create(model,request,obj) Called after the record is saved, if it was saved successfully
pre_retrieve(model,request,params) Called before the record is fetched
post_retrieve(model,request,obj) Called after the record is fetched
pre_update(model,request,params) Called before the record is updated
post_update(model,request,params) Called after the record is updated, if it was saved successfully
pre_destroy(model,request,params) Called before the record is destroyed
post_destroy(model,request,obj) Called after the record is destroyed
pre_collect(model,request,params) Called before the record is collected
post_collect(model,request,collection) Called after the record is collected

Parameters:

  • model is the model class for the endpoint
  • request is the current request object
  • obj is the ORM object corresponding to the record in question
  • collection is the collection returned by the ORM

If any of these hooks returns anything other than nil, it is assumed to be a response object, which is returned immediately, and no further processing is performed.

Collections

All models in the namespace are not collectable by default. To enable collections, you need to set the COLLECTABLE constant in the model:

module Models
  class Person
    include DataMapper::Resource

    property :id,   Serial
    property :name, String

    # Enable collection
    COLLECTABLE = 1
  end
end

You can set this constant to 0 to disable it. If you want to enable collections on all models in your namespace, simply specify the constant there:

If you want to not expose all models by default, simply define the constant as part of the Models module:

module Models
  # Enable collections on all models by default
  COLLECTABLE = 1
end

Selective Exposure

All models in the namespace passed to Rack::AutoCRUD are exposed by default.

You can selectively hide models by defining a constant called EXPOSE in your model definition. A value of 0 will cause Rack::AutoCRUD to not create and endpoint for that particular model.

Example:

module Models
  class Person
    include DataMapper::Resource

    property :id,   Serial
    property :name, String

    # Don't expose this model via AutoCRUD
    EXPOSE = 0
  end
end

If you want to not expose all models by default, simply define the constant as part of the Models module:

module Models
  # Hide all models by default
  EXPOSE = 0
end

module Models
  class ExposeMe
    include DataMapper::Resource

    property :id, Serial

    # Expose this model
    EXPOSE = 1
  end
end

Helper Functions

This middleware also adds a helper function to the endpoints, set_request_body, to allow you to replace the request body from the aforementioned hooks, namely pre_create and pre_update.

This function is defined as:

def set_request_body(new_body,content_type='text/json')

where new_body is expected to be a string.