vantage_api_client/graphql/
api.rs1use serde::Serialize;
13use serde_json::Value;
14use vantage_core::{Result, error};
15
16use crate::graphql::condition::FilterDialect;
17
18#[derive(Clone, Debug)]
25pub struct GraphqlApi {
26 endpoint: String,
27 client: reqwest::Client,
28 auth_header: Option<String>,
29 pub(crate) dialect: FilterDialect,
30 pub(crate) filter_arg_name: Option<String>,
31}
32
33impl GraphqlApi {
34 pub fn new(endpoint: impl Into<String>) -> Self {
38 GraphqlApi::builder(endpoint).build()
39 }
40
41 pub fn builder(endpoint: impl Into<String>) -> GraphqlApiBuilder {
43 GraphqlApiBuilder::new(endpoint.into())
44 }
45
46 pub fn endpoint(&self) -> &str {
48 &self.endpoint
49 }
50
51 pub fn dialect(&self) -> FilterDialect {
54 self.dialect
55 }
56
57 pub async fn post_graphql(
61 &self,
62 query: &str,
63 variables: &serde_json::Map<String, Value>,
64 ) -> Result<Value> {
65 #[derive(Serialize)]
66 struct Body<'a> {
67 query: &'a str,
68 variables: &'a serde_json::Map<String, Value>,
69 }
70
71 let body = Body { query, variables };
72
73 let mut req = self.client.post(&self.endpoint).json(&body);
74 if let Some(ref auth) = self.auth_header {
75 req = req.header("Authorization", auth);
76 }
77
78 let response = req.send().await.map_err(|e| {
79 error!(
80 "GraphQL request failed",
81 endpoint = self.endpoint.clone(),
82 detail = e.to_string()
83 )
84 })?;
85
86 if !response.status().is_success() {
87 return Err(error!(
88 "GraphQL endpoint returned error status",
89 endpoint = self.endpoint.clone(),
90 status = response.status().as_u16()
91 ));
92 }
93
94 let mut envelope: Value = response.json().await.map_err(|e| {
95 error!(
96 "Failed to parse GraphQL response as JSON",
97 detail = e.to_string()
98 )
99 })?;
100
101 if let Some(errors) = envelope.get("errors")
104 && let Some(arr) = errors.as_array()
105 && !arr.is_empty()
106 {
107 let summary = arr
108 .iter()
109 .filter_map(|e| e.get("message").and_then(|m| m.as_str()))
110 .collect::<Vec<_>>()
111 .join("; ");
112 return Err(error!("GraphQL response carried errors", errors = summary));
113 }
114
115 Ok(envelope
116 .get_mut("data")
117 .map(std::mem::take)
118 .unwrap_or(Value::Null))
119 }
120}
121
122#[derive(Debug, Clone)]
124pub struct GraphqlApiBuilder {
125 endpoint: String,
126 client: Option<reqwest::Client>,
127 auth_header: Option<String>,
128 dialect: FilterDialect,
129 filter_arg_name: Option<String>,
130}
131
132impl GraphqlApiBuilder {
133 pub(crate) fn new(endpoint: String) -> Self {
134 Self {
135 endpoint,
136 client: None,
137 auth_header: None,
138 dialect: FilterDialect::Generic,
139 filter_arg_name: None,
140 }
141 }
142
143 pub fn auth(mut self, auth: impl Into<String>) -> Self {
145 self.auth_header = Some(auth.into());
146 self
147 }
148
149 pub fn client(mut self, client: reqwest::Client) -> Self {
152 self.client = Some(client);
153 self
154 }
155
156 pub fn dialect(mut self, dialect: FilterDialect) -> Self {
159 self.dialect = dialect;
160 self
161 }
162
163 pub fn filter_arg_name(mut self, name: impl Into<String>) -> Self {
166 self.filter_arg_name = Some(name.into());
167 self
168 }
169
170 pub fn build(self) -> GraphqlApi {
171 GraphqlApi {
172 endpoint: self.endpoint,
173 client: self.client.unwrap_or_default(),
174 auth_header: self.auth_header,
175 dialect: self.dialect,
176 filter_arg_name: self.filter_arg_name,
177 }
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn new_keeps_endpoint() {
187 let api = GraphqlApi::new("https://api.spacex.land/graphql/");
188 assert_eq!(api.endpoint(), "https://api.spacex.land/graphql/");
189 }
190
191 #[test]
192 fn builder_sets_auth_without_panicking() {
193 let api = GraphqlApi::builder("https://example.test/graphql")
196 .auth("Bearer abc")
197 .build();
198 assert_eq!(api.endpoint(), "https://example.test/graphql");
199 }
200}