diff --git a/wp_api/src/comments.rs b/wp_api/src/comments.rs index 1380fb9e..2a5a4715 100644 --- a/wp_api/src/comments.rs +++ b/wp_api/src/comments.rs @@ -1,5 +1,5 @@ use crate::{ - UserAvatarSize, UserId, WpApiParamOrder, WpResponseString, + AnyJson, UserAvatarSize, UserId, WpApiParamOrder, WpResponseString, date::WpGmtDateTime, impl_as_query_value_for_new_type, impl_as_query_value_from_to_string, posts::PostId, @@ -8,7 +8,7 @@ use crate::{ }, }; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, num::ParseIntError, str::FromStr}; +use std::{collections::HashMap, num::ParseIntError, str::FromStr, sync::Arc}; use strum_macros::IntoStaticStr; use wp_contextual::WpContextual; @@ -528,6 +528,10 @@ pub struct SparseComment { pub comment_type: Option, #[WpContext(edit, embed, view)] pub author_avatar_urls: Option>, + #[serde(flatten)] + #[WpContext(edit, embed, view)] + #[WpContextualExcludeFromFields] + pub additional_fields: Option>, // meta field is omitted for now: https://github.com/Automattic/wordpress-rs/issues/422 } diff --git a/wp_api/src/lib.rs b/wp_api/src/lib.rs index c8a40984..2a9e6f8c 100644 --- a/wp_api/src/lib.rs +++ b/wp_api/src/lib.rs @@ -1,5 +1,6 @@ use plugins::*; use serde::{Deserialize, Serialize}; +use serde_json::Value; use std::collections::HashMap; use users::*; use wp_localization::{MessageBundle, WpMessages, WpSupportsLocalization}; @@ -118,6 +119,14 @@ pub enum JsonValue { Object(HashMap), } +/// Similar to `JsonValue`, but exported as a Uniffi object. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, uniffi::Object)] +#[uniffi::export(Eq, Hash)] +pub struct AnyJson { + #[serde(flatten)] + pub raw: Value, +} + uniffi::custom_newtype!(WpResponseString, Option); #[derive(Debug, Serialize, Deserialize)] #[serde(try_from = "BoolOrString")] @@ -235,4 +244,30 @@ mod tests { fn test_orderby_string_conversion(#[case] orderby: WpApiParamOrder) { assert_eq!(orderby, orderby.to_string().parse().unwrap()); } + + #[derive(Deserialize, Debug)] + struct Person { + name: String, + #[serde(flatten)] + other_fields: AnyJson, + } + + #[test] + fn test_parse_any_json() { + let json = r#"{"name": "Alice", "age": 30, "city": "Wonderland"}"#; + let person: Person = serde_json::from_str(json).unwrap(); + assert_eq!(person.name, "Alice"); + assert_eq!( + person.other_fields.raw, + serde_json::json!({"age": 30, "city": "Wonderland"}) + ); + } + + #[test] + fn test_parse_empty_any_json() { + let json = r#"{"name": "Alice"}"#; + let person: Person = serde_json::from_str(json).unwrap(); + assert_eq!(person.name, "Alice"); + assert_eq!(person.other_fields.raw, serde_json::json!({})); + } } diff --git a/wp_api/src/uniffi_serde.rs b/wp_api/src/uniffi_serde.rs index 41b53ba6..48d2f6ad 100644 --- a/wp_api/src/uniffi_serde.rs +++ b/wp_api/src/uniffi_serde.rs @@ -2,7 +2,7 @@ use wp_localization::{MessageBundle, WpMessages, WpSupportsLocalization}; use wp_localization_macro::WpDeriveLocalizable; #[derive(Debug, thiserror::Error, uniffi::Error, WpDeriveLocalizable)] -pub(crate) enum UniffiSerializationError { +pub enum UniffiSerializationError { Serde { reason: String }, } diff --git a/wp_api/src/wp_com/endpoint.rs b/wp_api/src/wp_com/endpoint.rs index 9c91f4b8..ccd283af 100644 --- a/wp_api/src/wp_com/endpoint.rs +++ b/wp_api/src/wp_com/endpoint.rs @@ -6,6 +6,7 @@ use crate::{ use std::sync::Arc; use strum::IntoEnumIterator; +pub mod extensions; pub mod followers_endpoint; pub mod jetpack_connection_endpoint; pub mod oauth2; diff --git a/wp_api/src/wp_com/endpoint/extensions/comments.rs b/wp_api/src/wp_com/endpoint/extensions/comments.rs new file mode 100644 index 00000000..de71a280 --- /dev/null +++ b/wp_api/src/wp_com/endpoint/extensions/comments.rs @@ -0,0 +1,42 @@ +use serde::Deserialize; + +use crate::{AnyJson, uniffi_serde::UniffiSerializationError}; + +#[derive(Debug, Deserialize, uniffi::Record)] +pub struct WpComCommentExtension { + #[serde(rename = "extended_post")] + pub post: Option, + #[serde(rename = "extended_i_replied")] + pub i_replied: bool, + #[serde(rename = "extended_like_count")] + pub like_count: u32, + #[serde(rename = "extended_i_like")] + pub i_like: bool, +} + +#[derive(Debug, Deserialize, uniffi::Record)] +pub struct WpComCommentExtensionPostInfo { + pub id: u64, + pub title: String, + #[serde(rename = "type")] + pub kind: String, + pub link: String, +} + +#[uniffi::export(with_foreign)] +pub trait WpComCommentExtensionProvider: Send + Sync { + fn parse_wpcom_comments_extension( + &self, + ) -> Result; +} + +#[uniffi::export] +impl WpComCommentExtensionProvider for AnyJson { + fn parse_wpcom_comments_extension( + &self, + ) -> Result { + serde_json::to_string(&self.raw) + .and_then(|json| serde_json::from_str(&json)) + .map_err(Into::into) + } +} diff --git a/wp_api/src/wp_com/endpoint/extensions/mod.rs b/wp_api/src/wp_com/endpoint/extensions/mod.rs new file mode 100644 index 00000000..a5c2bb6d --- /dev/null +++ b/wp_api/src/wp_com/endpoint/extensions/mod.rs @@ -0,0 +1 @@ +pub mod comments; diff --git a/wp_api_integration_tests/src/lib.rs b/wp_api_integration_tests/src/lib.rs index 8c1f5e9a..485fdfab 100644 --- a/wp_api_integration_tests/src/lib.rs +++ b/wp_api_integration_tests/src/lib.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -use wp_api::wp_com::client::WpComApiClient; +use wp_api::wp_com::{WpComBaseUrl, client::WpComApiClient, endpoint::WpComDotOrgApiUrlResolver}; pub mod mock; pub mod prelude; @@ -42,6 +42,7 @@ pub struct WpComTestCredentials { pub site_id: u64, pub wp_com_subscriber_user_id: i64, pub email_subscriber_subscription_id: u64, + pub comment_id: i64, } pub mod backend; @@ -152,6 +153,25 @@ pub fn wp_com_client() -> WpComApiClient { }) } +pub fn api_client_backed_by_wp_com(site_id: String) -> WpApiClient { + WpApiClient::new( + Arc::new(WpComDotOrgApiUrlResolver::new( + site_id, + WpComBaseUrl::Production, + )), + WpApiClientDelegate { + auth_provider: Arc::new(WpAuthenticationProvider::static_with_auth( + WpAuthentication::Bearer { + token: WpComTestCredentials::instance().bearer_token.to_string(), + }, + )), + request_executor: Arc::new(ReqwestRequestExecutor::default()), + middleware_pipeline: Arc::new(WpApiMiddlewarePipeline::default()), + app_notifier: Arc::new(EmptyAppNotifier), + }, + ) +} + pub fn test_site_url() -> ParsedUrl { let mut url: Url = TestCredentials::instance() .site_url diff --git a/wp_api_integration_tests/tests/test_comments_immut.rs b/wp_api_integration_tests/tests/test_comments_immut.rs index 0d27d41f..06b1dacd 100644 --- a/wp_api_integration_tests/tests/test_comments_immut.rs +++ b/wp_api_integration_tests/tests/test_comments_immut.rs @@ -1,3 +1,4 @@ +use serde_json::Value; use wp_api::{ comments::{ CommentId, CommentListParams, CommentRetrieveParams, CommentStatus, CommentType, @@ -207,6 +208,23 @@ async fn list_comments_with_edit_context_parse_author_avatar_urls( }); } +#[tokio::test] +#[parallel] +async fn parse_extras() { + let comment = api_client() + .comments() + .retrieve_with_edit_context(&FIRST_COMMENT_ID, &CommentRetrieveParams::default()) + .await + .assert_response() + .data; + match comment.additional_fields.raw { + Value::Object(ref map) => { + assert!(map.contains_key("_links")); + } + _ => panic!("Expected extras to be an object"), + } +} + #[template] #[rstest] #[case::default(CommentListParams::default())] diff --git a/wp_api_integration_tests/tests/test_wp_com_comments_immut.rs b/wp_api_integration_tests/tests/test_wp_com_comments_immut.rs new file mode 100644 index 00000000..698c5599 --- /dev/null +++ b/wp_api_integration_tests/tests/test_wp_com_comments_immut.rs @@ -0,0 +1,72 @@ +use wp_api::{ + comments::{CommentId, CommentRetrieveParams}, + wp_com::endpoint::extensions::comments::WpComCommentExtensionProvider, +}; + +use wp_api_integration_tests::{WpComTestCredentials, api_client_backed_by_wp_com, prelude::*}; + +#[tokio::test] +#[parallel] +#[ignore] +async fn parse_extension_view_context() { + let site_id = WpComTestCredentials::instance().site_id.to_string(); + let comment_id = CommentId(WpComTestCredentials::instance().comment_id); + let client = api_client_backed_by_wp_com(site_id); + + let comment = client + .comments() + .retrieve_with_view_context(&comment_id, &CommentRetrieveParams::default()) + .await + .assert_response() + .data; + assert!( + comment + .additional_fields + .parse_wpcom_comments_extension() + .is_ok() + ); +} + +#[tokio::test] +#[parallel] +#[ignore] +async fn parse_extension_edit_context() { + let site_id = WpComTestCredentials::instance().site_id.to_string(); + let comment_id = CommentId(WpComTestCredentials::instance().comment_id); + let client = api_client_backed_by_wp_com(site_id); + + let comment = client + .comments() + .retrieve_with_edit_context(&comment_id, &CommentRetrieveParams::default()) + .await + .assert_response() + .data; + assert!( + comment + .additional_fields + .parse_wpcom_comments_extension() + .is_ok() + ); +} + +#[tokio::test] +#[parallel] +#[ignore] +async fn parse_extension_embed_context() { + let site_id = WpComTestCredentials::instance().site_id.to_string(); + let comment_id = CommentId(WpComTestCredentials::instance().comment_id); + let client = api_client_backed_by_wp_com(site_id); + + let comment = client + .comments() + .retrieve_with_embed_context(&comment_id, &CommentRetrieveParams::default()) + .await + .assert_response() + .data; + assert!( + comment + .additional_fields + .parse_wpcom_comments_extension() + .is_ok() + ); +} diff --git a/wp_com_test_credentials.json-example b/wp_com_test_credentials.json-example index c73890f5..654d8553 100644 --- a/wp_com_test_credentials.json-example +++ b/wp_com_test_credentials.json-example @@ -2,5 +2,6 @@ "bearer_token": "replace_with_your_oauth2_token", "site_id": 0, "wp_com_subscriber_user_id": 0, - "email_subscriber_subscription_id": 0 + "email_subscriber_subscription_id": 0, + "comment_id": 0 } diff --git a/wp_contextual/src/wp_contextual.rs b/wp_contextual/src/wp_contextual.rs index 332d46f0..fa7e5570 100644 --- a/wp_contextual/src/wp_contextual.rs +++ b/wp_contextual/src/wp_contextual.rs @@ -275,6 +275,9 @@ fn generate_integration_test_helper( let mut assertions = Vec::with_capacity(fields.len()); let mut rs_test_cases = Vec::with_capacity(fields.len()); for f in fields { + if f.is_wp_contextual_exclude_from_fields { + continue; + } if let Some(f_ident) = &f.field.ident { let variant_ident = format_ident!("{}", f_ident.to_string().to_case(Case::UpperCamel)); let field_ident_str = f_ident.to_string();