aboutsummaryrefslogtreecommitdiff
path: root/lib/jsonfeed/src
diff options
context:
space:
mode:
authorChristine Dodrill <me@christine.website>2020-07-16 15:32:30 -0400
committerGitHub <noreply@github.com>2020-07-16 15:32:30 -0400
commit385d25c9f96c0acd5d932488e3bd0ed36ceb4dd7 (patch)
treeaf789f7250519b23038a7e5ea0ae7f4f4c1ffdfc /lib/jsonfeed/src
parent449e934246c82d90dd0aac2644d67f928befeeb4 (diff)
downloadxesite-385d25c9f96c0acd5d932488e3bd0ed36ceb4dd7.tar.xz
xesite-385d25c9f96c0acd5d932488e3bd0ed36ceb4dd7.zip
Rewrite site backend in Rust (#178)
* add shell.nix changes for Rust #176 * set up base crate layout * add first set of dependencies * start adding basic app modules * start html templates * serve index page * add contact and feeds pages * add resume rendering support * resume cleanups * get signalboost page working * rewrite config to be in dhall * more work * basic generic post loading * more tests * initial blog index support * fix routing? * render blogposts * X-Clacks-Overhead * split blog handlers into blog.rs * gallery index * gallery posts * fix hashtags * remove instantpage (it messes up the metrics) * talk support + prometheus * Create rust.yml * Update rust.yml * Update codeql-analysis.yml * add jsonfeed library * jsonfeed support * rss/atom * go mod tidy * atom: add posted date * rss: add publishing date * nix: build rust program * rip out go code * rip out go templates * prepare for serving in docker * create kubernetes deployment * create automagic deployment * build docker images on non-master * more fixes * fix timestamps * fix RSS/Atom/JSONFeed validation errors * add go vanity import redirecting * templates/header: remove this * atom feed: fixes * fix? * fix?? * fix rust tests * Update rust.yml * automatically show snow during the winter * fix dates * show commit link in footer * sitemap support * fix compiler warning * start basic patreon client * integrate kankyo * fix patreon client * add patrons page * remove this * handle patron errors better * fix build * clean up deploy * sort envvars for deploy * remove deps.nix * shell.nix: remove go * update README * fix envvars for tests * nice * blog: add rewrite in rust post * blog/site-update: more words
Diffstat (limited to 'lib/jsonfeed/src')
-rw-r--r--lib/jsonfeed/src/builder.rs204
-rw-r--r--lib/jsonfeed/src/errors.rs7
-rw-r--r--lib/jsonfeed/src/feed.rs296
-rw-r--r--lib/jsonfeed/src/item.rs493
-rw-r--r--lib/jsonfeed/src/lib.rs252
5 files changed, 1252 insertions, 0 deletions
diff --git a/lib/jsonfeed/src/builder.rs b/lib/jsonfeed/src/builder.rs
new file mode 100644
index 0000000..f17740f
--- /dev/null
+++ b/lib/jsonfeed/src/builder.rs
@@ -0,0 +1,204 @@
+use std::default::Default;
+
+use errors::*;
+use feed::{Feed, Author, Attachment};
+use item::{Content, Item};
+
+/// Feed Builder
+///
+/// This is used to programmatically build up a Feed object,
+/// which can be serialized later into a JSON string
+pub struct Builder(Feed);
+
+impl Builder {
+ pub fn new() -> Builder {
+ Builder(Feed::default())
+ }
+
+ pub fn title<I: Into<String>>(mut self, t: I) -> Builder {
+ self.0.title = t.into();
+ self
+ }
+
+ pub fn home_page_url<I: Into<String>>(mut self, url: I) -> Builder {
+ self.0.home_page_url = Some(url.into());
+ self
+ }
+
+ pub fn feed_url<I: Into<String>>(mut self, url: I) -> Builder {
+ self.0.feed_url = Some(url.into());
+ self
+ }
+
+ pub fn description<I: Into<String>>(mut self, desc: I) -> Builder {
+ self.0.description = Some(desc.into());
+ self
+ }
+
+ pub fn user_comment<I: Into<String>>(mut self, cmt: I) -> Builder {
+ self.0.user_comment = Some(cmt.into());
+ self
+ }
+
+ pub fn next_url<I: Into<String>>(mut self, url: I) -> Builder {
+ self.0.next_url = Some(url.into());
+ self
+ }
+
+ pub fn icon<I: Into<String>>(mut self, url: I) -> Builder {
+ self.0.icon = Some(url.into());
+ self
+ }
+
+ pub fn favicon<I: Into<String>>(mut self, url: I) -> Builder {
+ self.0.favicon = Some(url.into());
+ self
+ }
+
+ pub fn author(mut self, author: Author) -> Builder {
+ self.0.author = Some(author);
+ self
+ }
+
+ pub fn expired(mut self) -> Builder {
+ self.0.expired = Some(true);
+ self
+ }
+
+ pub fn item(mut self, item: Item) -> Builder {
+ self.0.items.push(item);
+ self
+ }
+
+ pub fn build(self) -> Feed {
+ self.0
+ }
+}
+
+/// Builder object for an item in a feed
+pub struct ItemBuilder {
+ pub id: Option<String>,
+ pub url: Option<String>,
+ pub external_url: Option<String>,
+ pub title: Option<String>,
+ pub content: Option<Content>,
+ pub summary: Option<String>,
+ pub image: Option<String>,
+ pub banner_image: Option<String>,
+ pub date_published: Option<String>,
+ pub date_modified: Option<String>,
+ pub author: Option<Author>,
+ pub tags: Option<Vec<String>>,
+ pub attachments: Option<Vec<Attachment>>,
+}
+
+impl ItemBuilder {
+ pub fn new() -> ItemBuilder {
+ ItemBuilder {
+ id: None,
+ url: None,
+ external_url: None,
+ title: None,
+ content: None,
+ summary: None,
+ image: None,
+ banner_image: None,
+ date_published: None,
+ date_modified: None,
+ author: None,
+ tags: None,
+ attachments: None,
+ }
+ }
+
+ pub fn title<I: Into<String>>(mut self, i: I) -> ItemBuilder {
+ self.title = Some(i.into());
+ self
+ }
+
+ pub fn image<I: Into<String>>(mut self, i: I) -> ItemBuilder {
+ self.image = Some(i.into());
+ self
+ }
+
+ pub fn id<I: Into<String>>(mut self, i: I) -> ItemBuilder {
+ self.id = Some(i.into());
+ self
+ }
+
+ pub fn url<I: Into<String>>(mut self, i: I) -> ItemBuilder {
+ self.url = Some(i.into());
+ self
+ }
+
+ pub fn external_url<I: Into<String>>(mut self, i: I) -> ItemBuilder {
+ self.external_url = Some(i.into());
+ self
+ }
+
+ pub fn date_modified<I: Into<String>>(mut self, i: I) -> ItemBuilder {
+ self.date_modified = Some(i.into());
+ self
+ }
+
+ pub fn date_published<I: Into<String>>(mut self, i: I) -> ItemBuilder {
+ self.date_published = Some(i.into());
+ self
+ }
+
+ pub fn tags(mut self, tags: Vec<String>) -> ItemBuilder {
+ self.tags = Some(tags);
+ self
+ }
+
+ pub fn author(mut self, who: Author) -> ItemBuilder {
+ self.author = Some(who);
+ self
+ }
+
+ pub fn content_html<I: Into<String>>(mut self, i: I) -> ItemBuilder {
+ match self.content {
+ Some(Content::Text(t)) => {
+ self.content = Some(Content::Both(i.into(), t));
+ },
+ _ => {
+ self.content = Some(Content::Html(i.into()));
+ }
+ }
+ self
+ }
+
+ pub fn content_text<I: Into<String>>(mut self, i: I) -> ItemBuilder {
+ match self.content {
+ Some(Content::Html(s)) => {
+ self.content = Some(Content::Both(s, i.into()));
+ },
+ _ => {
+ self.content = Some(Content::Text(i.into()));
+ },
+ }
+ self
+ }
+
+ pub fn build(self) -> Result<Item> {
+ if self.id.is_none() || self.content.is_none() {
+ return Err("missing field 'id' or 'content_*'".into());
+ }
+ Ok(Item {
+ id: self.id.unwrap(),
+ url: self.url,
+ external_url: self.external_url,
+ title: self.title,
+ content: self.content.unwrap(),
+ summary: self.summary,
+ image: self.image,
+ banner_image: self.banner_image,
+ date_published: self.date_published,
+ date_modified: self.date_modified,
+ author: self.author,
+ tags: self.tags,
+ attachments: self.attachments
+ })
+ }
+}
+
diff --git a/lib/jsonfeed/src/errors.rs b/lib/jsonfeed/src/errors.rs
new file mode 100644
index 0000000..936b7ec
--- /dev/null
+++ b/lib/jsonfeed/src/errors.rs
@@ -0,0 +1,7 @@
+use serde_json;
+error_chain!{
+ foreign_links {
+ Serde(serde_json::Error);
+ }
+}
+
diff --git a/lib/jsonfeed/src/feed.rs b/lib/jsonfeed/src/feed.rs
new file mode 100644
index 0000000..8b5b5ce
--- /dev/null
+++ b/lib/jsonfeed/src/feed.rs
@@ -0,0 +1,296 @@
+use std::default::Default;
+
+use item::Item;
+use builder::Builder;
+
+const VERSION_1: &'static str = "https://jsonfeed.org/version/1";
+
+/// Represents a single feed
+///
+/// # Examples
+///
+/// ```rust
+/// // Serialize a feed object to a JSON string
+///
+/// # extern crate jsonfeed;
+/// # use std::default::Default;
+/// # use jsonfeed::Feed;
+/// # fn main() {
+/// let feed: Feed = Feed::default();
+/// assert_eq!(
+/// jsonfeed::to_string(&feed).unwrap(),
+/// "{\"version\":\"https://jsonfeed.org/version/1\",\"title\":\"\",\"items\":[]}"
+/// );
+/// # }
+/// ```
+///
+/// ```rust
+/// // Deserialize a feed objects from a JSON String
+///
+/// # extern crate jsonfeed;
+/// # use jsonfeed::Feed;
+/// # fn main() {
+/// let json = "{\"version\":\"https://jsonfeed.org/version/1\",\"title\":\"\",\"items\":[]}";
+/// let feed: Feed = jsonfeed::from_str(&json).unwrap();
+/// assert_eq!(
+/// feed,
+/// Feed::default()
+/// );
+/// # }
+/// ```
+#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
+pub struct Feed {
+ pub version: String,
+ pub title: String,
+ pub items: Vec<Item>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub home_page_url: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub feed_url: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub description: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub user_comment: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub next_url: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub icon: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub favicon: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub author: Option<Author>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub expired: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub hubs: Option<Vec<Hub>>,
+}
+
+impl Feed {
+ /// Used to construct a Feed object
+ pub fn builder() -> Builder {
+ Builder::new()
+ }
+}
+
+impl Default for Feed {
+ fn default() -> Feed {
+ Feed {
+ version: VERSION_1.to_string(),
+ title: "".to_string(),
+ items: vec![],
+ home_page_url: None,
+ feed_url: None,
+ description: None,
+ user_comment: None,
+ next_url: None,
+ icon: None,
+ favicon: None,
+ author: None,
+ expired: None,
+ hubs: None,
+ }
+ }
+}
+
+/// Represents an `attachment` for an item
+#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
+pub struct Attachment {
+ url: String,
+ mime_type: String,
+ title: Option<String>,
+ size_in_bytes: Option<u64>,
+ duration_in_seconds: Option<u64>,
+}
+
+/// Represents an `author` in both a feed and a feed item
+#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
+pub struct Author {
+ name: Option<String>,
+ url: Option<String>,
+ avatar: Option<String>,
+}
+
+impl Author {
+ pub fn new() -> Author {
+ Author {
+ name: None,
+ url: None,
+ avatar: None,
+ }
+ }
+
+ pub fn name<I: Into<String>>(mut self, name: I) -> Self {
+ self.name = Some(name.into());
+ self
+ }
+
+ pub fn url<I: Into<String>>(mut self, url: I) -> Self {
+ self.url = Some(url.into());
+ self
+ }
+
+ pub fn avatar<I: Into<String>>(mut self, avatar: I) -> Self {
+ self.avatar = Some(avatar.into());
+ self
+ }
+}
+
+/// Represents a `hub` for a feed
+#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
+pub struct Hub {
+ #[serde(rename = "type")]
+ type_: String,
+ url: String,
+}
+
+#[cfg(test)]
+mod tests {
+ use serde_json;
+ use std::default::Default;
+ use super::*;
+
+ #[test]
+ fn serialize_feed() {
+ let feed = Feed {
+ version: "https://jsonfeed.org/version/1".to_string(),
+ title: "some title".to_string(),
+ items: vec![],
+ home_page_url: None,
+ description: None,
+ expired: Some(true),
+ ..Default::default()
+ };
+ assert_eq!(
+ serde_json::to_string(&feed).unwrap(),
+ r#"{"version":"https://jsonfeed.org/version/1","title":"some title","items":[],"expired":true}"#
+ );
+ }
+
+ #[test]
+ fn deserialize_feed() {
+ let json = r#"{"version":"https://jsonfeed.org/version/1","title":"some title","items":[]}"#;
+ let feed: Feed = serde_json::from_str(&json).unwrap();
+ let expected = Feed {
+ version: "https://jsonfeed.org/version/1".to_string(),
+ title: "some title".to_string(),
+ items: vec![],
+ ..Default::default()
+ };
+ assert_eq!(
+ feed,
+ expected
+ );
+ }
+
+ #[test]
+ fn serialize_attachment() {
+ let attachment = Attachment {
+ url: "http://example.com".to_string(),
+ mime_type: "application/json".to_string(),
+ title: Some("some title".to_string()),
+ size_in_bytes: Some(1),
+ duration_in_seconds: Some(1),
+ };
+ assert_eq!(
+ serde_json::to_string(&attachment).unwrap(),
+ r#"{"url":"http://example.com","mime_type":"application/json","title":"some title","size_in_bytes":1,"duration_in_seconds":1}"#
+ );
+ }
+
+ #[test]
+ fn deserialize_attachment() {
+ let json = r#"{"url":"http://example.com","mime_type":"application/json","title":"some title","size_in_bytes":1,"duration_in_seconds":1}"#;
+ let attachment: Attachment = serde_json::from_str(&json).unwrap();
+ let expected = Attachment {
+ url: "http://example.com".to_string(),
+ mime_type: "application/json".to_string(),
+ title: Some("some title".to_string()),
+ size_in_bytes: Some(1),
+ duration_in_seconds: Some(1),
+ };
+ assert_eq!(
+ attachment,
+ expected
+ );
+ }
+
+ #[test]
+ fn serialize_author() {
+ let author = Author {
+ name: Some("bob jones".to_string()),
+ url: Some("http://example.com".to_string()),
+ avatar: Some("http://img.com/blah".to_string()),
+ };
+ assert_eq!(
+ serde_json::to_string(&author).unwrap(),
+ r#"{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"}"#
+ );
+ }
+
+ #[test]
+ fn deserialize_author() {
+ let json = r#"{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"}"#;
+ let author: Author = serde_json::from_str(&json).unwrap();
+ let expected = Author {
+ name: Some("bob jones".to_string()),
+ url: Some("http://example.com".to_string()),
+ avatar: Some("http://img.com/blah".to_string()),
+ };
+ assert_eq!(
+ author,
+ expected
+ );
+ }
+
+ #[test]
+ fn serialize_hub() {
+ let hub = Hub {
+ type_: "some-type".to_string(),
+ url: "http://example.com".to_string(),
+ };
+ assert_eq!(
+ serde_json::to_string(&hub).unwrap(),
+ r#"{"type":"some-type","url":"http://example.com"}"#
+ )
+ }
+
+ #[test]
+ fn deserialize_hub() {
+ let json = r#"{"type":"some-type","url":"http://example.com"}"#;
+ let hub: Hub = serde_json::from_str(&json).unwrap();
+ let expected = Hub {
+ type_: "some-type".to_string(),
+ url: "http://example.com".to_string(),
+ };
+ assert_eq!(
+ hub,
+ expected
+ );
+ }
+
+ #[test]
+ fn deser_podcast() {
+ let json = r#"{
+ "version": "https://jsonfeed.org/version/1",
+ "title": "Timetable",
+ "home_page_url": "http://timetable.manton.org/",
+ "items": [
+ {
+ "id": "http://timetable.manton.org/2017/04/episode-45-launch-week/",
+ "url": "http://timetable.manton.org/2017/04/episode-45-launch-week/",
+ "title": "Episode 45: Launch week",
+ "content_html": "I’m rolling out early access to Micro.blog this week. I talk about how the first 2 days have gone, mistakes with TestFlight, and what to do next.",
+ "date_published": "2017-04-26T01:09:45+00:00",
+ "attachments": [
+ {
+ "url": "http://timetable.manton.org/podcast-download/139/episode-45-launch-week.mp3",
+ "mime_type": "audio/mpeg",
+ "size_in_bytes": 5236920
+ }
+ ]
+ }
+ ]
+}"#;
+ serde_json::from_str::<Feed>(&json).expect("Failed to deserialize podcast feed");
+ }
+}
diff --git a/lib/jsonfeed/src/item.rs b/lib/jsonfeed/src/item.rs
new file mode 100644
index 0000000..605525b
--- /dev/null
+++ b/lib/jsonfeed/src/item.rs
@@ -0,0 +1,493 @@
+use std::fmt;
+use std::default::Default;
+
+use feed::{Author, Attachment};
+use builder::ItemBuilder;
+
+use serde::ser::{Serialize, Serializer, SerializeStruct};
+use serde::de::{self, Deserialize, Deserializer, Visitor, MapAccess};
+
+/// 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<String>,
+ pub external_url: Option<String>,
+ pub title: Option<String>,
+ pub content: Content,
+ pub summary: Option<String>,
+ pub image: Option<String>,
+ pub banner_image: Option<String>,
+ pub date_published: Option<String>, // todo DateTime objects?
+ pub date_modified: Option<String>,
+ pub author: Option<Author>,
+ pub tags: Option<Vec<String>>,
+ pub attachments: Option<Vec<Attachment>>,
+}
+
+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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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::<Option<&str>>)?;
+ },
+ Content::Text(ref s) => {
+ state.serialize_field("content_html", &None::<Option<&str>>)?;
+ 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<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<E>(self, value: &str) -> Result<Field, E>
+ 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<V>(self, mut map: V) -> Result<Item, V::Error>
+ 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<String> = None;
+ let mut content_text: Option<String> = 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("<p>content</p>".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":"<p>content</p>","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(),