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        write!(f, "at least one of the conditions must be met: ")?;
121        for (i, sub) in self.paths.iter().enumerate() {
122            if i != 0 {
123                write!(f, " OR ")?;
124            }
125            write!(f, "({}) {}", i + 1, sub)?;
126        }
127        Ok(())
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn fmt_path_mismatch() {
137        let pm = PathMismatch {
138            subs: vec![
139                SubstitutionMismatch {
140                    field_name: "parent",
141                    problem: SubstitutionFail::MismatchExpecting(
142                        "project-id-only".to_string(),
143                        "projects/*",
144                    ),
145                },
146                SubstitutionMismatch {
147                    field_name: "location",
148                    problem: SubstitutionFail::UnsetExpecting("locations/*"),
149                },
150                SubstitutionMismatch {
151                    field_name: "id",
152                    problem: SubstitutionFail::Unset,
153                },
154            ],
155        };
156
157        let fmt = format!("{pm}");
158        let clauses: Vec<&str> = fmt.split(" AND ").collect();
159        assert!(clauses.len() == 3, "{fmt}");
160        let c0 = clauses[0];
161        assert!(
162            c0.contains("parent")
163                && !c0.contains("needs to be set")
164                && c0.contains("should match")
165                && c0.contains("projects/*")
166                && c0.contains("found")
167                && c0.contains("project-id-only"),
168            "{c0}"
169        );
170        let c1 = clauses[1];
171        assert!(
172            c1.contains("location")
173                && c1.contains("needs to be set")
174                && c1.contains("locations/*")
175                && !c1.contains("found"),
176            "{c1}"
177        );
178        let c2 = clauses[2];
179        assert!(
180            c2.contains("id") && c2.contains("needs to be set") && !c2.contains("found"),
181            "{c2}"
182        );
183    }
184
185    #[test]
186    fn fmt_binding_error() {
187        let e = BindingError {
188            paths: vec![
189                PathMismatch {
190                    subs: vec![SubstitutionMismatch {
191                        field_name: "parent",
192                        problem: SubstitutionFail::MismatchExpecting(
193                            "project-id-only".to_string(),
194                            "projects/*",
195                        ),
196                    }],
197                },
198                PathMismatch {
199                    subs: vec![SubstitutionMismatch {
200                        field_name: "location",
201                        problem: SubstitutionFail::UnsetExpecting("locations/*"),
202                    }],
203                },
204                PathMismatch {
205                    subs: vec![SubstitutionMismatch {
206                        field_name: "id",
207                        problem: SubstitutionFail::Unset,
208                    }],
209                },
210            ],
211        };
212        let fmt = format!("{e}");
213        assert!(fmt.contains("one of the conditions must be met"), "{fmt}");
214        let clauses: Vec<&str> = fmt.split(" OR ").collect();
215        assert!(clauses.len() == 3, "{fmt}");
216        let c0 = clauses[0];
217        assert!(
218            c0.contains("(1)")
219                && c0.contains("parent")
220                && c0.contains("should match")
221                && c0.contains("projects/*")
222                && c0.contains("project-id-only"),
223            "{c0}"
224        );
225        let c1 = clauses[1];
226        assert!(
227            c1.contains("(2)") && c1.contains("location") && c1.contains("locations/*"),
228            "{c1}"
229        );
230        let c2 = clauses[2];
231        assert!(
232            c2.contains("(3)") && c2.contains("id") && c2.contains("needs to be set"),
233            "{c2}"
234        );
235    }
236}