├── .awconfig ├── rebar.lock ├── images ├── bagofcat.jpg └── samurai.jpg ├── .gitignore ├── test ├── cover.spec ├── elvis.config ├── ktn_binary_SUITE.erl ├── ktn_numbers_SUITE.erl ├── ktn_recipe_example.erl ├── ktn_date_SUITE.erl ├── ktn_base16_SUITE.erl ├── ktn_random_SUITE.erl ├── ktn_lists_SUITE.erl ├── ktn_task_SUITE.erl ├── ktn_os_SUITE.erl ├── ktn_maps_SUITE.erl └── ktn_recipe_SUITE.erl ├── src ├── katana.app.src ├── ktn_strings.erl ├── ktn_numbers.erl ├── ktn_binary.erl ├── ktn_debug.erl ├── ktn_rpc.erl ├── ktn_user_default.erl ├── ktn_type.erl ├── ktn_base16.erl ├── ktn_json.erl ├── ktn_maps.erl ├── ktn_task.erl ├── ktn_random.erl ├── ktn_lists.erl ├── ktn_date.erl ├── ktn_os.erl ├── ktn_test_utils.erl ├── ktn_recipe_verify.erl └── ktn_recipe.erl ├── .github └── workflows │ └── ci.yml ├── elvis.config ├── scripts └── validate_config ├── LICENSE ├── rebar.config ├── README.md └── CHANGELOG.md /.awconfig: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /images/bagofcat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inaka/erlang-katana/HEAD/images/bagofcat.jpg -------------------------------------------------------------------------------- /images/samurai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inaka/erlang-katana/HEAD/images/samurai.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | katana.d 2 | .erlang.mk/ 3 | .eunit 4 | deps 5 | *.o 6 | *.beam 7 | *.plt 8 | erl_crash.dump 9 | log 10 | logs 11 | bin 12 | ebin 13 | deps 14 | .erlang.mk.* 15 | hexer 16 | hexer.config 17 | doc 18 | _build -------------------------------------------------------------------------------- /test/cover.spec: -------------------------------------------------------------------------------- 1 | %% Specific modules to include in cover. 2 | { 3 | incl_mods, 4 | [ktn_binary, 5 | ktn_date, 6 | ktn_debug, 7 | ktn_json, 8 | ktn_lists, 9 | ktn_maps, 10 | ktn_numbers, 11 | ktn_os, 12 | ktn_random, 13 | ktn_recipe, 14 | ktn_recipe_verify, 15 | ktn_rpc, 16 | ktn_task, 17 | ktn_type, 18 | ktn_user_default, 19 | secure_vault 20 | ] 21 | }. 22 | -------------------------------------------------------------------------------- /src/katana.app.src: -------------------------------------------------------------------------------- 1 | {application, katana, 2 | [ 3 | {description, 4 | "Erlang grab bag of useful functions. " 5 | "It should have been called swiss army knife " 6 | "but katanas are deadlier"}, 7 | {vsn, "1.0.0"}, 8 | {applications, [kernel, stdlib]}, 9 | {modules, []}, 10 | {registered, []}, 11 | {licenses, ["Apache 2.0"]}, 12 | {links, [{"Github", "https://github.com/inaka/erlang-katana"}]} 13 | ] 14 | }. 15 | -------------------------------------------------------------------------------- /src/ktn_strings.erl: -------------------------------------------------------------------------------- 1 | -module(ktn_strings). 2 | 3 | -export([to_string/1]). 4 | 5 | -spec to_string(atom() | integer() | binary() | list()) -> list(). 6 | to_string(Value) when is_atom(Value) -> 7 | atom_to_list(Value); 8 | to_string(Value) when is_integer(Value) -> 9 | integer_to_list(Value); 10 | to_string(Value) when is_binary(Value) -> 11 | binary_to_list(Value); 12 | to_string(Value) when is_list(Value) -> 13 | Value. 14 | -------------------------------------------------------------------------------- /src/ktn_numbers.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% ktn_numbers: functions useful for processing numeric values 3 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4 | -module(ktn_numbers). 5 | 6 | -export([ 7 | binary_to_number/1 8 | ]). 9 | 10 | -spec binary_to_number(binary()) -> float() | integer(). 11 | binary_to_number(Bin) -> 12 | N = binary_to_list(Bin), 13 | case string:to_float(N) of 14 | {error, no_float} -> list_to_integer(N); 15 | {F, _Rest} -> F 16 | end. 17 | -------------------------------------------------------------------------------- /src/ktn_binary.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% ktn_binary: functions useful for handling binaries 3 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4 | -module(ktn_binary). 5 | 6 | -export([ 7 | join/2 8 | ]). 9 | 10 | %% @doc Joins and returns a list of binaries 11 | -spec join([binary()], binary()) -> binary(). 12 | join([], _) -> 13 | <<>>; 14 | join([S], _) when is_binary(S) -> 15 | S; 16 | join([H | T], Sep) -> 17 | B = << <> || X <- T >>, 18 | <>. -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: build 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | jobs: 11 | ci: 12 | name: Run checks and tests over ${{matrix.otp_vsn}} and ${{matrix.os}} 13 | runs-on: ${{matrix.os}} 14 | strategy: 15 | matrix: 16 | otp_vsn: [21, 22, 23, 24] 17 | os: [ubuntu-latest] 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: erlef/setup-beam@v1 21 | with: 22 | otp-version: ${{matrix.otp_vsn}} 23 | rebar3-version: '3.14' 24 | - run: sudo rebar3 test 25 | -------------------------------------------------------------------------------- /elvis.config: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | elvis, 4 | [ 5 | {config, 6 | [#{dirs => ["src", "test"], 7 | filter => "*.erl", 8 | ruleset => erl_files, 9 | rules => [{elvis_style, invalid_dynamic_call, #{ignore => [ktn_recipe_verify]}}, 10 | {elvis_style, dont_repeat_yourself, #{min_complexity => 15}}, 11 | {elvis_style, no_debug_call, #{debug_functions => [{ct, pal}]}} 12 | ] 13 | }, 14 | #{dirs => ["."], 15 | filter => "elvis.config", 16 | ruleset => elvis_config 17 | } 18 | ] 19 | } 20 | ] 21 | } 22 | ]. 23 | 24 | 25 | -------------------------------------------------------------------------------- /scripts/validate_config: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %% -*- erlang -*- 3 | main([File]) -> 4 | try 5 | case file:consult(File) of 6 | {ok, _} -> 7 | io:format("OK~n"), 8 | halt(0); 9 | {error, Error} -> 10 | io:format("~s~n", [file:format_error(Error)]), 11 | halt(1) 12 | end 13 | catch 14 | _:Exception -> 15 | io:format("error: ~p", [Exception]), 16 | usage() 17 | end; 18 | main(_) -> 19 | usage(). 20 | 21 | usage() -> 22 | io:format("usage: validate_config FILE~n"), 23 | halt(1). 24 | 25 | -------------------------------------------------------------------------------- /src/ktn_debug.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% ktn_debug: functions useful for debugging 3 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4 | -module(ktn_debug). 5 | 6 | -export( 7 | [ ppst/1 8 | ]). 9 | 10 | -spec ppst([any()]) -> 11 | [any()]. 12 | ppst(StackTrace) -> 13 | F = 14 | fun({_Module, Function, Arity, Props}) -> 15 | File = proplists:get_value(file, Props), 16 | Line = proplists:get_value(line, Props), 17 | io_lib:format("\t~s:~p:~p/~p~n", [File, Line, Function, Arity]) 18 | end, 19 | lists:flatten(["\n", lists:map(F, StackTrace), "\n"]). 20 | -------------------------------------------------------------------------------- /src/ktn_rpc.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% ktn_rpc: functions useful for RPC mechanisms 3 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4 | -module(ktn_rpc). 5 | 6 | -export( 7 | [ multicall/3 8 | ]). 9 | 10 | %% @doc runs rpc:multicall(M, F, A) and emits warnigns for errors, 11 | %% but returns 'ok'. 12 | -spec multicall(module(), atom(), [term()]) -> 13 | ok. 14 | multicall(M, F, A) -> 15 | {Results, BadNodes} = rpc:multicall(M, F, A), 16 | Errors = [Error || {badrpc, Error} <- Results], 17 | case {Errors, BadNodes} of 18 | {[], []} -> ok; 19 | {[], BadNodes} -> 20 | {error, {bad_nodes, [{mfa, {M, F, A}}, {nodes, BadNodes}]}}; 21 | {Errors, _} -> {error, {unknown, [{mfa, {M, F, A}}, {errors, Errors}]}} 22 | end. 23 | -------------------------------------------------------------------------------- /test/elvis.config: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | elvis, 4 | [ 5 | {config, 6 | [#{dirs => ["../../src", "../../test"], 7 | filter => "*.erl", 8 | ruleset => erl_files, 9 | rules => [ 10 | {elvis_style, invalid_dynamic_call, #{ignore => [ktn_recipe_verify]}}, 11 | {elvis_style, dont_repeat_yourself, #{min_complexity => 15}}, 12 | {elvis_style, no_debug_call, #{debug_functions => [{ct, pal}]}} 13 | ] 14 | }, 15 | #{dirs => ["../.."], 16 | filter => "Makefile", 17 | ruleset => makefiles, 18 | rules => [{elvis_project, protocol_for_deps_erlang_mk, #{ regex => "(https://.*|[0-9]+([.][0-9]+)*)"}}] 19 | }, 20 | #{dirs => ["../.."], 21 | filter => "elvis.config", 22 | ruleset => elvis_config 23 | } 24 | ] 25 | } 26 | ] 27 | } 28 | ]. 29 | -------------------------------------------------------------------------------- /src/ktn_user_default.erl: -------------------------------------------------------------------------------- 1 | -module(ktn_user_default). 2 | -export([ xref/0 3 | , l_all/1 4 | , cmd/1 5 | , all_modules/0 6 | , mk/0 7 | ]). 8 | 9 | -spec xref() -> proplists:proplist(). 10 | xref() -> 11 | xref:d("ebin"). 12 | 13 | -spec l_all([module()]) -> [{module(), code:load_ret()}]. 14 | l_all(Mods) when is_list(Mods) -> 15 | lists:foldl(fun(Mod, A) -> 16 | Ret=[{Mod, shell_default:l(Mod)}], 17 | lists:append(A, Ret) 18 | end, [], Mods); 19 | l_all(Mod) -> 20 | l_all([Mod]). 21 | 22 | -spec cmd(iodata()) -> _. 23 | cmd(Cmd) -> 24 | io:format("~s~n", [os:cmd(Cmd)]). 25 | 26 | -spec all_modules() -> [module()]. 27 | all_modules() -> 28 | [ list_to_atom( 29 | re:replace( 30 | filename:basename(F), "[.]beam$", "", [{return, list}])) 31 | || P <- code:get_path(), 32 | string:str(P, code:lib_dir()) == 0, 33 | F <- filelib:wildcard(filename:join(P, "*.beam"))]. 34 | 35 | -spec mk() -> up_to_date. 36 | mk() -> up_to_date = make:all([load]). 37 | -------------------------------------------------------------------------------- /src/ktn_type.erl: -------------------------------------------------------------------------------- 1 | -module(ktn_type). 2 | 3 | -export([ 4 | get/1 5 | ]). 6 | 7 | -type type() :: integer 8 | | float 9 | | list 10 | | tuple 11 | | binary 12 | | bitstring 13 | | boolean 14 | | function 15 | | pid 16 | | port 17 | | reference 18 | | atom 19 | | unknown 20 | . 21 | 22 | -spec get(term()) -> type(). 23 | get(X) when is_integer(X) -> integer; 24 | get(X) when is_float(X) -> float; 25 | get(X) when is_list(X) -> list; 26 | get(X) when is_tuple(X) -> tuple; 27 | get(X) when is_binary(X) -> binary; 28 | get(X) when is_bitstring(X) -> bitstring; % will fail before e12 29 | get(X) when is_boolean(X) -> boolean; 30 | get(X) when is_function(X) -> function; 31 | get(X) when is_pid(X) -> pid; 32 | get(X) when is_port(X) -> port; 33 | get(X) when is_reference(X) -> reference; 34 | get(X) when is_atom(X) -> atom; 35 | 36 | get(_X) -> unknown. 37 | -------------------------------------------------------------------------------- /src/ktn_base16.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% ktn_base16: encoding and decoding Base16 binaries 3 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4 | -module(ktn_base16). 5 | 6 | -export([ encode/1 7 | , decode/1 8 | ]). 9 | 10 | %% @doc Encodes a binary in base 16. 11 | -spec encode(binary()) -> binary(). 12 | encode(Bin) -> 13 | << <<(encode_char(A)):8/unsigned-integer, 14 | (encode_char(B)):8/unsigned-integer>> || 15 | <> <= Bin>>. 16 | 17 | %% @doc Decodes a base 16 binary. 18 | -spec decode(binary()) -> binary(). 19 | decode(Bin) -> 20 | << <<(decode_char(A)):4/unsigned-integer, 21 | (decode_char(B)):4/unsigned-integer>> || 22 | <> <= Bin>>. 23 | 24 | encode_char(C) when C > 9 -> 25 | C + $A - 10; 26 | encode_char(C) -> 27 | C + $0. 28 | 29 | decode_char(C) when C >= $A -> 30 | C - $A + 10; 31 | decode_char(C) when C >= $a -> 32 | C - $a + 10; 33 | decode_char(C) -> 34 | C - $0. -------------------------------------------------------------------------------- /src/ktn_json.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% ktn_json: functions useful for processing & creating JSON 3 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4 | -module(ktn_json). 5 | 6 | -export( 7 | [ json_date/1 8 | , json_datetime/1 9 | ]). 10 | 11 | %% @doc Converts a date record into a binary representation of its data. 12 | -spec json_date(ktn_date:date()) -> binary(). 13 | json_date({date, {Yi, Mi, Di}}) -> 14 | Y = integer_to_list(Yi), 15 | M = integer_to_list(Mi), 16 | D = integer_to_list(Di), 17 | iolist_to_binary([Y, "-", M, "-", D, "T00:00:00.000000Z"]). 18 | 19 | %% @doc Converts a datetime record into a binary representation of its data. 20 | -spec json_datetime(ktn_date:datetime()) -> binary(). 21 | json_datetime({datetime, {{Yi, Mi, Di}, {Hi, Ni, Si}}}) -> 22 | Y = integer_to_list(Yi), 23 | M = integer_to_list(Mi), 24 | D = integer_to_list(Di), 25 | H = integer_to_list(Hi), 26 | N = integer_to_list(Ni), 27 | S = integer_to_list(Si), 28 | iolist_to_binary([Y, "-", M, "-", D, "T", H, ":", N, ":", S, ".000000Z"]). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Federico Carrone 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ktn_maps.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% ktn_maps: functions useful for handling maps 3 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4 | -module(ktn_maps). 5 | 6 | -export([ 7 | get/2, 8 | get/3 9 | ]). 10 | 11 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 12 | %%% Public API 13 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 14 | 15 | -spec get(term(), map()) -> term(). 16 | get(Keys, Map) -> 17 | get(Keys, Map, undefined, error). 18 | 19 | -spec get(term(), map(), term()) -> term(). 20 | get(Keys, Map, Default) -> 21 | get(Keys, Map, Default, default). 22 | 23 | %% @private 24 | -spec get(term(), map(), term(), error | default) -> term(). 25 | get([Key], Map, Default, Type) -> 26 | get(Key, Map, Default, Type); 27 | get([Key | Rest], Map, Default, Type) -> 28 | case get(Key, Map, Default, Type) of 29 | NewMap when is_map(NewMap) -> 30 | get(Rest, NewMap, Default, Type); 31 | _ -> 32 | Default 33 | end; 34 | get(Key, Map, Default, Type) -> 35 | case {Type, maps:is_key(Key, Map)} of 36 | {_, true} -> 37 | maps:get(Key, Map); 38 | {default, false} -> 39 | Default; 40 | {error, false} -> 41 | error(bad_path) 42 | end. 43 | -------------------------------------------------------------------------------- /test/ktn_binary_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(ktn_binary_SUITE). 2 | 3 | 4 | -export([ 5 | all/0, 6 | init_per_suite/1, 7 | end_per_suite/1 8 | ]). 9 | 10 | -export([ 11 | join/1 12 | ]). 13 | 14 | -define(EXCLUDED_FUNS, 15 | [ 16 | module_info, 17 | all, 18 | test, 19 | init_per_suite, 20 | end_per_suite 21 | ]). 22 | 23 | -type config() :: [{atom(), term()}]. 24 | 25 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 26 | %% Common test 27 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 28 | 29 | -spec all() -> [atom()]. 30 | all() -> 31 | Exports = ?MODULE:module_info(exports), 32 | [F || {F, _} <- Exports, not lists:member(F, ?EXCLUDED_FUNS)]. 33 | 34 | -spec init_per_suite(config()) -> config(). 35 | init_per_suite(Config) -> 36 | Config. 37 | 38 | -spec end_per_suite(config()) -> config(). 39 | end_per_suite(Config) -> 40 | Config. 41 | 42 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 43 | %% Test Cases 44 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 45 | 46 | -spec join(config()) -> ok. 47 | join(_Config) -> 48 | Binaries = [<<"foo">>, <<"bar">>, <<"buzz">>], 49 | <<>> = ktn_binary:join([], <<", ">>), 50 | <<"foo">> = ktn_binary:join([<<"foo">>], <<", ">>), 51 | <<"foo, bar, buzz">> = ktn_binary:join(Binaries, <<", ">>), 52 | ok. 53 | -------------------------------------------------------------------------------- /test/ktn_numbers_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(ktn_numbers_SUITE). 2 | 3 | -export([ 4 | all/0, 5 | init_per_suite/1, 6 | end_per_suite/1 7 | ]). 8 | 9 | -export([ 10 | bin_to_number/1 11 | ]). 12 | 13 | -define(EXCLUDED_FUNS, 14 | [ 15 | module_info, 16 | all, 17 | test, 18 | init_per_suite, 19 | end_per_suite 20 | ]). 21 | 22 | -type config() :: [{atom(), term()}]. 23 | 24 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 25 | %% Common test 26 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 27 | 28 | -spec all() -> [atom()]. 29 | all() -> 30 | Exports = ?MODULE:module_info(exports), 31 | [F || {F, _} <- Exports, not lists:member(F, ?EXCLUDED_FUNS)]. 32 | 33 | -spec init_per_suite(config()) -> config(). 34 | init_per_suite(Config) -> 35 | Config. 36 | 37 | -spec end_per_suite(config()) -> config(). 38 | end_per_suite(Config) -> 39 | Config. 40 | 41 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 42 | %% Test Cases 43 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 44 | 45 | -spec bin_to_number(config()) -> ok. 46 | bin_to_number(_Config) -> 47 | 102.5 = ktn_numbers:binary_to_number(<<"102.5">>), 48 | 102 = ktn_numbers:binary_to_number(<<"102">>), 49 | ok = try 50 | ktn_numbers:binary_to_number(<<"a">>) 51 | catch 52 | error:badarg -> ok 53 | end. 54 | -------------------------------------------------------------------------------- /test/ktn_recipe_example.erl: -------------------------------------------------------------------------------- 1 | -module(ktn_recipe_example). 2 | -author('igarai@gmail.com'). 3 | 4 | -behaviour (ktn_recipe). 5 | 6 | %%% Required 7 | -export( 8 | [ transitions/0 9 | , process_result/1 10 | , process_error/1 11 | ]). 12 | %%% Steps 13 | -export( 14 | [ s1/1 15 | , s2/1 16 | , s3/1 17 | , s4/1 18 | , s5/1 19 | , s6/1 20 | , s7/1 21 | , s8/1 22 | , s9/1 23 | , s0/1 24 | ]). 25 | 26 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 27 | %%% s3 error 28 | %%% / \ / 29 | %%% s1 -> s2 -> s5 -> s6 -> s7 -> s8 -> s9 -> s0 30 | %%% \ / \ 31 | %%% s4------ halt 32 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 33 | 34 | transitions() -> 35 | [ s1 36 | , {s2, i1, s3} 37 | , {s2, i2, s4} 38 | , {s2, i3, s5} 39 | , {s3, i4, s5} 40 | , {s4, i5, s6} 41 | , s5 42 | , {s6, ok, fun ktn_recipe_example:s7/1} 43 | , {fun ktn_recipe_example:s7/1, ok, s8} 44 | , s8 45 | , {s8, i6, error} 46 | , {s8, i7, halt} 47 | , fun ktn_recipe_example:s9/1 48 | , s0 49 | ]. 50 | 51 | process_result(S) -> 52 | {ok, S}. 53 | 54 | process_error(S) -> 55 | {error, S}. 56 | 57 | s1(S) -> {ok, [s1_ok | S]}. 58 | s2(S) -> {i1, [s2_ok | S]}. 59 | s3(S) -> {i4, [s3_ok | S]}. 60 | s4(S) -> {i5, [s4_ok | S]}. 61 | s5(S) -> {ok, [s5_ok | S]}. 62 | s6(S) -> {ok, [s6_ok | S]}. 63 | s7(S) -> {ok, [s7_ok | S]}. 64 | s8(S) -> {ok, [s8_ok | S]}. 65 | s9(S) -> {ok, [s9_ok | S]}. 66 | s0(S) -> {ok, [s0_ok | S]}. 67 | -------------------------------------------------------------------------------- /test/ktn_date_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(ktn_date_SUITE). 2 | 3 | -export([all/0]). 4 | 5 | -export([ 6 | shift_days/1, 7 | shift_months/1 8 | ]). 9 | 10 | -type config() :: [{atom(), term()}]. 11 | 12 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 13 | %% Common test 14 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 15 | 16 | -spec all() -> [atom()]. 17 | all() -> 18 | [ 19 | shift_days, 20 | shift_months 21 | ]. 22 | 23 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 24 | %% Test Cases 25 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 26 | 27 | -spec shift_days(config()) -> ok. 28 | shift_days(_Config) -> 29 | Datetime = {{2018, 8, 15}, {12, 12, 12}}, 30 | {{2018, 8, 16}, {12, 12, 12}} = ktn_date:shift_days(Datetime, 1), 31 | {{2018, 8, 14}, {12, 12, 12}} = ktn_date:shift_days(Datetime, -1), 32 | {{2018, 9, 4}, {12, 12, 12}} = ktn_date:shift_days(Datetime, 20), 33 | ok. 34 | 35 | -spec shift_months(config()) -> ok. 36 | shift_months(_Config) -> 37 | {2018, 10, 15} = ktn_date:shift_months({2018, 8, 15}, 2), 38 | {2018, 6, 15} = ktn_date:shift_months({2018, 8, 15}, -2), 39 | {2019, 2, 15} = ktn_date:shift_months({2018, 8, 15}, 6), 40 | 41 | {2018, 10, 31} = ktn_date:shift_months({2018, 8, 31}, 2), 42 | {2018, 6, 30} = ktn_date:shift_months({2018, 8, 31}, -2), 43 | {2019, 2, 28} = ktn_date:shift_months({2018, 8, 31}, 6), 44 | 45 | ok = try 46 | _ = ktn_date:shift_months({0, 8, 31}, -9), 47 | out_of_bounds_expected 48 | catch 49 | error:out_of_bounds -> ok 50 | end. -------------------------------------------------------------------------------- /src/ktn_task.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% ktn_task: functions useful for managing asyncronous tasks 3 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4 | -module(ktn_task). 5 | 6 | -export([ 7 | wait_for/2, 8 | wait_for/4, 9 | wait_for_success/1, 10 | wait_for_success/3 11 | ]). 12 | 13 | -type task(T) :: fun(() -> T). 14 | 15 | -spec wait_for(task(T1), T2) -> {error, {timeout, {badmatch, T1}}} | T2 16 | when is_subtype(T2, T1). 17 | wait_for(Task, ExpectedAnswer) -> 18 | wait_for(Task, ExpectedAnswer, 200, 10). 19 | 20 | -spec wait_for(task(T1), T2, pos_integer(), pos_integer()) -> 21 | {error, {timeout, {badmatch, T1}}} | T2 22 | when is_subtype(T2, T1). 23 | wait_for(Task, ExpectedAnswer, SleepTime, Retries) -> 24 | wait_for_success(fun() -> 25 | ExpectedAnswer = Task() 26 | end, SleepTime, Retries). 27 | 28 | -spec wait_for_success(task(T)) -> {error, {timeout, term()}} | T. 29 | wait_for_success(Task) -> 30 | wait_for_success(Task, 200, 10). 31 | 32 | -spec wait_for_success(task(T), pos_integer(), pos_integer()) -> 33 | {error, {timeout, term()}} | T. 34 | wait_for_success(Task, SleepTime, Retries) -> 35 | wait_for_success(Task, undefined, SleepTime, Retries). 36 | 37 | wait_for_success(_Task, Exception, _SleepTime, 0) -> 38 | {error, {timeout, Exception}}; 39 | wait_for_success(Task, _Exception, SleepTime, Retries) -> 40 | try 41 | Task() 42 | catch 43 | _:NewException -> 44 | timer:sleep(SleepTime), 45 | wait_for_success(Task, NewException, SleepTime, Retries - 1) 46 | end. 47 | -------------------------------------------------------------------------------- /test/ktn_base16_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(ktn_base16_SUITE). 2 | 3 | -export([ 4 | all/0, 5 | init_per_suite/1, 6 | end_per_suite/1 7 | ]). 8 | 9 | -export([ 10 | encode/1 11 | ]). 12 | 13 | -define(EXCLUDED_FUNS, 14 | [ 15 | module_info, 16 | all, 17 | test, 18 | init_per_suite, 19 | end_per_suite 20 | ]). 21 | 22 | -type config() :: [{atom(), term()}]. 23 | 24 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 25 | %% Common test 26 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 27 | 28 | -spec all() -> [atom()]. 29 | all() -> 30 | Exports = ?MODULE:module_info(exports), 31 | [F || {F, _} <- Exports, not lists:member(F, ?EXCLUDED_FUNS)]. 32 | 33 | -spec init_per_suite(config()) -> config(). 34 | init_per_suite(Config) -> 35 | Config. 36 | 37 | -spec end_per_suite(config()) -> config(). 38 | end_per_suite(Config) -> 39 | Config. 40 | 41 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 42 | %% Test Cases 43 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 44 | 45 | -spec encode(config()) -> ok. 46 | encode(_Config) -> 47 | % Test both encoding and decoding with known values 48 | <<"0102030405060708090A0B0C0D0E0F1021324B64FF">> = 49 | ktn_base16:encode(<<1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 50 | 16, 33, 50, 75, 100, 255>>), 51 | <<1, 35, 69, 103, 137, 171, 205, 239>> = 52 | ktn_base16:decode(<<"0123456789ABCDEF">>), 53 | % Test that lowecase works too 54 | <<1, 35, 69, 103, 137, 171, 205, 239, 171, 205, 239>> = 55 | ktn_base16:decode(<<"0123456789ABCDEFabcdef">>), 56 | ok. -------------------------------------------------------------------------------- /src/ktn_random.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% ktn_random: a wrapper for generating random alfanumeric strings 3 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4 | -module(ktn_random). 5 | 6 | -export([ 7 | string/0, 8 | string/1, 9 | uniform/1, 10 | uniform/2, 11 | pick/1 12 | ]). 13 | 14 | -spec string() -> nonempty_string(). 15 | string() -> 16 | Length = get_random_length(), 17 | string(Length). 18 | 19 | -spec string(pos_integer()) -> nonempty_string(). 20 | string(Length) -> 21 | RandomAllowedChars = get_random_allowed_chars(), 22 | [ random_alphanumeric(RandomAllowedChars) 23 | || _N <- lists:seq(1, Length) 24 | ]. 25 | 26 | -spec uniform(term()) -> non_neg_integer() | {error, {invalid_value, term()}}. 27 | uniform(Max) when Max > 0-> 28 | rand:uniform(Max); 29 | uniform(Max) -> 30 | {error, {invalid_value, Max}}. 31 | 32 | -spec uniform(term(), term()) -> 33 | non_neg_integer() | {error, {invalid_range, term(), term()}}. 34 | uniform(Min, Max) when Max > Min -> 35 | Min + rand:uniform(Max - Min + 1) - 1; 36 | uniform(Min, Max) -> 37 | {error, {invalid_range, Min, Max}}. 38 | 39 | %% @doc Randomly chooses one element from the list 40 | -spec pick([X, ...]) -> X. 41 | pick(List) -> lists:nth(uniform(length(List)), List). 42 | 43 | %% internal 44 | random_alphanumeric(AllowedChars) -> 45 | Length = erlang:length(AllowedChars), 46 | lists:nth(rand:uniform(Length), AllowedChars). 47 | 48 | get_random_length() -> 49 | case application:get_env(katana, random_length) of 50 | {ok, SecretLength} -> 51 | SecretLength; 52 | undefined -> 53 | 16 54 | end. 55 | 56 | get_random_allowed_chars() -> 57 | case application:get_env(katana, random_allowed_chars) of 58 | {ok, RandomAllowedChars} -> 59 | RandomAllowedChars; 60 | undefined -> 61 | "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 62 | end. 63 | -------------------------------------------------------------------------------- /test/ktn_random_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(ktn_random_SUITE). 2 | 3 | -export([ 4 | all/0, 5 | init_per_suite/1, 6 | end_per_suite/1 7 | ]). 8 | 9 | -export([ 10 | string/1, 11 | uniform/1, 12 | pick/1 13 | ]). 14 | 15 | -define(EXCLUDED_FUNS, 16 | [ 17 | module_info, 18 | all, 19 | test, 20 | init_per_suite, 21 | end_per_suite 22 | ]). 23 | 24 | -type config() :: [{atom(), term()}]. 25 | 26 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 27 | %% Common test 28 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 29 | 30 | -spec all() -> [atom()]. 31 | all() -> 32 | Exports = ?MODULE:module_info(exports), 33 | [F || {F, _} <- Exports, not lists:member(F, ?EXCLUDED_FUNS)]. 34 | 35 | -spec init_per_suite(config()) -> config(). 36 | init_per_suite(Config) -> 37 | Config. 38 | 39 | -spec end_per_suite(config()) -> config(). 40 | end_per_suite(Config) -> 41 | Config. 42 | 43 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 44 | %% Test Cases 45 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 46 | 47 | -spec string(config()) -> ok. 48 | string(_Config) -> 49 | true = is_list(ktn_random:string()), 50 | 16 = length(ktn_random:string()), 51 | 25 = length(ktn_random:string(25)), 52 | ok. 53 | 54 | -spec uniform(config()) -> ok. 55 | uniform(_Config) -> 56 | Times = 10000, 57 | do_times(fun (_) -> in_range(ktn_random:uniform(10), 1, 10) end, Times), 58 | {error, _} = ktn_random:uniform(0), 59 | 60 | do_times(fun (_) -> in_range(ktn_random:uniform(5, 90), 5, 90) end, Times), 61 | {error, _} = ktn_random:uniform(165, 165), 62 | {error, _} = ktn_random:uniform(15, 5), 63 | ok. 64 | 65 | -spec pick(config()) -> ok. 66 | pick(_Config) -> 67 | [1, 2, 3, 4] = [ktn_random:pick([I]) || I <- lists:seq(1, 4)], 68 | lists:foreach( 69 | fun(I) -> 70 | List = lists:seq($a, $a + I), 71 | K = ktn_random:pick(List), 72 | true = lists:member(K, List) 73 | end, lists:seq(1, 1000)). 74 | 75 | do_times(Fun, N) -> 76 | lists:foreach(Fun, lists:seq(1, N)). 77 | 78 | in_range(X, Min, Max) when Min =< X, X =< Max -> ok. 79 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang;erlang-indent-level: 2;indent-tabs-mode: nil -*- 2 | %% ex: ts=4 sw=4 ft=erlang et 3 | 4 | %% == Erlang Compiler == 5 | 6 | %% Erlang compiler options 7 | {erl_opts, [ warn_unused_vars 8 | , warn_export_all 9 | , warn_shadow_vars 10 | , warn_unused_import 11 | , warn_unused_function 12 | , warn_bif_clash 13 | , warn_unused_record 14 | , warn_deprecated_function 15 | , warn_obsolete_guard 16 | , strict_validation 17 | , warn_export_vars 18 | , warn_exported_vars 19 | , warn_missing_spec 20 | , warn_untyped_record 21 | , debug_info]}. 22 | 23 | {profiles, [ 24 | {test, [ 25 | {erl_opts, [nowarn_missing_spec]}, 26 | {dialyzer, [{plt_extra_apps, [common_test, dialyzer, tools]}]} 27 | ]} 28 | ]}. 29 | 30 | {alias, [{test, [xref, dialyzer, lint, hank, ct, cover, edoc]}]}. 31 | 32 | %% == Common Test == 33 | 34 | {ct_compile_opts, [ warn_unused_vars 35 | , warn_export_all 36 | , warn_shadow_vars 37 | , warn_unused_import 38 | , warn_unused_function 39 | , warn_bif_clash 40 | , warn_unused_record 41 | , warn_deprecated_function 42 | , warn_obsolete_guard 43 | , strict_validation 44 | , warn_export_vars 45 | , warn_exported_vars 46 | , warn_missing_spec 47 | , warn_untyped_record 48 | , debug_info]}. 49 | 50 | {ct_opts, []}. 51 | 52 | %% == Cover == 53 | 54 | {cover_enabled, true}. 55 | 56 | {cover_opts, [verbose]}. 57 | 58 | %% == Dependencies == 59 | 60 | {project_plugins, [ 61 | rebar3_lint, 62 | rebar3_hex, 63 | rebar3_hank 64 | ]}. 65 | 66 | %% == Dialyzer == 67 | 68 | {dialyzer, [ {warnings, [ unmatched_returns 69 | , error_handling 70 | , unknown 71 | ]} 72 | , {plt_extra_apps, [tools]}]}. 73 | 74 | %% == Xref == 75 | 76 | {xref_checks,[ undefined_function_calls 77 | , locals_not_used 78 | , deprecated_function_calls 79 | , deprecated_functions 80 | ]}. 81 | 82 | %% == hank == 83 | 84 | {hank, [ 85 | {ignore, [ 86 | {"test/**", unnecessary_function_arguments} 87 | ]} 88 | ]}. 89 | -------------------------------------------------------------------------------- /src/ktn_lists.erl: -------------------------------------------------------------------------------- 1 | -module(ktn_lists). 2 | 3 | -export([ 4 | delete_first/2, 5 | split_when/2, 6 | map/3, 7 | filter/3 8 | ]). 9 | 10 | %% @doc Returns a copy of List deleting the first Element where Fun(Element) 11 | %% returns true, if there is such an element. 12 | %% @end 13 | -spec delete_first(fun((term()) -> boolean()), list()) -> list(). 14 | delete_first(Fun, List) -> 15 | delete_first(Fun, List, []). 16 | 17 | delete_first(Fun, [], Acc) when is_function(Fun, 1) -> 18 | lists:reverse(Acc); 19 | delete_first(Fun, [Head | Tail], Acc) -> 20 | case Fun(Head) of 21 | false -> 22 | delete_first(Fun, Tail, [Head | Acc]); 23 | true -> 24 | lists:concat([lists:reverse(Acc), Tail]) 25 | end. 26 | 27 | %% @doc Splits a list whenever an element satisfies the When predicate. 28 | %% Returns a list of lists where each list includes the matched element 29 | %% as its last one. 30 | %% E.g. 31 | %% split_when(fun (X) -> $. == X end, "a.b.c") = ["a.", "b.", "c"] 32 | %% @end 33 | -spec split_when(fun(), list()) -> list(). 34 | split_when(When, List) -> 35 | split_when(When, List, [[]]). 36 | 37 | split_when(When, [], [[] | Results]) -> 38 | split_when(When, [], Results); 39 | split_when(_When, [], Results) -> 40 | Reversed = lists:map(fun lists:reverse/1, Results), 41 | lists:reverse(Reversed); 42 | split_when(When, [Head | Tail], [Current0 | Rest]) -> 43 | Current = [Head | Current0], 44 | Result = case When(Head) of 45 | true -> 46 | [[], Current | Rest]; 47 | false -> 48 | [Current | Rest] 49 | end, 50 | split_when(When, Tail, Result). 51 | 52 | %% @doc Like lists:map/2 but allows specifying additional arguments. 53 | %% E.g. 54 | %% ktn_lists:map(fun (X, Y) -> X * Y end, [2], [1, 2, 3]) = [2, 4, 6] 55 | %% @end 56 | -spec map(fun(), list(), list()) -> list(). 57 | map(Fun, Args, [Head | Tail]) -> 58 | [apply(Fun, [Head | Args])| map(Fun, Args, Tail)]; 59 | map(Fun, _, []) when is_function(Fun) -> 60 | []. 61 | 62 | %% @doc Like lists:filter/2 but allows specifying additional arguments. 63 | %% E.g. 64 | %% `ktn_lists:filter(fun (X, Y) -> X * Y < 3 end, [2], [1, 2, 3]) = [2]' 65 | %% @end 66 | -spec filter(fun(), list(), list()) -> list(). 67 | filter(Pred, Args, List) when is_function(Pred) -> 68 | [Elem || Elem <- List, apply(Pred, [Elem | Args])]. 69 | -------------------------------------------------------------------------------- /src/ktn_date.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% ktn_date: functions useful for handling dates and time values 3 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 4 | -module(ktn_date). 5 | 6 | -define(SECONDS_IN_A_DAY, 86400). 7 | 8 | -export([ 9 | now_human_readable/0, 10 | shift_days/2, 11 | shift_months/2 12 | ]). 13 | 14 | -type date() :: {date, {non_neg_integer(), 1..12, 1..31}}. 15 | -type datetime() :: { datetime 16 | , {{integer(), 1..12, 1..31} 17 | , {1..24, 1..60, 1..60}} 18 | }. 19 | 20 | -export_type( 21 | [ date/0 22 | , datetime/0 23 | ]). 24 | 25 | %% @doc Returns the current date in a human readable format binary. 26 | -spec now_human_readable() -> binary(). 27 | now_human_readable() -> 28 | TimeStamp = {_, _, Micro} = os:timestamp(), 29 | {{Year, Month, Day}, 30 | {Hour, Minute, Second}} = calendar:now_to_universal_time(TimeStamp), 31 | DateList = io_lib:format("~p-~2..0B-~2..0BT~p:~p:~p.~6..0wZ", 32 | [Year, Month, Day, Hour, Minute, Second, Micro]), 33 | list_to_binary(DateList). 34 | 35 | %% @doc Moves the received datetime `N' days to the future (or to the past) 36 | -spec shift_days(calendar:datetime(), integer()) -> calendar:datetime(). 37 | shift_days(Datetime, N) -> 38 | Shift = ?SECONDS_IN_A_DAY * N, 39 | Secs = calendar:datetime_to_gregorian_seconds(Datetime), 40 | calendar:gregorian_seconds_to_datetime(Secs + Shift). 41 | 42 | %% @doc Moves the received date `N' months to the future (or to the past) 43 | -spec shift_months(calendar:date(), integer()) -> calendar:date(). 44 | shift_months({Y, M, D}, N) -> 45 | %% in order for the modular arithmetic to work, months in this function 46 | %% range from 0 to 11 (January to December) 47 | TotalMonths = 12*Y + M-1 + N, 48 | 49 | case TotalMonths >= 0 of 50 | true -> 51 | Month = TotalMonths rem 12, 52 | Year = (TotalMonths - Month) div 12, 53 | 54 | %% add one back to the month to fix our tricky mod 12 55 | find_valid_date({Year, Month+1, D}); 56 | false -> 57 | error(out_of_bounds) 58 | end. 59 | 60 | %% @doc Returns `Date' if valid. Otherwise, returns `Date' replacing `Day' 61 | %% with the last day of the month. 62 | find_valid_date(Date) -> 63 | case calendar:valid_date(Date) of 64 | true -> 65 | Date; 66 | false -> 67 | {Y, M, _} = Date, 68 | {Y, M, calendar:last_day_of_the_month(Y, M)} 69 | end. 70 | -------------------------------------------------------------------------------- /src/ktn_os.erl: -------------------------------------------------------------------------------- 1 | %% @doc Utility functions to run commands in the underlying OS. 2 | -module(ktn_os). 3 | 4 | -export([command/1, command/2]). 5 | 6 | -type opts() :: #{ log_fun => fun((iodata()) -> any()) 7 | , timeout => integer() 8 | , monitor => reference() 9 | }. 10 | -type exit_status() :: integer(). 11 | 12 | -spec command(iodata()) -> {exit_status(), string()}. 13 | command(Cmd) -> 14 | Opts = #{log_fun => fun error_logger:info_msg/1}, 15 | command(Cmd, Opts). 16 | 17 | -spec command(iodata(), opts()) -> {exit_status(), string()}. 18 | command(Cmd, Opts) -> 19 | PortOpts = [hide, stream, exit_status, eof, stderr_to_stdout], 20 | Port = open_port({spawn, shell_cmd()}, PortOpts), 21 | MonRef = erlang:monitor(port, Port), 22 | true = erlang:port_command(Port, make_cmd(Cmd)), 23 | Result = get_data(Port, Opts#{monitor => MonRef}, []), 24 | _ = demonitor(MonRef, [flush]), 25 | Result. 26 | 27 | -spec get_data(port(), opts(), [string()]) -> {exit_status(), string()}. 28 | get_data(Port, Opts, Data) -> 29 | %% Get timeout option or an hour if undefined. 30 | Timeout = maps:get(timeout, Opts, 600000), 31 | MonRef = maps:get(monitor, Opts), 32 | receive 33 | {Port, {data, NewData}} -> 34 | case maps:get(log_fun, Opts, undefined) of 35 | Fun when is_function(Fun) -> Fun(NewData); 36 | undefined -> ok 37 | end, 38 | get_data(Port, Opts, [NewData | Data]); 39 | {Port, eof} -> 40 | catch port_close(Port), 41 | flush_until_down(Port, MonRef), 42 | receive 43 | {Port, {exit_status, ExitStatus}} -> 44 | {ExitStatus, lists:flatten(lists:reverse(Data))} 45 | end; 46 | {'DOWN', MonRef, _, _, Reason} -> 47 | flush_exit(Port), 48 | exit({error, Reason, lists:flatten(lists:reverse(Data))}) 49 | after 50 | Timeout -> exit(timeout) 51 | end. 52 | 53 | -spec make_cmd(string()) -> iodata(). 54 | make_cmd(Cmd) -> 55 | %% We insert a new line after the command, in case the command 56 | %% contains a comment character. 57 | [$(, unicode:characters_to_binary(Cmd), "\n) string(). 60 | shell_cmd() -> "/bin/sh -s unix:cmd". 61 | 62 | %% When port_close returns we know that all the 63 | %% messages sent have been sent and that the 64 | %% DOWN message is after them all. 65 | flush_until_down(Port, MonRef) -> 66 | receive 67 | {Port, {data, _Bytes}} -> 68 | flush_until_down(Port, MonRef); 69 | {'DOWN', MonRef, _, _, _} -> 70 | flush_exit(Port) 71 | end. 72 | 73 | %% The exit signal is always delivered before 74 | %% the down signal, so we can be sure that if there 75 | %% was an exit message sent, it will be in the 76 | %% mailbox now. 77 | flush_exit(Port) -> 78 | receive 79 | {'EXIT', Port, _} -> 80 | ok 81 | after 0 -> 82 | ok 83 | end. 84 | -------------------------------------------------------------------------------- /test/ktn_lists_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(ktn_lists_SUITE). 2 | 3 | -export([ 4 | all/0, 5 | init_per_suite/1, 6 | end_per_suite/1 7 | ]). 8 | 9 | -export([ 10 | delete_first/1, 11 | split_when/1, 12 | map/1, 13 | filter/1 14 | ]). 15 | 16 | -define(EXCLUDED_FUNS, 17 | [ 18 | module_info, 19 | all, 20 | test, 21 | init_per_suite, 22 | end_per_suite 23 | ]). 24 | 25 | -type config() :: [{atom(), term()}]. 26 | 27 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 28 | %% Common test 29 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 30 | 31 | -spec all() -> [atom()]. 32 | all() -> 33 | Exports = ?MODULE:module_info(exports), 34 | [F || {F, _} <- Exports, not lists:member(F, ?EXCLUDED_FUNS)]. 35 | 36 | -spec init_per_suite(config()) -> config(). 37 | init_per_suite(Config) -> 38 | Config. 39 | 40 | -spec end_per_suite(config()) -> config(). 41 | end_per_suite(Config) -> 42 | Config. 43 | 44 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 45 | %% Test Cases 46 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 47 | 48 | -spec delete_first(config()) -> ok. 49 | delete_first(_Config) -> 50 | Fun = fun(N) -> 0 == N rem 2 end, 51 | 52 | [] = ktn_lists:delete_first(Fun, []), 53 | [] = ktn_lists:delete_first(Fun, [4]), 54 | [4] = ktn_lists:delete_first(Fun, [4, 4]), 55 | [1, 3] = ktn_lists:delete_first(Fun, [1, 3]), 56 | [1, 3] = ktn_lists:delete_first(Fun, [1, 4, 3]), 57 | [1, 3, 4] = ktn_lists:delete_first(Fun, [1, 4, 3, 4]), 58 | ok. 59 | 60 | -spec split_when(config()) -> ok. 61 | split_when(_Config) -> 62 | IsDot = fun(Ch) -> $. == Ch end, 63 | 64 | ["{a}.", " {b}."] = ktn_lists:split_when(IsDot, "{a}. {b}."), 65 | [] = ktn_lists:split_when(IsDot, ""), 66 | ["."] = ktn_lists:split_when(IsDot, "."), 67 | ["{a}.", " {b}.", "{c, d, e}"] = 68 | ktn_lists:split_when(IsDot, "{a}. {b}.{c, d, e}"), 69 | ["{a} {b}{c, d, e}"] = ktn_lists:split_when(IsDot, "{a} {b}{c, d, e}"), 70 | ok. 71 | 72 | -spec map(config()) -> ok. 73 | map(_Config) -> 74 | Sum = fun(X, Y) -> X + Y end, 75 | [3, 4, 5] = ktn_lists:map(Sum, [2], [1, 2, 3]), 76 | 77 | Multiply = fun(X, Y) -> X * Y end, 78 | [2, 4, 6] = ktn_lists:map(Multiply, [2], [1, 2, 3]), 79 | 80 | SumMultiply = fun(X, Y, Z) -> (X + Y) * Z end, 81 | [30, 40, 50] = ktn_lists:map(SumMultiply, [2, 10], [1, 2, 3]), 82 | ok. 83 | 84 | -spec filter(config()) -> ok. 85 | filter(_Config) -> 86 | Sum = fun(X, Y) -> X + Y < 5 end, 87 | [1, 2] = ktn_lists:filter(Sum, [2], [1, 2, 3]), 88 | 89 | Multiply = fun(X, Y) -> X * Y == 6 end, 90 | [3] = ktn_lists:filter(Multiply, [2], [1, 2, 3]), 91 | 92 | SumMultiply = fun(X, Y, Z) -> (X + Y) * Z =/= 30 end, 93 | [2, 3] = ktn_lists:filter(SumMultiply, [2, 10], [1, 2, 3]), 94 | ok. 95 | -------------------------------------------------------------------------------- /test/ktn_task_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(ktn_task_SUITE). 2 | 3 | -export([ 4 | all/0, 5 | init_per_testcase/2, 6 | end_per_testcase/2 7 | ]). 8 | 9 | -export([ 10 | wait_for/1, 11 | wait_for_error/1 12 | ]). 13 | 14 | -define(EXCLUDED_FUNS, 15 | [ 16 | module_info, 17 | all, 18 | init_per_case, 19 | end_per_case 20 | ]). 21 | 22 | -type config() :: [{atom(), term()}]. 23 | -type task(T) :: fun(() -> T). 24 | 25 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 26 | %% Common test 27 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 28 | 29 | -spec all() -> [atom(), ...]. 30 | all() -> 31 | Exports = ?MODULE:module_info(exports), 32 | [F || {F, 1} <- Exports, not lists:member(F, ?EXCLUDED_FUNS)]. 33 | 34 | -spec init_per_testcase(term(), config()) -> config(). 35 | init_per_testcase(_Case, Config) -> 36 | _Tid = ets:new(?MODULE, [named_table, public]), 37 | Config. 38 | 39 | -spec end_per_testcase(term(), config()) -> config(). 40 | end_per_testcase(_Case, Config) -> 41 | ets:delete(?MODULE), 42 | Config. 43 | 44 | % Took this idea from 45 | % https://gist.github.com/garazdawi/17cdb5914b950f0acae21d9fcf7e8d41 46 | -spec cnt_incr(reference()) -> integer(). 47 | cnt_incr(Ref) -> 48 | ets:update_counter(?MODULE, Ref, {2, 1}). 49 | 50 | -spec cnt_read(reference()) -> integer(). 51 | cnt_read(Ref) -> 52 | ets:lookup_element(?MODULE, Ref, 2). 53 | 54 | -spec fail_until(integer()) -> task(_). 55 | fail_until(X) -> 56 | Ref = make_ref(), 57 | ets:insert(?MODULE, {Ref, 0}), 58 | fun () -> 59 | fail_until(X, cnt_read(Ref), Ref) 60 | end. 61 | 62 | -spec fail_until(integer(), integer(), reference()) -> no_return() | ok. 63 | fail_until(X, Curr, Ref) when X > Curr -> 64 | cnt_incr(Ref), 65 | throw({fail, {X, Curr, Ref}}); 66 | fail_until(_X, _Curr, _Ref) -> 67 | ok. 68 | 69 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 70 | %% Test Cases 71 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 72 | 73 | -spec wait_for(config()) -> ok. 74 | wait_for(_Config) -> 75 | ok = ktn_task:wait_for(fun() -> ok end, ok, 1, 1), 76 | ok = ktn_task:wait_for(fail_until(10), ok, 1, 11). 77 | 78 | -spec wait_for_error(config()) -> ok. 79 | wait_for_error(_Config) -> 80 | {error, {timeout, {fail}}} = 81 | ktn_task:wait_for(fun() -> 82 | case rand:uniform(99999999) of 83 | 1 -> 84 | ok; 85 | _ -> 86 | throw({fail}) 87 | end 88 | end, 89 | ok, 90 | 1, 91 | 3), 92 | {error, {timeout, {fail, {15, 10, _Ref}}}} = 93 | ktn_task:wait_for(fail_until(15), ok, 1, 11), 94 | ok. 95 | -------------------------------------------------------------------------------- /src/ktn_test_utils.erl: -------------------------------------------------------------------------------- 1 | -module(ktn_test_utils). 2 | 3 | -export( 4 | [ assert_response/4 5 | , test_response/4 6 | ]). 7 | 8 | -type test_subject() :: status | headers | body. 9 | -type match_type() :: exact | partial. 10 | -type response() :: #{status => term(), headers => term(), body => term()}. 11 | 12 | -spec assert_response(test_subject(), match_type(), term(), response()) -> ok. 13 | assert_response(Test, MatchType, Params, Response) -> 14 | ok = test_response(Test, MatchType, Params, Response). 15 | 16 | -spec test_response(test_subject(), match_type(), term(), term()) -> 17 | ok | {error, term()}. 18 | test_response(status, partial, [C, $?, $?], Response) -> 19 | Pattern = [C] ++ "[0-9][0-9]", 20 | test_response(status, partial, Pattern, Response); 21 | test_response(status, partial, [C1, C2, $?], Response) -> 22 | Pattern = [C1, C2] ++ "[0-9]", 23 | test_response(status, partial, Pattern, Response); 24 | test_response(status, partial, Pattern, Response) -> 25 | #{status := Status} = Response, 26 | ResStatus = ktn_strings:to_string(Status), 27 | case re:compile(Pattern) of 28 | {ok, MP} -> 29 | case re:run(ResStatus, MP, [global]) of 30 | match -> ok; 31 | {match, [[{0, 3}]]} -> ok; 32 | nomatch -> {error, {nomatch, Pattern, ResStatus}} 33 | end; 34 | {error, Error} -> 35 | {error, {regex_compile_fail, Error}} 36 | end; 37 | test_response(status, exact, Status, Response) -> 38 | #{status := ResStatus} = Response, 39 | StatusStr = ktn_strings:to_string(Status), 40 | ResStatusStr = ktn_strings:to_string(ResStatus), 41 | case ResStatusStr of 42 | StatusStr -> ok; 43 | _Other -> {error, {nomatch, StatusStr, ResStatusStr}} 44 | end; 45 | test_response(headers, partial, Headers, Response) -> 46 | #{headers := ResHeaders} = Response, 47 | HeadersNorm = 48 | [{string:to_lower(X), string:to_lower(Y)} || {X, Y} <- Headers], 49 | ResHeadersNorm = 50 | [{string:to_lower(X), string:to_lower(Y)} || {X, Y} <- ResHeaders], 51 | case HeadersNorm -- ResHeadersNorm of 52 | [] -> ok; 53 | MissingHeaders -> {error, {missing_headers, MissingHeaders, ResHeadersNorm}} 54 | end; 55 | test_response(headers, exact, Headers, Response) -> 56 | #{headers := ResHeaders} = Response, 57 | HeadersNorm = 58 | [{string:to_lower(X), string:to_lower(Y)} || {X, Y} <- Headers], 59 | ResHeadersNorm = 60 | [{string:to_lower(X), string:to_lower(Y)} || {X, Y} <- ResHeaders], 61 | case {HeadersNorm -- ResHeadersNorm, ResHeadersNorm -- HeadersNorm} of 62 | {[], []} -> ok; 63 | _ -> {error, {nomatch, HeadersNorm, ResHeadersNorm}} 64 | end; 65 | test_response(body, partial, Pattern, Response) -> 66 | #{body := ResBody} = Response, 67 | ResBodyStr = ktn_strings:to_string(ResBody), 68 | case re:compile(Pattern) of 69 | {ok, MP} -> 70 | case re:run(ResBodyStr, MP) of 71 | {match, _} -> ok; 72 | nomatch -> {error, {nomatch, Pattern, ResBodyStr}} 73 | end; 74 | {error, Error} -> 75 | {error, {regex_compile_fail, Error}} 76 | end; 77 | test_response(body, exact, Text, Response) -> 78 | #{body := ResBody} = Response, 79 | ResBodyStr = ktn_strings:to_string(ResBody), 80 | Body = ktn_strings:to_string(Text), 81 | case ResBodyStr of 82 | Body -> ok; 83 | _ -> {error, {nomatch, ResBodyStr, Body}} 84 | end. 85 | -------------------------------------------------------------------------------- /test/ktn_os_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(ktn_os_SUITE). 2 | 3 | -export([all/0]). 4 | 5 | -export([command/1]). 6 | 7 | -define(EXCLUDED_FUNS, 8 | [ 9 | module_info, 10 | all, 11 | test, 12 | init_per_suite, 13 | end_per_suite 14 | ]). 15 | 16 | -type config() :: [{atom(), term()}]. 17 | 18 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 19 | %% Common test 20 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 21 | 22 | -spec all() -> [atom()]. 23 | all() -> 24 | Exports = ?MODULE:module_info(exports), 25 | [F || {F, _} <- Exports, not lists:member(F, ?EXCLUDED_FUNS)]. 26 | 27 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 28 | %% Test Cases 29 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 30 | 31 | -spec command(config()) -> ok. 32 | command(_Config) -> 33 | Opts = #{log_fun => fun(_) -> ok end}, 34 | 35 | {0, "/\n"} = ktn_os:command("cd /; pwd", Opts), 36 | 37 | {ok, Cwd} = file:get_cwd(), 38 | Result = Cwd ++ "\n", 39 | {0, Result} = ktn_os:command("pwd", Opts), 40 | 41 | case ktn_os:command("pwd; ls w4th3v3r", Opts) of 42 | {0, _} -> ct:fail({error}); 43 | _ -> ok 44 | end, 45 | 46 | Result2 = Result ++ "Hi\n", 47 | {0, Result2} = ktn_os:command("pwd; echo Hi", #{}), 48 | 49 | {0, "/\n"} = ktn_os:command("cd /; pwd"), 50 | 51 | ok = try ktn_os:command("sleep 5", #{timeout => 1000}) 52 | catch _:timeout -> ok end, 53 | 54 | FilterFun = 55 | fun(Line) -> 56 | case re:run(Line, "=INFO REPORT==== .* ===") of 57 | nomatch -> false; 58 | {match, _}-> true 59 | end 60 | end, 61 | 62 | ct:capture_start(), 63 | {0, "/\n"} = ktn_os:command("cd /; pwd"), 64 | ct:capture_stop(), 65 | Lines = ct:capture_get([]), 66 | ListFun = fun(Line) -> FilterFun(Line) end, 67 | [_ | _] = lists:filter(ListFun, Lines), 68 | 69 | ct:comment("Check result when process is killed"), 70 | Self = self(), 71 | YesFun = fun() -> 72 | case ktn_os:command("yes > /dev/null") of 73 | {ExitStatus, _} when ExitStatus =/= 0 -> Self ! ok; 74 | UnexpectedResult -> Self ! {error, UnexpectedResult} 75 | end 76 | end, 77 | erlang:spawn_link(YesFun), 78 | [] = os:cmd("pkill yes"), 79 | ok = receive X -> X after 2500 -> timeout end, 80 | 81 | ct:comment("Check result when port is closed"), 82 | Yes2Fun = 83 | fun() -> 84 | process_flag(trap_exit, true), 85 | try ktn_os:command("yes > /dev/null") of 86 | UnexpectedResult -> Self ! {error, UnexpectedResult} 87 | catch 88 | exit:{error, _, _} -> Self ! ok 89 | end 90 | end, 91 | FindPort = 92 | fun(Proc) -> 93 | fun() -> 94 | {links, Links} = erlang:process_info(Proc, links), 95 | [_] = [P || P <- Links, is_port(P)] 96 | end 97 | end, 98 | Pid = erlang:spawn(Yes2Fun), 99 | try 100 | [Port] = ktn_task:wait_for_success(FindPort(Pid)), 101 | port_close(Port), 102 | ok = receive X2 -> X2 after 1000 -> timeout end 103 | after 104 | [] = os:cmd("pkill yes"), 105 | exit(Pid, kill) 106 | end, 107 | 108 | ct:comment("Check result when port is killed"), 109 | Pid2 = erlang:spawn(Yes2Fun), 110 | try 111 | [Port2] = ktn_task:wait_for_success(FindPort(Pid2)), 112 | exit(Port2, kill), 113 | ok = receive X3 -> X3 after 1000 -> timeout end 114 | after 115 | [] = os:cmd("pkill yes"), 116 | exit(Pid2, kill) 117 | end. 118 | -------------------------------------------------------------------------------- /test/ktn_maps_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(ktn_maps_SUITE). 2 | 3 | -export([ 4 | all/0, 5 | init_per_suite/1, 6 | end_per_suite/1 7 | ]). 8 | 9 | -export([ 10 | find_nested_values/1, 11 | find_shallow_values/1, 12 | dont_find_nested_values/1, 13 | dont_find_shallow_values/1, 14 | provide_default/1 15 | ]). 16 | 17 | -define(EXCLUDED_FUNS, 18 | [ 19 | module_info, 20 | all, 21 | test, 22 | init_per_suite, 23 | end_per_suite 24 | ]). 25 | 26 | -type config() :: [{atom(), term()}]. 27 | 28 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 29 | %% Common test 30 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 31 | 32 | -spec all() -> [atom()]. 33 | all() -> 34 | Exports = ?MODULE:module_info(exports), 35 | [F || {F, _} <- Exports, not lists:member(F, ?EXCLUDED_FUNS)]. 36 | 37 | -spec init_per_suite(config()) -> config(). 38 | init_per_suite(Config) -> 39 | Map = #{user => "john.doe", 40 | name => "John", 41 | last_name => "Doe", 42 | location => #{latitude => 1.5, 43 | longitude => 2.5}, 44 | conversation => #{destination => #{ip => "127.0.0.1", 45 | port => 8080}} 46 | }, 47 | [{map, Map} | Config]. 48 | 49 | -spec end_per_suite(config()) -> config(). 50 | end_per_suite(Config) -> 51 | Config. 52 | 53 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 54 | %% Test Cases 55 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 56 | 57 | -spec find_nested_values(config()) -> ok. 58 | find_nested_values(Config) -> 59 | Map = proplists:get_value(map, Config), 60 | 8080 = ktn_maps:get([conversation, destination, port], Map), 61 | 1.5 = ktn_maps:get([location, latitude], Map), 62 | ok. 63 | 64 | -spec find_shallow_values(config()) -> ok. 65 | find_shallow_values(Config) -> 66 | Map = proplists:get_value(map, Config), 67 | "john.doe" = ktn_maps:get(user, Map), 68 | "John" = ktn_maps:get(name, Map), 69 | "Doe" = ktn_maps:get(last_name, Map), 70 | "john.doe" = ktn_maps:get([user], Map), 71 | ok. 72 | 73 | -spec dont_find_nested_values(config()) -> ok. 74 | dont_find_nested_values(Config) -> 75 | Map = proplists:get_value(map, Config), 76 | ok = try 77 | ktn_maps:get([address, country, city], Map) 78 | catch 79 | error:bad_path -> ok 80 | end, 81 | ok = try 82 | ktn_maps:get([social, facebook], Map) 83 | catch 84 | error:bad_path -> ok 85 | end, 86 | ok. 87 | 88 | -spec dont_find_shallow_values(config()) -> ok. 89 | dont_find_shallow_values(Config) -> 90 | Map = proplists:get_value(map, Config), 91 | ok = try 92 | ktn_maps:get(username, Map) 93 | catch 94 | error:bad_path -> ok 95 | end, 96 | ok = try 97 | ktn_maps:get(email, Map) 98 | catch 99 | error:bad_path -> ok 100 | end, 101 | ok = try 102 | ktn_maps:get([email], Map) 103 | catch 104 | error:bad_path -> ok 105 | end, 106 | ok. 107 | 108 | -spec provide_default(config()) -> ok. 109 | provide_default(Config) -> 110 | Map = proplists:get_value(map, Config), 111 | 112 | 8080 = ktn_maps:get([conversation, destination, port], Map, default), 113 | 1.5 = ktn_maps:get([location, latitude], Map, default), 114 | 115 | "john.doe" = ktn_maps:get(user, Map, default), 116 | "John" = ktn_maps:get(name, Map, default), 117 | "Doe" = ktn_maps:get(last_name, Map, default), 118 | "john.doe" = ktn_maps:get([user], Map, default), 119 | 120 | default = ktn_maps:get([address, country, city], Map, default), 121 | default = ktn_maps:get([social, facebook], Map, default), 122 | 123 | default = ktn_maps:get(username, Map, default), 124 | default = ktn_maps:get(email, Map, default), 125 | default = ktn_maps:get([email], Map, default), 126 | ok. 127 | -------------------------------------------------------------------------------- /test/ktn_recipe_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(ktn_recipe_SUITE). 2 | 3 | %% Common test 4 | -export( 5 | [ all/0 6 | ]). 7 | %% Test cases 8 | -export( 9 | [ implicit_test/1 10 | , explicit_test/1 11 | , loop_test/1 12 | ]). 13 | %% Test case explicit_test/1 14 | -export( 15 | [ t1_s1/1 16 | , t1_s2/1 17 | , t1_s3/1 18 | , t1_s4/1 19 | ]). 20 | %% Test case implicit_test/1 21 | -export( 22 | [ t2_s1/1 23 | , t2_s2/1 24 | , t2_s3/1 25 | , t2_s4/1 26 | ]). 27 | %% Auxiliary exports 28 | -export( 29 | [ process_error/1 30 | , process_result/1 31 | ]). 32 | 33 | all() -> 34 | [ implicit_test 35 | , explicit_test 36 | , loop_test 37 | ]. 38 | 39 | %%% ---------------------------------------------------------------------------- 40 | %%% CASE: module_test 41 | %%% 42 | %%% s3 error 43 | %%% / \ / 44 | %%% s1 -> s2 -> s5 -> s6 -> s7 -> s8 -> s9 -> s0 45 | %%% \ / \ 46 | %%% s4------ halt 47 | %%% ---------------------------------------------------------------------------- 48 | 49 | implicit_test(_Config) -> 50 | ct:comment("Verifying transitions..."), 51 | 52 | ok = ktn_recipe:verify(ktn_recipe_example), 53 | 54 | ct:comment("Running recipe"), 55 | Result = 56 | {ok, [s0_ok, s9_ok, s8_ok, s7_ok, s6_ok, s5_ok, s3_ok, s2_ok, s1_ok]}, 57 | Result = ktn_recipe:run(ktn_recipe_example, []), 58 | 59 | {comment, ""}. 60 | 61 | %%% ---------------------------------------------------------------------------- 62 | %%% CASE: explicit_test 63 | %%% 64 | %%% s1 -> s2 -> s3 -> s4 65 | %%% | 66 | %%% +-> error 67 | %%% +-> halt 68 | %%% ---------------------------------------------------------------------------- 69 | 70 | t1_s1(S) -> io:format("s1~n"), {ok, [t1_s1_ok | S]}. 71 | t1_s2(S) -> io:format("s2~n"), {ok, [t1_s2_ok | S]}. 72 | t1_s3(S) -> io:format("s3~n"), {ok, [t1_s3_ok | S]}. 73 | t1_s4(S) -> io:format("s4~n"), {ok, [t1_s4_ok | S]}. 74 | 75 | explicit_test(_Config) -> 76 | Transitions = 77 | [ fun ?MODULE:t1_s1/1 78 | , fun ?MODULE:t1_s2/1 79 | , {fun ?MODULE:t1_s3/1, i1, halt} 80 | , {fun ?MODULE:t1_s3/1, i2, error} 81 | , {fun ?MODULE:t1_s3/1, ok, fun ?MODULE:t1_s4/1} 82 | , fun ?MODULE:t1_s4/1 83 | ], 84 | ResultFun = fun ?MODULE:process_result/1, 85 | ErrorFun = fun ?MODULE:process_error/1, 86 | InitialState = [], 87 | Result = {ok, [t1_s4_ok, t1_s3_ok, t1_s2_ok, t1_s1_ok]}, 88 | 89 | ct:comment("Verifying transitions..."), 90 | ok = ktn_recipe:verify(Transitions), 91 | 92 | ct:comment("Running recipe"), 93 | Result = ktn_recipe:run(Transitions, ResultFun, ErrorFun, InitialState), 94 | {comment, ""}. 95 | 96 | %%% ---------------------------------------------------------------------------- 97 | %%% CASE: loop_test 98 | %%% ____ 99 | %%% / \ 100 | %%% v | 101 | %%% s1 -> s2 -> s3 -> s4 102 | %%% ---------------------------------------------------------------------------- 103 | 104 | t2_s1(S) -> {ok, S#{s1 => ok}}. 105 | t2_s2(S = #{i := I}) -> {ok, S#{s2 => ok, i := I + 1}}. 106 | t2_s3(S = #{i := I}) when I < 4 -> {inc, S}; 107 | t2_s3(S) -> {ok, S#{s3 => ok}}. 108 | t2_s4(S) -> {ok, S#{s4 => ok}}. 109 | 110 | loop_test(_Config) -> 111 | Transitions = 112 | [ fun ?MODULE:t2_s1/1 113 | , fun ?MODULE:t2_s2/1 114 | , {fun ?MODULE:t2_s3/1, inc, fun ?MODULE:t2_s2/1} 115 | , {fun ?MODULE:t2_s3/1, ok, fun ?MODULE:t2_s4/1} 116 | , fun ?MODULE:t2_s4/1 117 | ], 118 | ResultFun = fun ?MODULE:process_result/1, 119 | ErrorFun = fun ?MODULE:process_error/1, 120 | InitialState = #{i => 0}, 121 | Result = {ok, #{i => 4, s1 => ok, s2 => ok, s3 => ok, s4 => ok}}, 122 | 123 | ct:comment("Verifying transitions..."), 124 | ok = ktn_recipe:verify(Transitions), 125 | 126 | ct:comment("Running recipe"), 127 | Result = ktn_recipe:run(Transitions, ResultFun, ErrorFun, InitialState), 128 | {comment, ""}. 129 | 130 | %%% ---------------------------------------------------------------------------- 131 | %%% Auxiliary functions 132 | %%% ---------------------------------------------------------------------------- 133 | 134 | process_result(S) -> 135 | {ok, S}. 136 | 137 | process_error(S) -> 138 | {error, S}. 139 | 140 | -------------------------------------------------------------------------------- /src/ktn_recipe_verify.erl: -------------------------------------------------------------------------------- 1 | -module(ktn_recipe_verify). 2 | 3 | -behaviour(ktn_recipe). 4 | 5 | %% Behaviour exports 6 | -export( 7 | [ transitions/0 8 | , process_result/1 9 | , process_error/1 10 | ]). 11 | %% Steps 12 | -export( 13 | [ verify_exports/1 14 | , verify_normalizability/1 15 | , verify_transitions/1 16 | , verify_transition_exports/1 17 | ]). 18 | 19 | -type error() :: term(). 20 | 21 | -type state() :: #{ recipe_type => implicit | explicit 22 | , recipe => module() | ktn_recipe:transitions() 23 | , transitions => ktn_recipe:normalized_transitions() 24 | , error => error() 25 | }. 26 | 27 | -spec transitions() -> [ verify_exports 28 | | verify_normalizability 29 | | verify_transitions 30 | | verify_transition_exports 31 | ]. 32 | transitions() -> 33 | [ verify_exports 34 | , verify_normalizability 35 | , verify_transitions 36 | , verify_transition_exports 37 | ]. 38 | 39 | -spec verify_exports(state()) -> {ok, state()} | {error, state()}. 40 | verify_exports(State = #{recipe_type := implicit, recipe := Mod}) -> 41 | % Ensure that the module exported transitions/0, process_result/1 and 42 | % process_error/1. 43 | Exports = proplists:get_value(exports, Mod:module_info()), 44 | TransitionsExported = lists:member({transitions, 0}, Exports), 45 | ResultFunExported = lists:member({process_result, 1}, Exports), 46 | ErrorFunExported = lists:member({process_error, 1}, Exports), 47 | case {TransitionsExported, ResultFunExported, ErrorFunExported} of 48 | {false, _, _} -> 49 | {error, State#{ error => {not_exported, transitions}} }; 50 | {_, false, _} -> 51 | {error, State#{ error => {not_exported, process_result}} }; 52 | {_, _, false} -> 53 | {error, State#{ error => {not_exported, process_error}} }; 54 | {true, true, true} -> 55 | {ok, State} 56 | end; 57 | verify_exports(State = #{recipe_type := explicit}) -> 58 | % Nothing needs to be done here for explicit recipes. 59 | {ok, State}. 60 | 61 | -spec verify_normalizability(state()) -> {ok, state()} | {error, state()}. 62 | verify_normalizability(State = #{recipe := Recipe}) -> 63 | try 64 | {ok, State#{transitions => ktn_recipe:normalize(Recipe)}} 65 | catch 66 | _:NormalizationError -> 67 | {error, State#{error => NormalizationError}} 68 | end. 69 | 70 | -spec verify_transitions(state()) -> {ok, state()} | {error, state()}. 71 | verify_transitions( 72 | State = #{recipe_type := implicit, transitions := Transitions} 73 | ) -> 74 | F = 75 | fun 76 | (X, A) when 77 | is_atom(X) -> 78 | A; 79 | (F, A) when 80 | is_function(F, 1) -> 81 | A; 82 | ({X, _, Y}, A) when 83 | is_function(X, 1), 84 | is_function(Y, 1) -> 85 | A; 86 | ({F, _, Y}, A) when 87 | is_function(F), 88 | is_atom(Y) -> 89 | A; 90 | ({X, _, F}, A) when 91 | is_function(F, 1), 92 | is_atom(X) -> 93 | A; 94 | ({F1, _, F2}, A) when 95 | is_function(F1, 1), 96 | is_function(F2, 1) -> 97 | A; 98 | (X, A) -> 99 | [X | A] 100 | end, 101 | verify_transitions(F, Transitions, State); 102 | verify_transitions( 103 | State = #{recipe_type := explicit, transitions := Transitions} 104 | ) -> 105 | F = 106 | fun 107 | (F, A) when 108 | is_function(F, 1) -> 109 | A; 110 | ({F, _, Action}, A) when 111 | is_function(F, 1), 112 | (Action == error orelse Action == halt) -> 113 | A; 114 | ({F1, _, F2}, A) when 115 | is_function(F1, 1), 116 | is_function(F2, 1) -> 117 | A; 118 | (X, A) -> 119 | [X | A] 120 | end, 121 | verify_transitions(F, Transitions, State). 122 | 123 | verify_transitions(F, Transitions, State) -> 124 | case lists:foldl(F, [], Transitions) of 125 | [] -> {ok, State}; 126 | InvalidElements -> 127 | { error 128 | , State#{error => {invalid_transition_table_elements, InvalidElements}} 129 | } 130 | end. 131 | 132 | -spec verify_transition_exports(state()) -> {ok, state()} | {error, state()}. 133 | verify_transition_exports(State = #{transitions := Transitions}) -> 134 | % Gather all the step functions mentioned in the transition table... 135 | % ...and ensure that they are exported. 136 | F = 137 | fun 138 | ({StepFun, _, Action}, A) when Action == halt; Action == error -> 139 | {module, M} = erlang:fun_info(StepFun, module), 140 | {name, F} = erlang:fun_info(StepFun, name), 141 | Exported = erlang:function_exported(M, F, 1), 142 | case Exported of 143 | true -> A; 144 | false -> [StepFun | A] 145 | end; 146 | ({StepFun1, _, StepFun2}, A) -> 147 | {module, M1} = erlang:fun_info(StepFun1, module), 148 | {module, M2} = erlang:fun_info(StepFun2, module), 149 | {name, F1} = erlang:fun_info(StepFun1, name), 150 | {name, F2} = erlang:fun_info(StepFun2, name), 151 | Exported1 = erlang:function_exported(M1, F1, 1), 152 | Exported2 = erlang:function_exported(M2, F2, 1), 153 | case {Exported1, Exported2} of 154 | {true, true} -> A; 155 | {true, false} -> [StepFun1 | A]; 156 | {false, true} -> [StepFun2 | A]; 157 | {false, false} -> [StepFun1, StepFun2 | A] 158 | end 159 | end, 160 | case lists:foldl(F, [], Transitions) of 161 | [] -> {ok, State}; 162 | NotExported -> {error, State#{error => {not_exported, NotExported}}} 163 | end. 164 | 165 | -spec process_result(state()) -> ok. 166 | process_result(_State) -> 167 | ok. 168 | 169 | -spec process_error(state()) -> {error, error()}. 170 | process_error(#{error := Error}) -> 171 | {error, Error}. 172 | -------------------------------------------------------------------------------- /src/ktn_recipe.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% ktn_recipe: a tool to structure code that consists of sequential steps 3 | %%% in which decisions are made. See README.md for documentation. 4 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 5 | 6 | -module(ktn_recipe). 7 | -author('igarai@gmail.com'). 8 | 9 | -export( 10 | [ run/2 11 | , run/4 12 | , pretty_print/1 13 | , verify/1 14 | , normalize/1 15 | ]). 16 | 17 | -type output() :: ok | error | halt | term(). 18 | -type invalid_result() :: term(). 19 | -type step_fun() :: fun ((term()) -> {output(), term()} | invalid_result()). 20 | -type transition() :: {step_fun(), output(), output() | step_fun()}. 21 | -type step() :: atom() | transition() | step_fun(). 22 | -type transitions() :: [step()]. 23 | -type normalized_transitions() :: [transition()]. 24 | 25 | -export_type([transitions/0]). 26 | -export_type([normalized_transitions/0]). 27 | 28 | -callback transitions() -> transitions(). 29 | -callback process_result(term()) -> term(). 30 | -callback process_error(term()) -> term(). 31 | 32 | -spec run(atom(), term()) -> term(). 33 | run(Mod, InitialState) when is_atom(Mod) -> 34 | NormalizedTransitions = normalize(Mod), 35 | InitialFun = initial_fun(NormalizedTransitions), 36 | ResultFun = fun Mod:process_result/1, 37 | ErrorFun = fun Mod:process_error/1, 38 | run(NormalizedTransitions, InitialFun, ResultFun, ErrorFun, InitialState). 39 | 40 | -spec run(transitions(), step_fun(), step_fun(), term()) -> term(). 41 | run(Transitions, ResultFun, ErrorFun, InitialState) -> 42 | NormalizedTransitions = normalize(Transitions), 43 | InitialFun = initial_fun(NormalizedTransitions), 44 | run(NormalizedTransitions, InitialFun, ResultFun, ErrorFun, InitialState). 45 | 46 | -spec run(normalized_transitions(), output(), step_fun(), step_fun(), term()) -> 47 | term(). 48 | run(_Transitions, error, _ResultFun, ErrorFun, State) -> 49 | ErrorFun(State); 50 | run(_Transitions, halt, ResultFun, _ErrorFun, State) -> 51 | ResultFun(State); 52 | run(Transitions, StepFun, ResultFun, ErrorFun, State) -> 53 | case StepFun(State) of 54 | {halt, NewState} -> 55 | ResultFun(NewState); 56 | {error, NewState} -> 57 | ErrorFun(NewState); 58 | {Output, NewState} -> 59 | NextStep = next_step(StepFun, Output, Transitions), 60 | run(Transitions, NextStep, ResultFun, ErrorFun, NewState); 61 | BadReturnValue -> 62 | Throw = 63 | [ {value, BadReturnValue} 64 | , {step, StepFun} 65 | , {transitions, Transitions} 66 | , {state, State} 67 | ], 68 | throw({bad_step_return_value, Throw}) 69 | end. 70 | 71 | -spec initial_fun(normalized_transitions()) -> step_fun(). 72 | initial_fun([{InitialFun, _, _} | _]) -> InitialFun. 73 | 74 | -spec normalize(module() | transitions()) -> normalized_transitions(). 75 | normalize(Mod) when is_atom(Mod) -> 76 | Transitions = Mod:transitions(), 77 | [ case Transition of 78 | Step when is_atom(Step) -> 79 | {fun Mod:Step/1, ok, next(Mod, Step, Transitions)}; 80 | StepFun when is_function(StepFun, 1) -> 81 | {StepFun, ok, next(Mod, StepFun, Transitions)}; 82 | {StepFun1, Input, StepFun2} -> 83 | {normalize_step(Mod, StepFun1), Input, normalize_step(Mod, StepFun2)}; 84 | Step -> 85 | throw({normalization_error, bad_step, Step}) 86 | end 87 | || Transition <- Transitions 88 | ]; 89 | normalize(Transitions) when is_list(Transitions) -> 90 | [ case Transition of 91 | StepFun when 92 | is_function(StepFun, 1) -> 93 | {StepFun, ok, next(StepFun, Transitions)}; 94 | {StepFun1, Input, StepFun2} when 95 | is_function(StepFun1, 1), 96 | is_function(StepFun2, 1) -> 97 | {StepFun1, Input, StepFun2}; 98 | {StepFun1, Input, Action} when 99 | Action == error; Action == halt -> 100 | {StepFun1, Input, Action}; 101 | Step -> 102 | throw({normalization_error, bad_step, Step}) 103 | end 104 | || Transition <- Transitions 105 | ]. 106 | 107 | -spec normalize_step(atom(), halt | error | step()) -> 108 | halt | error | step_fun(). 109 | normalize_step(_Mod, halt) -> 110 | halt; 111 | normalize_step(_Mod, error) -> 112 | error; 113 | normalize_step(Mod, Step) when is_atom(Mod), is_atom(Step) -> 114 | fun Mod:Step/1; 115 | normalize_step(_Mod, StepFun) when is_function(StepFun, 1) -> 116 | StepFun; 117 | normalize_step(_Mod, Step) -> 118 | throw({normalization_error, bad_step, Step}). 119 | 120 | %% next_step/3 computes the next step from a given step and an input. 121 | -spec next_step(step(), term(), normalized_transitions()) -> error | step(). 122 | next_step(_Step, _Input, []) -> error; 123 | next_step(Step, Input, [{Step, Input, Next} | _]) -> Next; 124 | next_step(Step, Input, [_ | Ts]) -> next_step(Step, Input, Ts). 125 | 126 | %% next/2-3 computes the implied next state in a transition table for states 127 | %% which do not explicitly name their next state. 128 | -spec next(atom(), step(), transitions()) -> error | halt | step_fun(). 129 | next(Mod, Step, Transitions) -> 130 | case next(Step, Transitions) of 131 | error -> error; 132 | halt -> halt; 133 | S when is_atom(S) -> fun Mod:S/1; 134 | S when is_function(S, 1) -> S 135 | end. 136 | 137 | -spec next(step(), normalized_transitions()) -> error | halt | step(). 138 | next(_, []) -> error; 139 | next(X, [X]) -> halt; 140 | next(X, [{Y, _, _} | T]) when X /= Y -> next(X, T); 141 | next(X, [Y | T]) when X /= Y -> next(X, T); 142 | next(X, [{X, _, _} | T]) -> next2(X, T); 143 | next(X, [X | T]) -> next2(X, T). 144 | 145 | next2(_, []) -> error; 146 | next2(X, [{X, _, _}]) -> halt; 147 | next2(X, [X]) -> halt; 148 | next2(X, [{X, _, _} | T]) -> next2(X, T); 149 | next2(X, [X | T]) -> next2(X, T); 150 | next2(X, [{Y, _, _} | _]) when X /= Y -> Y; 151 | next2(X, [Y | _]) when X /= Y -> Y. 152 | 153 | %% Pretty prints a procedure module's transition table. 154 | -spec pretty_print(atom() | transitions()) -> ok. 155 | pretty_print(Mod) when is_atom(Mod) -> 156 | pretty_print(normalize(Mod)); 157 | pretty_print(Transitions) when is_list(Transitions) -> 158 | pretty_print_normalized(normalize(Transitions)). 159 | 160 | -spec pretty_print_normalized(normalized_transitions()) -> ok. 161 | pretty_print_normalized(NormalizedTransitions) -> 162 | PrintFun = 163 | fun 164 | ({SF1, I, Action}) when Action == error; Action == halt -> 165 | {module, M1} = erlang:fun_info(SF1, module), 166 | {name, F1} = erlang:fun_info(SF1, name), 167 | io:format("~p:~p(~p) -> ~p~n", [M1, F1, I, Action]); 168 | ({SF1, I, SF2}) -> 169 | {module, M1} = erlang:fun_info(SF1, module), 170 | {module, M2} = erlang:fun_info(SF2, module), 171 | {name, F1} = erlang:fun_info(SF1, name), 172 | {name, F2} = erlang:fun_info(SF2, name), 173 | io:format("~p:~p(~p) -> ~p:~p~n", [M1, F1, I, M2, F2]) 174 | end, 175 | lists:foreach(PrintFun, NormalizedTransitions). 176 | 177 | %% Verifies that a procedure module's transition table meets certain minimal 178 | %% criteria. Returns ok if the transition table is a list of either atoms 179 | %% or ternary tuples, whose first and third elements are atoms, and that 180 | %% all states in the transition table are exported from the module with the 181 | %% correct arity. 182 | %% It does not verify structural properties of the FSM defined by the transition 183 | %% table (e.g. connected, acyclic, arboreal). 184 | -spec verify(atom() | transitions()) -> term(). 185 | verify(Mod) when is_atom(Mod) -> 186 | InitialState = #{recipe_type => implicit, recipe => Mod}, 187 | run(ktn_recipe_verify, InitialState); 188 | verify(Transitions) when is_list(Transitions) -> 189 | InitialState = #{recipe_type => explicit, recipe => Transitions}, 190 | run(ktn_recipe_verify, InitialState). 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | erlang katana 2 | ====== 3 | 4 | ![build](https://github.com/inaka/erlang-katana/workflows/build/badge.svg) 5 | 6 | ![samurai](https://raw.githubusercontent.com/unbalancedparentheses/katana/master/images/samurai.jpg) 7 | 8 | Even if you love Erlang as we do, from time to time you might ask yourself why 9 | some functions you normally find in other languages are not part of the erlang's 10 | standard library. When you ask yourself that type of question you should 11 | remember that an estimated 2 million people are currently working in COBOL and 12 | 1.5 million new lines of COBOL code are written every day. After feeling bad for 13 | those developers, you should send a pull request to erlang katana with the 14 | functions you use on a daily basis. 15 | 16 | To sum up this is a grab bag of useful functions (ideally). 17 | ![grabbag](https://raw.githubusercontent.com/unbalancedparentheses/erlang-katana/master/images/bagofcat.jpg) 18 | 19 | # Contact Us 20 | 21 | If you find any **bugs** or have a **problem** while using this library, please 22 | [open an issue](https://github.com/inaka/erlang-katana/issues/new) in this repo 23 | (or a pull request :)). 24 | 25 | And you can check all of our open-source projects at 26 | [inaka.github.io](http://inaka.github.io) 27 | 28 | #Objective 29 | - [20 cool Clojure functions](https://daveyarwood.github.io/2014/07/30/20-cool-clojure-functions/) 30 | - [Prismatic's Clojure utility belt](https://github.com/Prismatic/plumbing) 31 | 32 | ## Related Projects 33 | Check out as well [katana-code](https://github.com/inaka/katana-code) and [katana-test](https://github.com/inaka/katana-test) 34 | 35 | ## Included goodies: 36 | 37 | * `ktn_date`: functions useful for handling dates and time values. 38 | * `ktn_debug`: functions useful for debugging. 39 | * `ktn_json`: functions useful for processing & creating JSON. 40 | * `ktn_maps`: functions useful for handling maps. 41 | * `ktn_numbers`: functions useful for processing numeric values. 42 | * `ktn_recipe`: A tool to structure code that consists of sequential steps in which decisions are made. 43 | * `ktn_rpc`: functions useful for RPC mechanisms. 44 | * `ktn_task`: functions useful for managing asyncronous tasks. 45 | * `ktn_user_default`: useful functions for your erlang shell. 46 | 47 | ### `ktn_date` 48 | 49 | This module contains functions to manipulate date and time values. 50 | 51 | #### `shift_days` 52 | 53 | With `shift_days(Datetime :: calendar:datetime(), N :: integer())` you can move the date expressed in `DateTime`, `N` days to the future or to the past. 54 | 55 | #### `shift_months` 56 | 57 | With `shift_months(Date :: calendar:date(), N :: integer())` you can move the date expressed in `Date`, `N` months to the future or to the past. 58 | 59 | Examples: 60 | ```erlang 61 | $> ktn_date:shift_months({2018, 8, 15}, 2). 62 | {2018, 10, 15} 63 | $> ktn_date:shift_months({2018, 8, 15}, -2). 64 | {2018, 6, 15} 65 | $> ktn_date:shift_months({2018, 8, 15}, 6). 66 | {2019, 2, 15} 67 | ``` 68 | 69 | ### `ktn_user_default` 70 | 71 | This module contains functions that are nice to have in your user default 72 | module, and thereby added to your shell. To use them, copy the ones you want 73 | into your `~/user_default.erl` module. 74 | 75 | ### `ktn_test_utils` 76 | 77 | The Katana Test Utilities includes two functions useful for testing REST APIs: 78 | `test_response/4` and `assert_response/4`. 79 | 80 | ##### assert_response(Test, MatchType, Params, Response) 81 | 82 | `assert_response/4` uses `test_response/4` to check that a given assertion 83 | holds. It's usage is identical to `test_response/4`, except that it fails when 84 | the assertion fails. 85 | 86 | ##### test_response(Test, MatchType, Params, Response) 87 | 88 | `test_response/3` provides some useful checks regarding request responses. The 89 | call to test itself does not fail, `test_response` will return ok if the test 90 | passed and {error, Reason} when it does not. 91 | 92 | To have it fail on the same test use `assert_response/4`. `test_response/4` is 93 | intended to be called as: 94 | 95 | ```erlang 96 | ok = test_response(status, Response, "201"), 97 | {error, {nomatch, "204", _}} = test_response(status, partial, Response, "40?"), 98 | ``` 99 | 100 | The first argument is what part of the response must be tested, and may be one 101 | of: `status`, `headers` or `body`. 102 | 103 | The second argument must be a map with at least three keys: 104 | - `status`, the reponse status 105 | - `headers`, the list of headers in the response 106 | - `body`, the response body 107 | 108 | The status test matches the response status agains some regex pattern. It has a 109 | short form for specifying return codes: the third argument must be a string, 110 | either representing the exact return code, a string representing a partial 111 | return code (using the ? character as a wildcard, e.g. "20?", "4??"), or a 112 | regular expression as processed by the re module. You cannot specify patterns 113 | with a ? in the second digit position, e.g. "2?1". 114 | 115 | If the status test fails, it returns {error, {nomatch, Pattern, Status}} where 116 | Pattern is the provided pattern to match agains and Status is the response 117 | status. 118 | 119 | The headers test compares a list of headers agains those present in a response. 120 | The comparison can check whether the set of headers provided matches exactly 121 | (with exact) or if it is a subset of the response headers (with partial). The 122 | headers list of is a list of {Header, Value} tuples. Note that the elements in 123 | both headers lists do not have to be in the same order, nor have the same case. 124 | 125 | If the test should fail, the possible error values returned are: 126 | - {missing_headers, Headers, ResHeaders} where Headers is the list 127 | of headers missing from the set of headers in the response ResHeaders. 128 | - {nomatch, Headers, ResHeaders} if the test type was exact and the two sets 129 | of headers do not contain the same elements. 130 | 131 | The body test's third argument may specify whether the body must match a 132 | provided regex ({partial, Pattern}) or match a given body exactly ({match, 133 | Text}). 134 | 135 | Possible error values for the body assert are: 136 | - {regex_compile_fail, Error}} if the regular expression fails to compile. 137 | - {error, {nomatch, Pattern, Body}} if the body does not match the regex. 138 | - {nomatch, ResBody, Body}} if the body does not match the text. 139 | 140 | 141 | ### `ktn_recipe` 142 | 143 | #### What is a recipe? 144 | 145 | *Recipe* (noun): A set of conditions and parameters of an industrial process 146 | to obtain a given result. 147 | 148 | A recipe is a series of steps to obtain a result. This word was chosen 149 | because 'procedure' is overloaded in computer sciences, and it is not 150 | exactly a finite state machine. 151 | 152 | ktn_recipe arose from the need to restructure code implementing large 153 | application business logic decision trees. 154 | Long functions, deeply nested case expressions, code with several 155 | responsibilities, all make for unreadable and unmaintainable code. 156 | Every time one needs to make a change or add a feature, one must think about 157 | the non-obvious data flow, complex logic, possible side effects. 158 | Exceptions pile upon exceptions, until the code is a house of cards. 159 | If one is lucky (or responsible), one has comprehensive test suites covering 160 | all cases. 161 | 162 | A better way to structure the code would be preferable, one that makes the 163 | flow obvious and separates responsibilities into appropriate code segments. 164 | One way is a finite state machine. OTP even has a behaviour for exactly this 165 | purpose. However, gen_statem does not exactly fit our needs: 166 | To begin with, gen_statems run in their own process, which may not be what is 167 | needed. Second, logic flow is not immediately obvious because the FSM's 168 | state depends on the result of each state function. Finally, a gen_statem 169 | transitions only on receiving input, whereas we are looking for something 170 | that runs like normal code, "on its own". So, our FSM will be defined by 171 | something like a transition table, a single place you can look at and know 172 | how it executes. This specification will "drive" the recipe. 173 | 174 | The most common case envisioned is a sequential series of steps, each of 175 | which makes a decision affecting later outcomes, so our design should be 176 | optimized for a single, linear execution flow, allowing for writing the 177 | minimum amount of code for this particular case. Keep the common case fast. 178 | 179 | #### How do I use it? 180 | 181 | The simplest use case: create a module using the ktn_recipe behaviour. 182 | It must export `transitions/0`, `process_result/1`, `process_error/1` and a 183 | series of unary functions called step functions. 184 | `transitions/0` must return a list of atoms naming the step functions, in the 185 | order in which they must be called. Each step function takes a State 186 | variable as input and if the step was succesful emits `{ok, NewState}` with 187 | the updated state, or `{error, NewState}` if it was unsuccessful. 188 | After running all the steps, process_result will take the resulting state 189 | as input and should emit whatever you need. 190 | If one of the state functions returns `{error, NewState}`, then 191 | `process_error/1` will be called taking the resulting state as input and 192 | should handle all expected error conditions. 193 | 194 | To run the recipe, call `ktn_recipe:run(CALLBACK_MODULE, INITIAL_STATE)`. 195 | 196 | For more advanced uses, continue reading. 197 | 198 | #### How does it work? 199 | 200 | The main functions are `ktn_recipe:run/2-4`. These will run a recipe and 201 | return the result. 202 | 203 | Recipes may be specified in two ways, which we call "implicit" and 204 | "explicit", for lack of better words. In implicit mode, we pass `run/2` a 205 | callback module implementing the ktn_recipe behavior, which implements all 206 | necessary functions. In explicit mode, we explicitly give `run/4` the 207 | transition table, a function to process the state resulting from running all 208 | recipe steps to completion, a function to process erroneous states, and the 209 | initial state of the recipe. 210 | 211 | #### Step functions 212 | 213 | Step functions have the following type: 214 | 215 | ```erlang 216 | -type state() :: term(). 217 | -type output() :: term(). 218 | -spec Step(state()) -> {ok, state()} 219 | | {output(), state()} 220 | | {error, state()} 221 | | {halt, state()}. 222 | ``` 223 | 224 | The recipe state may be any value you need, a list, a proplist, a record, a 225 | map, etc. ktn_recipe does not use maps itself, although the test suite does. 226 | 227 | The initial state passed to `run/2-4` will be passed to the first step as-is, 228 | and the subsequent resulting states will be fed to each step. 229 | 230 | The step functions should, if possible, have no side effects. If so, and the 231 | recipe ends in an error state, it can be aborted without worrying about 232 | rolling back the side effects. 233 | 234 | For example, if the recipe involves making changes in a database, these 235 | should be made in the `process_result/1` function, once it is certain that 236 | all steps completed successfully. 237 | 238 | #### Result and Error processing 239 | 240 | In implicit mode, the module must export `process_error/1` and 241 | `process_result/1`; in explicit mode you may pass whichever function you 242 | desire. 243 | The purpose of the 'result processing function' is to transform the 244 | resulting state into a usable value. 245 | The purpose of the 'error processing function' is to extract the error value 246 | from the state and do something according. If you are unfortunate enough 247 | to have unavoidable side effects in your step functions, you may undo them 248 | here. 249 | 250 | #### Specifying transitions 251 | 252 | Transition tables are lists. 253 | As stated, the simplest transition table is a list of atoms naming the step 254 | functions, but this is not the only way recipe states may be specified. 255 | A transition table is a list of transitions. 256 | What is permissible as a transition depends on whether we are running in 257 | implicit or explicit mode. 258 | 259 | In explicit mode, a transition may be either be an external function, i.e. 260 | `fun module:function/arity`, or a ternary tuple `{F1, I, F2}` in which the _F1_ 261 | and _F2_ elements are explicit functions and the _I_ is the transition input. 262 | This form reprensents a transition from step _F1_ to step _F2_, if _F1_ outputs 263 | `{Input, State}` instead of `{ok, State}`. 264 | 265 | In implicit mode, in addition to the se two forms of specifying step 266 | functions, an atom may be used. The module in which the function resides is 267 | assumed to be the provided callback module, hence it is 'implicit'. 268 | 269 | Also as stated, in any mode, the return value of step functions must be one 270 | of: 271 | - `{ok, State}` 272 | - `{Output, State}` 273 | - `{halt, State}` 274 | - `{error, State}` 275 | 276 | If `{error, State}` is returned, `run/2-4` will call the error processing 277 | function. If {halt, State} is called, `run/2-4` will call the result 278 | processing function. 279 | If `{ok, State}` is returned, run will call the next function in the 280 | transition table. That is, it will search the transition table until it 281 | finds the current's functions position, if necessary skip over entries 282 | corresponding to the current function, and call the first function with a 283 | different name it encounters. 284 | If `{Output, State}` is returned, run will search the transition table for an 285 | entry matching `{Step, Output, NextStep}`, and select NextStep as the function 286 | to call. In this way, non-linear recipes that jump ahead or loop back may 287 | be specified. It is recommended that the transition table describe a DAG, 288 | unless the program logic really requires loops. 289 | 290 | Before running a recipe, `run/2-4` will 'normalize' the transition table to 291 | remove implicit assumptions and so that it is composed only of ternary 292 | tuples explicitly listing every transition. As a debugging aid, this 293 | function `normalize/1` is exported by the `ktn_recipe` module. 294 | 295 | #### Example 296 | 297 | Suppose you have a web server with an endpoint `/conversations`, accepting the 298 | `DELETE` method to delete conversations between users. To delete a 299 | conversation, one must fetch the converation entity from the database, fetch 300 | the contact (the other user in the conversation) specified by the user id 301 | in the conversation's contact_id attribute, check the contact's status (if 302 | it is blocked, etc) and also check whether the client is using version 1 of 303 | our REST API or version 2, since one really deletes the conversation from 304 | the database and the other merely clears messages from the conversation. 305 | 306 | We could implement this as a recipe with four steps: 307 | 308 | ```erlang 309 | transitions() -> 310 | [ get_conversation 311 | , get_contact 312 | , get_status 313 | , check_api_version 314 | ]. 315 | ``` 316 | 317 | get_conversation fetches it from the database. If it has already been 318 | deleted, it could return `{error, NewState}` and store the error in the state. 319 | Likewise for `get_contact`, and `get_status`. 320 | `get_api_version` would extract the api version from the request header or 321 | url, and store the result in the recipe state. If all steps complete 322 | successfully, `process_result` will effectively make the call to delete the 323 | conversation from the database. 324 | 325 | Using ktn_recipes, you could structure your application as: 326 | 327 | ``` 328 | +----------+ +-------------+ +---------+ +-----------+ 329 | | | | | | | | | 330 | | Endpoint +---> Recipes for +---> Entity +---> Databases | 331 | | handlers | | endpoint | | modules | | | 332 | | | | actions | | | | | 333 | | | | | | | | | 334 | +----------+ +-------------+ +---------+ +-----------+ 335 | ``` 336 | 337 | The handlers would have the sole responsibility of handling the request, 338 | i. e. parsing URL, header and body parameters, verfying them and invoking 339 | the correct recipe. 340 | Recipes would implement the business logic. 341 | Entity modules would abstract management of your systems entities, including 342 | issues such as caching, and eventually persisting the changes to the 343 | storage medium or obtaining the required information. 344 | 345 | #### What if something goes wrong? 346 | 347 | The function `ktn_recipe:verify/1` takes either a recipe module or an explicit 348 | transition table and will run several checks to verify that it will run 349 | correctly. You may use this function to test your transition tables. 350 | Note that verify/1 is implemented as a `ktn_recipe`, so you can use it as an 351 | example. 352 | 353 | If a step throws an exception, it will not be caught. That means you may see 354 | some of ktn_recipe's internal functions in the stacktrace. In general, the 355 | most common errors will be: 356 | 1) badly formed transition tables 357 | 2) not exporting step functions 358 | 3) bad return values from step functions 359 | 4) bad state values set by functions 360 | 361 | 1) and 2) should be detected by running verify on your recipe. 3) and 4) 362 | will be detected at run time and an appropriate error will be returned by 363 | `run/2-4`. Of course, your tests should detect these cases. You do have 364 | tests, right? 365 | 366 | Finally, there is one more function, `ktn_recipe:pretty_print/1`, which takes 367 | either a callback module or a transition table, and prints the normalized 368 | transition table. 369 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.0.0](https://github.com/inaka/erlang-katana/tree/1.0.0) (2020-12-03) 4 | 5 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.4.0...1.0.0) 6 | 7 | **Implemented enhancements:** 8 | 9 | - Update Elvis dependency [\#145](https://github.com/inaka/erlang-katana/issues/145) 10 | 11 | **Fixed bugs:** 12 | 13 | - test breaks [\#153](https://github.com/inaka/erlang-katana/issues/153) 14 | 15 | **Closed issues:** 16 | 17 | - Bump Version to 0.5.0 [\#157](https://github.com/inaka/erlang-katana/issues/157) 18 | - Upgrade dependencies [\#156](https://github.com/inaka/erlang-katana/issues/156) 19 | - Update elvis\_core dependency version to 0.3.9 [\#150](https://github.com/inaka/erlang-katana/issues/150) 20 | - Bump Version to 0.4.0 [\#147](https://github.com/inaka/erlang-katana/issues/147) 21 | - Bump version to 0.2.23 [\#131](https://github.com/inaka/erlang-katana/issues/131) 22 | - ktn\_code: nodes for implicit funs \(fun bla/0\) should have a different name than 'fun' [\#130](https://github.com/inaka/erlang-katana/issues/130) 23 | - Use aleppo 0.9.10 when published [\#129](https://github.com/inaka/erlang-katana/issues/129) 24 | - Really fix cyclic dependency… for REALZ [\#91](https://github.com/inaka/erlang-katana/issues/91) 25 | - Generate Abstract Syntax Forms from ktn\_code:tree\_node\(\)s [\#63](https://github.com/inaka/erlang-katana/issues/63) 26 | 27 | **Merged pull requests:** 28 | 29 | - Fix elvis [\#160](https://github.com/inaka/erlang-katana/pull/160) ([paulo-ferraz-oliveira](https://github.com/paulo-ferraz-oliveira)) 30 | - Functionality to shift days and months [\#158](https://github.com/inaka/erlang-katana/pull/158) ([Euen](https://github.com/Euen)) 31 | - Improvements from LambdaClass [\#155](https://github.com/inaka/erlang-katana/pull/155) ([elbrujohalcon](https://github.com/elbrujohalcon)) 32 | - Remove ktn\_fsm now that gen\_fsm is deprecated [\#154](https://github.com/inaka/erlang-katana/pull/154) ([elbrujohalcon](https://github.com/elbrujohalcon)) 33 | - Remove dead hipchat link [\#152](https://github.com/inaka/erlang-katana/pull/152) ([Euen](https://github.com/Euen)) 34 | - \[Fix \#150\] Update elvis\_core dependency version to 0.3.9 [\#151](https://github.com/inaka/erlang-katana/pull/151) ([harenson](https://github.com/harenson)) 35 | - Improve ktn\_os according to the latest changes to os:cmd [\#149](https://github.com/inaka/erlang-katana/pull/149) ([elbrujohalcon](https://github.com/elbrujohalcon)) 36 | 37 | ## [0.4.0](https://github.com/inaka/erlang-katana/tree/0.4.0) (2017-03-17) 38 | 39 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.3.1...0.4.0) 40 | 41 | **Merged pull requests:** 42 | 43 | - \[\#147\] bump version to 0.4.0 [\#148](https://github.com/inaka/erlang-katana/pull/148) ([Euen](https://github.com/Euen)) 44 | - \[\#145\] update Elvis dependency and ktn\_random module [\#146](https://github.com/inaka/erlang-katana/pull/146) ([Euen](https://github.com/Euen)) 45 | 46 | ## [0.3.1](https://github.com/inaka/erlang-katana/tree/0.3.1) (2016-08-05) 47 | 48 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.3.0...0.3.1) 49 | 50 | **Closed issues:** 51 | 52 | - Version Bump to 0.3.1 [\#143](https://github.com/inaka/erlang-katana/issues/143) 53 | - Remove unnecessary parce transform from rebar.config [\#141](https://github.com/inaka/erlang-katana/issues/141) 54 | - Add Meta Testing [\#133](https://github.com/inaka/erlang-katana/issues/133) 55 | 56 | **Merged pull requests:** 57 | 58 | - \[Close \#143\] version bump [\#144](https://github.com/inaka/erlang-katana/pull/144) ([Euen](https://github.com/Euen)) 59 | - \[Close \#141\] remove parse transform [\#142](https://github.com/inaka/erlang-katana/pull/142) ([Euen](https://github.com/Euen)) 60 | 61 | ## [0.3.0](https://github.com/inaka/erlang-katana/tree/0.3.0) (2016-08-04) 62 | 63 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.23...0.3.0) 64 | 65 | **Closed issues:** 66 | 67 | - Version Bump to 0.3.0 [\#139](https://github.com/inaka/erlang-katana/issues/139) 68 | - Move from erlang.mk to rebar3 [\#137](https://github.com/inaka/erlang-katana/issues/137) 69 | 70 | **Merged pull requests:** 71 | 72 | - \[Close \#139\] version bump to 0.3.0 [\#140](https://github.com/inaka/erlang-katana/pull/140) ([Euen](https://github.com/Euen)) 73 | - \[Colse \#173\] Move from erlang.mk to rebar3 [\#138](https://github.com/inaka/erlang-katana/pull/138) ([Euen](https://github.com/Euen)) 74 | - Added encoding and decoding for bas 16 binaries [\#136](https://github.com/inaka/erlang-katana/pull/136) ([HernanRivasAcosta](https://github.com/HernanRivasAcosta)) 75 | - Remove ktn\_meta\_SUITE from readme [\#135](https://github.com/inaka/erlang-katana/pull/135) ([harenson](https://github.com/harenson)) 76 | - Add sibling projects [\#134](https://github.com/inaka/erlang-katana/pull/134) ([elbrujohalcon](https://github.com/elbrujohalcon)) 77 | 78 | ## [0.2.23](https://github.com/inaka/erlang-katana/tree/0.2.23) (2016-02-24) 79 | 80 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.22...0.2.23) 81 | 82 | **Closed issues:** 83 | 84 | - Version bump 0.2.22 [\#127](https://github.com/inaka/erlang-katana/issues/127) 85 | - Break the Cycle! [\#121](https://github.com/inaka/erlang-katana/issues/121) 86 | 87 | **Merged pull requests:** 88 | 89 | - \[Closes \#121\] Moved ktn\_code and ktn\_meta\_SUITE to their own repos [\#132](https://github.com/inaka/erlang-katana/pull/132) ([igaray](https://github.com/igaray)) 90 | 91 | ## [0.2.22](https://github.com/inaka/erlang-katana/tree/0.2.22) (2016-01-22) 92 | 93 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.21...0.2.22) 94 | 95 | **Closed issues:** 96 | 97 | - Update aleppo to 0.9.9 which fixes an important bug [\#125](https://github.com/inaka/erlang-katana/issues/125) 98 | 99 | **Merged pull requests:** 100 | 101 | - \[Closes \#125\] Bump to version 0.2.22 [\#128](https://github.com/inaka/erlang-katana/pull/128) ([jfacorro](https://github.com/jfacorro)) 102 | - \[Closes \#125\] Update aleppo [\#126](https://github.com/inaka/erlang-katana/pull/126) ([jfacorro](https://github.com/jfacorro)) 103 | 104 | ## [0.2.21](https://github.com/inaka/erlang-katana/tree/0.2.21) (2016-01-21) 105 | 106 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.20...0.2.21) 107 | 108 | **Closed issues:** 109 | 110 | - Bump version to 0.2.21 [\#123](https://github.com/inaka/erlang-katana/issues/123) 111 | - Update ktn\_code for the latest aleppo modifications [\#112](https://github.com/inaka/erlang-katana/issues/112) 112 | 113 | **Merged pull requests:** 114 | 115 | - \[Closes \#123\] Bump version to 0.2.21 [\#124](https://github.com/inaka/erlang-katana/pull/124) ([jfacorro](https://github.com/jfacorro)) 116 | - \[Closes \#112\] Upgrade aleppo [\#122](https://github.com/inaka/erlang-katana/pull/122) ([jfacorro](https://github.com/jfacorro)) 117 | 118 | ## [0.2.20](https://github.com/inaka/erlang-katana/tree/0.2.20) (2016-01-19) 119 | 120 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.19...0.2.20) 121 | 122 | **Implemented enhancements:** 123 | 124 | - Update to last elvis\_core version [\#118](https://github.com/inaka/erlang-katana/issues/118) 125 | 126 | **Fixed bugs:** 127 | 128 | - Modify IGNORE\_DEPS in Makefile [\#113](https://github.com/inaka/erlang-katana/issues/113) 129 | 130 | **Closed issues:** 131 | 132 | - Version Bump to 0.2.20 [\#114](https://github.com/inaka/erlang-katana/issues/114) 133 | - Version Bump to 0.2.18 [\#109](https://github.com/inaka/erlang-katana/issues/109) 134 | 135 | **Merged pull requests:** 136 | 137 | - \[Fix \#118\] update elvis\_core dep [\#119](https://github.com/inaka/erlang-katana/pull/119) ([Euen](https://github.com/Euen)) 138 | - update gitignore [\#117](https://github.com/inaka/erlang-katana/pull/117) ([Euen](https://github.com/Euen)) 139 | - \[Fix \#114\] version bump [\#116](https://github.com/inaka/erlang-katana/pull/116) ([Euen](https://github.com/Euen)) 140 | - \[Fix \#113\] modify IGNORE\_DEPS in makefile [\#115](https://github.com/inaka/erlang-katana/pull/115) ([Euen](https://github.com/Euen)) 141 | 142 | ## [0.2.19](https://github.com/inaka/erlang-katana/tree/0.2.19) (2016-01-12) 143 | 144 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.18...0.2.19) 145 | 146 | **Merged pull requests:** 147 | 148 | - Switch build tools to erlang.mk and republish to hex.pm [\#111](https://github.com/inaka/erlang-katana/pull/111) ([elbrujohalcon](https://github.com/elbrujohalcon)) 149 | 150 | ## [0.2.18](https://github.com/inaka/erlang-katana/tree/0.2.18) (2015-12-31) 151 | 152 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.17...0.2.18) 153 | 154 | ## [0.2.17](https://github.com/inaka/erlang-katana/tree/0.2.17) (2015-12-31) 155 | 156 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.15...0.2.17) 157 | 158 | **Closed issues:** 159 | 160 | - Version Bump to 0.2.17 [\#106](https://github.com/inaka/erlang-katana/issues/106) 161 | - Hex Packages [\#97](https://github.com/inaka/erlang-katana/issues/97) 162 | 163 | **Merged pull requests:** 164 | 165 | - \[\#109\] Version bump to 0.2.18 [\#110](https://github.com/inaka/erlang-katana/pull/110) ([davecaos](https://github.com/davecaos)) 166 | - \[\#106\] Update Contributors [\#108](https://github.com/inaka/erlang-katana/pull/108) ([davecaos](https://github.com/davecaos)) 167 | - \[\#106.version.bump.0.2.16 [\#107](https://github.com/inaka/erlang-katana/pull/107) ([davecaos](https://github.com/davecaos)) 168 | - \[\#97\] Updated for hexer hex package tool [\#105](https://github.com/inaka/erlang-katana/pull/105) ([davecaos](https://github.com/davecaos)) 169 | 170 | ## [0.2.15](https://github.com/inaka/erlang-katana/tree/0.2.15) (2015-12-23) 171 | 172 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.14...0.2.15) 173 | 174 | **Closed issues:** 175 | 176 | - Bump version to 0.2.15 [\#103](https://github.com/inaka/erlang-katana/issues/103) 177 | - Use elvis\_core instead of elvis as a dep [\#101](https://github.com/inaka/erlang-katana/issues/101) 178 | - Remove shell target from Makefile [\#100](https://github.com/inaka/erlang-katana/issues/100) 179 | - Error in README example for ktn\_meta\_SUITE [\#98](https://github.com/inaka/erlang-katana/issues/98) 180 | 181 | **Merged pull requests:** 182 | 183 | - \[Closes \#103\] Bump version [\#104](https://github.com/inaka/erlang-katana/pull/104) ([jfacorro](https://github.com/jfacorro)) 184 | - \[Closes \#101\] Use elvis\_core [\#102](https://github.com/inaka/erlang-katana/pull/102) ([jfacorro](https://github.com/jfacorro)) 185 | - \[Fixed \#98\] Mixin usage for whole module [\#99](https://github.com/inaka/erlang-katana/pull/99) ([jfacorro](https://github.com/jfacorro)) 186 | - For ktn\_meta\_SUITE, base\_dir should be configurable [\#95](https://github.com/inaka/erlang-katana/pull/95) ([elbrujohalcon](https://github.com/elbrujohalcon)) 187 | 188 | ## [0.2.14](https://github.com/inaka/erlang-katana/tree/0.2.14) (2015-11-12) 189 | 190 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.13...0.2.14) 191 | 192 | **Closed issues:** 193 | 194 | - Version bump to 0.2.14 [\#93](https://github.com/inaka/erlang-katana/issues/93) 195 | - Bump version 0.2.13 [\#84](https://github.com/inaka/erlang-katana/issues/84) 196 | - Create a ktn\_sup module [\#45](https://github.com/inaka/erlang-katana/issues/45) 197 | 198 | **Merged pull requests:** 199 | 200 | - \[Closes \#93\] Version bump to 0.2.14 [\#94](https://github.com/inaka/erlang-katana/pull/94) ([jfacorro](https://github.com/jfacorro)) 201 | - Add options for xref in ktn\_meta\_SUITE [\#92](https://github.com/inaka/erlang-katana/pull/92) ([elbrujohalcon](https://github.com/elbrujohalcon)) 202 | - Upgrade erlang.mk, fix dialyzer warnings and other goodies [\#90](https://github.com/inaka/erlang-katana/pull/90) ([elbrujohalcon](https://github.com/elbrujohalcon)) 203 | - Create ktn\_meta\_SUITE [\#89](https://github.com/inaka/erlang-katana/pull/89) ([elbrujohalcon](https://github.com/elbrujohalcon)) 204 | - Create ktn\_fsm. [\#88](https://github.com/inaka/erlang-katana/pull/88) ([elbrujohalcon](https://github.com/elbrujohalcon)) 205 | - Update from Upstream [\#87](https://github.com/inaka/erlang-katana/pull/87) ([elbrujohalcon](https://github.com/elbrujohalcon)) 206 | - Update from upstream [\#86](https://github.com/inaka/erlang-katana/pull/86) ([elbrujohalcon](https://github.com/elbrujohalcon)) 207 | 208 | ## [0.2.13](https://github.com/inaka/erlang-katana/tree/0.2.13) (2015-09-07) 209 | 210 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.12...0.2.13) 211 | 212 | **Closed issues:** 213 | 214 | - Unhandled abstract form for `user\_type` [\#82](https://github.com/inaka/erlang-katana/issues/82) 215 | 216 | **Merged pull requests:** 217 | 218 | - \[\#84\] bump version [\#85](https://github.com/inaka/erlang-katana/pull/85) ([jfacorro](https://github.com/jfacorro)) 219 | - \[Closes \#82\] Handle user\_type [\#83](https://github.com/inaka/erlang-katana/pull/83) ([jfacorro](https://github.com/jfacorro)) 220 | 221 | ## [0.2.12](https://github.com/inaka/erlang-katana/tree/0.2.12) (2015-09-07) 222 | 223 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.11...0.2.12) 224 | 225 | **Closed issues:** 226 | 227 | - Bump version \(0.2.12 or maybe 0.3.0\) [\#80](https://github.com/inaka/erlang-katana/issues/80) 228 | - Update aleppo version to 0.9.2 [\#78](https://github.com/inaka/erlang-katana/issues/78) 229 | 230 | **Merged pull requests:** 231 | 232 | - \[Closes \#80\] Version bump [\#81](https://github.com/inaka/erlang-katana/pull/81) ([jfacorro](https://github.com/jfacorro)) 233 | - \[Closes \#78\] Update aleppo to 0.9.2 [\#79](https://github.com/inaka/erlang-katana/pull/79) ([jfacorro](https://github.com/jfacorro)) 234 | 235 | ## [0.2.11](https://github.com/inaka/erlang-katana/tree/0.2.11) (2015-09-02) 236 | 237 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.10...0.2.11) 238 | 239 | **Fixed bugs:** 240 | 241 | - Dialyze the Katana [\#44](https://github.com/inaka/erlang-katana/issues/44) 242 | 243 | **Closed issues:** 244 | 245 | - Bump to 0.2.11 [\#76](https://github.com/inaka/erlang-katana/issues/76) 246 | - Implement a ktn\_os:command/\[1, 2\] function that uses ports [\#74](https://github.com/inaka/erlang-katana/issues/74) 247 | - Bump to version 0.2.10 [\#72](https://github.com/inaka/erlang-katana/issues/72) 248 | 249 | **Merged pull requests:** 250 | 251 | - \[Closes \#76\] Version bump to 0.2.11 [\#77](https://github.com/inaka/erlang-katana/pull/77) ([jfacorro](https://github.com/jfacorro)) 252 | - \[Closes \#74\] ktn\_os:command/\[1,2\] [\#75](https://github.com/inaka/erlang-katana/pull/75) ([jfacorro](https://github.com/jfacorro)) 253 | 254 | ## [0.2.10](https://github.com/inaka/erlang-katana/tree/0.2.10) (2015-08-27) 255 | 256 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.9...0.2.10) 257 | 258 | **Closed issues:** 259 | 260 | - Include tokens in the root node returned by ktn\_code:parse\_tree/\[1,2\] [\#69](https://github.com/inaka/erlang-katana/issues/69) 261 | - Fulfill the open-source checklist [\#15](https://github.com/inaka/erlang-katana/issues/15) 262 | 263 | **Merged pull requests:** 264 | 265 | - \[\#72\] Bump version to 0.2.10 [\#73](https://github.com/inaka/erlang-katana/pull/73) ([jfacorro](https://github.com/jfacorro)) 266 | - \[\#69\] Fixed typo [\#71](https://github.com/inaka/erlang-katana/pull/71) ([jfacorro](https://github.com/jfacorro)) 267 | - \[\#69\] tokens as an attribute of root node. [\#70](https://github.com/inaka/erlang-katana/pull/70) ([jfacorro](https://github.com/jfacorro)) 268 | 269 | ## [0.2.9](https://github.com/inaka/erlang-katana/tree/0.2.9) (2015-08-21) 270 | 271 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.8...0.2.9) 272 | 273 | **Merged pull requests:** 274 | 275 | - Version Bump to 0.2.9 [\#68](https://github.com/inaka/erlang-katana/pull/68) ([elbrujohalcon](https://github.com/elbrujohalcon)) 276 | - ktn\_random:pick/1 [\#67](https://github.com/inaka/erlang-katana/pull/67) ([elbrujohalcon](https://github.com/elbrujohalcon)) 277 | 278 | ## [0.2.8](https://github.com/inaka/erlang-katana/tree/0.2.8) (2015-07-22) 279 | 280 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.7...0.2.8) 281 | 282 | **Closed issues:** 283 | 284 | - Version bump 0.2.7 [\#61](https://github.com/inaka/erlang-katana/issues/61) 285 | 286 | **Merged pull requests:** 287 | 288 | - Version bump to 0.2.8 [\#66](https://github.com/inaka/erlang-katana/pull/66) ([elbrujohalcon](https://github.com/elbrujohalcon)) 289 | - Remove eper from the project dependencies [\#65](https://github.com/inaka/erlang-katana/pull/65) ([elbrujohalcon](https://github.com/elbrujohalcon)) 290 | - Added an erlang config validation script [\#64](https://github.com/inaka/erlang-katana/pull/64) ([igaray](https://github.com/igaray)) 291 | 292 | ## [0.2.7](https://github.com/inaka/erlang-katana/tree/0.2.7) (2015-06-19) 293 | 294 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.6...0.2.7) 295 | 296 | **Closed issues:** 297 | 298 | - Unhandled abstract form issue with fun\(\(...\) [\#59](https://github.com/inaka/erlang-katana/issues/59) 299 | - Add a ktn\_lists:filter/3 function [\#48](https://github.com/inaka/erlang-katana/issues/48) 300 | 301 | **Merged pull requests:** 302 | 303 | - \[\#61\] Version bump to 0.2.7 [\#62](https://github.com/inaka/erlang-katana/pull/62) ([jfacorro](https://github.com/jfacorro)) 304 | - \[Closes \#59\] Handle 'fun\(...\) -\>' type expression [\#60](https://github.com/inaka/erlang-katana/pull/60) ([jfacorro](https://github.com/jfacorro)) 305 | 306 | ## [0.2.6](https://github.com/inaka/erlang-katana/tree/0.2.6) (2015-06-11) 307 | 308 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.5...0.2.6) 309 | 310 | **Implemented enhancements:** 311 | 312 | - Add functions to ktn\_random to generate random integers [\#53](https://github.com/inaka/erlang-katana/issues/53) 313 | 314 | **Closed issues:** 315 | 316 | - Add a ktn\_lists:map/3 function [\#46](https://github.com/inaka/erlang-katana/issues/46) 317 | 318 | **Merged pull requests:** 319 | 320 | - Version Bump to 0.2.6 [\#58](https://github.com/inaka/erlang-katana/pull/58) ([elbrujohalcon](https://github.com/elbrujohalcon)) 321 | - \[\#53\] Random uniform inclusive [\#57](https://github.com/inaka/erlang-katana/pull/57) ([jfacorro](https://github.com/jfacorro)) 322 | - \[Closes \#53\] Random uniform [\#56](https://github.com/inaka/erlang-katana/pull/56) ([jfacorro](https://github.com/jfacorro)) 323 | - Dialyze [\#52](https://github.com/inaka/erlang-katana/pull/52) ([elbrujohalcon](https://github.com/elbrujohalcon)) 324 | - Added feature: specify random string length [\#51](https://github.com/inaka/erlang-katana/pull/51) ([igaray](https://github.com/igaray)) 325 | - \[\#46\] Added ktn\_list:filter/3 and tests \(100%\) coverage of the module [\#50](https://github.com/inaka/erlang-katana/pull/50) ([jfacorro](https://github.com/jfacorro)) 326 | - \[Closes \#46\] Added ktn\_list:map/3 and tests \(100%\) coverage of the module [\#47](https://github.com/inaka/erlang-katana/pull/47) ([jfacorro](https://github.com/jfacorro)) 327 | - Changelog update [\#43](https://github.com/inaka/erlang-katana/pull/43) ([jfacorro](https://github.com/jfacorro)) 328 | 329 | ## [0.2.5](https://github.com/inaka/erlang-katana/tree/0.2.5) (2015-04-13) 330 | 331 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.4...0.2.5) 332 | 333 | **Fixed bugs:** 334 | 335 | - ktn\_date:now\_human\_readable should pad [\#39](https://github.com/inaka/erlang-katana/issues/39) 336 | 337 | **Merged pull requests:** 338 | 339 | - Version bump [\#42](https://github.com/inaka/erlang-katana/pull/42) ([igaray](https://github.com/igaray)) 340 | - Test utilities for REST APIs [\#41](https://github.com/inaka/erlang-katana/pull/41) ([igaray](https://github.com/igaray)) 341 | - \[\#39\] Add padding to month and day in human readable date format [\#40](https://github.com/inaka/erlang-katana/pull/40) ([jfacorro](https://github.com/jfacorro)) 342 | - \[\#35\] Update changelog. [\#38](https://github.com/inaka/erlang-katana/pull/38) ([jfacorro](https://github.com/jfacorro)) 343 | 344 | ## [0.2.4](https://github.com/inaka/erlang-katana/tree/0.2.4) (2015-03-20) 345 | 346 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.3...0.2.4) 347 | 348 | **Closed issues:** 349 | 350 | - Move `types` in `spec` node from `content` to `node\_attrs` [\#35](https://github.com/inaka/erlang-katana/issues/35) 351 | - Bump version to 0.2.3 [\#32](https://github.com/inaka/erlang-katana/issues/32) 352 | 353 | **Merged pull requests:** 354 | 355 | - \[\#35\] Bump version. [\#37](https://github.com/inaka/erlang-katana/pull/37) ([jfacorro](https://github.com/jfacorro)) 356 | - \[Closes \#35\] Moved `types` to `node\_attrs`. [\#36](https://github.com/inaka/erlang-katana/pull/36) ([jfacorro](https://github.com/jfacorro)) 357 | - \[Closes \#32\] Updated changelog. [\#34](https://github.com/inaka/erlang-katana/pull/34) ([jfacorro](https://github.com/jfacorro)) 358 | 359 | ## [0.2.3](https://github.com/inaka/erlang-katana/tree/0.2.3) (2015-03-19) 360 | 361 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.2...0.2.3) 362 | 363 | **Implemented enhancements:** 364 | 365 | - Add a ktn\_code:consult/1 function that behaves the same as file:consult/1 but for strings and binaries [\#24](https://github.com/inaka/erlang-katana/issues/24) 366 | 367 | **Fixed bugs:** 368 | 369 | - ktn\_code:consult/1 can't handle "{'.'}.\n" [\#26](https://github.com/inaka/erlang-katana/issues/26) 370 | 371 | **Closed issues:** 372 | 373 | - ktn\_code: separate attrs that are values from the ones that are nodes [\#30](https://github.com/inaka/erlang-katana/issues/30) 374 | - Add ktn\_code:beam\_to\_string/1 [\#28](https://github.com/inaka/erlang-katana/issues/28) 375 | 376 | **Merged pull requests:** 377 | 378 | - \[\#32\] Version bump. Added change log. [\#33](https://github.com/inaka/erlang-katana/pull/33) ([jfacorro](https://github.com/jfacorro)) 379 | - \[Closes \#30\] Separate attrs that are nodes form the ones that are not [\#31](https://github.com/inaka/erlang-katana/pull/31) ([jfacorro](https://github.com/jfacorro)) 380 | - \[Closes \#28\] Beam to string. [\#29](https://github.com/inaka/erlang-katana/pull/29) ([jfacorro](https://github.com/jfacorro)) 381 | 382 | ## [0.2.2](https://github.com/inaka/erlang-katana/tree/0.2.2) (2015-02-13) 383 | 384 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.1...0.2.2) 385 | 386 | **Merged pull requests:** 387 | 388 | - \[\#26\] Fixed bug. [\#27](https://github.com/inaka/erlang-katana/pull/27) ([jfacorro](https://github.com/jfacorro)) 389 | 390 | ## [0.2.1](https://github.com/inaka/erlang-katana/tree/0.2.1) (2015-02-13) 391 | 392 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.2.0...0.2.1) 393 | 394 | **Fixed bugs:** 395 | 396 | - user\_default conflicts [\#19](https://github.com/inaka/erlang-katana/issues/19) 397 | 398 | **Closed issues:** 399 | 400 | - Add ktn\_binary:join/2 [\#21](https://github.com/inaka/erlang-katana/issues/21) 401 | 402 | **Merged pull requests:** 403 | 404 | - \[\#24\] Added ktn\_code:consult/1 and moved split\_when/2 to ktn\_lists. [\#25](https://github.com/inaka/erlang-katana/pull/25) ([jfacorro](https://github.com/jfacorro)) 405 | - added ktn\_lists:delete\_first/2 [\#23](https://github.com/inaka/erlang-katana/pull/23) ([amilkr](https://github.com/amilkr)) 406 | - ktn\_binary:join/2 [\#22](https://github.com/inaka/erlang-katana/pull/22) ([amilkr](https://github.com/amilkr)) 407 | - \[Closes \#19\] Renamed user\_default.erl to avoid conflict [\#20](https://github.com/inaka/erlang-katana/pull/20) ([igaray](https://github.com/igaray)) 408 | - Update from upstream [\#18](https://github.com/inaka/erlang-katana/pull/18) ([elbrujohalcon](https://github.com/elbrujohalcon)) 409 | 410 | ## [0.2.0](https://github.com/inaka/erlang-katana/tree/0.2.0) (2014-10-22) 411 | 412 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/0.1.0...0.2.0) 413 | 414 | **Implemented enhancements:** 415 | 416 | - Add current functionality from elvis\_code to ktn\_code [\#16](https://github.com/inaka/erlang-katana/issues/16) 417 | 418 | **Merged pull requests:** 419 | 420 | - \[Closes \#16\] Added parsing code form elvis\_code [\#17](https://github.com/inaka/erlang-katana/pull/17) ([jfacorro](https://github.com/jfacorro)) 421 | - New stuff from @unbalancedparentheses [\#14](https://github.com/inaka/erlang-katana/pull/14) ([elbrujohalcon](https://github.com/elbrujohalcon)) 422 | 423 | ## [0.1.0](https://github.com/inaka/erlang-katana/tree/0.1.0) (2014-09-01) 424 | 425 | [Full Changelog](https://github.com/inaka/erlang-katana/compare/b8314f2e1819823f8fbe0d531a28833354d19eb8...0.1.0) 426 | 427 | **Implemented enhancements:** 428 | 429 | - Rename modules from katana to ktn [\#3](https://github.com/inaka/erlang-katana/issues/3) 430 | 431 | **Fixed bugs:** 432 | 433 | - Make katana releasable [\#8](https://github.com/inaka/erlang-katana/issues/8) 434 | - Syntax error in katana.app.src [\#5](https://github.com/inaka/erlang-katana/issues/5) 435 | 436 | **Closed issues:** 437 | 438 | - Create katana\_maps for maps utility functions [\#1](https://github.com/inaka/erlang-katana/issues/1) 439 | 440 | **Merged pull requests:** 441 | 442 | - Igaray.recipe [\#13](https://github.com/inaka/erlang-katana/pull/13) ([igaray](https://github.com/igaray)) 443 | - Igaray.recipe [\#12](https://github.com/inaka/erlang-katana/pull/12) ([igaray](https://github.com/igaray)) 444 | - Created ktn\_numbers module. [\#10](https://github.com/inaka/erlang-katana/pull/10) ([jfacorro](https://github.com/jfacorro)) 445 | - \[\#8\] Make katana releasable. [\#9](https://github.com/inaka/erlang-katana/pull/9) ([jfacorro](https://github.com/jfacorro)) 446 | - Added Microsends and resolved name bug [\#7](https://github.com/inaka/erlang-katana/pull/7) ([unbalancedparentheses](https://github.com/unbalancedparentheses)) 447 | - \[\#5\] Fixed syntax error. [\#6](https://github.com/inaka/erlang-katana/pull/6) ([jfacorro](https://github.com/jfacorro)) 448 | - \[\#3\] Renamed modules. [\#4](https://github.com/inaka/erlang-katana/pull/4) ([jfacorro](https://github.com/jfacorro)) 449 | - \[\#1\] get/\[2,3\] nested maps. [\#2](https://github.com/inaka/erlang-katana/pull/2) ([jfacorro](https://github.com/jfacorro)) 450 | 451 | 452 | 453 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 454 | --------------------------------------------------------------------------------