commit a32c7a5b7436f1d43344bdcc152b91240c25025d Author: Danylo Negrienko Date: Tue Mar 5 06:02:58 2024 -0500 Initial commit diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a24c5a --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +iban_ex-*.tar + +# Temporary files, for example, from tests. +/tmp/ diff --git a/.iex.exs b/.iex.exs new file mode 100644 index 0000000..9d5d6e9 --- /dev/null +++ b/.iex.exs @@ -0,0 +1 @@ +alias IbanEx.{Country, Formatter, Iban, Parser, Deserialize, Validator} diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e67579 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# IbanEx + +Library for working with IBAN numbers (parsing, validating and checking and formatting) + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `iban_ex` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:iban_ex, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at . + diff --git a/lib/iban_ex.ex b/lib/iban_ex.ex new file mode 100644 index 0000000..2a17e86 --- /dev/null +++ b/lib/iban_ex.ex @@ -0,0 +1,18 @@ +defmodule IbanEx do + @moduledoc """ + Documentation for `IbanEx`. + """ + + @doc """ + Hello world. + + ## Examples + + iex> IbanEx.hello() + :world + + """ + def hello do + :world + end +end diff --git a/lib/iban_ex/commons/commons.ex b/lib/iban_ex/commons/commons.ex new file mode 100644 index 0000000..713e9cb --- /dev/null +++ b/lib/iban_ex/commons/commons.ex @@ -0,0 +1,16 @@ +defmodule IbanEx.Commons do + @spec normalize(binary()) :: binary() + def normalize(string) do + string + |> to_string() + |> String.replace(~r/\s*/i, "") + |> String.upcase() + end + + @spec normalize_and_slice(binary(), Range.t()) :: binary() + def normalize_and_slice(string, range) do + string + |> normalize() + |> String.slice(range) + end +end diff --git a/lib/iban_ex/country.ex b/lib/iban_ex/country.ex new file mode 100644 index 0000000..ab2d8ce --- /dev/null +++ b/lib/iban_ex/country.ex @@ -0,0 +1,52 @@ +defmodule IbanEx.Country do + import IbanEx.Commons, only: [normalize: 1] + + @type country_code() :: <<_::16>> | atom() + @type error_tuple() :: {:error, atom()} + + @supported_countries %{ + "AT" => IbanEx.Country.AT, + "BE" => IbanEx.Country.BE, + "BG" => IbanEx.Country.BG, + "CH" => IbanEx.Country.CH, + "CY" => IbanEx.Country.CY, + "CZ" => IbanEx.Country.CZ, + "DE" => IbanEx.Country.DE, + "DK" => IbanEx.Country.DK, + "ES" => IbanEx.Country.ES, + "EE" => IbanEx.Country.EE, + "FR" => IbanEx.Country.FR, + "FI" => IbanEx.Country.FI, + "GB" => IbanEx.Country.GB, + "HR" => IbanEx.Country.HR, + "LT" => IbanEx.Country.LT, + "LU" => IbanEx.Country.LU, + "LV" => IbanEx.Country.LV, + "MT" => IbanEx.Country.MT, + "NL" => IbanEx.Country.NL, + "PL" => IbanEx.Country.PL, + "PT" => IbanEx.Country.PT, + "UA" => IbanEx.Country.UA + } + + @supported_country_codes Map.keys(@supported_countries) + + @spec supported_countries() :: map() + defp supported_countries(), do: @supported_countries + + @spec supported_country_codes() :: [country_code()] | [] + def supported_country_codes(), do: @supported_country_codes + + @spec country_module(country_code) :: Module.t() | error_tuple() + def country_module(country_code) when is_binary(country_code) or is_atom(country_code) do + normalized_country_code = normalize(country_code) + case is_country_code_supported?(normalized_country_code) do + true -> supported_countries()[normalized_country_code] + _ -> {:error, :unsupported_country_code} + end + end + + @spec is_country_code_supported?(country_code()) :: boolean() + def is_country_code_supported?(country_code) when is_binary(country_code) or is_atom(country_code), + do: Enum.member?(@supported_country_codes, normalize(country_code)) +end diff --git a/lib/iban_ex/country/at.ex b/lib/iban_ex/country/at.ex new file mode 100644 index 0000000..00ae2fe --- /dev/null +++ b/lib/iban_ex/country/at.ex @@ -0,0 +1,34 @@ +defmodule IbanEx.Country.AT do + @moduledoc """ + Austria IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 20 + + @rule ~r/^(?[0-9]{5})(?[0-9]{11})$/i + + @spec size() :: 20 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, account_number] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/be.ex b/lib/iban_ex/country/be.ex new file mode 100644 index 0000000..9b6046b --- /dev/null +++ b/lib/iban_ex/country/be.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.BE do + @moduledoc """ + Belgium IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 16 + + @rule ~r/^(?[0-9]{3})(?[0-9]{7})(?[0-9]{2})$/i + + @spec size() :: 16 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: _branch_code, + national_check: national_check, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, account_number, national_check] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/bg.ex b/lib/iban_ex/country/bg.ex new file mode 100644 index 0000000..8c6e290 --- /dev/null +++ b/lib/iban_ex/country/bg.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.BG do + @moduledoc """ + Bulgaria IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 22 + + @rule ~r/^(?[A-Z]{4})(?[0-9]{4})(?[0-9]{2}[0-9A-Z]{8})$/i + + @spec size() :: 22 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: branch_code, + national_check: _national_check, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, branch_code, account_number] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/ch.ex b/lib/iban_ex/country/ch.ex new file mode 100644 index 0000000..0aa9d96 --- /dev/null +++ b/lib/iban_ex/country/ch.ex @@ -0,0 +1,34 @@ +defmodule IbanEx.Country.CH do + @moduledoc """ + Switzerland IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 21 + + @rule ~r/^(?[0-9]{5})(?[0-9A-Z]{12})$/i + + @spec size() :: 21 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, account_number] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/cy.ex b/lib/iban_ex/country/cy.ex new file mode 100644 index 0000000..015eb56 --- /dev/null +++ b/lib/iban_ex/country/cy.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.CY do + @moduledoc """ + Cyprus IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 28 + + @rule ~r/^(?[0-9]{3})(?[0-9]{5})(?[0-9A-Z]{16})$/i + + @spec size() :: 28 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: branch_code, + national_check: _national_check, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, branch_code, account_number] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/cz.ex b/lib/iban_ex/country/cz.ex new file mode 100644 index 0000000..97953b2 --- /dev/null +++ b/lib/iban_ex/country/cz.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.CZ do + @moduledoc """ + Czech Republic IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 24 + + @rule ~r/^(?[0-9]{4})(?[0-9]{16})$/i + + @spec size() :: 24 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: _branch_code, + national_check: _national_check, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, account_number] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/de.ex b/lib/iban_ex/country/de.ex new file mode 100644 index 0000000..3df5418 --- /dev/null +++ b/lib/iban_ex/country/de.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.DE do + @moduledoc """ + Germany IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 22 + + @rule ~r/^(?[0-9]{8})(?[0-9]{10})$/i + + @spec size() :: 22 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: _branch_code, + national_check: _national_check, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, account_number] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/dk.ex b/lib/iban_ex/country/dk.ex new file mode 100644 index 0000000..46ea37a --- /dev/null +++ b/lib/iban_ex/country/dk.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.DK do + @moduledoc """ + Denmark IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 18 + + @rule ~r/^(?[0-9]{4})(?[0-9]{10})$/i + + @spec size() :: 18 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: _branch_code, + national_check: _national_check, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, account_number] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/ee.ex b/lib/iban_ex/country/ee.ex new file mode 100644 index 0000000..48e846a --- /dev/null +++ b/lib/iban_ex/country/ee.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.EE do + @moduledoc """ + Estonian IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 20 + + @rule ~r/^(?[0-9]{2})(?[0-9]{2})(?[0-9]{11})(?[0-9]{1})$/i + + @spec size() :: 20 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: branch_code, + national_check: national_check, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, branch_code, account_number, national_check] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/es.ex b/lib/iban_ex/country/es.ex new file mode 100644 index 0000000..adc3318 --- /dev/null +++ b/lib/iban_ex/country/es.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.ES do + @moduledoc """ + Spain IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 24 + + @rule ~r/^(?[0-9]{4})(?[0-9]{4})(?[0-9]{2})(?[0-9]{10})$/i + + @spec size() :: 24 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: branch_code, + national_check: national_check, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, branch_code, national_check, account_number] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/fi.ex b/lib/iban_ex/country/fi.ex new file mode 100644 index 0000000..601855b --- /dev/null +++ b/lib/iban_ex/country/fi.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.FI do + @moduledoc """ + Finland IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 18 + + @rule ~r/^(?[0-9]{6})(?[0-9]{7})(?[0-9]{1})$/i + + @spec size() :: 18 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: _branch_code, + national_check: national_check, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, account_number, national_check] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/fr.ex b/lib/iban_ex/country/fr.ex new file mode 100644 index 0000000..8908d6f --- /dev/null +++ b/lib/iban_ex/country/fr.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.FR do + @moduledoc """ + France IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 27 + + @rule ~r/^(?[0-9]{5})(?[0-9]{5})(?[0-9A-Z]{11})(?[0-9]{2})$/i + + @spec size() :: 27 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: branch_code, + national_check: national_check, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, branch_code, account_number, national_check] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/gb.ex b/lib/iban_ex/country/gb.ex new file mode 100644 index 0000000..e7099e9 --- /dev/null +++ b/lib/iban_ex/country/gb.ex @@ -0,0 +1,35 @@ +defmodule IbanEx.Country.GB do + @moduledoc """ + United Kingdom IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 22 + + @rule ~r/^(?[A-Z]{4})(?[0-9]{6})(?[0-9]{8})$/i + + @spec size() :: 22 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: branch_code, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, branch_code, account_number] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/hr.ex b/lib/iban_ex/country/hr.ex new file mode 100644 index 0000000..416a1fa --- /dev/null +++ b/lib/iban_ex/country/hr.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.HR do + @moduledoc """ + Croatia IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 21 + + @rule ~r/^(?[0-9]{7})(?[0-9]{10})$/i + + @spec size() :: 21 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: _branch_code, + national_check: _national_check, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, account_number] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/lt.ex b/lib/iban_ex/country/lt.ex new file mode 100644 index 0000000..bc21e5b --- /dev/null +++ b/lib/iban_ex/country/lt.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.LT do + @moduledoc """ + Lithuanian IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 20 + + @rule ~r/^(?[0-9]{5})(?[0-9]{11})$/i + + @spec size() :: 20 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: _branch_code, + national_check: _national_check, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, account_number] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/lu.ex b/lib/iban_ex/country/lu.ex new file mode 100644 index 0000000..f5dd419 --- /dev/null +++ b/lib/iban_ex/country/lu.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.LU do + @moduledoc """ + Luxembourg IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 20 + + @rule ~r/^(?[0-9]{3})(?[0-9A-Z]{13})$/i + + @spec size() :: 20 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: _branch_code, + national_check: _national_check, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, account_number] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/lv.ex b/lib/iban_ex/country/lv.ex new file mode 100644 index 0000000..64829ee --- /dev/null +++ b/lib/iban_ex/country/lv.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.LV do + @moduledoc """ + Latvian IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 21 + + @rule ~r/^(?[A-Z]{4})(?[0-9A-Z]{13})$/i + + @spec size() :: 21 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: _branch_code, + national_check: _national_check, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, account_number] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/mt.ex b/lib/iban_ex/country/mt.ex new file mode 100644 index 0000000..3c944b9 --- /dev/null +++ b/lib/iban_ex/country/mt.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.MT do + @moduledoc """ + Malta IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 31 + + @rule ~r/^(?[A-Z]{4})(?[0-9]{5})(?[0-9A-Z]{18})$/i + + @spec size() :: 31 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: branch_code, + national_check: _national_check, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, branch_code, account_number] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/nl.ex b/lib/iban_ex/country/nl.ex new file mode 100644 index 0000000..d910d30 --- /dev/null +++ b/lib/iban_ex/country/nl.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.NL do + @moduledoc """ + Netherlands IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 18 + + @rule ~r/^(?[A-Z]{4})(?[0-9]{10})$/i + + @spec size() :: 18 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: _branch_code, + national_check: _national_check, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, account_number] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/pl.ex b/lib/iban_ex/country/pl.ex new file mode 100644 index 0000000..7c42790 --- /dev/null +++ b/lib/iban_ex/country/pl.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.PL do + @moduledoc """ + Poland IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 28 + + @rule ~r/^(?[0-9]{3})(?[0-9]{4})(?[0-9]{1})(?[0-9]{16})$/i + + @spec size() :: 28 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: branch_code, + national_check: national_check, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, branch_code, national_check, account_number] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/pt.ex b/lib/iban_ex/country/pt.ex new file mode 100644 index 0000000..6195bb8 --- /dev/null +++ b/lib/iban_ex/country/pt.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.PT do + @moduledoc """ + Portugal IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 25 + + @rule ~r/^(?[0-9]{4})(?[0-9]{4})(?[0-9]{11})(?[0-9]{2})$/i + + @spec size() :: 25 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: branch_code, + account_number: account_number, + national_check: national_check + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, branch_code, account_number, national_check] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/country/template.ex b/lib/iban_ex/country/template.ex new file mode 100644 index 0000000..7f761a0 --- /dev/null +++ b/lib/iban_ex/country/template.ex @@ -0,0 +1,12 @@ +defmodule IbanEx.Country.Template do + alias IbanEx.Iban + @type size() :: non_neg_integer() + @type rule() :: Regex.t() + @type country_code() :: <<_::16>> | atom() + @type joiner() :: String.t() + + @callback size() :: size() + @callback rule() :: rule() + @callback to_s(Iban.t(), joiner()) :: String.t() + @callback to_s(Iban.t()) :: String.t() +end diff --git a/lib/iban_ex/country/ua.ex b/lib/iban_ex/country/ua.ex new file mode 100644 index 0000000..0b53d46 --- /dev/null +++ b/lib/iban_ex/country/ua.ex @@ -0,0 +1,36 @@ +defmodule IbanEx.Country.UA do + @moduledoc """ + Ukrainian IBAN parsing rules + """ + + alias IbanEx.Iban + + @behaviour IbanEx.Country.Template + + @size 29 + + @rule ~r/^(?[0-9]{6})(?[0-9A-Z]{19})$/i + + @spec size() :: 29 + def size(), do: @size + + @spec rule() :: Regex.t() + def rule(), do: @rule + + @spec to_s(Iban.t()) :: binary() + @spec to_s(Iban.t(), binary()) :: binary() + def to_s( + %Iban{ + country_code: country_code, + check_digits: check_digits, + bank_code: bank_code, + branch_code: _branch_code, + national_check: _national_check, + account_number: account_number + } = _iban, + joiner \\ " " + ) do + [country_code, check_digits, bank_code, account_number] + |> Enum.join(joiner) + end +end diff --git a/lib/iban_ex/deserialize.ex b/lib/iban_ex/deserialize.ex new file mode 100644 index 0000000..62679f4 --- /dev/null +++ b/lib/iban_ex/deserialize.ex @@ -0,0 +1,49 @@ +defprotocol IbanEx.Deserialize do + @type iban_or_error() :: IbanEx.Iban.t() | {:error, :can_not_parse_map | atom()} + @spec to_iban(t()) :: iban_or_error() + def to_iban(value) +end + +defimpl IbanEx.Deserialize, for: [BitString, String] do + alias IbanEx.{Parser, Error} + @type iban_or_error() :: IbanEx.Iban.t() | {:error, atom()} + @spec to_iban(String.t()) :: iban_or_error() + @spec to_iban(binary()) :: IbanEx.Iban.t() + def to_iban(string) do + case Parser.parse(string) do + {:ok, iban} -> iban + {:error, error_code} -> {error_code, Error.message(error_code)} + end + end +end + +defimpl IbanEx.Deserialize, for: Map do + alias IbanEx.Iban + @type iban_or_error() :: IbanEx.Iban.t() | {:error, :can_not_parse_map} + + @spec to_iban(map()) :: iban_or_error() + def to_iban( + %{ + country_code: _country_code, + check_digits: _check_sum_digits, + bank_code: _bank_code, + account_number: _account_number + } = map + ) do + struct(Iban, map) + end + + def to_iban( + %{ + "country_code" => _country_code, + "check_digits" => _check_sum_digits, + "bank_code" => _bank_code, + "account_number" => _account_number + } = map + ) do + atomized_map = for {key, val} <- map, into: %{}, do: {String.to_atom(key), val} + to_iban(atomized_map) + end + + def to_iban(map) when is_map(map), do: {:error, :can_not_parse_map} +end diff --git a/lib/iban_ex/error.ex b/lib/iban_ex/error.ex new file mode 100644 index 0000000..7ff22d9 --- /dev/null +++ b/lib/iban_ex/error.ex @@ -0,0 +1,33 @@ +defmodule IbanEx.Error do + @moduledoc """ + + """ + + @type error() :: + :unsupported_country_code + | :invalid_format + | :invalid_length + | :invalid_checksum + | :can_not_parse_map + | atom() + @type errors() :: [error()] + @errors [ + :unsupported_country_code, + :invalid_format, + :invalid_length, + :invalid_checksum, + :can_not_parse_map + ] + + @messages [ + unsupported_country_code: "Unsupported country code", + invalid_format: "IBAN violates required format", + invalid_length: "IBAN violates the required length", + invalid_checksum: "IBAN's checksum is invalid", + can_not_parse_map: "Can't parse map to IBAN struct" + ] + + @spec message(error()) :: String.t() + def message(error) when error in @errors, do: @messages[error] + def message(_error), do: "Undefined error" +end diff --git a/lib/iban_ex/formatter.ex b/lib/iban_ex/formatter.ex new file mode 100644 index 0000000..cdb7421 --- /dev/null +++ b/lib/iban_ex/formatter.ex @@ -0,0 +1,42 @@ +defmodule IbanEx.Formatter do + alias IbanEx.Country + import IbanEx.Commons, only: [normalize: 1] + + @available_formats [:compact, :pretty, :splitted] + + @type iban() :: IbanEx.Iban.t() + @type available_format() :: :compact | :pretty | :splitted + @type available_formats_list() :: [:compact | :pretty | :splitted ] + + @spec available_formats() :: available_formats_list() + def available_formats(), do: @available_formats + + @spec pretty(IbanEx.Iban.t()) :: binary() + def pretty(iban), do: format(iban, :pretty) + + @spec compact(IbanEx.Iban.t()) :: binary() + def compact(iban), do: format(iban, :compact) + + @spec splitted(IbanEx.Iban.t()) :: binary() + def splitted(iban), do: format(iban, :splitted) + + @spec format(iban()) :: String.t() + @spec format(iban(), available_format()) :: String.t() + def format(iban, format \\ :compact) + def format(iban, :compact), + do: format(iban, :pretty) |> normalize() + + def format(iban, :pretty) do + country_module = Country.country_module(iban.country_code) + country_module.to_s(iban) + end + + def format(iban, :splitted) do + compact = format(iban, :compact) + + ~r/.{1,4}/ + |> Regex.scan(compact) + |> List.flatten() + |> Enum.join(" ") + end +end diff --git a/lib/iban_ex/iban.ex b/lib/iban_ex/iban.ex new file mode 100644 index 0000000..0443ca5 --- /dev/null +++ b/lib/iban_ex/iban.ex @@ -0,0 +1,29 @@ +defmodule IbanEx.Iban do + alias IbanEx.Formatter + alias IbanEx.{Serialize} + + @type t :: %__MODULE__{ + country_code: <<_::16>>, + check_digits: String.t(), + bank_code: String.t(), + branch_code: String.t() | nil, + national_check: String.t() | nil, + account_number: String.t() + } + defstruct country_code: "UA", check_digits: nil, bank_code: nil, branch_code: nil, national_check: nil, account_number: nil + + @spec to_map(IbanEx.Iban.t()) :: map() + defdelegate to_map(iban), to: Serialize + + @spec to_string(IbanEx.Iban.t()) :: binary() + defdelegate to_string(iban), to: Serialize + + @spec pretty(IbanEx.Iban.t()) :: binary() + defdelegate pretty(iban), to: Formatter + + @spec splitted(IbanEx.Iban.t()) :: binary() + defdelegate splitted(iban), to: Formatter + + @spec compact(IbanEx.Iban.t()) :: binary() + defdelegate compact(iban), to: Formatter +end diff --git a/lib/iban_ex/parser.ex b/lib/iban_ex/parser.ex new file mode 100644 index 0000000..b2391cb --- /dev/null +++ b/lib/iban_ex/parser.ex @@ -0,0 +1,37 @@ +defmodule IbanEx.Parser do + alias IbanEx.{Country, Iban, Validator} + import IbanEx.Commons, only: [normalize_and_slice: 2] + + @type iban_string() :: String.t() + @type country_code_string() :: <<_::16>> + @type check_digits_string() :: <<_::16>> + @type iban_or_error() :: IbanEx.Iban.t() | {:error, atom()} + + @spec parse({:ok, String.t()} | String.t()) :: iban_or_error() + def parse({:ok, iban_string}), do: parse(iban_string) + def parse(iban_string) do + with {:ok, valid_iban} <- Validator.validate(iban_string) do + iban_map = %{ + country_code: country_code(valid_iban), + check_digits: check_digits(valid_iban), + } + + regex = Country.country_module(iban_map.country_code).rule() + bban = bban(iban_string) + bban_map = for {key, val} <- Regex.named_captures(regex, bban), into: %{}, do: {String.to_atom(key), val} + + {:ok, struct(Iban, Map.merge(iban_map, bban_map))} + else + {:error, error_type} -> {:error, error_type} + end + end + + @spec country_code(iban_string()) :: country_code_string() + def country_code(iban_string), do: normalize_and_slice(iban_string, 0..1) + + @spec check_digits(binary()) :: check_digits_string() + def check_digits(iban_string), do: normalize_and_slice(iban_string, 2..3) + + @spec bban(binary()) :: binary() + def bban(iban_string), do: normalize_and_slice(iban_string, 4..-1//1) +end diff --git a/lib/iban_ex/serialize.ex b/lib/iban_ex/serialize.ex new file mode 100644 index 0000000..0a5e0fc --- /dev/null +++ b/lib/iban_ex/serialize.ex @@ -0,0 +1,9 @@ +defmodule IbanEx.Serialize do + alias IbanEx.{Iban, Formatter} + + @spec to_string(Iban.t()) :: String.t() + def to_string(iban), do: Formatter.format(iban) + + @spec to_map(Iban.t()) :: Map.t() + def to_map(iban), do: Map.from_struct(iban) +end diff --git a/lib/iban_ex/validator/replacements.ex b/lib/iban_ex/validator/replacements.ex new file mode 100644 index 0000000..b7858b2 --- /dev/null +++ b/lib/iban_ex/validator/replacements.ex @@ -0,0 +1,46 @@ +defmodule IbanEx.Validator.Replacements do + @moduledoc """ + Replacements in IBANs for checksums calculations + """ + + import IbanEx.Commons, only: [normalize: 1] + + @type symbol() :: <<_::8>> + @type value() :: <<_::16>> + @type replacements() :: %{symbol() => value()} + + @replacements %{ + "A" => "10", + "B" => "11", + "C" => "12", + "D" => "13", + "E" => "14", + "F" => "15", + "G" => "16", + "H" => "17", + "I" => "18", + "J" => "19", + "K" => "20", + "L" => "21", + "M" => "22", + "N" => "23", + "O" => "24", + "P" => "25", + "Q" => "26", + "R" => "27", + "S" => "28", + "T" => "29", + "U" => "30", + "V" => "31", + "W" => "32", + "X" => "33", + "Y" => "34", + "Z" => "35" + } + + @spec replacements() :: replacements() + def replacements(), do: @replacements + + @spec replace(symbol()) :: value() + def replace(symbol), do: @replacements[normalize(symbol)] || normalize(symbol) +end diff --git a/lib/iban_ex/validator/validator.ex b/lib/iban_ex/validator/validator.ex new file mode 100644 index 0000000..ec7558a --- /dev/null +++ b/lib/iban_ex/validator/validator.ex @@ -0,0 +1,95 @@ +defmodule IbanEx.Validator do + alias IbanEx.{Country, Parser} + alias IbanEx.Validator.Replacements + import IbanEx.Commons, only: [normalize: 1] + + @spec validate(String.t()) :: {:ok, String.t()} | {:error} + def validate(iban) do + cond do + iban_violates_format?(iban) -> + {:error, :invalid_format} + + iban_unsupported_country?(iban) -> + {:error, :unsupported_country_code} + + iban_violates_length?(iban) -> + {:error, :invalid_length} + + iban_violates_country_rule?(iban) -> + {:error, :invalid_format} + + iban_violates_checksum?(iban) -> + {:error, :invalid_checksum} + + true -> + {:ok, normalize(iban)} + end + end + + @spec size(String.t()) :: non_neg_integer() + defp size(iban) do + iban + |> normalize() + |> String.length() + end + + # - Check whether a given IBAN violates the required format. + @spec iban_violates_format?(String.t()) :: boolean + defp iban_violates_format?(iban), + do: Regex.match?(~r/[^A-Z0-9]/i, normalize(iban)) + + # - Check whether a given IBAN violates the supported countries. + @spec iban_unsupported_country?(String.t()) :: boolean + defp iban_unsupported_country?(iban) do + supported? = + iban + |> Parser.country_code() + |> Country.is_country_code_supported?() + + !supported? + end + + # - Check whether a given IBAN violates the required length. + @spec iban_violates_length?(String.t()) :: boolean + defp iban_violates_length?(iban) do + with country_code <- Parser.country_code(iban), + country_module <- Country.country_module(country_code) do + size(iban) != country_module.size() + else + {:error, _} -> true + end + end + + # - Check whether a given IBAN violates the country rules. + @spec iban_violates_country_rule?(String.t()) :: boolean + defp iban_violates_country_rule?(iban) do + with country_code <- Parser.country_code(iban), + bban <- Parser.bban(iban), + country_module <- Country.country_module(country_code), + rule <- country_module.rule() do + !Regex.match?(rule, bban) + else + {:error, _} -> true + end + end + + # - Check whether a given IBAN violates the required checksum. + @spec iban_violates_checksum?(String.t()) :: boolean + defp iban_violates_checksum?(iban) do + check_sum_base = Parser.bban(iban) <> Parser.country_code(iban) <> "00" + + replacements = Replacements.replacements() + + remainder = + for(<>, into: "", do: replacements[<>] || <>) + |> String.to_integer() + |> rem(97) + + checksum = + (98 - remainder) + |> Integer.to_string() + |> String.pad_leading(2, "0") + + checksum !== Parser.check_digits(iban) + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..df6ab32 --- /dev/null +++ b/mix.exs @@ -0,0 +1,81 @@ +defmodule IbanEx.MixProject do + use Mix.Project + + @source_url "https://g.tulz.dev/negrienko/iban-ex" + @version "0.1.0" + + def project do + [ + app: :iban_ex, + version: @version, + elixir: "~> 1.16", + start_permanent: Mix.env() == :prod, + deps: deps(), + description: description(), + dialyzer: dialyzer(), + docs: docs(), + package: package() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + defp description() do + """ + Library for working with IBAN numbers (parsing, validating and checking and formatting) + """ + end + + defp dialyzer() do + [ + plt_add_apps: [:iban_ex] + ] + end + + defp package() do + [ + maintainers: ["Danylo Negrienko"], + licenses: ["Apache-2.0"], + links: %{"Git Repository" => @source_url, "Author's Blog" => "https://negrienko.com"} + ] + end + + defp docs() do + [ + main: "readme", + name: "IbanEx", + source_ref: "v#{@version}", + canonical: "http://hexdocs.pm/iban_ex", + source_url: @source_url, + extras: ["README.md"] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:bankster, "~> 0.4.0"}, + + # Checks + {:lettuce, "~> 0.3.0", only: :dev}, + {:ex_check, "~> 0.14.0", only: ~w(dev test)a, runtime: false}, + {:credo, ">= 0.0.0", only: ~w(dev test)a, runtime: false}, + {:dialyxir, ">= 0.0.0", only: ~w(dev test)a, runtime: false}, + {:doctor, ">= 0.0.0", only: ~w(dev test)a, runtime: false}, + {:ex_doc, ">= 0.0.0", only: ~w(dev test)a, runtime: false}, + {:sobelow, ">= 0.0.0", only: ~w(dev test)a, runtime: false}, + {:mix_audit, ">= 0.0.0", only: ~w(dev test)a, runtime: false}, + {:esbuild, "~> 0.7.0", runtime: Mix.env() == :dev}, + {:observer_cli, "~> 1.7.4", only: :dev, runtime: false}, + {:elixir_sense, github: "elixir-lsp/elixir_sense", only: ~w(dev)a} + + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..7338c01 --- /dev/null +++ b/mix.lock @@ -0,0 +1,28 @@ +%{ + "bankster": {:hex, :bankster, "0.4.0", "5e4f35ba574ec7ca9f85d303802ae4331b1fe58a9f75e6267256bfcbd69f20dc", [:mix], [], "hexpm", "814fd27e37ecad0b1bb33e57a49156444f9d0e25341c22e29e49f502964e590a"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, + "credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"}, + "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, + "doctor": {:hex, :doctor, "0.21.0", "20ef89355c67778e206225fe74913e96141c4d001cb04efdeba1a2a9704f1ab5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "a227831daa79784eb24cdeedfa403c46a4cb7d0eab0e31232ec654314447e4e0"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, + "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "885a63fc917a3f3468ddcf1b0855efd77be39182", []}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "esbuild": {:hex, :esbuild, "0.7.1", "fa0947e8c3c3c2f86c9bf7e791a0a385007ccd42b86885e8e893bdb6631f5169", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "66661cdf70b1378ee4dc16573fcee67750b59761b2605a0207c267ab9d19f13c"}, + "ex_check": {:hex, :ex_check, "0.14.0", "d6fbe0bcc51cf38fea276f5bc2af0c9ae0a2bb059f602f8de88709421dae4f0e", [:mix], [], "hexpm", "8a602e98c66e6a4be3a639321f1f545292042f290f91fa942a285888c6868af0"}, + "ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"}, + "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "lettuce": {:hex, :lettuce, "0.3.0", "823198f053714282f980acc68c7157b9c78c740910cb4f572a642e020417a850", [:mix], [], "hexpm", "a47479d94ac37460481133213f08c8283dabbe762f4f8f8028456500d1fca9c4"}, + "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, + "mix_audit": {:hex, :mix_audit, "2.1.2", "6cd5c5e2edbc9298629c85347b39fb3210656e541153826efd0b2a63767f3395", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "68d2f06f96b9c445a23434c9d5f09682866a5b4e90f631829db1c64f140e795b"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "observer_cli": {:hex, :observer_cli, "1.7.4", "3c1bfb6d91bf68f6a3d15f46ae20da0f7740d363ee5bc041191ce8722a6c4fae", [:mix, :rebar3], [{:recon, "~> 2.5.1", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "50de6d95d814f447458bd5d72666a74624eddb0ef98bdcee61a0153aae0865ff"}, + "recon": {:hex, :recon, "2.5.4", "05dd52a119ee4059fa9daa1ab7ce81bc7a8161a2f12e9d42e9d551ffd2ba901c", [:mix, :rebar3], [], "hexpm", "e9ab01ac7fc8572e41eb59385efeb3fb0ff5bf02103816535bacaedf327d0263"}, + "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, + "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, + "yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"}, +} diff --git a/test/iban_ex_test.exs b/test/iban_ex_test.exs new file mode 100644 index 0000000..85f2337 --- /dev/null +++ b/test/iban_ex_test.exs @@ -0,0 +1,8 @@ +defmodule IbanExTest do + use ExUnit.Case + doctest IbanEx + + test "greets the world" do + assert IbanEx.hello() == :world + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()