Skip to content

Commit

Permalink
feat[appbiotic-error]: initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
kriswuollett committed Dec 31, 2024
1 parent 335e2d3 commit d379fab
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 0 deletions.
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[workspace]

resolver = "2"
members = ["crates/*"]
18 changes: 18 additions & 0 deletions crates/error/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "appbiotic-error"
version = "0.1.0"
edition = "2021"

[features]
default = ["derive-new", "serde"]
serde = ["serde/derive", "serde/std"]
derive-new = ["derive-new/std"]

[dependencies]
derive-new = { version = "0.7.0", optional = true }
serde = { version = "1.0.217", optional = true }
strum = { version = "0.26.3", features = ["derive", "std"] }
thiserror = { version = "2.0.9", features = ["std"] }

[dev-dependencies]
serde_json = { version = "1.0.134", features = ["std"] }
190 changes: 190 additions & 0 deletions crates/error/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
use std::{collections::BTreeMap, fmt};

// TODO: Add short summarizing docs referring to primary source

#[cfg_attr(feature = "derive-new", derive(derive_new::new))]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")
)]
#[derive(Debug, Clone)]
pub enum ErrorDetails {
ErrorInfo(ErrorInfo),
}

#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "derive-new", derive(derive_new::new))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ErrorInfo {
// TODO: Add validation for [ErrorInfo::reason]
#[cfg_attr(feature = "derive-new", new(into))]
pub reason: String,
// TODO: Add validation for [ErrorInfo::domain]
#[cfg_attr(feature = "derive-new", new(into))]
pub domain: String,
// TODO: Add validation for [ErrorInfo::metadata] keys
#[cfg_attr(feature = "derive-new", new(default))]
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "BTreeMap::is_empty")
)]
pub metadata: BTreeMap<String, String>,
}

#[derive(Clone, Debug, strum::IntoStaticStr, thiserror::Error)]
#[cfg_attr(feature = "derive-new", derive(derive_new::new))]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(tag = "code", rename_all = "SCREAMING_SNAKE_CASE")
)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
pub enum Status {
#[error("{}: {}", Into::<&'static str>::into(self), .0)]
Cancelled(StatusDetails),

#[error("{}: {}", Into::<&'static str>::into(self), .0)]
Unknown(StatusDetails),

#[error("{}: {}", Into::<&'static str>::into(self), .0)]
InvalidArgument(StatusDetails),

#[error("{}: {}", Into::<&'static str>::into(self), .0)]
DeadlineExceeded(StatusDetails),

#[error("{}: {}", Into::<&'static str>::into(self), .0)]
NotFound(StatusDetails),

#[error("{}: {}", Into::<&'static str>::into(self), .0)]
AlreadyExists(StatusDetails),

#[error("{}: {}", Into::<&'static str>::into(self), .0)]
PermissionDenied(StatusDetails),

#[error("{}: {}", Into::<&'static str>::into(self), .0)]
Unauthenticated(StatusDetails),

#[error("{}: {}", Into::<&'static str>::into(self), .0)]
ResourceExhaused(StatusDetails),

#[error("{}: {}", Into::<&'static str>::into(self), .0)]
FailedPrecondition(StatusDetails),

#[error("{}: {}", Into::<&'static str>::into(self), .0)]
Aborted(StatusDetails),

#[error("{}: {}", Into::<&'static str>::into(self), .0)]
OutOfRange(StatusDetails),

#[error("{}: {}", Into::<&'static str>::into(self), .0)]
Unimplemented(StatusDetails),

#[error("{}: {}", Into::<&'static str>::into(self), .0)]
Internal(StatusDetails),

#[error("{}: {}", Into::<&'static str>::into(self), .0)]
Unavailable(StatusDetails),

#[error("{}: {}", Into::<&'static str>::into(self), .0)]
DataLoss(StatusDetails),
}

#[derive(Clone, Debug)]
#[cfg_attr(feature = "derive-new", derive(derive_new::new))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct StatusDetails {
#[cfg_attr(feature = "derive-new", new(into))]
pub message: String,
#[cfg_attr(feature = "derive-new", new(into_iter = "ErrorDetails"))]
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Vec::is_empty")
)]
pub error_details: Vec<ErrorDetails>,
}

impl fmt::Display for StatusDetails {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}

pub type StatusResult<T> = Result<T, Status>;

#[derive(Debug, thiserror::Error, strum::IntoStaticStr)]
#[cfg_attr(feature = "derive-new", derive(derive_new::new))]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")
)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
pub enum ValidationError {
#[error("{}: {message}", Into::<&'static str>::into(self))]
InvalidFormat {
#[cfg_attr(feature = "derive-new", new(into))]
message: String,
},
}

#[cfg(test)]
mod test {
use serde_json::json;

use crate::*;

#[test]
fn status_message() {
let status = Status::new_unknown(StatusDetails::new(
"Unsure about that",
[ErrorDetails::new_error_info(ErrorInfo::new(
"UNKNOWN_FAULT",
"com.appbiotic.error",
))],
));

assert_eq!("UNKNOWN: Unsure about that", status.to_string());
}

#[test]
fn status_serialization() {
let status = Status::new_unknown(StatusDetails::new(
"Unsure about that",
[ErrorDetails::new_error_info(ErrorInfo::new(
"UNKNOWN_FAULT",
"com.appbiotic.error",
))],
));
let value = serde_json::to_value(&status).unwrap();
let expected = json!({
"code": "UNKNOWN",
"message": "Unsure about that",
"error_details": [
{
"type": "ERROR_INFO",
"reason": "UNKNOWN_FAULT",
"domain": "com.appbiotic.error"
},
],
});
assert_eq!(value, expected);
}

#[test]
fn validation_error_message() {
let error = ValidationError::new_invalid_format("did not match regex");
assert_eq!("INVALID_FORMAT: did not match regex", error.to_string());
}

#[test]
fn validation_error_serialization() {
let error = ValidationError::new_invalid_format("did not match regex");
let value = serde_json::to_value(&error).unwrap();
let expected = json!({
"type": "INVALID_FORMAT",
"message": "did not match regex"
});
assert_eq!(value, expected);
}
}

0 comments on commit d379fab

Please sign in to comment.