#[derive(thiserror::Error, Debug, PartialEq)]
pub struct BindingError {
pub paths: Vec<PathMismatch>,
}
#[derive(Debug, Default, PartialEq)]
pub struct PathMismatch {
pub subs: Vec<SubstitutionMismatch>,
}
#[derive(Debug, PartialEq)]
pub enum SubstitutionFail {
Unset,
UnsetExpecting(&'static str),
MismatchExpecting(String, &'static str),
}
#[derive(Debug, PartialEq)]
pub struct SubstitutionMismatch {
pub field_name: &'static str,
pub problem: SubstitutionFail,
}
impl std::fmt::Display for SubstitutionMismatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.problem {
SubstitutionFail::Unset => {
write!(f, "field `{}` needs to be set.", self.field_name)
}
SubstitutionFail::UnsetExpecting(expected) => {
write!(
f,
"field `{}` needs to be set and match the template: '{}'",
self.field_name, expected
)
}
SubstitutionFail::MismatchExpecting(actual, expected) => {
write!(
f,
"field `{}` should match the template: '{}'; found: '{}'",
self.field_name, expected, actual
)
}
}
}
}
impl std::fmt::Display for PathMismatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (i, sub) in self.subs.iter().enumerate() {
if i != 0 {
write!(f, " AND ")?;
}
write!(f, "{sub}")?;
}
Ok(())
}
}
impl std::fmt::Display for BindingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.paths.len() == 1 {
return write!(f, "{}", self.paths[0]);
}
write!(f, "at least one of the conditions must be met: ")?;
for (i, sub) in self.paths.iter().enumerate() {
if i != 0 {
write!(f, " OR ")?;
}
write!(f, "({}) {}", i + 1, sub)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fmt_path_mismatch() {
let pm = PathMismatch {
subs: vec![
SubstitutionMismatch {
field_name: "parent",
problem: SubstitutionFail::MismatchExpecting(
"project-id-only".to_string(),
"projects/*",
),
},
SubstitutionMismatch {
field_name: "location",
problem: SubstitutionFail::UnsetExpecting("locations/*"),
},
SubstitutionMismatch {
field_name: "id",
problem: SubstitutionFail::Unset,
},
],
};
let fmt = format!("{pm}");
let clauses: Vec<&str> = fmt.split(" AND ").collect();
assert!(clauses.len() == 3, "{fmt}");
let c0 = clauses[0];
assert!(
c0.contains("parent")
&& !c0.contains("needs to be set")
&& c0.contains("should match")
&& c0.contains("projects/*")
&& c0.contains("found")
&& c0.contains("project-id-only"),
"{c0}"
);
let c1 = clauses[1];
assert!(
c1.contains("location")
&& c1.contains("needs to be set")
&& c1.contains("locations/*")
&& !c1.contains("found"),
"{c1}"
);
let c2 = clauses[2];
assert!(
c2.contains("id") && c2.contains("needs to be set") && !c2.contains("found"),
"{c2}"
);
}
#[test]
fn fmt_binding_error() {
let e = BindingError {
paths: vec![
PathMismatch {
subs: vec![SubstitutionMismatch {
field_name: "parent",
problem: SubstitutionFail::MismatchExpecting(
"project-id-only".to_string(),
"projects/*",
),
}],
},
PathMismatch {
subs: vec![SubstitutionMismatch {
field_name: "location",
problem: SubstitutionFail::UnsetExpecting("locations/*"),
}],
},
PathMismatch {
subs: vec![SubstitutionMismatch {
field_name: "id",
problem: SubstitutionFail::Unset,
}],
},
],
};
let fmt = format!("{e}");
assert!(fmt.contains("one of the conditions must be met"), "{fmt}");
let clauses: Vec<&str> = fmt.split(" OR ").collect();
assert!(clauses.len() == 3, "{fmt}");
let c0 = clauses[0];
assert!(
c0.contains("(1)")
&& c0.contains("parent")
&& c0.contains("should match")
&& c0.contains("projects/*")
&& c0.contains("project-id-only"),
"{c0}"
);
let c1 = clauses[1];
assert!(
c1.contains("(2)") && c1.contains("location") && c1.contains("locations/*"),
"{c1}"
);
let c2 = clauses[2];
assert!(
c2.contains("(3)") && c2.contains("id") && c2.contains("needs to be set"),
"{c2}"
);
}
#[test]
fn fmt_binding_error_one_path() {
let e = BindingError {
paths: vec![PathMismatch {
subs: vec![SubstitutionMismatch {
field_name: "parent",
problem: SubstitutionFail::MismatchExpecting(
"project-id-only".to_string(),
"projects/*",
),
}],
}],
};
let fmt = format!("{e}");
assert!(
!fmt.contains("one of the conditions must be met") && !fmt.contains(" OR "),
"{fmt}"
);
assert!(
fmt.contains("parent")
&& fmt.contains("should match")
&& fmt.contains("projects/*")
&& fmt.contains("project-id-only"),
"{fmt}"
);
}
}