lib/popit.rb
require 'json'
require 'httparty'
# A Ruby wrapper for the PopIt API.
#
# Instead of writing the path to an API endpoint, you can use method chaining.
# For example:
#
# require 'popit'
# api = PopIt.new :instance_name => 'demo'
# api.get 'persons/john-doe'
#
# can be written as:
#
# api.persons('john-doe').get
#
# All methods and arguments between `api` and the HTTP method - in this case,
# `get` - become parts of the path.
#
# @see https://github.com/mysociety/popit/blob/master/lib/apps/api/api_v1.js
class PopIt
class Error < StandardError; end
class PageNotFound < Error; end
class ServiceUnavailable < Error; end
class NotAuthenticated < Error; end
include HTTParty
# The instance name.
attr_reader :instance_name
# The PopIt API's host name, eg "popit.mysociety.org".
attr_reader :host_name
# The PopIt API's port, eg 443
attr_reader :port
# The PopIt API version, eg "v0.1"
attr_reader :version
# An API key.
attr_reader :apikey
# The maximum number of retries in case of HTTP 503 Service Unavailable errors.
attr_reader :max_retries
# Initializes a PopIt API client.
#
# @param [Hash] opts the API client's configuration
# @option opts [String] :instance_name the instance name
# @option opts [String] :host_name the PopIt API's host name, eg "popit.mysociety.org"
# @option opts [String] :post the PopIt API's port, eg 443
# @option opts [String] :version the PopIt API version, eg "v1"
# @option opts [String] :apikey an API key
# @option opts [String] :max_retries the maximum number of retries in case of
# HTTP 503 Service Unavailable errors
def initialize(opts = {})
unless opts.has_key?(:instance_name)
raise ArgumentError, 'Missing key :instance_name'
end
@instance_name = opts[:instance_name]
@host_name = opts[:host_name] || 'popit.mysociety.org'
@port = opts[:port] || 443
@version = opts[:version] || 'v0.1'
@apikey = opts[:apikey]
@max_retries = opts[:max_retries] || 0
end
# Sends a GET request.
#
# @param [String] path a path with no leading slash
# @param [Hash] opts key-value pairs for the query string
# @return the JSON response from the server
def get(path, opts = {})
request(:get, path, opts)
end
# Sends a POST request.
#
# @param [String] path a path with no leading slash
# @param [Hash] opts key-value pairs for the message body
# @return the JSON response from the server
def post(path, opts = {})
request(:post, path, opts)
end
# Sends a PUT request.
#
# @param [String] path a path with no leading slash
# @param [Hash] opts key-value pairs for the message body
# @return [nil] nothing
def put(path, opts = {})
request(:put, path, opts)
end
# Sends a DELETE request.
#
# @param [String] path a path with no leading slash
# @param [Hash] opts key-value pairs for the query string
# @return [Hash] an empty hash
def delete(path, opts = {})
request(:delete, path, opts)
end
private
def request(http_method, path, opts = {})
attempts ||= 0
path = "https://#{instance_name}.#{host_name}:#{port}/api/#{version}/#{path}"
response = case http_method
when :get
self.class.send(http_method, path, :query => opts)
when :delete
self.class.send(http_method, path, :query => opts.merge(:apikey => apikey))
when :post, :put
self.class.send(http_method, path, :body => JSON.dump(opts.merge(:apikey => apikey)), :headers => {'Content-Type' => 'application/json'})
end
unless ['200', '201', '204'].include?(response.response.code)
message = if Hash === response.parsed_response
if response.parsed_response['error']
response.parsed_response['error']
elsif response.parsed_response['errors']
response.parsed_response['errors'].join(', ')
else
response.parsed_response
end
else
response.parsed_response
end
case response.response.code
when '503'
raise PopIt::ServiceUnavailable
when '404'
raise PopIt::PageNotFound, message
when '401'
raise PopIt::NotAuthenticated # returns an HTML page
else
raise PopIt::Error, message
end
end
response.parsed_response && response.parsed_response['result']
rescue PopIt::ServiceUnavailable
attempts += 1
if attempts <= max_retries
sleep 2 ** attempts
retry
else
raise
end
end
def method_missing(*args)
Chain.new(self, args)
end
class Chain
attr_reader :klass, :chain
def initialize(klass, chain)
@klass = klass
@chain = chain
end
def get(opts = {})
@klass.get(chain.join('/'), opts)
end
def post(opts = {})
@klass.post(chain.join('/'), opts)
end
def put(opts = {})
@klass.put(chain.join('/'), opts)
end
def delete(opts = {})
@klass.delete(chain.join('/'), opts)
end
def method_missing(*args)
@chain += args
self
end
end
end