-module(ktn_recipe_verify). -behaviour(ktn_recipe). %% Behaviour exports -export( [ transitions/0 , process_result/1 , process_error/1 ]). %% Steps -export( [ verify_exports/1 , verify_normalizability/1 , verify_transitions/1 , verify_transition_exports/1 ]). -type error() :: term(). -type state() :: #{ recipe_type => implicit | explicit , recipe => module() | ktn_recipe:transitions() , transitions => ktn_recipe:normalized_transitions() , error => error() }. -spec transitions() -> [ verify_exports | verify_normalizability | verify_transitions | verify_transition_exports ]. transitions() -> [ verify_exports , verify_normalizability , verify_transitions , verify_transition_exports ]. -spec verify_exports(state()) -> {ok, state()} | {error, state()}. verify_exports(State = #{recipe_type := implicit, recipe := Mod}) -> % Ensure that the module exported transitions/0, process_result/1 and % process_error/1. Exports = proplists:get_value(exports, Mod:module_info()), TransitionsExported = lists:member({transitions, 0}, Exports), ResultFunExported = lists:member({process_result, 1}, Exports), ErrorFunExported = lists:member({process_error, 1}, Exports), case {TransitionsExported, ResultFunExported, ErrorFunExported} of {false, _, _} -> {error, State#{ error => {not_exported, transitions}} }; {_, false, _} -> {error, State#{ error => {not_exported, process_result}} }; {_, _, false} -> {error, State#{ error => {not_exported, process_error}} }; {true, true, true} -> {ok, State} end; verify_exports(State = #{recipe_type := explicit}) -> % Nothing needs to be done here for explicit recipes. {ok, State}. -spec verify_normalizability(state()) -> {ok, state()} | {error, state()}. verify_normalizability(State = #{recipe := Recipe}) -> try {ok, State#{transitions => ktn_recipe:normalize(Recipe)}} catch _:NormalizationError -> {error, State#{error => NormalizationError}} end. -spec verify_transitions(state()) -> {ok, state()} | {error, state()}. verify_transitions( State = #{recipe_type := implicit, transitions := Transitions} ) -> F = fun (X, A) when is_atom(X) -> A; (F, A) when is_function(F, 1) -> A; ({X, _, Y}, A) when is_function(X, 1), is_function(Y, 1) -> A; ({F, _, Y}, A) when is_function(F), is_atom(Y) -> A; ({X, _, F}, A) when is_function(F, 1), is_atom(X) -> A; ({F1, _, F2}, A) when is_function(F1, 1), is_function(F2, 1) -> A; (X, A) -> [X | A] end, verify_transitions(F, Transitions, State); verify_transitions( State = #{recipe_type := explicit, transitions := Transitions} ) -> F = fun (F, A) when is_function(F, 1) -> A; ({F, _, Action}, A) when is_function(F, 1), (Action == error orelse Action == halt) -> A; ({F1, _, F2}, A) when is_function(F1, 1), is_function(F2, 1) -> A; (X, A) -> [X | A] end, verify_transitions(F, Transitions, State). verify_transitions(F, Transitions, State) -> case lists:foldl(F, [], Transitions) of [] -> {ok, State}; InvalidElements -> { error , State#{error => {invalid_transition_table_elements, InvalidElements}} } end. -spec verify_transition_exports(state()) -> {ok, state()} | {error, state()}. verify_transition_exports(State = #{transitions := Transitions}) -> % Gather all the step functions mentioned in the transition table... % ...and ensure that they are exported. F = fun ({StepFun, _, Action}, A) when Action == halt; Action == error -> {module, M} = erlang:fun_info(StepFun, module), {name, F} = erlang:fun_info(StepFun, name), Exported = erlang:function_exported(M, F, 1), case Exported of true -> A; false -> [StepFun | A] end; ({StepFun1, _, StepFun2}, A) -> {module, M1} = erlang:fun_info(StepFun1, module), {module, M2} = erlang:fun_info(StepFun2, module), {name, F1} = erlang:fun_info(StepFun1, name), {name, F2} = erlang:fun_info(StepFun2, name), Exported1 = erlang:function_exported(M1, F1, 1), Exported2 = erlang:function_exported(M2, F2, 1), case {Exported1, Exported2} of {true, true} -> A; {true, false} -> [StepFun1 | A]; {false, true} -> [StepFun2 | A]; {false, false} -> [StepFun1, StepFun2 | A] end end, case lists:foldl(F, [], Transitions) of [] -> {ok, State}; NotExported -> {error, State#{error => {not_exported, NotExported}}} end. -spec process_result(state()) -> ok. process_result(_State) -> ok. -spec process_error(state()) -> {error, error()}. process_error(#{error := Error}) -> {error, Error}.