///|
/// A successfully converted value that still carries its JSON path.
struct Checked[T] {
val : T
path : Path
}
///|
/// Types that have a length, used by `nonempty`, `min_len`, and `max_len`.
pub(open) trait Length {
fn length(Self) -> Int
}
///|
pub impl Length for String with fn length(self) {
self.length()
}
///|
pub impl[T] Length for Array[T] with fn length(self) {
self.length()
}
///|
/// Unwrap the converted value.
pub fn[T] Checked::value(self : Checked[T]) -> T {
self.val
}
///|
/// Map each array element. Errors keep the element's path.
pub fn[A, B] Checked::map(
self : Checked[Array[A]],
f : (A) -> B raise?,
) -> Checked[Array[B]] raise? {
{ val: self.val.map(f), path: self.path, }
}
///|
/// Reject empty strings and arrays.
pub fn[T : Length] Checked::nonempty(
self : Checked[T],
) -> Checked[T] raise Invalid {
expect_nonempty(self.path, Length::length(self.val))
self
}
///|
/// Require a minimum length for strings and arrays.
pub fn[T : Length] Checked::min_len(
self : Checked[T],
n : Int,
) -> Checked[T] raise Invalid {
expect_min_len(self.path, Length::length(self.val), n)
self
}
///|
/// Require a maximum length for strings and arrays.
pub fn[T : Length] Checked::max_len(
self : Checked[T],
n : Int,
) -> Checked[T] raise Invalid {
expect_max_len(self.path, Length::length(self.val), n)
self
}
///|
/// Require a string to look like an email address.
pub fn Checked::email(self : Checked[String]) -> Checked[String] raise Invalid {
if is_email(self.val) {
self
} else {
err(self.path, "invalid email")
}
}
///|
/// Require `self >= bound`.
pub fn[T : Compare + Show] Checked::min(
self : Checked[T],
bound : T,
) -> Checked[T] raise Invalid {
if self.val >= bound {
self
} else {
err(self.path, "must be at least \{bound}")
}
}
///|
/// Require `self <= bound`.
pub fn[T : Compare + Show] Checked::max(
self : Checked[T],
bound : T,
) -> Checked[T] raise Invalid {
if self.val <= bound {
self
} else {
err(self.path, "must be at most \{bound}")
}
}
///|
/// Require `min <= self <= max`.
pub fn[T : Compare + Show] Checked::range(
self : Checked[T],
min : T,
max : T,
) -> Checked[T] raise Invalid {
if self.val >= min && self.val <= max {
self
} else {
err(self.path, "must be between \{min} and \{max}")
}
}
///|
/// Apply a custom predicate. The message is used when it returns `false`.
pub fn[T] Checked::refine(
self : Checked[T],
predicate : (T) -> Bool,
message : String,
) -> Checked[T] raise Invalid {
if predicate(self.val) {
self
} else {
err(self.path, message)
}
}
///|
fn expect_nonempty(path : Path, len : Int) -> Unit raise Invalid {
if len <= 0 {
err(path, "must not be empty")
}
}
///|
fn expect_min_len(path : Path, len : Int, n : Int) -> Unit raise Invalid {
if len < n {
err(path, "length must be at least \{n}")
}
}
///|
fn expect_max_len(path : Path, len : Int, n : Int) -> Unit raise Invalid {
if len > n {
err(path, "length must be at most \{n}")
}
}
///|
fn is_email(s : String) -> Bool {
guard s.split_once("@") is Some((user, domain)) else { return false }
!(user.is_empty() ||
domain.is_empty() ||
domain.contains("@") ||
s.contains_any(chars=" \t\r\n") ||
user.contains("..") ||
domain.contains("..") ||
domain.has_prefix(".") ||
domain.has_suffix(".") ||
user.has_prefix(".") ||
user.has_suffix(".") ||
!domain.contains("."))
}