Current section

Files

Jump to
snag src snag.gleam
Raw

src/snag.gleam

import gleam
import gleam/int
import gleam/list
import gleam/string
/// A Snag is a boilerplate-free error type that can be used to track why an
/// error happened, though does not store as much detail on specific errors as a
/// custom error type would.
///
/// It is useful in code where it must either pass or fail, and when it fails we
/// want good debugging information to print to the user. i.e. Command line
/// tools, data processing pipelines, etc.
///
/// If it not suited to code where the application needs to make a decision about
/// what to do in the event of an error, such as whether to give up or to try
/// again. i.e. Libraries, web application backends, API clients, etc.
/// In these situations it is recommended to create a custom type for your errors
/// as it can be pattern matched on and have any additional detail added as
/// fields.
pub type Snag {
Snag(issue: String, cause: List(String))
}
/// A concise alias for a `Result` that uses a `Snag` as the error value.
pub type Result(t) =
gleam.Result(t, Snag)
/// Create a new `Snag` with the given issue text.
///
/// See also the `error` function for creating a `Snag` wrapped in a `Result`.
///
/// ### Example
///
/// ```gleam
/// new("Not enough credit")
/// |> line_print
/// // -> "error: Not enough credit"
/// ```
pub fn new(issue: String) -> Snag {
Snag(issue: issue, cause: [])
}
/// Create a new `Snag` wrapped in a `Result` with the given issue text.
///
/// ### Example
///
/// ```gleam
/// error("Not enough credit")
/// // -> Error(new("Not enough credit"))
/// ```
pub fn error(issue: String) -> Result(success) {
Error(new(issue))
}
/// Add additional contextual information to a `Snag`.
///
/// See also the `context` function for adding contextual information to a `Snag`
/// wrapped in a `Result`.
///
/// # Example
///
/// ```gleam
/// new("Not enough credit")
/// |> layer("Unable to make purchase")
/// |> line_print
/// // -> "error: Unable to make purchase <- Not enough credit"
/// ```
pub fn layer(snag: Snag, issue: String) -> Snag {
Snag(issue: issue, cause: [snag.issue, ..snag.cause])
}
/// Add additional contextual information to a `Snag` wrapped in a `Result`.
///
/// ### Example
///
/// ```gleam
/// error("Not enough credit")
/// |> context("Unable to make purchase")
/// |> result.map_error(line_print)
/// // -> Error("error: Unable to make purchase <- Not enough credit")
/// ```
pub fn context(result: Result(success), issue: String) -> Result(success) {
case result {
Ok(_) -> result
Error(snag) -> Error(layer(snag, issue))
}
}
/// Map the error type in a `Result` to a `Snag` with the given describing
/// function.
///
/// The describing function should produce a human friendly text
/// reprensentation of the error.
///
/// ### Example
///
/// ```gleam
/// my_app.read_file("api_key.txt")
/// |> snag.map_error(my_app.describe_error)
/// |> snag.context("Could not load API key")
/// |> snag.line_print
/// // -> "error: Could not load API key <- File is locked"
/// ```
pub fn map_error(
result: gleam.Result(a, b),
with describer: fn(b) -> String,
) -> Result(a) {
case result {
Ok(a) -> Ok(a)
Error(b) -> describer(b) |> error
}
}
/// Replace the error type in a `Result` with a `Snag` with the given
/// issue text.
///
/// This is especially useful for converting functions that return a `Nil`
/// error into a `Snag`. Always prefer using the `map_error` function for
/// non `Nil` errors when possible.
///
/// ### Example
///
/// ```gleam
/// dict.get(users, "user_id")
/// |> snag.replace_error("User not found in dict")
/// |> snag.context("Could not get user data")
/// |> snag.line_print
/// // -> "error: Could not get user data <- User not found in dict"
/// ```
pub fn replace_error(
result: gleam.Result(a, b),
with issue: String,
) -> Result(a) {
case result {
Ok(a) -> Ok(a)
Error(_) -> error(issue)
}
}
/// Turn a snag into a multi-line string, optimised for readability.
///
/// ### Example
///
/// ```gleam
/// new("Not enough credit")
/// |> layer("Unable to make purchase")
/// |> layer("Character creation failed")
/// |> pretty_print
/// // -> "error: Character creation failed
/// //
/// // cause:
/// // 0: Unable to make purchase
/// // 1: Not enough credit
/// // "
/// ```
pub fn pretty_print(snag: Snag) -> String {
let output = "error: " <> snag.issue <> "\n"
case snag.cause {
[] -> output
cause -> output <> "\ncause:\n" <> pretty_print_cause(cause)
}
}
fn pretty_print_cause(cause) {
cause
|> list.index_map(fn(line, index) {
string.concat([" ", int.to_string(index), ": ", line, "\n"])
})
|> string.concat
}
/// Turn a snag into a single-line string, optimised for compactness. This may be
/// useful for logging snags.
///
/// ### Example
///
/// ```gleam
/// new("Not enough credit")
/// |> layer("Unable to make purchase")
/// |> layer("Character creation failed")
/// |> line_print
/// // -> "error: Character creation failed <- Unable to make purchase <- Not enough credit"
/// ```
pub fn line_print(snag: Snag) -> String {
[string.append("error: ", snag.issue), ..snag.cause]
|> string.join(" <- ")
}