openaustralia/planningalerts

View on GitHub
app/services/google_geocode_service.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
# typed: strict
# frozen_string_literal: true

class GoogleGeocodeService
  extend T::Sig

  sig { params(address: String, key: String).returns(GeocoderResults) }
  def self.call(address:, key:)
    new(address:, key:).call
  end

  sig { params(address: String, key: String).void }
  def initialize(address:, key:)
    @address = address
    @key = key
  end

  sig { returns(GeocoderResults) }
  def call
    return error("Please enter a street address") if address == ""

    parsed_response = call_google_api(address)

    # TODO: Raise a proper error class here
    raise "Google geocoding error" if parsed_response.nil?

    if parsed_response["status"] != "OK"
      return error(
        "Sorry we don’t understand that address. Try one like ‘1 Sowerby St, Goulburn, NSW’"
      )
    end

    results = parsed_response["results"]
    results = results.select do |result|
      country = component(result, "country")
      # Even though we've biased the results towards au by using region: "au",
      # Google can still return results outside our country of interest. So,
      # test for this and ignore those
      country && country["short_name"] == "AU"
    end

    if results.empty?
      return error(
        "Unfortunately we only cover Australia. It looks like that address is in another country."
      )
    end

    if results.first["partial_match"]
      return error(
        "Sorry we only got a partial match on that address"
      )
    end

    if results.first["geometry"]["location_type"] == "APPROXIMATE"
      return error(
        "Please enter a full street address like ‘36 Sowerby St, Goulburn, NSW’"
      )
    end

    all = results.map do |result|
      suburb = component(result, "locality")
      state = component(result, "administrative_area_level_1")
      postcode = component(result, "postal_code")

      GeocodedLocation.new(
        lat: result["geometry"]["location"]["lat"],
        lng: result["geometry"]["location"]["lng"],
        suburb: (suburb["long_name"] if suburb),
        state: (state["short_name"] if state),
        postcode: (postcode["long_name"] if postcode),
        full_address: result["formatted_address"].sub(", Australia", "")
      )
    end

    GeocoderResults.new(all, nil)
  end

  private

  sig { returns(String) }
  attr_reader :address

  sig { returns(String) }
  attr_reader :key

  # Returns nil if status is not valid
  sig { params(address: String).returns(T.nilable(T::Hash[String, T.untyped])) }
  def call_google_api_no_caching(address)
    params = { address:, key:, region: "au", sensor: false }
    response = HTTParty.get("https://maps.googleapis.com/maps/api/geocode/json?#{params.to_query}")
    response.parsed_response if %w[OK ZERO_RESULTS].include?(response.parsed_response["status"])
  end

  # This caches the returned value for 24 hours but only if it's valid (non nil)
  sig { params(address: String).returns(T.nilable(T::Hash[String, T.untyped])) }
  def call_google_api(address)
    # If we need to expire all the cached values for some reason then increment the version number below
    Rails.cache.fetch("google_geocode_service/v1/#{address}", expires_in: 24.hours, skip_nil: true) do
      call_google_api_no_caching(address)
    end
  end

  sig { params(text: String).returns(GeocoderResults) }
  def error(text)
    GeocoderResults.new([], text)
  end

  # Extract component by type for a particular result
  sig { params(result: T::Hash[String, T.untyped], type: String).returns(T.untyped) }
  def component(result, type)
    result["address_components"].find { |c| c["types"].include?(type) }
  end
end