rs_dapi_client/
dump.rs

1//! Dumping of requests and responses to disk
2
3use dapi_grpc::mock::Mockable;
4
5use crate::{
6    mock::{Key, MockResult},
7    transport::TransportRequest,
8    DapiClient,
9};
10use std::{any::type_name, path::PathBuf};
11
12/// Data format of dumps created with [DapiClient::dump_dir].
13#[derive(Clone)]
14pub struct DumpData<T: TransportRequest> {
15    /// Request that was sent to DAPI.
16    pub serialized_request: Vec<u8>,
17    /// Response that was received from DAPI.
18    pub serialized_response: Vec<u8>,
19
20    phantom: std::marker::PhantomData<T>,
21}
22impl<T: TransportRequest> DumpData<T> {
23    /// Return deserialized request
24    pub fn deserialize(&self) -> (T, MockResult<T>) {
25        let req = T::mock_deserialize(&self.serialized_request).unwrap_or_else(|| {
26            panic!(
27                "unable to deserialize mock data of type {}",
28                type_name::<T>()
29            )
30        });
31        let resp =
32            <MockResult<T>>::mock_deserialize(&self.serialized_response).unwrap_or_else(|| {
33                panic!(
34                    "unable to deserialize mock data of type {}",
35                    type_name::<T::Response>()
36                )
37            });
38
39        (req, resp)
40    }
41}
42
43impl<T: TransportRequest> dapi_grpc::mock::Mockable for DumpData<T>
44where
45    T: Mockable,
46    T::Response: Mockable,
47{
48    // We use null-delimited JSON as a format for dump data to make it readable.
49    fn mock_serialize(&self) -> Option<Vec<u8>> {
50        // nulls are not allowed in serialized data as we use it as a delimiter
51        if self.serialized_request.contains(&0) {
52            panic!("null byte in serialized request");
53        }
54        if self.serialized_response.contains(&0) {
55            panic!("null byte in serialized response");
56        }
57
58        let data = [
59            &self.serialized_request,
60            "\n\0\n".as_bytes(),
61            &self.serialized_response,
62        ]
63        .concat();
64
65        Some(data)
66    }
67
68    fn mock_deserialize(buf: &[u8]) -> Option<Self> {
69        // we panic as we expect this to be called only with data serialized by mock_serialize()
70
71        // Split data into request and response
72        let buf = buf.split(|&b| b == 0).collect::<Vec<_>>();
73        if buf.len() != 2 {
74            panic!("invalid mock data format, expected exactly two items separated by null byte");
75        }
76
77        let request = buf.first().expect("missing request in mock data");
78        let response = buf.last().expect("missing response in mock data");
79
80        Some(Self {
81            serialized_request: request.to_vec(),
82            serialized_response: response.to_vec(),
83            phantom: std::marker::PhantomData,
84        })
85    }
86}
87
88impl<T: TransportRequest> DumpData<T> {
89    /// Create new dump data.
90    pub fn new(request: &T, response: &MockResult<T>) -> Self {
91        let request = request
92            .mock_serialize()
93            .expect("unable to serialize request");
94        let response = response
95            .mock_serialize()
96            .expect("unable to serialize response");
97
98        Self {
99            serialized_request: request,
100            serialized_response: response,
101            phantom: std::marker::PhantomData,
102        }
103    }
104
105    // Return request type (T) name without module prefix
106    fn request_type() -> String {
107        let req_type = std::any::type_name::<T>();
108        req_type.rsplit(':').next().unwrap_or(req_type).to_string()
109    }
110    /// Generate unique filename for this dump.
111    ///
112    /// Filename consists of:
113    ///
114    /// * [DapiClient::DUMP_FILE_PREFIX]
115    /// * basename of the type of request, like `GetIdentityRequest`
116    /// * unique identifier (hash) of the request
117    pub fn filename(&self) -> Result<String, std::io::Error> {
118        let key = Key::try_new(&self.serialized_request)?;
119        // get request type without underscores (which we use as a file name separator)
120        let request_type = Self::request_type().replace('_', "-");
121
122        let file = format!(
123            "{}_{}_{}.json",
124            DapiClient::DUMP_FILE_PREFIX,
125            request_type,
126            key
127        );
128
129        Ok(file)
130    }
131
132    /// Load dump data from file.
133    pub fn load<P: AsRef<std::path::Path>>(file: P) -> Result<Self, std::io::Error>
134    where
135        T: Mockable,
136        T::Response: Mockable,
137    {
138        let data = std::fs::read(file)?;
139
140        Self::mock_deserialize(&data).ok_or(std::io::Error::new(
141            std::io::ErrorKind::InvalidData,
142            format!(
143                "unable to deserialize mock data of type {}",
144                type_name::<T>()
145            ),
146        ))
147    }
148
149    /// Save dump data to file.
150    pub fn save(&self, file: &std::path::Path) -> Result<(), std::io::Error>
151    where
152        T: Mockable,
153        T::Response: Mockable,
154    {
155        let encoded = self.mock_serialize().ok_or(std::io::Error::new(
156            std::io::ErrorKind::InvalidData,
157            format!("unable to serialize mock data of type {}", type_name::<T>()),
158        ))?;
159
160        std::fs::write(file, encoded)
161    }
162}
163
164impl DapiClient {
165    /// Prefix of dump files.
166    pub const DUMP_FILE_PREFIX: &'static str = "msg";
167
168    /// Define directory where dumps of all traffic will be saved.
169    ///
170    /// Each request and response pair will be saved to a JSON file in `dump_dir`.
171    /// Data is saved as [DumpData] structure.
172    /// Any errors are logged on `warn` level and ignored.
173    ///
174    /// Dump file name is generated by [DumpData::filename()].
175    ///
176    /// Useful for debugging and mocking.
177    /// See also [MockDapiClient::load()](crate::mock::MockDapiClient::load()).
178    pub fn dump_dir(mut self, dump_dir: Option<PathBuf>) -> Self {
179        self.dump_dir = dump_dir;
180
181        self
182    }
183
184    /// Save dump of request and response to disk.
185    ///
186    /// Any errors are logged on `warn` level and ignored.
187    pub(crate) fn dump_request_response<R: TransportRequest>(
188        request: &R,
189        response: &MockResult<R>,
190        dump_dir: Option<PathBuf>,
191    ) where
192        <R as TransportRequest>::Response: Mockable,
193    {
194        let path = match dump_dir {
195            Some(p) => p,
196            None => return,
197        };
198
199        let data = DumpData::new(request, response);
200
201        // Construct file name
202        let filename = match data.filename() {
203            Ok(f) => f,
204            Err(e) => return tracing::warn!("unable to create dump file name: {}", e),
205        };
206
207        let file = path.join(filename);
208
209        if let Err(e) = data.save(&file) {
210            tracing::warn!("unable to write dump file {:?}: {}", path, e);
211        }
212    }
213}