rotools 0.3.0

Helpful stuff.
Documentation
use anyhow::anyhow;
use serde::de::value::MapDeserializer;
use serde::Deserialize;
use std::fmt;

pub struct Hasura<T: reqwest::IntoUrl> {
    client: reqwest::Client,
    url: reqwest::Url,
    p1: std::marker::PhantomData<T>,
    code: String,
}

impl<T> Hasura<T>
where
    T: reqwest::IntoUrl,
{
    pub fn new(url: T, code: &str) -> Result<Self, Box<dyn std::error::Error>> {
        //let client = reqwest::Client::new();

        //let req = client
        //.post(graphql_endpoint.as_str())
        //.header("x-hasura-admin-secret", hasura_admin_secret.as_bytes())

        //.post(url)
        //.header("x-hasura-admin-secret", code.as_bytes());
        Ok(Self {
            client: reqwest::Client::new(),
            //url: reqwest::Url::try_from(url).unwrap(),
            url: url.into_url()?,
            p1: std::marker::PhantomData,
            code: code.to_string(),
        })
    }
    pub async fn send<S: serde::Serialize, D: serde::de::DeserializeOwned>(
        &self,
        body: S,
    ) -> Result<D, Box<dyn std::error::Error>> {
        let req = self
            .client
            .post(self.url.clone())
            .header("x-hasura-admin-secret", self.code.as_bytes())
            .json(&body);

        let res = graphql_request(req).await?;

        graphql_parse(res).await.map_err(|e| e.into())
    }
}

async fn graphql_request<D: serde::de::DeserializeOwned>(
    req: reqwest::RequestBuilder,
) -> anyhow::Result<graphql_client::Response<D>> {
    let res = req.send().await?;

    let res_ok = res.error_for_status()?;

    let decode: graphql_client::Response<D> = res_ok.json().await?;

    Ok(decode)
}

async fn graphql_parse<D: serde::de::DeserializeOwned>(
    body: graphql_client::Response<D>,
) -> anyhow::Result<D> {
    match body.data {
        Some(d) => Ok(d),
        None => match body.errors {
            Some(errors) => {
                let xs: Vec<_> = errors.into_iter().map(parse_error).collect();
                let ctx = HasuraErrors { errors: xs };
                Err(anyhow!(ctx))
            }
            None => Err(anyhow!("Hasura: No data or errors")),
        },
    }
}

fn parse_error(e: graphql_client::Error) -> ParsedError {
    let internal = e.extensions.as_ref().and_then(|ext| {
        HasuraInfo::deserialize(MapDeserializer::new(ext.clone().into_iter())).ok()
    });

    ParsedError {
        message: e.message,
        error: internal,
    }
}

impl fmt::Display for HasuraErrors {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}",
            serde_json::to_string_pretty(&self.errors).map_err(|_| fmt::Error)?
        )
    }
}

#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct HasuraErrors {
    pub errors: Vec<ParsedError>,
}

#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct ParsedError {
    pub message: String,
    pub error: Option<HasuraInfo>,
}

// I think for postgres errors
#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct HasuraInternal {
    pub description: Option<String>,
    pub exec_status: String,
    pub hint: Option<String>,
    pub message: String,
    pub status_code: String,
}

// https://github.com/hasura/graphql-engine/blob/b6eb71ae07ed72965db51ed6a15af55f70730324/server/src-lib/Hasura/Base/Error.hs#L178
#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct HasuraInfo {
    pub code: String,
    pub path: String,
    pub error: Option<String>,
    pub internal: Option<HasuraInternalError>,
}

#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct HasuraInternalError {
    pub error: HasuraInternal,
}