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.

241 lines
6.3KB

  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 layout, if we want to specify which tpl to render for that page
  31. #[serde(skip_serializing)]
  32. pub layout: 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. pub fn parse_date(&self) -> Option<NaiveDateTime> {
  58. match self.date {
  59. Some(ref d) => {
  60. if d.contains("T") {
  61. DateTime::parse_from_rfc3339(d).ok().and_then(|s| Some(s.naive_local()))
  62. } else {
  63. NaiveDate::parse_from_str(d, "%Y-%m-%d").ok().and_then(|s| Some(s.and_hms(0,0,0)))
  64. }
  65. },
  66. None => None,
  67. }
  68. }
  69. }
  70. #[cfg(test)]
  71. mod tests {
  72. use super::{FrontMatter};
  73. use tera::to_value;
  74. #[test]
  75. fn test_can_parse_a_valid_front_matter() {
  76. let content = r#"
  77. title = "Hello"
  78. description = "hey there""#;
  79. let res = FrontMatter::parse(content);
  80. println!("{:?}", res);
  81. assert!(res.is_ok());
  82. let res = res.unwrap();
  83. assert_eq!(res.title, "Hello".to_string());
  84. assert_eq!(res.description, "hey there".to_string());
  85. }
  86. #[test]
  87. fn test_can_parse_tags() {
  88. let content = r#"
  89. title = "Hello"
  90. description = "hey there"
  91. slug = "hello-world"
  92. tags = ["rust", "html"]"#;
  93. let res = FrontMatter::parse(content);
  94. assert!(res.is_ok());
  95. let res = res.unwrap();
  96. assert_eq!(res.title, "Hello".to_string());
  97. assert_eq!(res.slug.unwrap(), "hello-world".to_string());
  98. assert_eq!(res.tags.unwrap(), ["rust".to_string(), "html".to_string()]);
  99. }
  100. #[test]
  101. fn test_can_parse_extra_attributes_in_frontmatter() {
  102. let content = r#"
  103. title = "Hello"
  104. description = "hey there"
  105. slug = "hello-world"
  106. [extra]
  107. language = "en"
  108. authors = ["Bob", "Alice"]"#;
  109. let res = FrontMatter::parse(content);
  110. assert!(res.is_ok());
  111. let res = res.unwrap();
  112. assert_eq!(res.title, "Hello".to_string());
  113. assert_eq!(res.slug.unwrap(), "hello-world".to_string());
  114. let extra = res.extra.unwrap();
  115. assert_eq!(extra.get("language").unwrap(), &to_value("en").unwrap());
  116. assert_eq!(
  117. extra.get("authors").unwrap(),
  118. &to_value(["Bob".to_string(), "Alice".to_string()]).unwrap()
  119. );
  120. }
  121. #[test]
  122. fn test_is_ok_with_url_instead_of_slug() {
  123. let content = r#"
  124. title = "Hello"
  125. description = "hey there"
  126. url = "hello-world""#;
  127. let res = FrontMatter::parse(content);
  128. assert!(res.is_ok());
  129. let res = res.unwrap();
  130. assert!(res.slug.is_none());
  131. assert_eq!(res.url.unwrap(), "hello-world".to_string());
  132. }
  133. #[test]
  134. fn test_errors_with_empty_front_matter() {
  135. let content = r#" "#;
  136. let res = FrontMatter::parse(content);
  137. assert!(res.is_err());
  138. }
  139. #[test]
  140. fn test_errors_with_invalid_front_matter() {
  141. let content = r#"title = 1\n"#;
  142. let res = FrontMatter::parse(content);
  143. assert!(res.is_err());
  144. }
  145. #[test]
  146. fn test_errors_with_missing_required_value_front_matter() {
  147. let content = r#"title = """#;
  148. let res = FrontMatter::parse(content);
  149. assert!(res.is_err());
  150. }
  151. #[test]
  152. fn test_errors_on_non_string_tag() {
  153. let content = r#"
  154. title = "Hello"
  155. description = "hey there"
  156. slug = "hello-world"
  157. tags = ["rust", 1]"#;
  158. let res = FrontMatter::parse(content);
  159. assert!(res.is_err());
  160. }
  161. #[test]
  162. fn test_errors_on_present_but_empty_slug() {
  163. let content = r#"
  164. title = "Hello"
  165. description = "hey there"
  166. slug = """#;
  167. let res = FrontMatter::parse(content);
  168. assert!(res.is_err());
  169. }
  170. #[test]
  171. fn test_errors_on_present_but_empty_url() {
  172. let content = r#"
  173. title = "Hello"
  174. description = "hey there"
  175. url = """#;
  176. let res = FrontMatter::parse(content);
  177. assert!(res.is_err());
  178. }
  179. #[test]
  180. fn test_parse_date_yyyy_mm_dd() {
  181. let content = r#"
  182. title = "Hello"
  183. description = "hey there"
  184. date = "2016-10-10""#;
  185. let res = FrontMatter::parse(content).unwrap();
  186. assert!(res.parse_date().is_some());
  187. }
  188. #[test]
  189. fn test_parse_date_rfc3339() {
  190. let content = r#"
  191. title = "Hello"
  192. description = "hey there"
  193. date = "2002-10-02T15:00:00Z""#;
  194. let res = FrontMatter::parse(content).unwrap();
  195. assert!(res.parse_date().is_some());
  196. }
  197. #[test]
  198. fn test_cant_parse_random_date_format() {
  199. let content = r#"
  200. title = "Hello"
  201. description = "hey there"
  202. date = "2002/10/12""#;
  203. let res = FrontMatter::parse(content).unwrap();
  204. assert!(res.parse_date().is_none());
  205. }
  206. }