-module(testcontainer_compose). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/testcontainer_compose.gleam"). -export([from_file/1, with_project_name/2, with_env_override/3, parse_services_json/2, formula/1, with_compose/2, services/1, service_by_name/2, service_name/1, service_image/1, service_ports/1]). -export_type([compose_config/0, compose_services/0, service/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. -opaque compose_config() :: {compose_config, binary(), binary(), list({binary(), cowl:secret(binary())})}. -opaque compose_services() :: {compose_services, binary(), binary(), list(service())}. -type service() :: {service, binary(), binary(), list({integer(), integer()})}. -file("src/testcontainer_compose.gleam", 33). -spec from_file(binary()) -> compose_config(). from_file(File_path) -> {compose_config, File_path, <<"testcontainer"/utf8>>, []}. -file("src/testcontainer_compose.gleam", 41). -spec with_project_name(compose_config(), binary()) -> compose_config(). with_project_name(Cfg, Project_name) -> {compose_config, erlang:element(2, Cfg), Project_name, erlang:element(4, Cfg)}. -file("src/testcontainer_compose.gleam", 48). -spec with_env_override(compose_config(), binary(), binary()) -> compose_config(). with_env_override(Cfg, Key, Value) -> Secret = cowl:secret(Value), Overrides = lists:append(erlang:element(4, Cfg), [{Key, Secret}]), {compose_config, erlang:element(2, Cfg), erlang:element(3, Cfg), Overrides}. -file("src/testcontainer_compose.gleam", 187). -spec reveal_env_overrides(list({binary(), cowl:secret(binary())})) -> list({binary(), binary()}). reveal_env_overrides(Overrides) -> gleam@list:map( Overrides, fun(Pair) -> {Key, Secret} = Pair, {Key, cowl:reveal(Secret)} end ). -file("src/testcontainer_compose.gleam", 266). -spec parse_int_string(binary()) -> gleam@dynamic@decode:decoder(integer()). parse_int_string(S) -> case gleam_stdlib:parse_int(S) of {ok, N} -> gleam@dynamic@decode:success(N); {error, _} -> gleam@dynamic@decode:failure(0, <<"Int"/utf8>>) end. -file("src/testcontainer_compose.gleam", 247). -spec decode_service_ports(gleam@dynamic:dynamic_()) -> list({integer(), integer()}). decode_service_ports(Svc_dyn) -> Port_decoder = begin gleam@dynamic@decode:field( <<"target"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_int/1}, fun(Target) -> gleam@dynamic@decode:optional_field( <<"published"/utf8>>, Target, gleam@dynamic@decode:one_of( {decoder, fun gleam@dynamic@decode:decode_int/1}, [begin _pipe = {decoder, fun gleam@dynamic@decode:decode_string/1}, gleam@dynamic@decode:then( _pipe, fun parse_int_string/1 ) end] ), fun(Published) -> gleam@dynamic@decode:success({Published, Target}) end ) end ) end, Decoder = gleam@dynamic@decode:field( <<"ports"/utf8>>, gleam@dynamic@decode:list(Port_decoder), fun gleam@dynamic@decode:success/1 ), case gleam@dynamic@decode:run(Svc_dyn, Decoder) of {ok, Ports} -> Ports; {error, _} -> [] end. -file("src/testcontainer_compose.gleam", 239). -spec decode_service_image(gleam@dynamic:dynamic_()) -> binary(). decode_service_image(Svc_dyn) -> Decoder = gleam@dynamic@decode:field( <<"image"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun gleam@dynamic@decode:success/1 ), case gleam@dynamic@decode:run(Svc_dyn, Decoder) of {ok, Image} -> Image; {error, _} -> <<""/utf8>> end. -file("src/testcontainer_compose.gleam", 226). -spec services_from_dict(gleam@dict:dict(binary(), gleam@dynamic:dynamic_())) -> list(service()). services_from_dict(Services_dict) -> _pipe = Services_dict, _pipe@1 = maps:to_list(_pipe), gleam@list:map( _pipe@1, fun(Entry) -> {Name, Svc_dyn} = Entry, Image = decode_service_image(Svc_dyn), Ports = decode_service_ports(Svc_dyn), {service, Name, Image, Ports} end ). -file("src/testcontainer_compose.gleam", 201). ?DOC( " Parse a `docker compose config --format json` output string into typed\n" " `Service` records. Exposed for testability — useful when you have a\n" " pre-rendered compose JSON and want to validate parsing without Docker.\n" "\n" " `path` is only used to enrich error messages.\n" ). -spec parse_services_json(binary(), binary()) -> {ok, list(service())} | {error, testcontainer_compose@error:error()}. parse_services_json(Path, Json_str) -> Decoder = begin gleam@dynamic@decode:field( <<"services"/utf8>>, gleam@dynamic@decode:dict( {decoder, fun gleam@dynamic@decode:decode_string/1}, {decoder, fun gleam@dynamic@decode:decode_dynamic/1} ), fun(Services_dict) -> gleam@dynamic@decode:success(Services_dict) end ) end, case gleam@json:parse(Json_str, Decoder) of {ok, Services_dict@1} -> case maps:size(Services_dict@1) of 0 -> {error, {invalid_yaml, Path, <<"no services defined"/utf8>>}}; _ -> {ok, services_from_dict(Services_dict@1)} end; {error, _} -> {error, {invalid_yaml, Path, <<"compose config json parse failed"/utf8>>}} end. -file("src/testcontainer_compose.gleam", 165). -spec load_compose_file(compose_config()) -> {ok, list(service())} | {error, testcontainer_compose@error:error()}. load_compose_file(Cfg) -> case fio:exists(erlang:element(2, Cfg)) of false -> {error, {compose_file_not_found, erlang:element(2, Cfg)}}; true -> Env_pairs = reveal_env_overrides(erlang:element(4, Cfg)), gleam@result:'try'( begin _pipe = testcontainer_compose_ffi:compose_config_json( erlang:element(2, Cfg), Env_pairs ), gleam@result:map_error( _pipe, fun(Reason) -> {invalid_yaml, erlang:element(2, Cfg), Reason} end ) end, fun(Json_str) -> parse_services_json(erlang:element(2, Cfg), Json_str) end ) end. -file("src/testcontainer_compose.gleam", 58). -spec formula(compose_config()) -> {ok, testcontainer@formula:standalone_formula(compose_services(), testcontainer_compose@error:error())} | {error, testcontainer_compose@error:error()}. formula(Cfg) -> gleam@result:'try'( load_compose_file(Cfg), fun(Services) -> Stack = {compose_services, erlang:element(2, Cfg), erlang:element(3, Cfg), Services}, Env_pairs = reveal_env_overrides(erlang:element(4, Cfg)), {ok, testcontainer@formula:new_standalone( fun() -> _pipe = testcontainer_compose_ffi:run_compose_up( erlang:element(2, Cfg), erlang:element(3, Cfg), Env_pairs ), _pipe@1 = gleam@result:map(_pipe, fun(_) -> Stack end), gleam@result:map_error( _pipe@1, fun(Reason) -> {compose_failed, erlang:element(2, Cfg), Reason} end ) end, fun() -> _pipe@2 = testcontainer_compose_ffi:run_compose_down( erlang:element(2, Cfg), erlang:element(3, Cfg) ), _pipe@3 = gleam@result:map(_pipe@2, fun(_) -> nil end), gleam@result:map_error( _pipe@3, fun(Reason@1) -> {compose_failed, erlang:element(2, Cfg), Reason@1} end ) end )} end ). -file("src/testcontainer_compose.gleam", 93). ?DOC(" Spin up a compose stack, run the body, and always tear it down.\n"). -spec with_compose( compose_config(), fun((compose_services()) -> {ok, IZA} | {error, testcontainer_compose@error:error()}) ) -> {ok, IZA} | {error, testcontainer_compose@error:error()}. with_compose(Cfg, Body) -> gleam@result:'try'( load_compose_file(Cfg), fun(Services) -> Env_pairs = reveal_env_overrides(erlang:element(4, Cfg)), gleam@result:'try'( begin _pipe = testcontainer_compose_ffi:run_compose_up( erlang:element(2, Cfg), erlang:element(3, Cfg), Env_pairs ), gleam@result:map_error( _pipe, fun(Reason) -> {compose_failed, erlang:element(2, Cfg), Reason} end ) end, fun(_) -> Stack = {compose_services, erlang:element(2, Cfg), erlang:element(3, Cfg), Services}, Body_result = Body(Stack), Down_result = begin _pipe@1 = testcontainer_compose_ffi:run_compose_down( erlang:element(2, Cfg), erlang:element(3, Cfg) ), _pipe@2 = gleam@result:map(_pipe@1, fun(_) -> nil end), gleam@result:map_error( _pipe@2, fun(Reason@1) -> {compose_failed, erlang:element(2, Cfg), Reason@1} end ) end, case {Body_result, Down_result} of {{ok, _}, {error, E}} -> {error, E}; {_, _} -> Body_result end end ) end ). -file("src/testcontainer_compose.gleam", 139). -spec services(compose_services()) -> list(service()). services(Stack) -> erlang:element(4, Stack). -file("src/testcontainer_compose.gleam", 143). -spec service_by_name(compose_services(), binary()) -> gleam@option:option(service()). service_by_name(Stack, Name) -> case gleam@list:find( erlang:element(4, Stack), fun(Svc) -> erlang:element(2, Svc) =:= Name end ) of {ok, Svc@1} -> {some, Svc@1}; {error, _} -> none end. -file("src/testcontainer_compose.gleam", 153). -spec service_name(service()) -> binary(). service_name(Svc) -> erlang:element(2, Svc). -file("src/testcontainer_compose.gleam", 157). -spec service_image(service()) -> binary(). service_image(Svc) -> erlang:element(3, Svc). -file("src/testcontainer_compose.gleam", 161). -spec service_ports(service()) -> list({integer(), integer()}). service_ports(Svc) -> erlang:element(4, Svc).