neumachen
2/9/2015 - 7:03 PM

base.rb

class User < ActiveRecord::Base

  def self.prototype(overrides = {})
    attributes = {
      name: "James Miller",
      login: "bensie",
      email: "bensie@gmail.com",
      password: "foobar",
    }
    attributes.merge(overrides)
  end

  def api_base_hash
    {
      id:         id,
      login:      login,
      name:       name,
      email:      email,
      api_url:    "https://myapp.com/api/v1/users/#{login}",
      html_url:   "https://myapp.com/#{login}",
      created_at: created_at.utc.iso8601
    }
  end

  def api_full_hash
    api_base_hash.merge({
      updated_at: updated_at.utc.iso8601
    })
  end

  def api_authenticated_hash
    api_full_hash.merge({
      plan:     plan,
      cc_last4: cc_last4
    })
  end

end
require 'spec_helper'
require 'rack/test'

describe Api::Endpoints do
  let(:browser) { Rack::Test::Session.new(Rack::MockSession.new(Api::Endpoints, "myapp.dev")) }

  describe "base" do

    it "responds with json at the root" do
      browser.get("/")
      should_200({message: "Welcome to the API"})
      should_be_json
    end

    it "responds with 404 json at misc not found paths" do
      browser.get("/a")
      should_404
      should_be_json
      browser.get("/a-b")
      should_404
      should_be_json
      browser.get("/a/b/c")
      should_404
      should_be_json
    end
  end

  describe "users" do
    before do
      @user = User.create! User.prototype
      @rico = User.create! User.prototype(login: "rico", email: "rico@gmail.com")
      authorize(@user)
    end

    it "gets the authenticated user" do
      browser.get("/v1/me")
      lrb = decode_json(browser.last_response.body)
      lrb.should == @user.api_authenticated_hash
      should_be_json
    end

    it "gets the authenticated user when specifying the username" do
      browser.get("/v1/users/bensie")
      lrb = decode_json(browser.last_response.body)
      lrb.should == @user.api_authenticated_hash
      should_be_json
    end

    it "gets another user when specifying the username" do
      browser.get("/v1/users/rico")
      lrb = decode_json(browser.last_response.body)
      lrb.should == @rico.api_full_hash
      should_be_json
    end
  end

  describe "events" do
    before do
      @user = User.create! User.prototype
      @event = @user.events.create! Event.prototype
    end

    it "should fetch a collection of events" do
      browser.get("/v1/users/#{@user.login}/events")
      should_200([@event.api_base_hash])
    end
  end
end
require "sinatra/base"

module Sinatra
  module Pagination

    module Helpers

      def paginate(relation)
        @paginated = relation.paginate(page: page, per_page: per_page)
        add_pagination_headers
        return @paginated
      end

      private

      def add_pagination_headers
        request_url = request.url.split("?")[0]

        links = []
        links << %(<#{request_url}?page=#{@paginated.previous_page.to_s}&per_page=#{per_page}>; rel="prev") if @paginated.previous_page
        links << %(<#{request_url}?page=#{@paginated.next_page.to_s}&per_page=#{per_page}>; rel="next") if @paginated.next_page
        links << %(<#{request_url}?page=1&per_page=#{per_page}>; rel="first")
        links << %(<#{request_url}?page=#{@paginated.total_pages.to_s}&per_page=#{per_page}>; rel="last")

        headers "Link" => links.join(",")
      end

      # Ensure that invalid page numbers just return the first page
      # An out of range page number is still valid -- 0, -1, foo are not valid
      def page
        p = params[:page].to_i
        p.between?(1, Float::INFINITY) ? p : 1
      end

      # Default to 30 items per page
      # Permit up to 200 items per page, if more than 200 are requested, return 200
      def per_page
        max = 200
        if per = params[:per_page].to_i
          if per.between?(1, max)
            per
          elsif per > max
            max
          elsif per < 1
            30
          end
        else
          30
        end
      end

    end

    def self.registered(app)
      app.helpers Pagination::Helpers
    end

  end
  register Pagination
end
require 'multi_json'

module ApiMacros

  def json(content)
    MultiJson.dump(content, pretty: true)
  end

  def decode_json(content)
    MultiJson.load(content, symbolize_keys: true)
  end

  def authorize(user)
    browser.authorize(user.email, "foobar")
  end

  def should_be_json
    browser.last_response.headers["Content-Type"].should == "application/json;charset=utf-8"
  end

  def should_200(payload = nil)
    browser.last_response.body.should == json(payload)
    browser.last_response.status.should == 200
  end

  def should_201
    browser.last_response.status.should == 201
  end

  def should_204
    browser.last_response.status.should == 204
    browser.last_response.body.should == ""
    browser.last_response.headers["Content-Type"].should == nil
  end

  def should_400(message = nil)
    browser.last_response.body.should == json({message: message || "Bad request"})
    browser.last_response.status.should == 400
  end

  def should_401(payload = {message: "Authorization required"})
    browser.last_response.body.should == json(payload)
    browser.last_response.status.should == 401
  end

  def should_403(message = nil)
    browser.last_response.body.should == json({message: message || "Forbidden"})
    browser.last_response.status.should == 403
  end

  def should_404
    browser.last_response.body.should == json({message: "Not found"})
    browser.last_response.status.should == 404
  end

  def should_422
    browser.last_response.status.should == 422
  end
end
require "sinatra/base"

module Sinatra
  module ErrorHandling

    module Helpers
      def halt_with_400_bad_request(message = nil)
        message ||= "Bad request"
        halt 400, json({ message: message })
      end

      def halt_with_401_authorization_required(message = nil, realm = "App Name")
        message ||= "Authorization required"
        headers 'WWW-Authenticate' => %(Basic realm="#{realm}")
        halt 401, json({ message: message })
      end

      def halt_with_403_forbidden_error(message = nil)
        message ||= "Forbidden"
        halt 403, json({ message: message })
      end

      def halt_with_404_not_found
        halt 404, json({ message: "Not found" })
      end

      def halt_with_422_unprocessible_entity
        errors = []
        resource = env['sinatra.error'].record.class.to_s
        env['sinatra.error'].record.errors.each do |attribute, message|

          code = case message
          when "can't be blank"
            "missing_field"
          when "has already been taken"
            "already_exists"
          else
            "invalid"
          end

          errors << {
            resource: resource,
            field: attribute,
            code: code
          }
        end
        halt 422, json({
          message: "Validation failed",
          errors: errors
        })
      end

      def halt_with_500_internal_server_error
        halt 500, json({
          message: Rails.env.production? ? "Internal server error: this is a problem on our end and we've been notified of the issue" : env['sinatra.error'].message
        })
      end
    end

    def self.registered(app)
      app.helpers ErrorHandling::Helpers

      app.error ActiveRecord::RecordNotFound do
        halt_with_404_not_found
      end

      app.error ActiveRecord::RecordInvalid do
        halt_with_422_unprocessible_entity
      end

      app.error ActiveRecord::UnknownAttributeError do
        halt_with_422_unprocessible_entity
      end

      app.error ActiveRecord::DeleteRestrictionError do
        halt_with_400_bad_request
      end

      app.error MultiJson::DecodeError do
        halt_with_400_bad_request("Problems parsing JSON")
      end

      app.error do
        if ::Exceptional::Config.should_send_to_api?
          ::Exceptional::Remote.error(::Exceptional::ExceptionData.new(env['sinatra.error']))
        end
        halt_with_500_internal_server_error
      end
    end

  end
  register ErrorHandling
end
module Api
  class Endpoints < Base

    get "/" do
      json({ message: "Welcome to the API" })
    end

    get "/v1" do
      json({ message: "This is version 1 of the API" })
    end

    namespace "/v1" do
      get "/me" do
        authenticate!
        json current_user.api_authenticated_hash
      end
    end

    # Any unmatched request within the /api/ namespace should render 404 as JSON
    # Stop the request here so that JSON gets returned instead of having it
    # run through the whole Rails stack and spit HTML.
    get "/*" do
      halt_with_404_not_found
    end

    post "/*" do
      halt_with_404_not_found
    end

    put "/*" do
      halt_with_404_not_found
    end

    patch "/*" do
      halt_with_404_not_found
    end

    delete "/*" do
      halt_with_404_not_found
    end
  end
end
require "sinatra/base"
require "sinatra/namespace"
require "multi_json"

require "api/authentication"
require "api/error_handling"
require "api/pagination"

module Api
  class Base < ::Sinatra::Base
    register ::Sinatra::Namespace
    register ::Sinatra::ErrorHandling
    register ::Sinatra::Authentication
    register ::Sinatra::Pagination

    # We want JSON all the time, use our custom error handlers
    set :show_exceptions, false

    # Run the following before every API request
    before do
      content_type :json
      permit_authentication
    end

    # Global helper methods available to all namespaces
    helpers do

      # Shortcut to generate json from hash, make it look good
      def json(json)
        MultiJson.dump(json, pretty: true)
      end

      # Parse the request body and enforce that it is a JSON hash
      def parsed_request_body
        if request.content_type.include?("multipart/form-data;")
          parsed = params
        else
          parsed = MultiJson.load(request.body, symbolize_keys: true)
        end
        halt_with_400_bad_request("The request body you provide must be a JSON hash") unless parsed.is_a?(Hash)
        return parsed
      end
    end
    
  end
end