client: handle response data

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2023-08-08 16:52:11 +02:00
parent 1c96afd0ec
commit ffe908f636
4 changed files with 243 additions and 409 deletions

View File

@ -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<Authentication> for AuthenticationKind {
fn from(auth: Authentication) -> Self {
Self::Ticket(auth)

View File

@ -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<Box<dyn Future<Output = Result<HttpApiResponse, Error>> + 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<Vec<u8>, 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<Option<SecondFactorChallenge>, 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<Vec<u8>, Error> {
@ -276,63 +335,44 @@ async fn read_body(mut body: Body) -> Result<Vec<u8>, Error> {
}
impl HttpApiClient for Client {
type ResponseFuture = ResponseFuture;
type ResponseFuture<'a> =
Pin<Box<dyn Future<Output = Result<HttpApiResponse, Error>> + 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<T>(&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<http::Request<Body>, 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<http::request::Builder, http::request::Builder> {
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<E>(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<Option<SecondFactorChallenge>, 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<ApiResponse<R>, 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<ApiResponse<R>, 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<ApiResponse<R>, 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<ApiResponse<R>, 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<ApiResponse<R>, 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<ApiResponse<R>, 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<ApiResponse<R>, 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<u8>,
content_length: usize,
) -> Result<ApiResponse<R>, 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<u8>,
content_length: usize,
) -> Result<Response<Vec<u8>>, 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<R>(response: Response<Vec<u8>>) -> Result<ApiResponse<R>, 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<R> = 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<T> {
pub data: Option<T>,
pub attribs: HashMap<String, Value>,
}
impl<T> ApiResponse<T> {
pub fn into_data_or_err(mut self) -> Result<T, NoData> {
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<T> {
#[serde(default, deserialize_with = "proxmox_login::parse::deserialize_u16")]
pub status: Option<u16>,
pub message: Option<String>,
#[serde(default, deserialize_with = "proxmox_login::parse::deserialize_bool")]
pub success: Option<bool>,
pub data: Option<T>,
#[serde(default)]
pub errors: HashMap<String, String>,
#[serde(default, flatten)]
pub attribs: HashMap<String, Value>,
}
impl<T> RawApiResponse<T> {
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);
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<http::request::Builder, Error> {
Ok(self.login_auth()?.set_auth_headers(request))
}
}
*/

View File

@ -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<Box<dyn StdError + Send + Sync + 'static>>),
/// 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<T: Display>(status: http::StatusCode, msg: T) -> Self {
Self::Api(status, msg.to_string())
}
pub(crate) fn internal<E>(context: &'static str, err: E) -> Self
where
E: StdError + Send + Sync + 'static,
{
Self::Internal(context, Box::new(err))
}
}
impl From<proxmox_login::error::ResponseError> for Error {
fn from(err: proxmox_login::error::ResponseError) -> Self {
Self::Authentication(err)
}
}
impl Error {
pub(crate) fn bad_api<T, E>(msg: T, err: E) -> Self
where
T: Into<String>,
E: StdError + Send + Sync + 'static,
{
Self::BadApi(msg.into(), Some(Box::new(err)))
}
pub(crate) fn api<T: Into<String>>(status: http::StatusCode, msg: T) -> Self {
Self::Api(status, msg.into())
}
}

View File

@ -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<u8>,
}
/// 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<Output = Result<HttpApiResponse, Error>>;
type ResponseFuture<'a>: Future<Output = Result<HttpApiResponse, Error>> + '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<T>(&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<String>,
pub body: Vec<u8>,
}
impl HttpApiResponse {
/// Expect a JSON response as returend by the `extjs` formatter.
pub fn expect_json<T>(self) -> Result<ApiResponseData<T>, Error>
where
T: for<'de> Deserialize<'de>,
{
self.assert_json_content_type()?;
serde_json::from_slice::<RawApiResponse<T>>(&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::<RawApiResponse<()>>(&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<T> {
pub attribs: HashMap<String, Value>,
pub data: T,
}
#[derive(serde::Deserialize)]
struct RawApiResponse<T> {
#[serde(default, deserialize_with = "proxmox_login::parse::deserialize_u16")]
status: Option<u16>,
message: Option<String>,
#[serde(default, deserialize_with = "proxmox_login::parse::deserialize_bool")]
success: Option<bool>,
data: Option<T>,
#[serde(default)]
errors: HashMap<String, String>,
#[serde(default, flatten)]
attribs: HashMap<String, Value>,
}
impl<T> RawApiResponse<T> {
pub fn check(mut self) -> Result<ApiResponseData<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);
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,
})
}
}