From 492cb2378e99627eee86e0b0360a6efcaaad9af5 Mon Sep 17 00:00:00 2001 From: Danylo Negrienko Date: Tue, 2 Dec 2025 11:14:40 -0500 Subject: [PATCH] Improve type specifications and documentation - Added missing type specifications for Hello function and rules - Updated documentation for the Deserialize protocol - Cleaned up IBAN validation function documentation - Enhanced test fixture generation with clearer parsing and error messages --- lib/iban_ex.ex | 1 + lib/iban_ex/country/template.ex | 4 ++-- lib/iban_ex/deserialize.ex | 15 +++++++------- lib/iban_ex/parser.ex | 10 +++------- lib/iban_ex/validator/validator.ex | 12 +++++------ lib/mix/tasks/generate_fixtures.ex | 32 +++++++++++++++++------------- mix.lock | 3 --- test/support/iban_factory.exs | 3 +-- test/support/test_data.exs | 4 ++-- 9 files changed, 41 insertions(+), 43 deletions(-) diff --git a/lib/iban_ex.ex b/lib/iban_ex.ex index 2a17e86..724e5bd 100644 --- a/lib/iban_ex.ex +++ b/lib/iban_ex.ex @@ -12,6 +12,7 @@ defmodule IbanEx do :world """ + @spec hello() :: :world def hello do :world end diff --git a/lib/iban_ex/country/template.ex b/lib/iban_ex/country/template.ex index bb24c9b..dcdff3b 100644 --- a/lib/iban_ex/country/template.ex +++ b/lib/iban_ex/country/template.ex @@ -9,7 +9,7 @@ defmodule IbanEx.Country.Template do @callback size() :: size() @callback rule() :: rule() - @callback rules() :: [] + @callback rules() :: keyword() @callback rules_map() :: %{} @callback bban_fields() :: [atom()] @callback bban_size() :: non_neg_integer() @@ -66,7 +66,7 @@ def bban_fields(), do: rules_map() |> Map.keys() def rules_map(), do: rules() |> Map.new() @impl IbanEx.Country.Template - @spec rules() :: [] + @spec rules() :: keyword() def rules() do {rules, _bban_size} = calculate_rules() rules diff --git a/lib/iban_ex/deserialize.ex b/lib/iban_ex/deserialize.ex index cd3e656..4a82204 100644 --- a/lib/iban_ex/deserialize.ex +++ b/lib/iban_ex/deserialize.ex @@ -1,4 +1,10 @@ defprotocol IbanEx.Deserialize do + @moduledoc """ + Protocol for converting various data types into IBAN structs. + + Implementations exist for String, Map, and List types. + """ + @type iban() :: IbanEx.Iban.t() @type iban_or_error() :: iban() @@ -15,13 +21,8 @@ def to_iban(value) defimpl IbanEx.Deserialize, for: [BitString, String] do alias IbanEx.{Parser, Error} @type iban() :: IbanEx.Iban.t() - @type iban_or_error() :: - iban() - | {:invalid_checksum, binary()} - | {:invalid_format, binary()} - | {:invalid_length, binary()} - | {:can_not_parse_map, binary()} - | {:unsupported_country_code, binary()} + @type iban_or_error() :: iban() | {atom(), binary()} + def to_iban(string) do case Parser.parse(string) do {:ok, iban} -> iban diff --git a/lib/iban_ex/parser.ex b/lib/iban_ex/parser.ex index 74f378e..fdfb98f 100644 --- a/lib/iban_ex/parser.ex +++ b/lib/iban_ex/parser.ex @@ -9,17 +9,12 @@ defmodule IbanEx.Parser do @type check_digits_string() :: <<_::16>> @type iban() :: IbanEx.Iban.t() - @type iban_or_error() :: - {:ok, iban()} - | {:invalid_checksum, binary()} - | {:invalid_format, binary()} - | {:invalid_length, binary()} - | {:can_not_parse_map, binary()} - | {:unsupported_country_code, binary()} + @type iban_or_error() :: {:ok, iban()} | {:error, atom()} @spec parse({:ok, binary()}) :: iban_or_error() def parse({:ok, iban_string}), do: parse(iban_string) + @spec parse(binary(), keyword()) :: iban_or_error() def parse(iban_string, options \\ [incomplete: false]) def parse(iban_string, incomplete: false) do @@ -70,6 +65,7 @@ def parse(iban_string, incomplete: true) do end @spec parse_bban(binary(), <<_::16>>) :: map() + @spec parse_bban(binary(), <<_::16>>, keyword()) :: map() def parse_bban(bban_string, country_code, options \\ [incomplete: false]) def parse_bban(bban_string, country_code, incomplete: true) do diff --git a/lib/iban_ex/validator/validator.ex b/lib/iban_ex/validator/validator.ex index 426337d..aafab6b 100644 --- a/lib/iban_ex/validator/validator.ex +++ b/lib/iban_ex/validator/validator.ex @@ -101,7 +101,7 @@ defp size(iban) do |> String.length() end - # - Check whether a given IBAN violates the required format. + @doc "Check whether a given IBAN violates the required format." @spec iban_violates_format?(String.t() | nil) :: boolean def iban_violates_format?(nil), do: true @@ -119,21 +119,21 @@ def iban_violates_format?(iban) when is_binary(iban) do has_invalid_chars or country_code_lowercase end - # - Check whether a given IBAN violates the required format in bank_code. + @doc "Check whether a given IBAN violates the required format in bank_code." @spec iban_violates_bank_code_format?(binary()) :: boolean def iban_violates_bank_code_format?(iban), do: iban_violates_bban_part_format?(iban, :bank_code) - # - Check whether a given IBAN violates the required format in branch_code. + @doc "Check whether a given IBAN violates the required format in branch_code." @spec iban_violates_branch_code_format?(binary()) :: boolean def iban_violates_branch_code_format?(iban), do: iban_violates_bban_part_format?(iban, :branch_code) - # - Check whether a given IBAN violates the required format in account_number. + @doc "Check whether a given IBAN violates the required format in account_number." @spec iban_violates_account_number_format?(binary()) :: boolean def iban_violates_account_number_format?(iban), do: iban_violates_bban_part_format?(iban, :account_number) - # - Check whether a given IBAN violates the required format in national_check. + @doc "Check whether a given IBAN violates the required format in national_check." @spec iban_violates_national_check_format?(binary()) :: boolean def iban_violates_national_check_format?(iban), do: iban_violates_bban_part_format?(iban, :national_check) @@ -150,7 +150,7 @@ defp iban_violates_bban_part_format?(iban, part) do end end - # - Check whether a given IBAN violates the supported countries. + @doc "Check whether a given IBAN violates the supported countries." @spec iban_unsupported_country?(String.t()) :: boolean def iban_unsupported_country?(iban) do supported? = diff --git a/lib/mix/tasks/generate_fixtures.ex b/lib/mix/tasks/generate_fixtures.ex index 1637b2d..bc7b7de 100644 --- a/lib/mix/tasks/generate_fixtures.ex +++ b/lib/mix/tasks/generate_fixtures.ex @@ -8,6 +8,8 @@ defmodule Mix.Tasks.GenerateFixtures do use Mix.Task + @dialyzer {:nowarn_function, generate_country_specs: 0, get_bban_spec: 1, get_positions: 2} + @shortdoc "Generate test fixture data" # IBAN examples from SWIFT registry via wise.com @@ -163,14 +165,14 @@ defp generate_valid_ibans do defp generate_country_specs do @iban_examples - |> Enum.map(fn {code, iban} -> - case IbanEx.Parser.parse(iban) do + |> Enum.map(fn {code, iban_string} -> + case IbanEx.Parser.parse(iban_string) do {:ok, parsed} -> # Get BBAN and check if numeric only - bban = String.slice(iban, 4..-1//1) + bban = String.slice(iban_string, 4..-1//1) numeric_only = String.match?(bban, ~r/^[0-9]+$/) - iban_length = String.length(iban) + iban_length = String.length(iban_string) bban_length = iban_length - 4 # Use actual country code from parsed IBAN (e.g., FI for AX) actual_country_code = parsed.country_code @@ -184,17 +186,20 @@ defp generate_country_specs do "sepa" => code in @sepa_countries, "numeric_only" => numeric_only, "positions" => %{ - "bank_code" => get_positions(parsed.bank_code, iban), - "branch_code" => get_positions(parsed.branch_code, iban), - "account_number" => get_positions(parsed.account_number, iban), - "national_check" => get_positions(parsed.national_check, iban) + "bank_code" => get_positions(parsed.bank_code, iban_string), + "branch_code" => get_positions(parsed.branch_code, iban_string), + "account_number" => get_positions(parsed.account_number, iban_string), + "national_check" => get_positions(parsed.national_check, iban_string) } } {code, spec} - {:error, reason} -> - IO.puts("Warning: Failed to parse #{code} IBAN: #{iban} - #{inspect(reason)}") + {:error, error_code} -> + IO.puts( + "Warning: Failed to parse #{code} IBAN: #{iban_string} - #{inspect(error_code)}" + ) + nil end end) @@ -206,8 +211,7 @@ defp format_print(iban) do iban |> String.graphemes() |> Enum.chunk_every(4) - |> Enum.map(&Enum.join/1) - |> Enum.join(" ") + |> Enum.map_join(" ", &Enum.join/1) end defp country_name(code) do @@ -338,11 +342,11 @@ defp generate_metadata(valid_ibans, country_specs) do } end -defp get_positions(nil, _iban), do: %{"start" => 0, "end" => 0} + defp get_positions(nil, _iban), do: %{"start" => 0, "end" => 0} defp get_positions("", _iban), do: %{"start" => 0, "end" => 0} defp get_positions(value, iban) do -# Remove country code and check digits (first 4 chars) + # Remove country code and check digits (first 4 chars) bban = String.slice(iban, 4..-1//1) case :binary.match(bban, value) do diff --git a/mix.lock b/mix.lock index 80bc250..4d4c6f8 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,5 @@ %{ - "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.13", "126a0697df6b7b71cd18c81bc92335297839a806b6f62b61d417500d1070ff4e", [: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", "47641e6d2bbff1e241e87695b29f617f1a8f912adea34296fb10ecc3d7e9e84f"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, @@ -9,7 +7,6 @@ "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "elixir_sense": {:hex, :elixir_sense, "1.0.0", "ae4313b90e5564bd8b66aaed823b5e5f586db0dbb8849b7c4b7e07698c0df7bc", [:mix], [], "hexpm", "6a3baef02859e3e1a7d6f355ad6a4fc962ec56cb0f396a8be2bd8091aaa28821"}, "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.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, diff --git a/test/support/iban_factory.exs b/test/support/iban_factory.exs index 20abe59..88a0d4a 100644 --- a/test/support/iban_factory.exs +++ b/test/support/iban_factory.exs @@ -178,7 +178,7 @@ defp calculate_check_digits(country_code, bban) do numeric = rearranged |> String.graphemes() - |> Enum.map(fn char -> + |> Enum.map_join(fn char -> if char =~ ~r/[A-Z]/ do [char_code] = String.to_charlist(char) Integer.to_string(char_code - 55) @@ -186,7 +186,6 @@ defp calculate_check_digits(country_code, bban) do char end end) - |> Enum.join() # Calculate mod 97 remainder = diff --git a/test/support/test_data.exs b/test/support/test_data.exs index cbb935c..51f5422 100644 --- a/test/support/test_data.exs +++ b/test/support/test_data.exs @@ -214,7 +214,7 @@ defp filter_by_numeric_only(specs, numeric_only) do Enum.filter(specs, fn {_code, spec} -> # Use numeric_only field if available, otherwise fall back to bban_spec check case spec["numeric_only"] do - nil -> is_numeric_only?(spec["bban_spec"]) == numeric_only + nil -> numeric_only?(spec["bban_spec"]) == numeric_only value -> value == numeric_only end end) @@ -230,7 +230,7 @@ defp has_national_check?(spec) do positions != nil and positions["start"] != positions["end"] end - defp is_numeric_only?(bban_spec) do + defp numeric_only?(bban_spec) do !String.contains?(bban_spec, ["!a", "!c"]) end end