diff --git a/native/kotlin/api/kotlin/src/integrationTest/kotlin/TagsEndpointTest.kt b/native/kotlin/api/kotlin/src/integrationTest/kotlin/TagsEndpointTest.kt new file mode 100644 index 00000000..5231150d --- /dev/null +++ b/native/kotlin/api/kotlin/src/integrationTest/kotlin/TagsEndpointTest.kt @@ -0,0 +1,23 @@ +package rs.wordpress.api.kotlin + +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import uniffi.wp_api.TagListParams +import uniffi.wp_api.wpAuthenticationFromUsernameAndPassword + +class TagsEndpointTest { + private val testCredentials = TestCredentials.INSTANCE + private val siteUrl = testCredentials.parsedSiteUrl + private val authentication = wpAuthenticationFromUsernameAndPassword( + username = testCredentials.adminUsername, password = testCredentials.adminPassword + ) + private val client = WpApiClient(siteUrl, authentication) + + @Test + fun testTagListRequest() = runTest { + val tagList = client.request { requestBuilder -> + requestBuilder.tags().listWithEditContext(params = TagListParams()) + }.assertSuccessAndRetrieveData().data + assert(tagList.isNotEmpty()) + } +} diff --git a/wp_api/src/api_client.rs b/wp_api/src/api_client.rs index 5b792678..4bb60364 100644 --- a/wp_api/src/api_client.rs +++ b/wp_api/src/api_client.rs @@ -9,6 +9,7 @@ use crate::request::{ post_types_endpoint::{PostTypesRequestBuilder, PostTypesRequestExecutor}, posts_endpoint::{PostsRequestBuilder, PostsRequestExecutor}, site_settings_endpoint::{SiteSettingsRequestBuilder, SiteSettingsRequestExecutor}, + tags_endpoint::{TagsRequestBuilder, TagsRequestExecutor}, taxonomies_endpoint::{TaxonomiesRequestBuilder, TaxonomiesRequestExecutor}, users_endpoint::{UsersRequestBuilder, UsersRequestExecutor}, wp_site_health_tests_endpoint::{ @@ -48,6 +49,7 @@ pub struct WpApiRequestBuilder { post_types: Arc, posts: Arc, site_settings: Arc, + tags: Arc, taxonomies: Arc, users: Arc, wp_site_health_tests: Arc, @@ -65,9 +67,10 @@ impl WpApiRequestBuilder { plugins, post_types, posts, + site_settings, + tags, taxonomies, users, - site_settings, wp_site_health_tests ) } @@ -101,6 +104,7 @@ pub struct WpApiClient { post_types: Arc, posts: Arc, site_settings: Arc, + tags: Arc, taxonomies: Arc, users: Arc, wp_site_health_tests: Arc, @@ -125,6 +129,7 @@ impl WpApiClient { post_types, posts, site_settings, + tags, taxonomies, users, wp_site_health_tests @@ -139,6 +144,7 @@ api_client_generate_endpoint_impl!(WpApi, plugins); api_client_generate_endpoint_impl!(WpApi, post_types); api_client_generate_endpoint_impl!(WpApi, posts); api_client_generate_endpoint_impl!(WpApi, site_settings); +api_client_generate_endpoint_impl!(WpApi, tags); api_client_generate_endpoint_impl!(WpApi, taxonomies); api_client_generate_endpoint_impl!(WpApi, users); api_client_generate_endpoint_impl!(WpApi, wp_site_health_tests); diff --git a/wp_api/src/lib.rs b/wp_api/src/lib.rs index 39d6577f..6514b405 100644 --- a/wp_api/src/lib.rs +++ b/wp_api/src/lib.rs @@ -27,6 +27,7 @@ pub mod post_types; pub mod posts; pub mod request; pub mod site_settings; +pub mod tags; pub mod taxonomies; pub mod url_query; pub mod users; diff --git a/wp_api/src/posts.rs b/wp_api/src/posts.rs index b17a127f..9a174bda 100644 --- a/wp_api/src/posts.rs +++ b/wp_api/src/posts.rs @@ -8,6 +8,7 @@ use wp_serde_helper::{deserialize_from_string_of_json_array, serialize_as_json_s use crate::{ impl_as_query_value_for_new_type, impl_as_query_value_from_as_str, media::MediaId, + tags::TagId, url_query::{ AppendUrlQueryPairs, AsQueryValue, FromUrlQueryPairs, QueryPairs, QueryPairsExtension, UrlQueryPairsMap, @@ -536,19 +537,6 @@ impl std::fmt::Display for PostId { } } -impl_as_query_value_for_new_type!(TagId); -uniffi::custom_newtype!(TagId, i64); -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub struct TagId(pub i64); - -impl FromStr for TagId { - type Err = ParseIntError; - - fn from_str(s: &str) -> Result { - s.parse().map(Self) - } -} - impl_as_query_value_for_new_type!(CategoryId); uniffi::custom_newtype!(CategoryId, i64); #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] diff --git a/wp_api/src/request/endpoint.rs b/wp_api/src/request/endpoint.rs index 46037c6a..438b9268 100644 --- a/wp_api/src/request/endpoint.rs +++ b/wp_api/src/request/endpoint.rs @@ -9,6 +9,7 @@ pub mod plugins_endpoint; pub mod post_types_endpoint; pub mod posts_endpoint; pub mod site_settings_endpoint; +pub mod tags_endpoint; pub mod taxonomies_endpoint; pub mod users_endpoint; pub mod wp_site_health_tests_endpoint; diff --git a/wp_api/src/request/endpoint/posts_endpoint.rs b/wp_api/src/request/endpoint/posts_endpoint.rs index 4e5cc873..47a67922 100644 --- a/wp_api/src/request/endpoint/posts_endpoint.rs +++ b/wp_api/src/request/endpoint/posts_endpoint.rs @@ -73,13 +73,14 @@ mod tests { use crate::{ generate, posts::{ - CategoryId, PostRetrieveParams, PostStatus, TagId, WpApiParamPostsOrderBy, + CategoryId, PostRetrieveParams, PostStatus, WpApiParamPostsOrderBy, WpApiParamPostsSearchColumn, WpApiParamPostsTaxRelation, }, request::endpoint::{ tests::{fixture_api_base_url, validate_wp_v2_endpoint}, ApiBaseUrl, }, + tags::TagId, UserId, WpApiParamOrder, }; use rstest::*; diff --git a/wp_api/src/request/endpoint/tags_endpoint.rs b/wp_api/src/request/endpoint/tags_endpoint.rs new file mode 100644 index 00000000..460f0392 --- /dev/null +++ b/wp_api/src/request/endpoint/tags_endpoint.rs @@ -0,0 +1,25 @@ +use super::{AsNamespace, DerivedRequest, WpNamespace}; +use crate::{ + tags::{ + SparseTagFieldWithEditContext, SparseTagFieldWithEmbedContext, + SparseTagFieldWithViewContext, TagListParams, + }, + SparseField, +}; +use wp_derive_request_builder::WpDerivedRequest; + +#[derive(WpDerivedRequest)] +enum TagsRequest { + #[contextual_paged(url = "/tags", params = &TagListParams, output = Vec, filter_by = crate::tags::SparseTagField)] + List, +} + +impl DerivedRequest for TagsRequest { + fn namespace() -> impl AsNamespace { + WpNamespace::WpV2 + } +} + +super::macros::default_sparse_field_implementation_from_field_name!(SparseTagFieldWithEditContext); +super::macros::default_sparse_field_implementation_from_field_name!(SparseTagFieldWithEmbedContext); +super::macros::default_sparse_field_implementation_from_field_name!(SparseTagFieldWithViewContext); diff --git a/wp_api/src/tags.rs b/wp_api/src/tags.rs new file mode 100644 index 00000000..37049bd3 --- /dev/null +++ b/wp_api/src/tags.rs @@ -0,0 +1,206 @@ +use std::{num::ParseIntError, str::FromStr}; + +use serde::{Deserialize, Serialize}; +use strum_macros::IntoStaticStr; +use wp_contextual::WpContextual; + +use crate::{ + impl_as_query_value_for_new_type, impl_as_query_value_from_as_str, + posts::PostId, + taxonomies::TaxonomyType, + url_query::{ + AppendUrlQueryPairs, AsQueryValue, FromUrlQueryPairs, QueryPairs, QueryPairsExtension, + UrlQueryPairsMap, + }, + EnumFromStrParsingError, WpApiParamOrder, +}; + +impl_as_query_value_for_new_type!(TagId); +uniffi::custom_newtype!(TagId, i64); +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct TagId(pub i64); + +impl FromStr for TagId { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + s.parse().map(Self) + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, uniffi::Enum)] +pub enum WpApiParamTagsOrderBy { + Id, + Include, + #[default] + Name, + Slug, + IncludeSlugs, + TermGroup, + Description, + Count, +} + +impl_as_query_value_from_as_str!(WpApiParamTagsOrderBy); + +impl WpApiParamTagsOrderBy { + fn as_str(&self) -> &str { + match self { + Self::Id => "id", + Self::Include => "include", + Self::Name => "name", + Self::Slug => "slug", + Self::IncludeSlugs => "include_slugs", + Self::TermGroup => "term_group", + Self::Description => "description", + Self::Count => "count", + } + } +} + +impl FromStr for WpApiParamTagsOrderBy { + type Err = EnumFromStrParsingError; + + fn from_str(s: &str) -> Result { + match s { + "id" => Ok(Self::Id), + "include" => Ok(Self::Include), + "name" => Ok(Self::Name), + "slug" => Ok(Self::Slug), + "include_slugs" => Ok(Self::IncludeSlugs), + "term_group" => Ok(Self::TermGroup), + "description" => Ok(Self::Description), + "count" => Ok(Self::Count), + value => Err(EnumFromStrParsingError::UnknownVariant { + value: value.to_string(), + }), + } + } +} + +#[derive(Debug, Default, PartialEq, Eq, uniffi::Record)] +pub struct TagListParams { + /// Current page of the collection. + /// Default: `1` + #[uniffi(default = None)] + pub page: Option, + /// Maximum number of items to be returned in result set. + /// Default: `10` + #[uniffi(default = None)] + pub per_page: Option, + /// Limit results to those matching a string. + #[uniffi(default = None)] + pub search: Option, + /// Ensure result set excludes specific IDs. + #[uniffi(default = [])] + pub exclude: Vec, + /// Limit result set to specific IDs. + #[uniffi(default = [])] + pub include: Vec, + /// Offset the result set by a specific number of items. + #[uniffi(default = None)] + pub offset: Option, + /// Order sort attribute ascending or descending. + /// Default: `asc` + /// One of: `asc`, `desc` + #[uniffi(default = None)] + pub order: Option, + /// Sort collection by user attribute. + /// Default: `name` + /// One of: `id`, `include`, `name`, `slug`, `include_slugs`, `term_group`, `description`, `count` + #[uniffi(default = None)] + pub orderby: Option, + /// Whether to hide terms not assigned to any posts. + #[uniffi(default = None)] + pub hide_empty: Option, + /// Limit result set to terms assigned to a specific post. + #[uniffi(default = None)] + pub post: Option, + /// Limit result set to users with one or more specific slugs. + #[uniffi(default = [])] + pub slug: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, IntoStaticStr)] +enum TagListParamsField { + #[strum(serialize = "page")] + Page, + #[strum(serialize = "per_page")] + PerPage, + #[strum(serialize = "search")] + Search, + #[strum(serialize = "exclude")] + Exclude, + #[strum(serialize = "include")] + Include, + #[strum(serialize = "offset")] + Offset, + #[strum(serialize = "order")] + Order, + #[strum(serialize = "orderby")] + Orderby, + #[strum(serialize = "hide_empty")] + HideEmpty, + #[strum(serialize = "post")] + Post, + #[strum(serialize = "slug")] + Slug, +} + +impl AppendUrlQueryPairs for TagListParams { + fn append_query_pairs(&self, query_pairs_mut: &mut QueryPairs) { + query_pairs_mut + .append_option_query_value_pair(TagListParamsField::Page, self.page.as_ref()) + .append_option_query_value_pair(TagListParamsField::PerPage, self.per_page.as_ref()) + .append_option_query_value_pair(TagListParamsField::Search, self.search.as_ref()) + .append_vec_query_value_pair(TagListParamsField::Exclude, &self.exclude) + .append_vec_query_value_pair(TagListParamsField::Include, &self.include) + .append_option_query_value_pair(TagListParamsField::Offset, self.offset.as_ref()) + .append_option_query_value_pair(TagListParamsField::Order, self.order.as_ref()) + .append_option_query_value_pair(TagListParamsField::Orderby, self.orderby.as_ref()) + .append_option_query_value_pair(TagListParamsField::HideEmpty, self.hide_empty.as_ref()) + .append_option_query_value_pair(TagListParamsField::Post, self.post.as_ref()) + .append_vec_query_value_pair(TagListParamsField::Slug, &self.slug); + } +} + +impl FromUrlQueryPairs for TagListParams { + fn from_url_query_pairs(query_pairs: UrlQueryPairsMap) -> Option { + Some(Self { + page: query_pairs.get(TagListParamsField::Page), + per_page: query_pairs.get(TagListParamsField::PerPage), + search: query_pairs.get(TagListParamsField::Search), + exclude: query_pairs.get_csv(TagListParamsField::Exclude), + include: query_pairs.get_csv(TagListParamsField::Include), + offset: query_pairs.get(TagListParamsField::Offset), + order: query_pairs.get(TagListParamsField::Order), + orderby: query_pairs.get(TagListParamsField::Orderby), + hide_empty: query_pairs.get(TagListParamsField::HideEmpty), + post: query_pairs.get(TagListParamsField::Post), + slug: query_pairs.get_csv(TagListParamsField::Slug), + }) + } + + fn supports_pagination() -> bool { + true + } +} + +#[derive(Debug, Serialize, Deserialize, uniffi::Record, WpContextual)] +pub struct SparseTag { + #[WpContext(edit, embed, view)] + pub id: Option, + #[WpContext(edit, view)] + pub count: Option, + #[WpContext(edit, view)] + pub description: Option, + #[WpContext(edit, embed, view)] + pub link: Option, + #[WpContext(edit, embed, view)] + pub name: Option, + #[WpContext(edit, embed, view)] + pub slug: Option, + #[WpContext(edit, embed, view)] + pub taxonomy: Option, + // meta field is omitted for now: https://github.com/Automattic/wordpress-rs/issues/463 +} diff --git a/wp_api_integration_tests/src/lib.rs b/wp_api_integration_tests/src/lib.rs index 89af4548..d924a8be 100644 --- a/wp_api_integration_tests/src/lib.rs +++ b/wp_api_integration_tests/src/lib.rs @@ -5,11 +5,12 @@ use std::sync::Arc; use wp_api::{ comments::CommentId, media::MediaId, - posts::{CategoryId, PostId, TagId}, + posts::{CategoryId, PostId}, request::{ endpoint::media_endpoint::MediaUploadRequest, RequestExecutor, RequestMethod, WpNetworkHeaderMap, WpNetworkRequest, WpNetworkResponse, }, + tags::TagId, users::UserId, MediaUploadRequestExecutionError, ParsedUrl, RequestExecutionError, WpApiClient, WpApiError, WpAuthentication, WpErrorCode, diff --git a/wp_api_integration_tests/tests/test_posts_immut.rs b/wp_api_integration_tests/tests/test_posts_immut.rs index fb5f0ea6..900ce440 100644 --- a/wp_api_integration_tests/tests/test_posts_immut.rs +++ b/wp_api_integration_tests/tests/test_posts_immut.rs @@ -4,9 +4,10 @@ use serial_test::parallel; use wp_api::posts::{ CategoryId, PostId, PostListParams, PostRetrieveParams, PostStatus, SparsePostFieldWithEditContext, SparsePostFieldWithEmbedContext, - SparsePostFieldWithViewContext, TagId, WpApiParamPostsOrderBy, WpApiParamPostsSearchColumn, + SparsePostFieldWithViewContext, WpApiParamPostsOrderBy, WpApiParamPostsSearchColumn, WpApiParamPostsTaxRelation, }; +use wp_api::tags::TagId; use wp_api::{generate, WpApiParamOrder}; use wp_api_integration_tests::{ api_client, AssertResponse, TestCredentials, FIRST_POST_ID, FIRST_USER_ID, SECOND_USER_ID, diff --git a/wp_api_integration_tests/tests/test_tags_immut.rs b/wp_api_integration_tests/tests/test_tags_immut.rs new file mode 100644 index 00000000..fbbf01d9 --- /dev/null +++ b/wp_api_integration_tests/tests/test_tags_immut.rs @@ -0,0 +1,142 @@ +use rstest::*; +use rstest_reuse::{self, apply, template}; +use serial_test::parallel; +use wp_api::tags::{ + SparseTagFieldWithEditContext, SparseTagFieldWithEmbedContext, SparseTagFieldWithViewContext, + TagListParams, WpApiParamTagsOrderBy, +}; +use wp_api::{generate, WpApiParamOrder}; +use wp_api_integration_tests::{api_client, AssertResponse, FIRST_POST_ID, TAG_ID_100}; + +#[tokio::test] +#[apply(list_cases)] +#[parallel] +async fn list_with_edit_context(#[case] params: TagListParams) { + api_client() + .tags() + .list_with_edit_context(¶ms) + .await + .assert_response(); +} + +#[tokio::test] +#[apply(list_cases)] +#[parallel] +async fn list_with_embed_context(#[case] params: TagListParams) { + api_client() + .tags() + .list_with_embed_context(¶ms) + .await + .assert_response(); +} + +#[tokio::test] +#[apply(list_cases)] +#[parallel] +async fn list_with_view_context(#[case] params: TagListParams) { + api_client() + .tags() + .list_with_view_context(¶ms) + .await + .assert_response(); +} + +#[template] +#[rstest] +#[case::default(TagListParams::default())] +#[case::page(generate!(TagListParams, (page, Some(1))))] +#[case::per_page(generate!(TagListParams, (per_page, Some(3))))] +#[case::search(generate!(TagListParams, (search, Some("foo".to_string()))))] +#[case::exclude(generate!(TagListParams, (exclude, vec![TAG_ID_100])))] +#[case::include(generate!(TagListParams, (include, vec![TAG_ID_100])))] +#[case::offset(generate!(TagListParams, (offset, Some(2))))] +#[case::order(generate!(TagListParams, (order, Some(WpApiParamOrder::Asc))))] +#[case::orderby(generate!(TagListParams, (orderby, Some(WpApiParamTagsOrderBy::Id))))] +#[case::hide_empty_false(generate!(TagListParams, (hide_empty, Some(false))))] +#[case::hide_empty_true(generate!(TagListParams, (hide_empty, Some(true))))] +#[case::post(generate!(TagListParams, (post, Some(FIRST_POST_ID))))] +#[case::slug(generate!(TagListParams, (slug, vec!["foo".to_string(), "bar".to_string()])))] +pub fn list_cases(#[case] params: TagListParams) {} + +mod filter { + use super::*; + + wp_api::generate_sparse_tag_field_with_edit_context_test_cases!(); + wp_api::generate_sparse_tag_field_with_embed_context_test_cases!(); + wp_api::generate_sparse_tag_field_with_view_context_test_cases!(); + + #[apply(sparse_tag_field_with_edit_context_test_cases)] + #[case(&[SparseTagFieldWithEditContext::Name, SparseTagFieldWithEditContext::Slug])] + #[tokio::test] + #[parallel] + async fn filter_tags_with_edit_context( + #[case] fields: &[SparseTagFieldWithEditContext], + #[values( + TagListParams::default(), + generate!(TagListParams, (orderby, Some(WpApiParamTagsOrderBy::Id))), + generate!(TagListParams, (search, Some("foo".to_string()))) + )] + params: TagListParams, + ) { + api_client() + .tags() + .filter_list_with_edit_context(¶ms, fields) + .await + .assert_response() + .data + .iter() + .for_each(|tag| { + tag.assert_that_instance_fields_nullability_match_provided_fields(fields) + }); + } + + #[apply(sparse_tag_field_with_embed_context_test_cases)] + #[case(&[SparseTagFieldWithEmbedContext::Name, SparseTagFieldWithEmbedContext::Slug])] + #[tokio::test] + #[parallel] + async fn filter_tags_with_embed_context( + #[case] fields: &[SparseTagFieldWithEmbedContext], + #[values( + TagListParams::default(), + generate!(TagListParams, (orderby, Some(WpApiParamTagsOrderBy::Id))), + generate!(TagListParams, (search, Some("foo".to_string()))) + )] + params: TagListParams, + ) { + api_client() + .tags() + .filter_list_with_embed_context(¶ms, fields) + .await + .assert_response() + .data + .iter() + .for_each(|tag| { + tag.assert_that_instance_fields_nullability_match_provided_fields(fields) + }); + } + + #[apply(sparse_tag_field_with_view_context_test_cases)] + #[case(&[SparseTagFieldWithViewContext::Name, SparseTagFieldWithViewContext::Slug])] + #[tokio::test] + #[parallel] + async fn filter_tags_with_view_context( + #[case] fields: &[SparseTagFieldWithViewContext], + #[values( + TagListParams::default(), + generate!(TagListParams, (orderby, Some(WpApiParamTagsOrderBy::Id))), + generate!(TagListParams, (search, Some("foo".to_string()))) + )] + params: TagListParams, + ) { + api_client() + .tags() + .filter_list_with_view_context(¶ms, fields) + .await + .assert_response() + .data + .iter() + .for_each(|tag| { + tag.assert_that_instance_fields_nullability_match_provided_fields(fields) + }); + } +}