iota_graphql_rpc_client/
response.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{collections::BTreeMap, net::SocketAddr};
6
7use async_graphql::{Response, ServerError, Value};
8use iota_graphql_rpc_headers::VERSION_HEADER;
9use reqwest::{
10    Response as ReqwestResponse,
11    header::{HeaderMap, HeaderName},
12};
13use serde_json::json;
14
15use super::ClientError;
16
17#[derive(Debug)]
18pub struct GraphqlResponse {
19    headers: HeaderMap,
20    remote_address: Option<SocketAddr>,
21    http_version: reqwest::Version,
22    status: reqwest::StatusCode,
23    full_response: Response,
24}
25
26impl GraphqlResponse {
27    pub async fn from_resp(resp: ReqwestResponse) -> Result<Self, ClientError> {
28        let headers = resp.headers().clone();
29        let remote_address = resp.remote_addr();
30        let http_version = resp.version();
31        let status = resp.status();
32        let full_response: Response = resp.json().await.map_err(ClientError::InnerClient)?;
33
34        Ok(Self {
35            headers,
36            remote_address,
37            http_version,
38            status,
39            full_response,
40        })
41    }
42
43    #[expect(clippy::result_large_err)]
44    pub fn graphql_version(&self) -> Result<String, ClientError> {
45        Ok(self
46            .headers
47            .get(VERSION_HEADER.as_str())
48            .ok_or(ClientError::ServiceVersionHeaderNotFound)?
49            .to_str()
50            .map_err(|e| ClientError::ServiceVersionHeaderValueInvalidString { error: e })?
51            .to_string())
52    }
53
54    pub fn response_body(&self) -> &Response {
55        &self.full_response
56    }
57
58    pub fn response_body_json(&self) -> serde_json::Value {
59        json!(self.full_response)
60    }
61
62    pub fn response_body_json_pretty(&self) -> String {
63        serde_json::to_string_pretty(&self.full_response).unwrap()
64    }
65
66    pub fn http_status(&self) -> reqwest::StatusCode {
67        self.status
68    }
69
70    pub fn http_version(&self) -> reqwest::Version {
71        self.http_version
72    }
73
74    pub fn http_headers(&self) -> HeaderMap {
75        self.headers.clone()
76    }
77
78    /// Returns the HTTP headers without the `Date` header.
79    /// The `Date` header is removed because it is not deterministic.
80    pub fn http_headers_without_date(&self) -> HeaderMap {
81        let mut headers = self.http_headers().clone();
82        headers.remove(HeaderName::from_static("date"));
83        headers
84    }
85
86    pub fn remote_address(&self) -> Option<SocketAddr> {
87        self.remote_address
88    }
89
90    pub fn errors(&self) -> Vec<ServerError> {
91        self.full_response.errors.clone()
92    }
93
94    #[expect(clippy::result_large_err)]
95    pub fn usage(&self) -> Result<Option<BTreeMap<String, u64>>, ClientError> {
96        Ok(match self.full_response.extensions.get("usage").cloned() {
97            Some(Value::Object(obj)) => Some(
98                obj.into_iter()
99                    .map(|(k, v)| match v {
100                        Value::Number(n) => {
101                            n.as_u64().ok_or(ClientError::InvalidUsageNumber {
102                                usage_name: k.to_string(),
103                                usage_number: n,
104                            })
105                        }
106                        .map(|q| (k.to_string(), q)),
107                        _ => Err(ClientError::InvalidUsageValue {
108                            usage_name: k.to_string(),
109                            usage_value: v,
110                        }),
111                    })
112                    .collect::<Result<BTreeMap<String, u64>, ClientError>>()?,
113            ),
114            _ => None,
115        })
116    }
117}