Current section

Files

Jump to
validate_monadic src validate_monadic.gleam
Raw

src/validate_monadic.gleam

import gleam/list
import gleam/result
/// Simple type alias over a Result with a *non-empty* list off generic errors
/// for the Error branch. The non-empty list is important here. For validation we must
/// represent a Result type that can have multiple errors, but we must avoid allowing
/// something like `Error([])` in which we can have an error branch but no errors!
pub type Validation(validated, error) =
Result(validated, ErrorList(error))
/// Simple type alias for a non-empty list. A non-empty list is just a structure containing the
/// first item of the list, followed by the rest of the list. You can find a more
/// useful implementation [here](https://hexdocs.pm/non_empty_list/non_empty_list.html#NonEmptyList)
/// . For the purposes of not including an extra dependency, we just use a tuple here.
pub type ErrorList(error) =
#(error, List(error))
/// Convenience function for lifting a single error into our non-empty list error.
pub fn error(err: error) -> Validation(a, error) {
Error(#(err, []))
}
/// Convenience function for lifting a value into our validation type's `Ok` branch, like other methods
/// in this module, it is literally just an alias for a `Result` type method.
pub fn succeed(a) -> Validation(a, error) {
Ok(a)
}
/// Map a validation type to another type. This is often useful to nest the result of a validation
/// into a "ValidatedType". This is just an alias over `result.map`
///
/// ```gleam
/// pub type ValidatedLastName {
/// ValidatedLastName(String)
/// }
///
/// // ...
///
/// let last_name_result =
/// form.last_name
/// |> string_non_empty
/// |> validate.map(ValidatedLastName)
/// ```
pub fn map(
over validation: Validation(a, error),
with map_fn: fn(a) -> b,
) -> Validation(b, error) {
result.map(validation, map_fn)
}
/// Map over all the errors for a validation result.
/// This is very useful for cases where you have re-usable validators with generic error messages,
/// and you wish to specify the errors are associated with a specific field
///
/// ```gleam
///
/// let validation_result =
/// raw_field
/// |> validate.compose(no_numbers, [shorter_than_10])
/// |> validate.map_error(string.append("Field Name Error: ", _))
/// ```
pub fn map_error(
over validation: Validation(a, error_a),
with map_fn: fn(error_a) -> error_b,
) -> Validation(a, error_b) {
case validation {
Ok(v) -> Ok(v)
Error(#(head, rest)) -> {
Error(#(map_fn(head), list.map(rest, map_fn)))
}
}
}
/// Compose together multiple validations. This combine the errors of all validations that fail,
/// and does not stop at the first failure. Takes the input to be validated as the first argument,
/// then a non-empty list of unary functions that take that input and return a validation result
/// of the same type.
///
///
/// ```gleam
///
/// let validation_result =
/// raw_field
/// |> validate.compose(no_numbers, [shorter_than_10, no_symbols, no_whitespace])
/// ```
pub fn compose(
input: a,
validation: fn(a) -> Validation(b, error),
validations: List(fn(a) -> Validation(b, error)),
) -> Validation(b, error) {
list.fold(validations, validation(input), fn(acc, cur) {
and_also(acc, cur(input))
})
}
/// Combine two validation results into one. This mainly for merging errors. The `Ok `branch of the
/// last validation supplied will be the returned `Ok` branch. This is used internally by `compose`
pub fn and_also(
validation_a: Validation(a, error),
validation_b: Validation(a, error),
) -> Validation(a, error) {
case validation_a, validation_b {
Ok(_), Ok(a) -> Ok(a)
Error(#(err_a_head, err_a_rest)), Error(#(err_b_head, err_b_rest)) -> {
Error(#(
err_a_head,
list.concat([err_a_rest, list.prepend(err_b_rest, err_b_head)]),
))
}
Error(err), _ -> Error(err)
_, Error(err) -> Error(err)
}
}
/// Specify a validation that will run after a given validation, using its result. This is very
/// useful for validations that need to run after a transform is attempted.
///
/// ```gleam
/// let age_result =
/// form.age_string
/// |> is_parsable_int
/// |> validate.and_then(int_less_than(_, 101))
/// ```
///
/// It can easily be used in conjuction with `compose`
///
/// ```gleam
/// let age_result =
/// form.age_string
/// |> is_parsable_int
/// |> validate.and_then(
/// validate.compose(int_less_than(_, 101), [int_greater_than(_, 0)])
/// )
/// ```
pub fn and_then(
over validation: Validation(a, error),
bind bind_fn: fn(a) -> Validation(b, error),
) -> Validation(b, error) {
result.then(validation, bind_fn)
}
/// Used to create applicative chains of validation. This is very important for combining validation
/// of fields into the validation of an entire form.
///
/// ```gleam
/// let validate_form = function.curry3(fn (
/// ValidFirstName,
/// ValidLastName,
/// ValidAge
/// ) {
/// ValidForm(ValidFirstName, ValidLastName, ValidAge)
/// })
///
/// let validation_result =
/// validate.succeed(validate_form)
/// |> validate.and_map(first_name_result)
/// |> validate.and_map(last_name_result)
/// |> validate.and_map(age_result)
/// ```
pub fn and_map(
prev prev_validation: Validation(fn(a) -> b, error),
next validation: Validation(a, error),
) -> Validation(b, error) {
case prev_validation {
Ok(apply) -> {
case validation {
Ok(a) -> Ok(apply(a))
Error(err) -> Error(err)
}
}
Error(#(prev_err_head, prev_err_rest)) -> {
case validation {
Ok(_) -> {
Error(#(prev_err_head, prev_err_rest))
}
Error(#(next_err_head, next_err_rest)) -> {
Error(#(
prev_err_head,
list.flatten([
prev_err_rest,
list.prepend(next_err_rest, next_err_head),
]),
))
}
}
}
}
}