Build Status Code Climate

api-transformer

NOTE: this is currently a work in progress

A Ruby web server DSL for exposing one or more back-end services through a different API.

Installation

Add this line to your application's Gemfile:

gem "api-transformer"

And then execute:

$ bundle

Or install it yourself as:

$ gem install api-transformer

Usage

Quick Start

Create a file ip_server.rb:

require 'api_transformer'

class IpServer < ApiTransformer::Server
  base_url "http://ip.jsontest.com/"

  get "/ip" do
    request :ip do
      path "/"
      method :get
    end

    response do |data|
      success do
        status 200
        attribute :your_ip, data[:ip][:ip]
      end
    end
  end
end

Run it:

$ ruby ip_server.rb -sv

Try it:

$ curl -i http://localhost:9000/ip
HTTP/1.1 200 OK
Server: Goliath
Date: Tue, 30 Sep 2014 23:19:34 GMT

{"your_ip":"74.125.228.96"}

Let's break this down:

base_url "http://ip.jsontest.com/"

Declare the base URL for back-end requests, which will each provide a path under this.

get "/ip" do |params|

This defines a single endpoint that responds to GET requests at the path '/ip'.

request :ip do
  path "/"
  method :get
end

It makes a single GET request to the root path of the base_url (http://ip.jsontest.com/).

response do |data|
  success do
    status 200
    attribute :your_ip, data[:ip][:ip]
  end
end

Upon completion of the request to ip.jsontest.com with a 2XX response, it responds with a status of 200 and a JSON attribute of your_ip.

Endpoints without any requests

Back-end requests are not required.

get "/ping" do
  response do
    success { attribute :result, "pong" }
  end
end

Accepting parameters

Query params:

get "/some_params" do
  request :greeting do
    path "/greeting"
    method :get

    query_param :greeting, "hello"
    query_param :name, "Bob"
  end

  response do
    success { attribute :result, "hello Bob" }
  end
end

Form params:

post "/some_params" do
  request :greeting do
    path "/greeting"
    method :post

    form_param :greeting, "hello"
    form_param :name, "Bob"
  end

  response do
    success { attribute :result, "hello Bob" }
  end
end

JSON params:

post "/some_params" do
  request :greeting do
    path "/greeting"
    method :post

    json_param :greeting, "hello"
    json_param :name, "Bob"
  end

  response do
    success { attribute :result, "hello Bob" }
  end
end

JSON params are accepted as a JSON-encoded request body. In this case what would get sent to the back-end endpoint is "greeting":"hello","name":"Bob".

Form and JSON params can't both be used on the same request.

HTTP verbs

GET, POST, PUT and DELETE are provided:

delete "/ping" do
  response do
    success { attribute :result, "I should probably delete something" }
  end
end

Pass-through of headers

get "/user_agent" do |_, headers|
  response do
    success { attribute :user_agent, headers["User-Agent"] }
  end
end

JSON objects and arrays

Classes can be used to parse data and return JSON objects:

class Bob
  def initialize(last_name)
    @last_name = last_name
  end

  def to_hash
    { first_name: "Bob", last_name: @last_name }
  end
end

get "/object" do
  response do
    success { object :bob, Bob, "Saget" }
  end
end

# => {"bob":{"first_name":"Bob","last_name":"Saget"}}

These can also be used to return arrays of data:

get "/array" do
  response do
    success { array :bobs, Bob, %w(Ross Marley) }
  end
end

# => {"bobs":[{"first_name":"Bob","last_name":"Ross"},{"first_name":"Bob","last_name":"Marley"}]}

Use request data on subsequent requests

This is perhaps easiest to explain with an example:

get "/multi" do
  request :one do
    path "/one"
    method :get
  end

  request :two do |data|
    path "/two"
    method :get

    query_param :name, data[:one][:name]
  end

  response do |data|
    success { attribute :name, data[:two][:result] }
  end
end

get "/one" do
  response do
    success { attribute :name, "Bob" }
  end
end

get "/two" do |params|
  response do
    success { attribute :result, params[:name] }
  end
end

Notably, on this line:

request :two do |data|

data will contain the response data from the first request.

Conditional requests

It can be useful to only send back-end requests under certain conditions:

get "/conditional" do |params|
  request :start_background_job, when: params[:make_it_so] do
    path "/job"
    method :post
  end

  response do
    success { attribute :result, "OK" }
  end
end

Streaming

It can be done without a back-end request:

get "/stream_no_backend" do
  stream true

  response do
    success do
      stream do
        (1..9).each { |n| stream_write n }
      end
    end
  end
end

Things to notice:

  • Inside the endpoint block, stream true is needed
  • Inside the success block, there's a stream block
  • The stream block uses stream_write to write the response

This gets more useful when proxying to a back-end request that is going to return a lot of data:

get "/stream_download" do
  stream true

  request :download do
    path "/huge_download"
    method :get
  end

  response do |data|
    success do
      stream do
        data[:one].stream do |chunk|
          stream_write chunk
        end
      end
    end
  end
end

The stream do |chunk| block will receive the data in small chunks as it comes off the socket, so that the entire response body doesn't go into memory.

Testing

$ rake

Contributing

  1. Fork it ( https://github.com/CXInc/api-transformer/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am "Add some feature")
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request