diff --git a/Cargo.lock b/Cargo.lock index c1c7fa1..0a4a4c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,9 +64,9 @@ checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" [[package]] name = "cc" -version = "1.2.4" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" +checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" dependencies = [ "shlex", ] @@ -340,9 +340,9 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "libc" -version = "0.2.168" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libsqlite3-sys" @@ -402,9 +402,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -423,9 +423,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -450,9 +450,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -475,7 +475,7 @@ dependencies = [ [[package]] name = "roc_command" version = "0.0.1" -source = "git+https://github.com/roc-lang/basic-cli.git#71a0666c70950212714e881304c75d95e3f082b7" +source = "git+https://github.com/roc-lang/basic-cli.git#24075b397b52237d99bb665df4c0b4e31ef61629" dependencies = [ "roc_io_error", "roc_std", @@ -484,7 +484,7 @@ dependencies = [ [[package]] name = "roc_env" version = "0.0.1" -source = "git+https://github.com/roc-lang/basic-cli.git#71a0666c70950212714e881304c75d95e3f082b7" +source = "git+https://github.com/roc-lang/basic-cli.git#24075b397b52237d99bb665df4c0b4e31ef61629" dependencies = [ "roc_file", "roc_std", @@ -494,7 +494,7 @@ dependencies = [ [[package]] name = "roc_file" version = "0.0.1" -source = "git+https://github.com/roc-lang/basic-cli.git#71a0666c70950212714e881304c75d95e3f082b7" +source = "git+https://github.com/roc-lang/basic-cli.git#24075b397b52237d99bb665df4c0b4e31ef61629" dependencies = [ "memchr", "roc_io_error", @@ -533,7 +533,7 @@ dependencies = [ [[package]] name = "roc_http" version = "0.0.1" -source = "git+https://github.com/roc-lang/basic-cli.git#71a0666c70950212714e881304c75d95e3f082b7" +source = "git+https://github.com/roc-lang/basic-cli.git#24075b397b52237d99bb665df4c0b4e31ef61629" dependencies = [ "bytes", "hyper", @@ -549,7 +549,7 @@ dependencies = [ [[package]] name = "roc_io_error" version = "0.0.1" -source = "git+https://github.com/roc-lang/basic-cli.git#71a0666c70950212714e881304c75d95e3f082b7" +source = "git+https://github.com/roc-lang/basic-cli.git#24075b397b52237d99bb665df4c0b4e31ef61629" dependencies = [ "roc_std", "roc_std_heap", @@ -558,7 +558,7 @@ dependencies = [ [[package]] name = "roc_sqlite" version = "0.0.1" -source = "git+https://github.com/roc-lang/basic-cli.git#71a0666c70950212714e881304c75d95e3f082b7" +source = "git+https://github.com/roc-lang/basic-cli.git#24075b397b52237d99bb665df4c0b4e31ef61629" dependencies = [ "libsqlite3-sys", "roc_std", @@ -569,7 +569,7 @@ dependencies = [ [[package]] name = "roc_std" version = "0.0.1" -source = "git+https://github.com/roc-lang/roc.git#91ed6a5a8e67f4e1a1255fd21f7dca3570e95202" +source = "git+https://github.com/roc-lang/roc.git#846445197a022ad7ccc6cb3bb020535e1a0ba672" dependencies = [ "arrayvec", "static_assertions", @@ -578,7 +578,7 @@ dependencies = [ [[package]] name = "roc_std_heap" version = "0.0.1" -source = "git+https://github.com/roc-lang/roc.git#91ed6a5a8e67f4e1a1255fd21f7dca3570e95202" +source = "git+https://github.com/roc-lang/roc.git#846445197a022ad7ccc6cb3bb020535e1a0ba672" dependencies = [ "memmap2", "roc_std", @@ -587,7 +587,7 @@ dependencies = [ [[package]] name = "roc_stdio" version = "0.0.1" -source = "git+https://github.com/roc-lang/basic-cli.git#71a0666c70950212714e881304c75d95e3f082b7" +source = "git+https://github.com/roc-lang/basic-cli.git#24075b397b52237d99bb665df4c0b4e31ef61629" dependencies = [ "roc_io_error", "roc_std", @@ -675,9 +675,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -732,9 +732,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "2.0.90" +version = "2.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" dependencies = [ "proc-macro2", "quote", diff --git a/build.roc b/build.roc index 5bf3261..4b7a56f 100644 --- a/build.roc +++ b/build.roc @@ -1,117 +1,120 @@ -app [main] { - cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.17.0/lZFLstMUCUvd5bjnnpYromZJXkQUrdhbva4xdBInicE.tar.br", +app [main!] { + cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/bi5zubJ-_Hva9vxxPq4kNx4WHX6oFs8OP6Ad0tCYlrY.tar.br", + weaver: "https://github.com/smores56/weaver/releases/download/0.6.0/4GmRnyE7EFjzv6dDpebJoWWwXV285OMt4ntHIc6qvmY.tar.br", } import cli.Cmd import cli.Stdout import cli.Env import cli.Arg -import cli.Arg.Opt -import cli.Arg.Cli +import weaver.Opt +import weaver.Cli ## Builds the basic-webserver [platform](https://www.roc-lang.org/platforms). ## ## run with: roc ./build.roc ## -main : Task {} _ -main = +main! : _ => Result {} _ +main! = |args| - cli_parser = - Arg.Opt.maybeStr { short: "p", long: "roc", help: "Path to the roc executable. Can be just `roc` or a full path." } - |> Arg.Cli.finish { + parsed_args = + Result.on_err!( + Cli.parse_or_display_message(cli_parser, args, Arg.to_os_raw), + |message| Err(Exit(1, message)), + )? + + run!(parsed_args) + +cli_parser = + Opt.maybe_str({ short: "p", long: "roc", help: "Path to the roc executable. Can be just `roc` or a full path." }) + |> Cli.finish( + { name: "basic-webserver-builder", version: "", authors: ["Luke Boswell "], description: "Generates all files needed by Roc to use this basic-cli platform.", - } - |> Arg.Cli.assertValid - - when Arg.Cli.parseOrDisplayMessage cli_parser (Arg.list! {}) is - Ok parsed_args -> run parsed_args - Err err_msg -> Task.err (Exit 1 err_msg) + }, + ) + |> Cli.assert_valid -run : Result Str err -> Task {} _ -run = \maybe_roc -> +run! : Result Str err => Result {} _ +run! = |maybe_roc| # roc_cmd may be a path or just roc - roc_cmd = maybe_roc |> Result.withDefault "roc" + roc_cmd = maybe_roc ?? "roc" - roc_version! roc_cmd + roc_version!(roc_cmd)? - os_and_arch = get_os_and_arch! + os_and_arch = get_os_and_arch!({})? - stub_lib_path = "platform/libapp.$(stub_file_extension os_and_arch)" + stub_lib_path = "platform/libapp.${stub_file_extension(os_and_arch)}" - build_stub_app_lib! roc_cmd stub_lib_path + build_stub_app_lib!(roc_cmd, stub_lib_path)? - cargo_build_host! + cargo_build_host!({})? - rust_target_folder = get_rust_target_folder! + rust_target_folder = get_rust_target_folder!({})? - copy_host_lib! os_and_arch rust_target_folder + copy_host_lib!(os_and_arch, rust_target_folder)? - preprocess_host! roc_cmd stub_lib_path rust_target_folder + preprocess_host!(roc_cmd, stub_lib_path, rust_target_folder)? - info! "Successfully built platform files!" + info!("Successfully built platform files!") -roc_version : Str -> Task {} _ -roc_version = \roc_cmd -> +roc_version! : Str => Result {} _ +roc_version! = |roc_cmd| - info! "Checking provided roc; executing `$(roc_cmd) version`:" + info!("Checking provided roc; executing `${roc_cmd} version`:")? - roc_cmd - |> Cmd.exec ["version"] - |> Task.mapErr! RocVersionCheckFailed + Cmd.exec!(roc_cmd, ["version"]) + |> Result.map_err(RocVersionCheckFailed) -get_os_and_arch : Task OSAndArch _ -get_os_and_arch = +get_os_and_arch! : {} => Result OSAndArch _ +get_os_and_arch! = |{}| - info! "Getting the native operating system and architecture..." + info!("Getting the native operating system and architecture...")? - Env.platform - |> Task.await convert_os_and_arch + convert_os_and_arch(Env.platform!({})) -build_stub_app_lib : Str, Str -> Task {} _ -build_stub_app_lib = \roc_cmd, stub_lib_path -> +build_stub_app_lib! : Str, Str => Result {} _ +build_stub_app_lib! = |roc_cmd, stub_lib_path| - info! "Building stubbed app shared library ..." + info!("Building stubbed app shared library ...")? - roc_cmd - |> Cmd.exec ["build", "--lib", "platform/libapp.roc", "--output", stub_lib_path, "--optimize"] - |> Task.mapErr! ErrBuildingAppStub - -get_rust_target_folder : Task Str _ -get_rust_target_folder = - when Env.var "CARGO_BUILD_TARGET" |> Task.result! is - Ok target_env_var -> - if Str.isEmpty target_env_var then - Task.ok "target/release/" + Cmd.exec!(roc_cmd, ["build", "--lib", "platform/libapp.roc", "--output", stub_lib_path, "--optimize"]) + |> Result.map_err(ErrBuildingAppStub) + +get_rust_target_folder! : {} => Result Str _ +get_rust_target_folder! = |{}| + when Env.var!("CARGO_BUILD_TARGET") is + Ok(target_env_var) -> + if Str.is_empty(target_env_var) then + Ok("target/release/") else - Task.ok "target/$(target_env_var)/release/" + Ok("target/${target_env_var}/release/") + + Err(e) -> + info!("Failed to get env var CARGO_BUILD_TARGET with error ${Inspect.to_str(e)}. Assuming default CARGO_BUILD_TARGET (native)...")? - Err e -> - info! "Failed to get env var CARGO_BUILD_TARGET with error $(Inspect.toStr e). Assuming default CARGO_BUILD_TARGET (native)..." + Ok("target/release/") - Task.ok "target/release/" +cargo_build_host! : {} => Result {} _ +cargo_build_host! = |{}| -cargo_build_host : Task {} _ -cargo_build_host = + info!("Building rust host ...")? - info! "Building rust host ..." + Cmd.exec!("cargo", ["build", "--release"]) + |> Result.map_err(ErrBuildingHostBinaries) - "cargo" - |> Cmd.exec ["build", "--release"] - |> Task.mapErr! ErrBuildingHostBinaries +copy_host_lib! : OSAndArch, Str => Result {} _ +copy_host_lib! = |os_and_arch, rust_target_folder| + host_build_path = "${rust_target_folder}libhost.a" + host_dest_path = "platform/${prebuilt_static_lib_file(os_and_arch)}" -copy_host_lib : OSAndArch, Str -> Task {} _ -copy_host_lib = \os_and_arch, rust_target_folder -> - host_build_path = "$(rust_target_folder)libhost.a" - host_dest_path = "platform/$(prebuilt_static_lib_file os_and_arch)" + info!("Moving the prebuilt binary from ${host_build_path} to ${host_dest_path} ...")? - info! "Moving the prebuilt binary from $(host_build_path) to $(host_dest_path) ..." - "cp" - |> Cmd.exec [host_build_path, host_dest_path] - |> Task.mapErr! ErrMovingPrebuiltLegacyBinary + Cmd.exec!("cp", [host_build_path, host_dest_path]) + |> Result.map_err(ErrMovingPrebuiltLegacyBinary) OSAndArch : [ MacosArm64, @@ -122,24 +125,24 @@ OSAndArch : [ WindowsX64, ] -convert_os_and_arch : _ -> Task OSAndArch _ -convert_os_and_arch = \{ os, arch } -> +convert_os_and_arch : _ -> Result OSAndArch _ +convert_os_and_arch = |{ os, arch }| when (os, arch) is - (MACOS, AARCH64) -> Task.ok MacosArm64 - (MACOS, X64) -> Task.ok MacosX64 - (LINUX, AARCH64) -> Task.ok LinuxArm64 - (LINUX, X64) -> Task.ok LinuxX64 - _ -> Task.err (UnsupportedNative os arch) + (MACOS, AARCH64) -> Ok(MacosArm64) + (MACOS, X64) -> Ok(MacosX64) + (LINUX, AARCH64) -> Ok(LinuxArm64) + (LINUX, X64) -> Ok(LinuxX64) + _ -> Err(UnsupportedNative(os, arch)) stub_file_extension : OSAndArch -> Str -stub_file_extension = \os_and_arch -> +stub_file_extension = |os_and_arch| when os_and_arch is MacosX64 | MacosArm64 -> "dylib" LinuxArm64 | LinuxX64 -> "so" WindowsX64 | WindowsArm64 -> "dll" prebuilt_static_lib_file : OSAndArch -> Str -prebuilt_static_lib_file = \os_and_arch -> +prebuilt_static_lib_file = |os_and_arch| when os_and_arch is MacosArm64 -> "macos-arm64.a" MacosX64 -> "macos-x64.a" @@ -148,15 +151,17 @@ prebuilt_static_lib_file = \os_and_arch -> WindowsArm64 -> "windows-arm64.lib" WindowsX64 -> "windows-x64.lib" -preprocess_host : Str, Str, Str -> Task {} _ -preprocess_host = \roc_cmd, stub_lib_path, rust_target_folder -> - info! "Preprocessing surgical host ..." - surgical_build_path = "$(rust_target_folder)host" +preprocess_host! : Str, Str, Str => Result {} _ +preprocess_host! = |roc_cmd, stub_lib_path, rust_target_folder| + + info!("Preprocessing surgical host ...")? + + surgical_build_path = "${rust_target_folder}host" roc_cmd - |> Cmd.exec ["preprocess-host", surgical_build_path, "platform/main.roc", stub_lib_path] - |> Task.mapErr! ErrPreprocessingSurgicalBinary + |> Cmd.exec!(["preprocess-host", surgical_build_path, "platform/main.roc", stub_lib_path]) + |> Result.map_err(ErrPreprocessingSurgicalBinary) -info : Str -> Task {} _ -info = \msg -> - Stdout.line! "\u(001b)[34mINFO:\u(001b)[0m $(msg)" +info! : Str => Result {} _ +info! = |msg| + Stdout.line!("\u(001b)[34mINFO:\u(001b)[0m ${msg}") diff --git a/examples/command.roc b/examples/command.roc index 62d81be..9dc0543 100644 --- a/examples/command.roc +++ b/examples/command.roc @@ -7,18 +7,20 @@ import pf.Utc Model : {} init! : {} => Result Model _ -init! = \{} -> Ok {} +init! = |{}| Ok({}) respond! : Request, Model => Result Response [CmdStatusErr _] -respond! = \req, _ -> +respond! = |req, _| # Log request date, method and url using echo program - datetime = Utc.to_iso_8601 (Utc.now! {}) + datetime = Utc.to_iso_8601(Utc.now!({})) - try Cmd.exec! "echo" ["$(datetime) $(Inspect.toStr req.method) $(req.uri)"] + Cmd.exec!("echo", ["${datetime} ${Inspect.to_str(req.method)} ${req.uri}"])? - Ok { - status: 200, - headers: [], - body: Str.toUtf8 "Command succeeded.", - } + Ok( + { + status: 200, + headers: [], + body: Str.to_utf8("Command succeeded."), + }, + ) diff --git a/examples/dir.roc b/examples/dir.roc index 108749e..6a18cc5 100644 --- a/examples/dir.roc +++ b/examples/dir.roc @@ -9,38 +9,36 @@ import pf.Http exposing [Request, Response] Model : {} init! : {} => Result Model _ -init! = \{} -> +init! = |{}| + # Get current working directory - cwd = - Env.cwd! {} - |> Result.mapErr? \CwdUnavailable -> Exit 1 "Unable to read current working directory" + cwd = Env.cwd!({}) ? |CwdUnavailable| Exit(1, "Unable to read current working directory") - try Stdout.line! "The current working directory is $(Path.display cwd)" + Stdout.line!("The current working directory is ${Path.display(cwd)}")? # Try to set cwd to examples - Env.set_cwd! (Path.from_str "examples/") - |> Result.mapErr? \InvalidCwd -> Exit 1 "Unable to set cwd to examples/" + Env.set_cwd!(Path.from_str("examples/")) ? |InvalidCwd| Exit(1, "Unable to set cwd to examples/") - try Stdout.line! "Set cwd to examples/" + Stdout.line!("Set cwd to examples/")? # List contents of examples directory - paths = - Dir.list! "./" - |> Result.mapErr? \DirErr err -> Exit 1 "Error reading directory ./:\n\t$(Inspect.toStr err)" + paths = Dir.list!("./") ? |DirErr(err)| Exit(1, "Error reading directory ./:\n\t${Inspect.to_str(err)}") + + paths_str = + paths + |> List.map(Path.display) + |> Str.join_with(",") - paths - |> List.map Path.display - |> Str.joinWith "," - |> \paths_str -> "The paths are;\n$(paths_str)" - |> Stdout.line! - |> try + Stdout.line!("The paths are;\n${paths_str}")? - Ok {} + Ok({}) respond! : Request, Model => Result Response [] -respond! = \_, _ -> - Ok { - status: 200, - headers: [], - body: Str.toUtf8 "Logged request", - } +respond! = |_, _| + Ok( + { + status: 200, + headers: [], + body: Str.to_utf8("Logged request"), + }, + ) diff --git a/examples/echo.roc b/examples/echo.roc index 11c3371..83d4d20 100644 --- a/examples/echo.roc +++ b/examples/echo.roc @@ -7,19 +7,20 @@ import pf.Utc Model : {} init! : {} => Result Model [] -init! = \{} -> Ok {} +init! = |{}| Ok({}) respond! : Request, Model => Result Response [StdoutErr _] -respond! = \req, _ -> +respond! = |req, _| # Log request datetime, method and url - datetime = Utc.to_iso_8601 (Utc.now! {}) + datetime = Utc.to_iso_8601(Utc.now!({})) - try Stdout.line! "$(datetime) $(Inspect.toStr req.method) $(req.uri)" + Stdout.line!("${datetime} ${Inspect.to_str(req.method)} ${req.uri}")? # Respond with request body - if List.isEmpty req.body then - success [] + if List.is_empty(req.body) then + success([]) else - success req.body + success(req.body) -success = \body -> Ok { status: 200, headers: [], body } +success = |body| + Ok({ status: 200, headers: [], body }) diff --git a/examples/env.roc b/examples/env.roc index c24671d..fe92324 100644 --- a/examples/env.roc +++ b/examples/env.roc @@ -7,32 +7,32 @@ import pf.Env Model : [DebugPrintMode, NonDebugMode] init! : {} => Result Model [Exit I32 Str]_ -init! = \{} -> +init! = |{}| # Check if DEBUG environment variable is set - when Env.var! "DEBUG" is - Ok var if !(Str.isEmpty var) -> Ok DebugPrintMode - _ -> Ok NonDebugMode + when Env.var!("DEBUG") is + Ok(var) if !(Str.is_empty(var)) -> Ok(DebugPrintMode) + _ -> Ok(NonDebugMode) respond! : Request, Model => Result Response [ServerErr Str]_ -respond! = \_, debug -> +respond! = |_, debug| when debug is DebugPrintMode -> # Respond with all the current environment variables vars : Dict Str Str - vars = Env.dict! {} + vars = Env.dict!({}) # Convert the Dict to a list of key-value pairs body = vars - |> Dict.toList - |> List.map \(k, v) -> "$(k): $(v)" - |> Str.joinWith "\n" - |> Str.concat "\n" - |> Str.toUtf8 + |> Dict.to_list + |> List.map(|(k, v)| "${k}: ${v}") + |> Str.join_with("\n") + |> Str.concat("\n") + |> Str.to_utf8 - Ok { status: 200, headers: [], body } + Ok({ status: 200, headers: [], body }) NonDebugMode -> # Respond with a message that DEBUG is not set - Ok { status: 200, headers: [], body: Str.toUtf8 "DEBUG var not set" } + Ok({ status: 200, headers: [], body: Str.to_utf8("DEBUG var not set") }) diff --git a/examples/error-handling.roc b/examples/error-handling.roc index 636b4c6..8d32aff 100644 --- a/examples/error-handling.roc +++ b/examples/error-handling.roc @@ -9,10 +9,10 @@ import pf.Env Model : {} init! : {} => Result Model [] -init! = \{} -> Ok {} +init! = |{}| Ok({}) respond! : Request, Model => Result Response [ServerErr Str]_ -respond! = \req, _ -> handle_req! req |> Result.mapErr map_app_err +respond! = |req, _| handle_req!(req) |> Result.map_err(map_app_err) AppError : [ EnvVarNotSet Str, @@ -21,48 +21,50 @@ AppError : [ ] map_app_err : AppError -> [ServerErr Str] -map_app_err = \app_err -> +map_app_err = |app_err| when app_err is - EnvVarNotSet env_var_name -> ServerErr "Environment variable \"$(env_var_name)\" was not set." - BadBody err -> ServerErr "Http error fetching content:\n\t$(Inspect.toStr err)" - StdoutErr err -> ServerErr "Stdout error logging request:\n\t$(err)" + EnvVarNotSet(env_var_name) -> ServerErr("Environment variable \"${env_var_name}\" was not set.") + BadBody(err) -> ServerErr("Http error fetching content:\n\t${Inspect.to_str(err)}") + StdoutErr(err) -> ServerErr("Stdout error logging request:\n\t${err}") # Here we use AppError to ensure all errors must be handled within our application. handle_req! : Request => Result Response AppError -handle_req! = \req -> +handle_req! = |req| # Log the date, time, method, and url to stdout - try log_request! req + log_request!(req)? # Read environment variable - url = try read_env_var! "TARGET_URL" + url = read_env_var!("TARGET_URL")? # Fetch content of url - content = try fetch_content! url + content = fetch_content!(url)? # Respond with the website content - response_with_code! 200 content + response_with_code!(200, content) log_request! : Request => Result {} [StdoutErr Str] -log_request! = \req -> - datetime = Utc.to_iso_8601 (Utc.now! {}) +log_request! = |req| + datetime = Utc.to_iso_8601(Utc.now!({})) - Stdout.line! "$(datetime) $(Inspect.toStr req.method) $(req.uri)" - |> Result.mapErr \err -> StdoutErr (Inspect.toStr err) + Stdout.line!("${datetime} ${Inspect.to_str(req.method)} ${req.uri}") + |> Result.map_err(|err| StdoutErr(Inspect.to_str(err))) read_env_var! : Str => Result Str [EnvVarNotSet Str] -read_env_var! = \env_var_name -> - Env.var! env_var_name - |> Result.mapErr \_ -> EnvVarNotSet env_var_name +read_env_var! = |env_var_name| + Env.var!(env_var_name) + |> Result.map_err(|_| EnvVarNotSet(env_var_name)) fetch_content! : Str => Result Str _ -fetch_content! = \url -> - Http.get_utf8! url +fetch_content! = |url| + Http.get_utf8!(url) # Respond with the given status code and body response_with_code! : U16, Str => Result Response * -response_with_code! = \code, body -> - Ok { - status: code, - headers: [], - body: Str.toUtf8 body, - } +response_with_code! = |code, body| + Ok( + { + status: code, + headers: [], + body: Str.to_utf8(body), + }, + ) diff --git a/examples/file-upload-form.roc b/examples/file-upload-form.roc index 8164126..588eedc 100644 --- a/examples/file-upload-form.roc +++ b/examples/file-upload-form.roc @@ -1,6 +1,6 @@ app [Model, init!, respond!] { pf: platform "../platform/main.roc", - utils: "https://github.com/quelgar/roc-utils/releases/download/v0.1.0/keYHFjUG1pMAT8ECePEAIS-ncYxEV0DdhTvENUf0USs.tar.br", + utils: "https://github.com/lukewilliamboswell/roc-utils/releases/download/0.3.0/w1rWVzjiBSrvc1VHPNX10o0bHI7rmBdT36hFQ0f5R_w.tar.br", } import utils.Base64 @@ -11,84 +11,87 @@ import pf.MultipartFormData Model : {} init! : {} => Result Model [] -init! = \{} -> Ok {} +init! = |{}| Ok({}) respond! : Request, Model => Result Response [ServerErr Str]_ -respond! = \req, _ -> +respond! = |req, _| if req.method == GET then - body = - """ - - - - Image Upload Form - - + Ok( + { + status: 200, + headers: [ + { name: "Content-Type", value: "text/html" }, + ], + body: Str.to_utf8( + """ + + + + Image Upload Form + + -

Upload an Image

+

Upload an Image

-
-

-

- -
+
+

+

+ +
- - - """ - |> Str.toUtf8 - - Ok { - status: 200, - headers: [ - { name: "Content-Type", value: "text/html" }, - ], - body, - } + + + """, + ), + }, + ) else if req.method == POST then - page = \src -> - """ - - - - - Embedded Image - - - - -

You uploaded:

-
- - - """ - |> Str.toUtf8 + page = |src| + Str.to_utf8( + """ + + + + + Embedded Image + + + + +

You uploaded:

+
+ + + """, + ) maybe_image = { headers: req.headers, body: req.body } |> MultipartFormData.parse_multipart_form_data - |> Result.try List.first - |> Result.map .data - |> Result.map Base64.encode + |> Result.try(List.first) + |> Result.map_ok(.data) + |> Result.map_ok(Base64.encode) when maybe_image is - Ok img -> - Ok { - status: 200, - headers: [ - { name: "Content-Type", value: "text/html" }, - ], - body: page img, - } + Ok(img) -> + Ok( + { + status: 200, + headers: [ + { name: "Content-Type", value: "text/html" }, + ], + body: page(img), + }, + ) - Err err -> Ok { status: 500, headers: [], body: err |> Inspect.toStr |> Str.toUtf8 } + Err(err) -> Ok({ status: 500, headers: [], body: Str.to_utf8(Inspect.to_str(err)) }) else - Ok { status: 500, headers: [], body: [] } + Ok({ status: 500, headers: [], body: [] }) diff --git a/examples/file.roc b/examples/file.roc index ec162e3..5ab0a88 100644 --- a/examples/file.roc +++ b/examples/file.roc @@ -9,21 +9,27 @@ Model : Str # We only read the file once in `init`. If that fails, we don't launch the server. init! : {} => Result Model [Exit I32 Str]_ -init! = \{} -> +init! = |{}| # Read the contents of examples/file.roc - when File.read_utf8! "examples/file.roc" is - Ok contents -> - Ok "Source code of current program:\n\n$(contents)" + File.read_utf8!("examples/file.roc") + |> Result.map_ok(|contents| "Source code of current program:\n\n${contents}") + |> Result.map_err( + |err| + when err is + FileReadErr(path, file_err) -> + Exit( + -1, + "Failed to launch server!\nError reading file ${Path.display(path)}:\n\t${Inspect.to_str(file_err)}", + ) - Err (FileReadErr path err) -> - Exit -1 "Failed to launch server!\nError reading file $(Path.display path):\n\t$(Inspect.toStr err)" - |> Err - - Err (FileReadUtf8Err path _) -> - Exit -2 "Failed to launch server!\nError: failed to read file $(Path.display path) as utf8." - |> Err + FileReadUtf8Err(path, _) -> + Exit( + -2, + "Failed to launch server!\nError: failed to read file ${Path.display(path)} as utf8.", + ), + ) respond! : Request, Model => Result Response [ServerErr Str]_ -respond! = \_, model -> +respond! = |_, model| # If the server launched, the model contains the file content. - Ok { status: 200, headers: [], body: Str.toUtf8 model } + Ok({ status: 200, headers: [], body: Str.to_utf8(model) }) diff --git a/examples/hello-web.roc b/examples/hello-web.roc index 3b5926a..fe059ec 100644 --- a/examples/hello-web.roc +++ b/examples/hello-web.roc @@ -9,19 +9,21 @@ Model : {} # With `init` you can set up a database connection once at server startup, # generate css by running `tailwindcss`,... -# In this case we don't have anything to initialize, so it is just `Ok {}`. +# In this case we don't have anything to initialize, so it is just `Ok({})`. init! : {} => Result Model [] -init! = \{} -> Ok {} +init! = |{}| Ok({}) respond! : Request, Model => Result Response [ServerErr Str]_ -respond! = \req, _ -> +respond! = |req, _| # Log request datetime, method and url - datetime = Utc.to_iso_8601 (Utc.now! {}) + datetime = Utc.to_iso_8601(Utc.now!({})) - try Stdout.line! "$(datetime) $(Inspect.toStr req.method) $(req.uri)" + Stdout.line!("${datetime} ${Inspect.to_str(req.method)} ${req.uri}")? - Ok { - status: 200, - headers: [], - body: Str.toUtf8 "Hello from server
", - } + Ok( + { + status: 200, + headers: [], + body: Str.to_utf8("Hello from server
"), + }, + ) diff --git a/examples/init-basic.roc b/examples/init-basic.roc index 38028ed..8d703f7 100644 --- a/examples/init-basic.roc +++ b/examples/init-basic.roc @@ -9,15 +9,15 @@ Model : Str # With `init` you can set up a database connection once at server startup, # generate css by running `tailwindcss`,... -# In this example it is just `Ok "🎁"`. +# In this example it is just `Ok("🎁")`. init! : {} => Result Model [] -init! = \{} -> Ok "🎁" +init! = |{}| Ok("🎁") respond! : Request, Model => Result Response [ServerErr Str]_ -respond! = \req, model -> +respond! = |req, model| # Log request datetime, method and url - datetime = Utc.to_iso_8601 (Utc.now! {}) + datetime = Utc.to_iso_8601(Utc.now!({})) - try Stdout.line! "$(datetime) $(Inspect.toStr req.method) $(req.uri)" + Stdout.line!("${datetime} ${Inspect.to_str(req.method)} ${req.uri}")? - Ok { status: 200, headers: [], body: Str.toUtf8 "init gave me $(model)" } + Ok({ status: 200, headers: [], body: Str.to_utf8("init gave me ${model}") }) diff --git a/examples/result.roc b/examples/result.roc index f1e63ab..9d5f7f4 100644 --- a/examples/result.roc +++ b/examples/result.roc @@ -5,23 +5,23 @@ import pf.Http exposing [Request, Response] Model : {} init! : {} => Result Model [] -init! = \{} -> Ok {} +init! = |{}| Ok({}) respond! : Request, Model => Result Response [ServerErr Str]_ -respond! = \_, _ -> - when check_file! "good" is - Ok Good -> Ok { status: 200, headers: [], body: Str.toUtf8 "GOOD" } - Ok Bad -> Ok { status: 200, headers: [], body: Str.toUtf8 "BAD" } - Err IOError -> Ok { status: 500, headers: [], body: Str.toUtf8 "ERROR: IoError when executing checkFile!." } +respond! = |_, _| + when check_file!("good") is + Ok(Good) -> Ok({ status: 200, headers: [], body: Str.to_utf8("GOOD") }) + Ok(Bad) -> Ok({ status: 200, headers: [], body: Str.to_utf8("BAD") }) + Err(IOError) -> Ok({ status: 500, headers: [], body: Str.to_utf8("ERROR: IoError when executing checkFile!.") }) # imagine this function does some IO operation # and returns a Result, succeding with a tag either Good or Bad, # or failing with an IOError check_file! : Str => Result [Good, Bad] [IOError] -check_file! = \str -> +check_file! = |str| if str == "good" then - Ok Good + Ok(Good) else if str == "bad" then - Ok Bad + Ok(Bad) else - Err IOError + Err(IOError) diff --git a/examples/sqlite.roc b/examples/sqlite.roc index effed49..1269756 100644 --- a/examples/sqlite.roc +++ b/examples/sqlite.roc @@ -8,45 +8,46 @@ import pf.Env Model : { stmt : Sqlite.Stmt } init! : {} => Result Model _ -init! = \{} -> +init! = |{}| # Read DB_PATH environment variable - db_path = - Env.var! "DB_PATH" - |> Result.mapErr \_ -> ServerErr "DB_PATH not set on environment" - |> try + db_path = Env.var!("DB_PATH") ? |_| ServerErr("DB_PATH not set on environment") stmt = - Sqlite.prepare! { - path: db_path, - query: "SELECT id, task FROM todos WHERE status = :status;", - } - |> Result.mapErr \err -> ServerErr "Failed to prepare Sqlite statement: $(Inspect.toStr err)" - |> try + Sqlite.prepare!( + { + path: db_path, + query: "SELECT id, task FROM todos WHERE status = :status;", + }, + ) + ? |err| ServerErr("Failed to prepare Sqlite statement: ${Inspect.to_str(err)}") - Ok { stmt } + Ok({ stmt }) respond! : Request, Model => Result Response _ -respond! = \_, { stmt } -> +respond! = |_, { stmt }| # Query todos table strings : Str strings = - Sqlite.query_many_prepared! { - stmt, - bindings: [{ name: ":status", value: String "completed" }], - rows: { Sqlite.decode_record <- - id: Sqlite.i64 "id", - task: Sqlite.str "task", + Sqlite.query_many_prepared!( + { + stmt, + bindings: [{ name: ":status", value: String("completed") }], + rows: { Sqlite.decode_record <- + id: Sqlite.i64("id"), + task: Sqlite.str("task"), + }, }, - } - |> try - |> List.map \{ id, task } -> "row $(Num.toStr id), task: $(task)" - |> Str.joinWith "\n" + )? + |> List.map(|{ id, task }| "row ${Num.to_str(id)}, task: ${task}") + |> Str.join_with("\n") # Print out the results - try Stdout.line! strings - - Ok { - status: 200, - headers: [{ name: "Content-Type", value: "text/html; charset=utf-8" }], - body: Str.toUtf8 strings, - } + Stdout.line!(strings)? + + Ok( + { + status: 200, + headers: [{ name: "Content-Type", value: "text/html; charset=utf-8" }], + body: Str.to_utf8(strings), + }, + ) diff --git a/examples/temp-dir.roc b/examples/temp-dir.roc index b9d002d..8da6f09 100644 --- a/examples/temp-dir.roc +++ b/examples/temp-dir.roc @@ -12,11 +12,18 @@ import pf.Env Model : {} init! : {} => Result Model [] -init! = \{} -> Ok {} +init! = |{}| + Ok({}) respond! : Request, Model => Result Response [ServerErr Str]_ -respond! = \_, _ -> +respond! = |_, _| - temp_dir_str = Path.display (Env.temp_dir! {}) + temp_dir_str = Path.display(Env.temp_dir!({})) - Ok { status: 200, headers: [], body: Str.toUtf8 "The temp dir path is $(temp_dir_str)" } + Ok( + { + status: 200, + headers: [], + body: Str.to_utf8("The temp dir path is ${temp_dir_str}"), + }, + ) diff --git a/examples/todos.db-wal b/examples/todos.db-wal index e69de29..a0e8f16 100644 Binary files a/examples/todos.db-wal and b/examples/todos.db-wal differ diff --git a/examples/todos.roc b/examples/todos.roc index 419221d..39dadea 100644 --- a/examples/todos.roc +++ b/examples/todos.roc @@ -7,7 +7,7 @@ import pf.Sqlite import pf.Stdout import pf.Url import pf.Utc -import "todos.html" as todoHtml : List U8 +import "todos.html" as todo_html : List U8 Model : { list_todos_stmt : Sqlite.Stmt, @@ -19,37 +19,37 @@ Model : { } init! : {} => Result Model [Exit I32 Str]_ -init! = \{} -> - db_path = try read_env_var! "DB_PATH" +init! = |{}| + db_path = read_env_var!("DB_PATH")? - list_todos_stmt = try prepare_stmt! db_path "SELECT id, task, status FROM todos" - create_todo_stmt = try prepare_stmt! db_path "INSERT INTO todos (task, status) VALUES (:task, :status)" - last_created_todo_stmt = try prepare_stmt! db_path "SELECT id, task, status FROM todos WHERE id = last_insert_rowid()" - begin_stmt = try prepare_stmt! db_path "BEGIN" - end_stmt = try prepare_stmt! db_path "END" - rollback_stmt = try prepare_stmt! db_path "ROLLBACK" + list_todos_stmt = prepare_stmt!(db_path, "SELECT id, task, status FROM todos")? + create_todo_stmt = prepare_stmt!(db_path, "INSERT INTO todos (task, status) VALUES (:task, :status)")? + last_created_todo_stmt = prepare_stmt!(db_path, "SELECT id, task, status FROM todos WHERE id = last_insert_rowid()")? + begin_stmt = prepare_stmt!(db_path, "BEGIN")? + end_stmt = prepare_stmt!(db_path, "END")? + rollback_stmt = prepare_stmt!(db_path, "ROLLBACK")? - Ok { list_todos_stmt, create_todo_stmt, last_created_todo_stmt, begin_stmt, end_stmt, rollback_stmt } + Ok({ list_todos_stmt, create_todo_stmt, last_created_todo_stmt, begin_stmt, end_stmt, rollback_stmt }) respond! : Request, Model => Result Response [ServerErr Str]_ -respond! = \req, model -> +respond! = |req, model| response_task = - try log_request! req + log_request!(req)? split_url = req.uri |> Url.from_str |> Url.path - |> Str.splitOn "/" + |> Str.split_on("/") # Route to handler based on url path when split_url is - ["", ""] -> byte_response 200 todoHtml - ["", "todos", ..] -> route_todos! model req - _ -> text_response 404 "URL Not Found (404)" + ["", ""] -> byte_response(200, todo_html) + ["", "todos", ..] -> route_todos!(model, req) + _ -> text_response(404, "URL Not Found (404)") # Handle any application errors - response_task |> Result.mapErr map_app_err + response_task |> Result.map_err(map_app_err) AppError : [ EnvVarNotSet Str, @@ -57,183 +57,216 @@ AppError : [ ] map_app_err : AppError -> [ServerErr Str] -map_app_err = \app_err -> +map_app_err = |app_err| when app_err is - EnvVarNotSet var_name -> ServerErr "Environment variable \"$(var_name)\" was not set. Please set it to the path of todos.db" - StdoutErr msg -> ServerErr msg + EnvVarNotSet(var_name) -> ServerErr("Environment variable \"${var_name}\" was not set. Please set it to the path of todos.db") + StdoutErr(msg) -> ServerErr(msg) route_todos! : Model, Request => Result Response _ -route_todos! = \model, req -> +route_todos! = |model, req| when req.method is GET -> - list_todos! model + list_todos!(model) POST -> # Create todo - when task_from_query req.uri is - Ok props -> create_todo! model props - Err InvalidQuery -> text_response 400 "Invalid query string, I expected: ?task=foo&status=bar" + when task_from_query(req.uri) is + Ok(props) -> create_todo!(model, props) + Err(InvalidQuery) -> text_response(400, "Invalid query string, I expected: ?task=foo&status=bar") other_method -> # Not supported - text_response 405 "HTTP method $(Inspect.toStr other_method) is not supported for the URL $(req.uri)" + text_response(405, "HTTP method ${Inspect.to_str(other_method)} is not supported for the URL ${req.uri}") list_todos! : Model => Result Response _ -list_todos! = \{ list_todos_stmt } -> +list_todos! = |{ list_todos_stmt }| result = # TODO: it might be nicer if the decoder was stored with the prepared query instead of defined here. - Sqlite.query_many_prepared! { - stmt: list_todos_stmt, - bindings: [], - rows: { Sqlite.decode_record <- - id: Sqlite.i64 "id", - task: Sqlite.str "task", - status: Sqlite.str "status", + Sqlite.query_many_prepared!( + { + stmt: list_todos_stmt, + bindings: [], + rows: { Sqlite.decode_record <- + id: Sqlite.i64("id"), + task: Sqlite.str("task"), + status: Sqlite.str("status"), + }, }, - } + ) + when result is - Ok task -> + Ok(task) -> task - |> List.map encode_task - |> Str.joinWith "," - |> \list -> "[$(list)]" - |> Str.toUtf8 + |> List.map(encode_task) + |> Str.join_with(",") + |> |list| "[${list}]" + |> Str.to_utf8 |> json_response - Err err -> - err_response err + Err(err) -> + err_response(err) create_todo! : Model, { task : Str, status : Str } => Result Response _ -create_todo! = \model, params -> +create_todo! = |model, params| result = - exec_transaction! model \{} -> - try Sqlite.execute_prepared! { - stmt: model.create_todo_stmt, - bindings: [ - { name: ":task", value: String params.task }, - { name: ":status", value: String params.status }, - ], - } - Sqlite.query_prepared! { - stmt: model.last_created_todo_stmt, - bindings: [], - row: { Sqlite.decode_record <- - id: Sqlite.i64 "id", - task: Sqlite.str "task", - status: Sqlite.str "status", - }, - } + exec_transaction!( + model, + |{}| + # prepare create todo statement + Sqlite.execute_prepared!( + { + stmt: model.create_todo_stmt, + bindings: [ + { name: ":task", value: String(params.task) }, + { name: ":status", value: String(params.status) }, + ], + }, + )? + + # prepate last created todo statement + Sqlite.query_prepared!( + { + stmt: model.last_created_todo_stmt, + bindings: [], + row: { Sqlite.decode_record <- + id: Sqlite.i64("id"), + task: Sqlite.str("task"), + status: Sqlite.str("status"), + }, + }, + ), + ) when result is - Ok task -> + Ok(task) -> task |> encode_task - |> Str.toUtf8 + |> Str.to_utf8 |> json_response - Err err -> - err_response err + Err(err) -> + err_response(err) exec_transaction! : Model, ({} => Result ok err) => Result ok [FailedToBeginTransaction, FailedToEndTransaction, FailedToRollbackTransaction, TransactionFailed err] -exec_transaction! = \{ begin_stmt, rollback_stmt, end_stmt }, transaction! -> +exec_transaction! = |{ begin_stmt, rollback_stmt, end_stmt }, transaction!| + # TODO: create a nicer transaction wrapper - Sqlite.execute_prepared! { - stmt: begin_stmt, - bindings: [], - } - |> Result.mapErr \_ -> FailedToBeginTransaction - |> try - - end_transaction! = \res -> + Sqlite.execute_prepared!( + { + stmt: begin_stmt, + bindings: [], + }, + ) + ? |_| FailedToBeginTransaction + + end_transaction! = |res| when res is - Ok v -> - Sqlite.execute_prepared! { - stmt: end_stmt, + Ok(v) -> + Sqlite.execute_prepared!( + { + stmt: end_stmt, + bindings: [], + }, + ) + ? |_| FailedToEndTransaction + + Ok(v) + + Err(e) -> + Err(TransactionFailed(e)) + + when transaction!({}) |> end_transaction! is + Ok(v) -> + Ok(v) + + Err(e) -> + Sqlite.execute_prepared!( + { + stmt: rollback_stmt, bindings: [], - } - |> Result.mapErr \_ -> FailedToEndTransaction - |> try - Ok v - - Err e -> - Err (TransactionFailed e) - - when transaction! {} |> end_transaction! is - Ok v -> - Ok v - - Err e -> - Sqlite.execute_prepared! { - stmt: rollback_stmt, - bindings: [], - } - |> Result.mapErr \_ -> FailedToRollbackTransaction + }, + ) + |> Result.map_err(|_| FailedToRollbackTransaction) |> try - Err e + Err(e) task_from_query : Str -> Result { task : Str, status : Str } [InvalidQuery] -task_from_query = \url -> +task_from_query = |url| + params = url |> Url.from_str |> Url.query_params - when (params |> Dict.get "task", params |> Dict.get "status") is - (Ok task, Ok status) -> Ok { task: Str.replaceEach task "%20" " ", status: Str.replaceEach status "%20" " " } - _ -> Err InvalidQuery + when (params |> Dict.get("task"), params |> Dict.get("status")) is + (Ok(task), Ok(status)) -> + Ok( + { + task: Str.replace_each(task, "%20", " "), + status: Str.replace_each(status, "%20", " "), + }, + ) + + _ -> Err(InvalidQuery) encode_task : { id : I64, task : Str, status : Str } -> Str -encode_task = \{ id, task, status } -> +encode_task = |{ id, task, status }| # TODO: this should use our json encoder """ - {"id":$(Num.toStr id),"task":"$(task)","status":"$(status)"} + {"id":${Num.to_str(id)},"task":"${task}","status":"${status}"} """ json_response : List U8 -> Result Response [] -json_response = \bytes -> - Ok { - status: 200, - headers: [ - { name: "Content-Type", value: "application/json; charset=utf-8" }, - ], - body: bytes, - } +json_response = |bytes| + Ok( + { + status: 200, + headers: [ + { name: "Content-Type", value: "application/json; charset=utf-8" }, + ], + body: bytes, + }, + ) err_response : err -> Result Response * where err implements Inspect -err_response = \err -> - byte_response 500 (Str.toUtf8 (Inspect.toStr err)) +err_response = |err| + byte_response(500, Str.to_utf8(Inspect.to_str(err))) text_response : U16, Str -> Result Response [] -text_response = \status, str -> - Ok { - status, - headers: [ - { name: "Content-Type", value: "text/html; charset=utf-8" }, - ], - body: Str.toUtf8 str, - } +text_response = |status, str| + Ok( + { + status, + headers: [ + { name: "Content-Type", value: "text/html; charset=utf-8" }, + ], + body: Str.to_utf8(str), + }, + ) byte_response : U16, List U8 -> Result Response * -byte_response = \status, bytes -> - Ok { - status, - headers: [ - { name: "Content-Type", value: "text/html; charset=utf-8" }, - ], - body: bytes, - } +byte_response = |status, bytes| + Ok( + { + status, + headers: [ + { name: "Content-Type", value: "text/html; charset=utf-8" }, + ], + body: bytes, + }, + ) log_request! : Request => Result {} [StdoutErr Str] -log_request! = \req -> - datetime = Utc.to_iso_8601 (Utc.now! {}) +log_request! = |req| + datetime = Utc.to_iso_8601(Utc.now!({})) - Stdout.line! "$(datetime) $(Inspect.toStr req.method) $(req.uri)" - |> Result.mapErr \err -> StdoutErr (Inspect.toStr err) + Stdout.line!("${datetime} ${Inspect.to_str(req.method)} ${req.uri}") + |> Result.map_err(|err| StdoutErr(Inspect.to_str(err))) read_env_var! : Str => Result Str [EnvVarNotSet Str]_ -read_env_var! = \env_var_name -> - Env.var! env_var_name - |> Result.mapErr \_ -> EnvVarNotSet env_var_name +read_env_var! = |env_var_name| + Env.var!(env_var_name) + |> Result.map_err(|_| EnvVarNotSet(env_var_name)) prepare_stmt! : Str, Str => Result Sqlite.Stmt [FailedToPrepareQuery _] -prepare_stmt! = \path, query -> - Sqlite.prepare! { path, query } - |> Result.mapErr FailedToPrepareQuery +prepare_stmt! = |path, query| + Sqlite.prepare!({ path, query }) + |> Result.map_err(FailedToPrepareQuery) diff --git a/flake.lock b/flake.lock index fdc58e2..f6e196e 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1732722421, - "narHash": "sha256-HRJ/18p+WoXpWJkcdsk9St5ZiukCqSDgbOGFa8Okehg=", + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", "owner": "edolstra", "repo": "flake-compat", - "rev": "9ed2ac151eada2306ca8c418ebd97807bb08f6ac", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", "type": "github" }, "original": { @@ -102,11 +102,11 @@ "rust-overlay": "rust-overlay" }, "locked": { - "lastModified": 1735379823, - "narHash": "sha256-HVcLUsA9YhV7UVpRuZkgq1v4MLnZ1/ppjF5c9WmsVLs=", + "lastModified": 1737745102, + "narHash": "sha256-y/e5SAV36ESCU1eKPtfn44Vyl/LFt2AkuDu8G0YhTKc=", "owner": "roc-lang", "repo": "roc", - "rev": "9b13b19d08523982d11178cce45ed625ae1f9e0e", + "rev": "cf1cfefadbf16af21a74bbe16f1547f358ac9874", "type": "github" }, "original": { @@ -134,11 +134,11 @@ ] }, "locked": { - "lastModified": 1732802692, - "narHash": "sha256-kFrxb45qj52TT/OFUFyTdmvXkn/KXDUL0/DOtjHEQvs=", + "lastModified": 1736303309, + "narHash": "sha256-IKrk7RL+Q/2NC6+Ql6dwwCNZI6T6JH2grTdJaVWHF0A=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "34971069ec33755b2adf2481851f66d8ec9a6bfa", + "rev": "a0b81d4fa349d9af1765b0f0b4a899c13776f706", "type": "github" }, "original": { @@ -154,11 +154,11 @@ ] }, "locked": { - "lastModified": 1735352767, - "narHash": "sha256-3zXufMRWUdwmp8/BTmxVW/k4MyqsPjLnnt/IlQyZvhc=", + "lastModified": 1737771740, + "narHash": "sha256-lWIdF4qke63TdCHnJ0QaUHfG8YvsDrBqzL4jiHYQd+Y=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "a16b9a7cac7f4d39a84234d62e91890370c57d76", + "rev": "cfaaa1dddd280af09aca84af84612fbccd986ae2", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 7072449..5dc9808 100644 --- a/flake.nix +++ b/flake.nix @@ -3,6 +3,7 @@ inputs = { roc.url = "github:roc-lang/roc"; + nixpkgs.follows = "roc/nixpkgs"; diff --git a/platform/Cmd.roc b/platform/Cmd.roc index 109cd66..16b8e8d 100644 --- a/platform/Cmd.roc +++ b/platform/Cmd.roc @@ -24,78 +24,80 @@ Output : InternalCmd.Output ## Create a new command to execute the given program in a child process. new : Str -> Cmd -new = \program -> - @Cmd { - program, - args: [], - envs: [], - clear_envs: Bool.false, - } +new = |program| + @Cmd( + { + program, + args: [], + envs: [], + clear_envs: Bool.false, + }, + ) ## Add a single argument to the command. ## ! Shell features like variable subsitition (e.g. `$FOO`), glob patterns (e.g. `*.txt`), ... are not available. ## ## ``` ## # Represent the command "ls -l" -## Cmd.new "ls" -## |> Cmd.arg "-l" +## Cmd.new("ls") +## |> Cmd.arg("-l") ## ``` ## arg : Cmd, Str -> Cmd -arg = \@Cmd cmd, value -> - @Cmd ({ cmd & args: List.append cmd.args value }) +arg = |@Cmd(cmd), value| + @Cmd({ cmd & args: List.append(cmd.args, value) }) ## Add multiple arguments to the command. ## ! Shell features like variable subsitition (e.g. `$FOO`), glob patterns (e.g. `*.txt`), ... are not available. ## ## ``` ## # Represent the command "ls -l -a" -## Cmd.new "ls" -## |> Cmd.args ["-l", "-a"] +## Cmd.new("ls") +## |> Cmd.args(["-l", "-a"]) ## ``` ## args : Cmd, List Str -> Cmd -args = \@Cmd cmd, values -> - @Cmd ({ cmd & args: List.concat cmd.args values }) +args = |@Cmd(cmd), values| + @Cmd({ cmd & args: List.concat(cmd.args, values) }) ## Add a single environment variable to the command. ## ## ``` ## # Run "env" and add the environment variable "FOO" with value "BAR" -## Cmd.new "env" -## |> Cmd.env "FOO" "BAR" +## Cmd.new("env") +## |> Cmd.env("FOO", "BAR") ## ``` ## env : Cmd, Str, Str -> Cmd -env = \@Cmd cmd, key, value -> - @Cmd ({ cmd & envs: List.concat cmd.envs [key, value] }) +env = |@Cmd(cmd), key, value| + @Cmd({ cmd & envs: List.concat(cmd.envs, [key, value]) }) ## Add multiple environment variables to the command. ## ## ``` ## # Run "env" and add the variables "FOO" and "BAZ" -## Cmd.new "env" -## |> Cmd.envs [("FOO", "BAR"), ("BAZ", "DUCK")] +## Cmd.new("env") +## |> Cmd.envs([("FOO", "BAR"), ("BAZ", "DUCK")]) ## ``` ## envs : Cmd, List (Str, Str) -> Cmd -envs = \@Cmd cmd, key_values -> - values = key_values |> List.joinMap \(key, value) -> [key, value] - @Cmd { cmd & envs: List.concat cmd.envs values } +envs = |@Cmd(cmd), key_values| + values = key_values |> List.join_map(|(key, value)| [key, value]) + @Cmd({ cmd & envs: List.concat(cmd.envs, values) }) ## Clear all environment variables, and prevent inheriting from parent, only ## the environment variables provided to command are available to the child. ## ## ``` ## # Represents "env" with only "FOO" environment variable set -## Cmd.new "env" +## Cmd.new("env") ## |> Cmd.clear_envs -## |> Cmd.env "FOO" "BAR" +## |> Cmd.env("FOO", "BAR") ## ``` ## clear_envs : Cmd -> Cmd -clear_envs = \@Cmd cmd -> - @Cmd { cmd & clear_envs: Bool.true } +clear_envs = |@Cmd(cmd)| + @Cmd({ cmd & clear_envs: Bool.true }) ## Execute command and capture stdout and stderr ## @@ -103,32 +105,32 @@ clear_envs = \@Cmd cmd -> ## > to read from the stdin stream will result in the stream immediately closing. ## output! : Cmd => Output -output! = \@Cmd cmd -> - Host.command_output! cmd +output! = |@Cmd(cmd)| + Host.command_output!(cmd) |> InternalCmd.from_host_output ## Execute command and inherit stdin, stdout and stderr from parent ## status! : Cmd => Result I32 [CmdStatusErr InternalIOErr.IOErr] -status! = \@Cmd cmd -> - Host.command_status! cmd - |> Result.mapErr InternalIOErr.handle_err - |> Result.mapErr CmdStatusErr +status! = |@Cmd(cmd)| + Host.command_status!(cmd) + |> Result.map_err(InternalIOErr.handle_err) + |> Result.map_err(CmdStatusErr) ## Execute command and inherit stdin, stdout and stderr from parent ## ## ``` ## # Call echo to print "hello world" -## Cmd.exec! "echo" ["hello world"] +## Cmd.exec!("echo", ["hello world"]) ## ``` exec! : Str, List Str => Result {} [CmdStatusErr InternalIOErr.IOErr] -exec! = \program, arguments -> +exec! = |program, arguments| exit_code = - new program - |> args arguments + new(program) + |> args(arguments) |> status!? if exit_code == 0i32 then - Ok {} + Ok({}) else - Err (CmdStatusErr (Other "Non-zero exit code $(Num.toStr exit_code)")) + Err(CmdStatusErr(Other("Non-zero exit code ${Num.to_str(exit_code)}"))) diff --git a/platform/Dir.roc b/platform/Dir.roc index 5fd9a9b..4e9a843 100644 --- a/platform/Dir.roc +++ b/platform/Dir.roc @@ -25,8 +25,8 @@ DirEntry : Path.DirEntry ## ## > [Path.list_dir!] does the same thing, except it takes a [Path] instead of a [Str]. list! : Str => Result (List Path) [DirErr IOErr] -list! = \path -> - Path.list_dir! (Path.from_str path) +list! = |path| + Path.list_dir!(Path.from_str(path)) ## Deletes a directory if it's empty ## @@ -38,8 +38,8 @@ list! = \path -> ## ## > [Path.delete_empty!] does the same thing, except it takes a [Path] instead of a [Str]. delete_empty! : Str => Result {} [DirErr IOErr] -delete_empty! = \path -> - Path.delete_empty! (Path.from_str path) +delete_empty! = |path| + Path.delete_empty!(Path.from_str(path)) ## Recursively deletes the directory as well as all files and directories ## inside it. @@ -52,8 +52,8 @@ delete_empty! = \path -> ## ## > [Path.delete_all!] does the same thing, except it takes a [Path] instead of a [Str]. delete_all! : Str => Result {} [DirErr IOErr] -delete_all! = \path -> - Path.delete_all! (Path.from_str path) +delete_all! = |path| + Path.delete_all!(Path.from_str(path)) ## Creates a directory ## @@ -64,8 +64,8 @@ delete_all! = \path -> ## ## > [Path.create_dir!] does the same thing, except it takes a [Path] instead of a [Str]. create! : Str => Result {} [DirErr IOErr] -create! = \path -> - Path.create_dir! (Path.from_str path) +create! = |path| + Path.create_dir!(Path.from_str(path)) ## Creates a directory recursively adding any missing parent directories. ## @@ -75,5 +75,5 @@ create! = \path -> ## ## > [Path.create_all!] does the same thing, except it takes a [Path] instead of a [Str]. create_all! : Str => Result {} [DirErr IOErr] -create_all! = \path -> - Path.create_all! (Path.from_str path) +create_all! = |path| + Path.create_all!(Path.from_str(path)) diff --git a/platform/Env.roc b/platform/Env.roc index 14c015f..980f076 100644 --- a/platform/Env.roc +++ b/platform/Env.roc @@ -17,37 +17,37 @@ import Host ## Reads the [current working directory](https://en.wikipedia.org/wiki/Working_directory) ## from the environment. File operations on relative [Path]s are relative to this directory. cwd! : {} => Result Path [CwdUnavailable] -cwd! = \{} -> - bytes = Host.cwd! {} |> Result.withDefault [] +cwd! = |{}| + bytes = Host.cwd!({}) |> Result.with_default([]) - if List.isEmpty bytes then - Err CwdUnavailable + if List.is_empty(bytes) then + Err(CwdUnavailable) else - Ok (InternalPath.from_arbitrary_bytes bytes) + Ok(InternalPath.from_arbitrary_bytes(bytes)) ## Sets the [current working directory](https://en.wikipedia.org/wiki/Working_directory) ## in the environment. After changing it, file operations on relative [Path]s will be relative ## to this directory. set_cwd! : Path => Result {} [InvalidCwd] -set_cwd! = \path -> - Host.set_cwd! (InternalPath.to_bytes path) - |> Result.mapErr \{} -> InvalidCwd +set_cwd! = |path| + Host.set_cwd!(InternalPath.to_bytes(path)) + |> Result.map_err(|{}| InvalidCwd) ## Gets the path to the currently-running executable. exe_path! : {} => Result Path [ExePathUnavailable] -exe_path! = \{} -> - when Host.exe_path! {} is - Ok bytes -> Ok (InternalPath.from_os_bytes bytes) - Err {} -> Err ExePathUnavailable +exe_path! = |{}| + when Host.exe_path!({}) is + Ok(bytes) -> Ok(InternalPath.from_os_bytes(bytes)) + Err({}) -> Err(ExePathUnavailable) ## Reads the given environment variable. ## ## If the value is invalid Unicode, the invalid parts will be replaced with the ## [Unicode replacement character](https://unicode.org/glossary/#replacement_character) ('�'). var! : Str => Result Str [VarNotFound] -var! = \name -> - Host.env_var! name - |> Result.mapErr \{} -> VarNotFound +var! = |name| + Host.env_var!(name) + |> Result.map_err(|{}| VarNotFound) ## Reads the given environment variable and attempts to decode it. ## @@ -66,7 +66,7 @@ var! = \name -> ## ``` ## # Reads "NUM_THINGS" and decodes into a U16 ## get_u16_var! : Str => Result U16 [VarNotFound, DecodeErr DecodeError] [Read [Env]] -## get_u16_var! = \var -> Env.decode! var +## get_u16_var! = \var -> Env.decode!(var) ## ``` ## ## If `NUM_THINGS=123` then `getU16Var` succeeds with the value of `123u16`. @@ -75,27 +75,27 @@ var! = \name -> ## because `123456789` is too large to fit in a [U16](https://www.roc-lang.org/builtins/Num#U16). ## decode! : Str => Result val [VarNotFound, DecodeErr DecodeError] where val implements Decoding -decode! = \name -> - when Host.env_var! name is - Err {} -> Err VarNotFound - Ok var_str -> - Str.toUtf8 var_str - |> Decode.fromBytes (EnvDecoding.format {}) - |> Result.mapErr (\_ -> DecodeErr TooShort) +decode! = |name| + when Host.env_var!(name) is + Err({}) -> Err(VarNotFound) + Ok(var_str) -> + Str.to_utf8(var_str) + |> Decode.from_bytes(EnvDecoding.format({})) + |> Result.map_err(|_| DecodeErr(TooShort)) ## Reads all the process's environment variables into a [Dict]. ## ## If any key or value contains invalid Unicode, the [Unicode replacement character](https://unicode.org/glossary/#replacement_character) ## will be used in place of any parts of keys or values that are invalid Unicode. dict! : {} => Dict Str Str -dict! = \{} -> - Host.env_dict! {} - |> Dict.fromList +dict! = |{}| + Host.env_dict!({}) + |> Dict.from_list # ## Walks over the process's environment variables as key-value arguments to the walking function. # ## # ## Env.walk "Vars:\n" \state, key, value -> -# ## "- $(key): $(value)\n" +# ## "- ${key}: ${value}\n" # ## # This might produce a string such as: # ## # # ## # """ @@ -135,9 +135,9 @@ OS : [LINUX, MACOS, WINDOWS, OTHER Str] ## Note these values are constants from when the platform is built. ## platform! : {} => { arch : ARCH, os : OS } -platform! = \{} -> +platform! = |{}| - from_rust = Host.current_arch_os! {} + from_rust = Host.current_arch_os!({}) arch = when from_rust.arch is @@ -145,14 +145,14 @@ platform! = \{} -> "x86_64" -> X64 "arm" -> ARM "aarch64" -> AARCH64 - _ -> OTHER from_rust.arch + _ -> OTHER(from_rust.arch) os = when from_rust.os is "linux" -> LINUX "macos" -> MACOS "windows" -> WINDOWS - _ -> OTHER from_rust.os + _ -> OTHER(from_rust.os) { arch, os } @@ -166,6 +166,6 @@ platform! = \{} -> ## result in “insecure temporary file” security vulnerabilities. ## temp_dir! : {} => Path -temp_dir! = \{} -> - Host.temp_dir! {} +temp_dir! = |{}| + Host.temp_dir!({}) |> InternalPath.from_os_bytes diff --git a/platform/EnvDecoding.roc b/platform/EnvDecoding.roc index 0ca77b5..4c71080 100644 --- a/platform/EnvDecoding.roc +++ b/platform/EnvDecoding.roc @@ -27,82 +27,95 @@ EnvFormat := {} implements [ ] format : {} -> EnvFormat -format = \{} -> @EnvFormat {} +format = |{}| @EnvFormat({}) -decode_bytes_to_num = \bytes, transformer -> - when Str.fromUtf8 bytes is - Ok s -> - when transformer s is - Ok n -> { result: Ok n, rest: [] } - Err _ -> { result: Err TooShort, rest: bytes } +decode_bytes_to_num = |bytes, transformer| + when Str.from_utf8(bytes) is + Ok(s) -> + when transformer(s) is + Ok(n) -> { result: Ok(n), rest: [] } + Err(_) -> { result: Err(TooShort), rest: bytes } - Err _ -> { result: Err TooShort, rest: bytes } + Err(_) -> { result: Err(TooShort), rest: bytes } -env_u8 = Decode.custom \bytes, @EnvFormat {} -> decode_bytes_to_num bytes Str.toU8 -env_u16 = Decode.custom \bytes, @EnvFormat {} -> decode_bytes_to_num bytes Str.toU16 -env_u32 = Decode.custom \bytes, @EnvFormat {} -> decode_bytes_to_num bytes Str.toU32 -env_u64 = Decode.custom \bytes, @EnvFormat {} -> decode_bytes_to_num bytes Str.toU64 -env_u128 = Decode.custom \bytes, @EnvFormat {} -> decode_bytes_to_num bytes Str.toU128 -env_i8 = Decode.custom \bytes, @EnvFormat {} -> decode_bytes_to_num bytes Str.toI8 -env_i16 = Decode.custom \bytes, @EnvFormat {} -> decode_bytes_to_num bytes Str.toI16 -env_i32 = Decode.custom \bytes, @EnvFormat {} -> decode_bytes_to_num bytes Str.toI32 -env_i64 = Decode.custom \bytes, @EnvFormat {} -> decode_bytes_to_num bytes Str.toI64 -env_i128 = Decode.custom \bytes, @EnvFormat {} -> decode_bytes_to_num bytes Str.toI128 -env_f32 = Decode.custom \bytes, @EnvFormat {} -> decode_bytes_to_num bytes Str.toF32 -env_f64 = Decode.custom \bytes, @EnvFormat {} -> decode_bytes_to_num bytes Str.toF64 -env_dec = Decode.custom \bytes, @EnvFormat {} -> decode_bytes_to_num bytes Str.toDec +env_u8 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_u8)) +env_u16 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_u16)) +env_u32 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_u32)) +env_u64 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_u64)) +env_u128 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_u128)) +env_i8 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_i8)) +env_i16 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_i16)) +env_i32 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_i32)) +env_i64 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_i64)) +env_i128 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_i128)) +env_f32 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_f32)) +env_f64 = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_f64)) +env_dec = Decode.custom(|bytes, @EnvFormat({})| decode_bytes_to_num(bytes, Str.to_dec)) -env_bool = Decode.custom \bytes, @EnvFormat {} -> - when Str.fromUtf8 bytes is - Ok "true" -> { result: Ok Bool.true, rest: [] } - Ok "false" -> { result: Ok Bool.false, rest: [] } - _ -> { result: Err TooShort, rest: bytes } +env_bool = Decode.custom( + |bytes, @EnvFormat({})| + when Str.from_utf8(bytes) is + Ok("true") -> { result: Ok(Bool.true), rest: [] } + Ok("false") -> { result: Ok(Bool.false), rest: [] } + _ -> { result: Err(TooShort), rest: bytes }, +) -env_string = Decode.custom \bytes, @EnvFormat {} -> - when Str.fromUtf8 bytes is - Ok s -> { result: Ok s, rest: [] } - Err _ -> { result: Err TooShort, rest: bytes } +env_string = Decode.custom( + |bytes, @EnvFormat({})| + when Str.from_utf8(bytes) is + Ok(s) -> { result: Ok(s), rest: [] } + Err(_) -> { result: Err(TooShort), rest: bytes }, +) -env_list = \decode_elem -> Decode.custom \bytes, @EnvFormat {} -> - # Per our supported methods of decoding, this is either a list of strings or - # a list of numbers; in either case, the list of bytes must be Utf-8 - # decodable. So just parse it as a list of strings and pass each chunk to - # the element decoder. By construction, our element decoders expect to parse - # a whole list of bytes anyway. - decode_elems = \all_bytes, accum -> - { to_parse, remainder } = - when List.splitFirst all_bytes (Num.toU8 ',') is - Ok { before, after } -> - { to_parse: before, remainder: Some after } +env_list = |decode_elem| + Decode.custom( + |bytes, @EnvFormat({})| + # Per our supported methods of decoding, this is either a list of strings or + # a list of numbers; in either case, the list of bytes must be Utf-8 + # decodable. So just parse it as a list of strings and pass each chunk to + # the element decoder. By construction, our element decoders expect to parse + # a whole list of bytes anyway. + decode_elems = |all_bytes, accum| + { to_parse, remainder } = + when List.split_first(all_bytes, Num.to_u8(',')) is + Ok({ before, after }) -> + { to_parse: before, remainder: Some(after) } - Err NotFound -> - { to_parse: all_bytes, remainder: None } + Err(NotFound) -> + { to_parse: all_bytes, remainder: None } - when Decode.decodeWith to_parse decode_elem (@EnvFormat {}) is - { result, rest } -> - when result is - Ok val -> - when remainder is - Some rest_bytes -> decode_elems rest_bytes (List.append accum val) - None -> Done (List.append accum val) + when Decode.decode_with(to_parse, decode_elem, @EnvFormat({})) is + { result, rest } -> + when result is + Ok(val) -> + when remainder is + Some(rest_bytes) -> decode_elems(rest_bytes, List.append(accum, val)) + None -> Done(List.append(accum, val)) - Err e -> Errored e rest + Err(e) -> Errored(e, rest) - when decode_elems bytes [] is - Errored e rest -> { result: Err e, rest } - Done vals -> - { result: Ok vals, rest: [] } + when decode_elems(bytes, []) is + Errored(e, rest) -> { result: Err(e), rest } + Done(vals) -> + { result: Ok(vals), rest: [] }, + ) # TODO: we must currently annotate the arrows here so that the lambda sets are # exercised, and the solver can find an ambient lambda set for the # specialization. env_record : _, (_, _ -> [Keep (Decoder _ _), Skip]), (_, _ -> _) -> Decoder _ _ -env_record = \_initialState, _stepField, _finalizer -> Decode.custom \bytes, @EnvFormat {} -> - { result: Err TooShort, rest: bytes } +env_record = |_initialState, _stepField, _finalizer| + Decode.custom( + |bytes, @EnvFormat({})| + { result: Err(TooShort), rest: bytes }, + ) # TODO: we must currently annotate the arrows here so that the lambda sets are # exercised, and the solver can find an ambient lambda set for the # specialization. env_tuple : _, (_, _ -> [Next (Decoder _ _), TooLong]), (_ -> _) -> Decoder _ _ -env_tuple = \_initialState, _stepElem, _finalizer -> Decode.custom \bytes, @EnvFormat {} -> - { result: Err TooShort, rest: bytes } +env_tuple = |_initialState, _stepElem, _finalizer| + Decode.custom( + |bytes, @EnvFormat({})| + { result: Err(TooShort), rest: bytes }, + ) diff --git a/platform/File.roc b/platform/File.roc index 5d972c8..af9ace3 100644 --- a/platform/File.roc +++ b/platform/File.roc @@ -44,17 +44,18 @@ IOErr : InternalIOErr.IOErr ## ## First encode a `val` using a given `fmt` which implements the ability [Encode.EncoderFormatting](https://www.roc-lang.org/builtins/Encode#EncoderFormatting). ## -## For example, suppose you have a `Json.toCompactUtf8` which implements +## For example, suppose you have a `Json.to_compact_utf8` which implements ## [Encode.EncoderFormatting](https://www.roc-lang.org/builtins/Encode#EncoderFormatting). ## You can use this to write [JSON](https://en.wikipedia.org/wiki/JSON) ## data to a file like this: ## ## ``` ## # Writes `{"some":"json stuff"}` to the file `output.json`: -## File.write! -## { some: "json stuff" } -## (Path.from_str "output.json") -## Json.toCompactUtf8 +## File.write!( +## { some: "json stuff" }, +## Path.from_str("output.json"), +## Json.to_compact_utf8, +## ) ## ``` ## ## This opens the file first and closes it after writing to it. @@ -64,14 +65,14 @@ IOErr : InternalIOErr.IOErr ## > ## > [Path.write!] does the same thing, except it takes a [Path] instead of a [Str]. write! : val, Str, fmt => Result {} [FileWriteErr Path IOErr] where val implements Encoding, fmt implements EncoderFormatting -write! = \val, path, fmt -> - Path.write! val (Path.from_str path) fmt +write! = |val, path, fmt| + Path.write!(val, Path.from_str(path), fmt) ## Writes bytes to a file. ## ## ``` ## # Writes the bytes 1, 2, 3 to the file `myfile.dat`. -## File.write_bytes! [1, 2, 3] (Path.from_str "myfile.dat") +## File.write_bytes!([1, 2, 3], Path.from_str("myfile.dat"))? ## ``` ## ## This opens the file first and closes it after writing to it. @@ -80,14 +81,14 @@ write! = \val, path, fmt -> ## > ## > [Path.write_bytes!] does the same thing, except it takes a [Path] instead of a [Str]. write_bytes! : List U8, Str => Result {} [FileWriteErr Path IOErr] -write_bytes! = \bytes, path -> - Path.write_bytes! bytes (Path.from_str path) +write_bytes! = |bytes, path| + Path.write_bytes!(bytes, Path.from_str(path)) ## Writes a [Str] to a file, encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8). ## ## ``` ## # Writes "Hello!" encoded as UTF-8 to the file `myfile.txt`. -## File.write_utf8! "Hello!" "myfile.txt" +## File.write_utf8!("Hello!", "myfile.txt")? ## ``` ## ## This opens the file first and closes it after writing to it. @@ -96,8 +97,8 @@ write_bytes! = \bytes, path -> ## > ## > [Path.write_utf8!] does the same thing, except it takes a [Path] instead of a [Str]. write_utf8! : Str, Str => Result {} [FileWriteErr Path IOErr] -write_utf8! = \str, path -> - Path.write_utf8! str (Path.from_str path) +write_utf8! = |str, path| + Path.write_utf8!(str, Path.from_str(path)) ## Deletes a file from the filesystem. ## @@ -109,7 +110,7 @@ write_utf8! = \str, path -> ## ## ``` ## # Deletes the file named `myfile.dat` -## File.delete! (Path.from_str "myfile.dat") [1, 2, 3] +## File.delete!(Path.from_str("myfile.dat"), [1, 2, 3])? ## ``` ## ## > This does not securely erase the file's contents from disk; instead, the operating @@ -120,41 +121,41 @@ write_utf8! = \str, path -> ## > ## > [Path.delete!] does the same thing, except it takes a [Path] instead of a [Str]. delete! : Str => Result {} [FileWriteErr Path IOErr] -delete! = \path -> - Path.delete! (Path.from_str path) +delete! = |path| + Path.delete!(Path.from_str(path)) ## Reads all the bytes in a file. ## ## ``` ## # Read all the bytes in `myfile.txt`. -## File.read_bytes! "myfile.txt" +## bytes = File.read_bytes!("myfile.txt")? ## ``` ## ## This opens the file first and closes it after reading its contents. ## -## > To read and decode data from a file, you can use `File.read` instead. +## > To read and decode data from a file into a [Str], you can use [File.read_utf8!] instead. ## > ## > [Path.read_bytes!] does the same thing, except it takes a [Path] instead of a [Str]. read_bytes! : Str => Result (List U8) [FileReadErr Path IOErr] -read_bytes! = \path -> - Path.read_bytes! (Path.from_str path) +read_bytes! = |path| + Path.read_bytes!(Path.from_str(path)) ## Reads a [Str] from a file containing [UTF-8](https://en.wikipedia.org/wiki/UTF-8)-encoded text. ## ## ``` ## # Reads UTF-8 encoded text into a Str from the file "myfile.txt" -## File.read_utf8! "myfile.txt" +## str = File.read_utf8!("myfile.txt")? ## ``` ## -## This opens the file first and closes it after writing to it. +## This opens the file first and closes it after reading its contents. ## The task will fail with `FileReadUtf8Err` if the given file contains invalid UTF-8. ## ## > To read unformatted bytes from a file, you can use [File.read_bytes!] instead. ## ## > [Path.read_utf8!] does the same thing, except it takes a [Path] instead of a [Str]. read_utf8! : Str => Result Str [FileReadErr Path IOErr, FileReadUtf8Err Path _] -read_utf8! = \path -> - Path.read_utf8! (Path.from_str path) +read_utf8! = |path| + Path.read_utf8!(Path.from_str(path)) # read : Str, fmt => Result contents [FileReadErr Path ReadErr, FileReadDecodingFailed] where contents implements Decoding, fmt implements DecoderFormatting # read = \path, fmt -> @@ -169,8 +170,8 @@ read_utf8! = \path -> ## ## > [Path.hard_link!] does the same thing, except it takes a [Path] instead of a [Str]. hard_link! : Str => Result {} [LinkErr IOErr] -hard_link! = \path -> - Path.hard_link! (Path.from_str path) +hard_link! = |path| + Path.hard_link!(Path.from_str(path)) ## Returns True if the path exists on disk and is pointing at a directory. ## Returns False if the path exists and it is not a directory. If the path does not exist, @@ -180,8 +181,8 @@ hard_link! = \path -> ## ## > [Path.is_dir!] does the same thing, except it takes a [Path] instead of a [Str]. is_dir! : Str => Result Bool [PathErr IOErr] -is_dir! = \path -> - Path.is_dir! (Path.from_str path) +is_dir! = |path| + Path.is_dir!(Path.from_str(path)) ## Returns True if the path exists on disk and is pointing at a regular file. ## Returns False if the path exists and it is not a file. If the path does not exist, @@ -191,8 +192,8 @@ is_dir! = \path -> ## ## > [Path.is_file!] does the same thing, except it takes a [Path] instead of a [Str]. is_file! : Str => Result Bool [PathErr IOErr] -is_file! = \path -> - Path.is_file! (Path.from_str path) +is_file! = |path| + Path.is_file!(Path.from_str(path)) ## Returns True if the path exists on disk and is pointing at a symbolic link. ## Returns False if the path exists and it is not a symbolic link. If the path does not exist, @@ -202,16 +203,16 @@ is_file! = \path -> ## ## > [Path.is_sym_link!] does the same thing, except it takes a [Path] instead of a [Str]. is_sym_link! : Str => Result Bool [PathErr IOErr] -is_sym_link! = \path -> - Path.is_sym_link! (Path.from_str path) +is_sym_link! = |path| + Path.is_sym_link!(Path.from_str(path)) ## Return the type of the path if the path exists on disk. ## This uses [rust's std::path::is_symlink](https://doc.rust-lang.org/std/path/struct.Path.html#method.is_symlink). ## ## > [Path.type!] does the same thing, except it takes a [Path] instead of a [Str]. type! : Str => Result [IsFile, IsDir, IsSymLink] [PathErr IOErr] -type! = \path -> - Path.type! (Path.from_str path) +type! = |path| + Path.type!(Path.from_str(path)) Reader := { reader : Host.FileReader, path : Path } @@ -222,13 +223,13 @@ Reader := { reader : Host.FileReader, path : Path } ## ## Use [read_utf8!] if you want to get the entire file contents at once. open_reader! : Str => Result Reader [GetFileReadErr Path IOErr] -open_reader! = \path_str -> - path = Path.from_str path_str +open_reader! = |path_str| + path = Path.from_str(path_str) # 0 means with default capacity - Host.file_reader! (Str.toUtf8 path_str) 0 - |> Result.mapErr \err -> GetFileReadErr path (InternalIOErr.handle_err err) - |> Result.map \reader -> @Reader { reader, path } + Host.file_reader!(Str.to_utf8(path_str), 0) + |> Result.map_err(|err| GetFileReadErr(path, InternalIOErr.handle_err(err))) + |> Result.map_ok(|reader| @Reader({ reader, path })) ## Try to open a `File.Reader` for buffered (= part by part) reading given a path string. ## The buffer will be created with the specified capacity. @@ -238,12 +239,12 @@ open_reader! = \path_str -> ## ## Use [read_utf8!] if you want to get the entire file contents at once. open_reader_with_capacity! : Str, U64 => Result Reader [GetFileReadErr Path IOErr] -open_reader_with_capacity! = \path_str, capacity -> - path = Path.from_str path_str +open_reader_with_capacity! = |path_str, capacity| + path = Path.from_str(path_str) - Host.file_reader! (Str.toUtf8 path_str) capacity - |> Result.mapErr \err -> GetFileReadErr path (InternalIOErr.handle_err err) - |> Result.map \reader -> @Reader { reader, path } + Host.file_reader!(Str.to_utf8(path_str), capacity) + |> Result.map_err(|err| GetFileReadErr(path, InternalIOErr.handle_err(err))) + |> Result.map_ok(|reader| @Reader({ reader, path })) ## Try to read a line from a file given a Reader. ## The line will be provided as the list of bytes (`List U8`) until a newline (`0xA` byte). @@ -254,6 +255,6 @@ open_reader_with_capacity! = \path_str, capacity -> ## ## Use [read_utf8!] if you want to get the entire file contents at once. read_line! : Reader => Result (List U8) [FileReadErr Path IOErr] -read_line! = \@Reader { reader, path } -> - Host.file_read_line! reader - |> Result.mapErr \err -> FileReadErr path (InternalIOErr.handle_err err) +read_line! = |@Reader({ reader, path })| + Host.file_read_line!(reader) + |> Result.map_err(|err| FileReadErr(path, InternalIOErr.handle_err(err))) diff --git a/platform/FileMetadata.roc b/platform/FileMetadata.roc index b66dbc1..97c2231 100644 --- a/platform/FileMetadata.roc +++ b/platform/FileMetadata.roc @@ -20,19 +20,19 @@ FileMetadata := { ## Returns the number of bytes in the associated file. bytes : FileMetadata -> U64 -bytes = \@FileMetadata info -> info.bytes +bytes = |@FileMetadata(info)| info.bytes ## Returns [Bool.true] if the associated file is read-only. is_readonly : FileMetadata -> Bool -is_readonly = \@FileMetadata info -> info.is_readonly +is_readonly = |@FileMetadata(info)| info.is_readonly ## Returns the type of the associated file. type : FileMetadata -> [File, Dir, Symlink] -type = \@FileMetadata info -> info.type +type = |@FileMetadata(info)| info.type ## Returns the mode of the associated file. mode : FileMetadata -> [Unix U32, NonUnix] -mode = \@FileMetadata info -> info.mode +mode = |@FileMetadata(info)| info.mode # TODO need to create a Time module and return something like Time.Utc here. # lastModified : FileMetadata -> Utc diff --git a/platform/Http.roc b/platform/Http.roc index 696b7ff..f782764 100644 --- a/platform/Http.roc +++ b/platform/Http.roc @@ -48,7 +48,7 @@ default_request = { ## See common headers [here](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields). ## header : (Str, Str) -> Header -header = \(name, value) -> { name, value } +header = |(name, value)| { name, value } ## Send an HTTP request, succeeds with a value of [Str] or fails with an ## [Err]. @@ -56,16 +56,15 @@ header = \(name, value) -> { name, value } ## ``` ## # Prints out the HTML of the Roc-lang website. ## response = -## { Http.default_request & url: "https://www.roc-lang.org" } -## |> Http.send! +## Http.send!({ Http.default_request & url: "https://www.roc-lang.org" }) ## ## response.body -## |> Str.fromUtf8 -## |> Result.withDefault "Invalid UTF-8" +## |> Str.from_utf8 +## |> Result.with_default("Invalid UTF-8") ## |> Stdout.line ## ``` send! : Request => Response -send! = \request -> +send! = |request| request |> InternalHttp.to_host_request |> Host.send_request! @@ -77,20 +76,20 @@ send! = \request -> ## ``` ## import json.Json ## -## # On the server side we send `Encode.toBytes {foo: "Hello Json!"} Json.utf8` -## { foo } = Http.get! "http://localhost:8000" Json.utf8 +## # On the server side we send `Encode.to_bytes {foo: "Hello Json!"} Json.utf8` +## { foo } = Http.get!("http://localhost:8000", Json.utf8)? ## ``` get! : Str, fmt => Result body [HttpDecodingFailed] where body implements Decoding, fmt implements DecoderFormatting -get! = \uri, fmt -> - response = send! { default_request & uri } +get! = |uri, fmt| + response = send!({ default_request & uri }) - Decode.fromBytes response.body fmt - |> Result.mapErr \_ -> HttpDecodingFailed + Decode.from_bytes(response.body, fmt) + |> Result.map_err(|_| HttpDecodingFailed) get_utf8! : Str => Result Str [BadBody Str] -get_utf8! = \uri -> - response = send! { default_request & uri } +get_utf8! = |uri| + response = send!({ default_request & uri }) response.body - |> Str.fromUtf8 - |> Result.mapErr \_ -> BadBody "Invalid UTF-8" + |> Str.from_utf8 + |> Result.map_err(|_| BadBody("Invalid UTF-8")) diff --git a/platform/InternalCmd.roc b/platform/InternalCmd.roc index 2fe8f9b..b23ab71 100644 --- a/platform/InternalCmd.roc +++ b/platform/InternalCmd.roc @@ -21,8 +21,8 @@ Output : { } from_host_output : OutputFromHost -> Output -from_host_output = \{ status, stdout, stderr } -> { - status: Result.mapErr status InternalIOErr.handle_err, +from_host_output = |{ status, stdout, stderr }| { + status: Result.map_err(status, InternalIOErr.handle_err), stdout, stderr, } diff --git a/platform/InternalDateTime.roc b/platform/InternalDateTime.roc index 0b219e7..5349ce7 100644 --- a/platform/InternalDateTime.roc +++ b/platform/InternalDateTime.roc @@ -7,33 +7,33 @@ module [ DateTime : { year : I128, month : I128, day : I128, hours : I128, minutes : I128, seconds : I128 } to_iso_8601 : DateTime -> Str -to_iso_8601 = \{ year, month, day, hours, minutes, seconds } -> - year_str = year_with_padded_zeros year - month_str = month_with_padded_zeros month - day_str = day_with_padded_zeros day - hour_str = hours_with_padded_zeros hours - minute_str = minutes_with_padded_zeros minutes - seconds_str = seconds_with_padded_zeros seconds +to_iso_8601 = |{ year, month, day, hours, minutes, seconds }| + year_str = year_with_padded_zeros(year) + month_str = month_with_padded_zeros(month) + day_str = day_with_padded_zeros(day) + hour_str = hours_with_padded_zeros(hours) + minute_str = minutes_with_padded_zeros(minutes) + seconds_str = seconds_with_padded_zeros(seconds) - "$(year_str)-$(month_str)-$(day_str)T$(hour_str):$(minute_str):$(seconds_str)Z" + "${year_str}-${month_str}-${day_str}T${hour_str}:${minute_str}:${seconds_str}Z" year_with_padded_zeros : I128 -> Str -year_with_padded_zeros = \year -> - year_str = Num.toStr year +year_with_padded_zeros = |year| + year_str = Num.to_str(year) if year < 10 then - "000$(year_str)" + "000${year_str}" else if year < 100 then - "00$(year_str)" + "00${year_str}" else if year < 1000 then - "0$(year_str)" + "0${year_str}" else year_str month_with_padded_zeros : I128 -> Str -month_with_padded_zeros = \month -> - month_str = Num.toStr month +month_with_padded_zeros = |month| + month_str = Num.to_str(month) if month < 10 then - "0$(month_str)" + "0${month_str}" else month_str @@ -50,49 +50,49 @@ seconds_with_padded_zeros : I128 -> Str seconds_with_padded_zeros = month_with_padded_zeros is_leap_year : I128 -> Bool -is_leap_year = \year -> +is_leap_year = |year| (year % 4 == 0) - && # divided evenly by 4 unless... + and # divided evenly by 4 unless... ( (year % 100 != 0) - || # divided by 100 not a leap year + or # divided by 100 not a leap year (year % 400 == 0) # expecpt when also divisible by 400 ) -expect is_leap_year 2000 -expect is_leap_year 2012 -expect !(is_leap_year 1900) -expect !(is_leap_year 2015) -expect List.map [2023, 1988, 1992, 1996] is_leap_year == [Bool.false, Bool.true, Bool.true, Bool.true] -expect List.map [1700, 1800, 1900, 2100, 2200, 2300, 2500, 2600] is_leap_year == [Bool.false, Bool.false, Bool.false, Bool.false, Bool.false, Bool.false, Bool.false, Bool.false] +expect is_leap_year(2000) +expect is_leap_year(2012) +expect !(is_leap_year(1900)) +expect !(is_leap_year(2015)) +expect List.map([2023, 1988, 1992, 1996], is_leap_year) == [Bool.false, Bool.true, Bool.true, Bool.true] +expect List.map([1700, 1800, 1900, 2100, 2200, 2300, 2500, 2600], is_leap_year) == [Bool.false, Bool.false, Bool.false, Bool.false, Bool.false, Bool.false, Bool.false, Bool.false] days_in_month : I128, I128 -> I128 -days_in_month = \year, month -> - if List.contains [1, 3, 5, 7, 8, 10, 12] month then +days_in_month = |year, month| + if List.contains([1, 3, 5, 7, 8, 10, 12], month) then 31 - else if List.contains [4, 6, 9, 11] month then + else if List.contains([4, 6, 9, 11], month) then 30 else if month == 2 then - (if is_leap_year year then 29 else 28) + (if is_leap_year(year) then 29 else 28) else 0 -expect days_in_month 2023 1 == 31 # January -expect days_in_month 2023 2 == 28 # February -expect days_in_month 1996 2 == 29 # February in a leap year -expect days_in_month 2023 3 == 31 # March -expect days_in_month 2023 4 == 30 # April -expect days_in_month 2023 5 == 31 # May -expect days_in_month 2023 6 == 30 # June -expect days_in_month 2023 7 == 31 # July -expect days_in_month 2023 8 == 31 # August -expect days_in_month 2023 9 == 30 # September -expect days_in_month 2023 10 == 31 # October -expect days_in_month 2023 11 == 30 # November -expect days_in_month 2023 12 == 31 # December +expect days_in_month(2023, 1) == 31 # January +expect days_in_month(2023, 2) == 28 # February +expect days_in_month(1996, 2) == 29 # February in a leap year +expect days_in_month(2023, 3) == 31 # March +expect days_in_month(2023, 4) == 30 # April +expect days_in_month(2023, 5) == 31 # May +expect days_in_month(2023, 6) == 30 # June +expect days_in_month(2023, 7) == 31 # July +expect days_in_month(2023, 8) == 31 # August +expect days_in_month(2023, 9) == 30 # September +expect days_in_month(2023, 10) == 31 # October +expect days_in_month(2023, 11) == 30 # November +expect days_in_month(2023, 12) == 31 # December epoch_millis_to_datetime : I128 -> DateTime -epoch_millis_to_datetime = \millis -> +epoch_millis_to_datetime = |millis| seconds = millis // 1000 minutes = seconds // 60 hours = minutes // 60 @@ -100,56 +100,63 @@ epoch_millis_to_datetime = \millis -> month = 1 year = 1970 - epoch_millis_to_datetimeHelp { - year, - month, - day, - hours: hours % 24, - minutes: minutes % 60, - seconds: seconds % 60, - } + epoch_millis_to_datetimeHelp( + { + year, + month, + day, + hours: hours % 24, + minutes: minutes % 60, + seconds: seconds % 60, + }, + ) epoch_millis_to_datetimeHelp : DateTime -> DateTime -epoch_millis_to_datetimeHelp = \current -> - count_days_in_month = days_in_month current.year current.month +epoch_millis_to_datetimeHelp = |current| + count_days_in_month = days_in_month(current.year, current.month) count_days_in_prev_month = if current.month == 1 then - days_in_month (current.year - 1) 12 + days_in_month((current.year - 1), 12) else - days_in_month current.year (current.month - 1) + days_in_month(current.year, (current.month - 1)) if current.day < 1 then - epoch_millis_to_datetimeHelp + epoch_millis_to_datetimeHelp( { current & year: if current.month == 1 then current.year - 1 else current.year, month: if current.month == 1 then 12 else current.month - 1, day: current.day + count_days_in_prev_month, - } + }, + ) else if current.hours < 0 then - epoch_millis_to_datetimeHelp + epoch_millis_to_datetimeHelp( { current & day: current.day - 1, hours: current.hours + 24, - } + }, + ) else if current.minutes < 0 then - epoch_millis_to_datetimeHelp + epoch_millis_to_datetimeHelp( { current & hours: current.hours - 1, minutes: current.minutes + 60, - } + }, + ) else if current.seconds < 0 then - epoch_millis_to_datetimeHelp + epoch_millis_to_datetimeHelp( { current & minutes: current.minutes - 1, seconds: current.seconds + 60, - } + }, + ) else if current.day > count_days_in_month then - epoch_millis_to_datetimeHelp + epoch_millis_to_datetimeHelp( { current & year: if current.month == 12 then current.year + 1 else current.year, month: if current.month == 12 then 1 else current.month + 1, day: current.day - count_days_in_month, - } + }, + ) else current diff --git a/platform/InternalHttp.roc b/platform/InternalHttp.roc index 3936ee7..6bf0287 100644 --- a/platform/InternalHttp.roc +++ b/platform/InternalHttp.roc @@ -56,24 +56,24 @@ ResponseToAndFromHost : { } to_host_response : Response -> ResponseToAndFromHost -to_host_response = \{ status, headers, body } -> { +to_host_response = |{ status, headers, body }| { status, headers, body, } to_host_request : Request -> RequestToAndFromHost -to_host_request = \{ method, headers, uri, body, timeout_ms } -> { - method: to_host_method method, - method_ext: to_host_method_ext method, +to_host_request = |{ method, headers, uri, body, timeout_ms }| { + method: to_host_method(method), + method_ext: to_host_method_ext(method), headers, uri, body, - timeout_ms: to_host_timeout timeout_ms, + timeout_ms: to_host_timeout(timeout_ms), } to_host_method : Method -> _ -to_host_method = \method -> +to_host_method = |method| when method is OPTIONS -> 5 GET -> 3 @@ -84,31 +84,31 @@ to_host_method = \method -> TRACE -> 9 CONNECT -> 0 PATCH -> 6 - EXTENSION _ -> 2 + EXTENSION(_) -> 2 to_host_method_ext : Method -> Str -to_host_method_ext = \method -> +to_host_method_ext = |method| when method is - EXTENSION ext -> ext + EXTENSION(ext) -> ext _ -> "" to_host_timeout : _ -> U64 -to_host_timeout = \timeout -> +to_host_timeout = |timeout| when timeout is - TimeoutMilliseconds ms -> ms + TimeoutMilliseconds(ms) -> ms NoTimeout -> 0 from_host_request : RequestToAndFromHost -> Request -from_host_request = \{ method, method_ext, headers, uri, body, timeout_ms } -> { - method: from_host_method method method_ext, +from_host_request = |{ method, method_ext, headers, uri, body, timeout_ms }| { + method: from_host_method(method, method_ext), headers, uri, body, - timeout_ms: from_host_timeout timeout_ms, + timeout_ms: from_host_timeout(timeout_ms), } from_host_method : U64, Str -> Method -from_host_method = \tag, ext -> +from_host_method = |tag, ext| when tag is 5 -> OPTIONS 3 -> GET @@ -119,20 +119,20 @@ from_host_method = \tag, ext -> 9 -> TRACE 0 -> CONNECT 6 -> PATCH - 2 -> EXTENSION ext - _ -> crash "invalid tag from host" + 2 -> EXTENSION(ext) + _ -> crash("invalid tag from host") from_host_timeout : U64 -> [TimeoutMilliseconds U64, NoTimeout] -from_host_timeout = \timeout -> +from_host_timeout = |timeout| when timeout is 0 -> NoTimeout - _ -> TimeoutMilliseconds timeout + _ -> TimeoutMilliseconds(timeout) -expect from_host_timeout 0 == NoTimeout -expect from_host_timeout 1 == TimeoutMilliseconds 1 +expect from_host_timeout(0) == NoTimeout +expect from_host_timeout(1) == TimeoutMilliseconds(1) from_host_response : ResponseToAndFromHost -> Response -from_host_response = \{ status, headers, body } -> { +from_host_response = |{ status, headers, body }| { status, headers, body, diff --git a/platform/InternalIOErr.roc b/platform/InternalIOErr.roc index ef95d76..033a0da 100644 --- a/platform/InternalIOErr.roc +++ b/platform/InternalIOErr.roc @@ -46,7 +46,7 @@ IOErrFromHost : { } handle_err : IOErrFromHost -> IOErr -handle_err = \{ tag, msg } -> +handle_err = |{ tag, msg }| when tag is NotFound -> NotFound PermissionDenied -> PermissionDenied @@ -55,4 +55,4 @@ handle_err = \{ tag, msg } -> Interrupted -> Interrupted Unsupported -> Unsupported OutOfMemory -> OutOfMemory - Other | EndOfFile -> Other msg + Other | EndOfFile -> Other(msg) diff --git a/platform/InternalPath.roc b/platform/InternalPath.roc index 258fc8b..9713ca5 100644 --- a/platform/InternalPath.roc +++ b/platform/InternalPath.roc @@ -57,22 +57,22 @@ wrap : UnwrappedPath -> InternalPath wrap = @InternalPath unwrap : InternalPath -> UnwrappedPath -unwrap = \@InternalPath raw -> raw +unwrap = |@InternalPath(raw)| raw ## TODO do this in the host, and iterate over the Str ## bytes when possible instead of always converting to ## a heap-allocated List. to_bytes : InternalPath -> List U8 -to_bytes = \@InternalPath path -> +to_bytes = |@InternalPath(path)| when path is - FromOperatingSystem bytes -> bytes - ArbitraryBytes bytes -> bytes - FromStr str -> Str.toUtf8 str + FromOperatingSystem(bytes) -> bytes + ArbitraryBytes(bytes) -> bytes + FromStr(str) -> Str.to_utf8(str) from_arbitrary_bytes : List U8 -> InternalPath -from_arbitrary_bytes = \bytes -> - @InternalPath (ArbitraryBytes bytes) +from_arbitrary_bytes = |bytes| + @InternalPath(ArbitraryBytes(bytes)) from_os_bytes : List U8 -> InternalPath -from_os_bytes = \bytes -> - @InternalPath (FromOperatingSystem bytes) +from_os_bytes = |bytes| + @InternalPath(FromOperatingSystem(bytes)) diff --git a/platform/MultipartFormData.roc b/platform/MultipartFormData.roc index 585a226..18c488e 100644 --- a/platform/MultipartFormData.roc +++ b/platform/MultipartFormData.roc @@ -40,154 +40,181 @@ doubledash = ['-', '-'] ## ## Example call: ## ```roc -## parse_content_f { -## upper: Str.toUtf8 "Content-Disposition:", -## lower: Str.toUtf8 "content-disposition:", -## } +## parse_content_f({ +## upper: Str.to_utf8("Content-Disposition:"), +## lower: Str.to_utf8("content-disposition:"), +## }) ## -## input = Str.toUtf8 "\r\nContent-Disposition: form-data; name=\"sometext\"\r\nSome text here..." -## actual = parseContentDispositionF input -## expected = Ok { -## value: Str.toUtf8 " form-data; name=\"sometext\"", -## rest: Str.toUtf8 "\r\nSome text here...", -## } +## input = Str.to_utf8("\r\nContent-Disposition: form-data; name=\"sometext\"\r\nSome text here...") +## actual = parseContentDispositionF(input) +## expected = Ok({ +## value: Str.to_utf8(" form-data; name=\"sometext\""), +## rest: Str.to_utf8("\r\nSome text here..."), +## }) ## ``` ## parse_content_f : { upper : List U8, lower : List U8 } -> (List U8 -> Result { value : List U8, rest : List U8 } _) -parse_content_f = \{ upper, lower } -> \bytes -> - - toSearchUpper = List.concat newline upper - toSearchLower = List.concat newline lower - searchLength = List.len toSearchUpper - afterSearch = List.sublist bytes { start: searchLength, len: Num.maxU64 } - - if - List.startsWith bytes toSearchUpper - || List.startsWith bytes toSearchLower - then - nextLineStart = afterSearch |> List.findFirstIndex? \b -> b == '\r' - - Ok { - value: List.sublist afterSearch { start: 0, len: nextLineStart }, - rest: List.sublist afterSearch { start: nextLineStart, len: Num.maxU64 }, - } - else - Err ExpectedContent - -parseContentDispositionF = parse_content_f { - upper: Str.toUtf8 "Content-Disposition:", - lower: Str.toUtf8 "content-disposition:", -} +parse_content_f = |{ upper, lower }| + |bytes| + + to_search_upper = List.concat(newline, upper) + to_search_lower = List.concat(newline, lower) + search_length = List.len(to_search_upper) + after_search = List.sublist(bytes, { start: search_length, len: Num.max_u64 }) + + if + List.starts_with(bytes, to_search_upper) + or List.starts_with(bytes, to_search_lower) + then + next_line_start = after_search |> List.find_first_index?(|b| b == '\r') + + Ok( + { + value: List.sublist(after_search, { start: 0, len: next_line_start }), + rest: List.sublist(after_search, { start: next_line_start, len: Num.max_u64 }), + }, + ) + else + Err(ExpectedContent) + +parse_content_disposition_f = parse_content_f( + { + upper: Str.to_utf8("Content-Disposition:"), + lower: Str.to_utf8("content-disposition:"), + }, +) expect - input = Str.toUtf8 "\r\nContent-Disposition: form-data; name=\"sometext\"\r\nSome text here..." - actual = parseContentDispositionF input - expected = Ok { - value: Str.toUtf8 " form-data; name=\"sometext\"", - rest: Str.toUtf8 "\r\nSome text here...", - } + input = Str.to_utf8("\r\nContent-Disposition: form-data; name=\"sometext\"\r\nSome text here...") + actual = parse_content_disposition_f(input) + expected = Ok( + { + value: Str.to_utf8(" form-data; name=\"sometext\""), + rest: Str.to_utf8("\r\nSome text here..."), + }, + ) actual == expected -parseContentTypeF = parse_content_f { - upper: Str.toUtf8 "Content-Type:", - lower: Str.toUtf8 "content-type:", -} +parse_content_type_f = parse_content_f( + { + upper: Str.to_utf8("Content-Type:"), + lower: Str.to_utf8("content-type:"), + }, +) expect - input = Str.toUtf8 "\r\ncontent-type: multipart/mixed; boundary=abcde\r\nSome text here..." - actual = parseContentTypeF input - expected = Ok { - value: Str.toUtf8 " multipart/mixed; boundary=abcde", - rest: Str.toUtf8 "\r\nSome text here...", - } + input = Str.to_utf8("\r\ncontent-type: multipart/mixed; boundary=abcde\r\nSome text here...") + actual = parse_content_type_f(input) + expected = Ok( + { + value: Str.to_utf8(" multipart/mixed; boundary=abcde"), + rest: Str.to_utf8("\r\nSome text here..."), + }, + ) actual == expected -parseContentTransferEncodingF = parse_content_f { - upper: Str.toUtf8 "Content-Transfer-Encoding:", - lower: Str.toUtf8 "content-transfer-encoding:", -} +parse_content_transfer_encoding_f = parse_content_f( + { + upper: Str.to_utf8("Content-Transfer-Encoding:"), + lower: Str.to_utf8("content-transfer-encoding:"), + }, +) expect - input = Str.toUtf8 "\r\nContent-Transfer-Encoding: binary\r\nSome text here..." - actual = parseContentTransferEncodingF input - expected = Ok { - value: Str.toUtf8 " binary", - rest: Str.toUtf8 "\r\nSome text here...", - } + input = Str.to_utf8("\r\nContent-Transfer-Encoding: binary\r\nSome text here...") + actual = parse_content_transfer_encoding_f(input) + expected = Ok( + { + value: Str.to_utf8(" binary"), + rest: Str.to_utf8("\r\nSome text here..."), + }, + ) actual == expected ## Parses all headers: Content-Disposition, Content-Type and Content-Transfer-Encoding. -parseAllHeaders : List U8 -> Result FormData _ -parseAllHeaders = \bytes -> - - doubleNewlineLength = 4 # \r\n\r\n - - when parseContentDispositionF bytes is - Err err -> Err (ExpectedContentDisposition bytes err) - Ok { value: disposition, rest: first } -> - when parseContentTypeF first is - Err _ -> - Ok { - disposition, - type: [], - encoding: [], - data: List.dropFirst first doubleNewlineLength, - } - - Ok { value: type, rest: second } -> - when parseContentTransferEncodingF second is - Err _ -> - Ok { - disposition, - type, - encoding: [], - data: List.dropFirst second doubleNewlineLength, - } - - Ok { value: encoding, rest } -> - Ok { - disposition, - type, - encoding, - data: List.dropFirst rest doubleNewlineLength, - } +parse_all_headers : List U8 -> Result FormData _ +parse_all_headers = |bytes| + + double_newline_length = 4 # \r\n\r\n + + when parse_content_disposition_f(bytes) is + Err(err) -> Err(ExpectedContentDisposition(bytes, err)) + Ok({ value: disposition, rest: first }) -> + when parse_content_type_f(first) is + Err(_) -> + Ok( + { + disposition, + type: [], + encoding: [], + data: List.drop_first(first, double_newline_length), + }, + ) + + Ok({ value: type, rest: second }) -> + when parse_content_transfer_encoding_f(second) is + Err(_) -> + Ok( + { + disposition, + type, + encoding: [], + data: List.drop_first(second, double_newline_length), + }, + ) + + Ok({ value: encoding, rest }) -> + Ok( + { + disposition, + type, + encoding, + data: List.drop_first(rest, double_newline_length), + }, + ) expect header = "\r\nContent-Disposition: form-data; name=\"sometext\"\r\n\r\n" - actual = parseAllHeaders (Str.toUtf8 header) - expected = Ok { - disposition: Str.toUtf8 " form-data; name=\"sometext\"", - type: Str.toUtf8 "", - encoding: Str.toUtf8 "", - data: Str.toUtf8 "", - } + actual = parse_all_headers(Str.to_utf8(header)) + expected = Ok( + { + disposition: Str.to_utf8(" form-data; name=\"sometext\""), + type: Str.to_utf8(""), + encoding: Str.to_utf8(""), + data: Str.to_utf8(""), + }, + ) actual == expected expect header = "\r\nContent-Disposition: form-data; name=\"sometext\"\r\nContent-Type: multipart/mixed; boundary=abcde\r\n\r\n" - actual = parseAllHeaders (Str.toUtf8 header) - expected = Ok { - disposition: Str.toUtf8 " form-data; name=\"sometext\"", - type: Str.toUtf8 " multipart/mixed; boundary=abcde", - encoding: Str.toUtf8 "", - data: Str.toUtf8 "", - } + actual = parse_all_headers(Str.to_utf8(header)) + expected = Ok( + { + disposition: Str.to_utf8(" form-data; name=\"sometext\""), + type: Str.to_utf8(" multipart/mixed; boundary=abcde"), + encoding: Str.to_utf8(""), + data: Str.to_utf8(""), + }, + ) actual == expected expect header = "\r\nContent-Disposition: form-data; name=\"sometext\"\r\nContent-Type: multipart/mixed; boundary=abcde\r\nContent-Transfer-Encoding: binary\r\n\r\n" - actual = parseAllHeaders (Str.toUtf8 header) - expected = Ok { - disposition: Str.toUtf8 " form-data; name=\"sometext\"", - type: Str.toUtf8 " multipart/mixed; boundary=abcde", - encoding: Str.toUtf8 " binary", - data: Str.toUtf8 "", - } + actual = parse_all_headers(Str.to_utf8(header)) + expected = Ok( + { + disposition: Str.to_utf8(" form-data; name=\"sometext\""), + type: Str.to_utf8(" multipart/mixed; boundary=abcde"), + encoding: Str.to_utf8(" binary"), + data: Str.to_utf8(""), + }, + ) actual == expected @@ -208,25 +235,25 @@ parse_form_data : boundary : List U8, } -> Result (List FormData) [ExpectedEnclosedByBoundary] -parse_form_data = \{ body, boundary } -> +parse_form_data = |{ body, boundary }| - startMarker = List.join [doubledash, boundary] - endMarker = List.join [newline, doubledash, boundary, doubledash, newline] - boundaryWithPrefix = List.join [newline, doubledash, boundary] + start_marker = List.join([doubledash, boundary]) + end_marker = List.join([newline, doubledash, boundary, doubledash, newline]) + boundary_with_prefix = List.join([newline, doubledash, boundary]) - isEnclosedByBoundary = - List.startsWith body startMarker - && List.endsWith body endMarker + is_enclosed_by_boundary = + List.starts_with(body, start_marker) + and List.ends_with(body, end_marker) - if isEnclosedByBoundary then + if is_enclosed_by_boundary then body - |> List.dropFirst (List.len startMarker) - |> SplitList.split_on_list boundaryWithPrefix - |> List.dropIf \part -> part == doubledash - |> List.keepOks parseAllHeaders + |> List.drop_first(List.len(start_marker)) + |> SplitList.split_on_list(boundary_with_prefix) + |> List.drop_if(|part| part == doubledash) + |> List.keep_oks(parse_all_headers) |> Ok else - Err ExpectedEnclosedByBoundary + Err(ExpectedEnclosedByBoundary) expect # """ @@ -240,57 +267,67 @@ expect # ------WebKitFormBoundaryQGx5ri4XFwWbaAX4-- # # """ - actual = parse_form_data { - body: [45, 45, 45, 45, 45, 45, 87, 101, 98, 75, 105, 116, 70, 111, 114, 109, 66, 111, 117, 110, 100, 97, 114, 121, 70, 71, 54, 83, 65, 109, 121, 106, 112, 49, 108, 118, 102, 57, 80, 55, 13, 10, 67, 111, 110, 116, 101, 110, 116, 45, 68, 105, 115, 112, 111, 115, 105, 116, 105, 111, 110, 58, 32, 102, 111, 114, 109, 45, 100, 97, 116, 97, 59, 32, 110, 97, 109, 101, 61, 34, 102, 105, 108, 101, 34, 59, 32, 102, 105, 108, 101, 110, 97, 109, 101, 61, 34, 115, 97, 109, 112, 108, 101, 50, 46, 99, 115, 118, 34, 13, 10, 67, 111, 110, 116, 101, 110, 116, 45, 84, 121, 112, 101, 58, 32, 116, 101, 120, 116, 47, 99, 115, 118, 13, 10, 13, 10, 70, 105, 114, 115, 116, 44, 76, 97, 115, 116, 13, 10, 82, 97, 99, 104, 101, 108, 44, 66, 111, 111, 107, 101, 114, 13, 10, 13, 10, 45, 45, 45, 45, 45, 45, 87, 101, 98, 75, 105, 116, 70, 111, 114, 109, 66, 111, 117, 110, 100, 97, 114, 121, 70, 71, 54, 83, 65, 109, 121, 106, 112, 49, 108, 118, 102, 57, 80, 55, 45, 45, 13, 10], - boundary: [45, 45, 45, 45, 87, 101, 98, 75, 105, 116, 70, 111, 114, 109, 66, 111, 117, 110, 100, 97, 114, 121, 70, 71, 54, 83, 65, 109, 121, 106, 112, 49, 108, 118, 102, 57, 80, 55], - } - # Result.isErr actual - expected = Ok [ + actual = parse_form_data( { - data: [70, 105, 114, 115, 116, 44, 76, 97, 115, 116, 13, 10, 82, 97, 99, 104, 101, 108, 44, 66, 111, 111, 107, 101, 114, 13, 10], - disposition: [32, 102, 111, 114, 109, 45, 100, 97, 116, 97, 59, 32, 110, 97, 109, 101, 61, 34, 102, 105, 108, 101, 34, 59, 32, 102, 105, 108, 101, 110, 97, 109, 101, 61, 34, 115, 97, 109, 112, 108, 101, 50, 46, 99, 115, 118, 34], - encoding: [], - type: [32, 116, 101, 120, 116, 47, 99, 115, 118], + body: [45, 45, 45, 45, 45, 45, 87, 101, 98, 75, 105, 116, 70, 111, 114, 109, 66, 111, 117, 110, 100, 97, 114, 121, 70, 71, 54, 83, 65, 109, 121, 106, 112, 49, 108, 118, 102, 57, 80, 55, 13, 10, 67, 111, 110, 116, 101, 110, 116, 45, 68, 105, 115, 112, 111, 115, 105, 116, 105, 111, 110, 58, 32, 102, 111, 114, 109, 45, 100, 97, 116, 97, 59, 32, 110, 97, 109, 101, 61, 34, 102, 105, 108, 101, 34, 59, 32, 102, 105, 108, 101, 110, 97, 109, 101, 61, 34, 115, 97, 109, 112, 108, 101, 50, 46, 99, 115, 118, 34, 13, 10, 67, 111, 110, 116, 101, 110, 116, 45, 84, 121, 112, 101, 58, 32, 116, 101, 120, 116, 47, 99, 115, 118, 13, 10, 13, 10, 70, 105, 114, 115, 116, 44, 76, 97, 115, 116, 13, 10, 82, 97, 99, 104, 101, 108, 44, 66, 111, 111, 107, 101, 114, 13, 10, 13, 10, 45, 45, 45, 45, 45, 45, 87, 101, 98, 75, 105, 116, 70, 111, 114, 109, 66, 111, 117, 110, 100, 97, 114, 121, 70, 71, 54, 83, 65, 109, 121, 106, 112, 49, 108, 118, 102, 57, 80, 55, 45, 45, 13, 10], + boundary: [45, 45, 45, 45, 87, 101, 98, 75, 105, 116, 70, 111, 114, 109, 66, 111, 117, 110, 100, 97, 114, 121, 70, 71, 54, 83, 65, 109, 121, 106, 112, 49, 108, 118, 102, 57, 80, 55], }, - ] + ) + # Result.isErr actual + expected = Ok( + [ + { + data: [70, 105, 114, 115, 116, 44, 76, 97, 115, 116, 13, 10, 82, 97, 99, 104, 101, 108, 44, 66, 111, 111, 107, 101, 114, 13, 10], + disposition: [32, 102, 111, 114, 109, 45, 100, 97, 116, 97, 59, 32, 110, 97, 109, 101, 61, 34, 102, 105, 108, 101, 34, 59, 32, 102, 105, 108, 101, 110, 97, 109, 101, 61, 34, 115, 97, 109, 112, 108, 101, 50, 46, 99, 115, 118, 34], + encoding: [], + type: [32, 116, 101, 120, 116, 47, 99, 115, 118], + }, + ], + ) actual == expected expect - input = Str.toUtf8 "--12345\r\nContent-Disposition: form-data; name=\"sometext\"\r\n\r\nsome text sent via post...\r\n--12345--\r\n" - actual = parse_form_data { - body: input, - boundary: Str.toUtf8 "12345", - } - expected = Ok [ + input = Str.to_utf8("--12345\r\nContent-Disposition: form-data; name=\"sometext\"\r\n\r\nsome text sent via post...\r\n--12345--\r\n") + actual = parse_form_data( { - disposition: Str.toUtf8 " form-data; name=\"sometext\"", - type: [], - encoding: [], - data: Str.toUtf8 "some text sent via post...", + body: input, + boundary: Str.to_utf8("12345"), }, - ] + ) + expected = Ok( + [ + { + disposition: Str.to_utf8(" form-data; name=\"sometext\""), + type: [], + encoding: [], + data: Str.to_utf8("some text sent via post..."), + }, + ], + ) actual == expected expect - body = Str.toUtf8 "--AaB03x\r\nContent-Disposition: form-data; name=\"submit-name\"\r\n\r\nLarry\r\n--AaB03x\r\nContent-Disposition: form-data; name=\"files\"\r\nContent-Type: multipart/mixed; boundary=BbC04y\r\n\r\n--BbC04y\r\nContent-Disposition: file; filename=\"file1.txt\"\r\nContent-Type: text/plain\r\n\r\n... contents of file1.txt ...\r\n--BbC04y\r\nContent-Disposition: file; filename=\"file2.gif\"\r\nContent-Type: image/gif\r\nContent-Transfer-Encoding: binary\r\n\r\n...contents of file2.gif...\r\n--BbC04y--\r\n--AaB03x--\r\n" - boundary = Str.toUtf8 "AaB03x" - actual = parse_form_data { body, boundary } - expected = Ok [ - { - disposition: Str.toUtf8 " form-data; name=\"submit-name\"", - type: [], - encoding: [], - data: Str.toUtf8 "Larry", - }, - { - disposition: Str.toUtf8 " form-data; name=\"files\"", - type: Str.toUtf8 " multipart/mixed; boundary=BbC04y", - encoding: [], - data: Str.toUtf8 "--BbC04y\r\nContent-Disposition: file; filename=\"file1.txt\"\r\nContent-Type: text/plain\r\n\r\n... contents of file1.txt ...\r\n--BbC04y\r\nContent-Disposition: file; filename=\"file2.gif\"\r\nContent-Type: image/gif\r\nContent-Transfer-Encoding: binary\r\n\r\n...contents of file2.gif...\r\n--BbC04y--", - }, - ] + body = Str.to_utf8("--AaB03x\r\nContent-Disposition: form-data; name=\"submit-name\"\r\n\r\nLarry\r\n--AaB03x\r\nContent-Disposition: form-data; name=\"files\"\r\nContent-Type: multipart/mixed; boundary=BbC04y\r\n\r\n--BbC04y\r\nContent-Disposition: file; filename=\"file1.txt\"\r\nContent-Type: text/plain\r\n\r\n... contents of file1.txt ...\r\n--BbC04y\r\nContent-Disposition: file; filename=\"file2.gif\"\r\nContent-Type: image/gif\r\nContent-Transfer-Encoding: binary\r\n\r\n...contents of file2.gif...\r\n--BbC04y--\r\n--AaB03x--\r\n") + boundary = Str.to_utf8("AaB03x") + actual = parse_form_data({ body, boundary }) + expected = Ok( + [ + { + disposition: Str.to_utf8(" form-data; name=\"submit-name\""), + type: [], + encoding: [], + data: Str.to_utf8("Larry"), + }, + { + disposition: Str.to_utf8(" form-data; name=\"files\""), + type: Str.to_utf8(" multipart/mixed; boundary=BbC04y"), + encoding: [], + data: Str.to_utf8("--BbC04y\r\nContent-Disposition: file; filename=\"file1.txt\"\r\nContent-Type: text/plain\r\n\r\n... contents of file1.txt ...\r\n--BbC04y\r\nContent-Disposition: file; filename=\"file2.gif\"\r\nContent-Type: image/gif\r\nContent-Transfer-Encoding: binary\r\n\r\n...contents of file2.gif...\r\n--BbC04y--"), + }, + ], + ) actual == expected @@ -298,83 +335,91 @@ expect ## ## ``` ## expect -## bytes = Str.toUtf8 "todo=foo&status=bar" -## parsed = parse_form_url_encoded bytes |> Result.withDefault (Dict.empty {}) +## bytes = Str.to_utf8("todo=foo&status=bar") +## parsed = parse_form_url_encoded(bytes) |> Result.with_default(Dict.empty({})) ## -## Dict.toList parsed == [("todo", "foo"), ("status", "bar")] +## Dict.to_list(parsed) == [("todo", "foo"), ("status", "bar")] ## ``` parse_form_url_encoded : List U8 -> Result (Dict Str Str) [BadUtf8] -parse_form_url_encoded = \bytes -> +parse_form_url_encoded = |bytes| - chainUtf8 = \bytesList, tryFun -> Str.fromUtf8 bytesList |> mapUtf8Err |> Result.try tryFun + chain_utf8 = |bytes_list, try_fun| Str.from_utf8(bytes_list) |> map_utf8_err |> Result.try(try_fun) # simplify `BadUtf8 Utf8ByteProblem ...` error - mapUtf8Err = \err -> err |> Result.mapErr \_ -> BadUtf8 + map_utf8_err = |err| err |> Result.map_err(|_| BadUtf8) - help = \bytesRemaining, state, key, chomped, dict -> - tail = List.dropFirst bytesRemaining 1 + help = |bytes_remaining, state, key, chomped, dict| + tail = List.drop_first(bytes_remaining, 1) - when bytesRemaining is - [] if List.isEmpty chomped -> dict |> Ok + when bytes_remaining is + [] if List.is_empty(chomped) -> dict |> Ok [] -> # chomped last value key - |> chainUtf8 \keyStr -> - chomped - |> chainUtf8 \valueStr -> - Dict.insert dict keyStr valueStr |> Ok - - ['=', ..] -> help tail ParsingValue chomped [] dict # put chomped into key + |> chain_utf8( + |key_str| + chomped + |> chain_utf8( + |value_str| + Dict.insert(dict, key_str, value_str) |> Ok, + ), + ) + + ['=', ..] -> help(tail, ParsingValue, chomped, [], dict) # put chomped into key ['&', ..] -> key - |> chainUtf8 \keyStr -> - chomped - |> chainUtf8 \valueStr -> - help tail ParsingKey [] [] (Dict.insert dict keyStr valueStr) + |> chain_utf8( + |key_str| + chomped + |> chain_utf8( + |value_str| + help(tail, ParsingKey, [], [], Dict.insert(dict, key_str, value_str)), + ), + ) - ['%', secondByte, thirdByte, ..] -> - hex = Num.toU8 (hexBytesToU32 [secondByte, thirdByte]) + ['%', second_byte, third_byte, ..] -> + hex = Num.to_u8(hex_bytes_to_u32([second_byte, third_byte])) - help (List.dropFirst tail 2) state key (List.append chomped hex) dict + help(List.drop_first(tail, 2), state, key, List.append(chomped, hex), dict) - [firstByte, ..] -> help tail state key (List.append chomped firstByte) dict + [first_byte, ..] -> help(tail, state, key, List.append(chomped, first_byte), dict) - help bytes ParsingKey [] [] (Dict.empty {}) + help(bytes, ParsingKey, [], [], Dict.empty({})) -expect hexBytesToU32 ['2', '0'] == 32 +expect hex_bytes_to_u32(['2', '0']) == 32 expect - bytes = Str.toUtf8 "todo=foo&status=bar" - parsed = parse_form_url_encoded bytes |> Result.withDefault (Dict.empty {}) + bytes = Str.to_utf8("todo=foo&status=bar") + parsed = parse_form_url_encoded(bytes) |> Result.with_default(Dict.empty({})) - Dict.toList parsed == [("todo", "foo"), ("status", "bar")] + Dict.to_list(parsed) == [("todo", "foo"), ("status", "bar")] expect - Str.toUtf8 "task=asdfs%20adf&status=qwerwe" + Str.to_utf8("task=asdfs%20adf&status=qwerwe") |> parse_form_url_encoded - |> Result.withDefault (Dict.empty {}) - |> Dict.toList - |> Bool.isEq [("task", "asdfs adf"), ("status", "qwerwe")] + |> Result.with_default(Dict.empty({})) + |> Dict.to_list + |> Bool.is_eq([("task", "asdfs adf"), ("status", "qwerwe")]) -hexBytesToU32 : List U8 -> U32 -hexBytesToU32 = \bytes -> +hex_bytes_to_u32 : List U8 -> U32 +hex_bytes_to_u32 = |bytes| bytes |> List.reverse - |> List.walkWithIndex 0 \accum, byte, i -> accum + (Num.powInt 16 (Num.toU32 i)) * (hexToDec byte) - |> Num.toU32 - -expect hexBytesToU32 ['0', '0', '0', '0'] == 0 -expect hexBytesToU32 ['0', '0', '0', '1'] == 1 -expect hexBytesToU32 ['0', '0', '0', 'F'] == 15 -expect hexBytesToU32 ['0', '0', '1', '0'] == 16 -expect hexBytesToU32 ['0', '0', 'F', 'F'] == 255 -expect hexBytesToU32 ['0', '1', '0', '0'] == 256 -expect hexBytesToU32 ['0', 'F', 'F', 'F'] == 4095 -expect hexBytesToU32 ['1', '0', '0', '0'] == 4096 -expect hexBytesToU32 ['1', '6', 'F', 'F', '1'] == 94193 - -hexToDec : U8 -> U32 -hexToDec = \byte -> + |> List.walk_with_index(0, |accum, byte, i| accum + (Num.pow_int(16, Num.to_u32(i))) * (hex_to_dec(byte))) + |> Num.to_u32 + +expect hex_bytes_to_u32(['0', '0', '0', '0']) == 0 +expect hex_bytes_to_u32(['0', '0', '0', '1']) == 1 +expect hex_bytes_to_u32(['0', '0', '0', 'F']) == 15 +expect hex_bytes_to_u32(['0', '0', '1', '0']) == 16 +expect hex_bytes_to_u32(['0', '0', 'F', 'F']) == 255 +expect hex_bytes_to_u32(['0', '1', '0', '0']) == 256 +expect hex_bytes_to_u32(['0', 'F', 'F', 'F']) == 4095 +expect hex_bytes_to_u32(['1', '0', '0', '0']) == 4096 +expect hex_bytes_to_u32(['1', '6', 'F', 'F', '1']) == 94193 + +hex_to_dec : U8 -> U32 +hex_to_dec = |byte| when byte is '0' -> 0 '1' -> 1 @@ -392,10 +437,10 @@ hexToDec = \byte -> 'D' -> 13 'E' -> 14 'F' -> 15 - _ -> crash "Impossible error: the `when` block I'm in should have matched before reaching the catch-all `_`." + _ -> crash("Impossible error: the `when` block I'm in should have matched before reaching the catch-all `_`.") -expect hexToDec '0' == 0 -expect hexToDec 'F' == 15 +expect hex_to_dec('0') == 0 +expect hex_to_dec('F') == 15 ## For HTML forms that include files or large amounts of text. ## @@ -406,20 +451,24 @@ parse_multipart_form_data : body : List U8, } -> Result (List MultipartFormData.FormData) [InvalidMultipartFormData, ExpectedContentTypeHeader, InvalidContentTypeHeader] -parse_multipart_form_data = \args -> - decode_multipart_form_data_boundary args.headers - |> Result.try \boundary -> - { body: args.body, boundary } - |> parse_form_data - |> Result.mapErr \_ -> InvalidMultipartFormData +parse_multipart_form_data = |args| + decode_multipart_form_data_boundary(args.headers) + |> Result.try( + |boundary| + { body: args.body, boundary } + |> parse_form_data + |> Result.map_err(|_| InvalidMultipartFormData), + ) decode_multipart_form_data_boundary : List { name : Str, value : Str } -> Result (List U8) _ -decode_multipart_form_data_boundary = \headers -> +decode_multipart_form_data_boundary = |headers| headers - |> List.keepIf \{ name } -> name == "Content-Type" || name == "content-type" + |> List.keep_if(|{ name }| name == "Content-Type" or name == "content-type") |> List.first - |> Result.mapErr \ListWasEmpty -> ExpectedContentTypeHeader - |> Result.try \{ value } -> - when Str.splitLast value "=" is - Ok { after } -> Ok (Str.toUtf8 after) - Err NotFound -> Err InvalidContentTypeHeader + |> Result.map_err(|ListWasEmpty| ExpectedContentTypeHeader) + |> Result.try( + |{ value }| + when Str.split_last(value, "=") is + Ok({ after }) -> Ok(Str.to_utf8(after)) + Err(NotFound) -> Err(InvalidContentTypeHeader), + ) diff --git a/platform/Path.roc b/platform/Path.roc index d9ff10c..ff3a6c3 100644 --- a/platform/Path.roc +++ b/platform/Path.roc @@ -57,10 +57,11 @@ IOErr : InternalIOErr.IOErr ## ## ``` ## # Writes `{"some":"json stuff"}` to the file `output.json`: -## Path.write! -## { some: "json stuff" } -## (Path.from_str "output.json") -## Json.toCompactUtf8 +## Path.write!( +## { some: "json stuff" }, +## Path.from_str("output.json"), +## Json.toCompactUtf8, +## )? ## ``` ## ## This opens the file first and closes it after writing to it. @@ -68,45 +69,45 @@ IOErr : InternalIOErr.IOErr ## ## > To write unformatted bytes to a file, you can use [Path.write_bytes!] instead. write! : val, Path, fmt => Result {} [FileWriteErr Path IOErr] where val implements Encoding, fmt implements EncoderFormatting -write! = \val, path, fmt -> - bytes = Encode.toBytes val fmt +write! = |val, path, fmt| + bytes = Encode.to_bytes(val, fmt) # TODO handle encoding errors here, once they exist - write_bytes! bytes path + write_bytes!(bytes, path) ## Writes bytes to a file. ## ## ``` ## # Writes the bytes 1, 2, 3 to the file `myfile.dat`. -## Path.write_bytes! [1, 2, 3] (Path.from_str "myfile.dat") +## Path.write_bytes!([1, 2, 3], Path.from_str("myfile.dat"))? ## ``` ## ## This opens the file first and closes it after writing to it. ## ## > To format data before writing it to a file, you can use [Path.write!] instead. write_bytes! : List U8, Path => Result {} [FileWriteErr Path IOErr] -write_bytes! = \bytes, path -> - path_bytes = InternalPath.to_bytes path +write_bytes! = |bytes, path| + path_bytes = InternalPath.to_bytes(path) - Host.file_write_bytes! path_bytes bytes - |> Result.mapErr \err -> FileWriteErr path (InternalIOErr.handle_err err) + Host.file_write_bytes!(path_bytes, bytes) + |> Result.map_err(|err| FileWriteErr(path, InternalIOErr.handle_err(err))) ## Writes a [Str] to a file, encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8). ## ## ``` ## # Writes "Hello!" encoded as UTF-8 to the file `myfile.txt`. -## Path.write_utf8! "Hello!" (Path.from_str "myfile.txt") +## Path.write_utf8!("Hello!", Path.from_str("myfile.txt"))? ## ``` ## ## This opens the file first and closes it after writing to it. ## ## > To write unformatted bytes to a file, you can use [Path.write_bytes!] instead. write_utf8! : Str, Path => Result {} [FileWriteErr Path IOErr] -write_utf8! = \str, path -> - path_bytes = InternalPath.to_bytes path +write_utf8! = |str, path| + path_bytes = InternalPath.to_bytes(path) - Host.file_write_utf8! path_bytes str - |> Result.mapErr \err -> FileWriteErr path (InternalIOErr.handle_err err) + Host.file_write_utf8!(path_bytes, str) + |> Result.map_err(|err| FileWriteErr(path, InternalIOErr.handle_err(err))) ## Note that the path may not be valid depending on the filesystem where it is used. ## For example, paths containing `:` are valid on ext4 and NTFS filesystems, but not @@ -118,8 +119,8 @@ write_utf8! = \str, path -> ## (at the time). Otherwise, error handling can happen for that operation rather than validating ## up front for a false sense of security (given symlinks, parts of a path being renamed, etc.). from_str : Str -> Path -from_str = \str -> - FromStr str +from_str = |str| + FromStr(str) |> InternalPath.wrap ## Not all filesystems use Unicode paths. This function can be used to create a path which @@ -128,8 +129,8 @@ from_str = \str -> ## Note that if the list contains any `0` bytes, sending this path to any file operations ## (e.g. `Path.read_bytes` or `WriteStream.openPath`) will fail. from_bytes : List U8 -> Path -from_bytes = \bytes -> - ArbitraryBytes bytes +from_bytes = |bytes| + ArbitraryBytes(bytes) |> InternalPath.wrap ## Unfortunately, operating system paths do not include information about which charset @@ -162,16 +163,16 @@ from_bytes = \bytes -> ## [Here is an example.](https://unix.stackexchange.com/questions/667652/can-a-file-path-be-invalid-utf-8/667863#667863) ## ## If you happen to know the `Charset` that was used to encode the path, you can use -## `toStrUsingCharset` instead of [display]. +## `to_str_using_charset` instead of [display]. display : Path -> Str -display = \path -> - when InternalPath.unwrap path is - FromStr str -> str - FromOperatingSystem bytes | ArbitraryBytes bytes -> - when Str.fromUtf8 bytes is - Ok str -> str +display = |path| + when InternalPath.unwrap(path) is + FromStr(str) -> str + FromOperatingSystem(bytes) | ArbitraryBytes(bytes) -> + when Str.from_utf8(bytes) is + Ok(str) -> str # TODO: this should use the builtin Str.display to display invalid UTF-8 chars in just the right spots, but that does not exist yet! - Err _ -> "�" + Err(_) -> "�" ## Returns true if the path exists on disk and is pointing at a directory. ## Returns `Ok false` if the path exists and it is not a directory. If the path does not exist, @@ -181,9 +182,9 @@ display = \path -> ## ## > [`File.is_dir`](File#is_dir!) does the same thing, except it takes a [Str] instead of a [Path]. is_dir! : Path => Result Bool [PathErr IOErr] -is_dir! = \path -> - res = type!? path - Ok (res == IsDir) +is_dir! = |path| + res = type!(path)? + Ok((res == IsDir)) ## Returns true if the path exists on disk and is pointing at a regular file. ## Returns `Ok false` if the path exists and it is not a file. If the path does not exist, @@ -193,9 +194,9 @@ is_dir! = \path -> ## ## > [`File.is_file`](File#is_file!) does the same thing, except it takes a [Str] instead of a [Path]. is_file! : Path => Result Bool [PathErr IOErr] -is_file! = \path -> - res = type!? path - Ok (res == IsFile) +is_file! = |path| + res = type!(path)? + Ok((res == IsFile)) ## Returns true if the path exists on disk and is pointing at a symbolic link. ## Returns `Ok false` if the path exists and it is not a symbolic link. If the path does not exist, @@ -205,60 +206,62 @@ is_file! = \path -> ## ## > [`File.is_sym_link`](File#is_sym_link!) does the same thing, except it takes a [Str] instead of a [Path]. is_sym_link! : Path => Result Bool [PathErr IOErr] -is_sym_link! = \path -> - res = type!? path - Ok (res == IsSymLink) +is_sym_link! = |path| + res = type!(path)? + Ok((res == IsSymLink)) ## Return the type of the path if the path exists on disk. ## ## > [`File.type`](File#type!) does the same thing, except it takes a [Str] instead of a [Path]. type! : Path => Result [IsFile, IsDir, IsSymLink] [PathErr IOErr] -type! = \path -> - Host.path_type! (InternalPath.to_bytes path) - |> Result.mapErr \err -> PathErr (InternalIOErr.handle_err err) - |> Result.map \path_type -> - if path_type.is_sym_link then - IsSymLink - else if path_type.is_dir then - IsDir - else - IsFile +type! = |path| + Host.path_type!(InternalPath.to_bytes(path)) + |> Result.map_err(|err| PathErr(InternalIOErr.handle_err(err))) + |> Result.map_ok( + |path_type| + if path_type.is_sym_link then + IsSymLink + else if path_type.is_dir then + IsDir + else + IsFile, + ) ## If the last component of this path has no `.`, appends `.` followed by the given string. ## Otherwise, replaces everything after the last `.` with the given string. ## ## ``` ## # Each of these gives "foo/bar/baz.txt" -## Path.from_str "foo/bar/baz" |> Path.with_extension "txt" -## Path.from_str "foo/bar/baz." |> Path.with_extension "txt" -## Path.from_str "foo/bar/baz.xz" |> Path.with_extension "txt" +## Path.from_str("foo/bar/baz") |> Path.with_extension("txt") +## Path.from_str("foo/bar/baz.") |> Path.with_extension("txt") +## Path.from_str("foo/bar/baz.xz") |> Path.with_extension("txt") ## ``` with_extension : Path, Str -> Path -with_extension = \path, extension -> - when InternalPath.unwrap path is - FromOperatingSystem bytes | ArbitraryBytes bytes -> +with_extension = |path, extension| + when InternalPath.unwrap(path) is + FromOperatingSystem(bytes) | ArbitraryBytes(bytes) -> before_dot = - when List.splitLast bytes (Num.toU8 '.') is - Ok { before } -> before - Err NotFound -> bytes + when List.split_last(bytes, Num.to_u8('.')) is + Ok({ before }) -> before + Err(NotFound) -> bytes before_dot - |> List.reserve (Str.countUtf8Bytes extension |> Num.intCast |> Num.addSaturated 1) - |> List.append (Num.toU8 '.') - |> List.concat (Str.toUtf8 extension) + |> List.reserve((Str.count_utf8_bytes(extension) |> Num.int_cast |> Num.add_saturated(1))) + |> List.append(Num.to_u8('.')) + |> List.concat(Str.to_utf8(extension)) |> ArbitraryBytes |> InternalPath.wrap - FromStr str -> + FromStr(str) -> before_dot = - when Str.splitLast str "." is - Ok { before } -> before - Err NotFound -> str + when Str.split_last(str, ".") is + Ok({ before }) -> before + Err(NotFound) -> str before_dot - |> Str.reserve (Str.countUtf8Bytes extension |> Num.addSaturated 1) - |> Str.concat "." - |> Str.concat extension + |> Str.reserve((Str.count_utf8_bytes(extension) |> Num.add_saturated(1))) + |> Str.concat(".") + |> Str.concat(extension) |> FromStr |> InternalPath.wrap @@ -272,7 +275,7 @@ with_extension = \path, extension -> ## ## ``` ## # Deletes the file named `myfile.dat` -## Path.delete (Path.from_str "myfile.dat") [1, 2, 3] +## Path.delete(Path.from_str("myfile.dat"), [1, 2, 3])? ## ``` ## ## > This does not securely erase the file's contents from disk; instead, the operating @@ -283,57 +286,57 @@ with_extension = \path, extension -> ## ## > [`File.delete`](File#delete!) does the same thing, except it takes a [Str] instead of a [Path]. delete! : Path => Result {} [FileWriteErr Path IOErr] -delete! = \path -> - Host.file_delete! (InternalPath.to_bytes path) - |> Result.mapErr \err -> FileWriteErr path (InternalIOErr.handle_err err) +delete! = |path| + Host.file_delete!(InternalPath.to_bytes(path)) + |> Result.map_err(|err| FileWriteErr(path, InternalIOErr.handle_err(err))) ## Reads a [Str] from a file containing [UTF-8](https://en.wikipedia.org/wiki/UTF-8)-encoded text. ## ## ``` ## # Reads UTF-8 encoded text into a Str from the file "myfile.txt" -## Path.read_utf8 (Path.from_str "myfile.txt") +## contents_str = Path.read_utf8(Path.from_str("myfile.txt"))? ## ``` ## -## This opens the file first and closes it after writing to it. +## This opens the file first and closes it after reading its contents. ## The task will fail with `FileReadUtf8Err` if the given file contains invalid UTF-8. ## ## > To read unformatted bytes from a file, you can use [Path.read_bytes!] instead. ## > ## > [`File.read_utf8`](File#read_utf8!) does the same thing, except it takes a [Str] instead of a [Path]. read_utf8! : Path => Result Str [FileReadErr Path IOErr, FileReadUtf8Err Path _] -read_utf8! = \path -> +read_utf8! = |path| bytes = - Host.file_read_bytes! (InternalPath.to_bytes path) - |> Result.mapErr? \read_err -> FileReadErr path (InternalIOErr.handle_err read_err) + Host.file_read_bytes!(InternalPath.to_bytes(path)) + |> Result.map_err?(|read_err| FileReadErr(path, InternalIOErr.handle_err(read_err))) - Str.fromUtf8 bytes - |> Result.mapErr \err -> FileReadUtf8Err path err + Str.from_utf8(bytes) + |> Result.map_err(|err| FileReadUtf8Err(path, err)) ## Reads all the bytes in a file. ## ## ``` ## # Read all the bytes in `myfile.txt`. -## Path.read_bytes! (Path.from_str "myfile.txt") +## contents_bytes = Path.read_bytes!(Path.from_str("myfile.txt"))? ## ``` ## ## This opens the file first and closes it after reading its contents. ## -## > To read and decode data from a file, you can use `Path.read` instead. +## > To read and decode data from a file into a [Str], you can use [Path.read_utf8!] instead. ## > ## > [`File.read_bytes`](File#read_bytes!) does the same thing, except it takes a [Str] instead of a [Path]. read_bytes! : Path => Result (List U8) [FileReadErr Path IOErr] -read_bytes! = \path -> - Host.file_read_bytes! (InternalPath.to_bytes path) - |> Result.mapErr \err -> FileReadErr path (InternalIOErr.handle_err err) +read_bytes! = |path| + Host.file_read_bytes!(InternalPath.to_bytes(path)) + |> Result.map_err(|err| FileReadErr(path, InternalIOErr.handle_err(err))) ## Lists the files and directories inside the directory. ## ## > [`Dir.list`](Dir#list!) does the same thing, except it takes a [Str] instead of a [Path]. list_dir! : Path => Result (List Path) [DirErr IOErr] -list_dir! = \path -> - when Host.dir_list! (InternalPath.to_bytes path) is - Ok entries -> Ok (List.map entries InternalPath.from_os_bytes) - Err err -> Err (DirErr (InternalIOErr.handle_err err)) +list_dir! = |path| + when Host.dir_list!(InternalPath.to_bytes(path)) is + Ok(entries) -> Ok(List.map(entries, InternalPath.from_os_bytes)) + Err(err) -> Err(DirErr(InternalIOErr.handle_err(err))) ## Deletes a directory if it's empty ## @@ -345,9 +348,9 @@ list_dir! = \path -> ## ## > [`Dir.delete_empty`](Dir#delete_empty!) does the same thing, except it takes a [Str] instead of a [Path]. delete_empty! : Path => Result {} [DirErr IOErr] -delete_empty! = \path -> - Host.dir_delete_empty! (InternalPath.to_bytes path) - |> Result.mapErr \err -> DirErr (InternalIOErr.handle_err err) +delete_empty! = |path| + Host.dir_delete_empty!(InternalPath.to_bytes(path)) + |> Result.map_err(|err| DirErr(InternalIOErr.handle_err(err))) ## Recursively deletes a directory as well as all files and directories ## inside it. @@ -360,9 +363,9 @@ delete_empty! = \path -> ## ## > [`Dir.delete_all`](Dir#delete_all!) does the same thing, except it takes a [Str] instead of a [Path]. delete_all! : Path => Result {} [DirErr IOErr] -delete_all! = \path -> - Host.dir_delete_all! (InternalPath.to_bytes path) - |> Result.mapErr \err -> DirErr (InternalIOErr.handle_err err) +delete_all! = |path| + Host.dir_delete_all!(InternalPath.to_bytes(path)) + |> Result.map_err(|err| DirErr(InternalIOErr.handle_err(err))) ## Creates a directory ## @@ -373,9 +376,9 @@ delete_all! = \path -> ## ## > [`Dir.create`](Dir#create!) does the same thing, except it takes a [Str] instead of a [Path]. create_dir! : Path => Result {} [DirErr IOErr] -create_dir! = \path -> - Host.dir_create! (InternalPath.to_bytes path) - |> Result.mapErr \err -> DirErr (InternalIOErr.handle_err err) +create_dir! = |path| + Host.dir_create!(InternalPath.to_bytes(path)) + |> Result.map_err(|err| DirErr(InternalIOErr.handle_err(err))) ## Creates a directory recursively adding any missing parent directories. ## @@ -385,9 +388,9 @@ create_dir! = \path -> ## ## > [`Dir.create_all`](Dir#create_all!) does the same thing, except it takes a [Str] instead of a [Path]. create_all! : Path => Result {} [DirErr IOErr] -create_all! = \path -> - Host.dir_create_all! (InternalPath.to_bytes path) - |> Result.mapErr \err -> DirErr (InternalIOErr.handle_err err) +create_all! = |path| + Host.dir_create_all!(InternalPath.to_bytes(path)) + |> Result.map_err(|err| DirErr(InternalIOErr.handle_err(err))) ## Creates a new hard link on the filesystem. ## @@ -398,7 +401,7 @@ create_all! = \path -> ## ## > [File.hard_link!] does the same thing, except it takes a [Str] instead of a [Path]. hard_link! : Path => Result {} [LinkErr IOErr] -hard_link! = \path -> - Host.hard_link! (InternalPath.to_bytes path) - |> Result.mapErr InternalIOErr.handle_err - |> Result.mapErr LinkErr +hard_link! = |path| + Host.hard_link!(InternalPath.to_bytes(path)) + |> Result.map_err(InternalIOErr.handle_err) + |> Result.map_err(LinkErr) diff --git a/platform/SplitList.roc b/platform/SplitList.roc index 7261409..f41d1bd 100644 --- a/platform/SplitList.roc +++ b/platform/SplitList.roc @@ -7,35 +7,35 @@ module [ ## Example: ## ```roc ## input = [1,2,3,4,5,6,7,3,4,0,0] -## actual = splitOnList input [3,4] +## actual = split_on_list(input, [3,4]) ## expected = [[1,2], [5,6,7], [0, 0]] ## ``` ## split_on_list : List a, List a -> List (List a) where a implements Eq -split_on_list = \input_list, separator -> +split_on_list = |input_list, separator| # reserve capacity for markers which mark split boundaries - init_markers = List.withCapacity 100 + init_markers = List.with_capacity(100) # find all the start and stop markers - markers = List.walkWithIndex input_list init_markers (walk_help_find_starts input_list separator) + markers = List.walk_with_index(input_list, init_markers, walk_help_find_starts(input_list, separator)) # split the input based on the markers - walk_split_help input_list markers + walk_split_help(input_list, markers) # produces a Stop, followed by a sequence of Start, Stop, Start, Stop, ... -walk_help_find_starts = \input_list, separator_list -> - if input_list == [] || separator_list == [] then - \_, _, _ -> [] +walk_help_find_starts = |input_list, separator_list| + if input_list == [] or separator_list == [] then + |_, _, _| [] else - \all_markers, _, idx -> + |all_markers, _, idx| - len = List.len separator_list + len = List.len(separator_list) - if List.sublist input_list { start: idx, len } == separator_list then + if List.sublist(input_list, { start: idx, len }) == separator_list then all_markers - |> List.append (Stop idx) - |> List.append (Start (idx + len)) + |> List.append(Stop(idx)) + |> List.append(Start((idx + len))) else all_markers @@ -43,8 +43,8 @@ walk_help_find_starts = \input_list, separator_list -> expect input = [] separator = [1, 2, 3] - help = walk_help_find_starts input separator - actual = List.walkWithIndex input [] help + help = walk_help_find_starts(input, separator) + actual = List.walk_with_index(input, [], help) expected = [] actual == expected @@ -52,8 +52,8 @@ expect expect input = [1, 2, 3] separator = [] - help = walk_help_find_starts input separator - actual = List.walkWithIndex input [] help + help = walk_help_find_starts(input, separator) + actual = List.walk_with_index(input, [], help) expected = [] actual == expected @@ -61,86 +61,86 @@ expect expect input = [3, 4, 5, 6, 7, 8] separator = [3, 4, 5] - help = walk_help_find_starts input separator - actual = List.walkWithIndex input [] help - expected = [Stop 0, Start 3] + help = walk_help_find_starts(input, separator) + actual = List.walk_with_index(input, [], help) + expected = [Stop(0), Start(3)] actual == expected # multiple separators in the middle expect input = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 3, 4, 5, 6, 7, 8, 9, 10] separator = [3, 4, 5] - help = walk_help_find_starts input separator - actual = List.walkWithIndex input [] help - expected = [Stop 2, Start 5, Stop 10, Start 13] + help = walk_help_find_starts(input, separator) + actual = List.walk_with_index(input, [], help) + expected = [Stop(2), Start(5), Stop(10), Start(13)] actual == expected # separator at end expect input = [6, 7, 8, 3, 4, 5] separator = [3, 4, 5] - help = walk_help_find_starts input separator - actual = List.walkWithIndex input [] help - expected = [Stop 3, Start 6] + help = walk_help_find_starts(input, separator) + actual = List.walk_with_index(input, [], help) + expected = [Stop(3), Start(6)] actual == expected walk_split_help : List a, List [Start U64, Stop U64] -> List (List a) where a implements Eq -walk_split_help = \input, markers -> - go = \remaining_markers, state -> +walk_split_help = |input, markers| + go = |remaining_markers, state| when remaining_markers is [] -> state - [Stop stop, .. as rest] if stop == 0 -> go rest state - [Stop stop, .. as rest] -> - go rest (List.append state (List.sublist input { start: 0, len: stop })) + [Stop(stop), .. as rest] if stop == 0 -> go(rest, state) + [Stop(stop), .. as rest] -> + go(rest, List.append(state, List.sublist(input, { start: 0, len: stop }))) - [Start start, Stop stop, .. as rest] -> - go rest (List.append state (List.sublist input { start, len: stop - start })) + [Start(start), Stop(stop), .. as rest] -> + go(rest, List.append(state, List.sublist(input, { start, len: stop - start }))) - [Start start] if start >= List.len input -> state - [Start start] -> - List.append state (List.sublist input { start, len: ((List.len input) - start) }) + [Start(start)] if start >= List.len(input) -> state + [Start(start)] -> + List.append(state, List.sublist(input, { start, len: ((List.len(input)) - start) })) - _ -> crash "Unreachable:\n\tThis list should have matched earlier when branches: $(Inspect.toStr remaining_markers)" + _ -> crash("Unreachable:\n\tThis list should have matched earlier when branches: ${Inspect.to_str(remaining_markers)}") - go markers [] + go(markers, []) expect - actual = walk_split_help [1, 2, 3, 5, 6, 7, 8, 9, 10] [Stop 2] + actual = walk_split_help([1, 2, 3, 5, 6, 7, 8, 9, 10], [Stop(2)]) expected = [[1, 2]] actual == expected expect input = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 3, 4, 5, 6, 7, 8, 9, 10] - actual = walk_split_help input [Stop 2, Start 5, Stop 10, Start 13] + actual = walk_split_help(input, [Stop(2), Start(5), Stop(10), Start(13)]) expected = [[1, 2], [6, 7, 8, 9, 10], [6, 7, 8, 9, 10]] actual == expected expect input = [1, 2, 3, 4, 5, 6, 7, 3, 4, 0, 0] - actual = split_on_list input [3, 4] + actual = split_on_list(input, [3, 4]) expected = [[1, 2], [5, 6, 7], [0, 0]] actual == expected expect input = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 3, 4, 5, 6, 7, 8, 9, 10] - actual = split_on_list input [3, 4, 5] + actual = split_on_list(input, [3, 4, 5]) expected = [[1, 2], [6, 7, 8, 9, 10], [6, 7, 8, 9, 10]] actual == expected expect input = [One, Two, Three, Four, Five, Six, Seven, Eight, One, Two, Nine, Ten, Three, Four, Five, Six, Seven, One, Two, Eight, Nine, Ten] - actual = split_on_list input [One, Two] + actual = split_on_list(input, [One, Two]) expected = [[Three, Four, Five, Six, Seven, Eight], [Nine, Ten, Three, Four, Five, Six, Seven], [Eight, Nine, Ten]] actual == expected expect input = [6, 7, 8, 3, 4, 5] - actual = split_on_list input [3, 4, 5] + actual = split_on_list(input, [3, 4, 5]) expected = [[6, 7, 8]] actual == expected expect input = [3, 4, 5, 6, 7, 8] - actual = split_on_list input [3, 4, 5] + actual = split_on_list(input, [3, 4, 5]) expected = [[6, 7, 8]] actual == expected diff --git a/platform/Sqlite.roc b/platform/Sqlite.roc index dfe044f..c2342dc 100644 --- a/platform/Sqlite.roc +++ b/platform/Sqlite.roc @@ -145,19 +145,19 @@ Stmt := Box {} ## preparing the query each time it is called. This is usually done in `init!` with the prepared `Stmt` stored in the model. ## ## ``` -## prepared_query = try Sqlite.prepare! { +## prepared_query = Sqlite.prepare!({ ## path : "path/to/database.db", ## query : "SELECT * FROM todos;", -## } +## })? ## -## Sqlite.query_many_prepared! { +## Sqlite.query_many_prepared!({ ## stmt: prepared_query, ## bindings: [], ## rows: { Sqlite.decode_record <- -## id: Sqlite.i64 "id", -## task: Sqlite.str "task", +## id: Sqlite.i64("id"), +## task: Sqlite.str("task"), ## }, -## } +## }) ## ``` prepare! : { @@ -165,52 +165,52 @@ prepare! : query : Str, } => Result Stmt [SqliteErr ErrCode Str] -prepare! = \{ path, query: q } -> - Host.sqlite_prepare! path q - |> Result.map @Stmt - |> Result.mapErr internal_to_external_error +prepare! = |{ path, query: q }| + Host.sqlite_prepare!(path, q) + |> Result.map_ok(@Stmt) + |> Result.map_err(internal_to_external_error) # internal use only bind! : Stmt, List Binding => Result {} [SqliteErr ErrCode Str] -bind! = \@Stmt stmt, bindings -> - Host.sqlite_bind! stmt bindings - |> Result.mapErr internal_to_external_error +bind! = |@Stmt(stmt), bindings| + Host.sqlite_bind!(stmt, bindings) + |> Result.map_err(internal_to_external_error) # internal use only columns! : Stmt => List Str -columns! = \@Stmt stmt -> - Host.sqlite_columns! stmt +columns! = |@Stmt(stmt)| + Host.sqlite_columns!(stmt) # internal use only column_value! : Stmt, U64 => Result Value [SqliteErr ErrCode Str] -column_value! = \@Stmt stmt, i -> - Host.sqlite_column_value! stmt i - |> Result.mapErr internal_to_external_error +column_value! = |@Stmt(stmt), i| + Host.sqlite_column_value!(stmt, i) + |> Result.map_err(internal_to_external_error) # internal use only step! : Stmt => Result [Row, Done] [SqliteErr ErrCode Str] -step! = \@Stmt stmt -> - Host.sqlite_step! stmt - |> Result.mapErr internal_to_external_error +step! = |@Stmt(stmt)| + Host.sqlite_step!(stmt) + |> Result.map_err(internal_to_external_error) # internal use only reset! : Stmt => Result {} [SqliteErr ErrCode Str] -reset! = \@Stmt stmt -> - Host.sqlite_reset! stmt - |> Result.mapErr internal_to_external_error +reset! = |@Stmt(stmt)| + Host.sqlite_reset!(stmt) + |> Result.map_err(internal_to_external_error) ## Execute a SQL statement that doesn't return any rows (like INSERT, UPDATE, DELETE). ## ## Example: ## ``` -## Sqlite.execute! { +## Sqlite.execute!({ ## path: "path/to/database.db", ## query: "INSERT INTO users (first, last) VALUES (:first, :last);", ## bindings: [ -## { name: ":first", value: String "John" }, -## { name: ":last", value: String "Smith" }, +## { name: ":first", value: String("John") }, +## { name: ":last", value: String("Smith") }, ## ], -## } +## })? ## ``` execute! : { @@ -219,9 +219,9 @@ execute! : bindings : List Binding, } => Result {} [SqliteErr ErrCode Str, UnhandledRows] -execute! = \{ path, query: q, bindings } -> - stmt = try prepare! { path, query: q } - execute_prepared! { stmt, bindings } +execute! = |{ path, query: q, bindings }| + stmt = try(prepare!, { path, query: q }) + execute_prepared!({ stmt, bindings }) ## Execute a prepared SQL statement that doesn't return any rows. ## @@ -233,31 +233,31 @@ execute_prepared! : bindings : List Binding, } => Result {} [SqliteErr ErrCode Str, UnhandledRows] -execute_prepared! = \{ stmt, bindings } -> - try bind! stmt bindings - res = step! stmt - try reset! stmt +execute_prepared! = |{ stmt, bindings }| + try(bind!, stmt, bindings) + res = step!(stmt) + try(reset!, stmt) when res is - Ok Done -> - Ok {} + Ok(Done) -> + Ok({}) - Ok Row -> - Err UnhandledRows + Ok(Row) -> + Err(UnhandledRows) - Err e -> - Err e + Err(e) -> + Err(e) ## Execute a SQL query and decode exactly one row into a value. ## ## Example: ## ``` ## # count the number of rows in the `users` table -## count = try Sqlite.query! { +## count = Sqlite.query!({ ## path: db_path, ## query: "SELECT COUNT(*) as \"count\" FROM users;", ## bindings: [], -## row: Sqlite.u64 "count", -## } +## row: Sqlite.u64("count"), +## })? ## ``` query! : { @@ -267,9 +267,9 @@ query! : row : SqlDecode a (RowCountErr err), } => Result a (SqlDecodeErr (RowCountErr err)) -query! = \{ path, query: q, bindings, row } -> - stmt = try prepare! { path, query: q } - query_prepared! { stmt, bindings, row } +query! = |{ path, query: q, bindings, row }| + stmt = try(prepare!, { path, query: q }) + query_prepared!({ stmt, bindings, row }) ## Execute a prepared SQL query and decode exactly one row into a value. ## @@ -282,25 +282,25 @@ query_prepared! : row : SqlDecode a (RowCountErr err), } => Result a (SqlDecodeErr (RowCountErr err)) -query_prepared! = \{ stmt, bindings, row: decode } -> - try bind! stmt bindings - res = decode_exactly_one_row! stmt decode - try reset! stmt +query_prepared! = |{ stmt, bindings, row: decode }| + try(bind!, stmt, bindings) + res = decode_exactly_one_row!(stmt, decode) + try(reset!, stmt) res ## Execute a SQL query and decode multiple rows into a list of values. ## ## Example: ## ``` -## Sqlite.query_many! { +## rows = Sqlite.query_many!({ ## path: "path/to/database.db", ## query: "SELECT * FROM todos;", ## bindings: [], ## rows: { Sqlite.decode_record <- -## id: Sqlite.i64 "id", -## task: Sqlite.str "task", +## id: Sqlite.i64("id"), +## task: Sqlite.str("task"), ## }, -## } +## })? ## ``` query_many! : { @@ -310,9 +310,9 @@ query_many! : rows : SqlDecode a err, } => Result (List a) (SqlDecodeErr err) -query_many! = \{ path, query: q, bindings, rows } -> - stmt = try prepare! { path, query: q } - query_many_prepared! { stmt, bindings, rows } +query_many! = |{ path, query: q, bindings, rows }| + stmt = try(prepare!, { path, query: q }) + query_many_prepared!({ stmt, bindings, rows }) ## Execute a prepared SQL query and decode multiple rows into a list of values. ## @@ -325,10 +325,10 @@ query_many_prepared! : rows : SqlDecode a err, } => Result (List a) (SqlDecodeErr err) -query_many_prepared! = \{ stmt, bindings, rows: decode } -> - try bind! stmt bindings - res = decode_rows! stmt decode - try reset! stmt +query_many_prepared! = |{ stmt, bindings, rows: decode }| + try(bind!, stmt, bindings) + res = decode_rows!(stmt, decode) + try(reset!, stmt) res SqlDecodeErr err : [FieldNotFound Str, SqliteErr ErrCode Str]err @@ -339,119 +339,128 @@ SqlDecode a err := List Str -> (Stmt => Result a (SqlDecodeErr err)) ## Example: ## ``` ## { Sqlite.decode_record <- -## id: Sqlite.i64 "id", -## task: Sqlite.str "task", +## id: Sqlite.i64("id"), +## task: Sqlite.str("task"), ## } ## ``` decode_record : SqlDecode a err, SqlDecode b err, (a, b -> c) -> SqlDecode c err -decode_record = \@SqlDecode gen_first, @SqlDecode gen_second, mapper -> - @SqlDecode \cols -> - decode_first! = gen_first cols - decode_second! = gen_second cols - - \stmt -> - first = try decode_first! stmt - second = try decode_second! stmt - Ok (mapper first second) +decode_record = |@SqlDecode(gen_first), @SqlDecode(gen_second), mapper| + @SqlDecode( + |cols| + decode_first! = gen_first(cols) + decode_second! = gen_second(cols) + + |stmt| + first = try(decode_first!, stmt) + second = try(decode_second!, stmt) + Ok(mapper(first, second)), + ) ## Transform the output of a decoder by applying a function to the decoded value. ## ## Example: ## ``` -## Sqlite.i64 "id" |> Sqlite.map_value Num.toStr +## Sqlite.i64("id") |> Sqlite.map_value(Num.to_str) ## ``` map_value : SqlDecode a err, (a -> b) -> SqlDecode b err -map_value = \@SqlDecode gen_decode, mapper -> - @SqlDecode \cols -> - decode! = gen_decode cols +map_value = |@SqlDecode(gen_decode), mapper| + @SqlDecode( + |cols| + decode! = gen_decode(cols) - \stmt -> - val = try decode! stmt - Ok (mapper val) + |stmt| + val = try(decode!, stmt) + Ok(mapper(val)), + ) RowCountErr err : [NoRowsReturned, TooManyRowsReturned]err # internal use only decode_exactly_one_row! : Stmt, SqlDecode a (RowCountErr err) => Result a (SqlDecodeErr (RowCountErr err)) -decode_exactly_one_row! = \stmt, @SqlDecode gen_decode -> - cols = columns! stmt - decode_row! = gen_decode cols +decode_exactly_one_row! = |stmt, @SqlDecode(gen_decode)| + cols = columns!(stmt) + decode_row! = gen_decode(cols) - when try step! stmt is + when try(step!, stmt) is Row -> - row = try decode_row! stmt - when try step! stmt is + row = try(decode_row!, stmt) + when try(step!, stmt) is Done -> - Ok row + Ok(row) Row -> - Err TooManyRowsReturned + Err(TooManyRowsReturned) Done -> - Err NoRowsReturned + Err(NoRowsReturned) # internal use only decode_rows! : Stmt, SqlDecode a err => Result (List a) (SqlDecodeErr err) -decode_rows! = \stmt, @SqlDecode gen_decode -> - cols = columns! stmt - decode_row! = gen_decode cols +decode_rows! = |stmt, @SqlDecode(gen_decode)| + cols = columns!(stmt) + decode_row! = gen_decode(cols) - helper! = \out -> - when try step! stmt is + helper! = |out| + when try(step!, stmt) is Done -> - Ok out + Ok(out) Row -> - row = try decode_row! stmt + row = try(decode_row!, stmt) - List.append out row + List.append(out, row) |> helper! - helper! [] + helper!([]) # internal use only decoder : (Value -> Result a (SqlDecodeErr err)) -> (Str -> SqlDecode a err) -decoder = \fn -> \name -> - @SqlDecode \cols -> - - found = List.findFirstIndex cols \x -> x == name - when found is - Ok index -> - \stmt -> - try column_value! stmt index - |> fn - - Err NotFound -> - \_ -> - Err (FieldNotFound name) +decoder = |fn| + |name| + @SqlDecode( + |cols| + + found = List.find_first_index(cols, |x| x == name) + when found is + Ok(index) -> + |stmt| + try(column_value!, stmt, index) + |> fn + + Err(NotFound) -> + |_| + Err(FieldNotFound(name)), + ) ## Decode a [Value] keeping it tagged. This is useful when data could be many possible types. ## ## For example here we build a decoder that decodes the rows into a list of records with `id` and `mixed_data` fields: ## ``` -## Sqlite.query_many! { +## rows = Sqlite.query_many!({ ## path: "path/to/database.db", ## query: "SELECT id, mix_data FROM users;", ## bindings: [], ## rows: { Sqlite.decode_record <- -## id: Sqlite.i64 "id", -## mix_data: Sqlite.tagged_value "mixed_data", +## id: Sqlite.i64("id"), +## mix_data: Sqlite.tagged_value("mixed_data"), ## }, -## } +## })? ## ``` tagged_value : Str -> SqlDecode Value [] -tagged_value = decoder \val -> - Ok val +tagged_value = decoder( + |val| + Ok(val), +) -to_unexpected_type_err = \val -> +to_unexpected_type_err = |val| type = when val is - Integer _ -> Integer - Real _ -> Real - String _ -> String - Bytes _ -> Bytes + Integer(_) -> Integer + Real(_) -> Real + String(_) -> String + Bytes(_) -> Bytes Null -> Null - Err (UnexpectedType type) + Err(UnexpectedType(type)) UnexpectedTypeErr : [UnexpectedType [Integer, Real, String, Bytes, Null]] @@ -459,97 +468,105 @@ UnexpectedTypeErr : [UnexpectedType [Integer, Real, String, Bytes, Null]] ## ## For example here we build a decoder that decodes the rows into a list of records with `id` and `name` fields: ## ``` -## Sqlite.query_many! { +## rows = Sqlite.query_many!({ ## path: "path/to/database.db", ## query: "SELECT id, name FROM users;", ## bindings: [], ## rows: { Sqlite.decode_record <- -## id: Sqlite.i64 "id", -## task: Sqlite.str "name", +## id: Sqlite.i64("id"), +## task: Sqlite.str("name"), ## }, -## } +## })? ## ``` str : Str -> SqlDecode Str UnexpectedTypeErr -str = decoder \val -> - when val is - String s -> Ok s - _ -> to_unexpected_type_err val +str = decoder( + |val| + when val is + String(s) -> Ok(s) + _ -> to_unexpected_type_err(val), +) ## Decode a [Value] to a [List U8]. bytes : Str -> SqlDecode (List U8) UnexpectedTypeErr -bytes = decoder \val -> - when val is - Bytes b -> Ok b - _ -> to_unexpected_type_err val +bytes = decoder( + |val| + when val is + Bytes(b) -> Ok(b) + _ -> to_unexpected_type_err(val), +) # internal use only int_decoder : (I64 -> Result a err) -> (Str -> SqlDecode a [FailedToDecodeInteger err]UnexpectedTypeErr) -int_decoder = \cast -> - decoder \val -> - when val is - Integer i -> cast i |> Result.mapErr FailedToDecodeInteger - _ -> to_unexpected_type_err val +int_decoder = |cast| + decoder( + |val| + when val is + Integer(i) -> cast(i) |> Result.map_err(FailedToDecodeInteger) + _ -> to_unexpected_type_err(val), + ) # internal use only real_decoder : (F64 -> Result a err) -> (Str -> SqlDecode a [FailedToDecodeReal err]UnexpectedTypeErr) -real_decoder = \cast -> - decoder \val -> - when val is - Real r -> cast r |> Result.mapErr FailedToDecodeReal - _ -> to_unexpected_type_err val +real_decoder = |cast| + decoder( + |val| + when val is + Real(r) -> cast(r) |> Result.map_err(FailedToDecodeReal) + _ -> to_unexpected_type_err(val), + ) ## Decode a [Value] to a [I64]. ## ## For example here we build a decoder that decodes the rows into a list of records with `id` and `name` fields: ## ``` -## Sqlite.query_many! { +## rows = Sqlite.query_many!({ ## path: "path/to/database.db", ## query: "SELECT id, name FROM users;", ## bindings: [], ## rows: { Sqlite.decode_record <- -## id: Sqlite.i64 "id", -## task: Sqlite.str "name", +## id: Sqlite.i64("id"), +## task: Sqlite.str("name"), ## }, -## } +## })? ## ``` i64 : Str -> SqlDecode I64 [FailedToDecodeInteger []]UnexpectedTypeErr -i64 = int_decoder Ok +i64 = int_decoder(Ok) ## Decode a [Value] to a [I32]. i32 : Str -> SqlDecode I32 [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -i32 = int_decoder Num.toI32Checked +i32 = int_decoder(Num.to_i32_checked) ## Decode a [Value] to a [I16]. i16 : Str -> SqlDecode I16 [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -i16 = int_decoder Num.toI16Checked +i16 = int_decoder(Num.to_i16_checked) ## Decode a [Value] to a [I8]. i8 : Str -> SqlDecode I8 [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -i8 = int_decoder Num.toI8Checked +i8 = int_decoder(Num.to_i8_checked) ## Decode a [Value] to a [U64]. u64 : Str -> SqlDecode U64 [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -u64 = int_decoder Num.toU64Checked +u64 = int_decoder(Num.to_u64_checked) ## Decode a [Value] to a [U32]. u32 : Str -> SqlDecode U32 [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -u32 = int_decoder Num.toU32Checked +u32 = int_decoder(Num.to_u32_checked) ## Decode a [Value] to a [U16]. u16 : Str -> SqlDecode U16 [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -u16 = int_decoder Num.toU16Checked +u16 = int_decoder(Num.to_u16_checked) ## Decode a [Value] to a [U8]. u8 : Str -> SqlDecode U8 [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -u8 = int_decoder Num.toU8Checked +u8 = int_decoder(Num.to_u8_checked) ## Decode a [Value] to a [F64]. f64 : Str -> SqlDecode F64 [FailedToDecodeReal []]UnexpectedTypeErr -f64 = real_decoder Ok +f64 = real_decoder(Ok) ## Decode a [Value] to a [F32]. f32 : Str -> SqlDecode F32 [FailedToDecodeReal []]UnexpectedTypeErr -f32 = real_decoder (\x -> Num.toF32 x |> Ok) +f32 = real_decoder(|x| Num.to_f32(x) |> Ok) # TODO: Mising Num.toDec and Num.toDecChecked # dec = realSqlDecoder Ok @@ -562,90 +579,98 @@ Nullable a : [NotNull a, Null] ## Decode a [Value] to a [Nullable Str]. nullable_str : Str -> SqlDecode (Nullable Str) UnexpectedTypeErr -nullable_str = decoder \val -> - when val is - String s -> Ok (NotNull s) - Null -> Ok Null - _ -> to_unexpected_type_err val +nullable_str = decoder( + |val| + when val is + String(s) -> Ok(NotNull(s)) + Null -> Ok(Null) + _ -> to_unexpected_type_err(val), +) ## Decode a [Value] to a [Nullable (List U8)]. nullable_bytes : Str -> SqlDecode (Nullable (List U8)) UnexpectedTypeErr -nullable_bytes = decoder \val -> - when val is - Bytes b -> Ok (NotNull b) - Null -> Ok Null - _ -> to_unexpected_type_err val +nullable_bytes = decoder( + |val| + when val is + Bytes(b) -> Ok(NotNull(b)) + Null -> Ok(Null) + _ -> to_unexpected_type_err(val), +) # internal use only nullable_int_decoder : (I64 -> Result a err) -> (Str -> SqlDecode (Nullable a) [FailedToDecodeInteger err]UnexpectedTypeErr) -nullable_int_decoder = \cast -> - decoder \val -> - when val is - Integer i -> cast i |> Result.map NotNull |> Result.mapErr FailedToDecodeInteger - Null -> Ok Null - _ -> to_unexpected_type_err val +nullable_int_decoder = |cast| + decoder( + |val| + when val is + Integer(i) -> cast(i) |> Result.map_ok(NotNull) |> Result.map_err(FailedToDecodeInteger) + Null -> Ok(Null) + _ -> to_unexpected_type_err(val), + ) # internal use only nullable_real_decoder : (F64 -> Result a err) -> (Str -> SqlDecode (Nullable a) [FailedToDecodeReal err]UnexpectedTypeErr) -nullable_real_decoder = \cast -> - decoder \val -> - when val is - Real r -> cast r |> Result.map NotNull |> Result.mapErr FailedToDecodeReal - Null -> Ok Null - _ -> to_unexpected_type_err val +nullable_real_decoder = |cast| + decoder( + |val| + when val is + Real(r) -> cast(r) |> Result.map_ok(NotNull) |> Result.map_err(FailedToDecodeReal) + Null -> Ok(Null) + _ -> to_unexpected_type_err(val), + ) ## Decode a [Value] to a [Nullable I64]. nullable_i64 : Str -> SqlDecode (Nullable I64) [FailedToDecodeInteger []]UnexpectedTypeErr -nullable_i64 = nullable_int_decoder Ok +nullable_i64 = nullable_int_decoder(Ok) ## Decode a [Value] to a [Nullable I32]. nullable_i32 : Str -> SqlDecode (Nullable I32) [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -nullable_i32 = nullable_int_decoder Num.toI32Checked +nullable_i32 = nullable_int_decoder(Num.to_i32_checked) ## Decode a [Value] to a [Nullable I16]. nullable_i16 : Str -> SqlDecode (Nullable I16) [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -nullable_i16 = nullable_int_decoder Num.toI16Checked +nullable_i16 = nullable_int_decoder(Num.to_i16_checked) ## Decode a [Value] to a [Nullable I8]. nullable_i8 : Str -> SqlDecode (Nullable I8) [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -nullable_i8 = nullable_int_decoder Num.toI8Checked +nullable_i8 = nullable_int_decoder(Num.to_i8_checked) ## Decode a [Value] to a [Nullable U64]. nullable_u64 : Str -> SqlDecode (Nullable U64) [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -nullable_u64 = nullable_int_decoder Num.toU64Checked +nullable_u64 = nullable_int_decoder(Num.to_u64_checked) ## Decode a [Value] to a [Nullable U32]. nullable_u32 : Str -> SqlDecode (Nullable U32) [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -nullable_u32 = nullable_int_decoder Num.toU32Checked +nullable_u32 = nullable_int_decoder(Num.to_u32_checked) ## Decode a [Value] to a [Nullable U16]. nullable_u16 : Str -> SqlDecode (Nullable U16) [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -nullable_u16 = nullable_int_decoder Num.toU16Checked +nullable_u16 = nullable_int_decoder(Num.to_u16_checked) ## Decode a [Value] to a [Nullable U8]. nullable_u8 : Str -> SqlDecode (Nullable U8) [FailedToDecodeInteger [OutOfBounds]]UnexpectedTypeErr -nullable_u8 = nullable_int_decoder Num.toU8Checked +nullable_u8 = nullable_int_decoder(Num.to_u8_checked) ## Decode a [Value] to a [Nullable F64]. nullable_f64 : Str -> SqlDecode (Nullable F64) [FailedToDecodeReal []]UnexpectedTypeErr -nullable_f64 = nullable_real_decoder Ok +nullable_f64 = nullable_real_decoder(Ok) ## Decode a [Value] to a [Nullable F32]. nullable_f32 : Str -> SqlDecode (Nullable F32) [FailedToDecodeReal []]UnexpectedTypeErr -nullable_f32 = nullable_real_decoder (\x -> Num.toF32 x |> Ok) +nullable_f32 = nullable_real_decoder(|x| Num.to_f32(x) |> Ok) # TODO: Mising Num.toDec and Num.toDecChecked # nullable_dec = nullable_real_decoder Ok # internal use only internal_to_external_error : InternalSqlite.SqliteError -> [SqliteErr ErrCode Str] -internal_to_external_error = \{ code, message } -> - SqliteErr (code_from_i64 code) message +internal_to_external_error = |{ code, message }| + SqliteErr(code_from_i64(code), message) # internal use only code_from_i64 : I64 -> ErrCode -code_from_i64 = \code -> - if code == 1 || code == 0 then +code_from_i64 = |code| + if code == 1 or code == 0 then Error else if code == 2 then Internal @@ -706,11 +731,11 @@ code_from_i64 = \code -> else if code == 101 then Done else - Unknown code + Unknown(code) ## Convert a [ErrCode] to a pretty string for display purposes. errcode_to_str : ErrCode -> Str -errcode_to_str = \code -> +errcode_to_str = |code| when code is Error -> "Error: Sql error or missing database" Internal -> "Internal: Internal logic error in Sqlite" @@ -742,4 +767,4 @@ errcode_to_str = \code -> Warning -> "Warning: Warnings from sqlite3_log()" Row -> "Row: sqlite3_step() has another row ready" Done -> "Done: sqlite3_step() has finished executing" - Unknown c -> "Unknown: error code $(Num.toStr c) not known" + Unknown(c) -> "Unknown: error code ${Num.to_str(c)} not known" diff --git a/platform/Stderr.roc b/platform/Stderr.roc index d5aedf3..61735b1 100644 --- a/platform/Stderr.roc +++ b/platform/Stderr.roc @@ -34,25 +34,25 @@ IOErr : [ ] handle_err : InternalIOErr.IOErrFromHost -> [StderrErr IOErr] -handle_err = \{ tag, msg } -> +handle_err = |{ tag, msg }| when tag is - NotFound -> StderrErr NotFound - PermissionDenied -> StderrErr PermissionDenied - BrokenPipe -> StderrErr BrokenPipe - AlreadyExists -> StderrErr AlreadyExists - Interrupted -> StderrErr Interrupted - Unsupported -> StderrErr Unsupported - OutOfMemory -> StderrErr OutOfMemory - Other | EndOfFile -> StderrErr (Other msg) + NotFound -> StderrErr(NotFound) + PermissionDenied -> StderrErr(PermissionDenied) + BrokenPipe -> StderrErr(BrokenPipe) + AlreadyExists -> StderrErr(AlreadyExists) + Interrupted -> StderrErr(Interrupted) + Unsupported -> StderrErr(Unsupported) + OutOfMemory -> StderrErr(OutOfMemory) + Other | EndOfFile -> StderrErr(Other(msg)) ## Write the given string to [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)), ## followed by a newline. ## ## > To write to `stderr` without the newline, see [Stderr.write!]. line! : Str => Result {} [StderrErr IOErr] -line! = \str -> - Host.stderr_line! str - |> Result.mapErr handle_err +line! = |str| + Host.stderr_line!(str) + |> Result.map_err(handle_err) ## Write the given string to [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)). ## @@ -61,6 +61,6 @@ line! = \str -> ## ## > To write to `stderr` with a newline at the end, see [Stderr.line!]. write! : Str => Result {} [StderrErr IOErr] -write! = \str -> - Host.stderr_write! str - |> Result.mapErr handle_err +write! = |str| + Host.stderr_write!(str) + |> Result.map_err(handle_err) diff --git a/platform/Stdout.roc b/platform/Stdout.roc index 567ce3e..9d6b202 100644 --- a/platform/Stdout.roc +++ b/platform/Stdout.roc @@ -34,16 +34,16 @@ IOErr : [ ] handle_err : InternalIOErr.IOErrFromHost -> [StdoutErr IOErr] -handle_err = \{ tag, msg } -> +handle_err = |{ tag, msg }| when tag is - NotFound -> StdoutErr NotFound - PermissionDenied -> StdoutErr PermissionDenied - BrokenPipe -> StdoutErr BrokenPipe - AlreadyExists -> StdoutErr AlreadyExists - Interrupted -> StdoutErr Interrupted - Unsupported -> StdoutErr Unsupported - OutOfMemory -> StdoutErr OutOfMemory - Other | EndOfFile -> StdoutErr (Other msg) + NotFound -> StdoutErr(NotFound) + PermissionDenied -> StdoutErr(PermissionDenied) + BrokenPipe -> StdoutErr(BrokenPipe) + AlreadyExists -> StdoutErr(AlreadyExists) + Interrupted -> StdoutErr(Interrupted) + Unsupported -> StdoutErr(Unsupported) + OutOfMemory -> StdoutErr(OutOfMemory) + Other | EndOfFile -> StdoutErr(Other(msg)) ## Write the given string to [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)), ## followed by a newline. @@ -51,9 +51,9 @@ handle_err = \{ tag, msg } -> ## > To write to `stdout` without the newline, see [Stdout.write!]. ## line! : Str => Result {} [StdoutErr IOErr] -line! = \str -> - Host.stdout_line! str - |> Result.mapErr handle_err +line! = |str| + Host.stdout_line!(str) + |> Result.map_err(handle_err) ## Write the given string to [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)). ## @@ -62,6 +62,6 @@ line! = \str -> ## ## > To write to `stdout` with a newline at the end, see [Stdout.line!]. write! : Str => Result {} [StdoutErr IOErr] -write! = \str -> - Host.stdout_write! str - |> Result.mapErr handle_err +write! = |str| + Host.stdout_write!(str) + |> Result.map_err(handle_err) diff --git a/platform/Tcp.roc b/platform/Tcp.roc index 797f3d8..dd6b9fa 100644 --- a/platform/Tcp.roc +++ b/platform/Tcp.roc @@ -33,7 +33,7 @@ ConnectErr : [ ] parse_connect_err : Str -> ConnectErr -parse_connect_err = \err -> +parse_connect_err = |err| when err is "ErrorKind::PermissionDenied" -> PermissionDenied "ErrorKind::AddrInUse" -> AddrInUse @@ -42,7 +42,7 @@ parse_connect_err = \err -> "ErrorKind::Interrupted" -> Interrupted "ErrorKind::TimedOut" -> TimedOut "ErrorKind::Unsupported" -> Unsupported - other -> Unrecognized other + other -> Unrecognized(other) ## Represents errors that can occur when performing an effect with a [Stream]. StreamErr : [ @@ -57,7 +57,7 @@ StreamErr : [ ] parse_stream_err : Str -> StreamErr -parse_stream_err = \err -> +parse_stream_err = |err| when err is "StreamNotFound" -> StreamNotFound "ErrorKind::PermissionDenied" -> PermissionDenied @@ -66,13 +66,13 @@ parse_stream_err = \err -> "ErrorKind::Interrupted" -> Interrupted "ErrorKind::OutOfMemory" -> OutOfMemory "ErrorKind::BrokenPipe" -> BrokenPipe - other -> Unrecognized other + other -> Unrecognized(other) ## Opens a TCP connection to a remote host. ## ## ``` ## # Connect to localhost:8080 -## stream = Tcp.connect! "localhost" 8080 +## stream = Tcp.connect!("localhost", 8080)? ## ``` ## ## The connection is automatically closed when the last reference to the stream is dropped. @@ -84,47 +84,48 @@ parse_stream_err = \err -> ## - `roc-lang.org` ## connect! : Str, U16 => Result Stream ConnectErr -connect! = \host, port -> - Host.tcp_connect! host port - |> Result.map @Stream - |> Result.mapErr parse_connect_err +connect! = |host, port| + Host.tcp_connect!(host, port) + |> Result.map_ok(@Stream) + |> Result.map_err(parse_connect_err) ## Read up to a number of bytes from the TCP stream. ## ## ``` -## # Read up to 64 bytes from the stream and convert to a Str -## received = File.read_up_to! stream 64 -## Str.fromUtf8 received +## # Read up to 64 bytes from the stream +## received_bytes = File.read_up_to!(stream, 64)? ## ``` ## ## > To read an exact number of bytes or fail, you can use [Tcp.read_exactly!] instead. read_up_to! : Stream, U64 => Result (List U8) [TcpReadErr StreamErr] -read_up_to! = \@Stream stream, bytes_to_read -> - Host.tcp_read_up_to! stream bytes_to_read - |> Result.mapErr \err -> TcpReadErr (parse_stream_err err) +read_up_to! = |@Stream(stream), bytes_to_read| + Host.tcp_read_up_to!(stream, bytes_to_read) + |> Result.map_err(|err| TcpReadErr(parse_stream_err(err))) ## Read an exact number of bytes or fail. ## ## ``` -## bytes = File.read_exactly!? stream 64 +## bytes = File.read_exactly!(stream, 64)? ## ``` ## ## `TcpUnexpectedEOF` is returned if the stream ends before the specfied number of bytes is reached. ## read_exactly! : Stream, U64 => Result (List U8) [TcpReadErr StreamErr, TcpUnexpectedEOF] -read_exactly! = \@Stream stream, bytes_to_read -> - Host.tcp_read_exactly! stream bytes_to_read - |> Result.mapErr \err -> - if err == unexpected_eof_error_message then - TcpUnexpectedEOF - else - TcpReadErr (parse_stream_err err) +read_exactly! = |@Stream(stream), bytes_to_read| + Host.tcp_read_exactly!(stream, bytes_to_read) + |> Result.map_err( + |err| + if err == unexpected_eof_error_message then + TcpUnexpectedEOF + else + TcpReadErr(parse_stream_err(err)), + ) ## Read until a delimiter or EOF is reached. ## ## ``` ## # Read until null terminator -## File.read_until! stream 0 +## bytes = File.read_until!(stream, 0)? ## ``` ## ## If found, the delimiter is included as the last byte. @@ -132,62 +133,62 @@ read_exactly! = \@Stream stream, bytes_to_read -> ## > To read until a newline is found, you can use [Tcp.read_line!] which ## conveniently decodes to a [Str]. read_until! : Stream, U8 => Result (List U8) [TcpReadErr StreamErr] -read_until! = \@Stream stream, byte -> - Host.tcp_read_until! stream byte - |> Result.mapErr \err -> TcpReadErr (parse_stream_err err) +read_until! = |@Stream(stream), byte| + Host.tcp_read_until!(stream, byte) + |> Result.map_err(|err| TcpReadErr(parse_stream_err(err))) ## Read until a newline or EOF is reached. ## ## ``` ## # Read a line and then print it to `stdout` -## lineStr = File.read_line! stream -## Stdout.line lineStr +## line_str = File.read_line!(stream)? +## Stdout.line(lineStr)? ## ``` ## ## If found, the newline is included as the last character in the [Str]. ## read_line! : Stream => Result Str [TcpReadErr StreamErr, TcpReadBadUtf8 _] -read_line! = \stream -> - bytes = read_until!? stream '\n' +read_line! = |stream| + bytes = read_until!(stream, '\n')? - Str.fromUtf8 bytes - |> Result.mapErr TcpReadBadUtf8 + Str.from_utf8(bytes) + |> Result.map_err(TcpReadBadUtf8) ## Writes bytes to a TCP stream. ## ## ``` ## # Writes the bytes 1, 2, 3 -## Tcp.write!? stream [1, 2, 3] +## Tcp.write!(stream, [1, 2, 3])? ## ``` ## ## > To write a [Str], you can use [Tcp.write_utf8!] instead. write! : Stream, List U8 => Result {} [TcpWriteErr StreamErr] -write! = \@Stream stream, bytes -> - Host.tcp_write! stream bytes - |> Result.mapErr \err -> TcpWriteErr (parse_stream_err err) +write! = |@Stream(stream), bytes| + Host.tcp_write!(stream, bytes) + |> Result.map_err(|err| TcpWriteErr(parse_stream_err(err))) ## Writes a [Str] to a TCP stream, encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8). ## ## ``` ## # Write "Hi from Roc!" encoded as UTF-8 -## Tcp.write_utf8! stream "Hi from Roc!" +## Tcp.write_utf8!(stream, "Hi from Roc!")? ## ``` ## ## > To write unformatted bytes, you can use [Tcp.write!] instead. write_utf8! : Stream, Str => Result {} [TcpWriteErr StreamErr] -write_utf8! = \stream, str -> - write! stream (Str.toUtf8 str) +write_utf8! = |stream, str| + write!(stream, Str.to_utf8(str)) ## Convert a [ConnectErr] to a [Str] you can print. ## ## ``` ## when err is -## TcpPerfomErr (TcpConnectErr connectErr) -> -## Stderr.line (Tcp.connect_err_to_str connectErr) +## TcpPerfomErr(TcpConnectErr(connectErr)) -> +## Stderr.line!(Tcp.connect_err_to_str(connectErr)) ## ``` ## connect_err_to_str : ConnectErr -> Str -connect_err_to_str = \err -> +connect_err_to_str = |err| when err is PermissionDenied -> "PermissionDenied" AddrInUse -> "AddrInUse" @@ -196,23 +197,23 @@ connect_err_to_str = \err -> Interrupted -> "Interrupted" TimedOut -> "TimedOut" Unsupported -> "Unsupported" - Unrecognized message -> "Unrecognized Error: $(message)" + Unrecognized(message) -> "Unrecognized Error: ${message}" ## Convert a [StreamErr] to a [Str] you can print. ## ## ``` ## when err is -## TcpPerformErr (TcpReadErr err) -> -## errStr = Tcp.stream_err_to_str err -## Stderr.line "Error while reading: $(errStr)" +## TcpPerformErr(TcpReadErr(err)) -> +## errStr = Tcp.stream_err_to_str(err) +## Stderr.line!("Error while reading: ${errStr}") ## -## TcpPerformErr (TcpWriteErr err) -> -## errStr = Tcp.stream_err_to_str err -## Stderr.line "Error while writing: $(errStr)" +## TcpPerformErr(TcpWriteErr(err)) -> +## errStr = Tcp.stream_err_to_str(err) +## Stderr.line!("Error while writing: ${errStr}") ## ``` ## stream_err_to_str : StreamErr -> Str -stream_err_to_str = \err -> +stream_err_to_str = |err| when err is StreamNotFound -> "StreamNotFound" PermissionDenied -> "PermissionDenied" @@ -221,4 +222,4 @@ stream_err_to_str = \err -> Interrupted -> "Interrupted" OutOfMemory -> "OutOfMemory" BrokenPipe -> "BrokenPipe" - Unrecognized message -> "Unrecognized Error: $(message)" + Unrecognized(message) -> "Unrecognized Error: ${message}" diff --git a/platform/Url.roc b/platform/Url.roc index 71bc74a..20bac32 100644 --- a/platform/Url.roc +++ b/platform/Url.roc @@ -26,54 +26,54 @@ Url := Str implements [Inspect] ## ## The following example reserves 50 bytes, then builds the url `https://example.com/stuff?caf%C3%A9=du%20Monde&email=hi%40example.com`; ## ``` -## Url.from_str "https://example.com" -## |> Url.reserve 50 -## |> Url.append "stuff" -## |> Url.append_param "café" "du Monde" -## |> Url.append_param "email" "hi@example.com" +## Url.from_str("https://example.com") +## |> Url.reserve(50) +## |> Url.append("stuff") +## |> Url.append_param("café", "du Monde") +## |> Url.append_param("email", "hi@example.com") ## ``` -## The [Str.countUtf8Bytes](https://www.roc-lang.org/builtins/Str#countUtf8Bytes) function can be helpful in finding out how many bytes to reserve. +## The [Str.count_utf8_bytes](https://www.roc-lang.org/builtins/Str#countUtf8Bytes) function can be helpful in finding out how many bytes to reserve. ## -## There is no `Url.withCapacity` because it's better to reserve extra capacity +## There is no `Url.with_capacity` because it's better to reserve extra capacity ## on a [Str] first, and then pass that string to [Url.from_str]. This function will make use ## of the extra capacity. reserve : Url, U64 -> Url -reserve = \@Url str, cap -> - @Url (Str.reserve str (Num.intCast cap)) +reserve = |@Url(str), cap| + @Url(Str.reserve(str, Num.int_cast(cap))) ## Create a [Url] without validating or [percent-encoding](https://en.wikipedia.org/wiki/Percent-encoding) ## anything. ## ## ``` -## Url.from_str "https://example.com#stuff" +## Url.from_str("https://example.com#stuff") ## ``` ## ## URLs can be absolute, like `https://example.com`, or they can be relative, like `/blah`. ## ## ``` -## Url.from_str "/this/is#relative" +## Url.from_str("/this/is#relative") ## ``` ## ## Since nothing is validated, this can return invalid URLs. ## ## ``` -## Url.from_str "https://this is not a valid URL, not at all!" +## Url.from_str("https://this is not a valid URL, not at all!") ## ``` ## ## Naturally, passing invalid URLs to functions that need valid ones will tend to result in errors. ## from_str : Str -> Url -from_str = \str -> @Url str +from_str = |str| @Url(str) ## Return a [Str] representation of this URL. ## ``` ## # Gives "https://example.com/two%20words" -## Url.from_str "https://example.com" -## |> Url.append "two words" +## Url.from_str("https://example.com") +## |> Url.append("two words") ## |> Url.to_str ## ``` to_str : Url -> Str -to_str = \@Url str -> str +to_str = |@Url(str)| str ## [Percent-encodes](https://en.wikipedia.org/wiki/Percent-encoding) a ## [path component](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax) @@ -84,105 +84,105 @@ to_str = \@Url str -> str ## ## ``` ## # Gives https://example.com/some%20stuff -## Url.from_str "https://example.com" -## |> Url.append "some stuff" +## Url.from_str("https://example.com") +## |> Url.append("some stuff") ## ## # Gives https://example.com/stuff?search=blah#fragment -## Url.from_str "https://example.com?search=blah#fragment" -## |> Url.append "stuff" +## Url.from_str("https://example.com?search=blah#fragment") +## |> Url.append("stuff") ## ## # Gives https://example.com/things/stuff/more/etc/" ## Url.from_str "https://example.com/things/" -## |> Url.append "/stuff/" -## |> Url.append "/more/etc/" +## |> Url.append("/stuff/") +## |> Url.append("/more/etc/") ## ## # Gives https://example.com/things -## Url.from_str "https://example.com/things" -## |> Url.append "" +## Url.from_str("https://example.com/things") +## |> Url.append("") ## ``` append : Url, Str -> Url -append = \@Url url_str, suffix_unencoded -> - suffix = percent_encode suffix_unencoded +append = |@Url(url_str), suffix_unencoded| + suffix = percent_encode(suffix_unencoded) - when Str.splitFirst url_str "?" is - Ok { before, after } -> + when Str.split_first(url_str, "?") is + Ok({ before, after }) -> bytes = - Str.countUtf8Bytes before + Str.count_utf8_bytes(before) + 1 # for "/" - + Str.countUtf8Bytes suffix + + Str.count_utf8_bytes(suffix) + 1 # for "?" - + Str.countUtf8Bytes after + + Str.count_utf8_bytes(after) before - |> Str.reserve bytes - |> append_help suffix - |> Str.concat "?" - |> Str.concat after + |> Str.reserve(bytes) + |> append_help(suffix) + |> Str.concat("?") + |> Str.concat(after) |> @Url - Err NotFound -> + Err(NotFound) -> # There wasn't a query, but there might still be a fragment - when Str.splitFirst url_str "#" is - Ok { before, after } -> + when Str.split_first(url_str, "#") is + Ok({ before, after }) -> bytes = - Str.countUtf8Bytes before + Str.count_utf8_bytes(before) + 1 # for "/" - + Str.countUtf8Bytes suffix + + Str.count_utf8_bytes(suffix) + 1 # for "#" - + Str.countUtf8Bytes after + + Str.count_utf8_bytes(after) before - |> Str.reserve bytes - |> append_help suffix - |> Str.concat "#" - |> Str.concat after + |> Str.reserve(bytes) + |> append_help(suffix) + |> Str.concat("#") + |> Str.concat(after) |> @Url - Err NotFound -> + Err(NotFound) -> # No query and no fragment, so just append it - @Url (append_help url_str suffix) + @Url(append_help(url_str, suffix)) ## Internal helper append_help : Str, Str -> Str -append_help = \prefix, suffix -> - if Str.endsWith prefix "/" then - if Str.startsWith suffix "/" then +append_help = |prefix, suffix| + if Str.ends_with(prefix, "/") then + if Str.starts_with(suffix, "/") then # Avoid a double-slash by appending only the part of the suffix after the "/" - when Str.splitFirst suffix "/" is - Ok { after } -> + when Str.split_first(suffix, "/") is + Ok({ after }) -> # TODO `expect before == ""` - Str.concat prefix after + Str.concat(prefix, after) - Err NotFound -> + Err(NotFound) -> # This should never happen, because we already verified # that the suffix startsWith "/" # TODO `expect Bool.false` here with a comment - Str.concat prefix suffix + Str.concat(prefix, suffix) else # prefix ends with "/" but suffix doesn't start with one, so just append. - Str.concat prefix suffix - else if Str.startsWith suffix "/" then + Str.concat(prefix, suffix) + else if Str.starts_with(suffix, "/") then # Suffix starts with "/" but prefix doesn't end with one, so just append them. - Str.concat prefix suffix - else if Str.isEmpty prefix then + Str.concat(prefix, suffix) + else if Str.is_empty(prefix) then # Prefix is empty; return suffix. suffix - else if Str.isEmpty suffix then + else if Str.is_empty(suffix) then # Suffix is empty; return prefix. prefix else # Neither is empty, but neither has a "/", so add one in between. prefix - |> Str.concat "/" - |> Str.concat suffix + |> Str.concat("/") + |> Str.concat(suffix) ## Internal helper. This is intentionally unexposed so that you don't accidentally ## double-encode things. If you really want to percent-encode an arbitrary string, ## you can always do: ## ## ``` -## Url.from_str "" -## |> Url.append myStrToEncode +## Url.from_str("") +## |> Url.append(myStrToEncode) ## |> Url.to_str ## ``` ## @@ -191,41 +191,45 @@ append_help = \prefix, suffix -> ## use. See [this stackoverflow discussion](https://stackoverflow.com/questions/2678551/when-should-space-be-encoded-to-plus-or-20/47188851#47188851) ## for a detailed explanation. percent_encode : Str -> Str -percent_encode = \input -> +percent_encode = |input| # Optimistically assume we won't need any percent encoding, and can have # the same capacity as the input string. If we're wrong, it will get doubled. - initial_output = List.withCapacity (Str.countUtf8Bytes input |> Num.intCast) + initial_output = List.with_capacity((Str.count_utf8_bytes(input) |> Num.int_cast)) answer = - List.walk (Str.toUtf8 input) initial_output \output, byte -> - # Spec for percent-encoding: https://www.ietf.org/rfc/rfc3986.txt - if - (byte >= 97 && byte <= 122) # lowercase ASCII - || (byte >= 65 && byte <= 90) # uppercase ASCII - || (byte >= 48 && byte <= 57) # digit - then - # This is the most common case: an unreserved character, - # which needs no encoding in a path - List.append output byte - else - when byte is - 46 # '.' - | 95 # '_' - | 126 # '~' - | 150 -> # '-' - # These special characters can all be unescaped in paths - List.append output byte - - _ -> - # This needs encoding in a path - suffix = - Str.toUtf8 percent_encoded - |> List.sublist { len: 3, start: 3 * Num.intCast byte } - - List.concat output suffix - - Str.fromUtf8 answer - |> Result.withDefault "" # This should never fail + List.walk( + Str.to_utf8(input), + initial_output, + |output, byte| + # Spec for percent-encoding: https://www.ietf.org/rfc/rfc3986.txt + if + (byte >= 97 and byte <= 122) # lowercase ASCII + or (byte >= 65 and byte <= 90) # uppercase ASCII + or (byte >= 48 and byte <= 57) # digit + then + # This is the most common case: an unreserved character, + # which needs no encoding in a path + List.append(output, byte) + else + when byte is + 46 # '.' + | 95 # '_' + | 126 # '~' + | 150 -> # '-' + # These special characters can all be unescaped in paths + List.append(output, byte) + + _ -> + # This needs encoding in a path + suffix = + Str.to_utf8(percent_encoded) + |> List.sublist({ len: 3, start: 3 * Num.int_cast(byte) }) + + List.concat(output, suffix), + ) + + Str.from_utf8(answer) + |> Result.with_default("") # This should never fail ## Adds a [Str] query parameter to the end of the [Url]. ## @@ -233,49 +237,49 @@ percent_encode = \input -> ## ## ``` ## # Gives https://example.com?email=someone%40example.com -## Url.from_str "https://example.com" -## |> Url.append_param "email" "someone@example.com" +## Url.from_str("https://example.com") +## |> Url.append_param("email", "someone@example.com") ## ``` ## ## This can be called multiple times on the same URL. ## ## ``` ## # Gives https://example.com?caf%C3%A9=du%20Monde&email=hi%40example.com -## Url.from_str "https://example.com" -## |> Url.append_param "café" "du Monde" -## |> Url.append_param "email" "hi@example.com" +## Url.from_str("https://example.com") +## |> Url.append_param("café", "du Monde") +## |> Url.append_param("email", "hi@example.com") ## ``` ## append_param : Url, Str, Str -> Url -append_param = \@Url url_str, key, value -> +append_param = |@Url(url_str), key, value| { without_fragment, after_query } = - when Str.splitLast url_str "#" is - Ok { before, after } -> + when Str.split_last(url_str, "#") is + Ok({ before, after }) -> # The fragment is almost certainly going to be a small string, # so this interpolation should happen on the stack. - { without_fragment: before, after_query: "#$(after)" } + { without_fragment: before, after_query: "#${after}" } - Err NotFound -> + Err(NotFound) -> { without_fragment: url_str, after_query: "" } - encoded_key = percent_encode key - encoded_value = percent_encode value + encoded_key = percent_encode(key) + encoded_value = percent_encode(value) bytes = - Str.countUtf8Bytes without_fragment + Str.count_utf8_bytes(without_fragment) + 1 # for "?" or "&" - + Str.countUtf8Bytes encoded_key + + Str.count_utf8_bytes(encoded_key) + 1 # for "=" - + Str.countUtf8Bytes encoded_value - + Str.countUtf8Bytes after_query + + Str.count_utf8_bytes(encoded_value) + + Str.count_utf8_bytes(after_query) without_fragment - |> Str.reserve bytes - |> Str.concat (if has_query (@Url without_fragment) then "&" else "?") - |> Str.concat encoded_key - |> Str.concat "=" - |> Str.concat encoded_value - |> Str.concat after_query + |> Str.reserve(bytes) + |> Str.concat((if has_query(@Url(without_fragment)) then "&" else "?")) + |> Str.concat(encoded_key) + |> Str.concat("=") + |> Str.concat(encoded_value) + |> Str.concat(after_query) |> @Url ## Replaces the URL's [query](https://en.wikipedia.org/wiki/URL#Syntax)—the part @@ -285,44 +289,44 @@ append_param = \@Url url_str, key, value -> ## ## ``` ## # Gives https://example.com?newQuery=thisRightHere#stuff -## Url.from_str "https://example.com?key1=val1&key2=val2#stuff" -## |> Url.with_query "newQuery=thisRightHere" +## Url.from_str("https://example.com?key1=val1&key2=val2#stuff") +## |> Url.with_query("newQuery=thisRightHere") ## ## # Gives https://example.com#stuff -## Url.from_str "https://example.com?key1=val1&key2=val2#stuff" -## |> Url.with_query "" +## Url.from_str("https://example.com?key1=val1&key2=val2#stuff") +## |> Url.with_query("") ## ``` with_query : Url, Str -> Url -with_query = \@Url url_str, query_str -> +with_query = |@Url(url_str), query_str| { without_fragment, after_query } = - when Str.splitLast url_str "#" is - Ok { before, after } -> + when Str.split_last(url_str, "#") is + Ok({ before, after }) -> # The fragment is almost certainly going to be a small string, # so this interpolation should happen on the stack. - { without_fragment: before, after_query: "#$(after)" } + { without_fragment: before, after_query: "#${after}" } - Err NotFound -> + Err(NotFound) -> { without_fragment: url_str, after_query: "" } before_query = - when Str.splitLast without_fragment "?" is - Ok { before } -> before - Err NotFound -> without_fragment + when Str.split_last(without_fragment, "?") is + Ok({ before }) -> before + Err(NotFound) -> without_fragment - if Str.isEmpty query_str then - @Url (Str.concat before_query after_query) + if Str.is_empty(query_str) then + @Url(Str.concat(before_query, after_query)) else bytes = - Str.countUtf8Bytes before_query + Str.count_utf8_bytes(before_query) + 1 # for "?" - + Str.countUtf8Bytes query_str - + Str.countUtf8Bytes after_query + + Str.count_utf8_bytes(query_str) + + Str.count_utf8_bytes(after_query) before_query - |> Str.reserve bytes - |> Str.concat "?" - |> Str.concat query_str - |> Str.concat after_query + |> Str.reserve(bytes) + |> Str.concat("?") + |> Str.concat(query_str) + |> Str.concat(after_query) |> @Url ## Returns the URL's [query](https://en.wikipedia.org/wiki/URL#Syntax)—the part after @@ -332,40 +336,40 @@ with_query = \@Url url_str, query_str -> ## ## ``` ## # Gives "key1=val1&key2=val2&key3=val3" -## Url.from_str "https://example.com?key1=val1&key2=val2&key3=val3#stuff" +## Url.from_str("https://example.com?key1=val1&key2=val2&key3=val3#stuff") ## |> Url.query ## ## # Gives "" -## Url.from_str "https://example.com#stuff" +## Url.from_str("https://example.com#stuff") ## |> Url.query ## ``` ## query : Url -> Str -query = \@Url url_str -> +query = |@Url(url_str)| without_fragment = - when Str.splitLast url_str "#" is - Ok { before } -> before - Err NotFound -> url_str + when Str.split_last(url_str, "#") is + Ok({ before }) -> before + Err(NotFound) -> url_str - when Str.splitLast without_fragment "?" is - Ok { after } -> after - Err NotFound -> "" + when Str.split_last(without_fragment, "?") is + Ok({ after }) -> after + Err(NotFound) -> "" ## Returns [Bool.true] if the URL has a `?` in it. ## ## ``` ## # Gives Bool.true -## Url.from_str "https://example.com?key=value#stuff" +## Url.from_str("https://example.com?key=value#stuff") ## |> Url.has_query ## ## # Gives Bool.false -## Url.from_str "https://example.com#stuff" +## Url.from_str("https://example.com#stuff") ## |> Url.has_query ## ``` ## has_query : Url -> Bool -has_query = \@Url url_str -> - Str.contains url_str "?" +has_query = |@Url(url_str)| + Str.contains(url_str, "?") ## Returns the URL's [fragment](https://en.wikipedia.org/wiki/URL#Syntax)—the part after ## the `#`, if it has one. @@ -374,19 +378,19 @@ has_query = \@Url url_str -> ## ## ``` ## # Gives "stuff" -## Url.from_str "https://example.com#stuff" +## Url.from_str("https://example.com#stuff") ## |> Url.fragment ## ## # Gives "" -## Url.from_str "https://example.com" +## Url.from_str("https://example.com") ## |> Url.fragment ## ``` ## fragment : Url -> Str -fragment = \@Url url_str -> - when Str.splitLast url_str "#" is - Ok { after } -> after - Err NotFound -> "" +fragment = |@Url(url_str)| + when Str.split_last(url_str, "#") is + Ok({ after }) -> after + Err(NotFound) -> "" ## Replaces the URL's [fragment](https://en.wikipedia.org/wiki/URL#Syntax). ## @@ -394,52 +398,52 @@ fragment = \@Url url_str -> ## ## ``` ## # Gives https://example.com#things -## Url.from_str "https://example.com#stuff" -## |> Url.with_fragment "things" +## Url.from_str("https://example.com#stuff") +## |> Url.with_fragment("things") ## ## # Gives https://example.com#things -## Url.from_str "https://example.com" -## |> Url.with_fragment "things" +## Url.from_str("https://example.com") +## |> Url.with_fragment("things") ## ## # Gives https://example.com -## Url.from_str "https://example.com#stuff" +## Url.from_str("https://example.com#stuff") ## |> Url.with_fragment "" ## ``` ## with_fragment : Url, Str -> Url -with_fragment = \@Url url_str, fragment_str -> - when Str.splitLast url_str "#" is - Ok { before } -> - if Str.isEmpty fragment_str then +with_fragment = |@Url(url_str), fragment_str| + when Str.split_last(url_str, "#") is + Ok({ before }) -> + if Str.is_empty(fragment_str) then # If the given fragment is empty, remove the URL's fragment - @Url before + @Url(before) else # Replace the URL's old fragment with this one, discarding `after` - @Url "$(before)#$(fragment_str)" + @Url("${before}#${fragment_str}") - Err NotFound -> - if Str.isEmpty fragment_str then + Err(NotFound) -> + if Str.is_empty(fragment_str) then # If the given fragment is empty, leave the URL as having no fragment - @Url url_str + @Url(url_str) else # The URL didn't have a fragment, so give it this one - @Url "$(url_str)#$(fragment_str)" + @Url("${url_str}#${fragment_str}") ## Returns [Bool.true] if the URL has a `#` in it. ## ## ``` ## # Gives Bool.true -## Url.from_str "https://example.com?key=value#stuff" +## Url.from_str("https://example.com?key=value#stuff") ## |> Url.has_fragment ## ## # Gives Bool.false -## Url.from_str "https://example.com?key=value" +## Url.from_str("https://example.com?key=value") ## |> Url.has_fragment ## ``` ## has_fragment : Url -> Bool -has_fragment = \@Url url_str -> - Str.contains url_str "#" +has_fragment = |@Url(url_str)| + Str.contains(url_str, "#") # Adapted from the percent-encoding crate, © The rust-url developers, Apache2-licensed # @@ -448,13 +452,16 @@ percent_encoded : Str percent_encoded = "%00%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F%30%31%32%33%34%35%36%37%38%39%3A%3B%3C%3D%3E%3F%40%41%42%43%44%45%46%47%48%49%4A%4B%4C%4D%4E%4F%50%51%52%53%54%55%56%57%58%59%5A%5B%5C%5D%5E%5F%60%61%62%63%64%65%66%67%68%69%6A%6B%6C%6D%6E%6F%70%71%72%73%74%75%76%77%78%79%7A%7B%7C%7D%7E%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF" query_params : Url -> Dict Str Str -query_params = \url -> - query url - |> Str.splitOn "&" - |> List.walk (Dict.empty {}) \dict, pair -> - when Str.splitFirst pair "=" is - Ok { before, after } -> Dict.insert dict before after - Err NotFound -> Dict.insert dict pair "" +query_params = |url| + query(url) + |> Str.split_on("&") + |> List.walk( + Dict.empty({}), + |dict, pair| + when Str.split_first(pair, "=") is + Ok({ before, after }) -> Dict.insert(dict, before, after) + Err(NotFound) -> Dict.insert(dict, pair, ""), + ) ## Returns the URL's [path](https://en.wikipedia.org/wiki/URL#Syntax)—the part after ## the scheme and authority (e.g. `https://`) but before any `?` or `#` it might have. @@ -463,33 +470,33 @@ query_params = \url -> ## ## ``` ## # Gives "example.com/" -## Url.fromStr "https://example.com/?key1=val1&key2=val2&key3=val3#stuff" +## Url.fromStr("https://example.com/?key1=val1&key2=val2&key3=val3#stuff") ## |> Url.path ## ``` ## ## ``` ## # Gives "/foo/" -## Url.fromStr "/foo/?key1=val1&key2=val2&key3=val3#stuff" +## Url.fromStr("/foo/?key1=val1&key2=val2&key3=val3#stuff") ## |> Url.path ## ``` path : Url -> Str -path = \@Url urlStr -> - withoutAuthority = - when Str.splitFirst urlStr ":" is - Ok { after } -> - when Str.splitFirst after "//" is +path = |@Url(url_str)| + without_authority = + when Str.split_first(url_str, ":") is + Ok({ after }) -> + when Str.split_first(after, "//") is # Only drop the `//` if it's right after the `://` like in `https://` # (so, `before` is empty) - otherwise, the `//` is part of the path! - Ok { before, after: afterSlashes } if Str.isEmpty before -> afterSlashes + Ok({ before, after: after_slashes }) if Str.is_empty(before) -> after_slashes _ -> after # There's no `//` and also no `:` so this must be a path-only URL, e.g. "/foo?bar=baz#blah" - Err NotFound -> urlStr + Err(NotFound) -> url_str # Drop the query and/or fragment - when Str.splitLast withoutAuthority "?" is - Ok { before } -> before - Err NotFound -> - when Str.splitLast withoutAuthority "#" is - Ok { before } -> before - Err NotFound -> withoutAuthority + when Str.split_last(without_authority, "?") is + Ok({ before }) -> before + Err(NotFound) -> + when Str.split_last(without_authority, "#") is + Ok({ before }) -> before + Err(NotFound) -> without_authority diff --git a/platform/Utc.roc b/platform/Utc.roc index 9415b40..ae7da33 100644 --- a/platform/Utc.roc +++ b/platform/Utc.roc @@ -18,25 +18,25 @@ Utc := I128 implements [Inspect] ## Duration since UNIX EPOCH now! : {} => Utc -now! = \{} -> - @Utc (Num.toI128 (Host.posix_time! {})) +now! = |{}| + @Utc(Num.to_i128(Host.posix_time!({}))) # Constant number of nanoseconds in a millisecond nanos_per_milli = 1_000_000 ## Convert Utc timestamp to milliseconds to_millis_since_epoch : Utc -> I128 -to_millis_since_epoch = \@Utc nanos -> +to_millis_since_epoch = |@Utc(nanos)| nanos // nanos_per_milli ## Convert milliseconds to Utc timestamp from_millis_since_epoch : I128 -> Utc -from_millis_since_epoch = \millis -> - @Utc (millis * nanos_per_milli) +from_millis_since_epoch = |millis| + @Utc((millis * nanos_per_milli)) ## Convert Utc timestamp to nanoseconds to_nanos_since_epoch : Utc -> I128 -to_nanos_since_epoch = \@Utc nanos -> +to_nanos_since_epoch = |@Utc(nanos)| nanos ## Convert nanoseconds to Utc timestamp @@ -45,35 +45,35 @@ from_nanos_since_epoch = @Utc ## Calculate milliseconds between two Utc timestamps delta_as_millis : Utc, Utc -> U128 -delta_as_millis = \utc_a, utc_b -> - (delta_as_nanos utc_a utc_b) // nanos_per_milli +delta_as_millis = |utc_a, utc_b| + (delta_as_nanos(utc_a, utc_b)) // nanos_per_milli ## Calculate nanoseconds between two Utc timestamps delta_as_nanos : Utc, Utc -> U128 -delta_as_nanos = \@Utc nanos_a, @Utc nanos_b -> +delta_as_nanos = |@Utc(nanos_a), @Utc(nanos_b)| # bitwiseXor for best performance - nanos_a_shifted = Num.bitwiseXor (Num.toU128 nanos_a) (Num.shiftLeftBy 1 127) - nanos_b_shifted = Num.bitwiseXor (Num.toU128 nanos_b) (Num.shiftLeftBy 1 127) + nanos_a_shifted = Num.bitwise_xor(Num.to_u128(nanos_a), Num.shift_left_by(1, 127)) + nanos_b_shifted = Num.bitwise_xor(Num.to_u128(nanos_b), Num.shift_left_by(1, 127)) - Num.absDiff nanos_a_shifted nanos_b_shifted + Num.abs_diff(nanos_a_shifted, nanos_b_shifted) ## Convert Utc timestamp to ISO 8601 string ## Example: 2023-11-14T23:39:39Z to_iso_8601 : Utc -> Str -to_iso_8601 = \@Utc nanos -> +to_iso_8601 = |@Utc(nanos)| nanos - |> Num.divTrunc nanos_per_milli + |> Num.div_trunc(nanos_per_milli) |> InternalDateTime.epoch_millis_to_datetime |> InternalDateTime.to_iso_8601 # TESTS -expect delta_as_nanos (from_nanos_since_epoch 0) (from_nanos_since_epoch 0) == 0 -expect delta_as_nanos (from_nanos_since_epoch 1) (from_nanos_since_epoch 2) == 1 -expect delta_as_nanos (from_nanos_since_epoch -1) (from_nanos_since_epoch 1) == 2 -expect delta_as_nanos (from_nanos_since_epoch Num.minI128) (from_nanos_since_epoch Num.maxI128) == Num.maxU128 +expect delta_as_nanos(from_nanos_since_epoch(0), from_nanos_since_epoch(0)) == 0 +expect delta_as_nanos(from_nanos_since_epoch(1), from_nanos_since_epoch(2)) == 1 +expect delta_as_nanos(from_nanos_since_epoch(-1), from_nanos_since_epoch(1)) == 2 +expect delta_as_nanos(from_nanos_since_epoch(Num.min_i128), from_nanos_since_epoch(Num.max_i128)) == Num.max_u128 -expect delta_as_millis (from_millis_since_epoch 0) (from_millis_since_epoch 0) == 0 -expect delta_as_millis (from_nanos_since_epoch 1) (from_nanos_since_epoch 2) == 0 -expect delta_as_millis (from_millis_since_epoch 1) (from_millis_since_epoch 2) == 1 -expect delta_as_millis (from_millis_since_epoch -1) (from_millis_since_epoch 1) == 2 -expect delta_as_millis (from_nanos_since_epoch Num.minI128) (from_nanos_since_epoch Num.maxI128) == Num.maxU128 // nanos_per_milli +expect delta_as_millis(from_millis_since_epoch(0), from_millis_since_epoch(0)) == 0 +expect delta_as_millis(from_nanos_since_epoch(1), from_nanos_since_epoch(2)) == 0 +expect delta_as_millis(from_millis_since_epoch(1), from_millis_since_epoch(2)) == 1 +expect delta_as_millis(from_millis_since_epoch(-1), from_millis_since_epoch(1)) == 2 +expect delta_as_millis(from_nanos_since_epoch(Num.min_i128), from_nanos_since_epoch(Num.max_i128)) == Num.max_u128 // nanos_per_milli diff --git a/platform/libapp.roc b/platform/libapp.roc index 6c42358..082aa33 100644 --- a/platform/libapp.roc +++ b/platform/libapp.roc @@ -8,11 +8,15 @@ Model : {} # That file is an executable that runs a webserver that returns "I'm a stub...". init! : {} => Result Model [] -init! = \_ -> Ok {} +init! = |_| + Ok({}) respond! : _, _ => Result _ [] -respond! = \_, _ -> Ok { - status: 200, - headers: [], - body: Str.toUtf8 "I'm a stub, I should be replaced by the user's Roc app.", - } +respond! = |_, _| + Ok( + { + status: 200, + headers: [], + body: Str.to_utf8("I'm a stub, I should be replaced by the user's Roc app."), + }, + ) diff --git a/platform/main.roc b/platform/main.roc index d6f513f..b065a2f 100644 --- a/platform/main.roc +++ b/platform/main.roc @@ -28,34 +28,35 @@ import Stderr import InternalHttp init_for_host! : I32 => Result (Box Model) I32 -init_for_host! = \_ -> - when init! {} is - Ok model -> Ok (Box.box model) - Err (Exit code msg) -> - if Str.isEmpty msg then - Err code +init_for_host! = |_| + when init!({}) is + Ok(model) -> Ok(Box.box(model)) + Err(Exit(code, msg)) -> + if Str.is_empty(msg) then + Err(code) else - _ = Stderr.line! msg - Err code + _ = Stderr.line!(msg) + Err(code) - Err err -> - _ = Stderr.line! + Err(err) -> + _ = Stderr.line!( """ Program exited with error: - $(Inspect.toStr err) + ${Inspect.to_str(err)} - Tip: If you do not want to exit on this error, use `Result.mapErr` to handle the error. - Docs for `Result.mapErr`: - """ - Err 1 + Tip: If you do not want to exit on this error, use `Result.map_err` to handle the error. + Docs for `Result.map_err`: + """, + ) + Err(1) respond_for_host! : InternalHttp.RequestToAndFromHost, Box Model => InternalHttp.ResponseToAndFromHost -respond_for_host! = \request, boxed_model -> - when respond! (InternalHttp.from_host_request request) (Box.unbox boxed_model) is - Ok response -> InternalHttp.to_host_response response - Err (ServerErr msg) -> +respond_for_host! = |request, boxed_model| + when respond!(InternalHttp.from_host_request(request), Box.unbox(boxed_model)) is + Ok(response) -> InternalHttp.to_host_response(response) + Err(ServerErr(msg)) -> # dicard the err here if stderr fails - _ = Stderr.line! msg + _ = Stderr.line!(msg) # returns a http server error response { @@ -64,16 +65,17 @@ respond_for_host! = \request, boxed_model -> body: [], } - Err err -> + Err(err) -> # dicard the err here if stderr fails - _ = Stderr.line! + _ = Stderr.line!( """ Server error: - $(Inspect.toStr err) + ${Inspect.to_str(err)} - Tip: If you do not want to see this error, use `Result.mapErr` to handle the error. - Docs for `Result.mapErr`: - """ + Tip: If you do not want to see this error, use `Result.map_err` to handle the error. + Docs for `Result.map_err`: + """, + ) # returns a http server error response {