use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use crate::impl_str_enum;
use crate::model::user::Metal;
use crate::model::{bool_from_int, bool_from_zero, deserialize_sys_metal};
use crate::utils::error::Error;
fn normalize_float_numbers(value: &mut Value) {
match value {
Value::Array(arr) => {
for item in arr {
normalize_float_numbers(item);
}
}
Value::Object(map) => {
for v in map.values_mut() {
normalize_float_numbers(v);
}
}
Value::Number(num) => {
if num.as_u64().is_none()
&& num.as_i64().is_none()
&& let Some(f) = num.as_f64()
&& f.is_finite()
{
let n = if f <= 0.0 { 0 } else { f.trunc() as u64 };
*value = Value::Number(serde_json::Number::from(n));
}
}
_ => {}
}
}
fn parse_with_float_fallback<T>(data: &Value, type_name: &str) -> Result<T, Error>
where
T: for<'de> Deserialize<'de>,
{
match serde_json::from_value::<T>(data.clone()) {
Ok(v) => Ok(v),
Err(first_err) => {
let mut normalized = data.clone();
normalize_float_numbers(&mut normalized);
serde_json::from_value::<T>(normalized).map_err(|second_err| {
Error::Parse(format!(
"Failed to parse {}: {} (fallback after float-normalize also failed: {})",
type_name, first_err, second_err
))
})
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[allow(non_snake_case)]
pub struct ArticlePost {
#[serde(rename = "articleTitle")]
pub title: String,
#[serde(rename = "articleContent")]
pub content: String,
#[serde(rename = "articleTags")]
pub tags: String,
#[serde(rename = "articleCommentable")]
pub commentable: bool,
#[serde(rename = "articleNotifyFollowers")]
pub notifyFollowers: bool,
#[serde(rename = "articleType")]
pub type_: ArticleType,
#[serde(rename = "articleShowInList")]
pub showInList: u32,
#[serde(rename = "articleRewardContent")]
pub rewardContent: Option<String>,
#[serde(rename = "articleRewardPoint")]
pub rewardPoint: Option<String>,
#[serde(rename = "articleAnonymous")]
pub anonymous: Option<bool>,
#[serde(rename = "articleQnAOfferPoint")]
pub offerPoint: Option<u32>,
}
impl ArticlePost {
pub fn from_value(data: &Value) -> Result<Self, Error> {
serde_json::from_value(data.clone())
.map_err(|e| Error::Parse(format!("Failed to parse ArticlePost: {}", e)))
}
pub fn to_json(&self) -> Result<Value, Error> {
serde_json::to_value(self)
.map_err(|e| Error::Parse(format!("Failed to serialize ArticlePost: {}", e)))
}
}
#[derive(Clone, Debug, Deserialize)]
#[allow(non_snake_case)]
pub struct ArticleTag {
pub oId: String,
#[serde(rename = "tagTitle")]
pub title: String,
#[serde(rename = "tagDescription")]
pub description: String,
#[serde(rename = "tagIconPath")]
pub iconPath: String,
#[serde(rename = "tagURI")]
pub uri: String,
#[serde(rename = "tagCSS")]
pub diyCSS: String,
#[serde(rename = "tagBadCnt")]
pub badCnt: u64,
#[serde(rename = "tagCommentCount")]
pub commentCnt: u32,
#[serde(rename = "tagFollowerCount")]
pub followerCnt: u32,
#[serde(rename = "tagGoodCnt")]
pub goodCnt: u64,
#[serde(rename = "tagReferenceCount")]
pub referenceCnt: u32,
#[serde(rename = "tagLinkCount")]
pub linkCnt: u32,
#[serde(rename = "tagSeoDesc")]
pub seoDesc: String,
#[serde(rename = "tagSeoKeywords")]
pub seoKeywords: String,
#[serde(rename = "tagSeoTitle")]
pub seoTitle: String,
#[serde(rename = "tagAd")]
pub tagAd: String,
#[serde(rename = "tagShowSideAd")]
pub showSideAd: u32,
#[serde(rename = "tagStatus")]
pub status: u32,
#[serde(rename = "tagRandomDouble")]
pub randomDouble: f64,
}
impl ArticleTag {
pub fn from_value(data: &Value) -> Result<Self, Error> {
serde_json::from_value(data.clone())
.map_err(|e| Error::Parse(format!("Failed to parse ArticleTag: {}", e)))
}
}
#[derive(Clone, Debug)]
#[derive(Default)]
pub enum VoteStatus {
#[default]
Normal,
Up,
Down,
}
impl VoteStatus {
pub fn from_index(index: usize) -> Self {
match index {
1 => VoteStatus::Up,
2 => VoteStatus::Down,
_ => VoteStatus::Normal,
}
}
}
#[derive(Clone, Debug)]
pub enum ArticleStatus {
Normal,
Ban,
Lock,
}
impl ArticleStatus {
pub fn from_index(index: usize) -> Self {
match index {
0 => ArticleStatus::Normal,
1 => ArticleStatus::Ban,
_ => ArticleStatus::Lock, }
}
}
impl Default for ArticleStatus {
fn default() -> Self {
Self::Normal
}
}
pub fn deserialize_score<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
let value: u64 = Deserialize::deserialize(deserializer)?;
Ok(value.to_string())
}
pub fn deserialize_vote<'de, D>(deserializer: D) -> Result<VoteStatus, D::Error>
where
D: Deserializer<'de>,
{
let value: i64 = Deserialize::deserialize(deserializer)?;
Ok(VoteStatus::from_index((value + 1) as usize))
}
pub fn deserialize_status<'de, D>(deserializer: D) -> Result<ArticleStatus, D::Error>
where
D: Deserializer<'de>,
{
let value: u64 = Deserialize::deserialize(deserializer)?;
Ok(ArticleStatus::from_index(value as usize))
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default)]
#[allow(non_snake_case)]
pub struct ArticleAuthor {
pub isOnline: bool,
pub onlineMinute: u64,
#[serde(deserialize_with = "bool_from_zero")]
pub pointStatus: bool,
#[serde(deserialize_with = "bool_from_zero")]
pub followerStatus: bool,
pub guideStep: u64,
#[serde(deserialize_with = "bool_from_zero")]
pub onlineStatus: bool,
pub currentCheckinStreakStart: u64,
#[serde(deserialize_with = "bool_from_int")] pub isAutoBlur: bool,
pub tags: String,
#[serde(deserialize_with = "bool_from_zero")]
pub commentStatus: bool,
pub timezone: String,
pub homePage: String,
#[serde(deserialize_with = "bool_from_int")] pub isEnableForwardPage: bool,
#[serde(deserialize_with = "bool_from_zero")]
pub userUAStatus: bool,
pub userIndexRedirectURL: String,
pub latestArticleTime: u64,
pub tagCount: u64,
pub nickname: String,
pub listViewMode: u64,
pub longestCheckinStreak: u64,
pub avatarType: String,
pub subMailSendTime: u64,
pub updateTime: u64,
#[serde(deserialize_with = "bool_from_zero")]
pub subMailStatus: bool,
#[serde(deserialize_with = "bool_from_zero")]
pub isJoinPointRank: bool,
pub latestLoginTime: u64,
pub userAppRole: u64,
pub userAvatarViewMode: u64,
pub userStatus: u64,
pub longestCheckinStreakEnd: u64,
#[serde(deserialize_with = "bool_from_zero")]
pub watchingArticleStatus: bool,
pub latestCmtTime: u64,
pub province: String,
pub currentCheckinStreak: u64,
pub userNo: u64,
pub avatarURL: String,
#[serde(deserialize_with = "bool_from_zero")]
pub followingTagStatus: bool,
pub userLanguage: String,
#[serde(deserialize_with = "bool_from_zero")]
pub isJoinUsedPointRank: bool,
pub currentCheckinStreakEnd: u64,
#[serde(deserialize_with = "bool_from_zero")]
pub followingArticleStatus: bool,
#[serde(deserialize_with = "bool_from_zero")]
pub keyboardShortcutsStatus: bool,
#[serde(deserialize_with = "bool_from_zero")]
pub replyWatchArticleStatus: bool,
pub commentViewMode: u64,
#[serde(deserialize_with = "bool_from_zero")]
pub breezemoonStatus: bool,
pub userCheckinTime: u64,
pub usedPoint: u64,
#[serde(deserialize_with = "bool_from_zero")]
pub articleStatus: bool,
pub userPoint: u64,
pub commentCount: u64,
pub userIntro: String,
pub userMobileSkin: String,
pub listPageSize: u64,
pub oId: String,
pub userName: String,
#[serde(deserialize_with = "bool_from_zero")]
pub geoStatus: bool,
pub longestCheckinStreakStart: u64,
pub userSkin: String,
#[serde(deserialize_with = "bool_from_zero")]
pub notifyStatus: bool,
#[serde(deserialize_with = "bool_from_zero")]
pub followingUserStatus: bool,
pub articleCount: u64,
pub userRole: String,
#[serde(deserialize_with = "deserialize_sys_metal")]
pub sysMetal: Vec<Metal>,
}
impl ArticleAuthor {
pub fn from_value(data: &Value) -> Result<Self, Error> {
parse_with_float_fallback(data, "ArticleAuthor")
}
}
pub type CommentAuthor = ArticleAuthor;
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default)]
#[allow(non_snake_case)]
pub struct ArticleComment {
#[serde(rename = "commentNice")]
pub isNice: bool,
#[serde(rename = "commentCreateTimeStr")]
pub createTimeStr: String,
#[serde(rename = "commentAuthorId")]
pub authorId: String,
#[serde(deserialize_with = "deserialize_score")]
pub score: String,
#[serde(rename = "commentCreateTime")]
pub createTime: String,
#[serde(rename = "commentAuthorURL")]
pub authorURL: String,
#[serde(deserialize_with = "deserialize_vote")]
pub vote: VoteStatus,
#[serde(rename = "commentRevisionCount")]
pub revisionCount: u64,
#[serde(rename = "timeAgo")]
pub timeAgo: String,
#[serde(rename = "commentOriginalCommentId")]
pub replyId: String,
#[serde(deserialize_with = "deserialize_sys_metal")]
pub sysMetal: Vec<Metal>,
#[serde(rename = "commentGoodCnt")]
pub goodCnt: u64,
#[serde(deserialize_with = "bool_from_zero")]
pub visible: bool,
#[serde(rename = "commentOnArticleId")]
pub articleId: String,
#[serde(rename = "rewardedCnt")]
pub rewardedCnt: u64,
#[serde(rename = "commentSharpURL")]
pub sharpURL: String,
#[serde(deserialize_with = "bool_from_int")]
pub isAnonymous: bool,
#[serde(rename = "commentReplyCnt")]
pub replyCnt: u64,
#[serde(rename = "oId")]
pub oId: String,
#[serde(rename = "commentContent")]
pub content: String,
#[serde(deserialize_with = "deserialize_status")]
pub status: ArticleStatus,
pub commenter: CommentAuthor,
#[serde(rename = "commentAuthorName")]
pub author: String,
#[serde(rename = "commentThankCnt")]
pub thankCnt: u64,
#[serde(rename = "commentBadCnt")]
pub badCnt: u64,
#[serde(rename = "rewarded")]
pub rewarded: bool,
#[serde(rename = "commentAuthorThumbnailURL")]
pub thumbnailURL: String,
#[serde(rename = "commentAudioURL")]
pub audioURL: String,
#[serde(rename = "commentQnAOffered")]
pub offered: u64,
}
impl ArticleComment {
pub fn from_value(data: &Value) -> Result<Self, Error> {
parse_with_float_fallback(data, "ArticleComment")
}
}
#[derive(Clone, Debug, Default, Deserialize)]
#[allow(non_snake_case)]
pub struct Pagination {
#[serde(rename = "paginationPageCount")]
pub count: u32,
#[serde(rename = "paginationPageNums")]
pub pageNums: Vec<u32>,
}
impl Pagination {
pub fn from_value(data: &Value) -> Result<Self, Error> {
parse_with_float_fallback(data, "Pagination")
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[repr(u8)]
#[derive(Default)]
pub enum ArticleType {
Normal = 0,
Private = 1,
Broadcast = 2,
Thought = 3,
#[default]
Unknown = 4,
Question = 5,
}
impl ArticleType {
pub fn from_index(index: usize) -> Self {
match index {
0 => ArticleType::Normal,
1 => ArticleType::Private,
2 => ArticleType::Broadcast,
3 => ArticleType::Thought,
5 => ArticleType::Question,
_ => ArticleType::Unknown,
}
}
}
fn default_article_type() -> ArticleType {
ArticleType::Unknown
}
pub fn deserialize_type<'de, D>(deserializer: D) -> Result<ArticleType, D::Error>
where
D: Deserializer<'de>,
{
let value: u64 = Deserialize::deserialize(deserializer)?;
Ok(ArticleType::from_index(value as usize))
}
pub fn deserialize_reddit_score<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
let value: u64 = Deserialize::deserialize(deserializer)?;
Ok(value.to_string())
}
pub fn deserialize_tag_objs<'de, D>(deserializer: D) -> Result<Vec<ArticleTag>, D::Error>
where
D: Deserializer<'de>,
{
let arr: Vec<Value> = Deserialize::deserialize(deserializer)?;
arr.into_iter()
.map(|v| ArticleTag::from_value(&v))
.collect::<Result<Vec<_>, _>>()
.map_err(serde::de::Error::custom)
}
pub fn deserialize_author<'de, D>(deserializer: D) -> Result<ArticleAuthor, D::Error>
where
D: Deserializer<'de>,
{
let value: Value = Deserialize::deserialize(deserializer)?;
ArticleAuthor::from_value(&value).map_err(serde::de::Error::custom)
}
pub fn deserialize_pagination<'de, D>(deserializer: D) -> Result<Option<Pagination>, D::Error>
where
D: Deserializer<'de>,
{
let value: Option<Value> = Deserialize::deserialize(deserializer)?;
match value {
Some(v) => Pagination::from_value(&v)
.map(Some)
.map_err(serde::de::Error::custom),
None => Ok(None),
}
}
pub fn deserialize_comments<'de, D>(deserializer: D) -> Result<Vec<ArticleComment>, D::Error>
where
D: Deserializer<'de>,
{
let arr: Vec<Value> = Deserialize::deserialize(deserializer)?;
arr.into_iter()
.map(|v| ArticleComment::from_value(&v))
.collect::<Result<Vec<_>, _>>()
.map_err(serde::de::Error::custom)
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default)]
#[allow(non_snake_case)]
pub struct ArticleDetail {
#[serde(rename = "articleShowInList", deserialize_with = "bool_from_int")]
pub showInList: bool,
#[serde(rename = "articleCreateTime")]
pub createTime: String,
#[serde(rename = "articleAuthorId")]
pub authorId: String,
#[serde(rename = "articleBadCnt")]
pub badCnt: u32,
#[serde(rename = "articleLatestCmtTime")]
pub latestCmtTime: String,
#[serde(rename = "articleGoodCnt")]
pub goodCnt: u32,
#[serde(rename = "articleQnAOfferPoint")]
pub offerPoint: u64,
#[serde(rename = "articleThumbnailURL")]
pub thumbnailURL: String,
#[serde(rename = "articleStickRemains")]
pub stickRemains: u64,
#[serde(rename = "timeAgo")]
pub timeAgo: String,
#[serde(rename = "articleUpdateTimeStr")]
pub updateTimeStr: String,
#[serde(rename = "articleAuthorName")]
pub authorName: String,
#[serde(
rename = "articleType",
default = "default_article_type",
deserialize_with = "deserialize_type"
)]
pub type_: ArticleType,
#[serde(rename = "offered")]
pub offered: bool,
#[serde(rename = "articleCreateTimeStr")]
pub createTimeStr: String,
#[serde(rename = "articleViewCount")]
pub viewCnt: u64,
#[serde(rename = "articleAuthorThumbnailURL20")]
pub thumbnailURL20: String,
#[serde(rename = "articleWatchCnt")]
pub watchCnt: u64,
#[serde(rename = "articlePreviewContent")]
pub previewContent: String,
#[serde(rename = "articleTitleEmoj")]
pub titleEmoj: String,
#[serde(rename = "articleTitleEmojUnicode")]
pub titleEmojUnicode: String,
#[serde(rename = "articleTitle")]
pub title: String,
#[serde(rename = "articleAuthorThumbnailURL48")]
pub thumbnailURL48: String,
#[serde(rename = "articleCommentCount")]
pub commentCnt: u64,
#[serde(rename = "articleCollectCnt")]
pub collectCnt: u64,
#[serde(rename = "articleLatestCmterName")]
pub latestCmterName: String,
#[serde(rename = "articleTags")]
pub tags: String,
#[serde(rename = "oId")]
pub oId: String,
#[serde(rename = "cmtTimeAgo")]
pub cmtTimeAgo: String,
#[serde(rename = "articleStick")]
pub stick: u64,
#[serde(
rename = "articleTagObjs",
default,
deserialize_with = "deserialize_tag_objs"
)]
pub tagObjs: Vec<ArticleTag>,
#[serde(rename = "articleLatestCmtTimeStr")]
pub latestCmtTimeStr: String,
#[serde(rename = "articleAnonymous", deserialize_with = "bool_from_int")]
pub anonymous: bool,
#[serde(rename = "articleThankCnt")]
pub thankCnt: u64,
#[serde(rename = "articleUpdateTime")]
pub updateTime: String,
#[serde(rename = "articleStatus", deserialize_with = "deserialize_status")]
pub status: ArticleStatus,
#[serde(rename = "articleHeat")]
pub heat: u64,
#[serde(rename = "articlePerfect", deserialize_with = "bool_from_int")]
pub perfect: bool,
#[serde(rename = "articleAuthorThumbnailURL210")]
pub thumbnailURL210: String,
#[serde(rename = "articlePermalink")]
pub permalink: String,
#[serde(
rename = "articleAuthor",
default,
deserialize_with = "deserialize_author"
)]
pub author: ArticleAuthor,
#[serde(rename = "thankedCnt")]
pub thankedCnt: u64,
#[serde(rename = "articleAnonymousView")]
pub anonymousView: u64,
#[serde(rename = "articleViewCntDisplayFormat")]
pub viewCntFormat: String,
#[serde(rename = "articleCommentable")]
pub commentable: bool,
#[serde(rename = "rewarded")]
pub rewarded: bool,
#[serde(rename = "rewardedCnt")]
pub rewardedCnt: u64,
#[serde(rename = "articleRewardPoint")]
pub rewardPoint: u64,
#[serde(rename = "isFollowing")]
pub isFollowing: bool,
#[serde(rename = "isWatching")]
pub isWatching: bool,
#[serde(rename = "isMyArticle")]
pub isMyArticle: bool,
#[serde(rename = "thanked")]
pub thanked: bool,
#[serde(rename = "articleEditorType")]
pub editorType: u64,
#[serde(rename = "articleAudioURL")]
pub audioURL: String,
#[serde(rename = "articleToC")]
pub table: String,
#[serde(rename = "articleContent")]
pub content: String,
#[serde(rename = "articleOriginalContent")]
pub source: String,
#[serde(rename = "articleImg1URL")]
pub img1URL: String,
#[serde(rename = "articleVote", deserialize_with = "deserialize_vote")]
pub vote: VoteStatus,
#[serde(rename = "articleRandomDouble")]
pub randomDouble: f64,
#[serde(rename = "articleAuthorIntro")]
pub authorIntro: String,
#[serde(rename = "articleCity")]
pub city: String,
#[serde(rename = "articleIP")]
pub IP: String,
#[serde(rename = "articleAuthorURL")]
pub authorURL: String,
#[serde(rename = "articlePushOrder")]
pub pushOrder: u64,
#[serde(rename = "articleRewardContent")]
pub rewardContent: String,
#[serde(deserialize_with = "deserialize_reddit_score")]
pub redditScore: String,
#[serde(default, deserialize_with = "deserialize_pagination")]
pub pagination: Option<Pagination>,
#[serde(rename = "discussionViewable")]
pub commentViewable: bool,
#[serde(rename = "articleRevisionCount")]
pub revisionCount: u64,
#[serde(
rename = "articleComments",
default,
deserialize_with = "deserialize_comments"
)]
pub comments: Vec<ArticleComment>,
#[serde(
rename = "articleNiceComments",
default,
deserialize_with = "deserialize_comments"
)]
pub niceComments: Vec<ArticleComment>,
}
impl ArticleDetail {
pub fn from_value(data: &Value) -> Result<Self, Error> {
parse_with_float_fallback(data, "ArticleDetail")
}
}
pub fn deserialize_articles<'de, D>(deserializer: D) -> Result<Vec<ArticleDetail>, D::Error>
where
D: Deserializer<'de>,
{
let arr: Vec<Value> = Deserialize::deserialize(deserializer)?;
arr.into_iter()
.map(|v| ArticleDetail::from_value(&v))
.collect::<Result<Vec<_>, _>>()
.map_err(serde::de::Error::custom)
}
#[derive(Clone, Debug, Deserialize)]
#[allow(non_snake_case)]
pub struct ArticleList {
#[serde(rename = "articles", deserialize_with = "deserialize_articles")]
pub list: Vec<ArticleDetail>,
pub pagination: Pagination,
pub tag: Option<ArticleTag>,
}
impl ArticleList {
pub fn from_value(data: &Value) -> Result<Self, Error> {
parse_with_float_fallback(data, "ArticleList")
}
}
#[derive(Clone, Debug)]
pub enum ArticleListType {
Recent,
Hot,
Good,
Reply,
Perfect,
}
impl_str_enum! {
ArticleListType {
Recent => "recent",
Hot => "hot",
Good => "good",
Reply => "reply",
Perfect => "perfect",
}
}
impl ArticleListType {
pub fn to_code(&self) -> &'static str {
match self {
ArticleListType::Recent => "",
ArticleListType::Hot => "/hot",
ArticleListType::Good => "/good",
ArticleListType::Reply => "/reply",
ArticleListType::Perfect => "/perfect",
}
}
pub fn values() -> Vec<Self> {
vec![
ArticleListType::Recent,
ArticleListType::Hot,
ArticleListType::Good,
ArticleListType::Reply,
ArticleListType::Perfect,
]
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[allow(non_snake_case)]
pub struct CommentPost {
pub articleId: String,
#[serde(rename = "commentAnonymous")]
pub isAnonymous: bool,
#[serde(rename = "commentVisible")]
pub isVisible: bool,
#[serde(rename = "commentContent")]
pub content: String,
#[serde(rename = "commentOriginalCommentId")]
pub replyId: String,
}
impl CommentPost {
pub fn from_value(data: &Value) -> Result<Self, Error> {
serde_json::from_value(data.clone())
.map_err(|e| Error::Parse(format!("Failed to parse CommentPost: {}", e)))
}
pub fn to_value(&self) -> Result<Value, Error> {
serde_json::to_value(self)
.map_err(|e| Error::Parse(format!("Failed to serialize CommentPost: {}", e)))
}
}