use std::default::Default; use std::fmt; use builder::ItemBuilder; use feed::{Attachment, Author}; use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor}; use serde::ser::{Serialize, SerializeStruct, Serializer}; /// Represents the `content_html` and `content_text` attributes of an item #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub enum Content { Html(String), Text(String), Both(String, String), } /// Represents an item in a feed #[derive(Debug, Clone, PartialEq)] pub struct Item { pub id: String, pub url: Option, pub external_url: Option, pub title: Option, pub content: Content, pub summary: Option, pub image: Option, pub banner_image: Option, pub date_published: Option, // todo DateTime objects? pub date_modified: Option, pub author: Option, pub tags: Option>, pub attachments: Option>, } impl Item { pub fn builder() -> ItemBuilder { ItemBuilder::new() } } impl Default for Item { fn default() -> Item { Item { id: "".to_string(), url: None, external_url: None, title: None, content: Content::Text("".into()), summary: None, image: None, banner_image: None, date_published: None, date_modified: None, author: None, tags: None, attachments: None, } } } impl Serialize for Item { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut state = serializer.serialize_struct("Item", 14)?; state.serialize_field("id", &self.id)?; if self.url.is_some() { state.serialize_field("url", &self.url)?; } if self.external_url.is_some() { state.serialize_field("external_url", &self.external_url)?; } if self.title.is_some() { state.serialize_field("title", &self.title)?; } match self.content { Content::Html(ref s) => { state.serialize_field("content_html", s)?; state.serialize_field("content_text", &None::>)?; } Content::Text(ref s) => { state.serialize_field("content_html", &None::>)?; state.serialize_field("content_text", s)?; } Content::Both(ref s, ref t) => { state.serialize_field("content_html", s)?; state.serialize_field("content_text", t)?; } }; if self.summary.is_some() { state.serialize_field("summary", &self.summary)?; } if self.image.is_some() { state.serialize_field("image", &self.image)?; } if self.banner_image.is_some() { state.serialize_field("banner_image", &self.banner_image)?; } if self.date_published.is_some() { state.serialize_field("date_published", &self.date_published)?; } if self.date_modified.is_some() { state.serialize_field("date_modified", &self.date_modified)?; } if self.author.is_some() { state.serialize_field("author", &self.author)?; } if self.tags.is_some() { state.serialize_field("tags", &self.tags)?; } if self.attachments.is_some() { state.serialize_field("attachments", &self.attachments)?; } state.end() } } impl<'de> Deserialize<'de> for Item { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { enum Field { Id, Url, ExternalUrl, Title, ContentHtml, ContentText, Summary, Image, BannerImage, DatePublished, DateModified, Author, Tags, Attachments, } impl<'de> Deserialize<'de> for Field { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct FieldVisitor; impl<'de> Visitor<'de> for FieldVisitor { type Value = Field; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("non-expected field") } fn visit_str(self, value: &str) -> Result where E: de::Error, { match value { "id" => Ok(Field::Id), "url" => Ok(Field::Url), "external_url" => Ok(Field::ExternalUrl), "title" => Ok(Field::Title), "content_html" => Ok(Field::ContentHtml), "content_text" => Ok(Field::ContentText), "summary" => Ok(Field::Summary), "image" => Ok(Field::Image), "banner_image" => Ok(Field::BannerImage), "date_published" => Ok(Field::DatePublished), "date_modified" => Ok(Field::DateModified), "author" => Ok(Field::Author), "tags" => Ok(Field::Tags), "attachments" => Ok(Field::Attachments), _ => Err(de::Error::unknown_field(value, FIELDS)), } } } deserializer.deserialize_identifier(FieldVisitor) } } struct ItemVisitor; impl<'de> Visitor<'de> for ItemVisitor { type Value = Item; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("non-expected thing") } fn visit_map(self, mut map: V) -> Result where V: MapAccess<'de>, { let mut id = None; let mut url = None; let mut external_url = None; let mut title = None; let mut content_html: Option = None; let mut content_text: Option = None; let mut summary = None; let mut image = None; let mut banner_image = None; let mut date_published = None; let mut date_modified = None; let mut author = None; let mut tags = None; let mut attachments = None; while let Some(key) = map.next_key()? { match key { Field::Id => { if id.is_some() { return Err(de::Error::duplicate_field("id")); } id = Some(map.next_value()?); } Field::Url => { if url.is_some() { return Err(de::Error::duplicate_field("url")); } url = map.next_value()?; } Field::ExternalUrl => { if external_url.is_some() { return Err(de::Error::duplicate_field("external_url")); } external_url = map.next_value()?; } Field::Title => { if title.is_some() { return Err(de::Error::duplicate_field("title")); } title = map.next_value()?; } Field::ContentHtml => { if content_html.is_some() { return Err(de::Error::duplicate_field("content_html")); } content_html = map.next_value()?; } Field::ContentText => { if content_text.is_some() { return Err(de::Error::duplicate_field("content_text")); } content_text = map.next_value()?; } Field::Summary => { if summary.is_some() { return Err(de::Error::duplicate_field("summary")); } summary = map.next_value()?; } Field::Image => { if image.is_some() { return Err(de::Error::duplicate_field("image")); } image = map.next_value()?; } Field::BannerImage => { if banner_image.is_some() { return Err(de::Error::duplicate_field("banner_image")); } banner_image = map.next_value()?; } Field::DatePublished => { if date_published.is_some() { return Err(de::Error::duplicate_field("date_published")); } date_published = map.next_value()?; } Field::DateModified => { if date_modified.is_some() { return Err(de::Error::duplicate_field("date_modified")); } date_modified = map.next_value()?; } Field::Author => { if author.is_some() { return Err(de::Error::duplicate_field("author")); } author = map.next_value()?; } Field::Tags => { if tags.is_some() { return Err(de::Error::duplicate_field("tags")); } tags = map.next_value()?; } Field::Attachments => { if attachments.is_some() { return Err(de::Error::duplicate_field("attachments")); } attachments = map.next_value()?; } } } let id = id.ok_or_else(|| de::Error::missing_field("id"))?; let content = match (content_html, content_text) { (Some(s), Some(t)) => Content::Both(s.to_string(), t.to_string()), (Some(s), _) => Content::Html(s.to_string()), (_, Some(t)) => Content::Text(t.to_string()), _ => return Err(de::Error::missing_field("content_html or content_text")), }; Ok(Item { id, url, external_url, title, content, summary, image, banner_image, date_published, date_modified, author, tags, attachments, }) } } const FIELDS: &'static [&'static str] = &[ "id", "url", "external_url", "title", "content", "summary", "image", "banner_image", "date_published", "date_modified", "author", "tags", "attachments", ]; deserializer.deserialize_struct("Item", FIELDS, ItemVisitor) } } #[cfg(test)] mod tests { use super::*; use feed::Author; use serde_json; #[test] #[allow(non_snake_case)] fn serialize_item__content_html() { let item = Item { id: "1".into(), url: Some("http://example.com/feed.json".into()), external_url: Some("http://example.com/feed.json".into()), title: Some("feed title".into()), content: Content::Html("

content

".into()), summary: Some("feed summary".into()), image: Some("http://img.com/blah".into()), banner_image: Some("http://img.com/blah".into()), date_published: Some("2017-01-01 10:00:00".into()), date_modified: Some("2017-01-01 10:00:00".into()), author: Some( Author::new() .name("bob jones") .url("http://example.com") .avatar("http://img.com/blah"), ), tags: Some(vec!["json".into(), "feed".into()]), attachments: Some(vec![]), }; assert_eq!( serde_json::to_string(&item).unwrap(), r#"{"id":"1","url":"http://example.com/feed.json","external_url":"http://example.com/feed.json","title":"feed title","content_html":"

content

","content_text":null,"summary":"feed summary","image":"http://img.com/blah","banner_image":"http://img.com/blah","date_published":"2017-01-01 10:00:00","date_modified":"2017-01-01 10:00:00","author":{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"},"tags":["json","feed"],"attachments":[]}"# ); } #[test] #[allow(non_snake_case)] fn serialize_item__content_text() { let item = Item { id: "1".into(), url: Some("http://example.com/feed.json".into()), external_url: Some("http://example.com/feed.json".into()), title: Some("feed title".into()), content: Content::Text("content".into()), summary: Some("feed summary".into()), image: Some("http://img.com/blah".into()), banner_image: Some("http://img.com/blah".into()), date_published: Some("2017-01-01 10:00:00".into()), date_modified: Some("2017-01-01 10:00:00".into()), author: Some( Author::new() .name("bob jones") .url("http://example.com") .avatar("http://img.com/blah"), ), tags: Some(vec!["json".into(), "feed".into()]), attachments: Some(vec![]), }; assert_eq!( serde_json::to_string(&item).unwrap(), r#"{"id":"1","url":"http://example.com/feed.json","external_url":"http://example.com/feed.json","title":"feed title","content_html":null,"content_text":"content","summary":"feed summary","image":"http://img.com/blah","banner_image":"http://img.com/blah","date_published":"2017-01-01 10:00:00","date_modified":"2017-01-01 10:00:00","author":{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"},"tags":["json","feed"],"attachments":[]}"# ); } #[test] #[allow(non_snake_case)] fn serialize_item__content_both() { let item = Item { id: "1".into(), url: Some("http://example.com/feed.json".into()), external_url: Some("http://example.com/feed.json".into()), title: Some("feed title".into()), content: Content::Both("

content

".into(), "content".into()), summary: Some("feed summary".into()), image: Some("http://img.com/blah".into()), banner_image: Some("http://img.com/blah".into()), date_published: Some("2017-01-01 10:00:00".into()), date_modified: Some("2017-01-01 10:00:00".into()), author: Some( Author::new() .name("bob jones") .url("http://example.com") .avatar("http://img.com/blah"), ), tags: Some(vec!["json".into(), "feed".into()]), attachments: Some(vec![]), }; assert_eq!( serde_json::to_string(&item).unwrap(), r#"{"id":"1","url":"http://example.com/feed.json","external_url":"http://example.com/feed.json","title":"feed title","content_html":"

content

","content_text":"content","summary":"feed summary","image":"http://img.com/blah","banner_image":"http://img.com/blah","date_published":"2017-01-01 10:00:00","date_modified":"2017-01-01 10:00:00","author":{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"},"tags":["json","feed"],"attachments":[]}"# ); } #[test] #[allow(non_snake_case)] fn deserialize_item__content_html() { let json = r#"{"id":"1","url":"http://example.com/feed.json","external_url":"http://example.com/feed.json","title":"feed title","content_html":"

content

","content_text":null,"summary":"feed summary","image":"http://img.com/blah","banner_image":"http://img.com/blah","date_published":"2017-01-01 10:00:00","date_modified":"2017-01-01 10:00:00","author":{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"},"tags":["json","feed"],"attachments":[]}"#; let item: Item = serde_json::from_str(&json).unwrap(); let expected = Item { id: "1".into(), url: Some("http://example.com/feed.json".into()), external_url: Some("http://example.com/feed.json".into()), title: Some("feed title".into()), content: Content::Html("

content

".into()), summary: Some("feed summary".into()), image: Some("http://img.com/blah".into()), banner_image: Some("http://img.com/blah".into()), date_published: Some("2017-01-01 10:00:00".into()), date_modified: Some("2017-01-01 10:00:00".into()), author: Some( Author::new() .name("bob jones") .url("http://example.com") .avatar("http://img.com/blah"), ), tags: Some(vec!["json".into(), "feed".into()]), attachments: Some(vec![]), }; assert_eq!(item, expected); } #[test] #[allow(non_snake_case)] fn deserialize_item__content_text() { let json = r#"{"id":"1","url":"http://example.com/feed.json","external_url":"http://example.com/feed.json","title":"feed title","content_html":null,"content_text":"content","summary":"feed summary","image":"http://img.com/blah","banner_image":"http://img.com/blah","date_published":"2017-01-01 10:00:00","date_modified":"2017-01-01 10:00:00","author":{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"},"tags":["json","feed"],"attachments":[]}"#; let item: Item = serde_json::from_str(&json).unwrap(); let expected = Item { id: "1".into(), url: Some("http://example.com/feed.json".into()), external_url: Some("http://example.com/feed.json".into()), title: Some("feed title".into()), content: Content::Text("content".into()), summary: Some("feed summary".into()), image: Some("http://img.com/blah".into()), banner_image: Some("http://img.com/blah".into()), date_published: Some("2017-01-01 10:00:00".into()), date_modified: Some("2017-01-01 10:00:00".into()), author: Some( Author::new() .name("bob jones") .url("http://example.com") .avatar("http://img.com/blah"), ), tags: Some(vec!["json".into(), "feed".into()]), attachments: Some(vec![]), }; assert_eq!(item, expected); } #[test] #[allow(non_snake_case)] fn deserialize_item__content_both() { let json = r#"{"id":"1","url":"http://example.com/feed.json","external_url":"http://example.com/feed.json","title":"feed title","content_html":"

content

","content_text":"content","summary":"feed summary","image":"http://img.com/blah","banner_image":"http://img.com/blah","date_published":"2017-01-01 10:00:00","date_modified":"2017-01-01 10:00:00","author":{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"},"tags":["json","feed"],"attachments":[]}"#; let item: Item = serde_json::from_str(&json).unwrap(); let expected = Item { id: "1".into(), url: Some("http://example.com/feed.json".into()), external_url: Some("http://example.com/feed.json".into()), title: Some("feed title".into()), content: Content::Both("

content

".into(), "content".into()), summary: Some("feed summary".into()), image: Some("http://img.com/blah".into()), banner_image: Some("http://img.com/blah".into()), date_published: Some("2017-01-01 10:00:00".into()), date_modified: Some("2017-01-01 10:00:00".into()), author: Some( Author::new() .name("bob jones") .url("http://example.com") .avatar("http://img.com/blah"), ), tags: Some(vec!["json".into(), "feed".into()]), attachments: Some(vec![]), }; assert_eq!(item, expected); } }