use serde_json::Value;
use crate::error::{ValidationIssue, VldError};
use crate::schema::VldSchema;
mod checks;
mod formats;
#[cfg(feature = "openapi")]
use checks::regex_escape;
use checks::{Kind, StringCheck};
#[derive(Clone)]
enum StringTransform {
Trim,
ToLowerCase,
ToUpperCase,
}
#[derive(Clone)]
pub struct ZString {
checks: Vec<StringCheck>,
transforms: Vec<StringTransform>,
coerce: bool,
custom_type_error: Option<String>,
messages: crate::primitives::Messages,
}
impl ZString {
pub fn new() -> Self {
Self {
checks: vec![],
transforms: vec![],
coerce: false,
custom_type_error: None,
messages: None,
}
}
fn push(mut self, kind: Kind, msg: Option<String>) -> Self {
self.checks.push(StringCheck { kind, msg });
self
}
pub fn type_error(mut self, msg: impl Into<String>) -> Self {
self.custom_type_error = Some(msg.into());
self
}
pub fn with_messages<F>(mut self, f: F) -> Self
where
F: Fn(&str) -> Option<String> + Send + Sync + 'static,
{
self.messages = Some(std::sync::Arc::new(f));
self
}
pub fn min(self, len: usize) -> Self {
self.push(Kind::Min(len), None)
}
pub fn min_msg(self, len: usize, msg: impl Into<String>) -> Self {
self.push(Kind::Min(len), Some(msg.into()))
}
pub fn max(self, len: usize) -> Self {
self.push(Kind::Max(len), None)
}
pub fn max_msg(self, len: usize, msg: impl Into<String>) -> Self {
self.push(Kind::Max(len), Some(msg.into()))
}
pub fn len(self, len: usize) -> Self {
self.push(Kind::Len(len), None)
}
pub fn len_msg(self, len: usize, msg: impl Into<String>) -> Self {
self.push(Kind::Len(len), Some(msg.into()))
}
#[cfg(feature = "regex")]
#[cfg_attr(docsrs, doc(cfg(feature = "regex")))]
pub fn regex(self, re: regex_lite::Regex) -> Self {
self.push(Kind::Regex(re), None)
}
#[cfg(feature = "regex")]
#[cfg_attr(docsrs, doc(cfg(feature = "regex")))]
pub fn regex_msg(self, re: regex_lite::Regex, msg: impl Into<String>) -> Self {
self.push(Kind::Regex(re), Some(msg.into()))
}
pub fn starts_with(self, prefix: impl Into<String>) -> Self {
self.push(Kind::StartsWith(prefix.into()), None)
}
pub fn starts_with_msg(self, prefix: impl Into<String>, msg: impl Into<String>) -> Self {
self.push(Kind::StartsWith(prefix.into()), Some(msg.into()))
}
pub fn ends_with(self, suffix: impl Into<String>) -> Self {
self.push(Kind::EndsWith(suffix.into()), None)
}
pub fn ends_with_msg(self, suffix: impl Into<String>, msg: impl Into<String>) -> Self {
self.push(Kind::EndsWith(suffix.into()), Some(msg.into()))
}
pub fn contains(self, sub: impl Into<String>) -> Self {
self.push(Kind::Contains(sub.into()), None)
}
pub fn contains_msg(self, sub: impl Into<String>, msg: impl Into<String>) -> Self {
self.push(Kind::Contains(sub.into()), Some(msg.into()))
}
pub fn non_empty(self) -> Self {
self.push(Kind::NonEmpty, None)
}
pub fn non_empty_msg(self, msg: impl Into<String>) -> Self {
self.push(Kind::NonEmpty, Some(msg.into()))
}
pub fn trim(mut self) -> Self {
self.transforms.push(StringTransform::Trim);
self
}
pub fn to_lowercase(mut self) -> Self {
self.transforms.push(StringTransform::ToLowerCase);
self
}
pub fn to_uppercase(mut self) -> Self {
self.transforms.push(StringTransform::ToUpperCase);
self
}
pub fn coerce(mut self) -> Self {
self.coerce = true;
self
}
}
impl Default for ZString {
fn default() -> Self {
Self::new()
}
}
impl VldSchema for ZString {
type Output = String;
fn parse_value(&self, value: &Value) -> Result<String, VldError> {
let type_err = || crate::primitives::type_error(&self.custom_type_error, "string", value);
let mut s = match value {
Value::String(s) => s.clone(),
Value::Number(n) if self.coerce => n.to_string(),
Value::Bool(b) if self.coerce => b.to_string(),
_ => return Err(type_err()),
};
for t in &self.transforms {
s = match t {
StringTransform::Trim => s.trim().to_string(),
StringTransform::ToLowerCase => s.to_lowercase(),
StringTransform::ToUpperCase => s.to_uppercase(),
};
}
let mut received: Option<Value> = None;
let mut errors = VldError::new();
for check in &self.checks {
if let Some(code) = check.kind.failure(&s) {
let received = received.get_or_insert_with(|| Value::String(s.clone()));
let bulk = || self.messages.as_ref().and_then(|f| f(check.kind.key()));
let msg = check.msg.clone().or_else(bulk);
errors.issues.push(
ValidationIssue::with_override(code, msg.as_ref()).with_received(received),
);
}
}
if errors.is_empty() {
Ok(s)
} else {
Err(errors)
}
}
}
#[cfg(feature = "openapi")]
impl crate::openapi::JsonSchema for ZString {
fn json_schema(&self) -> serde_json::Value {
let mut schema = serde_json::json!({"type": "string"});
let (mut mins, mut maxs) = (vec![], vec![]);
let mut formats: Vec<&str> = vec![];
let mut patterns: Vec<String> = vec![];
for check in &self.checks {
match &check.kind {
Kind::Min(n) => mins.push(*n),
Kind::Max(n) => maxs.push(*n),
Kind::Len(n) => {
mins.push(*n);
maxs.push(*n);
}
Kind::NonEmpty => mins.push(1),
Kind::StartsWith(p) => patterns.push(format!("^{}", regex_escape(p))),
Kind::EndsWith(p) => patterns.push(format!("{}$", regex_escape(p))),
Kind::Contains(p) => patterns.push(regex_escape(p)),
#[cfg(feature = "regex")]
Kind::Regex(re) => patterns.push(re.as_str().to_string()),
Kind::Format(f) => {
if !formats.contains(&f.schema_format()) {
formats.push(f.schema_format());
}
}
}
}
crate::primitives::length_schema(&mut schema, ("minLength", "maxLength"), mins, maxs);
let mut all_of = vec![];
let mut formats = formats.into_iter();
if let Some(f) = formats.next() {
schema["format"] = serde_json::json!(f);
}
all_of.extend(formats.map(|f| serde_json::json!({ "format": f })));
let mut patterns = patterns.into_iter();
if let Some(p) = patterns.next() {
schema["pattern"] = serde_json::json!(p);
}
all_of.extend(patterns.map(|p| serde_json::json!({ "pattern": p })));
if !all_of.is_empty() {
schema["allOf"] = serde_json::Value::Array(all_of);
}
schema
}
}
#[cfg(feature = "openapi")]
impl crate::openapi::CollectNestedSchemas for ZString {}