From ffe908f636ffeadc29c806e6d81e28a3177ad1c0 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Tue, 8 Aug 2023 16:52:11 +0200 Subject: [PATCH] client: handle response data Signed-off-by: Wolfgang Bumiller --- proxmox-client/src/auth.rs | 16 -- proxmox-client/src/client.rs | 478 ++++++++--------------------------- proxmox-client/src/error.rs | 38 ++- proxmox-client/src/lib.rs | 120 ++++++++- 4 files changed, 243 insertions(+), 409 deletions(-) diff --git a/proxmox-client/src/auth.rs b/proxmox-client/src/auth.rs index 0587e39d..da0386b7 100644 --- a/proxmox-client/src/auth.rs +++ b/proxmox-client/src/auth.rs @@ -9,22 +9,6 @@ pub enum AuthenticationKind { Token(Token), } -impl AuthenticationKind { - pub fn set_auth_headers(&self, request: http::request::Builder) -> http::request::Builder { - match self { - AuthenticationKind::Ticket(auth) => auth.set_auth_headers(request), - AuthenticationKind::Token(auth) => auth.set_auth_headers(request), - } - } - - pub fn userid(&self) -> &str { - match self { - AuthenticationKind::Ticket(auth) => &auth.userid, - AuthenticationKind::Token(auth) => &auth.userid, - } - } -} - impl From for AuthenticationKind { fn from(auth: Authentication) -> Self { Self::Ticket(auth) diff --git a/proxmox-client/src/client.rs b/proxmox-client/src/client.rs index 499490cd..48b1a7a4 100644 --- a/proxmox-client/src/client.rs +++ b/proxmox-client/src/client.rs @@ -1,12 +1,10 @@ -use std::collections::HashMap; -use std::fmt; +use std::error::Error as StdError; use std::future::Future; use std::pin::Pin; use std::sync::Arc; use std::sync::Mutex; use http::request::Request; -use http::response::Response; use http::uri::PathAndQuery; use http::{StatusCode, Uri}; use hyper::body::{Body, HttpBody}; @@ -14,7 +12,6 @@ use openssl::hash::MessageDigest; use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; use openssl::x509::{self, X509}; use serde::Serialize; -use serde_json::Value; use proxmox_login::ticket::Validity; use proxmox_login::{Login, SecondFactorChallenge, TicketResult}; @@ -24,9 +21,6 @@ use crate::{Error, Token}; use super::{HttpApiClient, HttpApiResponse}; -#[allow(clippy::type_complexity)] -type ResponseFuture = Pin> + Send>>; - #[derive(Default)] pub enum TlsOptions { /// Default TLS verification. @@ -195,8 +189,19 @@ impl Client { return Err(Error::api(response.status, data)); } + let content_type = match response.headers.get(http::header::CONTENT_TYPE) { + None => None, + Some(value) => Some( + value + .to_str() + .map_err(|err| Error::internal("bad Content-Type header", err))? + .to_owned(), + ), + }; + Ok(HttpApiResponse { status: response.status.as_u16(), + content_type, body, }) } @@ -232,6 +237,29 @@ impl Client { Ok(()) } + async fn do_login_request(&self, request: proxmox_login::Request) -> Result, Error> { + let request = http::Request::builder() + .method(http::Method::POST) + .uri(request.url) + .header(http::header::CONTENT_TYPE, request.content_type) + .header( + http::header::CONTENT_LENGTH, + request.content_length.to_string(), + ) + .body(request.body.into()) + .map_err(|err| Error::internal("error building login http request", err))?; + + let api_response = self.client.request(request).await.map_err(Error::Anyhow)?; + if !api_response.status().is_success() { + return Err(Error::api(api_response.status(), "authentication failed")); + } + + let (_, body) = api_response.into_parts(); + let body = read_body(body).await?; + + Ok(body) + } + /// Attempt to refresh the current ticket. /// /// If not logged in at all yet, `Error::Unauthorized` will be returned. @@ -244,16 +272,10 @@ impl Client { let login = Login::renew(self.api_url.to_string(), auth.ticket.to_string()) .map_err(Error::Ticket)?; - let request = login_to_request(login.request())?; - let response = self.client.request(request).await.map_err(Error::Anyhow)?; - if !response.status().is_success() { - return Err(Error::api(response.status(), "authentication failed")); - } + let api_response = self.do_login_request(login.request()).await?; - let (_, body) = response.into_parts(); - let body = read_body(body).await?; - match login.response(&body)? { + match login.response(&api_response)? { TicketResult::Full(auth) => { *self.auth.lock().unwrap() = Some(Arc::new(auth.into())); Ok(()) @@ -264,6 +286,43 @@ impl Client { .into()), } } + + /// Attempt to login. + /// + /// This will propagate the PVE compatibility state and then perform the `Login` request via + /// the inner http client. + /// + /// If the authentication is complete, `None` is returned and the authentication state updated. + /// If a 2nd factor is required, `Some` is returned. + pub async fn login(&self, login: Login) -> Result, Error> { + let login = login.pve_compatibility(self.pve_compat); + + let api_response = self.do_login_request(login.request()).await?; + + Ok(match login.response(&api_response)? { + TicketResult::TfaRequired(challenge) => Some(challenge), + TicketResult::Full(auth) => { + *self.auth.lock().unwrap() = Some(Arc::new(auth.into())); + None + } + }) + } + + /// Attempt to finish a 2nd factor login. + /// + /// This will propagate the PVE compatibility state and then perform the `Login` request via + /// the inner http client. + pub async fn login_tfa( + &self, + challenge: SecondFactorChallenge, + challenge_response: proxmox_login::Request, + ) -> Result<(), Error> { + let api_response = self.do_login_request(challenge_response).await?; + + let auth = challenge.response(&api_response)?; + *self.auth.lock().unwrap() = Some(Arc::new(auth.into())); + Ok(()) + } } async fn read_body(mut body: Body) -> Result, Error> { @@ -276,63 +335,44 @@ async fn read_body(mut body: Body) -> Result, Error> { } impl HttpApiClient for Client { - type ResponseFuture = ResponseFuture; + type ResponseFuture<'a> = + Pin> + Send + 'a>>; - fn get(&self, path_and_query: &str) -> Self::ResponseFuture { - let client = Arc::clone(&self.client); - let request_params = self - .login_auth() - .and_then(|auth| self.build_uri(path_and_query).map(|uri| (auth, uri))); + fn get<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> { Box::pin(async move { - let (auth, uri) = request_params?; + let auth = self.login_auth()?; + let uri = self.build_uri(path_and_query)?; + let client = Arc::clone(&self.client); Self::authenticated_request(client, auth, http::Method::GET, uri, None).await }) } - fn post(&self, path_and_query: &str, params: &T) -> Self::ResponseFuture + fn post<'a, T>(&'a self, path_and_query: &'a str, params: &T) -> Self::ResponseFuture<'a> where T: ?Sized + Serialize, { - let client = Arc::clone(&self.client); - let request_params = self - .login_auth() - .and_then(|auth| self.build_uri(path_and_query).map(|uri| (auth, uri))) - .and_then(|(auth, uri)| { - serde_json::to_string(params) - .map_err(|err| Error::internal("failed to serialize parametres", err)) - .map(|params| (auth, uri, params)) - }); + let params = serde_json::to_string(params) + .map_err(|err| Error::internal("failed to serialize parametres", err)); + Box::pin(async move { - let (auth, uri, params) = request_params?; + let params = params?; + let auth = self.login_auth()?; + let uri = self.build_uri(path_and_query)?; + let client = Arc::clone(&self.client); Self::authenticated_request(client, auth, http::Method::POST, uri, Some(params)).await }) } - fn delete(&self, path_and_query: &str) -> Self::ResponseFuture { - let client = Arc::clone(&self.client); - let request_params = self - .login_auth() - .and_then(|auth| self.build_uri(path_and_query).map(|uri| (auth, uri))); + fn delete<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> { Box::pin(async move { - let (auth, uri) = request_params?; + let auth = self.login_auth()?; + let uri = self.build_uri(path_and_query)?; + let client = Arc::clone(&self.client); Self::authenticated_request(client, auth, http::Method::DELETE, uri, None).await }) } } -fn login_to_request(request: proxmox_login::Request) -> Result, Error> { - http::Request::builder() - .method(http::Method::POST) - .uri(request.url) - .header(http::header::CONTENT_TYPE, request.content_type) - .header( - http::header::CONTENT_LENGTH, - request.content_length.to_string(), - ) - .body(request.body.into()) - .map_err(|err| Error::internal("error building login http request", err)) -} - fn verify_fingerprint(chain: &x509::X509StoreContextRef, expected_fingerprint: &[u8]) -> bool { let Some(cert) = chain.current_cert() else { log::error!("no certificate in chain?"); @@ -369,325 +409,27 @@ fn fp_string(fp: &[u8]) -> String { out } -/* -impl Client { - /// If currently logged in, this will fill in the auth cookie and CSRFPreventionToken header - /// and return `Ok(request)`, otherwise it'll return `Err(request)` with the request - /// unmodified. - pub fn try_set_auth_headers( - &self, - request: http::request::Builder, - ) -> Result { - let auth = self.auth.lock().unwrap().clone(); - match auth { - Some(auth) => Ok(auth.set_auth_headers(request)), - None => Err(request), +impl Error { + pub(crate) fn internal(context: &'static str, err: E) -> Self + where + E: StdError + Send + Sync + 'static, + { + Self::Internal(context, Box::new(err)) + } +} + +impl AuthenticationKind { + pub fn set_auth_headers(&self, request: http::request::Builder) -> http::request::Builder { + match self { + AuthenticationKind::Ticket(auth) => auth.set_auth_headers(request), + AuthenticationKind::Token(auth) => auth.set_auth_headers(request), } } - /// Attempt to login. - /// - /// This will propagate the PVE compatibility state and then perform the `Login` request via - /// the inner http client. - /// - /// If the authentication is complete, `None` is returned and the authentication state updated. - /// If a 2nd factor is required, `Some` is returned. - pub async fn login(&self, login: Login) -> Result, Error> { - let login = login.pve_compatibility(self.pve_compat); - - let api_response = self.client.request(to_request(login.request())?).await?; - - if !api_response.status().is_success() { - // FIXME: does `http` somehow expose the status string? - return Err(Error::api(api_response.status(), "authentication failed")); - } - - Ok(match login.response(api_response.body())? { - TicketResult::TfaRequired(challenge) => Some(challenge), - TicketResult::Full(auth) => { - *self.auth.lock().unwrap() = Some(Arc::new(auth.into())); - None - } - }) - } - - /// Attempt to finish a 2nd factor login. - /// - /// This will propagate the PVE compatibility state and then perform the `Login` request via - /// the inner http client. - pub async fn login_tfa( - &self, - challenge: SecondFactorChallenge, - challenge_response: proxmox_login::Request, - ) -> Result<(), Error> { - let api_response = self.client.request(to_request(challenge_response)?).await?; - - if !api_response.status().is_success() { - // FIXME: does `http` somehow expose the status string? - return Err(Error::api(api_response.status(), "authentication failed")); - } - - let auth = challenge.response(api_response.body())?; - *self.auth.lock().unwrap() = Some(Arc::new(auth.into())); - Ok(()) - } - - /// Execute a `GET` request, possibly trying multiple cluster nodes. - pub async fn get<'a, R>(&'a self, uri: &str) -> Result, Error> - where - R: serde::de::DeserializeOwned, - { - let request = self - .set_auth_headers(Request::get(self.build_uri(uri)?)) - .await? - .body(Vec::new()) - .map_err(|err| Error::internal("failed to build request", err))?; - - Self::handle_response(self.client.request(request).await?) - } - - /// Execute a `GET` request with the given body, possibly trying multiple cluster nodes. - pub async fn get_with_body<'a, B, R>( - &'a self, - uri: &str, - body: &'a B, - ) -> Result, Error> - where - B: serde::Serialize, - R: serde::de::DeserializeOwned, - { - let auth = self.login_auth()?; - self.json_request(&auth, http::Method::GET, uri, body).await - } - - /// 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, Error> - where - B: serde::Serialize, - R: serde::de::DeserializeOwned, - { - let auth = self.login_auth()?; - self.json_request(&auth, http::Method::PUT, uri, body).await - } - - /// 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, Error> - where - B: serde::Serialize, - R: serde::de::DeserializeOwned, - { - let auth = self.login_auth()?; - self.json_request(&auth, http::Method::POST, uri, body) - .await - } - - /// Execute a `DELETE` request, possibly trying multiple cluster nodes. - pub async fn delete<'a, R>(&'a self, uri: &str) -> Result, Error> - where - R: serde::de::DeserializeOwned, - { - let request = self - .set_auth_headers(Request::delete(self.build_uri(uri)?)) - .await? - .body(Vec::new()) - .map_err(|err| Error::internal("failed to build request", err))?; - - Self::handle_response(self.client.request(request).await?) - } - - /// Execute a `DELETE` request with the given body, possibly trying multiple cluster nodes. - pub async fn delete_with_body<'a, B, R>( - &'a self, - uri: &str, - body: &'a B, - ) -> Result, Error> - where - B: serde::Serialize, - R: serde::de::DeserializeOwned, - { - let auth = self.login_auth()?; - self.json_request(&auth, http::Method::DELETE, uri, body) - .await - } - - /// Helper method for a JSON request with a JSON body `B`, yielding a JSON result type `R`. - pub(crate) async fn json_request<'a, B, R>( - &'a self, - auth: &'a AuthenticationKind, - method: http::Method, - uri: &str, - body: &'a B, - ) -> Result, Error> - where - B: serde::Serialize, - R: serde::de::DeserializeOwned, - { - 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 - } - - /// Helper method for a request with a byte body, yielding a JSON result of type `R`. - async fn json_request_bytes<'a, R>( - &'a self, - auth: &AuthenticationKind, - method: http::Method, - uri: &str, - body: Vec, - content_length: usize, - ) -> Result, Error> - where - R: serde::de::DeserializeOwned, - { - let response = self - .run_json_request_with_body(auth, method, uri, body, content_length) - .await?; - Self::handle_response(response) - } - - async fn run_json_request_with_body<'a>( - &'a self, - auth: &'a AuthenticationKind, - method: http::Method, - uri: &str, - body: Vec, - content_length: usize, - ) -> Result>, Error> { - let request = Request::builder() - .method(method.clone()) - .uri(self.build_uri(uri)?) - .header(http::header::CONTENT_TYPE, "application/json") - .header(http::header::CONTENT_LENGTH, content_length.to_string()); - - let request = auth - .set_auth_headers(request) - .body(body.clone()) - .map_err(|err| Error::internal("failed to build request", err))?; - - Ok(self.client.request(request).await?) - } - - /// 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(response: Response>) -> Result, Error> - where - R: serde::de::DeserializeOwned, - { - if response.status() == StatusCode::UNAUTHORIZED { - return Err(Error::Unauthorized); - } - - if !response.status().is_success() { - // FIXME: Decode json errors... - //match serde_json::from_slice(&body) - // Ok(value) => - // if value["error"] - let (response, body) = response.into_parts(); - 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 = serde_json::from_slice(&response.into_body()) - .map_err(|err| Error::internal("failed to deserialize api response", err))?; - - data.check() - } -} - -#[derive(Clone, Copy, Debug)] -pub struct NoData; - -impl std::error::Error for NoData {} -impl fmt::Display for NoData { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("api returned no data") - } -} - -pub struct ApiResponse { - pub data: Option, - pub attribs: HashMap, -} - -impl ApiResponse { - pub fn into_data_or_err(mut self) -> Result { - self.data.take().ok_or(NoData) - } -} - -#[derive(Clone, Copy, Debug)] -pub struct UnexpectedData; - -impl std::error::Error for UnexpectedData {} -impl fmt::Display for UnexpectedData { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("api returned unexpected data") - } -} - -impl ApiResponse<()> { - pub fn nodata(self) -> Result<(), UnexpectedData> { - if self.data.is_some() { - Err(UnexpectedData) - } else { - Ok(()) + pub fn userid(&self) -> &str { + match self { + AuthenticationKind::Ticket(auth) => &auth.userid, + AuthenticationKind::Token(auth) => &auth.userid, } } } - -#[derive(serde::Deserialize)] -struct RawApiResponse { - #[serde(default, deserialize_with = "proxmox_login::parse::deserialize_u16")] - pub status: Option, - pub message: Option, - #[serde(default, deserialize_with = "proxmox_login::parse::deserialize_bool")] - pub success: Option, - pub data: Option, - - #[serde(default)] - pub errors: HashMap, - - #[serde(default, flatten)] - pub attribs: HashMap, -} - -impl RawApiResponse { - pub fn check(mut self) -> Result, Error> { - if !self.success.unwrap_or(false) { - let status = http::StatusCode::from_u16(self.status.unwrap_or(400)) - .unwrap_or(http::StatusCode::BAD_REQUEST); - let mut message = self - .message - .take() - .unwrap_or_else(|| "no message provided".to_string()); - for (param, error) in self.errors { - use std::fmt::Write; - let _ = write!(message, "\n{param}: {error}"); - } - - return Err(Error::api(status, message)); - } - - Ok(ApiResponse { - data: self.data, - attribs: self.attribs, - }) - } -} - - - -impl Client { - /// Convenience method to login and set the authentication headers for a request. - pub fn set_auth_headers( - &self, - request: http::request::Builder, - ) -> Result { - Ok(self.login_auth()?.set_auth_headers(request)) - } -} -*/ diff --git a/proxmox-client/src/error.rs b/proxmox-client/src/error.rs index 3f2bbccb..6bbe275a 100644 --- a/proxmox-client/src/error.rs +++ b/proxmox-client/src/error.rs @@ -1,5 +1,5 @@ use std::error::Error as StdError; -use std::fmt::{self, Display}; +use std::fmt; #[derive(Debug)] #[non_exhaustive] @@ -10,6 +10,12 @@ pub enum Error { /// The API responded with an error code. Api(http::StatusCode, String), + /// The API returned something unexpected. + BadApi(String, Option>), + + /// An API call which is meant to return nothing returned unexpected data. + UnexpectedData, + /// An error occurred in the authentication API. Authentication(proxmox_login::error::ResponseError), @@ -33,6 +39,7 @@ impl StdError for Error { fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { Self::Authentication(err) => Some(err), + Self::BadApi(_, Some(err)) => Some(&**err), Self::Ticket(err) => Some(err), Self::Client(err) => Some(&**err), Self::Internal(_, err) => Some(&**err), @@ -47,6 +54,8 @@ impl fmt::Display for Error { match self { Self::Unauthorized => f.write_str("unauthorized"), Self::Api(status, msg) => write!(f, "api error (status = {status}): {msg}"), + Self::UnexpectedData => write!(f, "api unexpectedly returned data"), + Self::BadApi(msg, _) => write!(f, "api returned unexpected data - {msg}"), Self::Other(err) => f.write_str(err), Self::Authentication(err) => write!(f, "authentication error: {err}"), Self::Ticket(err) => write!(f, "authentication error: {err}"), @@ -57,21 +66,22 @@ impl fmt::Display for Error { } } -impl Error { - pub(crate) fn api(status: http::StatusCode, msg: T) -> Self { - Self::Api(status, msg.to_string()) - } - - pub(crate) fn internal(context: &'static str, err: E) -> Self - where - E: StdError + Send + Sync + 'static, - { - Self::Internal(context, Box::new(err)) - } -} - impl From for Error { fn from(err: proxmox_login::error::ResponseError) -> Self { Self::Authentication(err) } } + +impl Error { + pub(crate) fn bad_api(msg: T, err: E) -> Self + where + T: Into, + E: StdError + Send + Sync + 'static, + { + Self::BadApi(msg.into(), Some(Box::new(err))) + } + + pub(crate) fn api>(status: http::StatusCode, msg: T) -> Self { + Self::Api(status, msg.into()) + } +} diff --git a/proxmox-client/src/lib.rs b/proxmox-client/src/lib.rs index 939191b1..54e655ae 100644 --- a/proxmox-client/src/lib.rs +++ b/proxmox-client/src/lib.rs @@ -1,6 +1,8 @@ +use std::collections::HashMap; use std::future::Future; -use serde::Serialize; +use serde::{Deserialize, Serialize}; +use serde_json::Value; mod error; @@ -17,34 +19,130 @@ mod client; #[cfg(feature = "hyper-client")] pub use client::{Client, TlsOptions}; -/// A response from the HTTP API as required by the [`HttpApiClient`] trait. -pub struct HttpApiResponse { - pub status: u16, - pub body: Vec, -} - /// HTTP client backend trait. This should be implemented for a HTTP client capable of making /// *authenticated* API requests to a proxmox HTTP API. pub trait HttpApiClient: Send + Sync { /// An API call should return a status code and the raw body. - type ResponseFuture: Future>; + type ResponseFuture<'a>: Future> + 'a + where + Self: 'a; /// `GET` request with a path and query component (no hostname). /// /// For this request, authentication headers should be set! - fn get(&self, path_and_query: &str) -> Self::ResponseFuture; + fn get<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a>; /// `POST` request with a path and query component (no hostname), and a serializable body. /// /// The body should be serialized to json and sent with `Content-type: applicaion/json`. /// /// For this request, authentication headers should be set! - fn post(&self, path_and_query: &str, params: &T) -> Self::ResponseFuture + fn post<'a, T>(&'a self, path_and_query: &'a str, params: &T) -> Self::ResponseFuture<'a> where T: ?Sized + Serialize; /// `DELETE` request with a path and query component (no hostname). /// /// For this request, authentication headers should be set! - fn delete(&self, path_and_query: &str) -> Self::ResponseFuture; + fn delete<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a>; +} + +/// A response from the HTTP API as required by the [`HttpApiClient`] trait. +pub struct HttpApiResponse { + pub status: u16, + pub content_type: Option, + pub body: Vec, +} + +impl HttpApiResponse { + /// Expect a JSON response as returend by the `extjs` formatter. + pub fn expect_json(self) -> Result, Error> + where + T: for<'de> Deserialize<'de>, + { + self.assert_json_content_type()?; + + serde_json::from_slice::>(&self.body) + .map_err(|err| Error::bad_api("failed to parse api response", err))? + .check() + } + + fn assert_json_content_type(&self) -> Result<(), Error> { + match self.content_type.as_deref() { + Some("application/json") => Ok(()), + Some(other) => { + return Err(Error::BadApi( + format!("expected json body, got {other}",), + None, + )) + } + None => { + return Err(Error::BadApi( + "expected json body, but no Content-Type was sent".to_string(), + None, + )) + } + } + } + + /// Expect that the API call did *not* return any data in the `data` field. + pub fn nodata(self) -> Result<(), Error> { + let response = serde_json::from_slice::>(&self.body) + .map_err(|err| Error::bad_api("failed to parse api response", err))?; + + if response.data.is_some() { + Err(Error::UnexpectedData) + } else { + response.check()?; + Ok(()) + } + } +} + +/// API responses can have additional *attributes* added to their data. +pub struct ApiResponseData { + pub attribs: HashMap, + pub data: T, +} + +#[derive(serde::Deserialize)] +struct RawApiResponse { + #[serde(default, deserialize_with = "proxmox_login::parse::deserialize_u16")] + status: Option, + message: Option, + #[serde(default, deserialize_with = "proxmox_login::parse::deserialize_bool")] + success: Option, + data: Option, + + #[serde(default)] + errors: HashMap, + + #[serde(default, flatten)] + attribs: HashMap, +} + +impl RawApiResponse { + pub fn check(mut self) -> Result, Error> { + if !self.success.unwrap_or(false) { + let status = http::StatusCode::from_u16(self.status.unwrap_or(400)) + .unwrap_or(http::StatusCode::BAD_REQUEST); + let mut message = self + .message + .take() + .unwrap_or_else(|| "no message provided".to_string()); + for (param, error) in self.errors { + use std::fmt::Write; + let _ = write!(message, "\n{param}: {error}"); + } + + return Err(Error::api(status, message)); + } + + Ok(ApiResponseData { + data: self + .data + .ok_or_else(|| Error::BadApi("api returned no data".to_string(), None))?, + attribs: self.attribs, + }) + } }