1use http::StatusCode;
15
16use crate::{MaybeSend, MaybeSync};
17use serde::{Deserialize, Serialize};
18use std::fmt::{Debug, Display};
19use thiserror::Error;
20
21#[cfg(feature = "utoipa")]
22use utoipa::ToSchema;
23
24pub trait CqrsErrorCode: Debug + Display + Clone + MaybeSend + MaybeSync + 'static {
44 fn domain() -> &'static str;
46
47 fn domain_prefix() -> u16;
50
51 fn error_index(&self) -> u16;
53
54 fn http_status(&self) -> StatusCode;
56
57 fn internal_code(&self) -> u16 {
60 Self::domain_prefix() * 1000 + self.error_index()
61 }
62
63 fn code_string(&self) -> String {
66 format!("{}_{}", Self::domain().to_uppercase(), self)
67 }
68
69 fn error(&self, message: impl Into<String>) -> CqrsError
71 where
72 Self: Sized,
73 {
74 CqrsError::from_code(self, message)
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
84#[cfg_attr(feature = "utoipa", derive(ToSchema))]
85#[serde(rename_all = "camelCase")]
86pub struct CqrsErrorData {
87 pub domain: String,
89
90 pub code: String,
92
93 pub internal_code: u16,
95
96 #[serde(skip)]
98 pub status: u16,
99
100 pub message: String,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub details: Option<serde_json::Value>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub request_id: Option<String>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
114 pub type_uri: Option<String>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(transparent)]
136pub struct CqrsError(Box<CqrsErrorData>);
137
138impl std::ops::Deref for CqrsError {
139 type Target = CqrsErrorData;
140 fn deref(&self) -> &CqrsErrorData {
141 &self.0
142 }
143}
144
145impl std::ops::DerefMut for CqrsError {
146 fn deref_mut(&mut self) -> &mut CqrsErrorData {
147 &mut self.0
148 }
149}
150
151#[cfg(all(feature = "utoipa", not(feature = "problem-json")))]
155impl utoipa::PartialSchema for CqrsError {
156 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
157 CqrsErrorData::schema()
158 }
159}
160
161#[cfg(all(feature = "utoipa", feature = "problem-json"))]
162impl utoipa::PartialSchema for CqrsError {
163 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
164 crate::problem::ProblemDetails::schema()
165 }
166}
167
168#[cfg(feature = "utoipa")]
169impl utoipa::ToSchema for CqrsError {
170 fn name() -> std::borrow::Cow<'static, str> {
171 std::borrow::Cow::Borrowed("CqrsError")
172 }
173}
174
175impl CqrsError {
176 pub fn from_code<C: CqrsErrorCode>(code: &C, message: impl Into<String>) -> Self {
178 Self(Box::new(CqrsErrorData {
179 domain: C::domain().to_string(),
180 code: code.code_string(),
181 internal_code: code.internal_code(),
182 status: code.http_status().as_u16(),
183 message: message.into(),
184 details: None,
185 request_id: None,
186 type_uri: None,
187 }))
188 }
189
190 pub fn with_details(mut self, details: serde_json::Value) -> Self {
192 self.details = Some(details);
193 self
194 }
195
196 pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
198 self.request_id = Some(request_id.into());
199 self
200 }
201
202 pub fn with_request_id_if_absent(mut self, request_id: impl Into<String>) -> Self {
206 let request_id = request_id.into();
207 if self.request_id.is_none() && !request_id.is_empty() {
208 self.request_id = Some(request_id);
209 }
210 self
211 }
212
213 pub fn with_type_uri(mut self, type_uri: impl Into<String>) -> Self {
217 self.type_uri = Some(type_uri.into());
218 self
219 }
220
221 pub fn http_status(&self) -> StatusCode {
223 StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
224 }
225
226 pub fn to_problem(&self) -> crate::problem::ProblemDetails {
231 crate::problem::ProblemDetails::from(self)
232 }
233
234 pub fn not_found(message: impl Into<String>) -> Self {
240 GenericErrorCode::NotFound.error(message)
241 }
242
243 pub fn validation(message: impl Into<String>) -> Self {
245 GenericErrorCode::ValidationFailed.error(message)
246 }
247
248 pub fn internal(message: impl Into<String>) -> Self {
250 GenericErrorCode::InternalError.error(message)
251 }
252
253 pub fn conflict(message: impl Into<String>) -> Self {
255 GenericErrorCode::Conflict.error(message)
256 }
257
258 pub fn unauthorized(message: impl Into<String>) -> Self {
260 GenericErrorCode::Unauthorized.error(message)
261 }
262
263 pub fn forbidden(message: impl Into<String>) -> Self {
265 GenericErrorCode::Forbidden.error(message)
266 }
267
268 pub fn gone(message: impl Into<String>) -> Self {
270 GenericErrorCode::Gone.error(message)
271 }
272
273 pub fn unprocessable(message: impl Into<String>) -> Self {
278 GenericErrorCode::UnprocessableEntity.error(message)
279 }
280
281 pub fn precondition_failed(message: impl Into<String>) -> Self {
283 GenericErrorCode::PreconditionFailed.error(message)
284 }
285
286 pub fn precondition_required(message: impl Into<String>) -> Self {
288 GenericErrorCode::PreconditionRequired.error(message)
289 }
290
291 pub fn unsupported_media_type(message: impl Into<String>) -> Self {
293 GenericErrorCode::UnsupportedMediaType.error(message)
294 }
295
296 pub fn payload_too_large(message: impl Into<String>) -> Self {
298 GenericErrorCode::PayloadTooLarge.error(message)
299 }
300
301 pub fn too_many_requests(message: impl Into<String>) -> Self {
303 GenericErrorCode::TooManyRequests.error(message)
304 }
305
306 pub fn not_implemented(message: impl Into<String>) -> Self {
308 GenericErrorCode::NotImplemented.error(message)
309 }
310
311 pub fn service_unavailable(message: impl Into<String>) -> Self {
313 GenericErrorCode::ServiceUnavailable.error(message)
314 }
315
316 pub fn user_error(e: impl std::fmt::Display) -> Self {
322 InfrastructureErrorCode::DomainError.error(e.to_string())
323 }
324
325 pub fn database_error(e: impl std::fmt::Display) -> Self {
327 InfrastructureErrorCode::DatabaseError.error(e.to_string())
328 }
329
330 pub fn serialization_error(e: impl std::fmt::Display) -> Self {
332 InfrastructureErrorCode::SerializationError.error(e.to_string())
333 }
334
335 pub fn concurrency_error() -> Self {
337 InfrastructureErrorCode::ConcurrencyError.error("Version conflict")
338 }
339
340 pub fn aggregate_not_found(id: &str) -> Self {
342 InfrastructureErrorCode::AggregateNotFound.error(format!("Aggregate '{}' not found", id))
343 }
344
345 pub fn aggregate_already_exists(id: &str) -> Self {
347 InfrastructureErrorCode::Conflict.error(format!("Aggregate '{}' already exists", id))
348 }
349
350 pub fn from_status(status: StatusCode, message: impl Into<String>) -> Self {
356 GenericErrorCode::from(status).error(message)
357 }
358}
359
360impl Display for CqrsError {
361 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
362 write!(
363 f,
364 "[{}] {}: {}",
365 self.internal_code, self.code, self.message
366 )
367 }
368}
369
370impl std::error::Error for CqrsError {}
371
372impl From<std::io::Error> for CqrsError {
373 fn from(e: std::io::Error) -> Self {
374 CqrsError::user_error(e)
375 }
376}
377
378#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
387pub enum InfrastructureErrorCode {
388 #[error("INTERNAL_ERROR")]
389 InternalError,
390 #[error("VALIDATION_FAILED")]
391 ValidationFailed,
392 #[error("NOT_FOUND")]
393 NotFound,
394 #[error("CONFLICT")]
395 Conflict,
396 #[error("UNAUTHORIZED")]
397 Unauthorized,
398 #[error("FORBIDDEN")]
399 Forbidden,
400 #[error("GONE")]
401 Gone,
402 #[error("DATABASE_ERROR")]
403 DatabaseError,
404 #[error("SERIALIZATION_ERROR")]
405 SerializationError,
406 #[error("AGGREGATE_NOT_FOUND")]
407 AggregateNotFound,
408 #[error("CONCURRENCY_ERROR")]
409 ConcurrencyError,
410 #[error("DOMAIN_ERROR")]
411 DomainError,
412 #[error("CQRS_ERROR")]
413 CqrsInternalError,
414 #[error("CONFIGURATION_ERROR")]
415 ConfigurationError,
416 #[error("UNKNOWN")]
417 Unknown,
418}
419
420impl CqrsErrorCode for InfrastructureErrorCode {
421 fn domain() -> &'static str {
422 "infrastructure"
423 }
424 fn domain_prefix() -> u16 {
425 0
426 }
427
428 fn error_index(&self) -> u16 {
429 match self {
430 Self::InternalError => 0,
431 Self::ValidationFailed => 1,
432 Self::NotFound => 2,
433 Self::Conflict => 3,
434 Self::Unauthorized => 4,
435 Self::Forbidden => 5,
436 Self::Gone => 6,
437 Self::DatabaseError => 10,
438 Self::SerializationError => 11,
439 Self::AggregateNotFound => 12,
440 Self::ConcurrencyError => 13,
441 Self::DomainError => 14,
442 Self::CqrsInternalError => 15,
443 Self::ConfigurationError => 16,
444 Self::Unknown => 99,
445 }
446 }
447
448 fn http_status(&self) -> StatusCode {
449 match self {
450 Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
451 Self::ValidationFailed => StatusCode::BAD_REQUEST,
452 Self::NotFound | Self::AggregateNotFound => StatusCode::NOT_FOUND,
453 Self::Conflict | Self::ConcurrencyError => StatusCode::CONFLICT,
454 Self::Unauthorized => StatusCode::UNAUTHORIZED,
455 Self::Forbidden => StatusCode::FORBIDDEN,
456 Self::Gone => StatusCode::GONE,
457 Self::DatabaseError
458 | Self::SerializationError
459 | Self::CqrsInternalError
460 | Self::ConfigurationError
461 | Self::Unknown => StatusCode::INTERNAL_SERVER_ERROR,
462 Self::DomainError => StatusCode::BAD_REQUEST,
463 }
464 }
465}
466
467#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
485#[non_exhaustive]
486pub enum GenericErrorCode {
487 #[error("INTERNAL_ERROR")]
488 InternalError,
489 #[error("VALIDATION_FAILED")]
490 ValidationFailed,
491 #[error("NOT_FOUND")]
492 NotFound,
493 #[error("CONFLICT")]
494 Conflict,
495 #[error("UNAUTHORIZED")]
496 Unauthorized,
497 #[error("FORBIDDEN")]
498 Forbidden,
499 #[error("GONE")]
500 Gone,
501 #[error("PAYMENT_REQUIRED")]
502 PaymentRequired,
503 #[error("METHOD_NOT_ALLOWED")]
504 MethodNotAllowed,
505 #[error("NOT_ACCEPTABLE")]
506 NotAcceptable,
507 #[error("REQUEST_TIMEOUT")]
508 RequestTimeout,
509 #[error("PRECONDITION_FAILED")]
510 PreconditionFailed,
511 #[error("PAYLOAD_TOO_LARGE")]
512 PayloadTooLarge,
513 #[error("UNSUPPORTED_MEDIA_TYPE")]
514 UnsupportedMediaType,
515 #[error("UNPROCESSABLE_ENTITY")]
516 UnprocessableEntity,
517 #[error("LOCKED")]
518 Locked,
519 #[error("PRECONDITION_REQUIRED")]
520 PreconditionRequired,
521 #[error("TOO_MANY_REQUESTS")]
522 TooManyRequests,
523 #[error("NOT_IMPLEMENTED")]
524 NotImplemented,
525 #[error("SERVICE_UNAVAILABLE")]
526 ServiceUnavailable,
527 #[error("GATEWAY_TIMEOUT")]
528 GatewayTimeout,
529 #[error("HTTP_{0}")]
532 Other(u16),
533}
534
535impl CqrsErrorCode for GenericErrorCode {
536 fn domain() -> &'static str {
537 "generic"
538 }
539 fn domain_prefix() -> u16 {
540 1
541 }
542
543 fn error_index(&self) -> u16 {
544 match self {
545 Self::InternalError => 0,
546 Self::ValidationFailed => 1,
547 Self::NotFound => 2,
548 Self::Conflict => 3,
549 Self::Unauthorized => 4,
550 Self::Forbidden => 5,
551 Self::Gone => 6,
552 Self::PaymentRequired => 402,
554 Self::MethodNotAllowed => 405,
555 Self::NotAcceptable => 406,
556 Self::RequestTimeout => 408,
557 Self::PreconditionFailed => 412,
558 Self::PayloadTooLarge => 413,
559 Self::UnsupportedMediaType => 415,
560 Self::UnprocessableEntity => 422,
561 Self::Locked => 423,
562 Self::PreconditionRequired => 428,
563 Self::TooManyRequests => 429,
564 Self::NotImplemented => 501,
565 Self::ServiceUnavailable => 503,
566 Self::GatewayTimeout => 504,
567 Self::Other(status) => *status,
568 }
569 }
570
571 fn http_status(&self) -> StatusCode {
572 match self {
573 Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
574 Self::ValidationFailed => StatusCode::BAD_REQUEST,
575 Self::NotFound => StatusCode::NOT_FOUND,
576 Self::Conflict => StatusCode::CONFLICT,
577 Self::Unauthorized => StatusCode::UNAUTHORIZED,
578 Self::Forbidden => StatusCode::FORBIDDEN,
579 Self::Gone => StatusCode::GONE,
580 Self::PaymentRequired => StatusCode::PAYMENT_REQUIRED,
581 Self::MethodNotAllowed => StatusCode::METHOD_NOT_ALLOWED,
582 Self::NotAcceptable => StatusCode::NOT_ACCEPTABLE,
583 Self::RequestTimeout => StatusCode::REQUEST_TIMEOUT,
584 Self::PreconditionFailed => StatusCode::PRECONDITION_FAILED,
585 Self::PayloadTooLarge => StatusCode::PAYLOAD_TOO_LARGE,
586 Self::UnsupportedMediaType => StatusCode::UNSUPPORTED_MEDIA_TYPE,
587 Self::UnprocessableEntity => StatusCode::UNPROCESSABLE_ENTITY,
588 Self::Locked => StatusCode::LOCKED,
589 Self::PreconditionRequired => StatusCode::PRECONDITION_REQUIRED,
590 Self::TooManyRequests => StatusCode::TOO_MANY_REQUESTS,
591 Self::NotImplemented => StatusCode::NOT_IMPLEMENTED,
592 Self::ServiceUnavailable => StatusCode::SERVICE_UNAVAILABLE,
593 Self::GatewayTimeout => StatusCode::GATEWAY_TIMEOUT,
594 Self::Other(status) => {
595 StatusCode::from_u16(*status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
596 }
597 }
598 }
599}
600
601impl From<StatusCode> for GenericErrorCode {
602 fn from(status: StatusCode) -> Self {
606 match status.as_u16() {
607 400 => GenericErrorCode::ValidationFailed,
608 401 => GenericErrorCode::Unauthorized,
609 402 => GenericErrorCode::PaymentRequired,
610 403 => GenericErrorCode::Forbidden,
611 404 => GenericErrorCode::NotFound,
612 405 => GenericErrorCode::MethodNotAllowed,
613 406 => GenericErrorCode::NotAcceptable,
614 408 => GenericErrorCode::RequestTimeout,
615 409 => GenericErrorCode::Conflict,
616 410 => GenericErrorCode::Gone,
617 412 => GenericErrorCode::PreconditionFailed,
618 413 => GenericErrorCode::PayloadTooLarge,
619 415 => GenericErrorCode::UnsupportedMediaType,
620 422 => GenericErrorCode::UnprocessableEntity,
621 423 => GenericErrorCode::Locked,
622 428 => GenericErrorCode::PreconditionRequired,
623 429 => GenericErrorCode::TooManyRequests,
624 500 => GenericErrorCode::InternalError,
625 501 => GenericErrorCode::NotImplemented,
626 503 => GenericErrorCode::ServiceUnavailable,
627 504 => GenericErrorCode::GatewayTimeout,
628 other => GenericErrorCode::Other(other),
629 }
630 }
631}
632
633#[deprecated(since = "0.2.0", note = "Use CqrsError instead")]
638pub type AggregateError = CqrsError;
639
640#[macro_export]
668macro_rules! define_domain_errors {
669 (
670 domain: $domain:literal,
671 prefix: $prefix:expr,
672 errors: {
673 $( $variant:ident => ($index:expr, $status:expr, $display:literal) ),* $(,)?
674 }
675 ) => {
676 #[derive(Debug, Clone, Copy, PartialEq, Eq, ::thiserror::Error)]
678 pub enum ErrorCode {
679 $(
680 #[error($display)]
681 $variant,
682 )*
683 }
684
685 impl $crate::CqrsErrorCode for ErrorCode {
686 fn domain() -> &'static str { $domain }
687 fn domain_prefix() -> u16 { $prefix }
688
689 fn error_index(&self) -> u16 {
690 match self {
691 $( Self::$variant => $index, )*
692 }
693 }
694
695 fn http_status(&self) -> ::http::StatusCode {
696 match self {
697 $( Self::$variant => $status, )*
698 }
699 }
700 }
701 };
702}
703
704#[cfg(test)]
709mod tests {
710 use super::*;
711
712 #[test]
713 fn test_generic_error_code() {
714 let err = GenericErrorCode::NotFound.error("Resource not found");
715 assert_eq!(err.domain, "generic");
716 assert_eq!(err.code, "GENERIC_NOT_FOUND");
717 assert_eq!(err.internal_code, 1002);
718 assert_eq!(err.status, 404);
719 }
720
721 #[test]
722 fn test_infrastructure_error_code() {
723 let err = InfrastructureErrorCode::DatabaseError.error("Connection failed");
724 assert_eq!(err.domain, "infrastructure");
725 assert_eq!(err.code, "INFRASTRUCTURE_DATABASE_ERROR");
726 assert_eq!(err.internal_code, 10); assert_eq!(err.status, 500);
728 }
729
730 #[test]
731 fn test_convenience_constructors() {
732 let err = CqrsError::not_found("User not found");
733 assert_eq!(err.code, "GENERIC_NOT_FOUND");
734
735 let err = CqrsError::validation("Invalid email");
736 assert_eq!(err.code, "GENERIC_VALIDATION_FAILED");
737 }
738
739 #[test]
740 fn test_migration_constructors() {
741 let err = CqrsError::user_error("bad input");
742 assert_eq!(err.code, "INFRASTRUCTURE_DOMAIN_ERROR");
743 assert_eq!(err.status, 400);
744
745 let err = CqrsError::database_error("connection lost");
746 assert_eq!(err.code, "INFRASTRUCTURE_DATABASE_ERROR");
747 assert_eq!(err.status, 500);
748
749 let err = CqrsError::serialization_error("invalid json");
750 assert_eq!(err.code, "INFRASTRUCTURE_SERIALIZATION_ERROR");
751 assert_eq!(err.status, 500);
752
753 let err = CqrsError::concurrency_error();
754 assert_eq!(err.code, "INFRASTRUCTURE_CONCURRENCY_ERROR");
755 assert_eq!(err.status, 409);
756
757 let err = CqrsError::aggregate_not_found("abc");
758 assert_eq!(err.code, "INFRASTRUCTURE_AGGREGATE_NOT_FOUND");
759 assert_eq!(err.status, 404);
760 assert!(err.message.contains("abc"));
761
762 let err = CqrsError::aggregate_already_exists("xyz");
763 assert_eq!(err.code, "INFRASTRUCTURE_CONFLICT");
764 assert_eq!(err.status, 409);
765 assert!(err.message.contains("xyz"));
766 }
767
768 #[test]
769 fn test_from_status_keeps_historical_codes() {
770 for (status, code, internal) in [
772 (StatusCode::BAD_REQUEST, "GENERIC_VALIDATION_FAILED", 1001),
773 (StatusCode::NOT_FOUND, "GENERIC_NOT_FOUND", 1002),
774 (StatusCode::CONFLICT, "GENERIC_CONFLICT", 1003),
775 (StatusCode::UNAUTHORIZED, "GENERIC_UNAUTHORIZED", 1004),
776 (StatusCode::FORBIDDEN, "GENERIC_FORBIDDEN", 1005),
777 (StatusCode::GONE, "GENERIC_GONE", 1006),
778 (
779 StatusCode::INTERNAL_SERVER_ERROR,
780 "GENERIC_INTERNAL_ERROR",
781 1000,
782 ),
783 ] {
784 let err = CqrsError::from_status(status, "boom");
785 assert_eq!(err.status, status.as_u16());
786 assert_eq!(err.code, code);
787 assert_eq!(err.internal_code, internal);
788 }
789 }
790
791 #[test]
792 fn test_from_status_supports_additional_statuses() {
793 for (status, code, internal) in [
794 (
795 StatusCode::UNPROCESSABLE_ENTITY,
796 "GENERIC_UNPROCESSABLE_ENTITY",
797 1422,
798 ),
799 (
800 StatusCode::TOO_MANY_REQUESTS,
801 "GENERIC_TOO_MANY_REQUESTS",
802 1429,
803 ),
804 (
805 StatusCode::PRECONDITION_FAILED,
806 "GENERIC_PRECONDITION_FAILED",
807 1412,
808 ),
809 (
810 StatusCode::UNSUPPORTED_MEDIA_TYPE,
811 "GENERIC_UNSUPPORTED_MEDIA_TYPE",
812 1415,
813 ),
814 (
815 StatusCode::SERVICE_UNAVAILABLE,
816 "GENERIC_SERVICE_UNAVAILABLE",
817 1503,
818 ),
819 (
820 StatusCode::PAYMENT_REQUIRED,
821 "GENERIC_PAYMENT_REQUIRED",
822 1402,
823 ),
824 ] {
825 let err = CqrsError::from_status(status, "boom");
826 assert_eq!(err.status, status.as_u16(), "status for {status}");
827 assert_eq!(err.code, code);
828 assert_eq!(err.internal_code, internal);
829 }
830 }
831
832 #[test]
833 fn test_from_status_never_degrades_unknown_status() {
834 let err = CqrsError::from_status(StatusCode::IM_A_TEAPOT, "no coffee");
835 assert_eq!(err.status, 418);
836 assert_eq!(err.code, "GENERIC_HTTP_418");
837 assert_eq!(err.internal_code, 1418);
838 assert_eq!(err.http_status(), StatusCode::IM_A_TEAPOT);
839 }
840
841 #[test]
842 fn test_additional_convenience_constructors() {
843 assert_eq!(CqrsError::unprocessable("nope").status, 422);
844 assert_eq!(CqrsError::too_many_requests("slow down").status, 429);
845 assert_eq!(CqrsError::precondition_failed("etag").status, 412);
846 assert_eq!(CqrsError::precondition_required("etag").status, 428);
847 assert_eq!(CqrsError::unsupported_media_type("xml").status, 415);
848 assert_eq!(CqrsError::payload_too_large("too big").status, 413);
849 assert_eq!(CqrsError::not_implemented("later").status, 501);
850 assert_eq!(CqrsError::service_unavailable("maintenance").status, 503);
851 assert_eq!(CqrsError::gone("removed").status, 410);
852 }
853
854 #[test]
855 fn test_with_details() {
856 let err = GenericErrorCode::NotFound
857 .error("User not found")
858 .with_details(serde_json::json!({"user_id": "123"}));
859
860 assert!(err.details.is_some());
861 assert_eq!(err.details.as_ref().unwrap()["user_id"], "123");
862 }
863
864 #[test]
865 fn test_serialization() {
866 let err = GenericErrorCode::Conflict.error("Already exists");
867 let json = serde_json::to_string(&err).unwrap();
868
869 assert!(json.contains("\"domain\":\"generic\""));
870 assert!(json.contains("\"code\":\"GENERIC_CONFLICT\""));
871 assert!(json.contains("\"internalCode\":1003"));
872 assert!(json.contains("\"message\":\"Already exists\""));
873 assert!(!json.contains("\"status\""));
875 }
876}