Skip to main content

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