Skip to main content

protovalidate_buffa/
error_proto.rs

1use buffa_descriptor::generated::descriptor::field_descriptor_proto::Type;
2
3use crate::{FieldPath, FieldType, Subscript, ValidationError, Violation, proto};
4
5pub enum Exposure {
6    Diagnostic,
7    #[cfg(feature = "connect")]
8    Public,
9}
10
11impl ValidationError {
12    /// Copies all violations into the canonical `buf.validate.Violations` type.
13    ///
14    /// Requires `protos`. Preserves messages, map keys, indexes, schema paths,
15    /// and rule IDs without a size limit. Empty paths, rule IDs and messages are
16    /// absent; `for_key` is present only when true. Compilation and evaluation
17    /// diagnostics remain on `self`: the schema has no fields for them.
18    ///
19    /// This diagnostic representation can contain rejected data. For public RPC
20    /// request errors, use `into_connect_error` (requires `connect`), which
21    /// applies redaction, size limits, and status classification.
22    ///
23    /// ```
24    /// use protovalidate_buffa::{ValidationError, proto};
25    /// let error = ValidationError::default();
26    /// let details: proto::Violations = error.to_proto();
27    /// assert!(details.violations.is_empty());
28    /// ```
29    #[must_use]
30    pub fn to_proto(&self) -> proto::Violations {
31        proto::Violations {
32            violations: self
33                .violations
34                .iter()
35                .map(|v| convert_violation(v, &Exposure::Diagnostic))
36                .collect(),
37            ..Default::default()
38        }
39    }
40}
41
42pub fn convert_violation(v: &Violation, exposure: &Exposure) -> proto::Violation {
43    proto::Violation {
44        field: convert_path(&v.field, exposure).into(),
45        rule: convert_path(&v.rule, exposure).into(),
46        rule_id: nonempty(&v.rule_id),
47        message: match exposure {
48            Exposure::Diagnostic => nonempty(&v.message),
49            #[cfg(feature = "connect")]
50            Exposure::Public => None,
51        },
52        for_key: v.for_key.then_some(true),
53        ..Default::default()
54    }
55}
56
57fn nonempty(value: &str) -> Option<String> {
58    (!value.is_empty()).then(|| value.to_owned())
59}
60
61fn convert_path(path: &FieldPath, exposure: &Exposure) -> Option<proto::FieldPath> {
62    if path.elements.is_empty() {
63        return None;
64    }
65    Some(proto::FieldPath {
66        elements: path
67            .elements
68            .iter()
69            .map(|element| proto::FieldPathElement {
70                field_number: element.field_number,
71                field_name: element.field_name.as_deref().map(str::to_owned),
72                field_type: element.field_type.map(convert_type),
73                key_type: element.key_type.map(convert_type),
74                value_type: element.value_type.map(convert_type),
75                subscript: element
76                    .subscript
77                    .as_ref()
78                    .filter(|subscript| {
79                        let _ = subscript; // Also compiled without the connect feature.
80                        match exposure {
81                            Exposure::Diagnostic => true,
82                            // Indexes are structural; map keys contain input data.
83                            #[cfg(feature = "connect")]
84                            Exposure::Public => matches!(subscript, Subscript::Index(_)),
85                        }
86                    })
87                    .map(convert_subscript),
88                ..Default::default()
89            })
90            .collect(),
91        ..Default::default()
92    })
93}
94
95fn convert_subscript(
96    subscript: &Subscript,
97) -> proto::__buffa::oneof::field_path_element::Subscript {
98    use proto::__buffa::oneof::field_path_element::Subscript as S;
99    match subscript {
100        Subscript::Index(value) => S::Index(*value),
101        Subscript::BoolKey(value) => S::BoolKey(*value),
102        Subscript::IntKey(value) => S::IntKey(*value),
103        Subscript::UintKey(value) => S::UintKey(*value),
104        Subscript::StringKey(value) => S::StringKey(value.to_string()),
105    }
106}
107
108const fn convert_type(field_type: FieldType) -> Type {
109    match field_type {
110        FieldType::Double => Type::TYPE_DOUBLE,
111        FieldType::Float => Type::TYPE_FLOAT,
112        FieldType::Int64 => Type::TYPE_INT64,
113        FieldType::Uint64 => Type::TYPE_UINT64,
114        FieldType::Int32 => Type::TYPE_INT32,
115        FieldType::Fixed64 => Type::TYPE_FIXED64,
116        FieldType::Fixed32 => Type::TYPE_FIXED32,
117        FieldType::Bool => Type::TYPE_BOOL,
118        FieldType::String => Type::TYPE_STRING,
119        FieldType::Group => Type::TYPE_GROUP,
120        FieldType::Message => Type::TYPE_MESSAGE,
121        FieldType::Bytes => Type::TYPE_BYTES,
122        FieldType::Uint32 => Type::TYPE_UINT32,
123        FieldType::Enum => Type::TYPE_ENUM,
124        FieldType::Sfixed32 => Type::TYPE_SFIXED32,
125        FieldType::Sfixed64 => Type::TYPE_SFIXED64,
126        FieldType::Sint32 => Type::TYPE_SINT32,
127        FieldType::Sint64 => Type::TYPE_SINT64,
128    }
129}