diff --git a/Cargo.lock b/Cargo.lock index b14319f..a6f8126 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -316,6 +316,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8" + [[package]] name = "core-foundation" version = "0.9.3" @@ -359,7 +365,7 @@ version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", @@ -1330,6 +1336,7 @@ dependencies = [ name = "proc_macros" version = "0.0.1" dependencies = [ + "convert_case 0.5.0", "parse_duration", "quote", "syn", diff --git a/proc_macros/Cargo.toml b/proc_macros/Cargo.toml index f70487c..ae92068 100644 --- a/proc_macros/Cargo.toml +++ b/proc_macros/Cargo.toml @@ -11,6 +11,7 @@ path = "proc_macros.rs" proc-macro = true [dependencies] +convert_case = "0.5" parse_duration = "2" quote = "1" syn = { version = "1", features = ["full"] } diff --git a/proc_macros/proc_macros.rs b/proc_macros/proc_macros.rs index 461236c..4e2a9e8 100644 --- a/proc_macros/proc_macros.rs +++ b/proc_macros/proc_macros.rs @@ -1,3 +1,5 @@ +use convert_case::Case; +use convert_case::Casing; use proc_macro::TokenStream; use quote::quote; use syn::bracketed; @@ -511,3 +513,70 @@ pub fn filter_http_methods(item: TokenStream) -> TokenStream { }; stream.into() } + +struct CheckJsonKeys { + pub keys: Vec<(LitStr, bool)>, +} + +impl Parse for CheckJsonKeys { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let mut keys = Vec::new(); + let first: LitStr = input.parse()?; + match input.parse::() { + Ok(_) => { + keys.push((first, true)); + } + Err(_) => { + keys.push((first, false)); + } + } + while !input.is_empty() { + let _: token::Comma = input.parse()?; + if input.is_empty() { + break; + } + let key: LitStr = input.parse()?; + match input.parse::() { + Ok(_) => { + keys.push((key, true)); + } + Err(_) => { + keys.push((key, false)); + } + } + } + Ok(Self { keys }) + } +} + +#[proc_macro] +pub fn check_json_keys(item: TokenStream) -> TokenStream { + let CheckJsonKeys { keys } = parse_macro_input!(item as CheckJsonKeys); + let mut streams = Vec::new(); + for (key, check) in keys { + if check { + let k = key.value(); + let k = k.to_case(Case::Snake); + let fun = Ident::new(&k, key.span()); + streams.push(quote!(#key => { + self.#fun().try_err(gettext("The value of the key is missing.").replace("", key))?; + })); + } else { + streams.push(quote!(#key => {})); + } + } + let stream = quote!( + { + use crate::ext::try_err::TryErr; + use crate::gettext; + self.data.is_object().try_err(gettext("Data is not a object."))?; + for (key, _) in self.data.entries() { + match key { + #(#streams)* + _ => { Err(format!("{} {}", gettext("Key is handled:").replace("", key).as_str(), self.data))?; } + } + } + } + ); + stream.into() +} diff --git a/src/ext/try_err.rs b/src/ext/try_err.rs index 118b46a..67519fe 100644 --- a/src/ext/try_err.rs +++ b/src/ext/try_err.rs @@ -36,3 +36,13 @@ impl TryErr for Result { } } } + +impl TryErr<(), E> for bool { + fn try_err(self, err: E) -> Result<(), E> { + if self { + Ok(()) + } else { + Err(err) + } + } +} diff --git a/src/fanbox/article/block.rs b/src/fanbox/article/block.rs new file mode 100644 index 0000000..2bb7173 --- /dev/null +++ b/src/fanbox/article/block.rs @@ -0,0 +1,122 @@ +use super::super::check::CheckUnkown; +use json::JsonValue; +use proc_macros::check_json_keys; +use std::fmt::Debug; + +pub struct FanboxArticleParagraphBoldStyle { + pub data: JsonValue, +} + +impl FanboxArticleParagraphBoldStyle { + #[inline] + pub fn length(&self) -> Option { + self.data["length"].as_u64() + } + + #[inline] + /// Create a new instance + pub fn new(data: &JsonValue) -> Self { + Self { data: data.clone() } + } + + #[inline] + pub fn offset(&self) -> Option { + self.data["offset"].as_u64() + } +} + +impl CheckUnkown for FanboxArticleParagraphBoldStyle { + fn check_unknown(&self) -> Result<(), crate::fanbox::error::FanboxAPIError> { + check_json_keys!("offset"+, "length"+,); + Ok(()) + } +} + +impl Debug for FanboxArticleParagraphBoldStyle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FanboxArticleParagraphBoldStyle") + .field("length", &self.length()) + .field("offset", &self.offset()) + .finish_non_exhaustive() + } +} + +#[derive(Debug)] +pub enum FanboxArticleParagraphStyle { + Bold(FanboxArticleParagraphBoldStyle), + Unknown(JsonValue), +} + +impl FanboxArticleParagraphStyle { + #[inline] + /// Create a new instance + pub fn new(data: &JsonValue) -> Self { + match data["type"].as_str() { + Some(t) => match t { + "bold" => Self::Bold(FanboxArticleParagraphBoldStyle::new(data)), + _ => Self::Unknown(data.clone()), + }, + None => Self::Unknown(data.clone()), + } + } +} + +pub struct FanboxArticleParagraphBlock { + pub data: JsonValue, +} + +impl FanboxArticleParagraphBlock { + #[inline] + /// Create a new instance + pub fn new(data: &JsonValue) -> Self { + Self { data: data.clone() } + } + + #[inline] + pub fn styles(&self) -> Option> { + let styles = &self.data["styles"]; + if styles.is_array() { + let mut list = Vec::new(); + for i in styles.members() { + list.push(FanboxArticleParagraphStyle::new(i)) + } + Some(list) + } else { + None + } + } + + #[inline] + pub fn text(&self) -> Option<&str> { + self.data["text"].as_str() + } +} + +impl Debug for FanboxArticleParagraphBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FanboxArticleParagraphBlock") + .field("styles", &self.styles()) + .field("text", &self.text()) + .finish_non_exhaustive() + } +} + +#[derive(Debug)] +pub enum FanboxArticleBlock { + Paragraph(FanboxArticleParagraphBlock), + Unknown(JsonValue), +} + +impl FanboxArticleBlock { + #[inline] + /// Create a new instance + pub fn new(data: &JsonValue) -> Self { + match data["type"].as_str() { + Some(t) => match t { + "p" => Self::Paragraph(FanboxArticleParagraphBlock::new(data)), + _ => Self::Unknown(data.clone()), + }, + None => Self::Unknown(data.clone()), + } + } +} diff --git a/src/fanbox/article/body.rs b/src/fanbox/article/body.rs new file mode 100644 index 0000000..4306ec9 --- /dev/null +++ b/src/fanbox/article/body.rs @@ -0,0 +1,45 @@ +use super::block::FanboxArticleBlock; +use crate::fanbox_api::FanboxClientInternal; +use json::JsonValue; +use std::fmt::Debug; +use std::sync::Arc; + +/// Article body +pub struct FanboxArticleBody { + /// Raw data + pub data: JsonValue, + client: Arc, +} + +impl FanboxArticleBody { + #[inline] + pub fn blocks(&self) -> Option> { + let blocks = &self.data["blocks"]; + if blocks.is_array() { + let mut list = Vec::new(); + for i in blocks.members() { + list.push(FanboxArticleBlock::new(i)) + } + Some(list) + } else { + None + } + } + + #[inline] + /// Create a new instance + pub fn new(data: &JsonValue, client: Arc) -> Self { + Self { + data: data.clone(), + client, + } + } +} + +impl Debug for FanboxArticleBody { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FanboxArticleBody") + .field("blocks", &self.blocks()) + .finish_non_exhaustive() + } +} diff --git a/src/fanbox/article/mod.rs b/src/fanbox/article/mod.rs new file mode 100644 index 0000000..17590e6 --- /dev/null +++ b/src/fanbox/article/mod.rs @@ -0,0 +1,2 @@ +pub mod block; +pub mod body; diff --git a/src/fanbox/check.rs b/src/fanbox/check.rs new file mode 100644 index 0000000..b519dec --- /dev/null +++ b/src/fanbox/check.rs @@ -0,0 +1,6 @@ +use super::error::FanboxAPIError; + +/// Check if have data that we don't handle +pub trait CheckUnkown { + fn check_unknown(&self) -> Result<(), FanboxAPIError>; +} diff --git a/src/fanbox/mod.rs b/src/fanbox/mod.rs index 538c406..556ff09 100644 --- a/src/fanbox/mod.rs +++ b/src/fanbox/mod.rs @@ -1,3 +1,5 @@ +pub mod article; +pub mod check; pub mod comment; pub mod comment_list; pub mod creator; diff --git a/src/fanbox/post.rs b/src/fanbox/post.rs index 499b140..b218e0c 100644 --- a/src/fanbox/post.rs +++ b/src/fanbox/post.rs @@ -1,3 +1,4 @@ +use super::article::body::FanboxArticleBody; use super::comment_list::FanboxCommentList; use crate::fanbox_api::FanboxClientInternal; use crate::parser::json::parse_u64; @@ -16,6 +17,11 @@ pub struct FanboxPostArticle { } impl FanboxPostArticle { + #[inline] + pub fn body(&self) -> FanboxArticleBody { + FanboxArticleBody::new(&self.data["body"], Arc::clone(&self.client)) + } + #[inline] pub fn comment_count(&self) -> Option { self.data["commentCount"].as_u64() @@ -161,6 +167,7 @@ impl Debug for FanboxPostArticle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("FanboxPostArticle") .field("id", &self.id()) + .field("body", &self.body()) .field("comment_count", &self.comment_count()) .field("comment_list", &self.comment_list()) .field("cover_image_url", &self.cover_image_url())