Current section
Files
Jump to
Current section
Files
src/outcome.gleam
import gleam/list
import gleam/result
import gleam/string
import non_empty_list.{type NonEmptyList}
/// The error type ie. `Result(t, Problem)`
/// An application error is either a Defect or a Failure.
/// A Defect is an unexpected application error, which shouldn't be shown to the user.
/// A Failure is an expected error.
pub type Problem {
Defect(message: String, stack: Stack)
Failure(message: String, stack: Stack)
}
/// Stax entries
/// Context is just information about the place in the application to build a stack trace.
pub type StackEntry {
StackEntryContext(String)
StackEntryDefect(String)
StackEntryFailure(String)
}
/// A stack of problems or context
pub type Stack =
NonEmptyList(StackEntry)
/// Alias to Result with Problem as error type.
pub type Outcome(t) =
Result(t, Problem)
/// Create a new Defect
/// This is a lower level function.
/// Prefer to use `error_with_defect` instead,
/// which gives you a Defect wrapped in an Error
@internal
pub fn new_defect(message: String) -> Problem {
Defect(
message: message,
stack: non_empty_list.single(StackEntryDefect(message)),
)
}
/// Create a new Failure
/// This is a lower level function.
/// Prefer to use `error_with_failure` instead,
/// which gives you a Failure wrapped in an Error
@internal
pub fn new_failure(message: String) -> Problem {
Failure(
message: message,
stack: non_empty_list.single(StackEntryFailure(message)),
)
}
/// Create a Defect wrapped in an Error
///
/// ##Example
///
/// ```gleam
/// case something {
/// True -> Ok("Yay")
/// False -> error_with_defect("Something went wrong")
/// }
/// ```
///
pub fn error_with_defect(defect: String) -> Outcome(t) {
Error(new_defect(defect))
}
/// Create Failure wrapped in an Error
///
/// ##Example
///
/// ```gleam
/// case something {
/// True -> Ok("Yay")
/// False -> error_with_failure("Invalid input")
/// }
/// ```
///
pub fn error_with_failure(failure: String) -> Outcome(t) {
Error(new_failure(failure))
}
/// Convert an `Error(String)` into an `Error(Defect)`
/// This is useful when you have a `Result(t, String)` and
/// want to convert it into a `Result(t, Problem)`
///
/// ##Example
///
/// ```gleam
/// Error("Something went wrong")
/// |> into_defect
/// ```
pub fn into_defect(result: Result(t, String)) -> Outcome(t) {
result.map_error(result, new_defect)
}
/// Convert an `Error(String)` into an `Error(Failure)`
/// This is useful when you have a `Result(t, String)` and
/// want to convert it into a `Result(t, Problem)`
///
/// ##Example
///
/// ```gleam
/// Error("Invalid input")
/// |> into_failure
/// ```
pub fn into_failure(result: Result(t, String)) -> Outcome(t) {
result.map_error(result, new_failure)
}
/// Convert an `Error(t)` into a wrapped Defect, by using a mapping function
/// Similar to into_defect, but takes a function to map the error value to a string
pub fn map_into_defect(
result: Result(t, e),
mapper: fn(e) -> String,
) -> Outcome(t) {
result
|> result.map_error(mapper)
|> into_defect
}
/// Convert an `Error(t)` into a wrapped Failure, by using a mapping function
/// Similar to into_defect, but takes a function to map the error value to a string
pub fn map_into_failure(
result: Result(t, e),
mapper: fn(e) -> String,
) -> Outcome(t) {
result
|> result.map_error(mapper)
|> into_failure
}
/// Replaces an `Error(Nil)` with an `Error(Defect)`
/// This is useful when you have a `Result(t, Nil)` and
/// want to convert it into a `Result(t, Problem)`
///
/// ##Example
///
/// ```gleam
/// Error(Nil)
/// |> as_defect("Something went wrong")
/// ```
pub fn as_defect(result: Result(t, Nil), e: String) -> Outcome(t) {
result.replace_error(result, new_defect(e))
}
/// Replaces an Error(Nil) with an Error(Failure)
/// This is useful when you have a `Result(t, Nil)` and
/// want to convert it into a `Result(t, Problem)`
///
/// ##Example
///
/// ```gleam
/// Error(Nil)
/// |> as_failure("Invalid input")
/// ```
pub fn as_failure(result: Result(t, Nil), e: String) -> Outcome(t) {
result.replace_error(result, new_failure(e))
}
/// Add context to an Outcome
/// This will add a Context entry to the stack
///
/// ##Example
///
/// ```gleam
/// Error("Something went wrong")
/// |> into_defect
/// |> with_context("In find user function")
/// ```
///
pub fn with_context(
outcome outcome: Outcome(t),
context context: String,
) -> Outcome(t) {
result.map_error(outcome, fn(problem) {
add_context_to_problem(problem, context)
})
}
/// Add a defect to an Outcome (Result)
/// If the outcome is already a defect, it will stay as the previous defect.
/// This always adds the defect to the stack.
///
/// ##Example
///
/// ```gleam
/// Error("Invalid input")
/// |> into_failure
/// |> with_defect("Something went wrong")
/// ```
pub fn with_defect(
outcome outcome: Outcome(t),
defect_message defect_message: String,
) -> Outcome(t) {
result.map_error(outcome, problem_with_defect(_, defect_message))
}
/// Add a failure to an Outcome (Result).
/// If the outcome is already a defect, it will stay as the previous defect.
/// This always adds the failure to the stack.
///
/// ##Example
///
/// ```gleam
/// Error("Invalid input")
/// |> into_failure
/// |> with_failure("Another failure")
/// ```
pub fn with_failure(
outcome outcome: Outcome(t),
failure_message failure_message: String,
) -> Outcome(t) {
result.map_error(outcome, problem_with_failure(_, failure_message))
}
/// Add a defect to a Problem
/// This is a low level function.
/// Prefer to use `with_defect` instead which maps the Error.
@internal
pub fn problem_with_defect(problem: Problem, defect_message: String) -> Problem {
case problem {
Defect(current_defect_message, stack) ->
// A defect stays a defect
// We only push the failure into the stack
Defect(
current_defect_message,
push_to_stack(stack, StackEntryDefect(defect_message)),
)
Failure(_, stack) ->
// A failure becomes a defect
Defect(
defect_message,
push_to_stack(stack, StackEntryDefect(defect_message)),
)
}
}
/// Add a failure to a Problem
/// This is a low level function.
/// Prefer to use `with_failure` instead which maps the Error.
@internal
pub fn problem_with_failure(problem: Problem, failure_message: String) -> Problem {
case problem {
Defect(current_defect_message, stack) ->
// A defect stays a defect
// We only push the failure into the stack
Defect(
current_defect_message,
push_to_stack(stack, StackEntryFailure(failure_message)),
)
Failure(_, stack) ->
Failure(
failure_message,
push_to_stack(stack, StackEntryFailure(failure_message)),
)
}
}
/// Add an StackEntry to the top of a Problem stack.
/// This is a low level function.
/// You shouldn't need to use this, unless you need to change the stack directly.
fn add_to_problem_stack(problem: Problem, stack_entry: StackEntry) -> Problem {
case problem {
Defect(problem, stack) -> Defect(problem, push_to_stack(stack, stack_entry))
Failure(problem, stack) ->
Failure(problem, push_to_stack(stack, stack_entry))
}
}
fn push_to_stack(stack: Stack, entry: StackEntry) -> NonEmptyList(StackEntry) {
non_empty_list.prepend(stack, entry)
}
/// A context to a Problem
/// This is a low level function.
/// Prefer to use `with_context` instead which maps the Error.
fn add_context_to_problem(problem: Problem, value: String) -> Problem {
add_to_problem_stack(problem, StackEntryContext(value))
}
/// Use this to show a failure to a user.
/// If the Problem is a failure, it will show that
/// otherwise it will show the default message given.
/// We don't want to show defect messages to users.
///
/// ##Example
///
/// ```gleam
/// case result {
/// Ok(value) -> io.debug("Success")
/// Error(problem) -> io.error(unwrap_failure(problem, "Something went wrong"))
/// }
/// ```
pub fn unwrap_failure(problem: Problem, default_message: String) -> String {
case problem {
Defect(_, _) -> default_message
Failure(message, _) -> message
}
}
fn stack_to_lines(stack: Stack) -> List(String) {
stack
|> non_empty_list.to_list
|> list.map(pretty_print_stack_entry)
}
@internal
pub fn outcome_to_lines(outcome: Outcome(t)) -> List(String) {
case outcome {
Ok(_) -> []
Error(problem) -> stack_to_lines(problem.stack)
}
}
/// Pretty print a Problem, including the stack.
/// The latest problem appears at the top of the stack.
///
/// ##Example
///
/// ```gleam
/// Error("Something went wrong")
/// |> into_defect
/// |> with_context("In find user function")
/// |> with_failure("Another failure")
/// |> pretty_print
/// ```
///
/// ```
/// Defect: Something went wrong
///
/// stack:
/// Failure: Another failure
/// Context: In find user function
/// Defect: Something went wrong
/// ```
pub fn pretty_print(problem: Problem) -> String {
let stack =
problem.stack
|> stack_to_lines
|> string.join("\n ")
prettry_print_problem_value(problem) <> "\n\nstack:\n " <> stack
}
@internal
pub fn pretty_print_outcome(outcome: Outcome(t)) -> String {
case outcome {
Ok(_) -> "Ok"
Error(problem) -> pretty_print(problem)
}
}
fn prettry_print_problem_value(problem: Problem) -> String {
case problem {
Defect(value, _) -> "Defect: " <> value
Failure(value, _) -> "Failure: " <> value
}
}
fn pretty_print_stack_entry(entry: StackEntry) -> String {
case entry {
StackEntryContext(value) -> "Context: " <> value
StackEntryDefect(value) -> "Defect: " <> value
StackEntryFailure(value) -> "Failure: " <> value
}
}