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.

242 lines
6.4KB

  1. use std::collections::HashMap;
  2. use toml;
  3. use tera::Value;
  4. use chrono::prelude::*;
  5. use errors::{Result};
  6. /// The front matter of every page
  7. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
  8. pub struct FrontMatter {
  9. // Mandatory fields
  10. /// <title> of the page
  11. pub title: String,
  12. /// Description that appears when linked, e.g. on twitter
  13. pub description: String,
  14. // Optional stuff
  15. /// Date if we want to order pages (ie blog post)
  16. pub date: Option<String>,
  17. /// The page slug. Will be used instead of the filename if present
  18. /// Can't be an empty string if present
  19. pub slug: Option<String>,
  20. /// The url the page appears at, overrides the slug if set in the front-matter
  21. /// otherwise is set after parsing front matter and sections
  22. /// Can't be an empty string if present
  23. pub url: Option<String>,
  24. /// Tags, not to be confused with categories
  25. pub tags: Option<Vec<String>>,
  26. /// Whether this page is a draft and should be published or not
  27. pub draft: Option<bool>,
  28. /// Only one category allowed
  29. pub category: Option<String>,
  30. /// Optional template, if we want to specify which template to render for that page
  31. #[serde(skip_serializing)]
  32. pub template: Option<String>,
  33. /// Any extra parameter present in the front matter
  34. pub extra: Option<HashMap<String, Value>>,
  35. }
  36. impl FrontMatter {
  37. pub fn parse(toml: &str) -> Result<FrontMatter> {
  38. if toml.trim() == "" {
  39. bail!("Front matter of file is missing");
  40. }
  41. let f: FrontMatter = match toml::from_str(toml) {
  42. Ok(d) => d,
  43. Err(e) => bail!(e),
  44. };
  45. if let Some(ref slug) = f.slug {
  46. if slug == "" {
  47. bail!("`slug` can't be empty if present")
  48. }
  49. }
  50. if let Some(ref url) = f.url {
  51. if url == "" {
  52. bail!("`url` can't be empty if present")
  53. }
  54. }
  55. Ok(f)
  56. }
  57. /// Converts the date in the front matter, which can be in 2 formats, into a NaiveDateTime
  58. pub fn parse_date(&self) -> Option<NaiveDateTime> {
  59. match self.date {
  60. Some(ref d) => {
  61. if d.contains("T") {
  62. DateTime::parse_from_rfc3339(d).ok().and_then(|s| Some(s.naive_local()))
  63. } else {
  64. NaiveDate::parse_from_str(d, "%Y-%m-%d").ok().and_then(|s| Some(s.and_hms(0,0,0)))
  65. }
  66. },
  67. None => None,
  68. }
  69. }
  70. }
  71. #[cfg(test)]
  72. mod tests {
  73. use super::{FrontMatter};
  74. use tera::to_value;
  75. #[test]
  76. fn test_can_parse_a_valid_front_matter() {
  77. let content = r#"
  78. title = "Hello"
  79. description = "hey there""#;
  80. let res = FrontMatter::parse(content);
  81. println!("{:?}", res);
  82. assert!(res.is_ok());
  83. let res = res.unwrap();
  84. assert_eq!(res.title, "Hello".to_string());
  85. assert_eq!(res.description, "hey there".to_string());
  86. }
  87. #[test]
  88. fn test_can_parse_tags() {
  89. let content = r#"
  90. title = "Hello"
  91. description = "hey there"
  92. slug = "hello-world"
  93. tags = ["rust", "html"]"#;
  94. let res = FrontMatter::parse(content);
  95. assert!(res.is_ok());
  96. let res = res.unwrap();
  97. assert_eq!(res.title, "Hello".to_string());
  98. assert_eq!(res.slug.unwrap(), "hello-world".to_string());
  99. assert_eq!(res.tags.unwrap(), ["rust".to_string(), "html".to_string()]);
  100. }
  101. #[test]
  102. fn test_can_parse_extra_attributes_in_frontmatter() {
  103. let content = r#"
  104. title = "Hello"
  105. description = "hey there"
  106. slug = "hello-world"
  107. [extra]
  108. language = "en"
  109. authors = ["Bob", "Alice"]"#;
  110. let res = FrontMatter::parse(content);
  111. assert!(res.is_ok());
  112. let res = res.unwrap();
  113. assert_eq!(res.title, "Hello".to_string());
  114. assert_eq!(res.slug.unwrap(), "hello-world".to_string());
  115. let extra = res.extra.unwrap();
  116. assert_eq!(extra.get("language").unwrap(), &to_value("en").unwrap());
  117. assert_eq!(
  118. extra.get("authors").unwrap(),
  119. &to_value(["Bob".to_string(), "Alice".to_string()]).unwrap()
  120. );
  121. }
  122. #[test]
  123. fn test_is_ok_with_url_instead_of_slug() {
  124. let content = r#"
  125. title = "Hello"
  126. description = "hey there"
  127. url = "hello-world""#;
  128. let res = FrontMatter::parse(content);
  129. assert!(res.is_ok());
  130. let res = res.unwrap();
  131. assert!(res.slug.is_none());
  132. assert_eq!(res.url.unwrap(), "hello-world".to_string());
  133. }
  134. #[test]
  135. fn test_errors_with_empty_front_matter() {
  136. let content = r#" "#;
  137. let res = FrontMatter::parse(content);
  138. assert!(res.is_err());
  139. }
  140. #[test]
  141. fn test_errors_with_invalid_front_matter() {
  142. let content = r#"title = 1\n"#;
  143. let res = FrontMatter::parse(content);
  144. assert!(res.is_err());
  145. }
  146. #[test]
  147. fn test_errors_with_missing_required_value_front_matter() {
  148. let content = r#"title = """#;
  149. let res = FrontMatter::parse(content);
  150. assert!(res.is_err());
  151. }
  152. #[test]
  153. fn test_errors_on_non_string_tag() {
  154. let content = r#"
  155. title = "Hello"
  156. description = "hey there"
  157. slug = "hello-world"
  158. tags = ["rust", 1]"#;
  159. let res = FrontMatter::parse(content);
  160. assert!(res.is_err());
  161. }
  162. #[test]
  163. fn test_errors_on_present_but_empty_slug() {
  164. let content = r#"
  165. title = "Hello"
  166. description = "hey there"
  167. slug = """#;
  168. let res = FrontMatter::parse(content);
  169. assert!(res.is_err());
  170. }
  171. #[test]
  172. fn test_errors_on_present_but_empty_url() {
  173. let content = r#"
  174. title = "Hello"
  175. description = "hey there"
  176. url = """#;
  177. let res = FrontMatter::parse(content);
  178. assert!(res.is_err());
  179. }
  180. #[test]
  181. fn test_parse_date_yyyy_mm_dd() {
  182. let content = r#"
  183. title = "Hello"
  184. description = "hey there"
  185. date = "2016-10-10""#;
  186. let res = FrontMatter::parse(content).unwrap();
  187. assert!(res.parse_date().is_some());
  188. }
  189. #[test]
  190. fn test_parse_date_rfc3339() {
  191. let content = r#"
  192. title = "Hello"
  193. description = "hey there"
  194. date = "2002-10-02T15:00:00Z""#;
  195. let res = FrontMatter::parse(content).unwrap();
  196. assert!(res.parse_date().is_some());
  197. }
  198. #[test]
  199. fn test_cant_parse_random_date_format() {
  200. let content = r#"
  201. title = "Hello"
  202. description = "hey there"
  203. date = "2002/10/12""#;
  204. let res = FrontMatter::parse(content).unwrap();
  205. assert!(res.parse_date().is_none());
  206. }
  207. }