sui_graphql_rpc_client/
simple_client.rs1use crate::ClientError;
5use reqwest::Response;
6use reqwest::header;
7use reqwest::header::HeaderValue;
8use serde_json::Value;
9use std::collections::BTreeMap;
10use sui_graphql_rpc_headers::LIMITS_HEADER;
11
12use super::response::GraphqlResponse;
13
14#[derive(Clone, Debug)]
15pub struct GraphqlQueryVariable {
16 pub name: String,
17 pub ty: String,
18 pub value: Value,
19}
20
21#[derive(Clone)]
22pub struct SimpleClient {
23 inner: reqwest::Client,
24 url: String,
25}
26
27impl SimpleClient {
28 pub fn new<S: Into<String>>(base_url: S) -> Self {
29 Self {
30 inner: reqwest::Client::new(),
31 url: base_url.into(),
32 }
33 }
34
35 pub async fn execute(
36 &self,
37 query: String,
38 headers: Vec<(header::HeaderName, header::HeaderValue)>,
39 ) -> Result<serde_json::Value, ClientError> {
40 self.execute_impl(query, vec![], headers, false)
41 .await?
42 .json()
43 .await
44 .map_err(|e| e.into())
45 }
46
47 pub async fn execute_to_graphql(
48 &self,
49 query: String,
50 get_usage: bool,
51 variables: Vec<GraphqlQueryVariable>,
52 mut headers: Vec<(header::HeaderName, header::HeaderValue)>,
53 ) -> Result<GraphqlResponse, ClientError> {
54 if get_usage {
55 headers.push((
56 LIMITS_HEADER.clone().as_str().try_into().unwrap(),
57 HeaderValue::from_static("true"),
58 ));
59 }
60 GraphqlResponse::from_resp(self.execute_impl(query, variables, headers, false).await?).await
61 }
62
63 async fn execute_impl(
64 &self,
65 query: String,
66 variables: Vec<GraphqlQueryVariable>,
67 headers: Vec<(header::HeaderName, header::HeaderValue)>,
68 is_mutation: bool,
69 ) -> Result<Response, ClientError> {
70 let (type_defs, var_vals) = resolve_variables(&variables)?;
71 let body = if type_defs.is_empty() {
72 serde_json::json!({
73 "query": query,
74 })
75 } else {
76 let type_defs_csv = type_defs
78 .iter()
79 .map(|(name, ty)| format!("${}: {}", name, ty))
80 .collect::<Vec<_>>()
81 .join(", ");
82 let query = format!(
83 "{} ({}) {}",
84 if is_mutation { "mutation" } else { "query" },
85 type_defs_csv,
86 query
87 );
88 serde_json::json!({
89 "query": query,
90 "variables": var_vals,
91 })
92 };
93
94 let mut builder = self.inner.post(&self.url).json(&body);
95 for (key, value) in headers {
96 builder = builder.header(key, value);
97 }
98 builder.send().await.map_err(|e| e.into())
99 }
100
101 pub async fn execute_mutation_to_graphql(
102 &self,
103 mutation: String,
104 variables: Vec<GraphqlQueryVariable>,
105 ) -> Result<GraphqlResponse, ClientError> {
106 GraphqlResponse::from_resp(self.execute_impl(mutation, variables, vec![], true).await?)
107 .await
108 }
109
110 pub async fn ping(&self) -> Result<(), ClientError> {
112 self.inner
113 .get(format!("{}/health", self.url))
114 .send()
115 .await?;
116 Ok(())
117 }
118
119 pub fn url(&self) -> String {
120 self.url.clone()
121 }
122}
123
124#[allow(clippy::type_complexity, clippy::result_large_err)]
125pub fn resolve_variables(
126 vars: &[GraphqlQueryVariable],
127) -> Result<(BTreeMap<String, String>, BTreeMap<String, Value>), ClientError> {
128 let mut type_defs: BTreeMap<String, String> = BTreeMap::new();
129 let mut var_vals: BTreeMap<String, Value> = BTreeMap::new();
130
131 for (idx, GraphqlQueryVariable { name, ty, value }) in vars.iter().enumerate() {
132 if !is_valid_variable_name(name) {
133 return Err(ClientError::InvalidVariableName {
134 var_name: name.to_owned(),
135 });
136 }
137 if name.trim().is_empty() {
138 return Err(ClientError::InvalidEmptyItem {
139 item_type: "Variable name".to_owned(),
140 idx,
141 });
142 }
143 if ty.trim().is_empty() {
144 return Err(ClientError::InvalidEmptyItem {
145 item_type: "Variable type".to_owned(),
146 idx,
147 });
148 }
149 if let Some(var_type_prev) = type_defs.get(name) {
150 if var_type_prev != ty {
151 return Err(ClientError::VariableDefinitionConflict {
152 var_name: name.to_owned(),
153 var_type_prev: var_type_prev.to_owned(),
154 var_type_curr: ty.to_owned(),
155 });
156 }
157 if var_vals[name] != *value {
158 return Err(ClientError::VariableValueConflict {
159 var_name: name.to_owned(),
160 var_val_prev: var_vals[name].clone(),
161 var_val_curr: value.clone(),
162 });
163 }
164 }
165 type_defs.insert(name.to_owned(), ty.to_owned());
166 var_vals.insert(name.to_owned(), value.to_owned());
167 }
168
169 Ok((type_defs, var_vals))
170}
171
172pub fn is_valid_variable_name(s: &str) -> bool {
173 let mut cs = s.chars();
174 let Some(fst) = cs.next() else { return false };
175
176 match fst {
177 '_' => if s.len() > 1 {},
178 'a'..='z' | 'A'..='Z' => {}
179 _ => return false,
180 }
181
182 cs.all(|c| matches!(c, '_' | 'a' ..= 'z' | 'A' ..= 'Z' | '0' ..= '9'))
183}