Length checks added, validations added

This commit is contained in:
Danil Negrienko 2024-05-10 17:56:29 -04:00
parent 51c5c1d800
commit 4447a10bf2
6 changed files with 144 additions and 71 deletions

View File

@ -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 ### Successfull case to parse IBAN
#### Parse string with valid formatted IBAN from supported country ```elixir
iex> "FI2112345600000785" |> IbanEx.Parser.parse()
```elixir {:ok, %IbanEx.Iban{
{:ok, iban} = "FI2112345600000785" |> IbanEx.Parser.parse() country_code: "FI",
IO.inspect(iban) check_digits: "21",
IbanEx.Iban.pretty(iban) bank_code: "123456",
``` branch_code: nil,
national_check: "5",
#### Success case responses account_number: "0000078"
}}
```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"
```
### Errors cases of IBAN parsing ### Errors cases of IBAN parsing
#### Parse strings with invalid formatted IBANs from unsupported and supported countries #### To check IBAN's country is supported
```elixir ```elixir
{:error, unsupported_country_code} = "AZ21NABZ00000000137010001944" |> IbanEx.Parser.parse() iex> {:error, unsupported_country_code} = IbanEx.Parser.parse("AZ21NABZ00000000137010001944")
IO.inspect(IbanEx.Error.message(unsupported_country_code), label: unsupported_country_code) {:error, :unsupported_country_code}
iex> IbanEx.Error.message(unsupported_country_code)
"Unsupported country code"
```
{:error, invalid_length_code} = "AT6119043002345732012" |> IbanEx.Parser.parse() #### Validate and check IBAN length
IO.inspect(IbanEx.Error.message(invalid_length_code), label: invalid_length_code)
{:error, invalid_checksum} = "AT621904300234573201" |> IbanEx.Parser.parse() ```elixir
IO.inspect(IbanEx.Error.message(invalid_checksum), label: invalid_checksum) 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 #### Validate IBAN checksum
unsupported_country_code: "Unsupported country code"
invalid_length: "IBAN violates the required length" ```elixir
invalid_checksum: "IBAN's checksum is invalid" iex> {:error, invalid_checksum} = IbanEx.Parser.parse("AT621904300234573201")
``` {:error, :invalid_checksum}
iex> IbanEx.Error.message(invalid_checksum)
"IBAN's checksum is invalid"
```
## Installation ## Installation
@ -65,7 +71,7 @@ The package can be installed by adding `iban_ex` to your list of dependencies in
```elixir ```elixir
def deps do def deps do
[ [
{:iban_ex, "~> 0.1.1"} {:iban_ex, "~> 0.1.2"}
] ]
end end
``` ```

View File

@ -7,6 +7,8 @@ defmodule IbanEx.Error do
| :invalid_length | :invalid_length
| :invalid_checksum | :invalid_checksum
| :can_not_parse_map | :can_not_parse_map
| :length_to_long
| :length_to_short
| atom() | atom()
@type errors() :: [error()] @type errors() :: [error()]
@errors [ @errors [
@ -14,15 +16,19 @@ defmodule IbanEx.Error do
:invalid_format, :invalid_format,
:invalid_length, :invalid_length,
:invalid_checksum, :invalid_checksum,
:can_not_parse_map :can_not_parse_map,
] :length_to_long,
:length_to_short
]
@messages [ @messages [
unsupported_country_code: "Unsupported country code", unsupported_country_code: "Unsupported country code",
invalid_format: "IBAN violates required format", invalid_format: "IBAN violates required format",
invalid_length: "IBAN violates the required length", invalid_length: "IBAN violates the required length",
invalid_checksum: "IBAN's checksum is invalid", 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() @spec message(error()) :: String.t()

View File

@ -5,26 +5,49 @@ defmodule IbanEx.Validator do
alias IbanEx.Validator.Replacements alias IbanEx.Validator.Replacements
import IbanEx.Commons, only: [normalize: 1] 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()} @spec validate(String.t()) :: {:ok, String.t()} | {:error, Atom.t()}
def validate(iban) do def validate(iban) do
cond do cond do
iban_violates_format?(iban) -> iban_violates_format?(iban) -> {:error, :invalid_format}
{:error, :invalid_format} iban_unsupported_country?(iban) -> {:error, :unsupported_country_code}
iban_violates_length?(iban) -> {:error, :invalid_length}
iban_unsupported_country?(iban) -> iban_violates_country_rule?(iban) -> {:error, :invalid_format}
{:error, :unsupported_country_code} iban_violates_checksum?(iban) -> {:error, :invalid_checksum}
true -> {:ok, normalize(iban)}
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
end end
@ -37,12 +60,12 @@ defmodule IbanEx.Validator do
# - Check whether a given IBAN violates the required format. # - Check whether a given IBAN violates the required format.
@spec iban_violates_format?(String.t()) :: boolean @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)) do: Regex.match?(~r/[^A-Z0-9]/i, normalize(iban))
# - Check whether a given IBAN violates the supported countries. # - Check whether a given IBAN violates the supported countries.
@spec iban_unsupported_country?(String.t()) :: boolean @spec iban_unsupported_country?(String.t()) :: boolean
defp iban_unsupported_country?(iban) do def iban_unsupported_country?(iban) do
supported? = supported? =
iban iban
|> Parser.country_code() |> Parser.country_code()
@ -51,33 +74,52 @@ defmodule IbanEx.Validator do
!supported? !supported?
end 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 @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), 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() size(iban) != country_module.size()
else else
{:error, _} -> true {:error, _error} -> true
end end
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 @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), with country_code <- Parser.country_code(iban),
bban <- Parser.bban(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 rule <- country_module.rule() do
!Regex.match?(rule, bban) !Regex.match?(rule, bban)
else else
{:error, _} -> true {:error, _error} -> true
end end
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 @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" check_sum_base = Parser.bban(iban) <> Parser.country_code(iban) <> "00"
replacements = Replacements.replacements() replacements = Replacements.replacements()

View File

@ -2,7 +2,7 @@ defmodule IbanEx.MixProject do
use Mix.Project use Mix.Project
@source_url "https://g.tulz.dev/opensource/iban-ex" @source_url "https://g.tulz.dev/opensource/iban-ex"
@version "0.1.2" @version "0.1.3"
def project do def project do
[ [

View File

@ -1,6 +1,7 @@
defmodule IbanExTest do defmodule IbanExTest do
alias IbanEx.{Country, Iban, Parser} alias IbanEx.{Country, Iban, Parser}
use ExUnit.Case, async: true use ExUnit.Case, async: true
doctest_file "README.md"
doctest IbanEx.Country.AT doctest IbanEx.Country.AT
doctest IbanEx.Country.BE doctest IbanEx.Country.BE
doctest IbanEx.Country.BG doctest IbanEx.Country.BG

View File

@ -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