google_cloud_gax/error/
binding.rs

1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15/// A failure to determine the request [URI].
16///
17/// Some RPCs correspond to multiple URIs. The contents of the request determine
18/// which URI is used. The client library considers all possible URIs, and only
19/// returns an error if no URIs work.
20///
21/// The client cannot match a URI when a required parameter is missing, or when
22/// it is set to an invalid format.
23///
24/// For more details on the specification, see: [AIP-127].
25///
26/// Also see the [Handling binding errors] section in the user guide to learn
27/// how to resolve these errors.
28///
29/// [Handling binding errors]: https://google-cloud-rust.github.io/binding_errors.html
30/// [aip-127]: https://google.aip.dev/127
31/// [uri]: https://clouddocs.f5.com/api/irules/HTTP__uri.html
32#[derive(thiserror::Error, Debug, PartialEq)]
33pub struct BindingError {
34    /// A list of all the paths considered, and why exactly the binding failed
35    /// for each
36    pub paths: Vec<PathMismatch>,
37}
38
39/// A failure to bind to a specific [URI].
40///
41/// The client cannot match a URI when a required parameter is missing, or when
42/// it is set to an invalid format.
43///
44/// [uri]: https://clouddocs.f5.com/api/irules/HTTP__uri.html
45#[derive(Debug, Default, PartialEq)]
46pub struct PathMismatch {
47    /// All missing or misformatted fields needed to bind to this path
48    pub subs: Vec<SubstitutionMismatch>,
49}
50
51/// Ways substituting a variable from a request into a [URI] can fail.
52///
53/// [uri]: https://clouddocs.f5.com/api/irules/HTTP__uri.html
54#[derive(Debug, PartialEq)]
55pub enum SubstitutionFail {
56    /// A required field was not set
57    Unset,
58    /// A required field of a certain format was not set
59    UnsetExpecting(&'static str),
60    /// A required field was set, but to an invalid format
61    ///
62    /// # Parameters
63    ///
64    /// - self.0 - the actual value of the field
65    /// - self.1 - the expected format of the field
66    MismatchExpecting(String, &'static str),
67}
68
69/// A failure to substitute a variable from a request into a [URI].
70///
71/// [uri]: https://clouddocs.f5.com/api/irules/HTTP__uri.html
72#[derive(Debug, PartialEq)]
73pub struct SubstitutionMismatch {
74    /// The name of the field that was not substituted.
75    ///
76    /// Nested fields are '.'-separated.
77    pub field_name: &'static str,
78    /// Why the substitution failed.
79    pub problem: SubstitutionFail,
80}
81
82impl std::fmt::Display for SubstitutionMismatch {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        match &self.problem {
85            SubstitutionFail::Unset => {
86                write!(f, "field `{}` needs to be set.", self.field_name)
87            }
88            SubstitutionFail::UnsetExpecting(expected) => {
89                write!(
90                    f,
91                    "field `{}` needs to be set and match the template: '{}'",
92                    self.field_name, expected
93                )
94            }
95            SubstitutionFail::MismatchExpecting(actual, expected) => {
96                write!(
97                    f,
98                    "field `{}` should match the template: '{}'; found: '{}'",
99                    self.field_name, expected, actual
100                )
101            }
102        }
103    }
104}
105
106impl std::fmt::Display for PathMismatch {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        for (i, sub) in self.subs.iter().enumerate() {
109            if i != 0 {
110                write!(f, " AND ")?;
111            }
112            write!(f, "{sub}")?;
113        }
114        Ok(())
115    }
116}
117
118impl std::fmt::Display for BindingError {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        if self.paths.len() == 1 {
121            return write!(f, "{}", self.paths[0]);
122        }
123        write!(f, "at least one of the conditions must be met: ")?;
124        for (i, sub) in self.paths.iter().enumerate() {
125            if i != 0 {
126                write!(f, " OR ")?;
127            }
128            write!(f, "({}) {}", i + 1, sub)?;
129        }
130        Ok(())
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn fmt_path_mismatch() {
140        let pm = PathMismatch {
141            subs: vec![
142                SubstitutionMismatch {
143                    field_name: "parent",
144                    problem: SubstitutionFail::MismatchExpecting(
145                        "project-id-only".to_string(),
146                        "projects/*",
147                    ),
148                },
149                SubstitutionMismatch {
150                    field_name: "location",
151                    problem: SubstitutionFail::UnsetExpecting("locations/*"),
152                },
153                SubstitutionMismatch {
154                    field_name: "id",
155                    problem: SubstitutionFail::Unset,
156                },
157            ],
158        };
159
160        let fmt = format!("{pm}");
161        let clauses: Vec<&str> = fmt.split(" AND ").collect();
162        assert!(clauses.len() == 3, "{fmt}");
163        let c0 = clauses[0];
164        assert!(
165            c0.contains("parent")
166                && !c0.contains("needs to be set")
167                && c0.contains("should match")
168                && c0.contains("projects/*")
169                && c0.contains("found")
170                && c0.contains("project-id-only"),
171            "{c0}"
172        );
173        let c1 = clauses[1];
174        assert!(
175            c1.contains("location")
176                && c1.contains("needs to be set")
177                && c1.contains("locations/*")
178                && !c1.contains("found"),
179            "{c1}"
180        );
181        let c2 = clauses[2];
182        assert!(
183            c2.contains("id") && c2.contains("needs to be set") && !c2.contains("found"),
184            "{c2}"
185        );
186    }
187
188    #[test]
189    fn fmt_binding_error() {
190        let e = BindingError {
191            paths: vec![
192                PathMismatch {
193                    subs: vec![SubstitutionMismatch {
194                        field_name: "parent",
195                        problem: SubstitutionFail::MismatchExpecting(
196                            "project-id-only".to_string(),
197                            "projects/*",
198                        ),
199                    }],
200                },
201                PathMismatch {
202                    subs: vec![SubstitutionMismatch {
203                        field_name: "location",
204                        problem: SubstitutionFail::UnsetExpecting("locations/*"),
205                    }],
206                },
207                PathMismatch {
208                    subs: vec![SubstitutionMismatch {
209                        field_name: "id",
210                        problem: SubstitutionFail::Unset,
211                    }],
212                },
213            ],
214        };
215        let fmt = format!("{e}");
216        assert!(fmt.contains("one of the conditions must be met"), "{fmt}");
217        let clauses: Vec<&str> = fmt.split(" OR ").collect();
218        assert!(clauses.len() == 3, "{fmt}");
219        let c0 = clauses[0];
220        assert!(
221            c0.contains("(1)")
222                && c0.contains("parent")
223                && c0.contains("should match")
224                && c0.contains("projects/*")
225                && c0.contains("project-id-only"),
226            "{c0}"
227        );
228        let c1 = clauses[1];
229        assert!(
230            c1.contains("(2)") && c1.contains("location") && c1.contains("locations/*"),
231            "{c1}"
232        );
233        let c2 = clauses[2];
234        assert!(
235            c2.contains("(3)") && c2.contains("id") && c2.contains("needs to be set"),
236            "{c2}"
237        );
238    }
239
240    #[test]
241    fn fmt_binding_error_one_path() {
242        let e = BindingError {
243            paths: vec![PathMismatch {
244                subs: vec![SubstitutionMismatch {
245                    field_name: "parent",
246                    problem: SubstitutionFail::MismatchExpecting(
247                        "project-id-only".to_string(),
248                        "projects/*",
249                    ),
250                }],
251            }],
252        };
253        let fmt = format!("{e}");
254        assert!(
255            !fmt.contains("one of the conditions must be met") && !fmt.contains(" OR "),
256            "{fmt}"
257        );
258        assert!(
259            fmt.contains("parent")
260                && fmt.contains("should match")
261                && fmt.contains("projects/*")
262                && fmt.contains("project-id-only"),
263            "{fmt}"
264        );
265    }
266}