summaryrefslogtreecommitdiffstats
path: root/lib/cepis/address.ex
blob: 05639122bbc94704561ee248b5f7e21952d94ec0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
defmodule Cepis.Address do
  @moduledoc """
  A CEP and everything the database knows about it.

  **Field names are the API's own, in Portuguese** — `logradouro`, `bairro`,
  `localidade`. Everything else in this library is English; the fields are the
  one deliberate exception. The reason is fidelity: what you read in the JSON
  at <https://cep.is/01001000> is exactly what you access on the struct.

  Fields the database does not have for that CEP come back as empty strings,
  never `nil` — that is what the API returns, and changing it here would create
  a difference between reading the JSON and reading this library.

  `geo` is the exception: it is `nil` when there is no coordinate. There the
  difference is real, because the API **omits** the whole field in that case
  (about 43% of CEPs) rather than sending null.

  `raw` holds the decoded response exactly as it arrived, uninterpreted, so a
  field the API grows before this library is updated is not lost.
  """

  alias Cepis.Geo

  defstruct cep: "",
            logradouro: "",
            complemento: "",
            unidade: "",
            bairro: "",
            localidade: "",
            uf: "",
            estado: "",
            regiao: "",
            ibge: "",
            gia: "",
            ddd: "",
            siafi: "",
            geo: nil,
            raw: %{}

  @type t :: %__MODULE__{
          cep: String.t(),
          logradouro: String.t(),
          complemento: String.t(),
          unidade: String.t(),
          bairro: String.t(),
          localidade: String.t(),
          uf: String.t(),
          estado: String.t(),
          regiao: String.t(),
          ibge: String.t(),
          gia: String.t(),
          ddd: String.t(),
          siafi: String.t(),
          geo: Geo.t() | nil,
          raw: map()
        }

  @doc """
  The CEP with its mask: `"01001-000"`.

  The native contract always returns it unmasked; formatting is presentation,
  and it lives here so it does not become a repeated interpolation in every
  project that uses this. A CEP that is not 8 characters is returned unchanged.
  """
  @spec formatted_cep(t()) :: String.t()
  def formatted_cep(%__MODULE__{cep: <<prefix::binary-size(5), suffix::binary-size(3)>>}) do
    prefix <> "-" <> suffix
  end

  def formatted_cep(%__MODULE__{cep: cep}), do: cep

  @doc false
  @spec from_map(map()) :: t()
  def from_map(data) when is_map(data) do
    %__MODULE__{
      cep: text(data, "cep"),
      logradouro: text(data, "logradouro"),
      complemento: text(data, "complemento"),
      unidade: text(data, "unidade"),
      bairro: text(data, "bairro"),
      localidade: text(data, "localidade"),
      uf: text(data, "uf"),
      estado: text(data, "estado"),
      regiao: text(data, "regiao"),
      ibge: text(data, "ibge"),
      gia: text(data, "gia"),
      ddd: text(data, "ddd"),
      siafi: text(data, "siafi"),
      geo: Geo.from_map(Map.get(data, "geo")),
      raw: data
    }
  end

  defp text(data, key) do
    case Map.get(data, key) do
      value when is_binary(value) -> value
      value when is_number(value) -> to_string(value)
      _ -> ""
    end
  end
end