Current section

Files

Jump to
valguard src valguard.gleam
Raw

src/valguard.gleam

import gleam/list
import gleam/option.{type Option}
import gleam/regexp
import gleam/result
import gleam/string
import gleam/time/timestamp
import valguard/internal/utils
/// Key/Value pair for encoding validation errors.
/// Key indicates the name of the field that failed validation
/// and Value is the error message.
pub type ValidationError {
ValidationError(key: String, value: String)
}
/// Takes a validation result and list of validation errors
/// If result is error, it adds it to the list
pub fn append_error(
result: Result(Nil, ValidationError),
list: List(ValidationError),
) -> List(ValidationError) {
case result {
Error(validation_error) -> {
list.append(list, [validation_error])
}
Ok(Nil) -> list
}
}
/// Takes a list of validation results and returns a list of the errors
/// Returns an empty list if no errors
pub fn collect_errors(
inputs: List(Result(Nil, ValidationError)),
) -> List(ValidationError) {
// filter for errors
list.filter_map(inputs, fn(x) {
case x {
Ok(Nil) -> Error(Nil)
Error(validation_error) -> Ok(validation_error)
}
})
}
/// Takes a list of validation errors and converts it to a result
/// Returns ok if list is empty
pub fn prepare(
errors: List(ValidationError),
) -> Result(Nil, List(ValidationError)) {
case list.is_empty(errors) {
True -> Ok(Nil)
False -> Error(errors)
}
}
/// Takes a list of validation errors and converts it to a result
/// wrapping the errors in a custom type. Returns Ok(Nil) if list is empty.
///
/// This functions similar to `valguard.prepare`.
pub fn prepare_with(
errors: List(ValidationError),
custom_type,
) -> Result(Nil, custom_type) {
case list.is_empty(errors) {
True -> Ok(Nil)
False -> Error(custom_type(errors))
}
}
/// Validates a list of requirements lazily.
///
/// Takes a key and list of validation functions.
///
/// Returns Ok(Nil) if success or Error(String) at first issue
pub fn list(
key: String,
list: List(fn() -> Result(Nil, String)),
) -> Result(Nil, ValidationError) {
// recursively check list of validations
case list_inner(list, Ok(Nil)) {
Ok(Nil) -> Ok(Nil)
Error(value) -> Error(ValidationError(key:, value:))
}
}
/// Inner recursive loop for list
fn list_inner(
list: List(fn() -> Result(Nil, String)),
prev: Result(Nil, String),
) -> Result(Nil, String) {
// return early if previous value was error
use _ <- result.try(prev)
case list {
[] -> prev
[next, ..rest] -> list_inner(rest, next())
}
}
/// Validates a list of requirements lazily.
///
/// Takes a key, value and list of validation functions.
///
/// Runs validation functions lazily and returns the result.
/// Returns Ok(Nil) if success or Error(String) at first issue
pub fn with(
key: String,
value,
list: List(fn(value) -> Result(Nil, String)),
) -> Result(Nil, ValidationError) {
// recursively check list of validations
case with_inner(list, value, Ok(Nil)) {
Ok(Nil) -> Ok(Nil)
Error(value) -> Error(ValidationError(key:, value:))
}
}
/// Validates a list of requirements lazily.
///
/// Takes a key, an optional value and list of validation functions.
///
/// Runs validation functions lazily and returns the result.
/// Returns Ok(Nil) if optional value is None or all validations are successful.
/// Returns Error(String) at first issue.
pub fn with_optional(
key: String,
option: Option(value),
list: List(fn(value) -> Result(Nil, String)),
) -> Result(Nil, ValidationError) {
// return early if option in None
use value <- utils.some_or(option, return: Ok(Nil))
// recursively check list of validations
case with_inner(list, value, Ok(Nil)) {
Ok(Nil) -> Ok(Nil)
Error(value) -> Error(ValidationError(key:, value:))
}
}
/// Inner recursive loop for with
fn with_inner(
list: List(fn(value) -> Result(Nil, String)),
value,
prev: Result(Nil, String),
) -> Result(Nil, String) {
// return early if previous value was error
use _ <- result.try(prev)
case list {
[] -> prev
[next, ..rest] -> with_inner(rest, value, next(value))
}
}
// ================= Validation Functions =================
/// Regex for validating emails. Used by HTML5 email input type
///
/// See spec: https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
const email_regex_pattern: String = "^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
/// Requires a int to be less than or greater than 0 to be considered required
pub fn int_required(value: Int, message message: String) -> Result(Nil, String) {
case value != 0 {
True -> Ok(Nil)
False -> Error(message)
}
}
/// Validates an int is at least a minimum value. Returns an error if value is less than the minimum
pub fn int_min(
value: Int,
min minimum: Int,
message message: String,
) -> Result(Nil, String) {
case value < minimum {
True -> Error(message)
False -> Ok(Nil)
}
}
/// Validates an int is at most a maximum value. Returns an error if value is greater than the maximum
pub fn int_max(
value: Int,
max maximum: Int,
message message: String,
) -> Result(Nil, String) {
case value > maximum {
True -> Error(message)
False -> Ok(Nil)
}
}
/// Requires a float to be less than or greater than 0.0 to be considered required
pub fn float_required(
value: Float,
message message: String,
) -> Result(Nil, String) {
case value >. 0.0 || value <. 0.0 {
True -> Ok(Nil)
False -> Error(message)
}
}
/// Validates a float is at least a minimum value. Returns an error if value is less than the minimum
pub fn float_min(
value: Float,
min minimum: Float,
message message: String,
) -> Result(Nil, String) {
case value <. minimum {
True -> Error(message)
False -> Ok(Nil)
}
}
/// Validates a float is at most a maximum value. Returns an error if value is greater than the maximum
pub fn float_max(
value: Float,
max maximum: Float,
message message: String,
) -> Result(Nil, String) {
case value >. maximum {
True -> Error(message)
False -> Ok(Nil)
}
}
/// Requires a string to not be empty to be considered required
pub fn string_required(
value: String,
message message: String,
) -> Result(Nil, String) {
case string.is_empty(value) {
False -> Ok(Nil)
True -> Error(message)
}
}
/// Validates a string is a minimum length
pub fn string_min(
value: String,
min minimum: Int,
message message: String,
) -> Result(Nil, String) {
case string.length(value) < minimum {
True -> Error(message)
False -> Ok(Nil)
}
}
/// Validates a string is at most a maximum length
pub fn string_max(
value: String,
max maximum: Int,
message message: String,
) -> Result(Nil, String) {
case string.length(value) > maximum {
True -> Error(message)
False -> Ok(Nil)
}
}
// TODO: add more string validations
// - starts_with
// - ends_with
// - includes
// - length
// - regex
/// Validates if entered email is a valid email address
pub fn email_is_valid(
email: String,
message message: String,
) -> Result(Nil, String) {
use re <- result.try(
regexp.from_string(email_regex_pattern)
|> result.replace_error(message),
)
case regexp.check(re, email) {
False -> Error(message)
True -> Ok(Nil)
}
}
/// Validates that a date is valid by checking it can be parsed correctly
pub fn date_is_valid(
datetime: String,
message message: String,
) -> Result(Nil, String) {
case timestamp.parse_rfc3339(datetime) {
Ok(_) -> Ok(Nil)
Error(_) -> Error(message)
}
}