lib/get_geocode.ex

defmodule GetGeocode do
  alias GetGeocode.Apis.{ViaCep, Nominatim}
  alias GetGeocode.{Geocode, Coords}

  @moduledoc """
  The main module with `get/1` function to retrieve data from CEP (brazilian format), full address format (Nominatim), or a tuple with coordinates `{lat, lng}`.
  """
  @version "0.0.3"
  @moduledoc since: @version

  @doc """
  Gets geodata from `input`.
  Returns a tuple with `{:ok, %GetGeocode.Geocode{}}`.

  ## Examples
    ```
    # CEP format
    iex> GetGeocode.get "69030000"
    {:ok,
    %GetGeocode.Geocode{
      city: "Manaus",
      coords: %GetGeocode.Coords{lat: "-3.1054153", lng: "-60.0547259"},
      full_details: "Rua Izaurina Braga, Compensa, Manaus, Região Geográfica Imediata de Manaus, Região Geográfica Intermediária de Manaus, Amazonas, Região Norte, 69000-000, Brasil",
      neighborhood: "Compensa",
      postalcode: "69030-000",
      state: "AM",
      street: "Rua Izaurina Braga"
    }}
    
    # with full name
    iex> GetGeocode.get "Rua Compensa, Compensa, Amazonas"
    {:ok,
    %GetGeocode.Geocode{
      city: "Manaus",
      coords: %GetGeocode.Coords{lat: "-3.0967331", lng: "-60.0499325"},
      full_details: "Rua Guanapuris, Compensa, Manaus, Região Geográfica Imediata de Manaus, Região Geográfica Intermediária de Manaus, Amazonas, Região Norte, 69000-000, Brasil",
      neighborhood: "Compensa",
      postalcode: "69000-000",
      state: "Amazonas",
      street: "Rua Guanapuris"
    }}
    ```

    Also works with input being a tuple with coordinates, like `{lat, lng}`.
    ```
    iex> GetGeocode.get {-3.0999329, -60.0552931}
    {:ok,
    %GetGeocode.Geocode{
      city: "Manaus",
      coords: %GetGeocode.Coords{lat: "-3.1004858", lng: "-60.0549478"},
      full_details: "Rua Boa Esperança, Compensa, Manaus, Região Geográfica Imediata de Manaus, Região Geográfica Intermediária de Manaus, Amazonas, Região Norte, 69000-000, Brasil",
      neighborhood: "Compensa",
      postalcode: "69000-000",
      state: "Amazonas",
      street: "Rua Boa Esperança"
    }}
    ```
  """
  @doc since: @version
  def get(input) when is_binary(input) do
    cond do
      cep?(input) -> get_viacep(input)
      addr?(input) -> get_nominatim(input)
      true -> msg_invalid_input()
    end
  end

  def get(coords) when is_tuple(coords) do
    Nominatim.get_data(coords)
    |> builder_from_nominatim()
  end

  defp get_viacep(cep) do
    result = ViaCep.get_cep(cep)

    case result do
      %{"erro" => _} -> msg_invalid_input()
      _ -> builder_from_viacep(result)
    end
  end

  defp get_nominatim(addr) do
    result =
      Regex.replace(~r/(.)\1+/, ~s[#{addr}], "\\1")
      |> Nominatim.get_data()

    case result do
      {_, _} -> result
      _ -> builder_from_nominatim(result)
    end
  end

  defp addr?(addr) do
    addr
    |> String.match?(~r/([a-zA-Z]+[\s|,]+[a-zA-Z]+[\s]*)/)
  end

  defp cep?(cep) do
    cep
    |> String.match?(~r/\b([0-9]{5}-?[0-9]{3})$/)
  end

  defp builder_from_viacep(result) do
    %{
      "bairro" => neighborhood,
      "cep" => postalcode,
      "localidade" => city,
      "logradouro" => street,
      "uf" => state
    } = result

    %{
      "lon" => lng,
      "lat" => lat,
      "display_name" => full_details
    } =
      Regex.replace(~r/(.)\1+/, ~s[#{street},#{neighborhood},#{city}], "\\1")
      |> Nominatim.get_data()

    {:ok,
     %Geocode{
       coords: %Coords{
         lat: lat,
         lng: lng
       },
       postalcode: postalcode,
       street: street,
       neighborhood: neighborhood,
       city: city,
       state: state,
       full_details: full_details
     }}
  end

  defp builder_from_nominatim(result) do
    %{
      "display_name" => full_details,
      "lat" => lat,
      "lon" => lng
    } = result

    [street, neighborhood, city, _, _, state, _, postalcode | _] =
      full_details
      |> String.split(",")
      |> Enum.map(fn x -> String.trim(x) end)

    {:ok,
     %Geocode{
       coords: %Coords{
         lat: lat,
         lng: lng
       },
       postalcode: postalcode,
       street: street,
       neighborhood: neighborhood,
       city: city,
       state: state,
       full_details: full_details
     }}
  end

  defp msg_invalid_input() do
    {:error, "Invalid input"}
  end
end