88 lines
2.8 KiB
Rust
88 lines
2.8 KiB
Rust
//! Form types. A form lives in a newtype whose only constructor is a fallible
|
|
//! `parse`; deserializing goes through the same parse, so a malformed value can
|
|
//! neither be built in Rust nor read back from a store.
|
|
|
|
use std::fmt;
|
|
|
|
/// A value that is not of the form its type names.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct FormError {
|
|
pub form: &'static str,
|
|
pub got: String,
|
|
}
|
|
|
|
impl FormError {
|
|
pub fn new(form: &'static str, got: impl fmt::Display) -> Self {
|
|
Self {
|
|
form,
|
|
got: got.to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for FormError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{:?} is not a {}", self.got, self.form)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for FormError {}
|
|
|
|
/// Wires a string form type (with `parse` and `Display`) to FromStr, serde's
|
|
/// `try_from`/`into` String and a published schema.
|
|
macro_rules! string_form {
|
|
($t:ident, $schema:tt) => {
|
|
impl std::str::FromStr for $t {
|
|
type Err = $crate::form::FormError;
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Self::parse(s)
|
|
}
|
|
}
|
|
impl TryFrom<String> for $t {
|
|
type Error = $crate::form::FormError;
|
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
|
Self::parse(&s)
|
|
}
|
|
}
|
|
impl From<$t> for String {
|
|
fn from(v: $t) -> String {
|
|
v.to_string()
|
|
}
|
|
}
|
|
impl schemars::JsonSchema for $t {
|
|
fn schema_name() -> std::borrow::Cow<'static, str> {
|
|
stringify!($t).into()
|
|
}
|
|
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
|
schemars::json_schema!($schema)
|
|
}
|
|
}
|
|
};
|
|
}
|
|
pub(crate) use string_form;
|
|
|
|
/// A BCP 47-form language tag: 1 to 35 bytes of ASCII alphanumerics in
|
|
/// hyphen-separated subtags of 1 to 8 bytes; the first is 2 to 8 letters, or the
|
|
/// single letter `x` of a private-use tag. Form only: no registry is consulted.
|
|
pub(crate) fn is_language_tag(s: &str) -> bool {
|
|
if s.len() > 35 {
|
|
return false;
|
|
}
|
|
let mut subtags = s.split('-');
|
|
let first = subtags.next().unwrap_or("");
|
|
let first_ok = first.bytes().all(|b| b.is_ascii_alphabetic())
|
|
&& ((2..=8).contains(&first.len()) || first == "x");
|
|
first_ok
|
|
&& subtags
|
|
.all(|t| (1..=8).contains(&t.len()) && t.bytes().all(|b| b.is_ascii_alphanumeric()))
|
|
}
|
|
|
|
/// A slug: `[a-z0-9]` and inner `-`, 1 to 48 bytes, no edge `-`.
|
|
pub(crate) fn is_slug(s: &str) -> bool {
|
|
let b = s.as_bytes();
|
|
(1..=48).contains(&b.len())
|
|
&& b.iter()
|
|
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || *c == b'-')
|
|
&& b[0] != b'-'
|
|
&& b[b.len() - 1] != b'-'
|
|
}
|