Even better leniant parsing

This commit is contained in:
Leonora Tindall 2022-11-01 14:50:04 -05:00
parent fbcc5d536e
commit f1a0944688
Signed by: nora
GPG Key ID: 7A8B52EC67E09AAF
3 changed files with 3426 additions and 15 deletions

View File

@ -0,0 +1 @@
{"nItems":0,"nPages":0,"items":[],"_links":[{"href":"/api/v1/project/vogon","rel":"project","type":"GET"},{"href":"/api/v1/project/vogon/posts?page=998","rel":"prev","type":"GET"}]}

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::Deserialize; use serde::{Deserialize, Deserializer};
pub fn cohost_posts_api_url(project: impl AsRef<str>, page: u64) -> String { pub fn cohost_posts_api_url(project: impl AsRef<str>, page: u64) -> String {
format!( format!(
@ -11,7 +11,7 @@ pub fn cohost_posts_api_url(project: impl AsRef<str>, page: u64) -> String {
// Cohost doesn't give us Next links ("rel: next") for further pages, so we'll have to ALWAYS populate the rel=next field // Cohost doesn't give us Next links ("rel: next") for further pages, so we'll have to ALWAYS populate the rel=next field
#[derive(Deserialize)] #[derive(Debug, Deserialize)]
pub struct CohostPostsPage { pub struct CohostPostsPage {
#[serde(rename = "nItems")] #[serde(rename = "nItems")]
pub number_items: usize, pub number_items: usize,
@ -22,19 +22,27 @@ pub struct CohostPostsPage {
pub links: Vec<CohostPostLink>, pub links: Vec<CohostPostLink>,
} }
#[derive(Deserialize)] #[derive(Debug, Deserialize)]
pub struct CohostPost { pub struct CohostPost {
#[serde(rename = "postId")] #[serde(rename = "postId")]
pub id: u64, pub id: u64,
#[serde(default)] #[serde(deserialize_with = "deserialize_null_default", default)]
pub headline: String, pub headline: String,
#[serde(rename = "publishedAt")] #[serde(rename = "publishedAt")]
pub published_at: DateTime<Utc>, pub published_at: DateTime<Utc>,
pub cws: Vec<String>, pub cws: Vec<String>,
pub tags: Vec<String>, pub tags: Vec<String>,
#[serde(rename = "plainTextBody", default)] #[serde(
rename = "plainTextBody",
deserialize_with = "deserialize_null_default",
default
)]
pub plain_body: String, pub plain_body: String,
#[serde(rename = "singlePostPageUrl")] #[serde(
rename = "singlePostPageUrl",
deserialize_with = "deserialize_null_default",
default
)]
pub url: String, pub url: String,
#[serde(rename = "postingProject")] #[serde(rename = "postingProject")]
pub poster: CohostPostingProject, pub poster: CohostPostingProject,
@ -42,31 +50,49 @@ pub struct CohostPost {
pub share_tree: Vec<CohostPost>, pub share_tree: Vec<CohostPost>,
} }
#[derive(Deserialize)] #[derive(Debug, Deserialize)]
pub struct CohostPostingProject { pub struct CohostPostingProject {
#[serde(rename = "projectId")] #[serde(rename = "projectId")]
pub id: u64, pub id: u64,
#[serde(deserialize_with = "deserialize_null_default", default)]
pub handle: String, pub handle: String,
#[serde(rename = "displayName", default)] #[serde(
rename = "displayName",
deserialize_with = "deserialize_null_default",
default
)]
pub display_name: String, pub display_name: String,
#[serde(default)] #[serde(deserialize_with = "deserialize_null_default", default)]
pub dek: String, pub dek: String,
#[serde(default)] #[serde(deserialize_with = "deserialize_null_default", default)]
pub description: String, pub description: String,
#[serde(default)] #[serde(deserialize_with = "deserialize_null_default", default)]
pub pronouns: String, pub pronouns: String,
} }
#[derive(Deserialize)] #[derive(Debug, Deserialize)]
pub struct CohostPostLink { pub struct CohostPostLink {
#[serde(default)] #[serde(deserialize_with = "deserialize_null_default", default)]
pub href: String, pub href: String,
#[serde(default)] #[serde(deserialize_with = "deserialize_null_default", default)]
pub rel: String, pub rel: String,
#[serde(rename = "type", default)] #[serde(
rename = "type",
deserialize_with = "deserialize_null_default",
default
)]
pub t_type: String, pub t_type: String,
} }
fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
T: Default + Deserialize<'de>,
D: Deserializer<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}
#[test] #[test]
fn test_deserialize() -> Result<(), Box<dyn std::error::Error>> { fn test_deserialize() -> Result<(), Box<dyn std::error::Error>> {
let post_page_json = include_str!("../samples/cohost/api/v1/project_posts.json"); let post_page_json = include_str!("../samples/cohost/api/v1/project_posts.json");
@ -77,3 +103,19 @@ fn test_deserialize() -> Result<(), Box<dyn std::error::Error>> {
assert_eq!(post.poster.id, 32693); assert_eq!(post.poster.id, 32693);
Ok(()) Ok(())
} }
#[test]
fn test_deserialize_weird() -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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(())
}