Current section
Files
Jump to
Current section
Files
lib/address_input.ex
defmodule AddressInput do
@moduledoc """
High-level API for address metadata derived from the
[libaddressinput](https://github.com/google/libaddressinput) dataset.
The module loads a JSON dataset from `priv/metadata.json` (bundled with the
application) and exposes convenience functions for looking up normalized
[Country](`AddressInput.Country`) and [Subregion](`AddressInput.Subregion`)
structs.
## Primary entry points
- `countries/0` returns all available countries sorted by name. Each country
includes parsed subregions.
- `get_country/1` returns a single country by ISO 3166-1 alpha-2 code (case
insensitive), or `nil` if it is not present.
## Country fields
A [Country](`AddressInput.Country`) includes:
- `id` - ISO 3166-1 alpha-2 code.
- `name` - localized display name from the dataset.
- `default_language` - language code used for labels.
- `required_fields` - list of required address components as atoms, derived
from the dataset codes (`N`, `O`, `A`, `D`, `C`, `S`, `Z`, `X`).
- `subregion_type`, `sublocality_type`, `postal_code_type` - label hints.
- `postal_code_regex` - compiled regex for validating postal codes.
- `address_format` - parsed formatting template from the dataset (`fmt`).
- `local_address_format` - parsed local formatting template (`lfmt`) when present.
- `subregions` - list of [Subregion](`AddressInput.Subregion`) entries.
A [Subregion](`AddressInput.Subregion`) includes `id`, `iso_code`, and `name`.
## Example
iex> AddressInput.get_country("US")
%AddressInput.Country{
id: "US",
name: "UNITED STATES",
default_language: "en",
required_fields: [:address, :sublocality, :region, :postal_code],
sublocality_type: "city",
subregion_type: "state",
postal_code_type: "zip",
postal_code_regex: ~r/(\\d{5})(?:[ -](\\d{4}))?/,
address_format: [{:field, :name}, :newline, ...],
local_address_format: nil,
subregions: [
%AddressInput.Subregion{id: "AK", iso_code: "AK", name: "Alaska"},
...
]
}
"US"
"""
alias __MODULE__.Country
@type token ::
{:field, Country.field()}
| {:text, String.t()}
| :newline
@type address_format :: [token()]
@doc """
Returns the list of available countries as [Country](`AddressInput.Country`) structs.
"""
@spec countries() :: [Country.t()]
def countries do
{_, sorted} = country_tree()
sorted
end
@doc """
Returns a [Country](`AddressInput.Country`) by ISO 3166-1 alpha-2 country code.
Returns `nil` when the country is not found.
"""
@spec get_country(String.t()) :: Country.t() | nil
def get_country(country) do
# "ZZ" seems to be a blank country
key = String.upcase(country)
{tree, _} = country_tree()
if :gb_trees.is_defined(key, tree) do
:gb_trees.get(key, tree)
end
end
defp country_tree do
:persistent_term.get(:address_input_countries)
end
@doc """
Returns `{:ok, postal_code}` if the postal code matches the expected format
for the given country. Otherwise, returns `:error`. The provided country must
be either a `%Country{}` or a string that resolves to one.
"""
@spec parse_postal_code(String.t(), String.t() | Country.t()) :: {:ok, String.t()} | :error
def parse_postal_code(postal_code, country) do
case do_parse_postal_code(postal_code, country) do
[result] -> {:ok, result}
nil -> :error
end
end
defp do_parse_postal_code(postal_code, country) when is_binary(country) do
if country = get_country(country) do
do_parse_postal_code(postal_code, country)
end
end
defp do_parse_postal_code(postal_code, %Country{postal_code_regex: regex}) do
case regex do
%Regex{} -> Regex.run(regex, postal_code, capture: :first)
nil -> nil
end
end
end