use chrono::{DateTime, Utc}; use serde::{Deserialize, Deserializer}; pub fn cohost_posts_api_url(project: impl AsRef, page: u64) -> String { format!( "https://cohost.org/api/v1/project/{}/posts?page={}", project.as_ref(), page ) } // Cohost doesn't give us Next links ("rel: next") for further pages, so we'll have to ALWAYS populate the rel=next field #[derive(Debug, Clone, Deserialize)] pub struct CohostPostsPage { #[serde(rename = "nItems")] pub number_items: usize, #[serde(rename = "nPages")] pub number_pages: u64, pub items: Vec, #[serde(rename = "_links")] pub links: Vec, } #[derive(Debug, Clone, Deserialize)] pub struct CohostPost { #[serde(rename = "postId")] pub id: u64, #[serde(deserialize_with = "deserialize_null_default", default)] pub headline: String, #[serde(rename = "publishedAt")] pub published_at: DateTime, pub cws: Vec, pub tags: Vec, #[serde( rename = "plainTextBody", deserialize_with = "deserialize_null_default", default )] pub plain_body: String, #[serde( rename = "singlePostPageUrl", deserialize_with = "deserialize_null_default", default )] pub url: String, #[serde(rename = "postingProject")] pub poster: CohostPostingProject, #[serde(rename = "shareTree")] pub share_tree: Vec, } #[derive(Debug, Clone, Deserialize)] pub struct CohostPostingProject { #[serde(rename = "projectId")] pub id: u64, #[serde(deserialize_with = "deserialize_null_default", default)] pub handle: String, #[serde( rename = "displayName", deserialize_with = "deserialize_null_default", default )] pub display_name: String, #[serde(deserialize_with = "deserialize_null_default", default)] pub dek: String, #[serde(deserialize_with = "deserialize_null_default", default)] pub description: String, #[serde(deserialize_with = "deserialize_null_default", default)] pub pronouns: String, } #[derive(Debug, Clone, Deserialize)] pub struct CohostPostLink { #[serde(deserialize_with = "deserialize_null_default", default)] pub href: String, #[serde(deserialize_with = "deserialize_null_default", default)] pub rel: String, #[serde( rename = "type", deserialize_with = "deserialize_null_default", default )] pub t_type: String, } fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result where T: Default + Deserialize<'de>, D: Deserializer<'de>, { let opt = Option::deserialize(deserializer)?; Ok(opt.unwrap_or_default()) } #[test] fn test_deserialize() -> Result<(), Box> { let post_page_json = include_str!("../samples/cohost/api/v1/project_posts.json"); let post_page_actual: CohostPostsPage = serde_json::from_str(post_page_json)?; assert_eq!(post_page_actual.number_items, post_page_actual.items.len()); let post = &post_page_actual.items[0]; assert_eq!(post.id, 149268); assert_eq!(post.poster.id, 32693); Ok(()) } #[test] fn test_deserialize_weird() -> Result<(), Box> { let post_page_json = include_str!("../samples/cohost/api/v1/vogon_pathological.json"); let _post_page_actual: CohostPostsPage = serde_json::from_str(post_page_json)?; Ok(()) } #[test] fn test_deserialize_empty() -> Result<(), Box> { let post_page_json = include_str!("../samples/cohost/api/v1/empty_posts_age.json"); let post_page_actual: CohostPostsPage = serde_json::from_str(post_page_json)?; println!("{:?}", post_page_actual); assert!(post_page_actual.items.is_empty()); Ok(()) }