]> git.proxmox.com Git - proxmox.git/commitdiff
client: replace Error trait with a type
authorWolfgang Bumiller <w.bumiller@proxmox.com>
Mon, 7 Aug 2023 09:55:59 +0000 (11:55 +0200)
committerWolfgang Bumiller <w.bumiller@proxmox.com>
Wed, 9 Aug 2023 11:21:02 +0000 (13:21 +0200)
Because we ultimately also want to drop the `Environment` trait since
it is not suitable for all use cases (eg. wasm ui)

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
proxmox-client/src/client.rs
proxmox-client/src/environment.rs
proxmox-client/src/error.rs
proxmox-client/src/lib.rs

index a8135e22f532b3460384ef68ffd6fb90f7c2e033..2b9d8f096b67c17383b1ea98dfefa8252e3331d7 100644 (file)
@@ -13,14 +13,13 @@ use serde_json::Value;
 use proxmox_login::{Login, TicketResult};
 
 use crate::auth::AuthenticationKind;
-use crate::{Authentication, Environment, ErrorTrait, Token};
+use crate::{Authentication, Environment, Error, Token};
 
 /// HTTP client backend trait.
 ///
 /// An async [`Client`] requires some kind of async HTTP client implementation.
 pub trait HttpClient: Send + Sync {
-    type Error: ErrorTrait;
-    type ResponseFuture: Future<Output = Result<Response<Vec<u8>>, Self::Error>>;
+    type ResponseFuture: Future<Output = Result<Response<Vec<u8>>, Error>>;
 
     fn request(&self, request: Request<Vec<u8>>) -> Self::ResponseFuture;
 }
@@ -58,7 +57,7 @@ where
     }
 }
 
-fn to_request<E: ErrorTrait>(request: proxmox_login::Request) -> Result<http::Request<Vec<u8>>, E> {
+fn to_request(request: proxmox_login::Request) -> Result<http::Request<Vec<u8>>, Error> {
     http::Request::builder()
         .method(http::Method::POST)
         .uri(request.url)
@@ -68,7 +67,7 @@ fn to_request<E: ErrorTrait>(request: proxmox_login::Request) -> Result<http::Re
             request.content_length.to_string(),
         )
         .body(request.body.into_bytes())
-        .map_err(E::internal)
+        .map_err(|err| Error::internal("error building login http request", err))
 }
 
 impl<C, E: Environment> Client<C, E> {
@@ -83,7 +82,6 @@ impl<C, E> Client<C, E>
 where
     E: Environment,
     C: HttpClient,
-    E::Error: From<C::Error>,
 {
     /// Instantiate a client for an API with a given environment and HTTP client instance.
     pub fn with_client(api_url: Uri, environment: E, client: C) -> Self {
@@ -96,13 +94,13 @@ where
         }
     }
 
-    pub async fn login_auth(&self) -> Result<Arc<AuthenticationKind>, E::Error> {
+    pub async fn login_auth(&self) -> Result<Arc<AuthenticationKind>, Error> {
         self.login().await?;
         self.auth
             .lock()
             .unwrap()
             .clone()
-            .ok_or_else(|| E::Error::internal("login failed to set authentication information"))
+            .ok_or_else(|| Error::Other("login failed to set authentication information"))
     }
 
     /// If currently logged in, this will fill in the auth cookie and CSRFPreventionToken header
@@ -123,7 +121,7 @@ where
     pub async fn set_auth_headers(
         &self,
         request: http::request::Builder,
-    ) -> Result<http::request::Builder, E::Error> {
+    ) -> Result<http::request::Builder, Error> {
         Ok(self.login_auth().await?.set_auth_headers(request))
     }
 
@@ -134,7 +132,7 @@ where
     ///
     /// If no valid ticket is available already, this will connect to the PVE API and perform
     /// authentication.
-    pub async fn login(&self) -> Result<(), E::Error> {
+    pub async fn login(&self) -> Result<(), Error> {
         let (userid, login) = self.need_login().await?;
         let Some(login) = login else { return Ok(()) };
 
@@ -144,13 +142,10 @@ where
 
         if !response.status().is_success() {
             // FIXME: does `http` somehow expose the status string?
-            return Err(E::Error::api_error(
-                response.status(),
-                "authentication failed",
-            ));
+            return Err(Error::api(response.status(), "authentication failed"));
         }
 
-        let challenge = match login.response(response.body()).map_err(E::Error::bad_api)? {
+        let challenge = match login.response(response.body())? {
             TicketResult::Full(auth) => return self.finish_auth(&userid, auth).await,
             TicketResult::TfaRequired(challenge) => challenge,
         };
@@ -167,18 +162,16 @@ where
 
         let status = response.status();
         if !status.is_success() {
-            return Err(E::Error::api_error(status, "authentication failed"));
+            return Err(Error::api(status, "authentication failed"));
         }
 
-        let auth = challenge
-            .response(response.body())
-            .map_err(E::Error::bad_api)?;
+        let auth = challenge.response(response.body())?;
 
         self.finish_auth(&userid, auth).await
     }
 
     /// Get the current username and, if required, a `Login` request.
-    async fn need_login(&self) -> Result<(String, Option<Login>), E::Error> {
+    async fn need_login(&self) -> Result<(String, Option<Login>), Error> {
         use proxmox_login::ticket::Validity;
 
         let (userid, auth) = self.current_auth().await?;
@@ -211,7 +204,7 @@ where
                 userid,
                 Some(
                     Login::renew(self.api_url.to_string(), auth.ticket.to_string())
-                        .map_err(E::Error::custom)?,
+                        .map_err(Error::Ticket)?,
                 ),
             ),
 
@@ -229,8 +222,9 @@ where
     }
 
     /// Store the authentication info in our `auth` field and notify the environment.
-    async fn finish_auth(&self, userid: &str, auth: Authentication) -> Result<(), E::Error> {
-        let auth_string = serde_json::to_string(&auth).map_err(E::Error::internal)?;
+    async fn finish_auth(&self, userid: &str, auth: Authentication) -> Result<(), Error> {
+        let auth_string = serde_json::to_string(&auth)
+            .map_err(|err| Error::internal("failed to serialize authentication info", err))?;
         *self.auth.lock().unwrap() = Some(Arc::new(auth.into()));
         self.env
             .store_ticket_async(&self.api_url, userid, auth_string.as_bytes())
@@ -246,7 +240,7 @@ where
     /// If not authenticated yet, authenticate.
     ///
     /// This may cause the environment to be queried for user ids/passwords/FIDO/...
-    async fn current_auth(&self) -> Result<(String, Option<Arc<AuthenticationKind>>), E::Error> {
+    async fn current_auth(&self) -> Result<(String, Option<Arc<AuthenticationKind>>), Error> {
         let auth = self.auth.lock().unwrap().clone();
 
         let userid;
@@ -268,14 +262,14 @@ where
     async fn reload_existing_ticket(
         &self,
         userid: &str,
-    ) -> Result<Option<Arc<AuthenticationKind>>, E::Error> {
+    ) -> Result<Option<Arc<AuthenticationKind>>, Error> {
         let ticket = match self.env.load_ticket_async(&self.api_url, userid).await? {
             Some(auth) => auth,
             None => return Ok(None),
         };
 
         let auth: Authentication = serde_json::from_slice(&ticket)
-            .map_err(|err| E::Error::env(format!("bad ticket data: {err}")))?;
+            .map_err(|err| Error::internal("loaded bad ticket from environment", err))?;
 
         let auth = Arc::new(auth.into());
         *self.auth.lock().unwrap() = Some(Arc::clone(&auth));
@@ -283,7 +277,7 @@ where
     }
 
     /// Build a URI relative to the current API endpoint.
-    fn build_uri(&self, path: &str) -> Result<Uri, E::Error> {
+    fn build_uri(&self, path: &str) -> Result<Uri, Error> {
         let parts = self.api_url.clone().into_parts();
         let mut builder = http::uri::Builder::new();
         if let Some(scheme) = parts.scheme {
@@ -293,13 +287,16 @@ where
             builder = builder.authority(authority)
         }
         builder
-            .path_and_query(path.parse::<PathAndQuery>().map_err(E::Error::internal)?)
+            .path_and_query(
+                path.parse::<PathAndQuery>()
+                    .map_err(|err| Error::internal("failed to parse uri", err))?,
+            )
             .build()
-            .map_err(E::Error::internal)
+            .map_err(|err| Error::internal("failed to build Uri", err))
     }
 
     /// Execute a `GET` request, possibly trying multiple cluster nodes.
-    pub async fn get<'a, R>(&'a self, uri: &str) -> Result<ApiResponse<R>, E::Error>
+    pub async fn get<'a, R>(&'a self, uri: &str) -> Result<ApiResponse<R>, Error>
     where
         R: serde::de::DeserializeOwned,
     {
@@ -309,7 +306,7 @@ where
             .set_auth_headers(Request::get(self.build_uri(uri)?))
             .await?
             .body(Vec::new())
-            .map_err(E::Error::internal)?;
+            .map_err(|err| Error::internal("failed to build request", err))?;
 
         Self::handle_response(self.client.request(request).await?)
     }
@@ -319,7 +316,7 @@ where
         &'a self,
         uri: &str,
         body: &'a B,
-    ) -> Result<ApiResponse<R>, E::Error>
+    ) -> Result<ApiResponse<R>, Error>
     where
         B: serde::Serialize,
         R: serde::de::DeserializeOwned,
@@ -329,7 +326,7 @@ where
     }
 
     /// Execute a `PUT` request with the given body, possibly trying multiple cluster nodes.
-    pub async fn put<'a, B, R>(&'a self, uri: &str, body: &'a B) -> Result<ApiResponse<R>, E::Error>
+    pub async fn put<'a, B, R>(&'a self, uri: &str, body: &'a B) -> Result<ApiResponse<R>, Error>
     where
         B: serde::Serialize,
         R: serde::de::DeserializeOwned,
@@ -339,11 +336,7 @@ where
     }
 
     /// Execute a `POST` request with the given body, possibly trying multiple cluster nodes.
-    pub async fn post<'a, B, R>(
-        &'a self,
-        uri: &str,
-        body: &'a B,
-    ) -> Result<ApiResponse<R>, E::Error>
+    pub async fn post<'a, B, R>(&'a self, uri: &str, body: &'a B) -> Result<ApiResponse<R>, Error>
     where
         B: serde::Serialize,
         R: serde::de::DeserializeOwned,
@@ -354,7 +347,7 @@ where
     }
 
     /// Execute a `DELETE` request, possibly trying multiple cluster nodes.
-    pub async fn delete<'a, R>(&'a self, uri: &str) -> Result<ApiResponse<R>, E::Error>
+    pub async fn delete<'a, R>(&'a self, uri: &str) -> Result<ApiResponse<R>, Error>
     where
         R: serde::de::DeserializeOwned,
     {
@@ -364,7 +357,7 @@ where
             .set_auth_headers(Request::delete(self.build_uri(uri)?))
             .await?
             .body(Vec::new())
-            .map_err(E::Error::internal)?;
+            .map_err(|err| Error::internal("failed to build request", err))?;
 
         Self::handle_response(self.client.request(request).await?)
     }
@@ -374,7 +367,7 @@ where
         &'a self,
         uri: &str,
         body: &'a B,
-    ) -> Result<ApiResponse<R>, E::Error>
+    ) -> Result<ApiResponse<R>, Error>
     where
         B: serde::Serialize,
         R: serde::de::DeserializeOwned,
@@ -391,12 +384,13 @@ where
         method: http::Method,
         uri: &str,
         body: &'a B,
-    ) -> Result<ApiResponse<R>, E::Error>
+    ) -> Result<ApiResponse<R>, Error>
     where
         B: serde::Serialize,
         R: serde::de::DeserializeOwned,
     {
-        let body = serde_json::to_vec(&body).map_err(E::Error::internal)?;
+        let body = serde_json::to_vec(&body)
+            .map_err(|err| Error::internal("failed to serialize request body", err))?;
         let content_length = body.len();
         self.json_request_bytes(auth, method, uri, body, content_length)
             .await
@@ -410,7 +404,7 @@ where
         uri: &str,
         body: Vec<u8>,
         content_length: usize,
-    ) -> Result<ApiResponse<R>, E::Error>
+    ) -> Result<ApiResponse<R>, Error>
     where
         R: serde::de::DeserializeOwned,
     {
@@ -427,7 +421,7 @@ where
         uri: &str,
         body: Vec<u8>,
         content_length: usize,
-    ) -> Result<Response<Vec<u8>>, E::Error> {
+    ) -> Result<Response<Vec<u8>>, Error> {
         let request = Request::builder()
             .method(method.clone())
             .uri(self.build_uri(uri)?)
@@ -437,7 +431,7 @@ where
         let request = auth
             .set_auth_headers(request)
             .body(body.clone())
-            .map_err(E::Error::internal)?;
+            .map_err(|err| Error::internal("failed to build request", err))?;
 
         Ok(self.client.request(request).await?)
     }
@@ -445,12 +439,12 @@ where
     /// Check the status code, deserialize the json/extjs `RawApiResponse` and check for error
     /// messages inside.
     /// On success, deserialize the expected result type.
-    fn handle_response<R>(response: Response<Vec<u8>>) -> Result<ApiResponse<R>, E::Error>
+    fn handle_response<R>(response: Response<Vec<u8>>) -> Result<ApiResponse<R>, Error>
     where
         R: serde::de::DeserializeOwned,
     {
         if response.status() == StatusCode::UNAUTHORIZED {
-            return Err(E::Error::unauthorized());
+            return Err(Error::Unauthorized);
         }
 
         if !response.status().is_success() {
@@ -459,12 +453,13 @@ where
             //    Ok(value) =>
             //        if value["error"]
             let (response, body) = response.into_parts();
-            let body = String::from_utf8(body).map_err(E::Error::bad_api)?;
-            return Err(E::Error::api_error(response.status, body));
+            let body =
+                String::from_utf8(body).map_err(|_| Error::Other("API returned non-utf8 data"))?;
+            return Err(Error::api(response.status, body));
         }
 
-        let data: RawApiResponse<R> =
-            serde_json::from_slice(&response.into_body()).map_err(E::Error::bad_api)?;
+        let data: RawApiResponse<R> = serde_json::from_slice(&response.into_body())
+            .map_err(|err| Error::internal("failed to deserialize api response", err))?;
 
         data.check()
     }
@@ -528,7 +523,7 @@ struct RawApiResponse<T> {
 }
 
 impl<T> RawApiResponse<T> {
-    pub fn check<E: ErrorTrait>(mut self) -> Result<ApiResponse<T>, E> {
+    pub fn check(mut self) -> Result<ApiResponse<T>, Error> {
         if !self.success.unwrap_or(false) {
             let status = http::StatusCode::from_u16(self.status.unwrap_or(400))
                 .unwrap_or(http::StatusCode::BAD_REQUEST);
@@ -541,7 +536,7 @@ impl<T> RawApiResponse<T> {
                 let _ = write!(message, "\n{param}: {error}");
             }
 
-            return Err(E::api_error(status, message));
+            return Err(Error::api(status, message));
         }
 
         Ok(ApiResponse {
@@ -558,7 +553,6 @@ pub type HyperClient<E> = Client<Arc<proxmox_http::client::Client>, E>;
 impl<C, E> Client<C, E>
 where
     E: Environment,
-    E::Error: From<anyhow::Error>,
 {
     /// Create a new client instance which will connect to the provided endpoint.
     pub fn new(api_url: Uri, environment: E) -> HyperClient<E> {
@@ -575,7 +569,6 @@ mod hyper_client_extras {
     use std::future::Future;
     use std::sync::Arc;
 
-    use anyhow::format_err;
     use http::request::Request;
     use http::response::Response;
     use http::Uri;
@@ -586,7 +579,7 @@ mod hyper_client_extras {
     use proxmox_http::client::Client as ProxmoxClient;
 
     use super::{Client, HyperClient};
-    use crate::Environment;
+    use crate::{Environment, Error};
 
     #[derive(Default)]
     pub enum TlsOptions {
@@ -646,7 +639,6 @@ mod hyper_client_extras {
     impl<C, E> Client<C, E>
     where
         E: Environment,
-        E::Error: From<anyhow::Error>,
     {
         /// Create a new client instance which will connect to the provided endpoint.
         pub fn with_options(
@@ -654,9 +646,9 @@ mod hyper_client_extras {
             environment: E,
             tls_options: TlsOptions,
             http_options: proxmox_http::HttpOptions,
-        ) -> Result<HyperClient<E>, E::Error> {
+        ) -> Result<HyperClient<E>, Error> {
             let mut connector = SslConnector::builder(SslMethod::tls_client())
-                .map_err(|err| format_err!("failed to create ssl connector builder: {err}"))?;
+                .map_err(|err| Error::internal("failed to create ssl connector builder", err))?;
 
             match tls_options {
                 TlsOptions::Verify => (),
@@ -677,11 +669,11 @@ mod hyper_client_extras {
                 TlsOptions::CaCert(ca) => {
                     let mut store =
                         openssl::x509::store::X509StoreBuilder::new().map_err(|err| {
-                            format_err!("failed to create certificate store builder: {err}")
+                            Error::internal("failed to create certificate store builder", err)
                         })?;
                     store
                         .add_cert(ca)
-                        .map_err(|err| format_err!("failed to build certificate store: {err}"))?;
+                        .map_err(|err| Error::internal("failed to build certificate store", err))?;
                     connector.set_cert_store(store.build());
                 }
             }
@@ -693,10 +685,9 @@ mod hyper_client_extras {
     }
 
     impl super::HttpClient for Arc<proxmox_http::client::Client> {
-        type Error = anyhow::Error;
         #[allow(clippy::type_complexity)]
         type ResponseFuture =
-            std::pin::Pin<Box<dyn Future<Output = Result<Response<Vec<u8>>, Self::Error>> + Send>>;
+            std::pin::Pin<Box<dyn Future<Output = Result<Response<Vec<u8>>, Error>> + Send>>;
 
         fn request(&self, request: Request<Vec<u8>>) -> Self::ResponseFuture {
             let (parts, body) = request.into_parts();
@@ -705,15 +696,20 @@ mod hyper_client_extras {
             Box::pin(async move {
                 use hyper::body::HttpBody;
 
-                let (response, mut body) = (*this).request(request).await?.into_parts();
+                // FIXME: proxmox_http's client needs a way to return http status codes and such...
+                let (response, mut body) = (*this)
+                    .request(request)
+                    .await
+                    .map_err(Error::Anyhow)?
+                    .into_parts();
 
                 let mut data = Vec::<u8>::new();
                 while let Some(more) = body.data().await {
-                    let more = more?;
+                    let more = more.map_err(|err| Error::internal("error reading body", err))?;
                     data.extend(&more[..]);
                 }
 
-                Ok::<_, anyhow::Error>(Response::from_parts(response, data))
+                Ok::<_, Error>(Response::from_parts(response, data))
             })
         }
     }
index d87c282e11dfc1f11eda69c48b02ed96ad2bb686..2694e7d4c35c4a4bc7ff18735986ea77b3dccd36 100644 (file)
@@ -6,24 +6,20 @@ use http::Uri;
 
 use proxmox_login::tfa::TfaChallenge;
 
-use crate::ErrorTrait;
+use crate::Error;
 
 /// Provide input from the environment for storing/loading tickets or tokens and querying the user
 /// for passwords or 2nd factors.
 pub trait Environment: Send + Sync {
-    type Error: ErrorTrait;
-
     /// Store a ticket belonging to a user of an API.
     ///
     /// This is only used if `store_ticket_async` is not overwritten and may be left unimplemented
     /// in async code. By default it will just return an error.
     ///
     /// [`store_ticket_async`]: Environment::store_ticket_async
-    fn store_ticket(&self, api_url: &Uri, userid: &str, ticket: &[u8]) -> Result<(), Self::Error> {
+    fn store_ticket(&self, api_url: &Uri, userid: &str, ticket: &[u8]) -> Result<(), Error> {
         let _ = (api_url, userid, ticket);
-        Err(Self::Error::custom(
-            "missing store_ticket(_async) implementation",
-        ))
+        Err(Error::Other("missing store_ticket(_async) implementation"))
     }
 
     /// Load a user's cached ticket for an API url.
@@ -32,11 +28,9 @@ pub trait Environment: Send + Sync {
     /// in async code. By default it will just return an error.
     ///
     /// [`load_ticket_async`]: Environment::load_ticket_async
-    fn load_ticket(&self, api_url: &Uri, userid: &str) -> Result<Option<Vec<u8>>, Self::Error> {
+    fn load_ticket(&self, api_url: &Uri, userid: &str) -> Result<Option<Vec<u8>>, Error> {
         let _ = (api_url, userid);
-        Err(Self::Error::custom(
-            "missing load_ticket(_async) implementation",
-        ))
+        Err(Error::Other("missing load_ticket(_async) implementation"))
     }
 
     /// Query for a userid (name and realm).
@@ -45,11 +39,9 @@ pub trait Environment: Send + Sync {
     /// unimplemented in async code. By default it will just return an error.
     ///
     /// [`query_userid_async`]: Environment::query_userid_async
-    fn query_userid(&self, api_url: &Uri) -> Result<String, Self::Error> {
+    fn query_userid(&self, api_url: &Uri) -> Result<String, Error> {
         let _ = api_url;
-        Err(Self::Error::custom(
-            "missing query_userid(_async) implementation",
-        ))
+        Err(Error::Other("missing query_userid(_async) implementation"))
     }
 
     /// Query for a password.
@@ -58,9 +50,9 @@ pub trait Environment: Send + Sync {
     /// unimplemented in async code. By default it will just return an error.
     ///
     /// [`query_password_async`]: Environment::query_password_async
-    fn query_password(&self, api_url: &Uri, userid: &str) -> Result<String, Self::Error> {
+    fn query_password(&self, api_url: &Uri, userid: &str) -> Result<String, Error> {
         let _ = (api_url, userid);
-        Err(Self::Error::custom(
+        Err(Error::Other(
             "missing query_password(_async) implementation",
         ))
     }
@@ -76,9 +68,9 @@ pub trait Environment: Send + Sync {
         api_url: &Uri,
         userid: &str,
         challenge: &TfaChallenge,
-    ) -> Result<String, Self::Error> {
+    ) -> Result<String, Error> {
         let _ = (api_url, userid, challenge);
-        Err(Self::Error::second_factor_not_supported())
+        Err(Error::TfaNotSupported)
     }
 
     /// The client code uses async rust and it is fine to implement this instead of `store_ticket`.
@@ -87,7 +79,7 @@ pub trait Environment: Send + Sync {
         api_url: &'a Uri,
         userid: &'a str,
         ticket: &'a [u8],
-    ) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + 'a>> {
+    ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
         Box::pin(async move { self.store_ticket(api_url, userid, ticket) })
     }
 
@@ -96,14 +88,14 @@ pub trait Environment: Send + Sync {
         &'a self,
         api_url: &'a Uri,
         userid: &'a str,
-    ) -> Pin<Box<dyn Future<Output = Result<Option<Vec<u8>>, Self::Error>> + Send + 'a>> {
+    ) -> Pin<Box<dyn Future<Output = Result<Option<Vec<u8>>, Error>> + Send + 'a>> {
         Box::pin(async move { self.load_ticket(api_url, userid) })
     }
 
     fn query_userid_async<'a>(
         &'a self,
         api_url: &'a Uri,
-    ) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
+    ) -> Pin<Box<dyn Future<Output = Result<String, Error>> + Send + 'a>> {
         Box::pin(async move { self.query_userid(api_url) })
     }
 
@@ -111,7 +103,7 @@ pub trait Environment: Send + Sync {
         &'a self,
         api_url: &'a Uri,
         userid: &'a str,
-    ) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
+    ) -> Pin<Box<dyn Future<Output = Result<String, Error>> + Send + 'a>> {
         Box::pin(async move { self.query_password(api_url, userid) })
     }
 
@@ -120,7 +112,7 @@ pub trait Environment: Send + Sync {
         api_url: &'a Uri,
         userid: &'a str,
         challenge: &'a TfaChallenge,
-    ) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
+    ) -> Pin<Box<dyn Future<Output = Result<String, Error>> + Send + 'a>> {
         Box::pin(async move { self.query_second_factor(api_url, userid, challenge) })
     }
 
@@ -131,10 +123,8 @@ pub trait Environment: Send + Sync {
     /// # Panics
     ///
     /// The default implementation simply panics.
-    fn sleep(
-        time: Duration,
-    ) -> Result<Pin<Box<dyn Future<Output = ()> + Send + 'static>>, Self::Error> {
+    fn sleep(time: Duration) -> Result<Pin<Box<dyn Future<Output = ()> + Send + 'static>>, Error> {
         let _ = time;
-        Err(Self::Error::sleep_not_supported())
+        Err(Error::SleepNotSupported)
     }
 }
index a4dd504324c6189bb1dc317dff07e9b46125ec20..69c7590e6641fc088777885e6898f29f9f8469b1 100644 (file)
@@ -1,61 +1,86 @@
-use std::any::Any;
+use std::error::Error as StdError;
 use std::fmt::{self, Display};
 
-/// For error types provided by the user of this crate.
-pub trait ErrorTrait: Sized + Display + fmt::Debug + Any + Send + Sync + 'static {
-    /// An arbitrary error message.
-    fn custom<T: Display>(msg: T) -> Self;
+#[derive(Debug)]
+#[non_exhaustive]
+pub enum Error {
+    /// The environment did not provide a way to get a 2nd factor.
+    TfaNotSupported,
 
-    /// Successfully queried the status of a task, and the task has failed.
-    fn task_failed<T: Display>(msg: T) -> Self {
-        Self::custom(format!("task failed: {msg}"))
-    }
+    /// The task API wants to poll for completion of a task at regular intervals, for this it needs
+    /// to sleep. This signals that the environment does not support that.
+    SleepNotSupported,
 
-    /// An API call returned an error status.
-    fn api_error<T: Display>(status: http::StatusCode, msg: T) -> Self {
-        Self::custom(format!("api error (status = {status}): {msg}"))
-    }
+    /// Tried to make an API call without a ticket which requires ones.
+    Unauthorized,
 
-    /// The API behaved unexpectedly.
-    fn bad_api<T: Display>(msg: T) -> Self {
-        Self::custom(msg)
-    }
+    /// The API responded with an error code.
+    Api(http::StatusCode, String),
 
-    /// The environment returned an error or bad data.
-    fn env<T: Display>(msg: T) -> Self {
-        Self::custom(msg)
-    }
+    /// An error occurred in the authentication API.
+    Authentication(proxmox_login::error::ResponseError),
 
-    /// A second factor was required, but the [`Environment`](crate::Environment) did not provide
-    /// an implementation to get it.
-    fn second_factor_not_supported() -> Self {
-        Self::custom("not supported")
-    }
+    /// The current ticket was rejected.
+    Ticket(proxmox_login::error::TicketError),
+
+    /// Generic errors.
+    Other(&'static str),
 
-    /// There was an error building an [`http::Uri`].
-    fn uri(err: http::Error) -> Self {
-        Self::custom(err)
+    /// Generic errors bubbled up from a deeper source, usually the http client.
+    Client(Box<dyn StdError + Send + Sync + 'static>),
+
+    /// Another internal error occurred.
+    Internal(&'static str, Box<dyn StdError + Send + Sync + 'static>),
+
+    /// An `anyhow` error because `proxmox_http::Client` uses it...
+    Anyhow(anyhow::Error),
+}
+
+impl StdError for Error {
+    fn source(&self) -> Option<&(dyn StdError + 'static)> {
+        match self {
+            Self::Authentication(err) => Some(err),
+            Self::Ticket(err) => Some(err),
+            Self::Client(err) => Some(&**err),
+            Self::Internal(_, err) => Some(&**err),
+            Self::Anyhow(err) => err.chain().next(),
+            _ => None,
+        }
     }
+}
 
-    /// A generic internal error such as a serde_json serialization error.
-    fn internal<T: Display>(err: T) -> Self {
-        Self::custom(err)
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::TfaNotSupported => f.write_str("tfa not supported by environment"),
+            Self::SleepNotSupported => f.write_str("environment does not support sleeping"),
+            Self::Unauthorized => f.write_str("unauthorized"),
+            Self::Api(status, msg) => write!(f, "api error (status = {status}): {msg}"),
+            Self::Other(err) => f.write_str(err),
+            Self::Authentication(err) => write!(f, "authentication error: {err}"),
+            Self::Ticket(err) => write!(f, "authentication error: {err}"),
+            Self::Client(err) => fmt::Display::fmt(err, f),
+            Self::Internal(msg, _) => f.write_str(msg),
+            Self::Anyhow(err) => fmt::Display::fmt(err, f),
+        }
     }
+}
 
-    /// An API call which requires authorization was attempted without logging in first.
-    fn unauthorized() -> Self {
-        Self::custom("unauthorized")
+impl Error {
+    pub(crate) fn api<T: Display>(status: http::StatusCode, msg: T) -> Self {
+        Self::Api(status, msg.to_string())
     }
 
-    /// An extended client call required the ability to "pause" while polling API endpoints.
-    /// (Mostly to wait for "tasks" to finish.), and no implementation for this was provided.
-    fn sleep_not_supported() -> Self {
-        Self::custom("no async 'sleep' implementation available")
+    pub(crate) fn internal<E>(context: &'static str, err: E) -> Self
+    where
+        E: StdError + Send + Sync + 'static,
+    {
+        Self::Internal(context, Box::new(err))
     }
 }
 
-impl ErrorTrait for anyhow::Error {
-    fn custom<T: Display>(msg: T) -> Self {
-        anyhow::format_err!("{msg}")
+impl From<proxmox_login::error::ResponseError> for Error {
+    fn from(err: proxmox_login::error::ResponseError) -> Self {
+        Self::Authentication(err)
     }
 }
index e046d1c5ddf9c3cba757d10bb73634b7f9031076..929b4a1bf0d6a5b00d93abde23412422d4453786 100644 (file)
@@ -2,7 +2,7 @@ mod environment;
 mod error;
 
 pub use environment::Environment;
-pub use error::ErrorTrait;
+pub use error::Error;
 
 pub use proxmox_login::tfa::TfaChallenge;
 pub use proxmox_login::{Authentication, Ticket};