use super::error::{ErrorMeta, ErrorSeverity};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Constraint {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub pred: Predicate,
pub error: ErrorMeta,
}
impl<'de> Deserialize<'de> for Constraint {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
if value.get("pred").is_some() {
#[derive(Deserialize)]
struct WelConstraint {
#[serde(default)]
id: Option<String>,
pred: Predicate,
error: ErrorMeta,
}
let wel: WelConstraint =
serde_json::from_value(value).map_err(serde::de::Error::custom)?;
Ok(Constraint {
id: wel.id,
pred: wel.pred,
error: wel.error,
})
} else if value.get("type").is_some() {
let template: TemplateConstraint =
serde_json::from_value(value).map_err(serde::de::Error::custom)?;
Ok(template.into_constraint())
} else {
Err(serde::de::Error::custom(
"constraint must have either 'pred' (wellformed format) or 'type' (templates format)",
))
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
enum TemplateConstraint {
Pattern {
value: String,
#[serde(default)]
message: Option<String>,
#[serde(default)]
source: Option<String>,
},
MaxLength {
value: usize,
#[serde(default)]
message: Option<String>,
#[serde(default)]
source: Option<String>,
},
MinLength {
value: usize,
#[serde(default)]
message: Option<String>,
#[serde(default)]
source: Option<String>,
},
Format {
value: String,
#[serde(default)]
message: Option<String>,
#[serde(default)]
source: Option<String>,
},
Enum {
#[serde(alias = "value")]
values: Vec<Value>,
#[serde(default)]
message: Option<String>,
#[serde(default)]
source: Option<String>,
},
}
impl TemplateConstraint {
fn into_constraint(self) -> Constraint {
match self {
TemplateConstraint::Pattern {
value,
message,
source,
} => Constraint {
id: None,
pred: Predicate::Regex {
pattern: value.clone(),
flags: None,
},
error: ErrorMeta {
code: "PATTERN_MISMATCH".to_string(),
message: message.unwrap_or_else(|| format!("Must match pattern {}", value)),
path: None,
severity: ErrorSeverity::Error,
help: None,
source,
},
},
TemplateConstraint::MaxLength {
value,
message,
source,
} => Constraint {
id: None,
pred: Predicate::MaxLen { len: value },
error: ErrorMeta {
code: "MAX_LENGTH_EXCEEDED".to_string(),
message: message
.unwrap_or_else(|| format!("Must be at most {} characters", value)),
path: None,
severity: ErrorSeverity::Error,
help: None,
source,
},
},
TemplateConstraint::MinLength {
value,
message,
source,
} => Constraint {
id: None,
pred: Predicate::MinLen { len: value },
error: ErrorMeta {
code: "MIN_LENGTH_NOT_MET".to_string(),
message: message
.unwrap_or_else(|| format!("Must be at least {} characters", value)),
path: None,
severity: ErrorSeverity::Error,
help: None,
source,
},
},
TemplateConstraint::Format {
value,
message,
source,
} => {
Constraint {
id: None,
pred: Predicate::Call {
name: format!("format:{}", value),
args: Value::Null,
},
error: ErrorMeta {
code: "FORMAT_INVALID".to_string(),
message: message.unwrap_or_else(|| format!("Must be in {} format", value)),
path: None,
severity: ErrorSeverity::Error,
help: None,
source,
},
}
}
TemplateConstraint::Enum {
values,
message,
source,
} => Constraint {
id: None,
pred: Predicate::In {
path: String::new(),
values: values.clone(),
},
error: ErrorMeta {
code: "INVALID_ENUM_VALUE".to_string(),
message: message.unwrap_or_else(|| format!("Must be one of: {:?}", values)),
path: None,
severity: ErrorSeverity::Error,
help: None,
source,
},
},
}
}
}
impl Constraint {
pub fn new(pred: Predicate, error: ErrorMeta) -> Self {
Self {
id: None,
pred,
error,
}
}
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum TemplateLiteralPart {
Literal { value: String },
Digits {
#[serde(default, skip_serializing_if = "Option::is_none")]
min: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
max: Option<usize>,
},
AsciiLetters {
#[serde(default, skip_serializing_if = "Option::is_none")]
min: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
max: Option<usize>,
},
AsciiAlphanumeric {
#[serde(default, skip_serializing_if = "Option::is_none")]
min: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
max: Option<usize>,
},
Uppercase {
#[serde(default, skip_serializing_if = "Option::is_none")]
min: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
max: Option<usize>,
},
Lowercase {
#[serde(default, skip_serializing_if = "Option::is_none")]
min: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
max: Option<usize>,
},
Hex {
#[serde(default, skip_serializing_if = "Option::is_none")]
min: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
max: Option<usize>,
},
}
impl TemplateLiteralPart {
pub fn literal(value: impl Into<String>) -> Self {
Self::Literal {
value: value.into(),
}
}
pub fn digits(min: Option<usize>, max: Option<usize>) -> Self {
Self::Digits { min, max }
}
pub fn ascii_letters(min: Option<usize>, max: Option<usize>) -> Self {
Self::AsciiLetters { min, max }
}
pub fn ascii_alphanumeric(min: Option<usize>, max: Option<usize>) -> Self {
Self::AsciiAlphanumeric { min, max }
}
pub fn uppercase(min: Option<usize>, max: Option<usize>) -> Self {
Self::Uppercase { min, max }
}
pub fn lowercase(min: Option<usize>, max: Option<usize>) -> Self {
Self::Lowercase { min, max }
}
pub fn hex(min: Option<usize>, max: Option<usize>) -> Self {
Self::Hex { min, max }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Predicate {
Regex {
pattern: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
flags: Option<String>,
},
TemplateLiteral {
parts: Vec<TemplateLiteralPart>,
},
MinLen {
len: usize,
},
MaxLen {
len: usize,
},
Range {
#[serde(default, skip_serializing_if = "Option::is_none")]
min: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
max: Option<f64>,
},
Exists {
path: String,
},
Eq {
path: String,
value: Value,
},
In {
path: String,
values: Vec<Value>,
},
RequiredWith {
field: String,
with: String,
},
RequiredWithout {
field: String,
without: String,
},
ExactlyOneOf {
paths: Vec<String>,
},
EqFields {
left: String,
right: String,
},
GtField {
left: String,
right: String,
},
GteField {
left: String,
right: String,
},
LtField {
left: String,
right: String,
},
LteField {
left: String,
right: String,
},
SumEquals {
paths: Vec<String>,
target: String,
},
SumEqualsValue {
paths: Vec<String>,
value: f64,
},
And {
predicates: Vec<Predicate>,
},
Or {
predicates: Vec<Predicate>,
},
Not {
predicate: Box<Predicate>,
},
Implies {
#[serde(rename = "if")]
antecedent: Box<Predicate>,
#[serde(rename = "then")]
consequent: Box<Predicate>,
},
Call {
name: String,
#[serde(default, skip_serializing_if = "Value::is_null")]
args: Value,
},
True,
False,
}
impl Predicate {
pub fn regex(pattern: impl Into<String>) -> Self {
Self::Regex {
pattern: pattern.into(),
flags: None,
}
}
pub fn regex_with_flags(pattern: impl Into<String>, flags: impl Into<String>) -> Self {
Self::Regex {
pattern: pattern.into(),
flags: Some(flags.into()),
}
}
pub fn template_literal(parts: Vec<TemplateLiteralPart>) -> Self {
Self::TemplateLiteral { parts }
}
pub fn min_len(len: usize) -> Self {
Self::MinLen { len }
}
pub fn max_len(len: usize) -> Self {
Self::MaxLen { len }
}
pub fn range(min: Option<f64>, max: Option<f64>) -> Self {
Self::Range { min, max }
}
pub fn min(min: f64) -> Self {
Self::Range {
min: Some(min),
max: None,
}
}
pub fn max(max: f64) -> Self {
Self::Range {
min: None,
max: Some(max),
}
}
pub fn exists(path: impl Into<String>) -> Self {
Self::Exists { path: path.into() }
}
pub fn eq(path: impl Into<String>, value: impl Into<Value>) -> Self {
Self::Eq {
path: path.into(),
value: value.into(),
}
}
pub fn in_values(path: impl Into<String>, values: Vec<Value>) -> Self {
Self::In {
path: path.into(),
values,
}
}
pub fn required_with(field: impl Into<String>, with: impl Into<String>) -> Self {
Self::RequiredWith {
field: field.into(),
with: with.into(),
}
}
pub fn required_without(field: impl Into<String>, without: impl Into<String>) -> Self {
Self::RequiredWithout {
field: field.into(),
without: without.into(),
}
}
pub fn exactly_one_of(paths: Vec<String>) -> Self {
Self::ExactlyOneOf { paths }
}
pub fn and(predicates: Vec<Predicate>) -> Self {
Self::And { predicates }
}
pub fn or(predicates: Vec<Predicate>) -> Self {
Self::Or { predicates }
}
#[allow(clippy::should_implement_trait)]
pub fn not(predicate: Predicate) -> Self {
Self::Not {
predicate: Box::new(predicate),
}
}
pub fn implies(antecedent: Predicate, consequent: Predicate) -> Self {
Self::Implies {
antecedent: Box::new(antecedent),
consequent: Box::new(consequent),
}
}
pub fn call(name: impl Into<String>, args: impl Into<Value>) -> Self {
Self::Call {
name: name.into(),
args: args.into(),
}
}
pub fn call_no_args(name: impl Into<String>) -> Self {
Self::Call {
name: name.into(),
args: Value::Null,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_serde_regex() {
let p = Predicate::regex(r"^\d{9}$");
let json = serde_json::to_string(&p).unwrap();
assert!(json.contains("regex"));
assert!(json.contains(r"\\d{9}"));
let parsed: Predicate = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, p);
}
#[test]
fn test_serde_template_literal() {
let p = Predicate::template_literal(vec![
TemplateLiteralPart::literal("SFO-"),
TemplateLiteralPart::digits(Some(3), Some(4)),
TemplateLiteralPart::literal("-"),
TemplateLiteralPart::uppercase(Some(2), Some(2)),
]);
let json = serde_json::to_string(&p).unwrap();
assert!(json.contains("template_literal"));
assert!(json.contains("digits"));
assert!(json.contains("uppercase"));
let parsed: Predicate = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, p);
}
#[test]
fn test_serde_range() {
let p = Predicate::range(Some(0.0), Some(100.0));
let json = serde_json::to_string(&p).unwrap();
let parsed: Predicate = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, p);
}
#[test]
fn test_serde_implies() {
let p = Predicate::implies(
Predicate::eq("/is_foreign", json!(false)),
Predicate::exists("/zip"),
);
let json = serde_json::to_string_pretty(&p).unwrap();
assert!(json.contains("implies"));
assert!(json.contains("if"));
assert!(json.contains("then"));
let parsed: Predicate = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, p);
}
#[test]
fn test_serde_call() {
let p = Predicate::call("is_tin", json!({"kind": "ANY"}));
let json = serde_json::to_string(&p).unwrap();
assert!(json.contains("call"));
assert!(json.contains("is_tin"));
let parsed: Predicate = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, p);
}
#[test]
fn test_serde_and() {
let p = Predicate::and(vec![Predicate::min_len(1), Predicate::max_len(100)]);
let json = serde_json::to_string(&p).unwrap();
let parsed: Predicate = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, p);
}
#[test]
fn test_serde_required_with() {
let p = Predicate::required_with("/confirm_password", "/password");
let json = serde_json::to_string(&p).unwrap();
assert!(json.contains("\"required_with\""));
let parsed: Predicate = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, p);
}
#[test]
fn test_serde_required_without() {
let p = Predicate::required_without("/tax_id", "/ssn");
let json = serde_json::to_string(&p).unwrap();
assert!(json.contains("\"required_without\""));
let parsed: Predicate = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, p);
}
#[test]
fn test_serde_exactly_one_of() {
let p = Predicate::exactly_one_of(vec!["/ssn".to_string(), "/ein".to_string()]);
let json = serde_json::to_string(&p).unwrap();
assert!(json.contains("\"exactly_one_of\""));
let parsed: Predicate = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, p);
}
#[test]
fn test_constraint_with_id() {
let c = Constraint::new(
Predicate::regex(r"^\d{9}$"),
ErrorMeta::new("TIN_INVALID", "TIN must be 9 digits"),
)
.with_id("tin-format-check");
assert_eq!(c.id, Some("tin-format-check".to_string()));
}
#[test]
fn test_template_enum_constraint_targets_current_value() {
let c: Constraint =
serde_json::from_str(r#"{"type":"enum","value":["A"],"message":"pick A"}"#).unwrap();
assert_eq!(
c.pred,
Predicate::In {
path: String::new(),
values: vec![json!("A")]
}
);
}
}