1use crate::error::GraphqlError;
4use crate::fake::FakeGraphql;
5use bytes::Bytes;
6use serde::Serialize;
7use serde_json::{json, Value};
8use sova_core::extend::BoxFuture;
9use std::sync::Arc;
10
11#[derive(Debug, Clone)]
13pub struct GraphqlResponse {
14 pub raw: Value,
15}
16
17impl GraphqlResponse {
18 pub fn data(&self) -> Option<&Value> {
19 self.raw.get("data")
20 }
21
22 pub fn errors(&self) -> Option<&Value> {
23 self.raw.get("errors")
24 }
25
26 pub fn into_data(self) -> Result<Value, GraphqlError> {
28 if let Some(errs) = self.raw.get("errors") {
29 if !errs.is_null() && errs.as_array().map(|a| !a.is_empty()).unwrap_or(true) {
30 return Err(GraphqlError::Graphql(errs.to_string()));
31 }
32 }
33 self.raw
34 .get("data")
35 .cloned()
36 .ok_or_else(|| GraphqlError::Decode("missing data field".into()))
37 }
38}
39
40pub(crate) trait GraphqlTransport: Send + Sync {
41 fn post(&self, url: &str, body: Bytes) -> BoxFuture<Result<Bytes, GraphqlError>>;
42}
43
44struct HttpTransport {
45 client: reqwest::Client,
46}
47
48impl GraphqlTransport for HttpTransport {
49 fn post(&self, url: &str, body: Bytes) -> BoxFuture<Result<Bytes, GraphqlError>> {
50 let client = self.client.clone();
51 let url = url.to_string();
52 Box::pin(async move {
53 let res = client
54 .post(url)
55 .header(http::header::CONTENT_TYPE, "application/json")
56 .body(body)
57 .send()
58 .await
59 .map_err(|e| GraphqlError::Transport(e.to_string()))?;
60 let status = res.status().as_u16();
61 let bytes = res
62 .bytes()
63 .await
64 .map_err(|e| GraphqlError::Transport(e.to_string()))?;
65 if !(200..300).contains(&status) {
66 return Err(GraphqlError::Http {
67 status,
68 body: String::from_utf8_lossy(&bytes).into_owned(),
69 });
70 }
71 Ok(bytes)
72 })
73 }
74}
75
76#[derive(Clone)]
78pub struct GraphQlClient {
79 endpoint: String,
80 transport: Arc<dyn GraphqlTransport>,
81 fake: Option<FakeGraphql>,
82}
83
84impl GraphQlClient {
85 pub fn endpoint(&self) -> &str {
86 &self.endpoint
87 }
88
89 pub fn fake(&self) -> Option<&FakeGraphql> {
90 self.fake.as_ref()
91 }
92
93 pub fn query(&self, query: impl Into<String>) -> PendingGraphql {
94 PendingGraphql {
95 client: self.clone(),
96 query: query.into(),
97 operation_name: None,
98 variables: None,
99 }
100 }
101
102 pub fn mutation(&self, query: impl Into<String>) -> PendingGraphql {
103 self.query(query)
104 }
105
106 pub(crate) fn http(endpoint: impl Into<String>) -> Self {
107 Self {
108 endpoint: endpoint.into(),
109 transport: Arc::new(HttpTransport {
110 client: reqwest::Client::new(),
111 }),
112 fake: None,
113 }
114 }
115
116 pub(crate) fn with_fake(endpoint: impl Into<String>, fake: FakeGraphql) -> Self {
117 Self {
118 endpoint: endpoint.into(),
119 transport: Arc::new(fake.clone()),
120 fake: Some(fake),
121 }
122 }
123
124 pub(crate) async fn execute_raw(
125 &self,
126 query: &str,
127 operation_name: Option<&str>,
128 variables: Option<&Value>,
129 ) -> Result<GraphqlResponse, GraphqlError> {
130 let body = json!({
131 "query": query,
132 "operationName": operation_name,
133 "variables": variables.unwrap_or(&Value::Null),
134 });
135 let bytes = Bytes::from(
136 serde_json::to_vec(&body).map_err(|e| GraphqlError::Decode(e.to_string()))?,
137 );
138 let resp = self.transport.post(&self.endpoint, bytes).await?;
139 let raw: Value =
140 serde_json::from_slice(&resp).map_err(|e| GraphqlError::Decode(e.to_string()))?;
141 Ok(GraphqlResponse { raw })
142 }
143}
144
145pub struct PendingGraphql {
147 client: GraphQlClient,
148 query: String,
149 operation_name: Option<String>,
150 variables: Option<Value>,
151}
152
153impl PendingGraphql {
154 pub fn operation_name(mut self, name: impl Into<String>) -> Self {
155 self.operation_name = Some(name.into());
156 self
157 }
158
159 pub fn variables<V: Serialize>(mut self, vars: V) -> Self {
160 self.variables = Some(serde_json::to_value(vars).unwrap_or(Value::Null));
161 self
162 }
163
164 pub async fn send(self) -> Result<GraphqlResponse, GraphqlError> {
166 self.client
167 .execute_raw(
168 &self.query,
169 self.operation_name.as_deref(),
170 self.variables.as_ref(),
171 )
172 .await
173 }
174
175 pub async fn data(self) -> Result<Value, GraphqlError> {
177 self.send().await?.into_data()
178 }
179
180 pub async fn await_data(self) -> Result<Value, GraphqlError> {
182 self.data().await
183 }
184}