Skip to main content

apimock_server/response/
file_response.rs

1use console::style;
2use hyper::HeaderMap;
3use serde_json::{Map, Value};
4use tokio::task;
5
6use std::{collections::HashMap, fs};
7
8use crate::{
9    constant::CSV_RECORDS_DEFAULT_KEY,
10    json_path_util::resolve_with_json_compatible_extensions,
11    response::{error_response::not_found_response, json_response::json_response},
12    response_handler::ResponseHandler,
13    types::BoxBody,
14};
15
16use super::{
17    error_response::internal_server_error_response,
18    text_response::text_response,
19    util::{
20        binary_content_type, file_extension, json_value_with_jsonpath_key, text_file_content_type,
21    },
22};
23
24pub struct FileResponse {
25    file_path: String,
26    csv_records_key: Option<String>,
27    text_content: Option<String>,
28    binary_content: Option<Vec<u8>>,
29    custom_headers: Option<HashMap<String, Option<String>>>,
30    request_headers: HeaderMap,
31}
32
33impl FileResponse {
34    /// create instance
35    pub fn new(
36        file_path: &str,
37        custom_headers: Option<&HashMap<String, Option<String>>>,
38        request_headers: &HeaderMap,
39    ) -> Self {
40        FileResponse {
41            file_path: file_path.to_owned(),
42            csv_records_key: None,
43            text_content: None,
44            binary_content: None,
45            custom_headers: custom_headers.cloned(),
46            request_headers: request_headers.clone(),
47        }
48    }
49
50    /// create instance
51    pub fn new_with_csv_records_jsonpath(
52        file_path: &str,
53        custom_headers: Option<&HashMap<String, Option<String>>>,
54        csv_records_key: Option<String>,
55        request_headers: &HeaderMap,
56    ) -> Self {
57        let mut ret = FileResponse::new(file_path, custom_headers, request_headers);
58        ret.csv_records_key = csv_records_key;
59        ret
60    }
61
62    /// response from file path
63    pub async fn file_content_response(
64        &mut self,
65    ) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
66        let file_path = match resolve_with_json_compatible_extensions(self.file_path.as_str()) {
67            Some(x) => x,
68            None => {
69                log::warn!(
70                    "{}:\n{} (missing or a directory)",
71                    style("file not found").red(),
72                    self.file_path
73                );
74                return not_found_response(&self.request_headers);
75            }
76        };
77        self.file_path = file_path.clone();
78
79        // read file as text file in non-blocking task
80        let file_path_to_read_text_file = file_path.clone();
81        let content =
82            task::spawn_blocking(move || fs::read_to_string(file_path_to_read_text_file)).await;
83
84        let response = match content {
85            Ok(Ok(content)) => {
86                self.text_content = Some(content);
87                self.text_file_content_response()
88            }
89            Ok(Err(_)) => {
90                // read file as binary in non-blocking task
91                let file_path_to_read_binary = file_path.clone();
92                let content =
93                    task::spawn_blocking(move || fs::read(file_path_to_read_binary)).await;
94                match content {
95                    Ok(Ok(content)) => {
96                        self.binary_content = Some(content);
97                        self.binary_content_type_response()
98                    }
99                    Ok(Err(err)) => {
100                        return internal_server_error_response(
101                            &format!("{}: failed to read file - {}", self.file_path, err),
102                            &self.request_headers,
103                        )
104                    }
105                    Err(err) => {
106                        return internal_server_error_response(
107                            &format!("{}: async task failed - {}", self.file_path, err),
108                            &self.request_headers,
109                        )
110                    }
111                }
112            }
113            Err(err) => {
114                return internal_server_error_response(
115                    &format!("{}: async task failed - {}", self.file_path, err),
116                    &self.request_headers,
117                )
118            }
119        };
120
121        response
122    }
123
124    /// text file response
125    fn text_file_content_response(&self) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
126        match file_extension(self.file_path.as_str()) {
127            Some(ext) => match ext.as_str() {
128                "json" | "json5" => self.json_file_content_response(),
129                "csv" => self.csv_file_content_response(),
130                _ => text_response(
131                    self.text_content.clone().unwrap_or_default().as_str(),
132                    Some(text_file_content_type(ext).as_str()),
133                    None,
134                    &self.request_headers,
135                ),
136            },
137            None => text_response(
138                self.text_content.clone().unwrap_or_default().as_str(),
139                None,
140                None,
141                &self.request_headers,
142            ),
143        }
144    }
145
146    /// json file response
147    fn json_file_content_response(&self) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
148        let json_str = self.text_content.clone().unwrap_or_default();
149        json_response(
150            json_str.as_str(),
151            self.custom_headers.as_ref(),
152            &self.request_headers,
153            self.file_path.as_str(),
154        )
155    }
156
157    /// csv file response
158    fn csv_file_content_response(&self) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
159        let text_content = self.text_content.clone().unwrap_or_default();
160        let mut rdr = csv::ReaderBuilder::new()
161            .has_headers(true)
162            .from_reader(text_content.as_bytes());
163
164        let csv_headers = if let Ok(csv_headers) = rdr.headers() {
165            csv_headers.clone()
166        } else {
167            return internal_server_error_response(
168                &format!("{}: failed to analyze csv headers", self.file_path.as_str()),
169                &self.request_headers,
170            );
171        };
172
173        let rows = rdr
174            .records()
175            .map(|result| {
176                let record = result?;
177                let obj = csv_headers
178                    .iter()
179                    .zip(record.iter())
180                    .map(|(k, v)| (k.to_string(), Value::String(v.to_string())))
181                    .collect::<Map<_, _>>();
182                Ok(Value::Object(obj))
183            })
184            .collect::<Result<Vec<Value>, csv::Error>>();
185
186        match rows {
187            Ok(rows) => {
188                let jsonpath_key = if let Some(csv_records_key) = self.csv_records_key.as_ref() {
189                    csv_records_key.as_str()
190                } else {
191                    CSV_RECORDS_DEFAULT_KEY
192                };
193                let json_value = json_value_with_jsonpath_key(jsonpath_key, Value::from(rows));
194
195                let body = serde_json::to_string(&json_value);
196                match body {
197                    Ok(body) => json_response(
198                        body.as_str(),
199                        self.custom_headers.as_ref(),
200                        &self.request_headers,
201                        self.file_path.as_str(),
202                    ),
203                    Err(err) => internal_server_error_response(
204                        &format!(
205                            "{}: failed to convert csv records to json response - {}",
206                            self.file_path.as_str(),
207                            err
208                        ),
209                        &self.request_headers,
210                    ),
211                }
212            }
213            Err(err) => internal_server_error_response(
214                &format!(
215                    "{}: failed to analyze csv records - {}",
216                    self.file_path.as_str(),
217                    err
218                ),
219                &self.request_headers,
220            ),
221        }
222    }
223
224    /// binary file response
225    fn binary_content_type_response(&self) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
226        let mut response_handler = ResponseHandler::default();
227
228        if let Some(custom_headers) = self.custom_headers.clone() {
229            response_handler = response_handler.with_headers(custom_headers);
230        }
231
232        let content = self.binary_content.clone().unwrap_or_default().to_owned();
233        let content_type = binary_content_type(self.file_path.as_str());
234        response_handler
235            .with_binary_body(content, Some(content_type))
236            .into_response(&self.request_headers)
237    }
238}