davidcelis/geocodio

View on GitHub
lib/geocodio/client.rb

Summary

Maintainability
A
45 mins
Test Coverage
require 'net/http'
require 'json'
require 'cgi'

require 'geocodio/client/error'
require 'geocodio/client/response'
require 'geocodio/utils'

module Geocodio
  class Client
    include Geocodio::Utils

    CONTENT_TYPE = 'application/json'
    METHODS = {
      :get    => Net::HTTP::Get,
      :post   => Net::HTTP::Post,
      :put    => Net::HTTP::Put,
      :delete => Net::HTTP::Delete
    }
    HOST = 'api.geocod.io'
    BASE_PATH = '/v1.2'
    PORT = 80

    def initialize(api_key = ENV['GEOCODIO_API_KEY'])
      @api_key = api_key
    end

    # Geocodes one or more addresses. If one address is specified, a GET request
    # is submitted to http://api.geocod.io/v1/geocode. Multiple addresses will
    # instead submit a POST request.
    #
    # @param [Array<String>] addresses one or more String addresses
    # @param [Hash] options an options hash
    # @option options [Array] :fields a list of option fields to request (possible: "cd" or "cd113", "stateleg", "school", "timezone")
    # @return [Geocodio::Address, Array<Geocodio::AddressSet>] One or more Address Sets
    def geocode(addresses, options = {})
      if addresses.size < 1
        raise ArgumentError, 'You must provide at least one address to geocode.'
      elsif addresses.size == 1
        geocode_single(addresses.first, options)
      else
        geocode_batch(addresses, options)
      end
    end

    # Reverse geocodes one or more pairs of coordinates. Coordinate pairs may be
    # specified either as a comma-separated "latitude,longitude" string, or as
    # a Hash with :lat/:latitude and :lng/:longitude keys. If one pair of
    # coordinates is specified, a GET request is submitted to
    # http://api.geocod.io/v1/reverse. Multiple pairs of coordinates will
    # instead submit a POST request.
    #
    # @param [Array<String>, Array<Hash>] coordinates one or more pairs of coordinates
    # @param [Hash] options an options hash
    # @option options [Array] :fields a list of option fields to request (possible: "cd" or "cd113", "stateleg", "school", "timezone")
    # @return [Geocodio::Address, Array<Geocodio::AddressSet>] One or more Address Sets
    def reverse_geocode(coordinates, options = {})
      if coordinates.size < 1
        raise ArgumentError, 'You must provide coordinates to reverse geocode.'
      elsif coordinates.size == 1
        reverse_geocode_single(coordinates.first, options)
      else
        reverse_geocode_batch(coordinates, options)
      end
    end
    alias :reverse :reverse_geocode

    # Sends a GET request to http://api.geocod.io/v1/parse to correctly dissect
    # an address into individual parts. As this endpoint does not do any
    # geocoding, parts missing from the passed address will be missing from the
    # result.
    #
    # @param address [String] the full or partial address to parse
    # @return [Geocodio::Address] a parsed and formatted Address
    def parse(address, options = {})
      params, options = normalize_params_and_options(options)
      params[:q] = address

      Address.new get('/parse', params, options).body
    end

    private

      METHODS.each do |method, _|
        define_method(method) do |path, params = {}, options = {}|
          request method, path, options.merge(params: params)
        end
      end

      def geocode_single(address, options = {})
        params, options = normalize_params_and_options(options)
        params[:q] = address

        response  = get '/geocode', params, options
        addresses, input = parse_results(response)

        AddressSet.new(address, *addresses, input: input)
      end

      def reverse_geocode_single(pair, options = {})
        params, options = normalize_params_and_options(options)
        pair = normalize_coordinates(pair)
        params[:q] = pair

        response  = get '/reverse', params, options
        addresses, input = parse_results(response)

        AddressSet.new(pair, *addresses, input: input)
      end

      def geocode_batch(addresses, options = {})
        params, options = normalize_params_and_options(options)
        options[:body] = addresses

        response = post '/geocode', params, options

        parse_nested_results(response)
      end

      def reverse_geocode_batch(pairs, options = {})
        params, options = normalize_params_and_options(options)
        options[:body] = pairs.map { |pair| normalize_coordinates(pair) }

        response = post '/reverse', params, options

        parse_nested_results(response)
      end

      def request(method, path, options)
        path += "?api_key=#{@api_key}"

        if params = options[:params] and !params.empty?
          q = params.map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }
          path += "&#{q.join('&')}"
        end

        req = METHODS[method].new(BASE_PATH + path, 'Accept' => CONTENT_TYPE)

        if options.key?(:body)
          req['Content-Type'] = CONTENT_TYPE
          req.body = options[:body] ? JSON.dump(options[:body]) : ''
        end

        http = Net::HTTP.new HOST, PORT
        http.read_timeout = options[:timeout] if options[:timeout]
        res  = http.start { http.request(req) }

        case res
        when Net::HTTPSuccess
          return Response.new(res)
        else
          raise Error, res
        end
      end
  end
end