feedly_api 0.5.0

rust implementation of the feedly REST API
Documentation
use super::Category;
use super::Tag;
use crate::ApiError;
use serde_derive::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Debug, Serialize, Deserialize)]
pub struct Entry {
    pub id: String,
    #[serde(default)]
    pub title: Option<String>,
    #[serde(default)]
    pub content: Option<Content>,
    #[serde(default)]
    pub summary: Option<Content>,
    #[serde(default)]
    pub author: Option<String>,
    pub crawled: i64,
    #[serde(default)]
    pub recrawled: Option<i64>,
    pub published: i64,
    #[serde(default)]
    pub updated: Option<i64>,
    #[serde(default)]
    pub alternate: Option<Vec<Link>>,
    #[serde(default)]
    pub origin: Option<Origin>,
    #[serde(default)]
    pub keywords: Option<Vec<String>>,
    #[serde(default)]
    pub visual: Option<Visual>,
    pub unread: bool,
    #[serde(default)]
    pub tags: Option<Vec<Tag>>,
    #[serde(default)]
    pub categories: Option<Vec<Category>>,
    #[serde(default)]
    pub engagement: Option<i64>,
    #[serde(default)]
    #[serde(rename = "actionTimestamp")]
    pub action_timestamp: Option<i64>,
    #[serde(default)]
    pub enclosure: Option<Vec<Link>>,
    #[serde(default)]
    pub fingerprint: Option<String>,
    #[serde(default)]
    #[serde(rename = "originId")]
    pub origin_id: Option<String>,
    #[serde(default)]
    pub sid: Option<String>,
}

impl Entry {
    #[allow(clippy::type_complexity)]
    pub fn decompose(
        self,
    ) -> (
        String,
        Option<String>,
        Option<Content>,
        Option<Content>,
        Option<String>,
        i64,
        Option<i64>,
        i64,
        Option<i64>,
        Option<Vec<Link>>,
        Option<Origin>,
        Option<Vec<String>>,
        Option<Visual>,
        bool,
        Option<Vec<Tag>>,
        Option<Vec<Category>>,
        Option<i64>,
        Option<i64>,
        Option<Vec<Link>>,
        Option<String>,
        Option<String>,
        Option<String>,
    ) {
        (
            self.id,
            self.title,
            self.content,
            self.summary,
            self.author,
            self.crawled,
            self.recrawled,
            self.published,
            self.updated,
            self.alternate,
            self.origin,
            self.keywords,
            self.visual,
            self.unread,
            self.tags,
            self.categories,
            self.engagement,
            self.action_timestamp,
            self.enclosure,
            self.fingerprint,
            self.origin_id,
            self.sid,
        )
    }

    fn manual_deserialize(data: &Value) -> Result<Entry, ApiError> {
        let id = data["id"]
            .as_str()
            .ok_or(ApiError::ManualJson)
            .map_err(|e| {
                log::error!("entry doesn't contain an ID: {}", e);
                log::error!("entry value: {:?}", data);
                e
            })?
            .into();

        let title = data["title"].as_str().map(|t| t.into());

        let content = Content::manual_deserialize(&data["content"]).ok();
        let summary = Content::manual_deserialize(&data["summary"]).ok();

        let author = data["author"].as_str().map(|t| t.into());

        let crawled = data["crawled"]
            .as_u64()
            .ok_or(ApiError::ManualJson)
            .map_err(|e| {
                log::error!("entry doesn't contain 'crawled'");
                log::error!("entry value: {:?}", data);
                e
            })? as i64;

        let recrawled = data["recrawled"].as_u64().map(|r| r as i64);

        let published = data["published"]
            .as_i64()
            .ok_or(ApiError::ManualJson)
            .map_err(|e| {
                log::error!("entry doesn't contain 'published'");
                log::error!("entry value: {:?}", data);
                e
            })? as i64;

        let updated = data["updated"].as_u64().map(|u| u as i64);

        let alternate = match Link::manual_deserialize_vec(&data["alternate"]) {
            Ok(alternate) => {
                if alternate.is_empty() {
                    None
                } else {
                    Some(alternate)
                }
            }
            Err(_error) => None,
        };

        let origin = Origin::manual_deserialize(&data["origin"]).ok();

        let keywords = if let Some(keywords_values) = data["keywords"].as_array() {
            let mut keywords = Vec::new();
            for keyword_value in keywords_values {
                if let Some(keyword_str) = keyword_value.as_str() {
                    keywords.push(keyword_str.into());
                }
            }
            if keywords.is_empty() {
                None
            } else {
                Some(keywords)
            }
        } else {
            None
        };

        let visual = Visual::manual_deserialize(&data["visual"]).ok();

        let unread = data["unread"].as_bool().unwrap_or(false);

        let tags = match Tag::manual_deserialize_vec(&data["tags"]) {
            Ok(tags) => {
                if tags.is_empty() {
                    None
                } else {
                    Some(tags)
                }
            }
            Err(_) => None,
        };

        let categories = match Category::manual_deserialize_vec(&data["categories"]) {
            Ok(categories) => {
                if categories.is_empty() {
                    None
                } else {
                    Some(categories)
                }
            }
            Err(_) => None,
        };

        let engagement = data["engagement"].as_i64();
        let action_timestamp = data["actionTimestamp"].as_i64();

        let enclosure = match Link::manual_deserialize_vec(&data["enclosure"]) {
            Ok(enclosure) => {
                if enclosure.is_empty() {
                    None
                } else {
                    Some(enclosure)
                }
            }
            Err(_) => None,
        };

        let fingerprint = data["fingerprint"].as_str().map(|f| f.into());
        let origin_id = data["originId"].as_str().map(|f| f.into());
        let sid = data["sid"].as_str().map(|f| f.into());

        Ok(Entry {
            id,
            title,
            content,
            summary,
            author,
            crawled,
            recrawled,
            published,
            updated,
            alternate,
            origin,
            keywords,
            visual,
            unread,
            tags,
            categories,
            engagement,
            action_timestamp,
            enclosure,
            fingerprint,
            origin_id,
            sid,
        })
    }

    pub fn manual_deserialize_vec(data: &Value) -> Result<Vec<Entry>, ApiError> {
        let mut entries = Vec::new();

        let vector = match data.as_array() {
            Some(vector) => vector,
            None => {
                return Ok(entries);
            }
        };

        for entry_value in vector {
            match Self::manual_deserialize(entry_value) {
                Ok(entry) => entries.push(entry),
                Err(_error) => {
                    continue;
                }
            }
        }

        Ok(entries)
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Content {
    pub content: String,
    #[serde(default)]
    pub direction: Option<String>,
}

impl Content {
    pub fn decompose(self) -> (String, Option<String>) {
        (self.content, self.direction)
    }

    pub fn manual_deserialize(data: &Value) -> Result<Content, ApiError> {
        let content = data["content"].as_str().ok_or(ApiError::ManualJson)?.into();

        let direction = data["direction"].as_str().map(|t| t.into());

        Ok(Content { content, direction })
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Link {
    pub href: String,
    #[serde(default)]
    #[serde(rename = "type")]
    pub _type: Option<String>,
}

impl Link {
    pub fn decompose(self) -> (String, Option<String>) {
        (self.href, self._type)
    }

    fn manual_deserialize(data: &Value) -> Result<Link, ApiError> {
        let href = data["href"]
            .as_str()
            .ok_or(ApiError::ManualJson)
            .map_err(|e| {
                log::error!("Link doesn't contain 'href': {:?}", data);
                e
            })?
            .into();
        let _type = data["type"].as_str().map(|t| t.into());

        Ok(Link { href, _type })
    }

    pub fn manual_deserialize_vec(data: &Value) -> Result<Vec<Link>, ApiError> {
        let mut links = Vec::new();

        let vector = match data.as_array() {
            Some(vector) => vector,
            None => {
                return Ok(links);
            }
        };

        for link_value in vector {
            match Self::manual_deserialize(link_value) {
                Ok(link) => links.push(link),
                Err(_error) => {
                    continue;
                }
            }
        }

        Ok(links)
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Origin {
    #[serde(default)]
    #[serde(rename = "streamId")]
    pub stream_id: Option<String>,
    pub title: Option<String>,
    #[serde(default)]
    #[serde(rename = "htmlUrl")]
    pub html_url: Option<String>,
}

impl Origin {
    pub fn decompose(self) -> (Option<String>, Option<String>, Option<String>) {
        (self.stream_id, self.title, self.html_url)
    }

    pub fn manual_deserialize(data: &Value) -> Result<Origin, ApiError> {
        let stream_id = data["streamId"].as_str().map(|t| t.into());

        let title = data["title"].as_str().map(|t| t.into());

        let html_url = data["htmlUrl"].as_str().map(|t| t.into());

        Ok(Origin {
            stream_id,
            title,
            html_url,
        })
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Visual {
    pub url: String,
    #[serde(default)]
    pub width: Option<u32>,
    #[serde(default)]
    pub height: Option<u32>,
    #[serde(default)]
    #[serde(rename = "contentType")]
    pub content_type: Option<String>,
}

impl Visual {
    pub fn decompose(self) -> (String, Option<u32>, Option<u32>, Option<String>) {
        (self.url, self.width, self.height, self.content_type)
    }

    pub fn manual_deserialize(data: &Value) -> Result<Visual, ApiError> {
        let url = data["url"].as_str().ok_or(ApiError::ManualJson)?.into();

        let width = data["width"].as_u64().map(|w| w as u32);

        let height = data["height"].as_u64().map(|w| w as u32);

        let content_type = data["contentType"].as_str().map(|t| t.into());

        Ok(Visual {
            url,
            width,
            height,
            content_type,
        })
    }
}