diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6a2ded..9170378 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: with: otp-version: "25" rebar3-version: "3" - gleam-version: "0.29" + gleam-version: "0.30" - uses: denoland/setup-deno@v1 with: @@ -60,7 +60,7 @@ jobs: - if: ${{ !steps.cache-gleam.outputs.cache-hit }} run: gleam deps download - - run: echo "$PWD/priv" >> $GITHUB_PATH + - run: echo "$PWD/priv" >>$GITHUB_PATH - run: rad format --check diff --git a/CHANGELOG.md b/CHANGELOG.md index f30c02e..4acf38c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +- Rad now requires Gleam v0.30 or later. +- Rad can now be invoked via `gleam run --target=javascript -m rad` + (`--target=erlang` is currently unsupported). +- Rad no longer depends on `gleam_erlang` or `gleam_httpc`. + ## v0.3.0 - 2023-05-29 - Rad now requires Gleam v0.29 or later. diff --git a/README.md b/README.md index 8185ff2..740c7b3 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,12 @@ resides). ```shell $ ./build/packages/rad/priv/rad [flags] +$ # or +$ gleam run --target=javascript --module=rad -- [flags] ``` +_Note: `gleam run --target=erlang --module=rad ...` is currently unsupported!_ + For convenience when invoking `rad`, first perform one of the following operations in a manner consistent with your shell of choice. The goal is to get `priv/rad` or `priv/rad.ps1` somewhere in your `$PATH`; there are many ways to diff --git a/gleam.toml b/gleam.toml index c0d1dec..c6f32cf 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,8 +1,9 @@ name = "rad" -version = "0.3.0" +version = "0.4.0-dev" description = "A task runner for Gleam projects" licences = ["Apache-2.0"] target = "javascript" +gleam = "~> 0.30" [repository] repo = "rad" @@ -14,22 +15,17 @@ href = "https://gleam.run/" title = "Website" [dependencies] -gleam_erlang = "~> 0.19" -gleam_http = "~> 3.1" -gleam_httpc = "~> 2.0" -gleam_json = "~> 0.5" -gleam_stdlib = "~> 0.29" -glint = "~> 0.11.1" -shellout = "~> 1.3" +gleam_http = "~> 3.3" +gleam_json = "~> 0.6" +gleam_stdlib = "~> 0.30" +glint = "~> 0.11.4" +shellout = "~> 1.4" snag = "~> 0.2" -thoas = "~> 0.3" +thoas = "~> 0.4" tomerl = "~> 0.5" [dev-dependencies] -gleeunit = "~> 0.7" - -[javascript] -runtime = "deno" +gleeunit = "~> 0.11" [javascript.deno] allow_all = true @@ -42,7 +38,7 @@ with = "javascript" [[rad.formatters]] name = "erlang" check = ["erlfmt", "--check"] -run = ["erlfmt", "--write", "src/rad_ffi.erl"] +run = ["erlfmt", "--write", "src/rad_ffi.erl", "src/file_ffi.erl"] [[rad.formatters]] name = "javascript" diff --git a/manifest.toml b/manifest.toml index e5cbc37..4368d0d 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,31 +2,27 @@ # You typically do not need to edit this file packages = [ - { name = "gleam_bitwise", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_bitwise", source = "hex", outer_checksum = "6064699EFBABB1CA392DCB193D0E8B402FB042B4B46857B01E6875E643B57F54" }, - { name = "gleam_community_ansi", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_bitwise", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "6E4E0CF2B207C1A7FCD3C21AA43514D67BC7004F21F82045CDCCE6C727A14862" }, + { name = "gleam_bitwise", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_bitwise", source = "hex", outer_checksum = "E2A46EE42E5E9110DAD67E0F71E7358CBE54D5EC22C526DD48CBBA3223025792" }, + { name = "gleam_community_ansi", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_community_colour", "gleam_bitwise"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "6E4E0CF2B207C1A7FCD3C21AA43514D67BC7004F21F82045CDCCE6C727A14862" }, { name = "gleam_community_colour", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_bitwise"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "D27CE357ECB343929A8CEC3FBA0B499943A47F0EE1F589EE16AFC2DC21C61E5B" }, - { name = "gleam_erlang", version = "0.19.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "720D1E0A0CEBBD51C4AA88501D1D4FBFEF4AA7B3332C994691ED944767A52582" }, - { name = "gleam_http", version = "3.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "D034F5CE0639CD142CBA210B7D5D14236C284B0C5772A043D2E22128594573AE" }, - { name = "gleam_httpc", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "4B7DB74F13814DAC8A9F470DF41D3B8335FC9CD9C48BED0B8EFB012F22C1315E" }, - { name = "gleam_json", version = "0.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9A805C1E60FB9CD73AF3034EB464268A6B522D937FCD2DF92BD246F2F4B37930" }, - { name = "gleam_stdlib", version = "0.29.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "DB981FB670AAC6392C0694AF639C49ADF1C2E42664D5F90BBF573102667B8E53" }, - { name = "gleeunit", version = "0.10.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "ECEA2DE4BE6528D36AFE74F42A21CDF99966EC36D7F25DEB34D47DD0F7977BAF" }, - { name = "glint", version = "0.11.2", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_community_colour", "gleam_community_ansi", "snag"], otp_app = "glint", source = "hex", outer_checksum = "B3B95A640C86B5833033B4B74276C7B1D2AA04634D845B526C56A8BC52F5D15F" }, - { name = "shellout", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_erlang"], otp_app = "shellout", source = "hex", outer_checksum = "2F0BFBA7E8D18427525EE8611E832D59D72D261FFA533A3C0DD86987383A53A5" }, - { name = "snag", version = "0.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "35C63E478782C58236F1050297C2FDF9806A4DD55C6FAF0B6EC5E54BC119342D" }, + { name = "gleam_http", version = "3.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "FAE9AE3EB1CA90C2194615D20FFFD1E28B630E84DACA670B28D959B37BCBB02C" }, + { name = "gleam_json", version = "0.6.0", build_tools = ["gleam"], requirements = ["thoas", "gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C6CC5BEECA525117E97D0905013AB3F8836537455645DDDD10FE31A511B195EF" }, + { name = "gleam_stdlib", version = "0.30.2", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "8D8BF3790AA31176B1E1C0B517DD74C86DA8235CF3389EA02043EE4FD82AE3DC" }, + { name = "gleeunit", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "1397E5C4AC4108769EE979939AC39BF7870659C5AFB714630DEEEE16B8272AD5" }, + { name = "glint", version = "0.11.4", build_tools = ["gleam"], requirements = ["snag", "gleam_stdlib", "gleam_community_ansi", "gleam_community_colour"], otp_app = "glint", source = "hex", outer_checksum = "9508BF037E35F549C51F9F1D2CC4736CEA7F7A49E21CCA9B4540452C7D6CC4C5" }, + { name = "shellout", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "shellout", source = "hex", outer_checksum = "995564B69D40146B7A424CA21D32A68D668A882F88BDAD0EFA2C18C7EC412564" }, + { name = "snag", version = "0.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "8FD70D8FB3728E08AC425283BB509BB0F012BE1AE218424A597CDE001B0EE589" }, { name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" }, { name = "tomerl", version = "0.5.0", build_tools = ["rebar3"], requirements = [], otp_app = "tomerl", source = "hex", outer_checksum = "2A7FB62F9EBF0E75561B39255638BC2B805B437C86FEC538657E7C3B576979FA" }, ] [requirements] -gleam_erlang = "~> 0.19" -gleam_http = "~> 3.1" -gleam_httpc = "~> 2.0" -gleam_json = "~> 0.5" -gleam_stdlib = "~> 0.29" -gleeunit = "~> 0.7" -glint = "~> 0.11.1" -shellout = "~> 1.3" -snag = "~> 0.2" -thoas = "~> 0.3" -tomerl = "~> 0.5" +gleam_http = { version = "~> 3.3" } +gleam_json = { version = "~> 0.6" } +gleam_stdlib = { version = "~> 0.30" } +gleeunit = { version = "~> 0.11" } +glint = { version = "~> 0.11.4" } +shellout = { version = "~> 1.4" } +snag = { version = "~> 0.2" } +thoas = { version = "~> 0.4" } +tomerl = { version = "~> 0.5" } diff --git a/priv/gleam.main.mjs b/priv/gleam.main.mjs new file mode 100644 index 0000000..bf4c936 --- /dev/null +++ b/priv/gleam.main.mjs @@ -0,0 +1,2 @@ +import { main } from "./rad.mjs"; +main(); diff --git a/priv/rad b/priv/rad index cb9f53a..8f6fec7 100755 --- a/priv/rad +++ b/priv/rad @@ -3,11 +3,14 @@ set -e self="rad" -module="./build/dev/javascript/${self}/${self}.mjs" +build_dir="./build/dev/javascript/${self}" +main_module="${build_dir}/${self}.mjs" +run="gleam.main" +run_module="${build_dir}/${run}.mjs" fail() { message="${1}" - echo "\033[91m${message}\033[0m" 1>&2 + printf %b "\033[91m${message}\033[0m\n" >&2 exit 1 } @@ -24,7 +27,7 @@ fi snag="" for dependency in "gleam" "${runtime}"; do - if ! type "${dependency}" > /dev/null 2>&1; then + if ! type "${dependency}" >/dev/null 2>&1; then if test -n "${snag}"; then snag="${snag}\n" fi @@ -39,25 +42,28 @@ fi # # Redirect stdout to stderr, keeping stdout clear for the given task. # -if ! test -f "${module}"; then - gleam build --target=javascript 1>&2 +if ! test -f "${main_module}"; then + gleam build --target=javascript >&2 fi -if ! test -f "${module}"; then - fail "error: \`${module}\` not found; try \`gleam add --dev ${self}\`" +if ! test -f "${main_module}"; then + fail "error: \`${main_module}\` not found; try \`gleam add --dev ${self}\`" +fi +if ! test -f "${run_module}"; then + cp "./priv/${run}.mjs" "${run_module}" fi -script="import('${module}').then(module => module.main())" if test "${runtime}" = "deno"; then - exec deno \ - eval "${script}" \ + exec deno run \ + --allow-all \ --unstable \ - -- "${@}" + "${run_module}" \ + "${@}" else exec node \ --experimental-fetch \ --experimental-repl-await \ --no-warnings \ --title="${self}" \ - --eval="${script}" \ - -- "${@}" + "${run_module}" \ + "${@}" fi diff --git a/priv/rad.ps1 b/priv/rad.ps1 index 594fd95..818dd58 100755 --- a/priv/rad.ps1 +++ b/priv/rad.ps1 @@ -1,5 +1,8 @@ $self = "rad" -$module = "./build/dev/javascript/${self}/${self}.mjs" +$build_dir = "./build/dev/javascript/${self}" +$main_module = "${build_dir}/${self}.mjs" +$run = "gleam.main" +$run_module = "${build_dir}/${run}.mjs" function Fail { param($message) @@ -36,28 +39,34 @@ if ("${snag}" -ne "") { # # Redirect stdout to stderr, keeping stdout clear for the given task. # -if (-not (Test-Path -Type Leaf "${module}")) { +if (-not (Test-Path -Type Leaf "${main_module}")) { gleam build --target=javascript | Out-Host if ($LastExitCode -ne 0) { Exit 1 } } -if (-not (Test-Path -Type Leaf "${module}")) { - Fail "error: ``${module}`` not found; try ``gleam add --dev ${self}``" +if (-not (Test-Path -Type Leaf "${main_module}")) { + Fail "error: ``${main_module}`` not found; try ``gleam add --dev ${self}``" +} +if (-not (Test-Path -Type Leaf "${run_module}")) { + Copy-Item -Path "./priv/${run}.mjs" -Destination "${run_module}" + if ($LastExitCode -ne 0) { + Exit 1 + } } -$script = "import('${module}').then(module => module.main())" if ("${runtime}" -eq "deno") { - & deno ` - eval "${script}" ` + & deno run ` + --allow-all ` --unstable ` - -- @Args + "${run_module}" ` + @Args } else { & node ` --experimental-fetch ` --experimental-repl-await ` --no-warnings ` --title="${self}" ` - --eval="${script}" ` - -- @Args + "${run_module}" ` + @Args } diff --git a/src/file_ffi.erl b/src/file_ffi.erl new file mode 100644 index 0000000..5ba62fc --- /dev/null +++ b/src/file_ffi.erl @@ -0,0 +1,267 @@ +-module(file_ffi). +-export([ + atom_from_dynamic/1, + rescue/1, + atom_from_string/1, + get_line/1, + ensure_all_started/1, + sleep/1, + os_family/0, + sleep_forever/0, + read_file/1, + append_file/2, + write_file/2, + delete_file/1, + get_all_env/0, + get_env/1, + set_env/2, + unset_env/1, + delete_directory/1, + recursive_delete/1, + list_directory/1, + demonitor/1, + make_directory/1, + new_selector/0, + link/1, + insert_selector_handler/3, + select/1, select/2, + trap_exits/1, + map_selector/2, + merge_selector/2, + flush_messages/0, + file_info/1, + link_info/1, + priv_directory/1 +]). + +-define(is_posix_error(Error), + Error =:= eacces orelse Error =:= eagain orelse Error =:= ebadf orelse + Error =:= ebadmsg orelse Error =:= ebusy orelse Error =:= edeadlk orelse + Error =:= edeadlock orelse Error =:= edquot orelse Error =:= eexist orelse + Error =:= efault orelse Error =:= efbig orelse Error =:= eftype orelse + Error =:= eintr orelse Error =:= einval orelse Error =:= eio orelse + Error =:= eisdir orelse Error =:= eloop orelse Error =:= emfile orelse + Error =:= emlink orelse Error =:= emultihop orelse Error =:= enametoolong orelse + Error =:= enfile orelse Error =:= enobufs orelse Error =:= enodev orelse + Error =:= enolck orelse Error =:= enolink orelse Error =:= enoent orelse + Error =:= enomem orelse Error =:= enospc orelse Error =:= enosr orelse + Error =:= enostr orelse Error =:= enosys orelse Error =:= enotblk orelse + Error =:= enotdir orelse Error =:= enotsup orelse Error =:= enxio orelse + Error =:= eopnotsupp orelse Error =:= eoverflow orelse Error =:= eperm orelse + Error =:= epipe orelse Error =:= erange orelse Error =:= erofs orelse + Error =:= espipe orelse Error =:= esrch orelse Error =:= estale orelse + Error =:= etxtbsy orelse Error =:= exdev +). + +-spec atom_from_string(binary()) -> {ok, atom()} | {error, atom_not_loaded}. +atom_from_string(S) -> + try + {ok, binary_to_existing_atom(S)} + catch + error:badarg -> {error, atom_not_loaded} + end. + +atom_from_dynamic(Data) when is_atom(Data) -> + {ok, Data}; +atom_from_dynamic(Data) -> + {error, [{decode_error, <<"Atom">>, gleam@dynamic:classify(Data), []}]}. + +-spec get_line(io:prompt()) -> {ok, unicode:unicode_binary()} | {error, eof | no_data}. +get_line(Prompt) -> + case io:get_line(Prompt) of + eof -> {error, eof}; + {error, _} -> {error, no_data}; + Data when is_binary(Data) -> {ok, Data}; + Data when is_list(Data) -> {ok, unicode:characters_to_binary(Data)} + end. + +rescue(F) -> + try + {ok, F()} + catch + throw:X -> {error, {thrown, X}}; + error:X -> {error, {errored, X}}; + exit:X -> {error, {exited, X}} + end. + +ensure_all_started(Application) -> + case application:ensure_all_started(Application) of + {ok, _} = Ok -> + Ok; + {error, {ProblemApp, {"no such file or directory", _}}} -> + {error, {unknown_application, ProblemApp}} + end. + +sleep(Microseconds) -> + timer:sleep(Microseconds), + nil. + +sleep_forever() -> + timer:sleep(infinity), + nil. + +file_info_result(Result) -> + case Result of + {ok, + {file_info, Size, Type, Access, Atime, Mtime, Ctime, Mode, Links, MajorDevice, + MinorDevice, Inode, Uid, + Gid}} when Access =:= none -> + {ok, + {file_info, Size, Type, no_access, Atime, Mtime, Ctime, Mode, Links, MajorDevice, + MinorDevice, Inode, Uid, Gid}}; + {ok, _} -> + Result; + {error, Reason} when ?is_posix_error(Reason) -> + Result + end. + +file_info(Filename) -> + file_info_result(file:read_file_info(Filename, [{time, posix}])). + +link_info(Filename) -> + file_info_result(file:read_link_info(Filename, [{time, posix}])). + +posix_result(Result) -> + case Result of + ok -> {ok, nil}; + {ok, Value} -> {ok, Value}; + {error, Reason} when ?is_posix_error(Reason) -> {error, Reason} + end. + +read_file(Filename) -> + posix_result(file:read_file(Filename)). + +write_file(Contents, Filename) -> + posix_result(file:write_file(Filename, Contents)). + +append_file(Contents, Filename) -> + posix_result(file:write_file(Filename, Contents, [append])). + +delete_file(Filename) -> + posix_result(file:delete(Filename)). + +make_directory(Dir) -> + posix_result(file:make_dir(Dir)). + +list_directory(Dir) -> + case file:list_dir(Dir) of + {ok, Filenames} -> + {ok, [list_to_binary(Filename) || Filename <- Filenames]}; + {error, Reason} when ?is_posix_error(Reason) -> + {error, Reason} + end. + +delete_directory(Dir) -> + posix_result(file:del_dir(Dir)). + +recursive_delete(Dir) -> + posix_result(file:del_dir_r(Dir)). + +get_all_env() -> + BinVars = lists:map( + fun(VarString) -> + [VarName, VarVal] = string:split(VarString, "="), + {list_to_binary(VarName), list_to_binary(VarVal)} + end, + os:getenv() + ), + maps:from_list(BinVars). + +get_env(Name) -> + case os:getenv(binary_to_list(Name)) of + false -> {error, nil}; + Value -> {ok, list_to_binary(Value)} + end. + +set_env(Name, Value) -> + os:putenv(binary_to_list(Name), binary_to_list(Value)), + nil. + +unset_env(Name) -> + os:unsetenv(binary_to_list(Name)), + nil. + +os_family() -> + case os:type() of + {win32, nt} -> + windows_nt; + {unix, linux} -> + linux; + {unix, darwin} -> + darwin; + {unix, freebsd} -> + free_bsd; + {_, Other} -> + {other, atom_to_binary(Other, utf8)} + end. + +new_selector() -> + {selector, #{}}. + +map_selector({selector, Handlers}, Fn) -> + MappedHandlers = maps:map( + fun(_Tag, Handler) -> + fun(Message) -> Fn(Handler(Message)) end + end, + Handlers + ), + {selector, MappedHandlers}. + +merge_selector({selector, HandlersA}, {selector, HandlersB}) -> + {selector, maps:merge(HandlersA, HandlersB)}. + +insert_selector_handler({selector, Handlers}, Tag, Fn) -> + {selector, Handlers#{Tag => Fn}}. + +select(Selector) -> + {ok, Message} = select(Selector, infinity), + Message. + +select({selector, Handlers}, Timeout) -> + AnythingHandler = maps:get(anything, Handlers, undefined), + receive + % Monitored process down messages. + % This is special cased so we can selectively receive based on the + % reference as well as the record tag. + {'DOWN', Ref, process, Pid, Reason} when is_map_key(Ref, Handlers) -> + Fn = maps:get(Ref, Handlers), + {ok, Fn({process_down, Pid, Reason})}; + Msg when is_map_key({element(1, Msg), tuple_size(Msg)}, Handlers) -> + Fn = maps:get({element(1, Msg), tuple_size(Msg)}, Handlers), + {ok, Fn(Msg)}; + Msg when AnythingHandler =/= undefined -> + {ok, AnythingHandler(Msg)} + after Timeout -> + {error, nil} + end. + +demonitor({_, Reference}) -> + erlang:demonitor(Reference, [flush]). + +link(Pid) -> + try + erlang:link(Pid) + catch + error:_ -> false + end. + +trap_exits(ShouldTrap) -> + erlang:process_flag(trap_exit, ShouldTrap), + nil. + +flush_messages() -> + receive + _Message -> flush_messages() + after 0 -> nil + end. + +priv_directory(Name) -> + try erlang:binary_to_existing_atom(Name) of + Atom -> + case code:priv_dir(Atom) of + {error, _} -> {error, nil}; + Path -> {ok, unicode:characters_to_binary(Path)} + end + catch + error:badarg -> {error, nil} + end. diff --git a/src/rad.gleam b/src/rad.gleam index 355f57b..daf8514 100644 --- a/src/rad.gleam +++ b/src/rad.gleam @@ -118,7 +118,7 @@ type Scope { fn arguments(scope: Scope) -> List(String) { let filter = string.starts_with(_, "--with=") let arguments = - shellout.arguments() + start_arguments() |> list.filter(for: case scope { Global -> filter Normal -> @@ -153,6 +153,11 @@ fn arguments(scope: Scope) -> List(String) { } } +@external(javascript, "./rad_ffi.mjs", "start_arguments") +fn start_arguments() -> List(String) { + shellout.arguments() +} + fn end_task(result: Result(String, Snag)) -> Nil { case result { Ok(output) -> { @@ -188,9 +193,9 @@ fn rad_run(package: String, with: String, fun: fn() -> Nil) -> Nil { [ ["-noshell"], ["-eval", package <> "@@main:run(rad)"], - ["-extra", ..shellout.arguments()], + ["-extra", ..start_arguments()], ] - |> list.flatten + |> list.concat |> util.erlang_run(opt: [LetBeStderr, LetBeStdout]) } |> end_task @@ -199,8 +204,8 @@ fn rad_run(package: String, with: String, fun: fn() -> Nil) -> Nil { let script = "import('./build/dev/javascript/rad/rad.mjs').then(module => module.main())" util.javascript_run( - deno: ["eval", script, "--unstable", "--", ..shellout.arguments()], - or: ["--eval=" <> script, "--title=rad", "--", ..shellout.arguments()], + deno: ["eval", script, "--unstable", "--", ..start_arguments()], + or: ["--eval=" <> script, "--title=rad", "--", ..start_arguments()], opt: [LetBeStderr, LetBeStdout], ) |> end_task @@ -219,21 +224,19 @@ fn is_running(target: String) -> Bool { |> do_is_running } -if erlang { - fn do_is_running(target: String) -> Bool { - case target { - "erlang" -> True - _else -> False - } +@target(erlang) +fn do_is_running(target: String) -> Bool { + case target { + "erlang" -> True + _else -> False } } -if javascript { - fn do_is_running(target: String) -> Bool { - case target { - "javascript" -> True - _else -> False - } +@target(javascript) +fn do_is_running(target: String) -> Bool { + case target { + "javascript" -> True + _else -> False } } @@ -248,35 +251,35 @@ fn gleam_run(package: String, module: String) -> Nil { do_gleam_run(package, module) } -if erlang { - fn do_gleam_run(package: String, module: String) -> Nil { - let _result = - package - |> maybe_run(module) - |> result.lazy_or(fn() { - let result = gleam_build("erlang") - io.println("") - result - |> result.try(apply: fn(_result) { maybe_run(package, module) }) - }) - |> result.map_error(with: fn(snag) { - let _nil = - snag - |> util.snag_pretty_print - |> io.println - erlang_gleam_run(package, default_workbook) - }) - Nil - } +@target(erlang) +fn do_gleam_run(package: String, module: String) -> Nil { + let _result = + package + |> maybe_run(module) + |> result.lazy_or(fn() { + let result = gleam_build("erlang") + io.println("") + result + |> result.try(apply: fn(_result) { maybe_run(package, module) }) + }) + |> result.map_error(with: fn(snag) { + let _nil = + snag + |> util.snag_pretty_print + |> io.println + erlang_gleam_run(package, default_workbook) + }) + Nil +} - external fn maybe_run(String, String) -> Result(String, Snag) = - "rad_ffi" "maybe_run" +@target(javascript) +@external(javascript, "./rad_ffi.mjs", "gleam_run") +fn do_gleam_run(package: String, module: String) -> Nil - external fn erlang_gleam_run(String, String) -> dynamic.Dynamic = - "rad_ffi" "gleam_run" -} +@target(erlang) +@external(erlang, "rad_ffi", "maybe_run") +fn maybe_run(package: String, module: String) -> Result(String, Snag) -if javascript { - external fn do_gleam_run(String, String) -> Nil = - "./rad_ffi.mjs" "gleam_run" -} +@target(erlang) +@external(erlang, "rad_ffi", "gleam_run") +fn erlang_gleam_run(package: String, module: String) -> dynamic.Dynamic diff --git a/src/rad/internal/file.gleam b/src/rad/internal/file.gleam new file mode 100644 index 0000000..1fff0f4 --- /dev/null +++ b/src/rad/internal/file.gleam @@ -0,0 +1,723 @@ +//// Working with files on the filesystem. +//// +//// The functions included in this module are for high-level concepts such as +//// reading and writing. + +import gleam/bit_string +import gleam/result + +/// Reason represents all of the reasons that Erlang surfaces of why a file +/// system operation could fail. Most of these reasons are POSIX errors, which +/// come from the operating system and start with `E`. Others have been added to +/// represent other issues that may arise. +pub type Reason { + /// Permission denied. + Eacces + /// Resource temporarily unavailable. + Eagain + /// Bad file number + Ebadf + /// Bad message. + Ebadmsg + /// File busy. + Ebusy + /// Resource deadlock avoided. + Edeadlk + /// On most architectures, same as `Edeadlk`. On some architectures, it + /// means "File locking deadlock error." + Edeadlock + /// Disk quota exceeded. + Edquot + /// File already exists. + Eexist + /// Bad address in system call argument. + Efault + /// File too large. + Efbig + /// Inappropriate file type or format. Usually caused by trying to set the + /// "sticky bit" on a regular file (not a directory). + Eftype + /// Interrupted system call. + Eintr + /// Invalid argument. + Einval + /// I/O error. + Eio + /// Illegal operation on a directory. + Eisdir + /// Too many levels of symbolic links. + Eloop + /// Too many open files. + Emfile + /// Too many links. + Emlink + /// Multihop attempted. + Emultihop + /// Filename too long + Enametoolong + /// File table overflow + Enfile + /// No buffer space available. + Enobufs + /// No such device. + Enodev + /// No locks available. + Enolck + /// Link has been severed. + Enolink + /// No such file or directory. + Enoent + /// Not enough memory. + Enomem + /// No space left on device. + Enospc + /// No STREAM resources. + Enosr + /// Not a STREAM. + Enostr + /// Function not implemented. + Enosys + /// Block device required. + Enotblk + /// Not a directory. + Enotdir + /// Operation not supported. + Enotsup + /// No such device or address. + Enxio + /// Operation not supported on socket. + Eopnotsupp + /// Value too large to be stored in data type. + Eoverflow + /// Not owner. + Eperm + /// Broken pipe. + Epipe + /// Result too large. + Erange + /// Read-only file system. + Erofs + /// Invalid seek. + Espipe + /// No such process. + Esrch + /// Stale remote file handle. + Estale + /// Text file busy. + Etxtbsy + /// Cross-domain link. + Exdev + /// File was requested to be read as UTF-8, but is not UTF-8 encoded. + NotUtf8 +} + +/// The type of file found by `file_info` or `link_info`. +/// +pub type FileType { + Device + Directory + Other + Regular + Symlink +} + +/// The read/write permissions a user can have for a file. +/// +pub type Access { + NoAccess + Read + ReadWrite + Write +} + +/// Meta information for a file. +/// +/// Timestamps are in seconds before or after the Unix time epoch, +/// `1970-01-01 00:00:00 UTC`. +/// +pub type FileInfo { + FileInfo( + /// File size in bytes. + /// + size: Int, + /// `Regular`, `Directory`, `Symlink`, `Device`, or `Other`. + /// + file_type: FileType, + /// `ReadWrite`, `Read`, `Write`, or `NoAccess`. + /// + access: Access, + /// Timestamp of most recent access. + /// + atime: Int, + /// Timestamp of most recent modification. + /// + mtime: Int, + /// Timestamp of most recent change (or file creation, depending on + /// operating system). + /// + ctime: Int, + /// File permissions encoded as a sum of bit values, including but not + /// limited to: + /// + /// Owner read, write, execute. + /// + /// `0o400`, `0o200`, `0o100` + /// + /// Group read, write, execute. + /// + /// `0o40`, `0o20`, `0o10` + /// + /// Other read, write, execute. + /// + /// `0o4`, `0o2`, `0o1` + /// + /// Set user ID, group ID on execution. + /// + /// `0x800`, `0x400` + /// + mode: Int, + /// Total links to a file (always `1` for file systems without links). + /// + links: Int, + /// The file system where a file is located (`0` for drive `A:` on Windows, + /// `1` for `B:`, etc.). + /// + major_device: Int, + /// Character device (or `0` on non-Unix systems). + /// + minor_device: Int, + /// The `inode` number for a file (always `0` on non-Unix file systems). + /// + inode: Int, + /// The owner of a file (always `0` on non-Unix file systems). + /// + user_id: Int, + /// The group id of a file (always `0` on non-Unix file systems). + /// + group_id: Int, + ) +} + +/// Results in `FileInfo` about the given `path` on success, otherwise a +/// `Reason` for failure. +/// +/// When `path` refers to a symlink, the result pertains to the link's target. +/// To get `FileInfo` about a symlink itself, use `link_info`. +/// +/// ## Examples +/// +/// ```gleam +/// > file_info("gleam.toml") +/// Ok(FileInfo( +/// size: 430, +/// file_type: Regular, +/// access: ReadWrite, +/// atime: 1680580321, +/// mtime: 1680580272, +/// ctime: 1680580272, +/// mode: 33188, +/// links: 1, +/// major_device: 64, +/// minor_device: 0, +/// inode: 469028, +/// user_id: 1000, +/// group_id: 1000, +/// )) +/// +/// > file_info("/root") +/// Ok(FileInfo( +/// size: 16, +/// file_type: Directory, +/// access: Read, +/// atime: 1677789967, +/// mtime: 1664561240, +/// ctime: 1664561240, +/// mode: 16877, +/// links: 11, +/// major_device: 54, +/// minor_device: 0, +/// inode: 34, +/// user_id: 0, +/// group_id: 0, +/// )) +/// +/// > file_info("./build/dev/erlang/rad/priv") +/// Ok(FileInfo( +/// size: 140, +/// file_type: Directory, +/// access: ReadWrite, +/// atime: 1680580321, +/// mtime: 1680580272, +/// ctime: 1680580272, +/// mode: 33188, +/// links: 1, +/// major_device: 64, +/// minor_device: 0, +/// inode: 469028, +/// user_id: 1000, +/// group_id: 1000, +/// )) +/// +/// > file_info("/does_not_exist") +/// Error(Enoent) +/// +/// > file_info("/root/.local/maybe_exists") +/// Error(Eacces) +/// ``` +/// +@external(erlang, "file_ffi", "file_info") +@external(javascript, "./rad_ffi.mjs", "no_fun") +pub fn file_info(a: String) -> Result(FileInfo, Reason) + +/// Results in `FileInfo` about the given `path` on success, otherwise a +/// `Reason` for failure. +/// +/// When `path` refers to a symlink, the result pertains to the link itself. +/// To get `FileInfo` about a symlink's target, use `file_info`. +/// +/// ## Examples +/// +/// ```gleam +/// > link_info("gleam.toml") +/// Ok(FileInfo( +/// size: 430, +/// file_type: Regular, +/// access: ReadWrite, +/// atime: 1680580321, +/// mtime: 1680580272, +/// ctime: 1680580272, +/// mode: 33188, +/// links: 1, +/// major_device: 64, +/// minor_device: 0, +/// inode: 469028, +/// user_id: 1000, +/// group_id: 1000, +/// )) +/// +/// > link_info("/root") +/// Ok(FileInfo( +/// size: 16, +/// file_type: Directory, +/// access: Read, +/// atime: 1677789967, +/// mtime: 1664561240, +/// ctime: 1664561240, +/// mode: 16877, +/// links: 11, +/// major_device: 54, +/// minor_device: 0, +/// inode: 34, +/// user_id: 0, +/// group_id: 0, +/// )) +/// +/// > link_info("./build/dev/erlang/rad/priv") +/// Ok(FileInfo( +/// size: 41, +/// file_type: Symlink, +/// access: ReadWrite, +/// atime: 1680581150, +/// mtime: 1680581150, +/// ctime: 1680581150, +/// mode: 41471, +/// links: 1, +/// major_device: 64, +/// minor_device: 0, +/// inode: 471587, +/// user_id: 1000, +/// group_id: 1000, +/// )) +/// +/// > link_info("/does_not_exist") +/// Error(Enoent) +/// +/// > link_info("/root/.local/maybe_exists") +/// Error(Eacces) +/// ``` +/// +@external(erlang, "file_ffi", "link_info") +@external(javascript, "./rad_ffi.mjs", "no_fun") +pub fn link_info(a: String) -> Result(FileInfo, Reason) + +/// Results in a `Bool` on success that indicates whether the given `path` has +/// a `Directory` `FileType`, otherwise a `Reason` for failure. +/// +/// When `path` refers to a symlink, the result pertains to the link's target. +/// +/// ## Examples +/// +/// ```gleam +/// > is_directory("/tmp") +/// Ok(True) +/// +/// > is_directory("resume.pdf") +/// Ok(False) +/// +/// > is_directory("/does_not_exist") +/// Error(Enoent) +/// ``` +/// +pub fn is_directory(path: String) -> Result(Bool, Reason) { + use FileInfo(file_type: file_type, ..) <- result.map(over: file_info(path)) + file_type == Directory +} + +/// Results in a `Bool` on success that indicates whether the given `path` has +/// a `Regular` `FileType`, otherwise a `Reason` for failure. +/// +/// When `path` refers to a symlink, the result pertains to the link's target. +/// +/// ## Examples +/// +/// ```gleam +/// > is_regular("resume.pdf") +/// Ok(True) +/// +/// > is_regular("/tmp") +/// Ok(False) +/// +/// > is_regular("/does_not_exist.txt") +/// Error(Enoent) +/// ``` +/// +pub fn is_regular(path: String) -> Result(Bool, Reason) { + use FileInfo(file_type: file_type, ..) <- result.map(over: file_info(path)) + file_type == Regular +} + +/// Results in a `Bool` on success that indicates whether the given `path` +/// exists, otherwise a `Reason` for failure. +/// +/// When `path` refers to a symlink, the result pertains to the link's target. +/// To find whether a symlink itself exists, use `link_exists`. +/// +/// ## Examples +/// +/// ```gleam +/// > file_exists("resume.pdf") +/// Ok(True) +/// +/// > file_exists("/tmp") +/// Ok(True) +/// +/// > file_exists("/does_not_exist") +/// Ok(False) +/// +/// > file_exists("/root/.local/maybe_exists") +/// Error(Eacces) +/// ``` +/// +pub fn file_exists(path: String) -> Result(Bool, Reason) { + let result = + path + |> file_info + |> result.replace(True) + case result { + Error(Enoent) -> Ok(False) + _ -> result + } +} + +/// Results in a `Bool` on success that indicates whether the given `path` +/// exists, otherwise a `Reason` for failure. +/// +/// When `path` refers to a symlink, the result pertains to the link itself. +/// To find whether a symlink's target exists, use `file_exists`. +/// +/// ## Examples +/// +/// ```gleam +/// > link_exists("resume.pdf") +/// Ok(True) +/// +/// > link_exists("/tmp") +/// Ok(True) +/// +/// > link_exists("/does_not_exist") +/// Ok(False) +/// +/// > link_exists("/root/.local/maybe_exists") +/// Error(Eacces) +/// ``` +/// +pub fn link_exists(path: String) -> Result(Bool, Reason) { + let result = + path + |> link_info + |> result.replace(True) + case result { + Error(Enoent) -> Ok(False) + _ -> result + } +} + +/// Tries to create a directory. Missing parent directories are not created. +/// +/// Returns a Result of nil if the directory is created or Reason if the +/// operation failed. +/// +/// ## Examples +/// +/// ```gleam +/// > make_directory("/tmp/foo") +/// Ok(Nil) +/// +/// > make_directory("relative_directory") +/// Ok(Nil) +/// +/// > make_directory("/tmp/missing_intermediate_directory/foo") +/// Error(Enoent) +/// ``` +/// +@external(erlang, "file_ffi", "make_directory") +@external(javascript, "./rad_ffi.mjs", "no_fun") +pub fn make_directory(a: String) -> Result(Nil, Reason) + +/// Lists all files in a directory, except files with +/// [raw filenames](https://www.erlang.org/doc/apps/stdlib/unicode_usage.html#notes-about-raw-filenames). +/// +/// Returns a Result containing the list of filenames in the directory, or Reason +/// if the operation failed. +/// +/// ## Examples +/// +/// ```gleam +/// > list_directory("/tmp") +/// Ok(["FB01293B-8597-4359-80D5-130140A0C0DE","AlTest2.out"]) +/// +/// > list_directory("resume.docx") +/// Error(Enotdir) +/// ``` +/// +@external(erlang, "file_ffi", "list_directory") +@external(javascript, "./rad_ffi.mjs", "no_fun") +pub fn list_directory(a: String) -> Result(List(String), Reason) + +/// Deletes a directory. +/// +/// The directory must be empty before it can be deleted. Returns a nil Success +/// or Reason if the operation failed. +/// +/// ## Examples +/// +/// ```gleam +/// > delete_directory("foo") +/// Ok(Nil) +/// +/// > delete_directory("does_not_exist/") +/// Error(Enoent) +/// ``` +/// +@external(erlang, "file_ffi", "delete_directory") +@external(javascript, "./rad_ffi.mjs", "no_fun") +pub fn delete_directory(a: String) -> Result(Nil, Reason) + +/// Deletes a file or directory recursively. +/// +/// Returns a nil Success or Reason if the operation failed. +/// +/// ## Examples +/// +/// ```gleam +/// > recursive_delete("foo") +/// Ok(Nil) +/// +/// > recursive_delete("/bar") +/// Ok(Nil) +/// +/// > recursive_delete("does_not_exist/") +/// Error(Enoent) +/// ``` +/// +@external(erlang, "file_ffi", "recursive_delete") +@external(javascript, "./rad_ffi.mjs", "no_fun") +pub fn recursive_delete(a: String) -> Result(Nil, Reason) + +/// Read the contents of the given file as a String +/// +/// Assumes the file is UTF-8 encoded. Returns a Result containing the file's +/// contents as a String if the operation was successful, or Reason if the file +/// operation failed. If the file is not UTF-8 encoded, the `NotUTF8` variant +/// will be returned. +/// +/// ## Examples +/// +/// ```gleam +/// > read("example.txt") +/// Ok("Hello, World!") +/// +/// > read(from: "example.txt") +/// Ok("Hello, World!") +/// +/// > read("does_not_exist.txt") +/// Error(Enoent) +/// +/// > read("cat.gif") +/// Error(NotUTF8) +/// ``` +/// +pub fn read(from path: String) -> Result(String, Reason) { + path + |> do_read_bits() + |> result.then(fn(content) { + case bit_string.to_string(content) { + Ok(string) -> Ok(string) + Error(Nil) -> Error(NotUtf8) + } + }) +} + +/// Read the contents of the given file as a BitString +/// +/// Returns a Result containing the file's contents as a BitString if the +/// operation was successful, or Reason if the operation failed. +/// +/// ## Examples +/// +/// ```gleam +/// > read_bits("example.txt") +/// Ok(<<"Hello, World!">>) +/// +/// > read_bits(from: "cat.gif") +/// Ok(<<71,73,70,56,57,97,1,0,1,0,0,0,0,59>>) +/// +/// > read_bits("does_not_exist.txt") +/// Error(Enoent) +/// ``` +/// +pub fn read_bits(from path: String) -> Result(BitString, Reason) { + do_read_bits(path) +} + +@external(erlang, "file_ffi", "read_file") +@external(javascript, "./rad_ffi.mjs", "no_fun") +fn do_read_bits(a: path) -> Result(BitString, Reason) + +/// Write the given String contents to a file of the given name. +/// +/// Returns a Result with Nil if the operation was successful or a Reason +/// otherwise. +/// +/// ## Examples +/// +/// ```gleam +/// > write("Hello, World!", "file.txt") +/// Ok(Nil) +/// +/// > write(to: "file.txt", contents: "Hello, World!") +/// Ok(Nil) +/// +/// > write("Hello, World!", "does_not_exist/file.txt") +/// Error(Enoent) +/// ``` +/// +pub fn write(contents contents: String, to path: String) -> Result(Nil, Reason) { + contents + |> bit_string.from_string + |> do_write_bits(path) +} + +/// Write the given BitString contents to a file of the given name. +/// +/// Returns a Result with Nil if the operation was successful or a Reason +/// otherwise. +/// +/// ## Examples +/// +/// ```gleam +/// > write_bits(<<71,73,70,56,57,97,1,0,1,0,0,0,0,59>>, "cat.gif") +/// Ok(Nil) +/// +/// > write_bits(to: "cat.gif", contents: <<71,73,70,56,57,97,1,0,1,0,0,0,0,59>>) +/// Ok(Nil) +/// +/// > write_bits(<<71,73,70,56,57,97,1,0,1,0,0,0,0,59>>, "does_not_exist/cat.gif") +/// Error(Enoent) +/// ``` +/// +pub fn write_bits( + contents contents: BitString, + to path: String, +) -> Result(Nil, Reason) { + do_write_bits(contents, path) +} + +@external(erlang, "file_ffi", "write_file") +@external(javascript, "./rad_ffi.mjs", "no_fun") +fn do_write_bits(a: BitString, b: String) -> Result(Nil, Reason) + +/// Append the given String contents to a file of the given name. +/// +/// Returns a Result with Nil if the operation was successful or a Reason +/// otherwise. +/// +/// ## Examples +/// +/// ```gleam +/// > append("Hello, World!", "file.txt") +/// Ok(Nil) +/// +/// > append(to: "file.txt", contents: "Hello, World!") +/// Ok(Nil) +/// +/// > append("Hello, World!", "does_not_exist/file.txt") +/// Error(Enoent) +/// ``` +/// +pub fn append(contents contents: String, to path: String) -> Result(Nil, Reason) { + contents + |> bit_string.from_string + |> do_append_bits(path) +} + +/// Append the given BitString contents to a file of the given name. +/// +/// Returns a Result with Nil if the operation was successful or a Reason +/// otherwise. +/// +/// ## Examples +/// +/// ```gleam +/// > append_bits(<<71,73,70,56,57,97,1,0,1,0,0,0,0,59>>, "cat.gif") +/// Ok(Nil) +/// +/// > append_bits(to: "cat.gif", contents: <<71,73,70,56,57,97,1,0,1,0,0,0,0,59>>) +/// Ok(Nil) +/// +/// > append_bits(<<71,73,70,56,57,97,1,0,1,0,0,0,0,59>>, "does_not_exist/cat.gif") +/// Error(Enoent) +/// ``` +/// +pub fn append_bits( + contents contents: BitString, + to path: String, +) -> Result(Nil, Reason) { + do_append_bits(contents, path) +} + +@external(erlang, "file_ffi", "append_file") +@external(javascript, "./rad_ffi.mjs", "no_fun") +fn do_append_bits( + contents contents: BitString, + path path: String, +) -> Result(Nil, Reason) + +/// Delete the given file. +/// +/// Returns a Result with Nil if the operation was successful or a Reason +/// otherwise. +/// +/// ## Examples +/// +/// ```gleam +/// > delete("file.txt") +/// Ok(Nil) +/// +/// > delete("does_not_exist.txt") +/// Error(Enoent) +/// ``` +/// +@external(erlang, "file_ffi", "delete_file") +@external(javascript, "./rad_ffi.mjs", "no_fun") +pub fn delete(a: String) -> Result(Nil, Reason) diff --git a/src/rad/internal/httpc.gleam b/src/rad/internal/httpc.gleam new file mode 100644 index 0000000..95c7aa4 --- /dev/null +++ b/src/rad/internal/httpc.gleam @@ -0,0 +1,141 @@ +import gleam/dynamic.{Dynamic} +import gleam/http.{Method} +import gleam/http/response.{Response} +import gleam/http/request.{Request} +import gleam/bit_string +import gleam/result +import gleam/list +import gleam/uri + +/// Results in `Nil` if the `inets` application is successfully started, or +/// `Nil` on error. +/// +/// `inets` must be started before sending any `Request`. +/// +pub fn ensure_started() -> Result(Nil, Nil) { + "inets" + |> ensure_all_started + |> result.replace(Nil) + |> result.replace_error(Nil) +} + +type Atom + +fn ensure_all_started(application: String) -> Result(List(Atom), Dynamic) { + application + |> string_to_atom + |> erlang_ensure_all_started +} + +@external(erlang, "erlang", "binary_to_atom") +@external(javascript, "./rad_ffi.mjs", "no_fun") +fn string_to_atom(a: String) -> Atom + +@external(erlang, "application", "ensure_all_started") +@external(javascript, "./rad_ffi.mjs", "no_fun") +fn erlang_ensure_all_started( + application application: Atom, +) -> Result(List(Atom), Dynamic) + +type Charlist + +@external(erlang, "erlang", "binary_to_list") +@external(javascript, "./rad_ffi.mjs", "no_fun") +fn binary_to_list(a: String) -> Charlist + +@external(erlang, "erlang", "list_to_binary") +@external(javascript, "./rad_ffi.mjs", "no_fun") +fn list_to_binary(a: Charlist) -> String + +type ErlHttpOption + +type BodyFormat { + Binary +} + +type ErlOption { + BodyFormat(BodyFormat) +} + +@external(erlang, "httpc", "request") +@external(javascript, "./rad_ffi.mjs", "no_fun") +fn erl_request( + a: Method, + b: #(Charlist, List(#(Charlist, Charlist)), Charlist, BitString), + c: List(ErlHttpOption), + d: List(ErlOption), +) -> Result( + #(#(Charlist, Int, Charlist), List(#(Charlist, Charlist)), BitString), + Dynamic, +) + +@external(erlang, "httpc", "request") +@external(javascript, "./rad_ffi.mjs", "no_fun") +fn erl_request_no_body( + a: Method, + b: #(Charlist, List(#(Charlist, Charlist))), + c: List(ErlHttpOption), + d: List(ErlOption), +) -> Result( + #(#(Charlist, Int, Charlist), List(#(Charlist, Charlist)), BitString), + Dynamic, +) + +fn charlist_header(header: #(String, String)) -> #(Charlist, Charlist) { + let #(k, v) = header + #(binary_to_list(k), binary_to_list(v)) +} + +fn string_header(header: #(Charlist, Charlist)) -> #(String, String) { + let #(k, v) = header + #(list_to_binary(k), list_to_binary(v)) +} + +// TODO: test +// TODO: refine error type +pub fn send_bits( + req: Request(BitString), +) -> Result(Response(BitString), Dynamic) { + let erl_url = + req + |> request.to_uri + |> uri.to_string + |> binary_to_list + let erl_headers = list.map(req.headers, charlist_header) + let erl_http_options = [] + let erl_options = [BodyFormat(Binary)] + + use response <- result.then(case req.method { + http.Options | http.Head | http.Get -> { + let erl_req = #(erl_url, erl_headers) + erl_request_no_body(req.method, erl_req, erl_http_options, erl_options) + } + _ -> { + let erl_content_type = + req + |> request.get_header("content-type") + |> result.unwrap("application/octet-stream") + |> binary_to_list + let erl_req = #(erl_url, erl_headers, erl_content_type, req.body) + erl_request(req.method, erl_req, erl_http_options, erl_options) + } + }) + + let #(#(_version, status, _status), headers, resp_body) = response + Ok(Response(status, list.map(headers, string_header), resp_body)) +} + +// TODO: test +// TODO: refine error type +pub fn send(req: Request(String)) -> Result(Response(String), Dynamic) { + use resp <- result.then( + req + |> request.map(bit_string.from_string) + |> send_bits, + ) + + case bit_string.to_string(resp.body) { + Ok(body) -> Ok(response.set_body(resp, body)) + Error(_) -> Error(dynamic.from("Response body was not valid UTF-8")) + } +} diff --git a/src/rad/task.gleam b/src/rad/task.gleam index ce22059..0349396 100644 --- a/src/rad/task.gleam +++ b/src/rad/task.gleam @@ -477,7 +477,7 @@ pub fn packages() -> Iterable(a) { [ self, ..[dependencies, dev_dependencies] - |> list.flatten + |> list.concat |> list.map(with: pair.first) |> list.sort(by: string.compare) ] @@ -545,7 +545,7 @@ pub fn basic(command: List(String)) -> Runner(Result) { fn(input: CommandInput, _task) { let [command, ..args] = command [args, util.relay_flags(input.flags), input.args] - |> list.flatten + |> list.concat |> shellout.command(run: command, in: ".", opt: util.quiet_or_spawn(input)) |> result.replace_error(snag.new("failed running task")) } @@ -560,7 +560,7 @@ pub fn basic(command: List(String)) -> Runner(Result) { pub fn gleam(arguments: List(String)) -> Runner(Result) { fn(input: CommandInput, _task) { [arguments, util.relay_flags(input.flags), input.args] - |> list.flatten + |> list.concat |> shellout.command(run: "gleam", in: ".", opt: util.quiet_or_spawn(input)) |> result.replace("") |> result.replace_error(snag.new("failed running task")) diff --git a/src/rad/toml.gleam b/src/rad/toml.gleam index 91b7673..e0d9d8f 100644 --- a/src/rad/toml.gleam +++ b/src/rad/toml.gleam @@ -10,15 +10,14 @@ import gleam/list import gleam/result import gleam/string import snag.{Snag} - -if erlang { - import gleam/function - import gleam/map -} +@target(erlang) +import gleam/function +@target(erlang) +import gleam/map /// A TOML [table](https://toml.io/en/v1.0.0#table) of dynamic data. /// -pub external type Toml +pub type Toml /// Results in typed Gleam data decoded from the given [`Toml`](#Toml)'s /// `key_path` on success, or a [`Snag`](https://hexdocs.pm/snag/snag.html#Snag) @@ -43,15 +42,9 @@ pub fn decode( |> result.map_error(with: decode_errors_to_snag(_, key_path)) } -if erlang { - external fn do_toml_get(Toml, List(String)) -> Result(Dynamic, Nil) = - "rad_ffi" "toml_get" -} - -if javascript { - external fn do_toml_get(Toml, List(String)) -> Result(Dynamic, Nil) = - "../rad_ffi.mjs" "toml_get" -} +@external(erlang, "rad_ffi", "toml_get") +@external(javascript, "../rad_ffi.mjs", "toml_get") +fn do_toml_get(toml: Toml, key_path: List(String)) -> Result(Dynamic, Nil) /// Results in a list of every key-value pair for which the value can be /// successfully decoded from the given [`Toml`](#Toml)'s `key_path` using the @@ -67,59 +60,58 @@ pub fn decode_every( do_decode_every(toml, key_path, decoder) } -if erlang { - fn do_decode_every( - toml: Toml, - key_path: List(String), - decoder: Decoder(a), - ) -> Result(List(#(String, a)), Snag) { - use map <- result.try( - key_path - |> decode( - from: toml, - expect: dynamic.from - |> function.compose(dynamic.map(of: dynamic.string, to: dynamic.dynamic)), - ), - ) +@target(erlang) +fn do_decode_every( + toml: Toml, + key_path: List(String), + decoder: Decoder(a), +) -> Result(List(#(String, a)), Snag) { + use map <- result.try( + key_path + |> decode( + from: toml, + expect: dynamic.from + |> function.compose(dynamic.map(of: dynamic.string, to: dynamic.dynamic)), + ), + ) - map - |> map.to_list - |> list.filter_map(with: fn(tuple) { - let key = "" - let assert Ok(toml) = - [#(key, tuple)] - |> map.from_list - |> dynamic.from - |> from_dynamic - [key] - |> decode( - from: toml, - expect: dynamic.tuple2(first: dynamic.string, second: decoder), - ) - }) - |> Ok - } + map + |> map.to_list + |> list.filter_map(with: fn(tuple) { + let key = "" + let assert Ok(toml) = + [#(key, tuple)] + |> map.from_list + |> dynamic.from + |> from_dynamic + [key] + |> decode( + from: toml, + expect: dynamic.tuple2(first: dynamic.string, second: decoder), + ) + }) + |> Ok } -if javascript { - fn do_decode_every( - toml: Toml, - key_path: List(String), - decoder: Decoder(a), - ) -> Result(List(#(String, a)), Snag) { - toml - |> javascript_decode_every(key_path, decoder) - |> result.map_error(with: decode_errors_to_snag(_, key_path)) - } - - external fn javascript_decode_every( - Toml, - List(String), - Decoder(a), - ) -> Result(List(#(String, a)), DecodeErrors) = - "../rad_ffi.mjs" "toml_decode_every" +@target(javascript) +fn do_decode_every( + toml: Toml, + key_path: List(String), + decoder: Decoder(a), +) -> Result(List(#(String, a)), Snag) { + toml + |> javascript_decode_every(key_path, decoder) + |> result.map_error(with: decode_errors_to_snag(_, key_path)) } +@target(javascript) +@external(javascript, "../rad_ffi.mjs", "toml_decode_every") +fn javascript_decode_every( + toml: Toml, + key_path: List(String), + decoder: Decoder(a), +) -> Result(List(#(String, a)), DecodeErrors) + /// Results in a [`Toml`](#Toml) decoded from the given dynamic `data` on /// success, or /// [`DecodeErrors`](https://hexdocs.pm/gleam_stdlib/gleam/dynamic.html#DecodeErrors) @@ -129,15 +121,9 @@ pub fn from_dynamic(data: Dynamic) -> Result(Toml, DecodeErrors) { decode_object(data) } -if erlang { - external fn decode_object(Dynamic) -> Result(Toml, DecodeErrors) = - "rad_ffi" "decode_object" -} - -if javascript { - external fn decode_object(Dynamic) -> Result(Toml, DecodeErrors) = - "../rad_ffi.mjs" "decode_object" -} +@external(erlang, "rad_ffi", "decode_object") +@external(javascript, "../rad_ffi.mjs", "decode_object") +fn decode_object(data: Dynamic) -> Result(Toml, DecodeErrors) /// Returns a new, empty [`Toml`](#Toml). /// @@ -145,15 +131,9 @@ pub fn new() -> Toml { do_new() } -if erlang { - external fn do_new() -> Toml = - "rad_ffi" "toml_new" -} - -if javascript { - external fn do_new() -> Toml = - "../rad_ffi.mjs" "toml_new" -} +@external(erlang, "rad_ffi", "toml_new") +@external(javascript, "../rad_ffi.mjs", "toml_new") +fn do_new() -> Toml /// Results in a [`Toml`](#Toml) parsed from the given file `path` on success, /// or a [`Snag`](https://hexdocs.pm/snag/snag.html#Snag) on failure. @@ -177,15 +157,9 @@ pub fn parse_file(path: String) -> Result(Toml, Snag) { }) } -if erlang { - external fn do_parse_file(String) -> Result(Dynamic, Nil) = - "rad_ffi" "toml_read_file" -} - -if javascript { - external fn do_parse_file(String) -> Result(Dynamic, Nil) = - "../rad_ffi.mjs" "toml_read_file" -} +@external(erlang, "rad_ffi", "toml_read_file") +@external(javascript, "../rad_ffi.mjs", "toml_read_file") +fn do_parse_file(path: String) -> Result(Dynamic, Nil) fn decode_errors_to_snag(decode_errors: DecodeErrors, key_path: List(String)) { let [head, ..rest] = diff --git a/src/rad/util.gleam b/src/rad/util.gleam index e186579..746e8fb 100644 --- a/src/rad/util.gleam +++ b/src/rad/util.gleam @@ -16,10 +16,8 @@ import glint/flag import rad/toml import shellout.{CommandOpt, LetBeStderr, LetBeStdout, Lookups} import snag.{Snag} - -if erlang { - import gleam/erlang/file.{Enoent} -} +@target(erlang) +import rad/internal/file.{Enoent} /// Custom color [`Lookups`](https://hexdocs.pm/shellout/shellout.html#Lookups) /// for `rad`. @@ -59,7 +57,7 @@ pub fn erlang_run( |> result.replace_error(snag.new("failed to find `ebin` paths")) |> result.try(apply: fn(ebins) { [["-pa", ..ebins], args] - |> list.flatten + |> list.concat |> shellout.command(run: "erl", in: ".", opt: options) |> result.replace_error(snag.new("failed to run `erl`")) }) @@ -72,23 +70,21 @@ pub fn ebin_paths() -> Result(List(String), Nil) { do_ebin_paths() } -if erlang { - fn do_ebin_paths() -> Result(List(String), Nil) { - let prefix = "./build/dev/erlang" - prefix - |> file.list_directory - |> result.map(with: list.map(_, with: fn(subdirectory) { - [prefix, subdirectory, "ebin"] - |> string.join(with: "/") - })) - |> result.nil_error - } +@target(erlang) +fn do_ebin_paths() -> Result(List(String), Nil) { + let prefix = "./build/dev/erlang" + prefix + |> file.list_directory + |> result.map(with: list.map(_, with: fn(subdirectory) { + [prefix, subdirectory, "ebin"] + |> string.join(with: "/") + })) + |> result.nil_error } -if javascript { - external fn do_ebin_paths() -> Result(List(String), Nil) = - "../rad_ffi.mjs" "ebin_paths" -} +@target(javascript) +@external(javascript, "../rad_ffi.mjs", "ebin_paths") +fn do_ebin_paths() -> Result(List(String), Nil) /// Runs Deno or Node.js (depending on the JavaScript runtime specified in your /// project's `gleam.toml` config) with the given arguments and shellout @@ -153,18 +149,16 @@ pub fn file_exists(path: String) -> Bool { do_file_exists(path) } -if erlang { - fn do_file_exists(path: String) -> Bool { - path - |> file.file_exists - |> result.unwrap(or: False) - } +@target(erlang) +fn do_file_exists(path: String) -> Bool { + path + |> file.file_exists + |> result.unwrap(or: False) } -if javascript { - external fn do_file_exists(String) -> Bool = - "../rad_ffi.mjs" "file_exists" -} +@target(javascript) +@external(javascript, "../rad_ffi.mjs", "file_exists") +fn do_file_exists(path: String) -> Bool /// Tries to write some `contents` to a file at the given `path`. /// @@ -185,16 +179,14 @@ pub fn file_write( }) } -if erlang { - fn do_file_write(contents: String, path: String) -> Result(Nil, file.Reason) { - file.write(contents: contents, to: path) - } +@target(erlang) +fn do_file_write(contents: String, path: String) -> Result(Nil, file.Reason) { + file.write(contents: contents, to: path) } -if javascript { - external fn do_file_write(String, String) -> Result(Nil, dynamic.Dynamic) = - "../rad_ffi.mjs" "file_write" -} +@target(javascript) +@external(javascript, "../rad_ffi.mjs", "file_write") +fn do_file_write(contents: String, path: String) -> Result(Nil, dynamic.Dynamic) /// Returns a boolean indicating whether or not a directory exists at the given /// `path`. @@ -203,18 +195,16 @@ pub fn is_directory(path: String) -> Bool { do_is_directory(path) } -if erlang { - fn do_is_directory(path: String) -> Bool { - path - |> file.is_directory - |> result.unwrap(or: False) - } +@target(erlang) +fn do_is_directory(path: String) -> Bool { + path + |> file.is_directory + |> result.unwrap(or: False) } -if javascript { - external fn do_is_directory(String) -> Bool = - "../rad_ffi.mjs" "is_directory" -} +@target(javascript) +@external(javascript, "../rad_ffi.mjs", "is_directory") +fn do_is_directory(path: String) -> Bool /// Tries to create a new directory at the given `path`. /// @@ -234,16 +224,14 @@ pub fn make_directory(path: String) -> Result(String, Snag) { }) } -if erlang { - fn do_make_directory(path: String) -> Result(Nil, file.Reason) { - file.make_directory(path) - } +@target(erlang) +fn do_make_directory(path: String) -> Result(Nil, file.Reason) { + file.make_directory(path) } -if javascript { - external fn do_make_directory(String) -> Result(Nil, String) = - "../rad_ffi.mjs" "make_directory" -} +@target(javascript) +@external(javascript, "../rad_ffi.mjs", "make_directory") +fn do_make_directory(path: String) -> Result(Nil, String) /// Tries to recursively delete the given `path`. /// @@ -264,19 +252,17 @@ pub fn recursive_delete(path: String) -> Result(String, Snag) { }) } -if erlang { - fn do_recursive_delete(path: String) -> Result(Nil, file.Reason) { - case file.recursive_delete(path) { - Error(Enoent) -> Ok(Nil) - result -> result - } +@target(erlang) +fn do_recursive_delete(path: String) -> Result(Nil, file.Reason) { + case file.recursive_delete(path) { + Error(Enoent) -> Ok(Nil) + result -> result } } -if javascript { - external fn do_recursive_delete(String) -> Result(Nil, String) = - "../rad_ffi.mjs" "recursive_delete" -} +@target(javascript) +@external(javascript, "../rad_ffi.mjs", "recursive_delete") +fn do_recursive_delete(path: String) -> Result(Nil, String) /// Tries to move a given `source` path to a new location. /// @@ -292,15 +278,13 @@ pub fn rename(from source: String, to dest: String) -> Result(String, Snag) { }) } -if erlang { - external fn do_rename(String, String) -> Result(Nil, file.Reason) = - "rad_ffi" "rename" -} +@target(erlang) +@external(erlang, "rad_ffi", "rename") +fn do_rename(from: String, to: String) -> Result(Nil, file.Reason) -if javascript { - external fn do_rename(String, String) -> Result(Nil, String) = - "../rad_ffi.mjs" "rename" -} +@target(javascript) +@external(javascript, "../rad_ffi.mjs", "rename") +fn do_rename(from: String, to: String) -> Result(Nil, String) /// Results in the current working directory path on success, or a /// [`Snag`](https://hexdocs.pm/snag/snag.html#Snag) on failure. @@ -310,15 +294,13 @@ pub fn working_directory() -> Result(String, Snag) { |> result.replace_error(snag.new("failed to get current working directory")) } -if erlang { - external fn do_working_directory() -> Result(String, file.Reason) = - "rad_ffi" "working_directory" -} +@target(erlang) +@external(erlang, "rad_ffi", "working_directory") +fn do_working_directory() -> Result(String, file.Reason) -if javascript { - external fn do_working_directory() -> Result(String, Nil) = - "../rad_ffi.mjs" "working_directory" -} +@target(javascript) +@external(javascript, "../rad_ffi.mjs", "working_directory") +fn do_working_directory() -> Result(String, Nil) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~// // Miscellaneous Functions // @@ -330,15 +312,9 @@ pub fn encode_json(data: a) -> String { do_encode_json(data) } -if erlang { - external fn do_encode_json(a) -> String = - "rad_ffi" "encode_json" -} - -if javascript { - external fn do_encode_json(a) -> String = - "../rad_ffi.mjs" "encode_json" -} +@external(erlang, "rad_ffi", "encode_json") +@external(javascript, "../rad_ffi.mjs", "encode_json") +fn do_encode_json(data: a) -> String /// Turns a [`Snag`](https://hexdocs.pm/snag/snag.html#Snag) into a multiline /// string optimized for readability. diff --git a/src/rad/workbook/standard.gleam b/src/rad/workbook/standard.gleam index 70ae400..a7a7935 100644 --- a/src/rad/workbook/standard.gleam +++ b/src/rad/workbook/standard.gleam @@ -38,15 +38,12 @@ import rad/util import rad/workbook.{Workbook} import shellout.{LetBeStderr, LetBeStdout, StyleFlags} import snag.{Snag} - -if erlang { - import gleam/http/request - import gleam/httpc -} - -if javascript { - import gleam/json -} +@target(erlang) +import gleam/http/request +@target(erlang) +import rad/internal/httpc +@target(javascript) +import gleam/json /// Directories that are omitted when printing, watching, etc. /// @@ -555,7 +552,7 @@ pub fn docs_serve(input: CommandInput, _task: Task(Result)) -> Result { util.relay_flags(flags), ["--", "./build/dev/docs"], ] - |> list.flatten + |> list.concat util.javascript_run( deno: [ "run", @@ -725,70 +722,67 @@ pub fn ping(input: CommandInput, _task: Task(Result)) -> Result { ) } -if erlang { - fn do_ping( - uri_string: String, - headers: List(Header), - ) -> gleam.Result(Int, Int) { - use uri <- result.try( - uri_string - |> uri.parse - |> result.replace_error(400), - ) +@target(erlang) +fn do_ping(uri_string: String, headers: List(Header)) -> gleam.Result(Int, Int) { + use uri <- result.try( + uri_string + |> uri.parse + |> result.replace_error(400), + ) - use request <- result.try( - uri - |> request.from_uri - |> result.replace_error(400), - ) + use request <- result.try( + uri + |> request.from_uri + |> result.replace_error(400), + ) - headers - |> list.fold( - from: request, - with: fn(acc, header) { request.prepend_header(acc, header.0, header.1) }, - ) - |> httpc.send - |> result.map(with: fn(response) { response.status }) - |> result.replace_error(503) - } + use _ <- result.try( + httpc.ensure_started() + |> result.replace_error(400), + ) + + headers + |> list.fold( + from: request, + with: fn(acc, header) { request.prepend_header(acc, header.0, header.1) }, + ) + |> httpc.send + |> result.map(with: fn(response) { response.status }) + |> result.replace_error(503) } -if javascript { - fn do_ping( - uri_string: String, - headers: List(Header), - ) -> gleam.Result(Int, Int) { - let headers = - headers - |> list.map(with: fn(header) { #(header.0, json.string(header.1)) }) - |> json.object - |> json.to_string - - let script = - [ - "fetch('" <> uri_string <> "', " <> headers <> ")", - ".then(response => response.status)", - ".catch(() => 503)", - ".then(console.log)", - ] - |> string.concat - use status <- result.try( - util.javascript_run( - deno: ["eval", script, "--unstable"], - or: ["--eval=" <> script], - opt: [], - ) - |> result.replace_error(503), +@target(javascript) +fn do_ping(uri_string: String, headers: List(Header)) -> gleam.Result(Int, Int) { + let headers = + headers + |> list.map(with: fn(header) { #(header.0, json.string(header.1)) }) + |> json.object + |> json.to_string + + let script = + [ + "fetch('" <> uri_string <> "', " <> headers <> ")", + ".then(response => response.status)", + ".catch(() => 503)", + ".then(console.log)", + ] + |> string.concat + use status <- result.try( + util.javascript_run( + deno: ["eval", script, "--unstable"], + or: ["--eval=" <> script], + opt: [], ) + |> result.replace_error(503), + ) - let assert Ok(status) = - status - |> string.trim - |> int.parse - case status < 400 { - True -> Ok(status) - False -> Error(status) - } + let assert Ok(status) = + status + |> string.trim + |> int.parse + case status < 400 { + True -> Ok(status) + False -> Error(status) } } @@ -829,73 +823,71 @@ pub fn shell(input: CommandInput, task: Task(Result)) -> Result { do_shell(input, task) } -if erlang { - fn do_shell(_input: CommandInput, _task: Task(Result)) -> Result { - util.refuse_erlang() - } +@target(erlang) +fn do_shell(_input: CommandInput, _task: Task(Result)) -> Result { + util.refuse_erlang() } -if javascript { - fn do_shell(input: CommandInput, task: Task(Result)) -> Result { - let options = [LetBeStderr, LetBeStdout] - let runtime = case input.args { - [runtime, ..] -> runtime - _else -> "erlang" - } - let javascript = - [ - "import('" <> util.rad_path <> "/rad_ffi.mjs')", - ".then(module => module.load_modules())", - ] - |> string.concat +@target(javascript) +fn do_shell(input: CommandInput, task: Task(Result)) -> Result { + let options = [LetBeStderr, LetBeStdout] + let runtime = case input.args { + [runtime, ..] -> runtime + _else -> "erlang" + } + let javascript = + [ + "import('" <> util.rad_path <> "/rad_ffi.mjs')", + ".then(module => module.load_modules())", + ] + |> string.concat - case runtime { - "elixir" | "iex" -> { - let assert Parsed(config) = task.config - use name <- result.try( - ["name"] - |> toml.decode(from: config, expect: dynamic.string), - ) - use ebins <- result.try( - util.ebin_paths() - |> result.replace_error(snag.new("failed to find `ebin` paths")), - ) + case runtime { + "elixir" | "iex" -> { + let assert Parsed(config) = task.config + use name <- result.try( + ["name"] + |> toml.decode(from: config, expect: dynamic.string), + ) + use ebins <- result.try( + util.ebin_paths() + |> result.replace_error(snag.new("failed to find `ebin` paths")), + ) + [ + ["--app", name], [ - ["--app", name], - [ - "--erl", - ["-pa", ..ebins] - |> string.join(with: " "), - ], - ] - |> list.flatten - |> shellout.command(run: "iex", in: ".", opt: options) - |> result.replace_error(snag.new("failed to run `elixir` shell")) - } + "--erl", + ["-pa", ..ebins] + |> string.join(with: " "), + ], + ] + |> list.concat + |> shellout.command(run: "iex", in: ".", opt: options) + |> result.replace_error(snag.new("failed to run `elixir` shell")) + } - "erlang" | "erl" -> - [] - |> util.erlang_run(opt: options) - |> result.replace_error(snag.new("failed to run `erlang` shell")) + "erlang" | "erl" -> + [] + |> util.erlang_run(opt: options) + |> result.replace_error(snag.new("failed to run `erlang` shell")) - "deno" -> - ["repl", "--eval=" <> javascript, "--allow-all", "--unstable"] - |> shellout.command(run: "deno", in: ".", opt: options) - |> result.replace_error(snag.new("failed to run `deno` shell")) + "deno" -> + ["repl", "--eval=" <> javascript, "--allow-all", "--unstable"] + |> shellout.command(run: "deno", in: ".", opt: options) + |> result.replace_error(snag.new("failed to run `deno` shell")) - "nodejs" | "node" -> - [ - "--interactive", - "--eval=" <> javascript, - "--experimental-fetch", - "--experimental-repl-await", - "--no-warnings", - ] - |> shellout.command(run: "node", in: ".", opt: options) - |> result.replace_error(snag.new("failed to run `nodejs` shell")) + "nodejs" | "node" -> + [ + "--interactive", + "--eval=" <> javascript, + "--experimental-fetch", + "--experimental-repl-await", + "--no-warnings", + ] + |> shellout.command(run: "node", in: ".", opt: options) + |> result.replace_error(snag.new("failed to run `nodejs` shell")) - _else -> snag.error("unsupported runtime `" <> runtime <> "`") - } + _else -> snag.error("unsupported runtime `" <> runtime <> "`") } } @@ -1000,7 +992,7 @@ pub fn tree(_input: CommandInput, _task: Task(Result)) -> Result { ["--noreport"], [working_directory], ] - |> list.flatten + |> list.concat |> shellout.command(run: "tree", in: ".", opt: []) |> result.replace_error(snag.layer(error, "command `tree` not found")) } @@ -1110,95 +1102,94 @@ pub fn watch(input: CommandInput, _task: Task(Result)) -> Result { do_watch(input) } -if erlang { - fn do_watch(_input: CommandInput) -> Result { - util.refuse_erlang() - } +@target(erlang) +fn do_watch(_input: CommandInput) -> Result { + util.refuse_erlang() } -if javascript { - fn do_watch(input: CommandInput) -> Result { - let options = [LetBeStderr, LetBeStdout] - let [command, ..args] as watch_do = case input.args { - [] -> { - let rad = util.which_rad() - let flags = util.relay_flags(input.flags) - [rad, "watch", "do", ..flags] - } - args -> args +@target(javascript) +fn do_watch(input: CommandInput) -> Result { + let options = [LetBeStderr, LetBeStdout] + let [command, ..args] as watch_do = case input.args { + [] -> { + let rad = util.which_rad() + let flags = util.relay_flags(input.flags) + [rad, "watch", "do", ..flags] } + args -> args + } + [ + " Watching" + |> shellout.style(with: shellout.color(["magenta"]), custom: util.lookups), + " … " + |> shellout.style(with: shellout.color(["cyan"]), custom: util.lookups), + "(Ctrl+C to quit)", + ] + |> string.concat + |> io.println + + let result = [ - " Watching" - |> shellout.style(with: shellout.color(["magenta"]), custom: util.lookups), - " … " - |> shellout.style(with: shellout.color(["cyan"]), custom: util.lookups), - "(Ctrl+C to quit)", + ignore_glob + |> string.split(on: "|") + |> list.map(with: fn(directory) { + ["--ignore=**/", directory, "/**"] + |> string.concat + }), + ["--no-shell"], + ["--postpone"], + ["--watch-when-idle"], + ["--", ..watch_do], ] - |> string.concat - |> io.println - - let result = - [ - ignore_glob - |> string.split(on: "|") - |> list.map(with: fn(directory) { - ["--ignore=**/", directory, "/**"] - |> string.concat - }), - ["--no-shell"], - ["--postpone"], - ["--watch-when-idle"], - ["--", ..watch_do], - ] - |> list.flatten - |> shellout.command(run: "watchexec", in: ".", opt: options) - |> result.replace_error(snag.new("command `watchexec` not found")) - case result { - Ok(_output) -> result - Error(error) -> - watch_loop( - on: fn() { + |> list.concat + |> shellout.command(run: "watchexec", in: ".", opt: options) + |> result.replace_error(snag.new("command `watchexec` not found")) + case result { + Ok(_output) -> result + Error(error) -> + watch_loop( + on: fn() { + [ + ["--event", "create"], + ["--event", "delete"], + ["--event", "modify"], + ["--event", "move"], [ - ["--event", "create"], - ["--event", "delete"], - ["--event", "modify"], - ["--event", "move"], - [ - "--exclude", - ["^[./\\\\]*(", ignore_glob, ")([/\\\\].*)*$"] - |> string.concat, - ], - ["-qq"], - ["--recursive"], - ["."], - ] - |> list.flatten - |> shellout.command(run: "inotifywait", in: ".", opt: options) - }, - do: fn() { - command - |> shellout.command(with: args, in: ".", opt: options) - }, - ) - |> result.replace_error(snag.layer( - error, - "command `inotifywait` not found", - )) - } - |> result.map_error(with: function.compose( - snag.layer(_, "failed to find a known watcher command"), - snag.layer(_, "failed to run task"), - )) + "--exclude", + ["^[./\\\\]*(", ignore_glob, ")([/\\\\].*)*$"] + |> string.concat, + ], + ["-qq"], + ["--recursive"], + ["."], + ] + |> list.concat + |> shellout.command(run: "inotifywait", in: ".", opt: options) + }, + do: fn() { + command + |> shellout.command(with: args, in: ".", opt: options) + }, + ) + |> result.replace_error(snag.layer( + error, + "command `inotifywait` not found", + )) } - - external fn watch_loop( - on: fn() -> gleam.Result(String, #(Int, String)), - do: fn() -> gleam.Result(String, #(Int, String)), - ) -> gleam.Result(String, Nil) = - "../../rad_ffi.mjs" "watch_loop" + |> result.map_error(with: function.compose( + snag.layer(_, "failed to find a known watcher command"), + snag.layer(_, "failed to run task"), + )) } +@target(javascript) +@external(javascript, "../../rad_ffi.mjs", "watch_loop") +fn watch_loop( + on watch_fun: fn() -> gleam.Result(String, #(Int, String)), + do do_fun: fn() -> gleam.Result(String, #(Int, String)), +) -> gleam.Result(String, Nil) + /// Runs several `rad` tasks in succession: renders the project's HTML /// documentation, signals the documentation server to do a live reload for all /// known client connections, and runs the project's tests for all specified diff --git a/src/rad_ffi.mjs b/src/rad_ffi.mjs index 3b48f90..f0d3741 100644 --- a/src/rad_ffi.mjs +++ b/src/rad_ffi.mjs @@ -20,6 +20,11 @@ const LetBeStdout = new shellout.LetBeStdout(); const prefix = "./build/dev/javascript"; const default_workbook = "../rad/rad/workbook/standard.mjs"; +export function start_arguments() { + let args = shellout.arguments$(); + return (args.isEmpty() || globalThis.Deno) ? args : args.tail; +} + export function ebin_paths() { let prefix = "./build/dev/erlang"; try { @@ -264,3 +269,11 @@ export function working_directory() { return new GleamError(Nil); } } + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~// +// Miscellaneous Functions // +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~// + +export function no_fun() { + throw Error("Unsupported target"); +} diff --git a/test/rad/task_test.gleam b/test/rad/task_test.gleam index 4403c30..259de67 100644 --- a/test/rad/task_test.gleam +++ b/test/rad/task_test.gleam @@ -126,11 +126,11 @@ pub fn builder_test() { |> should.equal(shortdoc) [[flag1], flags, [flag4]] - |> list.flatten + |> list.concat |> should.equal(builder.flags) [[parameter1], parameters, [parameter4]] - |> list.flatten + |> list.concat |> should.equal(builder.parameters) builder.config @@ -480,7 +480,7 @@ pub fn trainer_test() { }) |> task.with_manifest - ["requirements", "gleam_stdlib"] + ["requirements", "gleam_stdlib", "version"] |> rad_test.input(flags: []) |> rad_test.run(builder) |> should.be_ok @@ -523,7 +523,7 @@ pub fn sort_test() { |> workbook.to_tasks |> list.sized_chunk(into: 3) |> list.reverse - |> list.flatten + |> list.concat |> task.sort head.path