Expose LemmyErrorType in lemmy_api_common (#4439)

* Expose LemmyErrorType in lemmy_api_common

* Make conditional compilation gates for utils

* Make it so api_common doesn't pull in unnecessary deps

* Make error type non exhaustive

* Fix formatting

* Format toml

* Add some convenience derives to LemmyError

* Simplify features

* Fix CI compile error

---------

Co-authored-by: SleeplessOne1917 <insomnia-void@protonmail.com>
This commit is contained in:
SleeplessOne1917 2024-02-25 00:54:27 +00:00 committed by GitHub
parent f56b84615c
commit f42420809b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 225 additions and 169 deletions

1
Cargo.lock generated
View file

@ -2875,6 +2875,7 @@ version = "0.19.3"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"anyhow", "anyhow",
"cfg-if",
"deser-hjson", "deser-hjson",
"diesel", "diesel",
"doku", "doku",

View file

@ -89,7 +89,7 @@ unwrap_used = "deny"
lemmy_api = { version = "=0.19.3", path = "./crates/api" } lemmy_api = { version = "=0.19.3", path = "./crates/api" }
lemmy_api_crud = { version = "=0.19.3", path = "./crates/api_crud" } lemmy_api_crud = { version = "=0.19.3", path = "./crates/api_crud" }
lemmy_apub = { version = "=0.19.3", path = "./crates/apub" } lemmy_apub = { version = "=0.19.3", path = "./crates/apub" }
lemmy_utils = { version = "=0.19.3", path = "./crates/utils" } lemmy_utils = { version = "=0.19.3", path = "./crates/utils", default-features = false }
lemmy_db_schema = { version = "=0.19.3", path = "./crates/db_schema" } lemmy_db_schema = { version = "=0.19.3", path = "./crates/db_schema" }
lemmy_api_common = { version = "=0.19.3", path = "./crates/api_common" } lemmy_api_common = { version = "=0.19.3", path = "./crates/api_common" }
lemmy_routes = { version = "=0.19.3", path = "./crates/routes" } lemmy_routes = { version = "=0.19.3", path = "./crates/routes" }

View file

@ -18,7 +18,7 @@ doctest = false
workspace = true workspace = true
[dependencies] [dependencies]
lemmy_utils = { workspace = true } lemmy_utils = { workspace = true, features = ["default"] }
lemmy_db_schema = { workspace = true, features = ["full"] } lemmy_db_schema = { workspace = true, features = ["full"] }
lemmy_db_views = { workspace = true, features = ["full"] } lemmy_db_views = { workspace = true, features = ["full"] }
lemmy_db_views_moderator = { workspace = true, features = ["full"] } lemmy_db_views_moderator = { workspace = true, features = ["full"] }

View file

@ -20,10 +20,10 @@ workspace = true
full = [ full = [
"tracing", "tracing",
"rosetta-i18n", "rosetta-i18n",
"lemmy_utils",
"lemmy_db_views/full", "lemmy_db_views/full",
"lemmy_db_views_actor/full", "lemmy_db_views_actor/full",
"lemmy_db_views_moderator/full", "lemmy_db_views_moderator/full",
"lemmy_utils/default",
"activitypub_federation", "activitypub_federation",
"encoding", "encoding",
"reqwest-middleware", "reqwest-middleware",
@ -44,7 +44,7 @@ lemmy_db_views = { workspace = true }
lemmy_db_views_moderator = { workspace = true } lemmy_db_views_moderator = { workspace = true }
lemmy_db_views_actor = { workspace = true } lemmy_db_views_actor = { workspace = true }
lemmy_db_schema = { workspace = true } lemmy_db_schema = { workspace = true }
lemmy_utils = { workspace = true, optional = true } lemmy_utils = { workspace = true, features = ["error-type"] }
activitypub_federation = { workspace = true, optional = true } activitypub_federation = { workspace = true, optional = true }
serde = { workspace = true } serde = { workspace = true }
serde_with = { workspace = true } serde_with = { workspace = true }

View file

@ -23,7 +23,9 @@ pub extern crate lemmy_db_schema;
pub extern crate lemmy_db_views; pub extern crate lemmy_db_views;
pub extern crate lemmy_db_views_actor; pub extern crate lemmy_db_views_actor;
pub extern crate lemmy_db_views_moderator; pub extern crate lemmy_db_views_moderator;
pub extern crate lemmy_utils;
pub use lemmy_utils::LemmyErrorType;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::Duration; use std::time::Duration;

View file

@ -13,7 +13,7 @@ repository.workspace = true
workspace = true workspace = true
[dependencies] [dependencies]
lemmy_utils = { workspace = true } lemmy_utils = { workspace = true, features = ["default"] }
lemmy_db_schema = { workspace = true, features = ["full"] } lemmy_db_schema = { workspace = true, features = ["full"] }
lemmy_db_views = { workspace = true, features = ["full"] } lemmy_db_views = { workspace = true, features = ["full"] }
lemmy_db_views_actor = { workspace = true, features = ["full"] } lemmy_db_views_actor = { workspace = true, features = ["full"] }

View file

@ -18,7 +18,7 @@ doctest = false
workspace = true workspace = true
[dependencies] [dependencies]
lemmy_utils = { workspace = true } lemmy_utils = { workspace = true, features = ["default"] }
lemmy_db_schema = { workspace = true, features = ["full"] } lemmy_db_schema = { workspace = true, features = ["full"] }
lemmy_db_views = { workspace = true, features = ["full"] } lemmy_db_views = { workspace = true, features = ["full"] }
lemmy_db_views_actor = { workspace = true, features = ["full"] } lemmy_db_views_actor = { workspace = true, features = ["full"] }

View file

@ -19,6 +19,6 @@ diesel = { workspace = true }
diesel-async = { workspace = true } diesel-async = { workspace = true }
lemmy_db_schema = { workspace = true } lemmy_db_schema = { workspace = true }
lemmy_db_views = { workspace = true, features = ["full"] } lemmy_db_views = { workspace = true, features = ["full"] }
lemmy_utils = { workspace = true } lemmy_utils = { workspace = true, features = ["default"] }
tokio = { workspace = true } tokio = { workspace = true }
url = { workspace = true } url = { workspace = true }

View file

@ -48,7 +48,7 @@ strum = { workspace = true }
strum_macros = { workspace = true } strum_macros = { workspace = true }
serde_json = { workspace = true, optional = true } serde_json = { workspace = true, optional = true }
activitypub_federation = { workspace = true, optional = true } activitypub_federation = { workspace = true, optional = true }
lemmy_utils = { workspace = true, optional = true } lemmy_utils = { workspace = true, optional = true, features = ["default"] }
bcrypt = { workspace = true, optional = true } bcrypt = { workspace = true, optional = true }
diesel = { workspace = true, features = [ diesel = { workspace = true, features = [
"postgres", "postgres",

View file

@ -29,7 +29,7 @@ full = [
[dependencies] [dependencies]
lemmy_db_schema = { workspace = true } lemmy_db_schema = { workspace = true }
lemmy_utils = { workspace = true, optional = true } lemmy_utils = { workspace = true, optional = true, features = ["default"] }
diesel = { workspace = true, optional = true } diesel = { workspace = true, optional = true }
diesel-async = { workspace = true, optional = true } diesel-async = { workspace = true, optional = true }
diesel_ltree = { workspace = true, optional = true } diesel_ltree = { workspace = true, optional = true }

View file

@ -16,7 +16,7 @@ doctest = false
workspace = true workspace = true
[dependencies] [dependencies]
lemmy_utils = { workspace = true } lemmy_utils = { workspace = true, features = ["default"] }
lemmy_db_views = { workspace = true } lemmy_db_views = { workspace = true }
lemmy_db_views_actor = { workspace = true } lemmy_db_views_actor = { workspace = true }
lemmy_db_schema = { workspace = true } lemmy_db_schema = { workspace = true }

View file

@ -13,42 +13,82 @@ name = "lemmy_utils"
path = "src/lib.rs" path = "src/lib.rs"
doctest = false doctest = false
[[bin]]
name = "lemmy_util_bin"
path = "src/main.rs"
required-features = ["default"]
[lints] [lints]
workspace = true workspace = true
[features] [features]
full = ["ts-rs"] default = [
"error-type",
"dep:serde_json",
"dep:anyhow",
"dep:tracing-error",
"dep:diesel",
"dep:http",
"dep:actix-web",
"dep:reqwest-middleware",
"dep:tracing",
"dep:actix-web",
"dep:deser-hjson",
"dep:regex",
"dep:urlencoding",
"dep:doku",
"dep:url",
"dep:once_cell",
"dep:smart-default",
"dep:enum-map",
"dep:futures",
"dep:tokio",
"dep:openssl",
"dep:html2text",
"dep:lettre",
"dep:uuid",
"dep:rosetta-i18n",
"dep:itertools",
"dep:markdown-it",
]
full = ["default", "dep:ts-rs"]
error-type = ["dep:serde", "dep:strum"]
[dependencies] [dependencies]
regex = { workspace = true } regex = { workspace = true, optional = true }
tracing = { workspace = true } tracing = { workspace = true, optional = true }
tracing-error = { workspace = true } tracing-error = { workspace = true, optional = true }
itertools = { workspace = true } itertools = { workspace = true, optional = true }
serde = { workspace = true } serde = { workspace = true, optional = true }
serde_json = { workspace = true } serde_json = { workspace = true, optional = true }
once_cell = { workspace = true } once_cell = { workspace = true, optional = true }
url = { workspace = true } url = { workspace = true, optional = true }
actix-web = { workspace = true } actix-web = { workspace = true, optional = true }
anyhow = { workspace = true } anyhow = { workspace = true, optional = true }
reqwest-middleware = { workspace = true } reqwest-middleware = { workspace = true, optional = true }
strum = { workspace = true } strum = { workspace = true, optional = true }
strum_macros = { workspace = true } strum_macros = { workspace = true }
futures = { workspace = true } futures = { workspace = true, optional = true }
diesel = { workspace = true, features = ["chrono"] } diesel = { workspace = true, features = ["chrono"], optional = true }
http = { workspace = true } http = { workspace = true, optional = true }
doku = { workspace = true, features = ["url-2"] } doku = { workspace = true, features = ["url-2"], optional = true }
uuid = { workspace = true, features = ["serde", "v4"] } uuid = { workspace = true, features = ["serde", "v4"], optional = true }
rosetta-i18n = { workspace = true } rosetta-i18n = { workspace = true, optional = true }
tokio = { workspace = true } tokio = { workspace = true, optional = true }
urlencoding = { workspace = true } urlencoding = { workspace = true, optional = true }
openssl = "0.10.63" openssl = { version = "0.10.63", optional = true }
html2text = "0.6.0" html2text = { version = "0.6.0", optional = true }
deser-hjson = "2.2.4" deser-hjson = { version = "2.2.4", optional = true }
smart-default = "0.7.1" smart-default = { version = "0.7.1", optional = true }
lettre = { version = "0.11.3", features = ["tokio1", "tokio1-native-tls"] } lettre = { version = "0.11.3", features = [
markdown-it = "0.6.0" "tokio1",
"tokio1-native-tls",
], optional = true }
markdown-it = { version = "0.6.0", optional = true }
ts-rs = { workspace = true, optional = true } ts-rs = { workspace = true, optional = true }
enum-map = { workspace = true } enum-map = { workspace = true, optional = true }
cfg-if = "1"
[dev-dependencies] [dev-dependencies]
reqwest = { workspace = true } reqwest = { workspace = true }

View file

@ -1,79 +1,15 @@
use cfg_if::cfg_if;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::fmt::Debug;
fmt, use strum_macros::{Display, EnumIter};
fmt::{Debug, Display},
};
use tracing_error::SpanTrace;
#[cfg(feature = "full")] #[cfg(feature = "full")]
use ts_rs::TS; use ts_rs::TS;
pub type LemmyResult<T> = Result<T, LemmyError>; #[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, Eq, EnumIter, Hash)]
#[cfg_attr(feature = "ts-rs", derive(TS))]
pub struct LemmyError { #[cfg_attr(feature = "ts-rs", ts(export))]
pub error_type: LemmyErrorType,
pub inner: anyhow::Error,
pub context: SpanTrace,
}
/// Maximum number of items in an array passed as API parameter. See [[LemmyErrorType::TooManyItems]]
pub const MAX_API_PARAM_ELEMENTS: usize = 10_000;
impl<T> From<T> for LemmyError
where
T: Into<anyhow::Error>,
{
fn from(t: T) -> Self {
let cause = t.into();
LemmyError {
error_type: LemmyErrorType::Unknown(format!("{}", &cause)),
inner: cause,
context: SpanTrace::capture(),
}
}
}
impl Debug for LemmyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LemmyError")
.field("message", &self.error_type)
.field("inner", &self.inner)
.field("context", &self.context)
.finish()
}
}
impl Display for LemmyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: ", &self.error_type)?;
// print anyhow including trace
// https://docs.rs/anyhow/latest/anyhow/struct.Error.html#display-representations
// this will print the anyhow trace (only if it exists)
// and if RUST_BACKTRACE=1, also a full backtrace
writeln!(f, "{:?}", self.inner)?;
fmt::Display::fmt(&self.context, f)
}
}
impl actix_web::error::ResponseError for LemmyError {
fn status_code(&self) -> http::StatusCode {
if self.error_type == LemmyErrorType::IncorrectLogin {
return http::StatusCode::UNAUTHORIZED;
}
match self.inner.downcast_ref::<diesel::result::Error>() {
Some(diesel::result::Error::NotFound) => http::StatusCode::NOT_FOUND,
_ => http::StatusCode::BAD_REQUEST,
}
}
fn error_response(&self) -> actix_web::HttpResponse {
actix_web::HttpResponse::build(self.status_code()).json(&self.error_type)
}
}
#[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, EnumIter)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
#[serde(tag = "error", content = "message", rename_all = "snake_case")] #[serde(tag = "error", content = "message", rename_all = "snake_case")]
#[non_exhaustive]
// TODO: order these based on the crate they belong to (utils, federation, db, api) // TODO: order these based on the crate they belong to (utils, federation, db, api)
pub enum LemmyErrorType { pub enum LemmyErrorType {
ReportReasonRequired, ReportReasonRequired,
@ -231,7 +167,75 @@ pub enum LemmyErrorType {
Unknown(String), Unknown(String),
} }
impl From<LemmyErrorType> for LemmyError { cfg_if! {
if #[cfg(feature = "default")] {
use tracing_error::SpanTrace;
use std::fmt;
pub type LemmyResult<T> = Result<T, LemmyError>;
pub struct LemmyError {
pub error_type: LemmyErrorType,
pub inner: anyhow::Error,
pub context: SpanTrace,
}
/// Maximum number of items in an array passed as API parameter. See [[LemmyErrorType::TooManyItems]]
pub const MAX_API_PARAM_ELEMENTS: usize = 10_000;
impl<T> From<T> for LemmyError
where
T: Into<anyhow::Error>,
{
fn from(t: T) -> Self {
let cause = t.into();
LemmyError {
error_type: LemmyErrorType::Unknown(format!("{}", &cause)),
inner: cause,
context: SpanTrace::capture(),
}
}
}
impl Debug for LemmyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LemmyError")
.field("message", &self.error_type)
.field("inner", &self.inner)
.field("context", &self.context)
.finish()
}
}
impl fmt::Display for LemmyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: ", &self.error_type)?;
// print anyhow including trace
// https://docs.rs/anyhow/latest/anyhow/struct.Error.html#display-representations
// this will print the anyhow trace (only if it exists)
// and if RUST_BACKTRACE=1, also a full backtrace
writeln!(f, "{:?}", self.inner)?;
fmt::Display::fmt(&self.context, f)
}
}
impl actix_web::error::ResponseError for LemmyError {
fn status_code(&self) -> http::StatusCode {
if self.error_type == LemmyErrorType::IncorrectLogin {
return http::StatusCode::UNAUTHORIZED;
}
match self.inner.downcast_ref::<diesel::result::Error>() {
Some(diesel::result::Error::NotFound) => http::StatusCode::NOT_FOUND,
_ => http::StatusCode::BAD_REQUEST,
}
}
fn error_response(&self) -> actix_web::HttpResponse {
actix_web::HttpResponse::build(self.status_code()).json(&self.error_type)
}
}
impl From<LemmyErrorType> for LemmyError {
fn from(error_type: LemmyErrorType) -> Self { fn from(error_type: LemmyErrorType) -> Self {
let inner = anyhow::anyhow!("{}", error_type); let inner = anyhow::anyhow!("{}", error_type);
LemmyError { LemmyError {
@ -240,13 +244,13 @@ impl From<LemmyErrorType> for LemmyError {
context: SpanTrace::capture(), context: SpanTrace::capture(),
} }
} }
} }
pub trait LemmyErrorExt<T, E: Into<anyhow::Error>> { pub trait LemmyErrorExt<T, E: Into<anyhow::Error>> {
fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError>; fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError>;
} }
impl<T, E: Into<anyhow::Error>> LemmyErrorExt<T, E> for Result<T, E> { impl<T, E: Into<anyhow::Error>> LemmyErrorExt<T, E> for Result<T, E> {
fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> { fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> {
self.map_err(|error| LemmyError { self.map_err(|error| LemmyError {
error_type, error_type,
@ -254,13 +258,13 @@ impl<T, E: Into<anyhow::Error>> LemmyErrorExt<T, E> for Result<T, E> {
context: SpanTrace::capture(), context: SpanTrace::capture(),
}) })
} }
} }
pub trait LemmyErrorExt2<T> { pub trait LemmyErrorExt2<T> {
fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError>; fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError>;
fn into_anyhow(self) -> Result<T, anyhow::Error>; fn into_anyhow(self) -> Result<T, anyhow::Error>;
} }
impl<T> LemmyErrorExt2<T> for Result<T, LemmyError> { impl<T> LemmyErrorExt2<T> for Result<T, LemmyError> {
fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> { fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> {
self.map_err(|mut e| { self.map_err(|mut e| {
e.error_type = error_type; e.error_type = error_type;
@ -271,6 +275,8 @@ impl<T> LemmyErrorExt2<T> for Result<T, LemmyError> {
fn into_anyhow(self) -> Result<T, anyhow::Error> { fn into_anyhow(self) -> Result<T, anyhow::Error> {
self.map_err(|e| e.inner) self.map_err(|e| e.inner)
} }
}
}
} }
#[cfg(test)] #[cfg(test)]

View file

@ -1,23 +1,29 @@
#[macro_use] use cfg_if::cfg_if;
extern crate strum_macros;
#[macro_use]
extern crate smart_default;
pub mod apub; cfg_if! {
pub mod cache_header; if #[cfg(feature = "default")] {
pub mod email; pub mod apub;
pub mod error; pub mod cache_header;
pub mod rate_limit; pub mod email;
pub mod request; pub mod error;
pub mod response; pub mod rate_limit;
pub mod settings; pub mod request;
pub mod utils; pub mod response;
pub mod version; pub mod settings;
pub mod utils;
pub mod version;
} else {
mod error;
}
}
cfg_if! {
if #[cfg(feature = "error-type")] {
pub use error::LemmyErrorType;
}
}
use error::LemmyError;
use futures::Future;
use std::time::Duration; use std::time::Duration;
use tracing::Instrument;
pub type ConnectionId = usize; pub type ConnectionId = usize;
@ -35,10 +41,14 @@ macro_rules! location_info {
}; };
} }
#[cfg(feature = "default")]
/// tokio::spawn, but accepts a future that may fail and also /// tokio::spawn, but accepts a future that may fail and also
/// * logs errors /// * logs errors
/// * attaches the spawned task to the tracing span of the caller for better logging /// * attaches the spawned task to the tracing span of the caller for better logging
pub fn spawn_try_task(task: impl Future<Output = Result<(), LemmyError>> + Send + 'static) { pub fn spawn_try_task(
task: impl futures::Future<Output = Result<(), error::LemmyError>> + Send + 'static,
) {
use tracing::Instrument;
tokio::spawn( tokio::spawn(
async { async {
if let Err(e) = task.await { if let Err(e) = task.await {

View file

@ -6,6 +6,7 @@ use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr}, net::{IpAddr, Ipv4Addr, Ipv6Addr},
time::Instant, time::Instant,
}; };
use strum_macros::AsRefStr;
use tracing::debug; use tracing::debug;
static START_TIME: Lazy<Instant> = Lazy::new(Instant::now); static START_TIME: Lazy<Instant> = Lazy::new(Instant::now);

View file

@ -1,8 +1,4 @@
use crate::{ use crate::{error::LemmyError, location_info};
error::LemmyError,
location_info,
settings::structs::{PictrsConfig, Settings},
};
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use deser_hjson::from_str; use deser_hjson::from_str;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
@ -12,8 +8,7 @@ use urlencoding::encode;
pub mod structs; pub mod structs;
use crate::settings::structs::PictrsImageMode; use structs::{DatabaseConnection, PictrsConfig, PictrsImageMode, Settings};
use structs::DatabaseConnection;
static DEFAULT_CONFIG_FILE: &str = "config/config.hjson"; static DEFAULT_CONFIG_FILE: &str = "config/config.hjson";

View file

@ -1,5 +1,6 @@
use doku::Document; use doku::Document;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use smart_default::SmartDefault;
use std::{ use std::{
env, env,
net::{IpAddr, Ipv4Addr}, net::{IpAddr, Ipv4Addr},