Skip to main content

roboplc_rpc/tools/
http.rs

1use core::fmt;
2use std::collections::BTreeMap;
3
4use http::{header, StatusCode};
5use serde::{de::DeserializeOwned, Serialize};
6use serde_json::{json, Value};
7
8/// HTTP tools error type
9#[derive(thiserror::Error, Debug)]
10pub enum Error {
11    /// Serialization error
12    #[error("pack error: {0}")]
13    Serialization(#[from] serde_json::Error),
14    /// Invalid data
15    #[error("invalid data: {0}")]
16    InvalidData(String),
17}
18
19use crate::{request::Request, response::Response};
20
21/// Query string representation of a JSON-RPC request,
22/// as: `i=1&m=method&param1=value1&param2=value2`, where id is optional
23///
24/// Booleans ("true"/"false"), numbers and "null" are parsed automatically,
25#[derive(Debug)]
26pub struct QueryString(String);
27
28impl QueryString {
29    /// Create a new query string from a string
30    pub fn new(s: &str) -> Self {
31        QueryString(s.to_owned())
32    }
33}
34
35impl From<String> for QueryString {
36    fn from(s: String) -> Self {
37        QueryString(s)
38    }
39}
40
41impl fmt::Display for QueryString {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        write!(f, "{}", self.0)
44    }
45}
46
47impl From<QueryString> for String {
48    fn from(qs: QueryString) -> Self {
49        qs.0
50    }
51}
52
53impl AsRef<str> for QueryString {
54    fn as_ref(&self) -> &str {
55        &self.0
56    }
57}
58
59impl<M: Serialize> TryFrom<Request<M>> for QueryString {
60    type Error = Error;
61
62    fn try_from(req: Request<M>) -> Result<Self, Self::Error> {
63        request_into_query_string(&req).map(QueryString)
64    }
65}
66
67impl<M: DeserializeOwned + Serialize> TryFrom<QueryString> for Request<M> {
68    type Error = Error;
69
70    fn try_from(qs: QueryString) -> Result<Self, Self::Error> {
71        request_from_query_string(&qs.0)
72    }
73}
74
75fn parse_string(s: &str) -> Value {
76    if s == "true" {
77        Value::Bool(true)
78    } else if s == "false" {
79        Value::Bool(false)
80    } else if s == "null" {
81        Value::Null
82    } else if let Ok(n) = s.parse::<u64>() {
83        Value::Number(n.into())
84    } else if let Ok(n) = s.parse::<i64>() {
85        Value::Number(n.into())
86    } else if let Ok(n) = s.parse::<f64>() {
87        Value::Number(serde_json::value::Number::from_f64(n).unwrap())
88    } else {
89        Value::String(s.to_string())
90    }
91}
92
93fn request_from_query_string<M: DeserializeOwned + Serialize>(
94    qs: &str,
95) -> Result<Request<M>, Error> {
96    let mut id: Option<Value> = None;
97    let mut method: Option<String> = None;
98    let mut params: BTreeMap<String, Value> = BTreeMap::new();
99    for (i, (name, value)) in url::form_urlencoded::parse(qs.as_bytes())
100        .into_iter()
101        .enumerate()
102    {
103        match name.as_ref() {
104            "i" if i == 0 => {
105                id = Some(serde_json::from_str(&value)?);
106            }
107            "m" if method.is_none() => {
108                method = Some(value.to_string());
109            }
110            n => {
111                params.insert(n.to_string(), parse_string(&value));
112            }
113        }
114    }
115    let method_name = method.ok_or(Error::InvalidData("the method is missing".into()))?;
116    #[cfg(feature = "canonical")]
117    let method = serde_json::from_value(json!({
118        "method": method_name,
119        "params": params,
120    }))?;
121    #[cfg(not(feature = "canonical"))]
122    let method = serde_json::from_value(json!({
123        "m": method_name,
124        "p": params,
125    }))?;
126    if let Some(id) = id {
127        Ok(Request::new(id, method))
128    } else {
129        Ok(Request::new0(method))
130    }
131}
132
133fn value_to_string(field: &str, value: &Value) -> Result<String, Error> {
134    Ok(match value {
135        Value::Null => "null".to_string(),
136        Value::Bool(b) => b.to_string(),
137        Value::Number(n) => n.to_string(),
138        Value::String(s) => s.to_string(),
139        _ => {
140            return Err(Error::InvalidData(format!(
141                "unsupported value type for field '{}'",
142                field
143            )))
144        }
145    })
146}
147
148fn request_into_query_string<M: Serialize>(req: &Request<M>) -> Result<String, Error> {
149    let mut pairs = Vec::new();
150    if let Some(id) = &req.id {
151        pairs.push(("i", id.to_string()));
152    }
153    let req_value = serde_json::to_value(&req.method)?;
154    let req_map = req_value
155        .as_object()
156        .ok_or(Error::InvalidData("invalid request".into()))?;
157    let method = req_map
158        .get("method")
159        .ok_or(Error::InvalidData("method is missing".into()))?;
160    pairs.push((
161        "m",
162        method
163            .as_str()
164            .ok_or(Error::InvalidData("invalid method name".into()))?
165            .to_string(),
166    ));
167    if let Some(params) = req_map.get("params") {
168        let params = params
169            .as_object()
170            .ok_or(Error::InvalidData("params must be object".into()))?;
171        for (name, value) in params {
172            pairs.push((name, value_to_string(name, value)?));
173        }
174    }
175    Ok(url::form_urlencoded::Serializer::new(String::new())
176        .extend_pairs(pairs)
177        .finish())
178}
179
180#[derive(Debug)]
181#[allow(clippy::module_name_repetitions)]
182/// A minimalistic HTTP response (no JSON RPC version, call id is placed to `X-JSONRPC-ID` header)
183pub struct HttpResponse {
184    status: http::StatusCode,
185    headers: http::header::HeaderMap,
186    body: String,
187}
188
189impl HttpResponse {
190    /// HTTP status code (200 for success, 500 for error)
191    pub fn status(&self) -> http::StatusCode {
192        self.status
193    }
194    /// HTTP headers
195    pub fn headers(&self) -> &http::header::HeaderMap {
196        &self.headers
197    }
198    /// HTTP body
199    pub fn body(&self) -> &str {
200        &self.body
201    }
202    /// Mutable reference to HTTP headers
203    pub fn headers_mut(&mut self) -> &mut http::header::HeaderMap {
204        &mut self.headers
205    }
206    /// Split the response into parts
207    pub fn into_parts(self) -> (http::StatusCode, http::header::HeaderMap, String) {
208        (self.status, self.headers, self.body)
209    }
210}
211
212impl<R: Serialize> TryFrom<Response<R>> for HttpResponse {
213    type Error = Error;
214
215    fn try_from(response: Response<R>) -> Result<Self, Self::Error> {
216        let (id, res) = response.into_parts();
217        let status = if res.is_ok() {
218            StatusCode::OK
219        } else {
220            StatusCode::INTERNAL_SERVER_ERROR
221        };
222        let mut headers = header::HeaderMap::new();
223        headers.insert(
224            header::CONTENT_TYPE,
225            header::HeaderValue::from_static("application/json"),
226        );
227        headers.insert(
228            "X-JSONRPC-ID",
229            value_to_string("", &id)?.parse().map_err(|e| {
230                Error::InvalidData(format!("failed to parse id as http header: {}", e))
231            })?,
232        );
233        Ok(HttpResponse {
234            status,
235            headers,
236            body: serde_json::to_string(&res)?,
237        })
238    }
239}