Skip to main content

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://googleapis.github.io/google-cloud-rust/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)]
55#[allow(clippy::exhaustive_enums)]
56pub enum SubstitutionFail {
57    /// A required field was not set
58    Unset,
59    /// A required field of a certain format was not set
60    UnsetExpecting(&'static str),
61    /// A required field was set, but to an invalid format
62    ///
63    /// # Parameters
64    ///
65    /// - self.0 - the actual value of the field
66    /// - self.1 - the expected format of the field
67    MismatchExpecting(String, &'static str),
68}
69
70/// A failure to substitute a variable from a request into a [URI].
71///
72/// [uri]: https://clouddocs.f5.com/api/irules/HTTP__uri.html
73#[derive(Debug, PartialEq)]
74pub struct SubstitutionMismatch {
75    /// The name of the field that was not substituted.
76    ///
77    /// Nested fields are '.'-separated.
78    pub field_name: &'static str,
79    /// Why the substitution failed.
80    pub problem: SubstitutionFail,
81}
82
83impl std::fmt::Display for SubstitutionMismatch {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        match &self.problem {
86            SubstitutionFail::Unset => {
87                write!(f, "field `{}` needs to be set.", self.field_name)
88            }
89            SubstitutionFail::UnsetExpecting(expected) => {
90                write!(
91                    f,
92                    "field `{}` needs to be set and match the template: '{}'",
93                    self.field_name, expected
94                )
95            }
96            SubstitutionFail::MismatchExpecting(actual, expected) => {
97                write!(
98                    f,
99                    "field `{}` should match the template: '{}'; found: '{}'",
100                    self.field_name, expected, actual
101                )
102            }
103        }
104    }
105}
106
107impl std::fmt::Display for PathMismatch {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        for (i, sub) in self.subs.iter().enumerate() {
110            if i != 0 {
111                write!(f, " AND ")?;
112            }
113            write!(f, "{sub}")?;
114        }
115        Ok(())
116    }
117}
118
119impl std::fmt::Display for BindingError {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        if self.paths.len() == 1 {
122            return write!(f, "{}", self.paths[0]);
123        }
124        write!(f, "at least one of the conditions must be met: ")?;
125        for (i, sub) in self.paths.iter().enumerate() {
126            if i != 0 {
127                write!(f, " OR ")?;
128            }
129            write!(f, "({}) {}", i + 1, sub)?;
130        }
131        Ok(())
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn fmt_path_mismatch() {
141        let pm = PathMismatch {
142            subs: vec![
143                SubstitutionMismatch {
144                    field_name: "parent",
145                    problem: SubstitutionFail::MismatchExpecting(
146                        "project-id-only".to_string(),
147                        "projects/*",
148                    ),
149                },
150                SubstitutionMismatch {
151                    field_name: "location",
152                    problem: SubstitutionFail::UnsetExpecting("locations/*"),
153                },
154                SubstitutionMismatch {
155                    field_name: "id",
156                    problem: SubstitutionFail::Unset,
157                },
158            ],
159        };
160
161        let fmt = format!("{pm}");
162        let clauses: Vec<&str> = fmt.split(" AND ").collect();
163        assert!(clauses.len() == 3, "{fmt}");
164        let c0 = clauses[0];
165        assert!(
166            c0.contains("parent")
167                && !c0.contains("needs to be set")
168                && c0.contains("should match")
169                && c0.contains("projects/*")
170                && c0.contains("found")
171                && c0.contains("project-id-only"),
172            "{c0}"
173        );
174        let c1 = clauses[1];
175        assert!(
176            c1.contains("location")
177                && c1.contains("needs to be set")
178                && c1.contains("locations/*")
179                && !c1.contains("found"),
180            "{c1}"
181        );
182        let c2 = clauses[2];
183        assert!(
184            c2.contains("id") && c2.contains("needs to be set") && !c2.contains("found"),
185            "{c2}"
186        );
187    }
188
189    #[test]
190    fn fmt_binding_error() {
191        let e = BindingError {
192            paths: vec![
193                PathMismatch {
194                    subs: vec![SubstitutionMismatch {
195                        field_name: "parent",
196                        problem: SubstitutionFail::MismatchExpecting(
197                            "project-id-only".to_string(),
198                            "projects/*",
199                        ),
200                    }],
201                },
202                PathMismatch {
203                    subs: vec![SubstitutionMismatch {
204                        field_name: "location",
205                        problem: SubstitutionFail::UnsetExpecting("locations/*"),
206                    }],
207                },
208                PathMismatch {
209                    subs: vec![SubstitutionMismatch {
210                        field_name: "id",
211                        problem: SubstitutionFail::Unset,
212                    }],
213                },
214            ],
215        };
216        let fmt = format!("{e}");
217        assert!(fmt.contains("one of the conditions must be met"), "{fmt}");
218        let clauses: Vec<&str> = fmt.split(" OR ").collect();
219        assert!(clauses.len() == 3, "{fmt}");
220        let c0 = clauses[0];
221        assert!(
222            c0.contains("(1)")
223                && c0.contains("parent")
224                && c0.contains("should match")
225                && c0.contains("projects/*")
226                && c0.contains("project-id-only"),
227            "{c0}"
228        );
229        let c1 = clauses[1];
230        assert!(
231            c1.contains("(2)") && c1.contains("location") && c1.contains("locations/*"),
232            "{c1}"
233        );
234        let c2 = clauses[2];
235        assert!(
236            c2.contains("(3)") && c2.contains("id") && c2.contains("needs to be set"),
237            "{c2}"
238        );
239    }
240
241    #[test]
242    fn fmt_binding_error_one_path() {
243        let e = BindingError {
244            paths: vec![PathMismatch {
245                subs: vec![SubstitutionMismatch {
246                    field_name: "parent",
247                    problem: SubstitutionFail::MismatchExpecting(
248                        "project-id-only".to_string(),
249                        "projects/*",
250                    ),
251                }],
252            }],
253        };
254        let fmt = format!("{e}");
255        assert!(
256            !fmt.contains("one of the conditions must be met") && !fmt.contains(" OR "),
257            "{fmt}"
258        );
259        assert!(
260            fmt.contains("parent")
261                && fmt.contains("should match")
262                && fmt.contains("projects/*")
263                && fmt.contains("project-id-only"),
264            "{fmt}"
265        );
266    }
267}