1#![allow(clippy::question_mark)]
2use prost::Message;
3use prost_types::Any;
4use serde_json::{Map, Value};
5
6#[derive(Clone, PartialEq, Message)]
7struct GoogleRpcStatus {
8 #[prost(int32, tag = "1")]
9 code: i32,
10 #[prost(string, tag = "2")]
11 message: String,
12 #[prost(message, repeated, tag = "3")]
13 details: Vec<Any>,
14}
15
16pub struct ErrorHandler;
17
18impl ErrorHandler {
19 pub fn status_matches_expected(
20 status: &apif_grpc_transport::GrpcError,
21 expected: &Value,
22 ) -> bool {
23 Self::status_matches_expected_with_options(status, expected, false)
24 }
25 pub fn status_matches_expected_with_options(
26 status: &apif_grpc_transport::GrpcError,
27 expected: &Value,
28 partial: bool,
29 ) -> bool {
30 if !partial && !Self::status_has_no_unexpected_top_level_fields(status, expected) {
31 return false;
32 }
33 if partial {
34 return Self::is_subset_match(&Self::status_to_json(status), expected);
35 }
36 if !Self::message_matches_status(status, expected)
37 || !Self::code_matches_status(status, expected)
38 {
39 return false;
40 }
41 if !expected.get("details").is_some() {
42 return status.details().is_empty();
43 }
44 let Some(actual) = Self::decode_status_details(status.details()) else {
45 return false;
46 };
47 Self::compare_details(expected, actual).is_none()
48 }
49 pub fn status_details_json(status: &apif_grpc_transport::GrpcError) -> Value {
50 match Self::decode_status_details(status.details()) {
51 Some(d) => Value::Array(d),
52 None => Value::Null,
53 }
54 }
55 pub fn status_to_json(status: &apif_grpc_transport::GrpcError) -> Value {
56 let mut obj = Map::with_capacity(3);
57 obj.insert("code".into(), Value::from(status.code() as i64));
58 obj.insert("message".into(), Value::from(status.message()));
59 if let Some(details) = Self::decode_status_details(status.details())
60 && !details.is_empty()
61 {
62 obj.insert("details".into(), Value::Array(details));
63 }
64 Value::Object(obj)
65 }
66 pub fn status_mismatch_reason(
67 status: &apif_grpc_transport::GrpcError,
68 expected: &Value,
69 ) -> Option<String> {
70 Self::status_mismatch_reason_with_options(status, expected, false)
71 }
72 pub fn status_mismatch_reason_with_options(
73 status: &apif_grpc_transport::GrpcError,
74 expected: &Value,
75 partial: bool,
76 ) -> Option<String> {
77 if !partial
78 && let Some(r) = Self::status_unexpected_top_level_field_reason(status, expected)
79 {
80 return Some(r);
81 }
82 if partial {
83 let actual = Self::status_to_json(status);
84 return Self::partial_mismatch_reason(&actual, expected);
85 }
86 let mut reasons = Vec::new();
87 if !Self::code_matches_status(status, expected) {
88 reasons.push("code mismatch".to_string());
89 }
90 if !Self::message_matches_status(status, expected) {
91 reasons.push("message mismatch".to_string());
92 }
93 if reasons.is_empty()
94 && let Some(actual) = Self::decode_status_details(status.details())
95 && let Some(r) = Self::details_mismatch_reason(expected, actual)
96 {
97 reasons.push(r);
98 }
99 if reasons.is_empty() {
100 None
101 } else {
102 Some(reasons.join("; "))
103 }
104 }
105 fn status_has_no_unexpected_top_level_fields(
106 status: &apif_grpc_transport::GrpcError,
107 expected: &Value,
108 ) -> bool {
109 Self::status_unexpected_top_level_field_reason(status, expected).is_none()
110 }
111 fn status_unexpected_top_level_field_reason(
112 status: &apif_grpc_transport::GrpcError,
113 expected: &Value,
114 ) -> Option<String> {
115 let actual = Self::status_to_json(status);
116 let Some(actual_obj) = actual.as_object() else {
117 return None;
118 };
119 let Some(expected_obj) = expected.as_object() else {
120 return None;
121 };
122 for (k, _) in actual_obj {
123 if !expected_obj.contains_key(k) {
124 return Some(format!("Unexpected field '{}' in gRPC status JSON", k));
125 }
126 }
127 None
128 }
129 fn message_matches_status(status: &apif_grpc_transport::GrpcError, expected: &Value) -> bool {
130 expected
131 .get("message")
132 .and_then(|m| m.as_str())
133 .map(|exp_msg| status.message().contains(exp_msg))
134 .unwrap_or(true)
135 }
136 fn code_matches_status(status: &apif_grpc_transport::GrpcError, expected: &Value) -> bool {
137 expected
138 .get("code")
139 .and_then(|c| c.as_i64())
140 .map(|exp_code| status.code() as i64 == exp_code)
141 .unwrap_or(true)
142 }
143 pub fn grpc_code_name_from_numeric(code: i64) -> Option<&'static str> {
144 Some(match code {
145 0 => "OK",
146 1 => "CANCELLED",
147 2 => "UNKNOWN",
148 3 => "INVALID_ARGUMENT",
149 4 => "DEADLINE_EXCEEDED",
150 5 => "NOT_FOUND",
151 6 => "ALREADY_EXISTS",
152 7 => "PERMISSION_DENIED",
153 8 => "RESOURCE_EXHAUSTED",
154 9 => "FAILED_PRECONDITION",
155 10 => "ABORTED",
156 11 => "OUT_OF_RANGE",
157 12 => "UNIMPLEMENTED",
158 13 => "INTERNAL",
159 14 => "UNAVAILABLE",
160 15 => "DATA_LOSS",
161 16 => "UNAUTHENTICATED",
162 _ => return None,
163 })
164 }
165 pub fn error_matches_expected(error_text: &str, expected: &Value) -> bool {
166 if let Value::Object(obj) = expected
167 && let Some(code_val) = obj.get("code").and_then(|c| c.as_i64())
168 && let Some(msg_val) = obj.get("message").and_then(|m| m.as_str())
169 {
170 return error_text.contains(&format!("code={}", code_val))
171 && error_text.contains(msg_val);
172 }
173 false
174 }
175
176 fn decode_status_details(details: &[u8]) -> Option<Vec<Value>> {
177 if details.is_empty() {
178 return None;
179 }
180 let Ok(status) = GoogleRpcStatus::decode(details) else {
181 return None;
182 };
183 let mut items = Vec::new();
184 for any in status.details {
185 if let Ok(msg) = GoogleRpcErrorInfo::decode(any.value.as_slice()) {
186 let mut obj = Map::new();
187 obj.insert("reason".into(), Value::String(msg.reason));
188 obj.insert("domain".into(), Value::String(msg.domain));
189 let meta: Map<String, Value> = msg
190 .metadata
191 .into_iter()
192 .map(|(k, v)| (k, Value::String(v)))
193 .collect();
194 obj.insert("metadata".into(), Value::Object(meta));
195 items.push(Value::Object(obj));
196 }
197 }
198 if items.is_empty() { None } else { Some(items) }
199 }
200 fn compare_details(expected: &Value, actual: Vec<Value>) -> Option<String> {
201 let expected_details = expected.get("details").and_then(|d| d.as_array())?;
202 if expected_details.len() != actual.len() {
203 return Some(format!(
204 "details count mismatch: expected {} got {}",
205 expected_details.len(),
206 actual.len()
207 ));
208 }
209 None
210 }
211 fn details_mismatch_reason(expected: &Value, actual: Vec<Value>) -> Option<String> {
212 Self::compare_details(expected, actual)
213 }
214 fn is_subset_match(actual: &Value, expected: &Value) -> bool {
215 match (actual, expected) {
216 (Value::Object(a), Value::Object(e)) => e.iter().all(|(k, v)| {
217 a.get(k)
218 .map(|av| Self::is_subset_match(av, v))
219 .unwrap_or(false)
220 }),
221 (Value::Array(a), Value::Array(e)) => e
222 .iter()
223 .all(|ev| a.iter().any(|av| Self::is_subset_match(av, ev))),
224 _ => actual == expected,
225 }
226 }
227 fn partial_mismatch_reason(actual: &Value, expected: &Value) -> Option<String> {
228 if Self::is_subset_match(actual, expected) {
229 None
230 } else {
231 Some("partial match failed".to_string())
232 }
233 }
234}
235
236#[derive(Clone, PartialEq, Message)]
237struct GoogleRpcErrorInfo {
238 #[prost(string, tag = "1")]
239 reason: String,
240 #[prost(string, tag = "2")]
241 domain: String,
242 #[prost(map(string, string), tag = "3")]
243 metadata: std::collections::HashMap<String, String>,
244}