opengovernment/askthem

View on GitHub
lib/project_vote_smart.rb

Summary

Maintainability
B
6 hrs
Test Coverage
# -*- coding: utf-8 -*-
# A simple wrapper for the Project VoteSmart API.
class ProjectVoteSmart
  class Error < StandardError; end
  class EndpointNotFound < Error; end
  class DocumentNotFound < Error; end

  # Based on API documentation. Not all endpoints have been tested.
  LIST_ENDPOINTS = [
    # http://api.votesmart.org/docs/Address.html
    'Address.getOfficeByOfficeState',
    # http://api.votesmart.org/docs/Measure.html
    'Measure.getMeasuresByYearState',
    # http://api.votesmart.org/docs/Candidates.html
    'Candidates.getByOfficeState',
    'Candidates.getByOfficeTypeState',
    'Candidates.getByLastname',
    'Candidates.getByLevenshtein',
    'Candidates.getByElection',
    'Candidates.getByDistrict',
    # http://api.votesmart.org/docs/Committee.html
    'Committee.getTypes',
    'Committee.getCommitteesByTypeState',
    # http://api.votesmart.org/docs/District.html
    'District.getByOfficeState',
    # http://api.votesmart.org/docs/Election.html
    'Election.getElection',
    'Election.getElectionByYearState',
    'Election.getElectionByZip', # hmm, all other zip methods include a zipMessage
    'Election.getStageCandidates',
    # http://api.votesmart.org/docs/Leadership.html
    'Leadership.getPositions',
    'Leadership.getOfficials',
    # http://api.votesmart.org/docs/Local.html
    'Local.getCounties',
    'Local.getCities',
    'Local.getOfficials',
    # http://api.votesmart.org/docs/Office.html
    'Office.getTypes',
    'Office.getBranches',
    'Office.getLevels',
    'Office.getOfficesByType',
    'Office.getOfficesByLevel',
    'Office.getOfficesByTypeLevel',
    'Office.getOfficesByBranchLevel',
    # http://api.votesmart.org/docs/Officials.html
    'Officials.getStatewide',
    'Officials.getByOfficeState',
    'Officials.getByOfficeTypeState',
    'Officials.getByLastname',
    'Officials.getByLevenshtein',
    'Officials.getByDistrict',
    # http://api.votesmart.org/docs/Rating.html
    'Rating.getCategories',
    'Rating.getSigList',
    'Rating.getRating',
    # http://api.votesmart.org/docs/State.html
    'State.getStateIDs',
    # http://api.votesmart.org/docs/Votes.html
    'Votes.getCategories',
    'Votes.getBillActionVotes',
    'Votes.getByBillNumber',
    'Votes.getBillsByCategoryYearState',
    'Votes.getBillsByYearState',
    'Votes.getBillsByOfficialYearOffice',
    'Votes.getBillsByOfficialCategoryOffice',
    'Votes.getByOfficial',
    'Votes.getBillsBySponsorYear',
    'Votes.getBillsBySponsorCategory',
    'Votes.getBillsByStateRecent',
    'Votes.getVetoes',
  ]

  # Sets the API key on the API client.
  #
  # @params [Hash] opts optional arguments
  # @option opts [String] :api_key a Project VoteSmart API key
  def initialize(opts = {})
    @api_key = opts[:api_key] || ENV['PROJECT_VOTE_SMART_API_KEY']
  end

  # Sends a request to the Project VoteSmart API and returns the response in a
  # somewhat more consistent format than provided by the API.
  #
  # @param [String] endpoint the API endpoint
  # @param [Hash] params the API request's parameters
  # @see http://api.votesmart.org/docs/common.html
  # @see http://api.votesmart.org/docs/State.html
  def get(endpoint, params = {})
    begin
      result = JSON.parse(RestClient.get("http://api.votesmart.org/#{endpoint}", params: params.merge(key: @api_key, o: 'JSON')))

      if result['error']
        if result['error']['errorMessage'][/\ANo (bill|categor|official|rating|SIG)/i]
          raise ProjectVoteSmart::DocumentNotFound, result['error']['errorMessage']
        else
          raise ProjectVoteSmart::Error, result['error']['errorMessage']
        end
      end

      # There is always a root element.
      if result.size == 1
        result = result[result.keys.first]
        # State.getStateIDs (uniquely) adds an extra layer of nesting.
        if result.key?('list')
          result = result['list']
        # We want to access the key that's not "generalInfo".
        elsif result.key?('generalInfo')
          result.delete('generalInfo')
        end
        # If there is only one key besides "generalInfo".
        if result.size == 1
          result = result[result.keys.first]
        end
        # If there is a single result, Project VoteSmart will not wrap it in an array.
        if LIST_ENDPOINTS.include?(endpoint)
          result = [result] unless Array === result
        end
      end

      result
    rescue Errno::ETIMEDOUT, RestClient::ServerBrokeConnection
      wait *= 2 # exponential backoff
      sleep wait
      retry
    end
  rescue RestClient::ResourceNotFound
    raise ProjectVoteSmart::EndpointNotFound, 'HTTP 404 Not Found'
  end

  def officials_by_state_and_office(state_id, office_ids)
    office_ids.each_with_index do |office_id, index|
      begin
        state_id = state_id == 'us' ? 'NA' : state_id.upcase
        return get('Officials.getByOfficeState',
                   officeId: office_id,
                   stateId: state_id)

      rescue ProjectVoteSmart::DocumentNotFound => e
        raise e if index + 1 == office_ids.size # if none of the officeIds work
      end
    end
  end

  # @see http://api.votesmart.org/docs/semi-static.html
  def office_ids(options)
    # Chairman, Councilmember
    return [347, 368] if options[:state] == 'dc'

    office_ids = []
    case options[:political_position]
    when 'governor'
      office_ids << 3
    when 'mayor'
      office_ids << 73
    when 'lower'
      if options[:state] == 'us'
        # Federal House
        office_ids << 5
      else
        # State Assembly, State House
        office_ids += [7, 8]
      end
    else
      if options[:state] == 'us'
        # Federal Senate
        office_ids << 6
      else
        # State Senate
        office_ids << 9
      end
    end

    office_ids
  end
end