use std::collections::BTreeMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use gpui::SharedString;
static NEXT_RECORD_REVISION: AtomicU64 = AtomicU64::new(1);
fn next_record_revision() -> u64 {
NEXT_RECORD_REVISION.fetch_add(1, Ordering::Relaxed)
}
#[derive(Clone, Debug)]
pub struct ValidationErrors {
revision: u64,
entries: BTreeMap<SharedString, Vec<SharedString>>,
}
impl ValidationErrors {
pub fn new() -> Self {
Self {
revision: next_record_revision(),
entries: BTreeMap::new(),
}
}
pub fn revision(&self) -> u64 {
self.revision
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn get(&self, name: &str) -> Option<&[SharedString]> {
self.entries.get(name).map(Vec::as_slice)
}
pub fn iter(&self) -> impl Iterator<Item = (&SharedString, &[SharedString])> {
self.entries
.iter()
.map(|(name, messages)| (name, &**messages))
}
pub fn names(&self) -> impl Iterator<Item = &SharedString> {
self.entries.keys()
}
pub fn set(mut self, name: impl Into<SharedString>, message: impl Into<SharedString>) -> Self {
self.entries.insert(name.into(), vec![message.into()]);
self.revision = next_record_revision();
self
}
pub fn set_many(
mut self,
name: impl Into<SharedString>,
messages: impl IntoIterator<Item = impl Into<SharedString>>,
) -> Self {
let messages: Vec<SharedString> = messages.into_iter().map(Into::into).collect();
self.entries.insert(name.into(), messages);
self.revision = next_record_revision();
self
}
}
impl Default for ValidationErrors {
fn default() -> Self {
Self::new()
}
}
impl PartialEq for ValidationErrors {
fn eq(&self, other: &Self) -> bool {
self.entries == other.entries
}
}
impl Eq for ValidationErrors {}
pub type Validator<T> = Arc<dyn Fn(&T) -> Option<SharedString> + 'static>;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Validity {
pub is_invalid: bool,
pub messages: Vec<SharedString>,
}
impl Validity {
pub fn first(&self) -> Option<SharedString> {
self.messages.first().cloned()
}
pub fn joined(&self) -> String {
self.messages
.iter()
.map(|message| &message[..])
.collect::<Vec<&str>>()
.join(" ")
}
}
pub fn resolve(
is_invalid: bool,
errors: &[SharedString],
validate_result: Option<SharedString>,
own_message: Option<SharedString>,
) -> Validity {
let mut messages: Vec<SharedString> = Vec::new();
messages.extend(errors.iter().cloned());
if let Some(m) = validate_result {
messages.push(m);
}
if messages.is_empty() {
if let Some(m) = own_message {
messages.push(m);
}
}
Validity {
is_invalid: is_invalid || !messages.is_empty(),
messages,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn s(v: &str) -> SharedString {
SharedString::from(v.to_owned())
}
#[test]
fn clean_value_is_valid() {
let v = resolve(false, &[], None, None);
assert!(!v.is_invalid);
assert!(v.messages.is_empty());
assert_eq!(v.first(), None);
}
#[test]
fn is_invalid_alone_marks_the_field() {
let v = resolve(true, &[], None, None);
assert!(v.is_invalid);
assert!(v.messages.is_empty());
}
#[test]
fn a_validate_failure_marks_and_reports() {
let v = resolve(false, &[], Some(s("Too short")), None);
assert!(v.is_invalid);
assert_eq!(v.first(), Some(s("Too short")));
}
#[test]
fn server_errors_come_first() {
let v = resolve(false, &[s("Already taken")], Some(s("Too short")), None);
assert_eq!(v.messages, vec![s("Already taken"), s("Too short")]);
assert_eq!(v.first(), Some(s("Already taken")));
}
#[test]
fn own_message_is_a_fallback_not_an_addition() {
let v = resolve(
false,
&[],
Some(s("Too short")),
Some(s("Check this field")),
);
assert_eq!(v.messages, vec![s("Too short")]);
let v = resolve(false, &[], None, Some(s("Check this field")));
assert_eq!(v.messages, vec![s("Check this field")]);
assert!(v.is_invalid);
}
#[test]
fn joined_renders_every_message_in_upstream_order() {
let v = resolve(
false,
&[s("Already registered"), s("Check the server")],
Some(s("Too short")),
None,
);
assert_eq!(v.joined(), "Already registered Check the server Too short");
assert_eq!(v.first(), Some(s("Already registered")));
assert_eq!(resolve(false, &[], None, None).joined(), "");
}
#[test]
fn a_record_maps_names_to_one_or_many_messages() {
let record = ValidationErrors::new()
.set("email", "Already registered")
.set_many("roles", ["Role A", "Role B"]);
assert_eq!(record.get("email"), Some(&[s("Already registered")][..]));
assert_eq!(record.get("roles"), Some(&[s("Role A"), s("Role B")][..]));
assert_eq!(record.get("absent"), None);
assert!(!record.is_empty());
assert_eq!(
record.names().cloned().collect::<Vec<_>>(),
vec![s("email"), s("roles")]
);
assert_eq!(record.iter().count(), 2);
assert!(ValidationErrors::new().is_empty());
}
#[test]
fn a_clone_keeps_record_identity_while_a_new_record_mints() {
let record = ValidationErrors::new().set("email", "Taken");
let clone = record.clone();
assert_eq!(clone.revision(), record.revision(), "clones share identity");
assert_eq!(clone, record, "equality is content only");
let rebuilt = ValidationErrors::new().set("email", "Taken");
assert_eq!(rebuilt, record, "content equality holds");
assert_ne!(
rebuilt.revision(),
record.revision(),
"a genuinely new record must carry a new revision"
);
assert_ne!(
ValidationErrors::default().revision(),
ValidationErrors::default().revision(),
"even two defaults are distinct responses"
);
let built = ValidationErrors::default().set("email", "Taken");
assert_ne!(built.revision(), 0);
assert_ne!(built.revision(), ValidationErrors::default().revision());
}
}