Repository navigation
Add information about violations to error message #554
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,12 @@ | |
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| use std::fmt; | ||
| use std::fmt::{self, Write as _}; | ||
|
|
||
| use buffa::Message as _; | ||
|
|
||
| use crate::validate::__buffa::oneof::field_path_element::Subscript; | ||
| use crate::validate::{FieldPath, Violation as ViolationPb, Violations}; | ||
|
|
||
| /// A descriptor that could not be registered. | ||
| #[derive(Debug)] | ||
|
|
@@ -80,21 +85,25 @@ impl<E: std::error::Error + 'static> std::error::Error for Error<E> { | |
| } | ||
| } | ||
|
|
||
| /// One or more rule violations, carried by [`Error::Validation`] as a | ||
| /// serialized `buf.validate.Violations`. | ||
| /// One or more rule violations, carried by [`Error::Validation`]. | ||
| pub struct ValidationError { | ||
| violations: Vec<u8>, | ||
| /// Never empty. | ||
| violations: Vec<ViolationPb>, | ||
| } | ||
|
|
||
| impl ValidationError { | ||
| pub(crate) fn new(violations: Vec<u8>) -> Self { | ||
| pub(crate) fn new(violations: Vec<ViolationPb>) -> Self { | ||
| Self { violations } | ||
| } | ||
|
|
||
| /// The violations, as a serialized `buf.validate.Violations`. | ||
| /// Encodes the violations as a `buf.validate.Violations`. | ||
| #[must_use] | ||
| pub fn violations(&self) -> &[u8] { | ||
| &self.violations | ||
| pub fn encode_violations(&self) -> Vec<u8> { | ||
| Violations { | ||
| violations: self.violations.clone(), | ||
| ..Default::default() | ||
| } | ||
| .encode_to_vec() | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -108,8 +117,151 @@ impl fmt::Debug for ValidationError { | |
| } | ||
| } | ||
|
|
||
| /// The first violation, followed by the number of others. | ||
| /// For example, `user.email: must be a valid email address, and 2 more violations`. | ||
| impl fmt::Display for ValidationError { | ||
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| f.write_str("validation failed") | ||
| write_violation(f, &self.violations[0])?; | ||
| match self.violations.len() - 1 { | ||
| 0 => Ok(()), | ||
| 1 => f.write_str(", and 1 more violation"), | ||
| more => write!(f, ", and {more} more violations"), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Writes `violation` as `<field path>: <message>`, or `[<rule id>]` in place | ||
| /// of an empty message, omitting an empty field path. | ||
| fn write_violation(f: &mut fmt::Formatter<'_>, violation: &ViolationPb) -> fmt::Result { | ||
| if !violation.field.elements.is_empty() { | ||
| write_field_path(f, &violation.field)?; | ||
| f.write_str(": ")?; | ||
| } | ||
| match ( | ||
| violation.message.as_deref().unwrap_or_default(), | ||
| violation.rule_id.as_deref().unwrap_or_default(), | ||
| ) { | ||
| ("", "") => f.write_str("[unknown]"), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if both are present, why not display both?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was to apply the change made to Go in bufbuild/protovalidate-go#291 (comment) here. I don't feel too strongly either way but it was suggested in bufbuild/protovalidate#536, lean towards going with it |
||
| ("", rule_id) => write!(f, "[{rule_id}]"), | ||
| (message, _) => f.write_str(message), | ||
| } | ||
| } | ||
|
|
||
| fn write_field_path(f: &mut fmt::Formatter<'_>, path: &FieldPath) -> fmt::Result { | ||
| for (i, element) in path.elements.iter().enumerate() { | ||
| let name = element.field_name.as_deref().unwrap_or_default(); | ||
| // Extension names are already bracketed, as in `[pkg.ext]`. | ||
| if i > 0 && !name.starts_with('[') { | ||
| f.write_str(".")?; | ||
| } | ||
| f.write_str(name)?; | ||
| match &element.subscript { | ||
| None => {} | ||
| Some(Subscript::Index(index) | Subscript::UintKey(index)) => write!(f, "[{index}]")?, | ||
| Some(Subscript::IntKey(key)) => write!(f, "[{key}]")?, | ||
| Some(Subscript::BoolKey(key)) => write!(f, "[{key}]")?, | ||
| Some(Subscript::StringKey(key)) => { | ||
| f.write_str("[\"")?; | ||
| for c in key.chars() { | ||
| match c { | ||
| '\\' => f.write_str("\\\\")?, | ||
| '"' => f.write_str("\\\"")?, | ||
| '\r' => f.write_str("\\r")?, | ||
| '\n' => f.write_str("\\n")?, | ||
| c => f.write_char(c)?, | ||
| } | ||
| } | ||
| f.write_str("\"]")?; | ||
| } | ||
| } | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use buffa::MessageField; | ||
|
|
||
| use super::{FieldPath, Subscript, ValidationError, ViolationPb}; | ||
| use crate::validate::FieldPathElement; | ||
|
|
||
| fn element(name: &str, subscript: Option<Subscript>) -> FieldPathElement { | ||
| FieldPathElement { | ||
| field_name: Some(name.to_owned()), | ||
| subscript, | ||
| ..Default::default() | ||
| } | ||
| } | ||
|
|
||
| fn violation(field: Vec<FieldPathElement>, message: &str, rule_id: &str) -> ViolationPb { | ||
| ViolationPb { | ||
| field: if field.is_empty() { | ||
| MessageField::none() | ||
| } else { | ||
| MessageField::some(FieldPath { | ||
| elements: field, | ||
| ..Default::default() | ||
| }) | ||
| }, | ||
| message: Some(message.to_owned()), | ||
| rule_id: Some(rule_id.to_owned()), | ||
| ..Default::default() | ||
| } | ||
| } | ||
|
|
||
| fn display(violations: Vec<ViolationPb>) -> String { | ||
| ValidationError::new(violations).to_string() | ||
| } | ||
|
|
||
| #[test] | ||
| fn displays_field_path() { | ||
| let field = vec![ | ||
| element("a", None), | ||
| element("b", Some(Subscript::Index(0))), | ||
| element("[pkg.ext]", None), | ||
| element("c", Some(Subscript::BoolKey(true))), | ||
| element("d", Some(Subscript::IntKey(-1))), | ||
| element("e", Some(Subscript::UintKey(2))), | ||
| element("f", Some(Subscript::StringKey("x\\\"\r\n".to_owned()))), | ||
| ]; | ||
| assert_eq!( | ||
| display(vec![violation(field, "must be set", "required")]), | ||
| r#"a.b[0][pkg.ext].c[true].d[-1].e[2].f["x\\\"\r\n"]: must be set"#, | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn falls_back_to_rule_id() { | ||
| assert_eq!( | ||
| display(vec![violation(vec![], "must be set", "required")]), | ||
| "must be set", | ||
| ); | ||
| assert_eq!( | ||
| display(vec![violation(vec![element("a", None)], "", "custom")]), | ||
| "a: [custom]", | ||
| ); | ||
| assert_eq!(display(vec![violation(vec![], "", "custom")]), "[custom]"); | ||
| assert_eq!( | ||
| display(vec![violation(vec![element("a", None)], "", "")]), | ||
| "a: [unknown]", | ||
| ); | ||
| assert_eq!(display(vec![violation(vec![], "", "")]), "[unknown]"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn counts_other_violations() { | ||
| let first = || violation(vec![element("a", None)], "bad", "rule"); | ||
| assert_eq!( | ||
| display(vec![first(), violation(vec![], "", "other")]), | ||
| "a: bad, and 1 more violation", | ||
| ); | ||
| assert_eq!( | ||
| display(vec![ | ||
| first(), | ||
| violation(vec![], "", "other"), | ||
| violation(vec![], "", "other"), | ||
| ]), | ||
| "a: bad, and 2 more violations", | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe support a
#flag to display all violation messages.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great idea for rust! Python doesn't have anything like that (just the thrown exception), so let's keep this as-is for now and possibly add it in the future if supporting rust itself to prevent dead code here