///|
/// Require a string to look like an email address.
pub fn Typed::email(self : Typed[String]) -> Typed[String] raise Invalid {
if is_email(self.val) {
self
} else {
err(self.path, "invalid email")
}
}
///|
/// Require a string to look like a URL.
pub fn Typed::url(self : Typed[String]) -> Typed[String] raise Invalid {
if is_url(self.val) {
self
} else {
err(self.path, "invalid url")
}
}
///|
/// Require a string to look like a UUID.
pub fn Typed::uuid(self : Typed[String]) -> Typed[String] raise Invalid {
if is_uuid(self.val) {
self
} else {
err(self.path, "invalid uuid")
}
}
///|
/// Require a string to look like an IPv4 address.
pub fn Typed::ipv4(self : Typed[String]) -> Typed[String] raise Invalid {
if is_ipv4(self.val) {
self
} else {
err(self.path, "invalid ipv4")
}
}
///|
/// Require a string to look like an IPv6 address.
pub fn Typed::ipv6(self : Typed[String]) -> Typed[String] raise Invalid {
if is_ipv6(self.val) {
self
} else {
err(self.path, "invalid ipv6")
}
}
///|
/// Require a string to be an ISO 8601 date (`YYYY-MM-DD`).
pub fn Typed::date(self : Typed[String]) -> Typed[String] raise Invalid {
if is_date(self.val) {
self
} else {
err(self.path, "invalid date")
}
}
///|
/// Require a string to be an ISO 8601 time (`HH:MM[:SS[.s+]]`).
pub fn Typed::time(self : Typed[String]) -> Typed[String] raise Invalid {
if is_time(self.val) {
self
} else {
err(self.path, "invalid time")
}
}
///|
/// Require a string to be an ISO 8601 datetime.
pub fn Typed::datetime(self : Typed[String]) -> Typed[String] raise Invalid {
if is_datetime(self.val) {
self
} else {
err(self.path, "invalid datetime")
}
}
///|
/// Require a string to be standard Base64.
pub fn Typed::base64(self : Typed[String]) -> Typed[String] raise Invalid {
if is_base64(self.val) {
self
} else {
err(self.path, "invalid base64")
}
}
///|
fn is_email(s : StringView) -> 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("."))
}
///|
fn is_url(s : StringView) -> Bool {
if s.contains_any(chars=" \t\r\n") {
return false
}
guard s.split_once(":") is Some((scheme, rest)) else { return false }
!rest.is_empty() && is_url_scheme(scheme)
}
///|
fn is_url_scheme(s : StringView) -> Bool {
if s.is_empty() {
return false
}
let mut first = true
for c in s {
if first {
if !c.is_ascii_alphabetic() {
return false
}
first = false
} else if !(is_ascii_alnum(c) || c == '+' || c == '.' || c == '-') {
return false
}
}
true
}
///|
fn is_uuid(s : StringView) -> Bool {
if s.length() != 36 {
return false
}
let mut i = 0
for c in s {
if i == 8 || i == 13 || i == 18 || i == 23 {
if c != '-' {
return false
}
} else if !c.is_ascii_hexdigit() {
return false
}
i = i + 1
}
true
}
///|
fn is_ipv4(s : StringView) -> Bool {
let parts = split_sep(s, ".")
if parts.length() != 4 {
return false
}
for part in parts {
guard parse_octet(part) is Some(_) else { return false }
}
true
}
///|
fn parse_octet(s : StringView) -> Int? {
if s.is_empty() || s.length() > 3 {
return None
}
let mut n = 0
let mut i = 0
for c in s {
guard digit(c) is Some(d) else { return None }
if i == 0 && d == 0 && s.length() > 1 {
return None
}
n = n * 10 + d
i = i + 1
}
if n > 255 {
None
} else {
Some(n)
}
}
///|
fn is_ipv6(s : StringView) -> Bool {
if s.contains(":::") {
return false
}
if s.contains("::") {
guard s.split_once("::") is Some((left, right)) else { return false }
if left.contains("::") || right.contains("::") {
return false
}
guard ipv6_group_count(left, ipv4=false) is Some(ln) else { return false }
guard ipv6_group_count(right, ipv4=true) is Some(rn) else { return false }
ln + rn < 8
} else {
match ipv6_group_count(s, ipv4=true) {
Some(n) => n == 8
None => false
}
}
}
///|
fn ipv6_group_count(s : StringView, ipv4~ : Bool) -> Int? {
if s.is_empty() {
return Some(0)
}
let parts = split_sep(s, ":")
let last = parts.length() - 1
let mut n = 0
for i in 0.. Bool {
let n = s.length()
if n < 1 || n > 4 {
return false
}
for c in s {
if !c.is_ascii_hexdigit() {
return false
}
}
true
}
///|
fn is_date(s : StringView) -> Bool {
if s.length() != 10 {
return false
}
let mut i = 0
let mut year = 0
let mut month = 0
let mut day = 0
for c in s {
if i == 4 || i == 7 {
if c != '-' {
return false
}
} else {
guard digit(c) is Some(d) else { return false }
if i < 4 {
year = year * 10 + d
} else if i < 7 {
month = month * 10 + d
} else {
day = day * 10 + d
}
}
i = i + 1
}
month >= 1 && month <= 12 && day >= 1 && day <= days_in_month(year, month)
}
///|
fn days_in_month(year : Int, month : Int) -> Int {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31
4 | 6 | 9 | 11 => 30
2 => if is_leap_year(year) { 29 } else { 28 }
_ => 0
}
}
///|
fn is_leap_year(year : Int) -> Bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
///|
fn is_time(s : StringView) -> Bool {
let parts = split_sep(s, ":")
match parts.length() {
2 => is_hour(parts[0]) && is_minute(parts[1])
3 => is_hour(parts[0]) && is_minute(parts[1]) && is_second(parts[2])
_ => false
}
}
///|
fn is_datetime(s : StringView) -> Bool {
guard s.split_once("T") is Some((date, rest)) else { return false }
if !is_date(date) {
return false
}
if rest.has_suffix("Z") {
guard rest.strip_suffix("Z") is Some(time) else { return false }
return is_time(time)
}
match split_offset(rest) {
Some((time, offset)) => is_time(time) && is_offset(offset)
None => is_time(rest)
}
}
///|
fn split_offset(s : StringView) -> (StringView, StringView)? {
match s.split_once("+") {
Some((time, offset)) =>
if time.is_empty() {
None
} else {
Some((time, offset))
}
None =>
match s.split_once("-") {
Some((time, offset)) =>
if time.is_empty() {
None
} else {
Some((time, offset))
}
None => None
}
}
}
///|
fn is_offset(s : StringView) -> Bool {
let parts = split_sep(s, ":")
parts.length() == 2 && is_hour(parts[0]) && is_minute(parts[1])
}
///|
fn is_hour(s : StringView) -> Bool {
is_two_digits(s, 23)
}
///|
fn is_minute(s : StringView) -> Bool {
is_two_digits(s, 59)
}
///|
fn is_second(s : StringView) -> Bool {
match s.split_once(".") {
Some((sec, frac)) =>
is_two_digits(sec, 59) && !frac.is_empty() && is_all_digits(frac)
None => is_two_digits(s, 59)
}
}
///|
fn is_two_digits(s : StringView, max : Int) -> Bool {
if s.length() != 2 {
return false
}
guard parse_nonneg(s) is Some(n) else { return false }
n <= max
}
///|
fn parse_nonneg(s : StringView) -> Int? {
if s.is_empty() {
return None
}
let mut n = 0
for c in s {
guard digit(c) is Some(d) else { return None }
n = n * 10 + d
}
Some(n)
}
///|
fn is_all_digits(s : StringView) -> Bool {
for c in s {
if !c.is_ascii_digit() {
return false
}
}
true
}
///|
fn is_base64(s : StringView) -> Bool {
if s.length() % 4 != 0 {
return false
}
let mut padding = 0
for c in s {
if padding > 0 {
if c != '=' {
return false
}
padding = padding + 1
} else if c == '=' {
padding = 1
} else if !is_base64_char(c) {
return false
}
}
padding <= 2
}
///|
fn is_base64_char(c : Char) -> Bool {
is_ascii_alnum(c) || c == '+' || c == '/'
}
///|
fn is_ascii_alnum(c : Char) -> Bool {
c.is_ascii_alphabetic() || c.is_ascii_digit()
}
///|
fn digit(c : Char) -> Int? {
if c.is_ascii_digit() {
Some(c.to_int() - '0'.to_int())
} else {
None
}
}
///|
fn split_sep(s : StringView, sep : StringView) -> Array[StringView] {
let parts : Array[StringView] = []
fn push_parts(rest : StringView) -> Unit {
match rest.split_once(sep) {
Some((head, tail)) => {
parts.push(head)
push_parts(tail)
}
None => parts.push(rest)
}
}
push_parts(s)
parts
}