Skip to content

Instantly share code, notes, and snippets.

@gareth
Created August 4, 2011 22:36
Show Gist options
  • Save gareth/1126465 to your computer and use it in GitHub Desktop.
Save gareth/1126465 to your computer and use it in GitHub Desktop.
FestivalsLab API access for Rails apps

Introduction

This snippet is something I put together to play around with the FestivalsLab API inside a Rails app I was building. I built it with Rails 3 on Ruby 1.9.2 but there's no reason it shouldn't work on older versions of either of those.

This could potentially be used as a bare Ruby library without Rails, but I am using a couple of useful features - Rack's query string parsing and some MIME type handling - that would have to be rewritten to do that. I'm using these features because I don't think that any URI string manipulation should happen outside of a URI library, your philosophy may be different ;)

This code should be considered a work in progress, there are no guarantees that it will work for you, or that it won't wipe your hard drive. But you can see the code, it's all pretty simple.

Installation

The best way to integrate this into your Rails app at the moment is to copy the enclosed files into your /lib folder, then create an initializer in /config/initializers which loads the FestivalsLab code and contains your API keys:

require 'festivals_lab'

FestivalsLab.setup do |config|
  config.access_key = ...
  config.secret_key = ...
end

Usage

Once loaded, FestivalsLab.events() and FestivalsLab.event() can be called from your app as documented in the code, to retrieve nested Hashes of the API data based on the JSON data feed. The library doesn't do any caching or processing of the data, that's all up to you :)

# A small utility module which makes a module's mattr_accessors available via a
# setup method
module Configurable
module ClassMethods
def setup
yield self
end
end
def self.included(receiver)
receiver.extend ClassMethods
end
end
#
# festivals_lab.rb
#
# Created by Gareth Adams on 2011-08-04.
# Copyright 2011. All rights reserved.
#
# This API library is not affiliated with festivalslab
#
# Ruby standard library dependencies
require 'net/http'
require 'openssl'
require 'uri'
# Gem dependencies
require 'json'
# Internal dependencies
require 'configurable'
# Encapsulates the Edinburgh Festivals API as described at
# http://api.festivalslab.com
#
# Setup the module in an initializer with your API keys from the API
# registration
#
# FestivalsLab.setup do |config|
# config.access_key = "ABCDEFGH12345678"
# config.secret_key = "ABCDEFGH12345678abcdefgh12345678"
# end
#
module FestivalsLab
include Configurable
mattr_accessor :access_key, :secret_key
HOST = "api.festivalslab.com"
PATH = "/events"
AUTH_DIGEST = OpenSSL::Digest::Digest.new('sha1')
class << self
# Returns the JSON object corresponding to the given UUID
def event uuid
response = request event_uri(uuid), :json
extract_events response
end
# Returns all events matching the given parameters as defined by the
# festivals api (http://api.festivalslab.com/).
#
# Results are returned as an array of Hashes as unwrapped from the JSON
# formatted response.
#
# Examples:
#
# FestivalsLab.events(
# :date_from => "2011-08-05",
# :date_to => "2011-08-09",
# :genre => "comedy",
# :festival => "fringe"
# )
#
# FestivalsLab.events(
# :price_to => 5,
# :size => 10,
# :from => 70
# )
def events params = {}
response = request query_uri(params), :json
extract_events response
end
private
# Build a URI based on the search parameters given.
#--
# TODO: restrict params to only valid API parameters because the API
# silently ignores invalid parameters.
def query_uri params = {}
raise ArgumentError, "No access_key specified" unless access_key
URI::HTTP.build(:path => PATH, :query => URI.encode_www_form(params.merge(:key => access_key)))
end
# Build a URI for the event UUID given.
def event_uri uuid
raise ArgumentError, "No access_key specified" unless access_key
URI::HTTP.build(:path => "#{PATH}/#{uuid}", :query => URI.encode_www_form(:key => access_key))
end
# Request the given URI from the API server in the given format. Signs the
# URI before requesting it.
#
# The +format+ parameter should be an extension that Mime::Type recognises.
#
# Returns a Net::HTTPResponse regardless of whether the request was
# successful or not.
def request unsigned_uri, format = :json
signed_uri = signed_uri(unsigned_uri)
signed_uri.host = HOST
Net::HTTP.start(signed_uri.host, signed_uri.port) do |http|
http.get(signed_uri.request_uri, "Accept" => Mime::Type.lookup_by_extension(format).to_s)
end
end
# Returns a new URI with an API signature parameter added.
def signed_uri unsigned_uri
unsigned_uri.dup.tap do |uri|
params = Rack::Utils.parse_nested_query uri.query
uri.query = URI.encode_www_form(params.merge(:signature => signature(unsigned_uri)))
end
end
# Returns the correct API signature for the given URI.
def signature unsigned_uri
raise ArgumentError, "No secret_key specified" unless secret_key
OpenSSL::HMAC.hexdigest(AUTH_DIGEST, secret_key, unsigned_uri.request_uri)
end
# Return any events contained in a JSON response.
def extract_events http_response
case http_response
when Net::HTTPSuccess
case http_response.content_type
when "application/json"
JSON.parse http_response.body
else
raise UnexpectedFormat.new(http_response), "Unexpected response format #{http_response.content_type}"
end
else
error = nil
# Currently the API returns HTML error messages, if available we use
# Nokogiri to extract the error message for a more useful response.
case http_response.content_type
when "text/html"
if Object.const_defined? :Nokogiri
error = (Nokogiri::HTML(http_response.body) % "pre").text rescue nil
end
end
raise ApiError.new(http_response), (error if error)
end
end
end
class ApiError < StandardError
attr_reader :response
def initialize response
@response = response
end
end
class UnexpectedFormat < ApiError; end
end
# Add the IVES format
Mime::Type.register "application/vnd.ives+xml", :ives
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment