-module(embeds@generate). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([default_print/2, describe_error/1, describe_errors/1, guard_gleam_project/1, bits_to_data/1, data_to_bits/1, result_to_const/1, skip_or_const/1, result_to_fn/1, skip_or_fn/1, to_gleam_identifier/1, to_gleam_module_name/1, generate_module_file/3, generate_modules/6, generate/4]). -export_type([error/0, data/0, code/0, definition/0]). -type error() :: not_inside_gleam_project | {invalid_argument, binary(), binary(), binary()} | {could_not_list_files, binary(), simplifile:file_error()} | {could_not_read, binary(), binary()} | {duplicate_var_name, binary(), binary()} | {printer_error, binary(), binary()} | {write_error, binary(), simplifile:file_error()}. -type data() :: {text, binary()} | {binary, bitstring()}. -type code() :: skip | {generate_error, binary()} | {constant, binary()} | {function_body, binary()}. -type definition() :: {definition, binary(), fun(() -> {ok, data()} | {error, binary()}), integer(), binary(), binary()}. -spec default_print(binary(), data()) -> code(). default_print(_, Data) -> Code = case Data of {text, Text} -> gleam@string:inspect(Text); {binary, Bits} -> gleam@bit_array:inspect(Bits) end, {constant, Code}. -spec describe_error(error()) -> binary(). describe_error(Err) -> case Err of not_inside_gleam_project -> <<"generate needs to be called inside the root directory of your Gleam project!"/utf8>>; {invalid_argument, Name, Value, Reason} -> <<<<<<<<<<"Invalid argument "/utf8, Name/binary>>/binary, " ("/utf8>>/binary, Value/binary>>/binary, "): "/utf8>>/binary, Reason/binary>>; {could_not_list_files, Path, E} -> <<<<<<"Could not list files from "/utf8, Path/binary>>/binary, ": "/utf8>>/binary, (simplifile:describe_error(E))/binary>>; {could_not_read, Path@1, E@1} -> <<<<<<"Could not read "/utf8, Path@1/binary>>/binary, ": "/utf8>>/binary, E@1/binary>>; {duplicate_var_name, A, B} -> <<<<<<<<"I have found 2 source definitions "/utf8, A/binary>>/binary, " and "/utf8>>/binary, B/binary>>/binary, " - both would produce the same constant in the same module!"/utf8>>; {printer_error, Path@2, E@2} -> <<<<<<"The printer for "/utf8, Path@2/binary>>/binary, " returned an error: "/utf8>>/binary, E@2/binary>>; {write_error, Path@3, E@3} -> <<<<<<"Error writing module "/utf8, Path@3/binary>>/binary, ": "/utf8>>/binary, (simplifile:describe_error(E@3))/binary>> end. -spec describe_errors(list(error())) -> binary(). describe_errors(Errs) -> gleam@string:join(gleam@list:map(Errs, fun describe_error/1), <<"\n"/utf8>>). -spec guard_gleam_project(fun(() -> {ok, ING} | {error, list(error())})) -> {ok, ING} | {error, list(error())}. guard_gleam_project(F) -> case simplifile_erl:is_file(<<"gleam.toml"/utf8>>) of {ok, true} -> F(); _ -> {error, [not_inside_gleam_project]} end. -spec bits_to_data(bitstring()) -> data(). bits_to_data(Bits) -> case gleam@bit_array:to_string(Bits) of {ok, Text} -> {text, Text}; {error, _} -> {binary, Bits} end. -spec data_to_bits(data()) -> bitstring(). data_to_bits(Data) -> case Data of {text, Text} -> gleam_stdlib:identity(Text); {binary, Bits} -> Bits end. -spec result_to_const({ok, binary()} | {error, binary()}) -> code(). result_to_const(Result) -> case Result of {ok, Code} -> {constant, Code}; {error, Err} -> {generate_error, Err} end. -spec skip_or_const({ok, binary()} | {error, nil}) -> code(). skip_or_const(Result) -> case Result of {ok, Code} -> {constant, Code}; {error, nil} -> skip end. -spec result_to_fn({ok, binary()} | {error, binary()}) -> code(). result_to_fn(Result) -> case Result of {ok, Code} -> {function_body, Code}; {error, Err} -> {generate_error, Err} end. -spec skip_or_fn({ok, binary()} | {error, nil}) -> code(). skip_or_fn(Result) -> case Result of {ok, Code} -> {function_body, Code}; {error, nil} -> skip end. -spec to_gleam_identifier(binary()) -> binary(). to_gleam_identifier(Str) -> _assert_subject = gleam@regex:from_string(<<"[^a-zA-Z0-9]+"/utf8>>), {ok, Re} = case _assert_subject of {ok, _} -> _assert_subject; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"embeds/generate"/utf8>>, function => <<"to_gleam_identifier"/utf8>>, line => 234}) end, Var_name = begin _pipe = Str, _pipe@1 = gleam_stdlib:regex_replace(Re, _pipe, <<" "/utf8>>), _pipe@2 = gleam@string:trim(_pipe@1), _pipe@3 = gleam@string:replace(_pipe@2, <<" "/utf8>>, <<"_"/utf8>>), gleam@string:lowercase(_pipe@3) end, case Var_name of <<"0"/utf8, _/binary>> -> <<"n"/utf8, Var_name/binary>>; <<"1"/utf8, _/binary>> -> <<"n"/utf8, Var_name/binary>>; <<"2"/utf8, _/binary>> -> <<"n"/utf8, Var_name/binary>>; <<"3"/utf8, _/binary>> -> <<"n"/utf8, Var_name/binary>>; <<"4"/utf8, _/binary>> -> <<"n"/utf8, Var_name/binary>>; <<"5"/utf8, _/binary>> -> <<"n"/utf8, Var_name/binary>>; <<"6"/utf8, _/binary>> -> <<"n"/utf8, Var_name/binary>>; <<"7"/utf8, _/binary>> -> <<"n"/utf8, Var_name/binary>>; <<"8"/utf8, _/binary>> -> <<"n"/utf8, Var_name/binary>>; <<"9"/utf8, _/binary>> -> <<"n"/utf8, Var_name/binary>>; <<""/utf8>> -> <<"empty"/utf8>>; <<"as"/utf8>> -> <>; <<"assert"/utf8>> -> <>; <<"auto"/utf8>> -> <>; <<"case"/utf8>> -> <>; <<"const"/utf8>> -> <>; <<"delegate"/utf8>> -> <>; <<"derive"/utf8>> -> <>; <<"echo"/utf8>> -> <>; <<"else"/utf8>> -> <>; <<"fn"/utf8>> -> <>; <<"if"/utf8>> -> <>; <<"implement"/utf8>> -> <>; <<"import"/utf8>> -> <>; <<"let"/utf8>> -> <>; <<"macro"/utf8>> -> <>; <<"opaque"/utf8>> -> <>; <<"panic"/utf8>> -> <>; <<"pub"/utf8>> -> <>; <<"test"/utf8>> -> <>; <<"todo"/utf8>> -> <>; <<"type"/utf8>> -> <>; <<"use"/utf8>> -> <>; _ -> Var_name end. -spec to_gleam_module_name(binary()) -> binary(). to_gleam_module_name(Path) -> Module = gleam@string:join( (gleam@list:filter_map( begin _pipe = Path, _pipe@1 = gleam@string:replace( _pipe, <<"\\"/utf8>>, <<"/"/utf8>> ), gleam@string:split(_pipe@1, <<"/"/utf8>>) end, fun(Part) -> case Part of <<"."/utf8>> -> {error, nil}; <<".."/utf8>> -> {error, nil}; <<""/utf8>> -> {error, nil}; _ -> {ok, to_gleam_identifier(Part)} end end )), <<"/"/utf8>> ), case Module of <<""/utf8>> -> <<"assets"/utf8>>; _ -> Module end. -spec module_file(binary()) -> binary(). module_file(Module) -> filepath:join(<<"src"/utf8>>, <>). -spec get_mtime(binary()) -> {ok, integer()} | {error, simplifile:file_error()}. get_mtime(Path) -> gleam@result:map( simplifile_erl:file_info(Path), fun(Info) -> erlang:element(10, Info) end ). -spec write(binary(), binary()) -> {ok, nil} | {error, simplifile:file_error()}. write(Path, Contents) -> gleam@result:'try'( begin _pipe = Path, _pipe@1 = filepath:directory_name(_pipe), simplifile:create_directory_all(_pipe@1) end, fun(_) -> simplifile:write(Path, Contents) end ). -spec do_gather_by(list(IOR), fun((IOR) -> IOT), list({IOT, list(IOR)})) -> list({IOT, list(IOR)}). do_gather_by(List, To_key, Acc) -> case List of [] -> lists:reverse(Acc); [Head | Tail] -> Key = To_key(Head), {Matching, Remaining} = gleam@list:partition( Tail, fun(Other) -> To_key(Other) =:= Key end ), do_gather_by(Remaining, To_key, [{Key, [Head | Matching]} | Acc]) end. -spec gather_by(list(IOM), fun((IOM) -> IOO)) -> list({IOO, list(IOM)}). gather_by(List, To_key) -> do_gather_by(List, To_key, []). -spec enforce_unique_by(list(IOY), fun((IOY) -> any())) -> {ok, list(IOY)} | {error, {IOY, IOY}}. enforce_unique_by(List, To_key) -> gleam@list:try_fold( List, [], fun(Acc, Elem) -> Key = To_key(Elem), case gleam@list:find( Acc, fun(Existing) -> To_key(Existing) =:= Key end ) of {ok, Existing@1} -> {error, {Existing@1, Elem}}; {error, nil} -> {ok, [Elem | Acc]} end end ). -spec do_traverse_map( list(IPP), fun((IPP) -> {ok, IPR} | {error, list(IPS)}), list(IPR), list(IPS) ) -> {ok, list(IPR)} | {error, list(IPS)}. do_traverse_map(List, Body, Oks, Errs) -> case List of [] -> case Errs of [] -> {ok, lists:reverse(Oks)}; _ -> {error, lists:reverse(Errs)} end; [Head | Tail] -> case Body(Head) of {ok, Ok} -> do_traverse_map(Tail, Body, [Ok | Oks], Errs); {error, Err} -> do_traverse_map(Tail, Body, Oks, lists:append(Err, Errs)) end end. -spec traverse_map(list(IPE), fun((IPE) -> {ok, IPG} | {error, list(IPH)})) -> {ok, list(IPG)} | {error, list(IPH)}. traverse_map(List, Body) -> do_traverse_map(List, Body, [], []). -spec 'try'( {ok, IQC} | {error, IQD}, fun((IQC) -> {ok, IQG} | {error, list(IQD)}) ) -> {ok, IQG} | {error, list(IQD)}. 'try'(Result, F) -> case Result of {ok, Ok} -> F(Ok); {error, Err} -> {error, [Err]} end. -spec generate_module_file( list(definition()), binary(), fun((binary(), data()) -> code()) ) -> {ok, binary()} | {error, list(error())}. generate_module_file(Definitions, Header, Print) -> gleam@result:'try'( (traverse_map( Definitions, fun(Definition) -> 'try'( begin _pipe = (erlang:element(3, Definition))(), gleam@result:map_error( _pipe, fun(E) -> {could_not_read, erlang:element(2, Definition), E} end ) end, fun(Data) -> Code = Print(erlang:element(2, Definition), Data), case Code of skip -> {error, []}; {generate_error, Err} -> {error, [{printer_error, erlang:element(2, Definition), Err}]}; {constant, Code@1} -> {ok, <<<<<<<<<<<<"/// "/utf8, (erlang:element( 2, Definition ))/binary>>/binary, "\n"/utf8>>/binary, "pub const "/utf8>>/binary, (erlang:element(6, Definition))/binary>>/binary, " = "/utf8>>/binary, Code@1/binary>>}; {function_body, Code@2} -> {ok, <<<<<<<<<<<<<<"/// "/utf8, (erlang:element( 2, Definition ))/binary>>/binary, "\n"/utf8>>/binary, "pub fn "/utf8>>/binary, (erlang:element( 6, Definition ))/binary>>/binary, "() {\n"/utf8>>/binary, Code@2/binary>>/binary, "\n}"/utf8>>} end end ) end )), fun(Definitions@1) -> case {Definitions@1, Header} of {[], _} -> {ok, <<""/utf8>>}; {_, <<""/utf8>>} -> {ok, gleam@string:join(Definitions@1, <<"\n\n"/utf8>>)}; {_, _} -> {ok, <<<
>/binary, (gleam@string:join(Definitions@1, <<"\n\n"/utf8>>))/binary>>} end end ). -spec generate_modules( list(definition()), binary(), fun((binary(), data()) -> code()), boolean(), fun((binary()) -> {ok, integer()} | {error, simplifile:file_error()}), fun((binary(), binary()) -> {ok, nil} | {error, simplifile:file_error()}) ) -> {ok, nil} | {error, list(error())}. generate_modules(Defs, Header, Print, Force, Get_mtime, Write) -> gleam@result:map( (gleam@result:'try'( begin _pipe = Defs, _pipe@1 = gather_by( _pipe, fun(File) -> erlang:element(5, File) end ), traverse_map( _pipe@1, fun(Pair) -> case enforce_unique_by( erlang:element(2, Pair), fun(File@1) -> erlang:element(6, File@1) end ) of {ok, Files} -> Files@1 = begin _pipe@2 = Files, gleam@list:sort( _pipe@2, fun(A, B) -> gleam@string:compare( erlang:element(6, A), erlang:element(6, B) ) end ) end, {ok, {erlang:element(1, Pair), Files@1}}; {error, Conflict} -> {error, [{duplicate_var_name, erlang:element( 2, (erlang:element(1, Conflict)) ), erlang:element( 2, (erlang:element(2, Conflict)) )}]} end end ) end, fun(Module_specs) -> traverse_map( Module_specs, fun(_use0) -> {Module_name, Defs@1} = _use0, Module_file = module_file(Module_name), Needs_update = case Get_mtime(Module_file) of {ok, Module_ts} -> gleam@list:any( Defs@1, fun(_use0@1) -> {definition, _, _, File_ts, _, _} = _use0@1, (File_ts =< 0) orelse (File_ts >= Module_ts) end ); {error, _} -> true end, gleam@bool:guard( not Force andalso not Needs_update, {error, []}, fun() -> gleam@result:'try'( generate_module_file(Defs@1, Header, Print), fun(Contents) -> 'try'( begin _pipe@3 = Write( Module_file, Contents ), gleam@result:map_error( _pipe@3, fun(E) -> {write_error, Module_file, E} end ) end, fun(_) -> {ok, nil} end ) end ) end ) end ) end )), fun(_) -> nil end ). -spec generate( list(definition()), binary(), fun((binary(), data()) -> code()), boolean() ) -> {ok, nil} | {error, list(error())}. generate(Defs, Header, Print, Force) -> generate_modules(Defs, Header, Print, Force, fun get_mtime/1, fun write/2).