use serde::Serialize;
use serde_json::Value;
use vantage_core::{Result, error};
use crate::graphql::condition::FilterDialect;
#[derive(Clone, Debug)]
pub struct GraphqlApi {
endpoint: String,
client: reqwest::Client,
auth_header: Option<String>,
pub(crate) dialect: FilterDialect,
pub(crate) filter_arg_name: Option<String>,
}
impl GraphqlApi {
pub fn new(endpoint: impl Into<String>) -> Self {
GraphqlApi::builder(endpoint).build()
}
pub fn builder(endpoint: impl Into<String>) -> GraphqlApiBuilder {
GraphqlApiBuilder::new(endpoint.into())
}
pub fn endpoint(&self) -> &str {
&self.endpoint
}
pub fn dialect(&self) -> FilterDialect {
self.dialect
}
pub async fn post_graphql(
&self,
query: &str,
variables: &serde_json::Map<String, Value>,
) -> Result<Value> {
#[derive(Serialize)]
struct Body<'a> {
query: &'a str,
variables: &'a serde_json::Map<String, Value>,
}
let body = Body { query, variables };
let mut req = self.client.post(&self.endpoint).json(&body);
if let Some(ref auth) = self.auth_header {
req = req.header("Authorization", auth);
}
let response = req.send().await.map_err(|e| {
error!(
"GraphQL request failed",
endpoint = self.endpoint.clone(),
detail = e.to_string()
)
})?;
if !response.status().is_success() {
return Err(error!(
"GraphQL endpoint returned error status",
endpoint = self.endpoint.clone(),
status = response.status().as_u16()
));
}
let mut envelope: Value = response.json().await.map_err(|e| {
error!(
"Failed to parse GraphQL response as JSON",
detail = e.to_string()
)
})?;
if let Some(errors) = envelope.get("errors")
&& let Some(arr) = errors.as_array()
&& !arr.is_empty()
{
let summary = arr
.iter()
.filter_map(|e| e.get("message").and_then(|m| m.as_str()))
.collect::<Vec<_>>()
.join("; ");
return Err(error!("GraphQL response carried errors", errors = summary));
}
Ok(envelope
.get_mut("data")
.map(std::mem::take)
.unwrap_or(Value::Null))
}
}
#[derive(Debug, Clone)]
pub struct GraphqlApiBuilder {
endpoint: String,
client: Option<reqwest::Client>,
auth_header: Option<String>,
dialect: FilterDialect,
filter_arg_name: Option<String>,
}
impl GraphqlApiBuilder {
pub(crate) fn new(endpoint: String) -> Self {
Self {
endpoint,
client: None,
auth_header: None,
dialect: FilterDialect::Generic,
filter_arg_name: None,
}
}
pub fn auth(mut self, auth: impl Into<String>) -> Self {
self.auth_header = Some(auth.into());
self
}
pub fn client(mut self, client: reqwest::Client) -> Self {
self.client = Some(client);
self
}
pub fn dialect(mut self, dialect: FilterDialect) -> Self {
self.dialect = dialect;
self
}
pub fn filter_arg_name(mut self, name: impl Into<String>) -> Self {
self.filter_arg_name = Some(name.into());
self
}
pub fn build(self) -> GraphqlApi {
GraphqlApi {
endpoint: self.endpoint,
client: self.client.unwrap_or_default(),
auth_header: self.auth_header,
dialect: self.dialect,
filter_arg_name: self.filter_arg_name,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_keeps_endpoint() {
let api = GraphqlApi::new("https://api.spacex.land/graphql/");
assert_eq!(api.endpoint(), "https://api.spacex.land/graphql/");
}
#[test]
fn builder_sets_auth_without_panicking() {
let api = GraphqlApi::builder("https://example.test/graphql")
.auth("Bearer abc")
.build();
assert_eq!(api.endpoint(), "https://example.test/graphql");
}
}