Skip to main content

fuel_core_client/
reqwest_ext.rs

1use cynic::{
2    GraphQlResponse,
3    Operation,
4    http::CynicReqwestError,
5};
6use fuel_core_types::{
7    blockchain::header::{
8        ConsensusParametersVersion,
9        StateTransitionBytecodeVersion,
10    },
11    fuel_types::BlockHeight,
12};
13use std::{
14    future::Future,
15    marker::PhantomData,
16    pin::Pin,
17};
18
19#[derive(Debug, Clone, serde::Serialize)]
20pub struct ExtensionsRequest {
21    pub required_fuel_block_height: Option<BlockHeight>,
22}
23
24#[derive(Debug, Clone, serde::Deserialize)]
25pub struct ExtensionsResponse {
26    pub required_fuel_block_height: Option<BlockHeight>,
27    pub current_fuel_block_height: Option<BlockHeight>,
28    pub fuel_block_height_precondition_failed: Option<bool>,
29    pub current_stf_version: Option<StateTransitionBytecodeVersion>,
30    pub current_consensus_parameters_version: Option<ConsensusParametersVersion>,
31}
32
33#[derive(Debug, serde::Serialize)]
34pub struct FuelOperation<Operation> {
35    #[serde(flatten)]
36    pub operation: Operation,
37    pub extensions: ExtensionsRequest,
38}
39
40#[derive(Debug, serde::Deserialize)]
41pub struct FuelGraphQlResponse<T, ErrorExtensions = serde::de::IgnoredAny> {
42    #[serde(flatten)]
43    pub response: GraphQlResponse<T, ErrorExtensions>,
44    pub extensions: Option<ExtensionsResponse>,
45}
46
47impl<Operation> FuelOperation<Operation> {
48    pub fn new(
49        operation: Operation,
50        required_fuel_block_height: Option<BlockHeight>,
51    ) -> Self {
52        Self {
53            operation,
54            extensions: ExtensionsRequest {
55                required_fuel_block_height,
56            },
57        }
58    }
59}
60
61#[cfg(not(target_arch = "wasm32"))]
62type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
63
64#[cfg(target_arch = "wasm32")]
65type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
66
67/// An extension trait for reqwest::RequestBuilder.
68///
69/// ```rust,no_run
70/// # mod schema {
71/// #   cynic::use_schema!("../schemas/starwars.schema.graphql");
72/// # }
73/// #
74/// # #[derive(cynic::QueryFragment)]
75/// # #[cynic(
76/// #    schema_path = "../schemas/starwars.schema.graphql",
77/// #    schema_module = "schema",
78/// # )]
79/// # struct Film {
80/// #    title: Option<String>,
81/// #    director: Option<String>
82/// # }
83/// #
84/// # #[derive(cynic::QueryFragment)]
85/// # #[cynic(
86/// #     schema_path = "../schemas/starwars.schema.graphql",
87/// #     schema_module = "schema",
88/// #     graphql_type = "Root"
89/// # )]
90/// # struct FilmDirectorQuery {
91/// #     #[arguments(id = cynic::Id::new("ZmlsbXM6MQ=="))]
92/// #     film: Option<Film>,
93/// # }
94/// use cynic::{http::ReqwestExt, QueryBuilder};
95///
96/// # async move {
97/// let operation = FilmDirectorQuery::build(());
98///
99/// let client = reqwest::Client::new();
100/// let response = client.post("https://swapi-graphql.netlify.app/.netlify/functions/index")
101///     .run_graphql(operation)
102///     .await
103///     .unwrap();
104///
105/// println!(
106///     "The director is {}",
107///     response.data
108///         .and_then(|d| d.film)
109///         .and_then(|f| f.director)
110///         .unwrap()
111/// );
112/// # };
113/// ```
114pub trait ReqwestExt {
115    /// Runs a GraphQL query with the parameters in RequestBuilder, deserializes
116    /// the and returns the result.
117    fn run_fuel_graphql<ResponseData, Vars>(
118        self,
119        operation: FuelOperation<Operation<ResponseData, Vars>>,
120    ) -> CynicReqwestBuilder<ResponseData>
121    where
122        Vars: serde::Serialize,
123        ResponseData: serde::de::DeserializeOwned + 'static;
124}
125
126/// A builder for cynics reqwest integration
127///
128/// Implements `IntoFuture`, users should `.await` the builder or call
129/// `into_future` directly when they're ready to send the request.
130pub struct CynicReqwestBuilder<ResponseData, ErrorExtensions = serde::de::IgnoredAny> {
131    builder: reqwest::RequestBuilder,
132    _marker: std::marker::PhantomData<fn() -> (ResponseData, ErrorExtensions)>,
133}
134
135impl<ResponseData, Errors> CynicReqwestBuilder<ResponseData, Errors> {
136    pub fn new(builder: reqwest::RequestBuilder) -> Self {
137        Self {
138            builder,
139            _marker: std::marker::PhantomData,
140        }
141    }
142}
143
144impl<ResponseData, Errors> IntoFuture for CynicReqwestBuilder<ResponseData, Errors>
145where
146    ResponseData: serde::de::DeserializeOwned + Send + 'static,
147    Errors: serde::de::DeserializeOwned + Send + 'static,
148{
149    type Output = Result<FuelGraphQlResponse<ResponseData, Errors>, anyhow::Error>;
150
151    type IntoFuture = BoxFuture<
152        'static,
153        Result<FuelGraphQlResponse<ResponseData, Errors>, anyhow::Error>,
154    >;
155
156    fn into_future(self) -> Self::IntoFuture {
157        Box::pin(async move {
158            let http_result = self.builder.send().await;
159            deser_gql(http_result).await
160        })
161    }
162}
163
164impl<ResponseData> CynicReqwestBuilder<ResponseData, serde::de::IgnoredAny> {
165    /// Sets the type that will be deserialized for the extensions fields of any errors in the response
166    pub fn retain_extensions<ErrorExtensions>(
167        self,
168    ) -> CynicReqwestBuilder<ResponseData, ErrorExtensions>
169    where
170        ErrorExtensions: serde::de::DeserializeOwned,
171    {
172        let CynicReqwestBuilder { builder, _marker } = self;
173
174        CynicReqwestBuilder {
175            builder,
176            _marker: PhantomData,
177        }
178    }
179}
180
181async fn deser_gql<ResponseData, ErrorExtensions>(
182    response: Result<reqwest::Response, reqwest::Error>,
183) -> Result<FuelGraphQlResponse<ResponseData, ErrorExtensions>, anyhow::Error>
184where
185    ResponseData: serde::de::DeserializeOwned + Send + 'static,
186    ErrorExtensions: serde::de::DeserializeOwned + Send + 'static,
187{
188    let response = match response {
189        Ok(response) => response,
190        Err(e) => return Err(anyhow::anyhow!("{e}")),
191    };
192
193    let status = response.status();
194    if !status.is_success() {
195        let text = response.text().await;
196        let text = match text {
197            Ok(text) => text,
198            Err(e) => return Err(anyhow::anyhow!("{e}")),
199        };
200
201        let Ok(deserred) = serde_json::from_str(&text) else {
202            let error = CynicReqwestError::ErrorResponse(status, text);
203            return Err(anyhow::anyhow!("{error}"));
204        };
205
206        Ok(deserred)
207    } else {
208        let json = response.json().await;
209        json.map_err(|e| anyhow::anyhow!("{e}"))
210    }
211}
212
213impl ReqwestExt for reqwest::RequestBuilder {
214    fn run_fuel_graphql<ResponseData, Vars>(
215        self,
216        operation: FuelOperation<Operation<ResponseData, Vars>>,
217    ) -> CynicReqwestBuilder<ResponseData>
218    where
219        Vars: serde::Serialize,
220        ResponseData: serde::de::DeserializeOwned + 'static,
221    {
222        CynicReqwestBuilder::new(self.json(&operation))
223    }
224}