From 4447a10bf2afc375b7d414db67e472386b635d30 Mon Sep 17 00:00:00 2001 From: Danylo Negrienko Date: Fri, 10 May 2024 17:56:29 -0400 Subject: [PATCH] Length checks added, validations added --- README.md | 82 ++++++++++++----------- lib/iban_ex/error.ex | 12 +++- lib/iban_ex/validator/validator.ex | 100 ++++++++++++++++++++--------- mix.exs | 2 +- test/iban_ex_test.exs | 1 + test/iban_ex_validator_test.exs | 18 ++++++ 6 files changed, 144 insertions(+), 71 deletions(-) create mode 100644 test/iban_ex_validator_test.exs diff --git a/README.md b/README.md index 357a7b4..b8d4391 100644 --- a/README.md +++ b/README.md @@ -12,51 +12,57 @@ In just a few letters and numbers, the IBAN captures all of the country, bank, a ### Successfull case to parse IBAN -#### Parse string with valid formatted IBAN from supported country - -```elixir -{:ok, iban} = "FI2112345600000785" |> IbanEx.Parser.parse() -IO.inspect(iban) -IbanEx.Iban.pretty(iban) -``` - -#### Success case responses - -```elixir -%IbanEx.Iban{ - country_code: "FI", - check_digits: "21", - bank_code: "123456", - branch_code: nil, - national_check: "5", - account_number: "0000078" -} - -"FI 21 123456 0000078 5" -``` + ```elixir + iex> "FI2112345600000785" |> IbanEx.Parser.parse() + {:ok, %IbanEx.Iban{ + country_code: "FI", + check_digits: "21", + bank_code: "123456", + branch_code: nil, + national_check: "5", + account_number: "0000078" + }} + ``` ### Errors cases of IBAN parsing -#### Parse strings with invalid formatted IBANs from unsupported and supported countries +#### To check IBAN's country is supported -```elixir -{:error, unsupported_country_code} = "AZ21NABZ00000000137010001944" |> IbanEx.Parser.parse() -IO.inspect(IbanEx.Error.message(unsupported_country_code), label: unsupported_country_code) + ```elixir + iex> {:error, unsupported_country_code} = IbanEx.Parser.parse("AZ21NABZ00000000137010001944") + {:error, :unsupported_country_code} + iex> IbanEx.Error.message(unsupported_country_code) + "Unsupported country code" + ``` -{:error, invalid_length_code} = "AT6119043002345732012" |> IbanEx.Parser.parse() -IO.inspect(IbanEx.Error.message(invalid_length_code), label: invalid_length_code) +#### Validate and check IBAN length -{:error, invalid_checksum} = "AT621904300234573201" |> IbanEx.Parser.parse() -IO.inspect(IbanEx.Error.message(invalid_checksum), label: invalid_checksum) -``` + ```elixir + iex> {:error, invalid_length} = IbanEx.Parser.parse("AT6119043002345732012") + {:error, :invalid_length} + iex> IbanEx.Error.message(invalid_length) + "IBAN violates the required length" + ``` -#### Error cases response + ```elixir + iex> {:error, length_to_long} = IbanEx.Validator.check_iban_length("AT6119043002345732012") + {:error, :length_to_long} + iex> IbanEx.Error.message(length_to_long) + "IBAN longer then required length" + iex> {:error, length_to_short} = IbanEx.Validator.check_iban_length("AT61190430023457320") + {:error, :length_to_short} + iex> IbanEx.Error.message(length_to_short) + "IBAN shorter then required length" + ``` -```elixir -unsupported_country_code: "Unsupported country code" -invalid_length: "IBAN violates the required length" -invalid_checksum: "IBAN's checksum is invalid" -``` +#### Validate IBAN checksum + + ```elixir + iex> {:error, invalid_checksum} = IbanEx.Parser.parse("AT621904300234573201") + {:error, :invalid_checksum} + iex> IbanEx.Error.message(invalid_checksum) + "IBAN's checksum is invalid" + ``` ## Installation @@ -65,7 +71,7 @@ The package can be installed by adding `iban_ex` to your list of dependencies in ```elixir def deps do [ - {:iban_ex, "~> 0.1.1"} + {:iban_ex, "~> 0.1.2"} ] end ``` diff --git a/lib/iban_ex/error.ex b/lib/iban_ex/error.ex index 9012d45..f119949 100644 --- a/lib/iban_ex/error.ex +++ b/lib/iban_ex/error.ex @@ -7,6 +7,8 @@ defmodule IbanEx.Error do | :invalid_length | :invalid_checksum | :can_not_parse_map + | :length_to_long + | :length_to_short | atom() @type errors() :: [error()] @errors [ @@ -14,15 +16,19 @@ defmodule IbanEx.Error do :invalid_format, :invalid_length, :invalid_checksum, - :can_not_parse_map - ] + :can_not_parse_map, + :length_to_long, + :length_to_short +] @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" + can_not_parse_map: "Can't parse map to IBAN struct", + length_to_long: "IBAN longer then required length", + length_to_short: "IBAN shorter then required length" ] @spec message(error()) :: String.t() diff --git a/lib/iban_ex/validator/validator.ex b/lib/iban_ex/validator/validator.ex index e4d9caa..3782a67 100644 --- a/lib/iban_ex/validator/validator.ex +++ b/lib/iban_ex/validator/validator.ex @@ -5,26 +5,49 @@ defmodule IbanEx.Validator do alias IbanEx.Validator.Replacements import IbanEx.Commons, only: [normalize: 1] + defp error_accumulator(acc, error_message) + defp error_accumulator(acc, {:error, error}), do: [error | acc] + defp error_accumulator(acc, _), do: acc + + defp violation_functions(), + do: [ + {&__MODULE__.iban_violates_format?/1, {:error, :invalid_format}}, + {&__MODULE__.iban_unsupported_country?/1, {:error, :unsupported_country_code}}, + {&__MODULE__.iban_violates_length?/1, {:error, :invalid_length}}, + {&__MODULE__.iban_violates_country_rule?/1, {:error, :invalid_format}}, + {&__MODULE__.iban_violates_checksum?/1, {:error, :invalid_checksum}} + ] + + @doc """ + Accumulate check results in the list of errors + Check iban_violates_format?, iban_unsupported_country?, iban_violates_length?, iban_violates_country_rule?, iban_violates_checksum? + """ + @spec violations(String.t()) :: [] | [atom()] + def violations(iban) do + violation_functions() + |> Enum.reduce([], fn {fun, value}, acc -> error_accumulator(acc, !fun.(iban) or value) end) + |> Enum.reverse() + end + + @doc """ + Make checks in this order step-by-step before first error -> + + iban_violates_format?, + iban_unsupported_country?, + iban_violates_length?, + iban_violates_country_rule?, + iban_violates_checksum? + + """ @spec validate(String.t()) :: {:ok, String.t()} | {:error, Atom.t()} 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)} + 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 @@ -37,12 +60,12 @@ defmodule IbanEx.Validator do # - Check whether a given IBAN violates the required format. @spec iban_violates_format?(String.t()) :: boolean - defp iban_violates_format?(iban), + def 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 + def iban_unsupported_country?(iban) do supported? = iban |> Parser.country_code() @@ -51,33 +74,52 @@ defmodule IbanEx.Validator do !supported? end - # - Check whether a given IBAN violates the required length. + @doc "Check whether a given IBAN violates the required length." @spec iban_violates_length?(String.t()) :: boolean - defp iban_violates_length?(iban) do + def iban_violates_length?(iban) do with country_code <- Parser.country_code(iban), - country_module <- Country.country_module(country_code) do + country_module when is_atom(country_module) <- Country.country_module(country_code) do size(iban) != country_module.size() else - {:error, _} -> true + {:error, _error} -> true end end - # - Check whether a given IBAN violates the country rules. + @doc "Check length of IBAN" + @spec check_iban_length(String.t()) :: {:error, :length_to_short | :length_to_long } | :ok + def check_iban_length(iban) do + unless iban_unsupported_country?(iban) do + country_module = + iban + |> Parser.country_code() + |> Country.country_module() + + case country_module.size() - size(iban) do + diff when diff > 0 -> {:error, :length_to_short} + diff when diff < 0 -> {:error, :length_to_long} + 0 -> :ok + end + else + {:error, :unsupported_country_code} + end + end + + @doc "Check whether a given IBAN violates the country rules" @spec iban_violates_country_rule?(String.t()) :: boolean - defp iban_violates_country_rule?(iban) do + def iban_violates_country_rule?(iban) do with country_code <- Parser.country_code(iban), bban <- Parser.bban(iban), - country_module <- Country.country_module(country_code), + country_module when is_atom(country_module) <- Country.country_module(country_code), rule <- country_module.rule() do !Regex.match?(rule, bban) else - {:error, _} -> true + {:error, _error} -> true end end - # - Check whether a given IBAN violates the required checksum. + @doc "Check whether a given IBAN violates the required checksum." @spec iban_violates_checksum?(String.t()) :: boolean - defp iban_violates_checksum?(iban) do + def iban_violates_checksum?(iban) do check_sum_base = Parser.bban(iban) <> Parser.country_code(iban) <> "00" replacements = Replacements.replacements() diff --git a/mix.exs b/mix.exs index 5a3ee06..ffaad25 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule IbanEx.MixProject do use Mix.Project @source_url "https://g.tulz.dev/opensource/iban-ex" - @version "0.1.2" + @version "0.1.3" def project do [ diff --git a/test/iban_ex_test.exs b/test/iban_ex_test.exs index c4cf468..dde8525 100644 --- a/test/iban_ex_test.exs +++ b/test/iban_ex_test.exs @@ -1,6 +1,7 @@ defmodule IbanExTest do alias IbanEx.{Country, Iban, Parser} use ExUnit.Case, async: true + doctest_file "README.md" doctest IbanEx.Country.AT doctest IbanEx.Country.BE doctest IbanEx.Country.BG diff --git a/test/iban_ex_validator_test.exs b/test/iban_ex_validator_test.exs new file mode 100644 index 0000000..e02759a --- /dev/null +++ b/test/iban_ex_validator_test.exs @@ -0,0 +1,18 @@ +defmodule IbanExValidatorTest do + alias IbanEx.{Validator} + use ExUnit.Case, async: true + + test "check IBANs length" do + cases = [ + {"FG2112345CC6000007", {:error, :unsupported_country_code}}, + {"UK2112345CC6000007", {:error, :unsupported_country_code}}, + {"FI2112345CC6000007", :ok}, + {"FI2112345CC6000007a", {:error, :length_to_long}}, + {"FI2112345CC600007", {:error, :length_to_short}} + ] + + Enum.all?(cases, fn {iban, result} -> + assert Validator.check_iban_length(iban) == result + end) + end +end