1#[cfg(feature = "use_cloud_rpc")]
16use google_cloud_rpc::model as cloud_rpc;
17use std::error::Error;
18use std::fmt::{Display, Write};
19use std::num::NonZeroI32;
20
21use crate::any_details::any::Details;
22use crate::builder;
23use crate::status_code;
24use crate::thin_arc_or_int::{IsizeInPtr, ThinArcOrInt};
25
26type Result<T, Error = ThinStatus> = std::result::Result<T, Error>;
27
28#[derive(Clone, Debug, PartialEq)]
31#[cfg_attr(not(feature = "use_any"), derive(Copy, Eq, PartialOrd, Ord, Hash))]
32pub(crate) struct FullStatus {
33 pub(crate) code: NonZeroI32,
34 pub(crate) details: Details,
35}
36
37impl TryFrom<FullStatus> for IsizeInPtr {
43 type Error = FullStatus;
44
45 fn try_from(value: FullStatus) -> Result<IsizeInPtr, FullStatus> {
46 if value.details.is_empty() {
47 value.code.get().try_into().map_err(|_| value)
48 } else {
49 Err(value)
50 }
51 }
52}
53
54#[derive(Clone, Debug, PartialEq)]
65#[cfg_attr(not(feature = "use_any"), derive(Eq, PartialOrd, Ord, Hash))]
66#[must_use = "this `ThinStatus` represents an error that should be handled"]
67pub struct ThinStatus {
68 thin: ThinArcOrInt<FullStatus, u8>,
71}
72
73impl ThinStatus {
74 pub fn builder<'a, C: Into<NonZeroI32>>(code: C) -> builder::ThinStatusBuilder<'a> {
76 builder::ThinStatusBuilder::new(code)
77 }
78
79 pub(crate) fn from_builder(builder: builder::ThinStatusBuilder) -> Self {
83 ThinStatus {
84 thin: ThinArcOrInt::from_convertible(builder.full, builder.message.as_bytes()),
85 }
86 }
87
88 pub fn from_code<C: Into<NonZeroI32>>(code: C) -> Self {
91 ThinStatus {
92 thin: ThinArcOrInt::from_convertible(
93 FullStatus {
94 code: code.into(),
95 details: Default::default(),
96 },
97 &[],
98 ),
99 }
100 }
101
102 pub fn code(&self) -> Option<status_code::ErrorCode> {
105 self.code_raw().get().try_into().ok()
106 }
107
108 pub fn code_or_unknown(&self) -> status_code::ErrorCode {
110 self.code().unwrap_or(status_code::ErrorCode::Unknown)
111 }
112
113 pub fn code_raw(&self) -> NonZeroI32 {
115 match self.thin.as_isize() {
116 Some(code) => unsafe { NonZeroI32::new_unchecked(code as i32) },
117 None => {
118 let val = &self.thin.as_arc();
119 let val = val.expect("ThinArcOrInt contains neither code nor arc");
120 val.header.header.code
121 }
122 }
123 }
124
125 pub fn message(&self) -> &str {
127 match self.thin.as_arc() {
128 None => "",
129 Some(arc) => unsafe { str::from_utf8_unchecked(&arc.slice) },
130 }
131 }
132
133 #[cfg(feature = "use_any")]
135 pub fn details(&self) -> &[google_cloud_wkt::Any] {
136 let &arc = &self.thin.as_arc();
137 arc.map_or::<&[google_cloud_wkt::Any], _>(&[], |t| &t.header.header.details.get())
138 }
139
140 #[cfg(feature = "use_cloud_rpc")]
142 pub fn to_cloud_rpc(&self) -> cloud_rpc::Status {
143 self.into()
144 }
145
146 #[cfg(feature = "use_cloud_rpc")]
148 pub fn try_from_cloud_rpc(status: cloud_rpc::Status) -> Result<Self, ()> {
149 status.try_into()
150 }
151
152 #[cfg(feature = "use_cloud_rpc")]
155 pub fn try_from_cloud_rpc_ref(status: &cloud_rpc::Status) -> Result<Self, ()> {
156 status.try_into()
157 }
158}
159
160impl Display for ThinStatus {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 let code = self.code_raw().get();
167 if let Some(error_code) = status_code::ErrorCode::try_from(code).ok() {
168 error_code.fmt(f)?;
169 } else {
170 code.fmt(f)?;
171 }
172 let msg = self.message();
173 if !msg.is_empty() {
174 write!(f, ": {}", msg)?;
175 }
176 Ok(())
177 }
178}
179
180impl Error for ThinStatus {}
181
182impl From<status_code::ErrorCode> for ThinStatus {
183 fn from(code: status_code::ErrorCode) -> Self {
184 Self::from_code(NonZeroI32::from(code))
185 }
186}
187
188impl From<std::io::Error> for ThinStatus {
189 fn from(err: std::io::Error) -> Self {
190 Self::from(&err)
191 }
192}
193
194impl From<&std::io::Error> for ThinStatus {
195 fn from(err: &std::io::Error) -> Self {
196 let mut builder = Self::builder(status_code::ErrorCode::from_error_kind(err.kind()));
197 let _ = write!(builder, "{}", err);
198 builder.build()
199 }
200}
201
202#[cfg(feature = "use_cloud_rpc")]
204impl TryFrom<cloud_rpc::Status> for ThinStatus {
205 type Error = ();
206
207 fn try_from(status: cloud_rpc::Status) -> Result<Self, ()> {
208 let code: NonZeroI32 = status.code.try_into().map_err(|_| ())?;
209 Ok(Self::builder(code)
210 .message(&status.message)
211 .details(status.details)
212 .build())
213 }
214}
215
216#[cfg(feature = "use_cloud_rpc")]
219impl TryFrom<&cloud_rpc::Status> for ThinStatus {
220 type Error = ();
221
222 fn try_from(status: &cloud_rpc::Status) -> Result<Self, ()> {
223 let code: NonZeroI32 = status.code.try_into().map_err(|_| ())?;
224 Ok(Self::builder(code)
225 .message(&status.message)
226 .details(status.details.clone())
227 .build())
228 }
229}
230
231#[cfg(feature = "use_cloud_rpc")]
233impl From<&ThinStatus> for cloud_rpc::Status {
234 fn from(status: &ThinStatus) -> Self {
235 let mut result = Self::new();
236 result.code = status.code_raw().get();
237 result.message = status.message().to_string();
238 result.details = status.details().to_vec();
239 result
240 }
241}
242
243#[cfg(test)]
244mod thin_status_tests {
245 use super::*;
246 use std::fmt::Write;
247
248 const MAX_THIN: NonZeroI32 = NonZeroI32::new(if IsizeInPtr::MAX as u64 <= i32::MAX as u64 {
251 IsizeInPtr::MAX as i32
252 } else {
253 i32::MAX
254 })
255 .unwrap();
256
257 const fn non_zero(value: i32) -> NonZeroI32 {
258 NonZeroI32::new(value).unwrap()
259 }
260
261 #[test]
262 fn test_size_optimization() {
263 assert_eq!(
265 std::mem::size_of::<ThinStatus>(),
266 std::mem::size_of::<usize>()
267 );
268 }
269
270 #[test]
271 fn test_from_error_code_raw() {
272 let status: ThinStatus = status_code::ErrorCode::NotFound.into();
274 assert_eq!(i32::from(status.code_raw()), 5);
275 assert_eq!(status.message(), "");
276 #[cfg(feature = "use_any")]
277 assert_eq!(status.details(), &[]);
278 assert!(status.thin.has_number());
279 assert_eq!(format!("{}", status), "NOT_FOUND");
280 }
281
282 #[test]
283 fn test_from_code_within_max_thin() {
284 let status_pos = ThinStatus::from_code(MAX_THIN);
286 assert_eq!(status_pos.code_raw(), MAX_THIN);
287 assert_eq!(status_pos.message(), "");
288 assert!(status_pos.thin.has_number());
289
290 let status_neg = ThinStatus::from_code(-MAX_THIN);
291 assert_eq!(status_neg.code_raw(), -MAX_THIN);
292 assert_eq!(
293 status_neg.code_or_unknown(),
294 status_code::ErrorCode::Unknown
295 );
296 assert_eq!(status_neg.message(), "");
297 assert!(status_neg.thin.has_number());
298
299 let status_normal = ThinStatus::from_code(non_zero(42));
300 assert_eq!(status_normal.code_raw(), non_zero(42));
301 assert_eq!(status_normal.message(), "");
302 assert!(status_normal.thin.has_number());
303 }
304
305 #[test]
307 fn test_from_code_outside_max_thin() {
308 if MAX_THIN.get() < NonZeroI32::MAX.into() {
311 let larger: NonZeroI32 = non_zero(MAX_THIN.get() + 1);
312 let status_overflow = ThinStatus::from_code(larger);
313 assert_eq!(status_overflow.code_raw(), larger);
314 assert_eq!(status_overflow.message(), "");
315
316 let status_underflow = ThinStatus::from_code(-larger);
317 assert_eq!(status_underflow.code_raw(), -larger);
318 assert_eq!(status_underflow.message(), "");
319 assert!(status_overflow.thin.has_ref());
320 assert!(status_underflow.thin.has_ref());
321 }
322
323 let status_max = ThinStatus::from_code(NonZeroI32::MAX);
325 assert_eq!(status_max.code_raw(), NonZeroI32::MAX);
326 let status_min = ThinStatus::from_code(NonZeroI32::MIN);
327 assert_eq!(status_min.code_raw(), NonZeroI32::MIN);
328 if MAX_THIN.get() < NonZeroI32::MAX.into() {
329 assert!(status_max.thin.has_ref());
330 assert!(status_min.thin.has_ref());
331 }
332 }
333
334 #[test]
335 fn test_from_error_code_and_message() {
336 let mut builder = ThinStatus::builder(status_code::ErrorCode::NotFound);
337 write!(builder, "message").expect("write! failed");
338 let status = builder.build();
339 assert_eq!(i32::from(status.code_raw()), 5);
340 assert_eq!(status.code(), Some(status_code::ErrorCode::NotFound));
341 assert_eq!(status.code_or_unknown(), status_code::ErrorCode::NotFound);
342 assert_eq!(status.message(), "message");
343 #[cfg(feature = "use_any")]
344 assert_eq!(status.details(), &[]);
345 assert!(status.thin.has_ref());
346 assert_eq!(format!("{}", status), "NOT_FOUND: message");
347 }
348
349 #[test]
350 fn test_from_io_error() {
351 let err = std::io::Error::new(std::io::ErrorKind::NotFound, "meaning of life");
352 let status = ThinStatus::from(err);
353 assert_eq!(status.code(), Some(status_code::ErrorCode::NotFound));
354 assert_eq!(status.message(), "meaning of life");
355 }
356
357 #[cfg(feature = "use_any")]
358 #[test]
359 fn test_with_details() {
360 let detail =
361 google_cloud_wkt::Any::from_msg(&google_cloud_wkt::Duration::clamp(123, 456)).unwrap();
362 let status = ThinStatus::builder(status_code::ErrorCode::NotFound)
363 .add_detail(detail.clone())
364 .build();
365 assert_eq!(i32::from(status.code_raw()), 5);
366 assert_eq!(status.message(), "");
367 assert_eq!(status.details(), vec![detail]);
368 assert!(status.thin.has_ref());
369 let formatted = format!("{:#?}", status);
370 assert!(formatted.contains("Any"));
371 assert!(formatted.contains("type.googleapis.com/google.protobuf.Duration"));
372 assert!(formatted.contains("123"));
373 }
374
375 #[test]
376 fn test_clone_and_equality() {
377 let status1 = ThinStatus::builder(non_zero(13))
378 .message("Permission Denied")
379 .build();
380 let status2 = status1.clone();
381
382 assert_eq!(status1, status2);
383 assert_eq!(status1.code_raw(), status2.code_raw());
384 assert_eq!(status1.message(), status2.message());
385
386 let _status_different = ThinStatus::from_code(non_zero(13));
387 assert_ne!(status1, _status_different);
388 }
389
390 #[cfg(feature = "use_cloud_rpc")]
391 #[test]
392 fn test_cloud_rpc_status_conversions() {
393 assert!(
394 ThinStatus::try_from(cloud_rpc::Status::new()).is_err(),
395 "OK status shouldn't be convertible to ThinStatus"
396 );
397
398 let detail =
399 google_cloud_wkt::Any::from_msg(&google_cloud_wkt::Duration::clamp(123, 456)).unwrap();
400 let status = ThinStatus::builder(status_code::ErrorCode::NotFound)
401 .message("Be yourself! Everyone else is already taken.")
402 .add_detail(detail)
403 .build();
404 assert_eq!(
405 ThinStatus::try_from(cloud_rpc::Status::from(&status)).as_ref(),
406 Ok(&status)
407 );
408
409 assert_eq!(
410 ThinStatus::try_from(&cloud_rpc::Status::from(&status)),
411 Ok(status)
412 );
413 }
414}
415
416trait ThinStatusExtSealed {}
417
418#[allow(private_bounds)]
419pub trait ThinStatusExt: ThinStatusExtSealed {
420 fn error_code_builder(&self, code: status_code::ErrorCode) -> builder::ThinStatusBuilder<'_>;
421
422 fn error_code(&self, code: status_code::ErrorCode) -> ThinStatus {
423 self.error_code_builder(code).build()
424 }
425}
426
427impl<M: Display> ThinStatusExtSealed for M {}
428
429impl<M: Display> ThinStatusExt for M {
430 fn error_code_builder(&self, code: status_code::ErrorCode) -> builder::ThinStatusBuilder<'_> {
431 ThinStatus::builder(code).message(self.to_string())
432 }
433
434 fn error_code(&self, code: status_code::ErrorCode) -> ThinStatus {
435 self.error_code_builder(code).build()
436 }
437}
438
439#[cfg(test)]
440mod thin_status_ext_tests {
441 use super::*;
442
443 #[test]
444 fn test_from_error_code_and_message() {
445 let status = "message".error_code(status_code::ErrorCode::NotFound);
446 assert_eq!(i32::from(status.code_raw()), 5);
447 assert_eq!(status.code(), Some(status_code::ErrorCode::NotFound));
448 assert_eq!(status.message(), "message");
449 }
450}