1use std::collections::BTreeMap;
6
7use iota_graphql_rpc_headers::LIMITS_HEADER;
8use reqwest::{Response, header, header::HeaderValue};
9use serde_json::Value;
10
11use super::response::GraphqlResponse;
12use crate::ClientError;
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
120#[expect(clippy::type_complexity, clippy::result_large_err)]
121pub fn resolve_variables(
122 vars: &[GraphqlQueryVariable],
123) -> Result<(BTreeMap<String, String>, BTreeMap<String, Value>), ClientError> {
124 let mut type_defs: BTreeMap<String, String> = BTreeMap::new();
125 let mut var_vals: BTreeMap<String, Value> = BTreeMap::new();
126
127 for (idx, GraphqlQueryVariable { name, ty, value }) in vars.iter().enumerate() {
128 if !is_valid_variable_name(name) {
129 return Err(ClientError::InvalidVariableName {
130 var_name: name.to_owned(),
131 });
132 }
133 if name.trim().is_empty() {
134 return Err(ClientError::InvalidEmptyItem {
135 item_type: "Variable name".to_owned(),
136 idx,
137 });
138 }
139 if ty.trim().is_empty() {
140 return Err(ClientError::InvalidEmptyItem {
141 item_type: "Variable type".to_owned(),
142 idx,
143 });
144 }
145 if let Some(var_type_prev) = type_defs.get(name) {
146 if var_type_prev != ty {
147 return Err(ClientError::VariableDefinitionConflict {
148 var_name: name.to_owned(),
149 var_type_prev: var_type_prev.to_owned(),
150 var_type_curr: ty.to_owned(),
151 });
152 }
153 if var_vals[name] != *value {
154 return Err(ClientError::VariableValueConflict {
155 var_name: name.to_owned(),
156 var_val_prev: var_vals[name].clone(),
157 var_val_curr: value.clone(),
158 });
159 }
160 }
161 type_defs.insert(name.to_owned(), ty.to_owned());
162 var_vals.insert(name.to_owned(), value.to_owned());
163 }
164
165 Ok((type_defs, var_vals))
166}
167
168pub fn is_valid_variable_name(s: &str) -> bool {
169 let mut cs = s.chars();
170 let Some(fst) = cs.next() else { return false };
171
172 match fst {
173 '_' => if s.len() > 1 {},
174 'a'..='z' | 'A'..='Z' => {}
175 _ => return false,
176 }
177
178 cs.all(|c| matches!(c, '_' | 'a' ..= 'z' | 'A' ..= 'Z' | '0' ..= '9'))
179}