You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

513 lines
16KB

  1. extern crate serde_json;
  2. extern crate toml;
  3. use utils::de::fix_toml_dates;
  4. use utils::fs::{get_file_time, is_path_in_directory, read_file};
  5. use reqwest::{header, Client};
  6. use std::collections::hash_map::DefaultHasher;
  7. use std::fmt;
  8. use std::hash::{Hash, Hasher};
  9. use std::str::FromStr;
  10. use url::Url;
  11. use std::path::PathBuf;
  12. use std::sync::{Arc, Mutex};
  13. use csv::Reader;
  14. use std::collections::HashMap;
  15. use tera::{from_value, to_value, Error, Function as TeraFn, Map, Result, Value};
  16. static GET_DATA_ARGUMENT_ERROR_MESSAGE: &str =
  17. "`load_data`: requires EITHER a `path` or `url` argument";
  18. enum DataSource {
  19. Url(Url),
  20. Path(PathBuf),
  21. }
  22. #[derive(Debug)]
  23. enum OutputFormat {
  24. Toml,
  25. Json,
  26. Csv,
  27. Plain,
  28. }
  29. impl fmt::Display for OutputFormat {
  30. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  31. fmt::Debug::fmt(self, f)
  32. }
  33. }
  34. impl Hash for OutputFormat {
  35. fn hash<H: Hasher>(&self, state: &mut H) {
  36. self.to_string().hash(state);
  37. }
  38. }
  39. impl FromStr for OutputFormat {
  40. type Err = Error;
  41. fn from_str(output_format: &str) -> Result<Self> {
  42. match output_format {
  43. "toml" => Ok(OutputFormat::Toml),
  44. "csv" => Ok(OutputFormat::Csv),
  45. "json" => Ok(OutputFormat::Json),
  46. "plain" => Ok(OutputFormat::Plain),
  47. format => Err(format!("Unknown output format {}", format).into()),
  48. }
  49. }
  50. }
  51. impl OutputFormat {
  52. fn as_accept_header(&self) -> header::HeaderValue {
  53. header::HeaderValue::from_static(match self {
  54. OutputFormat::Json => "application/json",
  55. OutputFormat::Csv => "text/csv",
  56. OutputFormat::Toml => "application/toml",
  57. OutputFormat::Plain => "text/plain",
  58. })
  59. }
  60. }
  61. impl DataSource {
  62. fn from_args(
  63. path_arg: Option<String>,
  64. url_arg: Option<String>,
  65. content_path: &PathBuf,
  66. ) -> Result<Self> {
  67. if path_arg.is_some() && url_arg.is_some() {
  68. return Err(GET_DATA_ARGUMENT_ERROR_MESSAGE.into());
  69. }
  70. if let Some(path) = path_arg {
  71. let full_path = content_path.join(path);
  72. if !full_path.exists() {
  73. return Err(format!("{} doesn't exist", full_path.display()).into());
  74. }
  75. return Ok(DataSource::Path(full_path));
  76. }
  77. if let Some(url) = url_arg {
  78. return Url::parse(&url)
  79. .map(DataSource::Url)
  80. .map_err(|e| format!("Failed to parse {} as url: {}", url, e).into());
  81. }
  82. Err(GET_DATA_ARGUMENT_ERROR_MESSAGE.into())
  83. }
  84. fn get_cache_key(&self, format: &OutputFormat) -> u64 {
  85. let mut hasher = DefaultHasher::new();
  86. format.hash(&mut hasher);
  87. self.hash(&mut hasher);
  88. hasher.finish()
  89. }
  90. }
  91. impl Hash for DataSource {
  92. fn hash<H: Hasher>(&self, state: &mut H) {
  93. match self {
  94. DataSource::Url(url) => url.hash(state),
  95. DataSource::Path(path) => {
  96. path.hash(state);
  97. get_file_time(&path).expect("get file time").hash(state);
  98. }
  99. };
  100. }
  101. }
  102. fn get_data_source_from_args(
  103. content_path: &PathBuf,
  104. args: &HashMap<String, Value>,
  105. ) -> Result<DataSource> {
  106. let path_arg = optional_arg!(String, args.get("path"), GET_DATA_ARGUMENT_ERROR_MESSAGE);
  107. let url_arg = optional_arg!(String, args.get("url"), GET_DATA_ARGUMENT_ERROR_MESSAGE);
  108. DataSource::from_args(path_arg, url_arg, content_path)
  109. }
  110. fn read_data_file(base_path: &PathBuf, full_path: PathBuf) -> Result<String> {
  111. if !is_path_in_directory(&base_path, &full_path)
  112. .map_err(|e| format!("Failed to read data file {}: {}", full_path.display(), e))?
  113. {
  114. return Err(format!(
  115. "{} is not inside the base site directory {}",
  116. full_path.display(),
  117. base_path.display()
  118. )
  119. .into());
  120. }
  121. read_file(&full_path).map_err(|e| {
  122. format!("`load_data`: error {} loading file {}", full_path.to_str().unwrap(), e).into()
  123. })
  124. }
  125. fn get_output_format_from_args(
  126. args: &HashMap<String, Value>,
  127. data_source: &DataSource,
  128. ) -> Result<OutputFormat> {
  129. let format_arg = optional_arg!(
  130. String,
  131. args.get("format"),
  132. "`load_data`: `format` needs to be an argument with a string value, being one of the supported `load_data` file types (csv, json, toml)"
  133. );
  134. if let Some(format) = format_arg {
  135. return OutputFormat::from_str(&format);
  136. }
  137. let from_extension = if let DataSource::Path(path) = data_source {
  138. let extension_result: Result<&str> =
  139. path.extension().map(|extension| extension.to_str().unwrap()).ok_or_else(|| {
  140. format!("Could not determine format for {} from extension", path.display()).into()
  141. });
  142. extension_result?
  143. } else {
  144. "plain"
  145. };
  146. OutputFormat::from_str(from_extension)
  147. }
  148. /// A Tera function to load data from a file or from a URL
  149. /// Currently the supported formats are json, toml, csv and plain text
  150. #[derive(Debug)]
  151. pub struct LoadData {
  152. content_path: PathBuf,
  153. base_path: PathBuf,
  154. client: Arc<Mutex<Client>>,
  155. result_cache: Arc<Mutex<HashMap<u64, Value>>>,
  156. }
  157. impl LoadData {
  158. pub fn new(content_path: PathBuf, base_path: PathBuf) -> Self {
  159. let client = Arc::new(Mutex::new(Client::builder().build().expect("reqwest client build")));
  160. let result_cache = Arc::new(Mutex::new(HashMap::new()));
  161. Self { content_path, base_path, client, result_cache }
  162. }
  163. }
  164. impl TeraFn for LoadData {
  165. fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
  166. let data_source = get_data_source_from_args(&self.content_path, &args)?;
  167. let file_format = get_output_format_from_args(&args, &data_source)?;
  168. let cache_key = data_source.get_cache_key(&file_format);
  169. let mut cache = self.result_cache.lock().expect("result cache lock");
  170. let response_client = self.client.lock().expect("response client lock");
  171. if let Some(cached_result) = cache.get(&cache_key) {
  172. return Ok(cached_result.clone());
  173. }
  174. let data = match data_source {
  175. DataSource::Path(path) => read_data_file(&self.base_path, path),
  176. DataSource::Url(url) => {
  177. let mut response = response_client
  178. .get(url.as_str())
  179. .header(header::ACCEPT, file_format.as_accept_header())
  180. .send()
  181. .and_then(|res| res.error_for_status())
  182. .map_err(|e| {
  183. format!(
  184. "Failed to request {}: {}",
  185. url,
  186. e.status().expect("response status")
  187. )
  188. })?;
  189. response
  190. .text()
  191. .map_err(|e| format!("Failed to parse response from {}: {:?}", url, e).into())
  192. }
  193. }?;
  194. let result_value: Result<Value> = match file_format {
  195. OutputFormat::Toml => load_toml(data),
  196. OutputFormat::Csv => load_csv(data),
  197. OutputFormat::Json => load_json(data),
  198. OutputFormat::Plain => to_value(data).map_err(|e| e.into()),
  199. };
  200. if let Ok(data_result) = &result_value {
  201. cache.insert(cache_key, data_result.clone());
  202. }
  203. result_value
  204. }
  205. }
  206. /// Parse a JSON string and convert it to a Tera Value
  207. fn load_json(json_data: String) -> Result<Value> {
  208. let json_content: Value =
  209. serde_json::from_str(json_data.as_str()).map_err(|e| format!("{:?}", e))?;
  210. Ok(json_content)
  211. }
  212. /// Parse a TOML string and convert it to a Tera Value
  213. fn load_toml(toml_data: String) -> Result<Value> {
  214. let toml_content: toml::Value = toml::from_str(&toml_data).map_err(|e| format!("{:?}", e))?;
  215. let toml_value = to_value(toml_content).expect("Got invalid JSON that was valid TOML somehow");
  216. match toml_value {
  217. Value::Object(m) => Ok(fix_toml_dates(m)),
  218. _ => unreachable!("Loaded something other than a TOML object"),
  219. }
  220. }
  221. /// Parse a CSV string and convert it to a Tera Value
  222. ///
  223. /// An example csv file `example.csv` could be:
  224. /// ```csv
  225. /// Number, Title
  226. /// 1,Gutenberg
  227. /// 2,Printing
  228. /// ```
  229. /// The json value output would be:
  230. /// ```json
  231. /// {
  232. /// "headers": ["Number", "Title"],
  233. /// "records": [
  234. /// ["1", "Gutenberg"],
  235. /// ["2", "Printing"]
  236. /// ],
  237. /// }
  238. /// ```
  239. fn load_csv(csv_data: String) -> Result<Value> {
  240. let mut reader = Reader::from_reader(csv_data.as_bytes());
  241. let mut csv_map = Map::new();
  242. {
  243. let hdrs = reader.headers().map_err(|e| {
  244. format!("'load_data': {} - unable to read CSV header line (line 1) for CSV file", e)
  245. })?;
  246. let headers_array = hdrs.iter().map(|v| Value::String(v.to_string())).collect();
  247. csv_map.insert(String::from("headers"), Value::Array(headers_array));
  248. }
  249. {
  250. let records = reader.records();
  251. let mut records_array: Vec<Value> = Vec::new();
  252. for result in records {
  253. let record = match result {
  254. Ok(r) => r,
  255. Err(e) => {
  256. return Err(tera::Error::chain(
  257. String::from("Error encountered when parsing csv records"),
  258. e,
  259. ));
  260. }
  261. };
  262. let mut elements_array: Vec<Value> = Vec::new();
  263. for e in record.into_iter() {
  264. elements_array.push(Value::String(String::from(e)));
  265. }
  266. records_array.push(Value::Array(elements_array));
  267. }
  268. csv_map.insert(String::from("records"), Value::Array(records_array));
  269. }
  270. let csv_value: Value = Value::Object(csv_map);
  271. to_value(csv_value).map_err(|err| err.into())
  272. }
  273. #[cfg(test)]
  274. mod tests {
  275. use super::{DataSource, LoadData, OutputFormat};
  276. use std::collections::HashMap;
  277. use std::path::PathBuf;
  278. use tera::{to_value, Function};
  279. fn get_test_file(filename: &str) -> PathBuf {
  280. let test_files = PathBuf::from("../utils/test-files").canonicalize().unwrap();
  281. return test_files.join(filename);
  282. }
  283. #[test]
  284. fn fails_when_missing_file() {
  285. let static_fn =
  286. LoadData::new(PathBuf::from("../utils/test-files"), PathBuf::from("../utils"));
  287. let mut args = HashMap::new();
  288. args.insert("path".to_string(), to_value("../../../READMEE.md").unwrap());
  289. let result = static_fn.call(&args);
  290. assert!(result.is_err());
  291. assert!(result.unwrap_err().to_string().contains("READMEE.md doesn't exist"));
  292. }
  293. #[test]
  294. fn cant_load_outside_content_dir() {
  295. let static_fn =
  296. LoadData::new(PathBuf::from("../utils/test-files"), PathBuf::from("../utils"));
  297. let mut args = HashMap::new();
  298. args.insert("path".to_string(), to_value("../../../README.md").unwrap());
  299. args.insert("format".to_string(), to_value("plain").unwrap());
  300. let result = static_fn.call(&args);
  301. assert!(result.is_err());
  302. assert!(result
  303. .unwrap_err()
  304. .to_string()
  305. .contains("README.md is not inside the base site directory"));
  306. }
  307. #[test]
  308. fn calculates_cache_key_for_path() {
  309. // We can't test against a fixed value, due to the fact the cache key is built from the absolute path
  310. let cache_key =
  311. DataSource::Path(get_test_file("test.toml")).get_cache_key(&OutputFormat::Toml);
  312. let cache_key_2 =
  313. DataSource::Path(get_test_file("test.toml")).get_cache_key(&OutputFormat::Toml);
  314. assert_eq!(cache_key, cache_key_2);
  315. }
  316. #[test]
  317. fn calculates_cache_key_for_url() {
  318. let cache_key =
  319. DataSource::Url("https://api.github.com/repos/getzola/zola".parse().unwrap())
  320. .get_cache_key(&OutputFormat::Plain);
  321. assert_eq!(cache_key, 8916756616423791754);
  322. }
  323. #[test]
  324. fn different_cache_key_per_filename() {
  325. let toml_cache_key =
  326. DataSource::Path(get_test_file("test.toml")).get_cache_key(&OutputFormat::Toml);
  327. let json_cache_key =
  328. DataSource::Path(get_test_file("test.json")).get_cache_key(&OutputFormat::Toml);
  329. assert_ne!(toml_cache_key, json_cache_key);
  330. }
  331. #[test]
  332. fn different_cache_key_per_format() {
  333. let toml_cache_key =
  334. DataSource::Path(get_test_file("test.toml")).get_cache_key(&OutputFormat::Toml);
  335. let json_cache_key =
  336. DataSource::Path(get_test_file("test.toml")).get_cache_key(&OutputFormat::Json);
  337. assert_ne!(toml_cache_key, json_cache_key);
  338. }
  339. #[test]
  340. fn can_load_remote_data() {
  341. let static_fn = LoadData::new(PathBuf::new(), PathBuf::new());
  342. let mut args = HashMap::new();
  343. args.insert("url".to_string(), to_value("https://httpbin.org/json").unwrap());
  344. args.insert("format".to_string(), to_value("json").unwrap());
  345. let result = static_fn.call(&args).unwrap();
  346. assert_eq!(
  347. result.get("slideshow").unwrap().get("title").unwrap(),
  348. &to_value("Sample Slide Show").unwrap()
  349. );
  350. }
  351. #[test]
  352. fn fails_when_request_404s() {
  353. let static_fn = LoadData::new(PathBuf::new(), PathBuf::new());
  354. let mut args = HashMap::new();
  355. args.insert("url".to_string(), to_value("https://httpbin.org/status/404/").unwrap());
  356. args.insert("format".to_string(), to_value("json").unwrap());
  357. let result = static_fn.call(&args);
  358. assert!(result.is_err());
  359. assert_eq!(
  360. result.unwrap_err().to_string(),
  361. "Failed to request https://httpbin.org/status/404/: 404 Not Found"
  362. );
  363. }
  364. #[test]
  365. fn can_load_toml() {
  366. let static_fn = LoadData::new(
  367. PathBuf::from("../utils/test-files"),
  368. PathBuf::from("../utils/test-files"),
  369. );
  370. let mut args = HashMap::new();
  371. args.insert("path".to_string(), to_value("test.toml").unwrap());
  372. let result = static_fn.call(&args.clone()).unwrap();
  373. // TOML does not load in order
  374. assert_eq!(
  375. result,
  376. json!({
  377. "category": {
  378. "date": "1979-05-27T07:32:00Z",
  379. "key": "value"
  380. },
  381. })
  382. );
  383. }
  384. #[test]
  385. fn can_load_csv() {
  386. let static_fn = LoadData::new(
  387. PathBuf::from("../utils/test-files"),
  388. PathBuf::from("../utils/test-files"),
  389. );
  390. let mut args = HashMap::new();
  391. args.insert("path".to_string(), to_value("test.csv").unwrap());
  392. let result = static_fn.call(&args.clone()).unwrap();
  393. assert_eq!(
  394. result,
  395. json!({
  396. "headers": ["Number", "Title"],
  397. "records": [
  398. ["1", "Gutenberg"],
  399. ["2", "Printing"]
  400. ],
  401. })
  402. )
  403. }
  404. // Test points to bad csv file with uneven row lengths
  405. #[test]
  406. fn bad_csv_should_result_in_error() {
  407. let static_fn = LoadData::new(
  408. PathBuf::from("../utils/test-files"),
  409. PathBuf::from("../utils/test-files"),
  410. );
  411. let mut args = HashMap::new();
  412. args.insert("path".to_string(), to_value("uneven_rows.csv").unwrap());
  413. let result = static_fn.call(&args.clone());
  414. assert!(result.is_err());
  415. let error_kind = result.err().unwrap().kind;
  416. match error_kind {
  417. tera::ErrorKind::Msg(msg) => {
  418. if msg != String::from("Error encountered when parsing csv records") {
  419. panic!("Error message is wrong. Perhaps wrong error is being returned?");
  420. }
  421. }
  422. _ => panic!("Error encountered was not expected CSV error"),
  423. }
  424. }
  425. #[test]
  426. fn can_load_json() {
  427. let static_fn = LoadData::new(
  428. PathBuf::from("../utils/test-files"),
  429. PathBuf::from("../utils/test-files"),
  430. );
  431. let mut args = HashMap::new();
  432. args.insert("path".to_string(), to_value("test.json").unwrap());
  433. let result = static_fn.call(&args.clone()).unwrap();
  434. assert_eq!(
  435. result,
  436. json!({
  437. "key": "value",
  438. "array": [1, 2, 3],
  439. "subpackage": {
  440. "subkey": 5
  441. }
  442. })
  443. )
  444. }
  445. }