-module(pact_verifier). -include_lib("inets/include/httpd.hrl"). -behaviour(gen_server). %% Internal web server API -export([ start/2, stop/1, do/1 ]). %% Pact Verifier APIs -export([ start_verifier/2, verify/1, stop_verifier/1 ]). -export([init/1, handle_call/3, terminate/2, handle_cast/2]). -dialyzer(no_behaviours). start(Port, Name) -> {ok, Pid} = inets:start(httpd, [ {bind_address, "127.0.0.1"}, {port, Port}, {server_name, binary_to_list(Name)}, {server_root, "./"}, {document_root, "./"}, {modules, [pact_verifier]} ]), Info = httpd:info(Pid), {port, ListenPort} = lists:keyfind(port, 1, Info), {ok, ListenPort, Pid}. stop(Pid) -> inets:stop(httpd, Pid). do(ModData) -> case catch process_data(ModData) of {'EXIT', Reason} -> io:format("Error: ~p~n", [Reason]), [{response, {500, "Internal Server Error"}}]; Response -> Response end. process_data( #mod{request_uri = "/message_pact/verify", method = "POST", entity_body = Body} = ModData ) -> {ok, StateReq} = thoas:decode(Body), Description = maps:get(<<"description">>, StateReq, <<"">>), Port = extract_address_port(ModData#mod.absolute_uri), Info = httpd:info({127, 0, 0, 1}, Port), {server_name, ServerName} = lists:keyfind(server_name, 1, Info), {M, F, Args} = get_mfa_from_description(ServerName, Description), Message = erlang:apply(M, F, Args), make_json_response(200, Message). make_json_response(Code, Body) -> BodyJson = erlang:binary_to_list(thoas:encode(Body)), Length = io_lib:format("~w", [io_lib:chars_length(BodyJson)]), {proceed, [ {response, {response, [{code, Code}, {content_length, Length}, {content_type, "application/json"}], BodyJson}} ]}. %% Web Server End %% ================================================================================================ %% Gen Server -type provider() :: binary(). -type provider_opts() :: map(). -type verifier_ref() :: pid(). %% erlfmt-ignore -record(pact_verifier, { provider :: provider(), provider_opts = #{} :: provider_opts(), provider_port_details :: tuple() }). %% @doc Starts pact provider verifier -spec start_verifier(provider(), provider_opts()) -> gen_server:start_ret(). start_verifier(Provider, ProviderOpts) -> Protocol = maps:get(protocol, ProviderOpts, <<"http">>), ProviderPort = maps:get(port, ProviderOpts, undefined), {Port, HttpPid} = case Protocol of <<"http">> -> Port1 = ProviderPort, {Port1, undefined}; <<"message">> -> case ProviderPort of undefined -> %% Start a new port for message protocol {ok, Port1, HttpPid1} = pact_verifier:start(0, Provider), {Port1, HttpPid1}; Port1 -> {Port1, undefined} end end, gen_server:start( {global, {?MODULE, Provider}}, ?MODULE, #pact_verifier{ provider = Provider, provider_opts = ProviderOpts, provider_port_details = {Port, HttpPid} }, [] ). %% @doc Verifies pacts for the given provider %% Returns 0 if all pacts are verified successfully -spec verify(verifier_ref()) -> integer(). verify(VerifierRef) -> {ProviderOpts, ProviderPortDetails} = gen_server:call(VerifierRef, {get_provider_state_details}), verify_pacts_internal(VerifierRef, ProviderOpts, ProviderPortDetails). -spec get_mfa_from_description(string(), binary()) -> tuple(). get_mfa_from_description(Provider, Description) -> {Map, FallbackProviderMFA} = gen_server:call({global, {?MODULE, list_to_binary(Provider)}}, { get_message_providers_map }), case maps:get(Description, Map, undefined) of undefined -> FallbackProviderMFA; ProviderMFA -> ProviderMFA end. stop_verifier(VerifierRef) -> ok = gen_server:stop(VerifierRef). %% message_providers map example %% Sample description to MFA mapping for pact verifier to know which MFA to test % #{ % <<"weather api 1">> => {M, F, ArgsList} % } %% Gen_server callbacks init(#pact_verifier{provider = _Provider, provider_opts = _ProviderOpts} = State) -> {ok, State}. handle_call({get_provider_state_details}, _From, State) -> %% Verify file pacts or broker pacts or both here %% Based on pact_source_opts key in Opts. ProviderOpts = State#pact_verifier.provider_opts, ProviderPortDetails = State#pact_verifier.provider_port_details, {reply, {ProviderOpts, ProviderPortDetails}, State}; handle_call({get_message_providers_map}, _From, State) -> ProviderOpts = State#pact_verifier.provider_opts, MessageProvidersMap = maps:get(message_providers, ProviderOpts, #{}), FallbackProviderFunc = maps:get(fallback_message_provider, ProviderOpts, undefined), {reply, {MessageProvidersMap, FallbackProviderFunc}, State}. handle_cast(_Msg, State) -> {noreply, State}. terminate(_Reason, _State) -> ok. verify_pacts_internal(VerifierRef, ProviderOpts, ProviderPortDetails) -> {Port, HttpPid} = ProviderPortDetails, #{ name := Name, version := Version, host := Host, branch := Branch, pact_source_opts := PactSourceOpts } = ProviderOpts, Protocol = maps:get(protocol, ProviderOpts, <<"http">>), BaseUrl = maps:get(base_url, ProviderOpts, <<"/">>), StateChangeUrl = maps:get(state_change_url, ProviderOpts, <<"">>), Scheme = maps:get(scheme, ProviderOpts, <<"http">>), FilePath = maps:get(file_path, PactSourceOpts, undefined), PactBrokerUrl = maps:get(broker_url, PactSourceOpts, undefined), %% PublishVerificationResults = 1 means publish verification results %% Otherwise do not publish PublishVerificationResults = maps:get(publish_verification_results, ProviderOpts, 1), OutputFileVerification = case FilePath of undefined -> 0; _ -> pactffi_nif:verify_file_pacts( Name, Scheme, Host, Port, BaseUrl, Version, Branch, FilePath, Protocol, StateChangeUrl, PublishVerificationResults ) end, OutputBrokerVerification = case PactBrokerUrl of undefined -> 0; _ -> #{ broker_username := BrokerUser, broker_password := BrokerPassword, enable_pending := EnablePending, consumer_version_selectors := ConsumerVersionSelectors } = PactSourceOpts, pactffi_nif:verify_broker_pacts( Name, Scheme, Host, Port, BaseUrl, Version, Branch, PactBrokerUrl, BrokerUser, BrokerPassword, EnablePending, ConsumerVersionSelectors, Protocol, StateChangeUrl, PublishVerificationResults ) end, case Protocol of <<"message">> -> case HttpPid of undefined -> %% If HttpPid is undefined, it means we are not using HTTP protocol %% and we should not stop the HTTP server. ok; _ -> %% Stop the HTTP server if it was started for message protocol pact_verifier:stop(HttpPid) end, stop_verifier(VerifierRef); _ -> stop_verifier(VerifierRef) end, combine_return_codes(OutputFileVerification, OutputBrokerVerification). combine_return_codes(0, 0) -> 0; combine_return_codes(Code1, _) when Code1 =/= 0 -> Code1; combine_return_codes(_, Code2) -> Code2. extract_address_port(Url) -> % Define a regex pattern to match the address and port Pattern = ":(\\d+)/", % Use re module to match the pattern against the URL case re:run(Url, Pattern, [{capture, all_but_first, list}]) of {match, [PortStr]} -> % Convert the port string to an integer Port = list_to_integer(PortStr), Port end.