diff --git a/native/kotlin/api/kotlin/src/integrationTest/kotlin/CategoriesEndpointTest.kt b/native/kotlin/api/kotlin/src/integrationTest/kotlin/CategoriesEndpointTest.kt index a687b50e..ecdd56af 100644 --- a/native/kotlin/api/kotlin/src/integrationTest/kotlin/CategoriesEndpointTest.kt +++ b/native/kotlin/api/kotlin/src/integrationTest/kotlin/CategoriesEndpointTest.kt @@ -2,7 +2,6 @@ package rs.wordpress.api.kotlin import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test -import uniffi.wp_api.PostListParams import uniffi.wp_api.SparseCategoryFieldWithEditContext import uniffi.wp_api.CategoryCreateParams import uniffi.wp_api.CategoryListParams @@ -48,7 +47,7 @@ class CategoriesEndpointTest { } @Test - fun testRetrieveMediaRequest() = runTest { + fun testRetrieveCategoryRequest() = runTest { val category = client.request { requestBuilder -> requestBuilder.categories().retrieveWithEditContext(CATEGORY_ID_59) }.assertSuccessAndRetrieveData().data diff --git a/native/kotlin/api/kotlin/src/integrationTest/kotlin/ThemesEndpointTest.kt b/native/kotlin/api/kotlin/src/integrationTest/kotlin/ThemesEndpointTest.kt new file mode 100644 index 00000000..7c77e6f6 --- /dev/null +++ b/native/kotlin/api/kotlin/src/integrationTest/kotlin/ThemesEndpointTest.kt @@ -0,0 +1,78 @@ +package rs.wordpress.api.kotlin + +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import uniffi.wp_api.SparseThemeFieldWithEditContext +import uniffi.wp_api.ThemeListParams +import uniffi.wp_api.ThemeStylesheet +import uniffi.wp_api.WpErrorCode +import uniffi.wp_api.wpAuthenticationFromUsernameAndPassword +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +private const val THEME_TWENTY_TWENTY_FIVE: String = "twentytwentyfive" + +class ThemesEndpointTest { + 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 testThemeListRequest() = runTest { + val themeList = client.request { requestBuilder -> + requestBuilder.themes().listWithEditContext(params = ThemeListParams()) + }.assertSuccessAndRetrieveData().data + assert(themeList.isNotEmpty()) + } + + @Test + fun testFilterThemeListRequest() = runTest { + val themeList = client.request { requestBuilder -> + requestBuilder.themes().filterListWithEditContext( + params = ThemeListParams(), + fields = listOf( + SparseThemeFieldWithEditContext.NAME, + SparseThemeFieldWithEditContext.AUTHOR + ) + ) + }.assertSuccessAndRetrieveData().data + assert(themeList.isNotEmpty()) + assertNull(themeList.first().description) + } + + @Test + fun testRetrieveThemeRequest() = runTest { + val theme = client.request { requestBuilder -> + requestBuilder.themes() + .retrieveWithEditContext(ThemeStylesheet(THEME_TWENTY_TWENTY_FIVE)) + }.assertSuccessAndRetrieveData().data + assertNotNull(theme) + } + + @Test + fun testFilterRetrieveThemeRequest() = runTest { + val theme = client.request { requestBuilder -> + requestBuilder.themes().filterRetrieveWithEditContext( + ThemeStylesheet(THEME_TWENTY_TWENTY_FIVE), + fields = listOf( + SparseThemeFieldWithEditContext.NAME, + SparseThemeFieldWithEditContext.STYLESHEET + ) + ) + }.assertSuccessAndRetrieveData().data + assertNull(theme.description) + } + + @Test + fun testErrorThemeNotFound() = runTest { + val result = + client.request { requestBuilder -> + requestBuilder.themes() + .retrieveWithEditContext(ThemeStylesheet("invalid_stylesheet")) + } + assert(result.wpErrorCode() is WpErrorCode.ThemeNotFound) + } +} \ No newline at end of file diff --git a/wp_api/src/api_client.rs b/wp_api/src/api_client.rs index 6ed1ae9d..915a2734 100644 --- a/wp_api/src/api_client.rs +++ b/wp_api/src/api_client.rs @@ -12,6 +12,7 @@ use crate::request::{ site_settings_endpoint::{SiteSettingsRequestBuilder, SiteSettingsRequestExecutor}, tags_endpoint::{TagsRequestBuilder, TagsRequestExecutor}, taxonomies_endpoint::{TaxonomiesRequestBuilder, TaxonomiesRequestExecutor}, + themes_endpoint::{ThemesRequestBuilder, ThemesRequestExecutor}, users_endpoint::{UsersRequestBuilder, UsersRequestExecutor}, wp_site_health_tests_endpoint::{ WpSiteHealthTestsRequestBuilder, WpSiteHealthTestsRequestExecutor, @@ -53,6 +54,7 @@ pub struct WpApiRequestBuilder { site_settings: Arc, tags: Arc, taxonomies: Arc, + themes: Arc, users: Arc, wp_site_health_tests: Arc, } @@ -73,6 +75,7 @@ impl WpApiRequestBuilder { site_settings, tags, taxonomies, + themes, users, wp_site_health_tests ) @@ -110,6 +113,7 @@ pub struct WpApiClient { site_settings: Arc, tags: Arc, taxonomies: Arc, + themes: Arc, users: Arc, wp_site_health_tests: Arc, } @@ -136,6 +140,7 @@ impl WpApiClient { site_settings, tags, taxonomies, + themes, users, wp_site_health_tests ) @@ -152,6 +157,7 @@ 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, themes); api_client_generate_endpoint_impl!(WpApi, users); api_client_generate_endpoint_impl!(WpApi, wp_site_health_tests); diff --git a/wp_api/src/api_error.rs b/wp_api/src/api_error.rs index ef798542..6838bb45 100644 --- a/wp_api/src/api_error.rs +++ b/wp_api/src/api_error.rs @@ -151,10 +151,14 @@ pub enum WpErrorCode { CannotUpdate, #[serde(rename = "rest_cannot_view")] CannotView, + #[serde(rename = "rest_cannot_view_active_theme")] + CannotViewActiveTheme, #[serde(rename = "rest_cannot_view_plugin")] CannotViewPlugin, #[serde(rename = "rest_cannot_view_plugins")] CannotViewPlugins, + #[serde(rename = "rest_cannot_view_themes")] + CannotViewThemes, #[serde(rename = "comment_author_column_length")] CommentAuthorColumnLength, #[serde(rename = "rest_comment_author_data_required")] @@ -217,6 +221,8 @@ pub enum WpErrorCode { TaxonomyInvalid, #[serde(rename = "rest_term_invalid")] TermInvalid, + #[serde(rename = "rest_theme_not_found")] + ThemeNotFound, #[serde(rename = "rest_type_invalid")] TypeInvalid, #[serde(rename = "rest_not_logged_in")] diff --git a/wp_api/src/lib.rs b/wp_api/src/lib.rs index 1c1743bd..3f998210 100644 --- a/wp_api/src/lib.rs +++ b/wp_api/src/lib.rs @@ -30,6 +30,7 @@ pub mod request; pub mod site_settings; pub mod tags; pub mod taxonomies; +pub mod themes; pub mod url_query; pub mod users; pub mod wordpress_org; @@ -189,6 +190,13 @@ pub enum BoolOrString { String(String), } +#[derive(Debug, Serialize, Deserialize, uniffi::Enum)] +#[serde(untagged)] +pub enum BoolOrVecString { + Bool(bool), + VecString(Vec), +} + #[macro_export] macro_rules! generate { ($type_name:ident) => { diff --git a/wp_api/src/request/endpoint.rs b/wp_api/src/request/endpoint.rs index 2c4d0fa7..f2de0ecc 100644 --- a/wp_api/src/request/endpoint.rs +++ b/wp_api/src/request/endpoint.rs @@ -12,6 +12,7 @@ pub mod posts_endpoint; pub mod site_settings_endpoint; pub mod tags_endpoint; pub mod taxonomies_endpoint; +pub mod themes_endpoint; pub mod users_endpoint; pub mod wp_site_health_tests_endpoint; diff --git a/wp_api/src/request/endpoint/themes_endpoint.rs b/wp_api/src/request/endpoint/themes_endpoint.rs new file mode 100644 index 00000000..7f45b8b8 --- /dev/null +++ b/wp_api/src/request/endpoint/themes_endpoint.rs @@ -0,0 +1,262 @@ +use crate::themes::{ + SparseThemeFieldWithEditContext, SparseThemeFieldWithEmbedContext, + SparseThemeFieldWithViewContext, ThemeStylesheet, +}; +use crate::SparseField; +use wp_derive_request_builder::WpDerivedRequest; + +use super::{AsNamespace, DerivedRequest, WpNamespace}; +#[derive(WpDerivedRequest)] +enum ThemesRequest { + #[contextual_get(url = "/themes", params = &crate::themes::ThemeListParams, output = Vec, filter_by = crate::themes::SparseThemeField)] + List, + #[contextual_get(url = "/themes/", output = crate::themes::SparseTheme, filter_by = crate::themes::SparseThemeField)] + Retrieve, +} + +impl DerivedRequest for ThemesRequest { + fn namespace() -> impl AsNamespace { + WpNamespace::WpV2 + } +} + +super::macros::default_sparse_field_implementation_from_field_name!( + SparseThemeFieldWithEditContext +); +super::macros::default_sparse_field_implementation_from_field_name!( + SparseThemeFieldWithEmbedContext +); +super::macros::default_sparse_field_implementation_from_field_name!( + SparseThemeFieldWithViewContext +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + generate, + request::endpoint::{ + tests::{fixture_api_base_url, validate_wp_v2_endpoint}, + ApiBaseUrl, + }, + themes::{ThemeListParams, ThemeStatus}, + }; + use rstest::*; + use std::sync::Arc; + + #[rstest] + #[case(ThemeListParams::default(), "")] + #[case(generate!(ThemeListParams, (status, Some(ThemeStatus::Active))), "status=active")] + #[case(generate!(ThemeListParams, (status, Some(ThemeStatus::Inactive))), "status=inactive")] + fn list_themes( + endpoint: ThemesRequestEndpoint, + #[case] params: ThemeListParams, + #[case] expected_additional_params: &str, + ) { + let expected_path = |context: &str| { + if expected_additional_params.is_empty() { + format!("/themes?context={}", context) + } else { + format!("/themes?context={}&{}", context, expected_additional_params) + } + }; + validate_wp_v2_endpoint( + endpoint.list_with_edit_context(¶ms), + &expected_path("edit"), + ); + validate_wp_v2_endpoint( + endpoint.list_with_embed_context(¶ms), + &expected_path("embed"), + ); + validate_wp_v2_endpoint( + endpoint.list_with_view_context(¶ms), + &expected_path("view"), + ); + } + + #[rstest] + #[case(ThemeListParams::default(), &[], "/themes?context=edit&_fields=")] + #[case(ThemeListParams::default(), ALL_SPARSE_THEME_FIELDS_WITH_EDIT_CONTEXT, &format!("/themes?context=edit&{}", EXPECTED_QUERY_PAIRS_FOR_ALL_SPARSE_THEME_FIELDS_WITH_EDIT_CONTEXT))] + #[case(generate!(ThemeListParams, (status, Some(ThemeStatus::Active))), &[], "/themes?context=edit&status=active&_fields=")] + #[case(generate!(ThemeListParams, (status, Some(ThemeStatus::Inactive))), ALL_SPARSE_THEME_FIELDS_WITH_EDIT_CONTEXT, &format!("/themes?context=edit&status=inactive&{}", EXPECTED_QUERY_PAIRS_FOR_ALL_SPARSE_THEME_FIELDS_WITH_EDIT_CONTEXT))] + fn filter_list_themes_with_edit_context( + endpoint: ThemesRequestEndpoint, + #[case] params: ThemeListParams, + #[case] fields: &[SparseThemeFieldWithEditContext], + #[case] expected_path: &str, + ) { + validate_wp_v2_endpoint( + endpoint.filter_list_with_edit_context(¶ms, fields), + expected_path, + ); + } + + #[rstest] + #[case(ThemeListParams::default(), &[], "/themes?context=embed&_fields=")] + #[case(ThemeListParams::default(), ALL_SPARSE_THEME_FIELDS_WITH_EMBED_CONTEXT, &format!("/themes?context=embed&{}", EXPECTED_QUERY_PAIRS_FOR_ALL_SPARSE_THEME_FIELDS_WITH_EMBED_CONTEXT))] + #[case(generate!(ThemeListParams, (status, Some(ThemeStatus::Active))), &[], "/themes?context=embed&status=active&_fields=")] + #[case(generate!(ThemeListParams, (status, Some(ThemeStatus::Inactive))), ALL_SPARSE_THEME_FIELDS_WITH_EMBED_CONTEXT, &format!("/themes?context=embed&status=inactive&{}", EXPECTED_QUERY_PAIRS_FOR_ALL_SPARSE_THEME_FIELDS_WITH_EMBED_CONTEXT))] + fn filter_list_themes_with_embed_context( + endpoint: ThemesRequestEndpoint, + #[case] params: ThemeListParams, + #[case] fields: &[SparseThemeFieldWithEmbedContext], + #[case] expected_path: &str, + ) { + validate_wp_v2_endpoint( + endpoint.filter_list_with_embed_context(¶ms, fields), + expected_path, + ); + } + + #[rstest] + #[case(ThemeListParams::default(), &[], "/themes?context=view&_fields=")] + #[case(ThemeListParams::default(), ALL_SPARSE_THEME_FIELDS_WITH_VIEW_CONTEXT, &format!("/themes?context=view&{}", EXPECTED_QUERY_PAIRS_FOR_ALL_SPARSE_THEME_FIELDS_WITH_VIEW_CONTEXT))] + #[case(generate!(ThemeListParams, (status, Some(ThemeStatus::Active))), &[], "/themes?context=view&status=active&_fields=")] + #[case(generate!(ThemeListParams, (status, Some(ThemeStatus::Inactive))), ALL_SPARSE_THEME_FIELDS_WITH_VIEW_CONTEXT, &format!("/themes?context=view&status=inactive&{}", EXPECTED_QUERY_PAIRS_FOR_ALL_SPARSE_THEME_FIELDS_WITH_VIEW_CONTEXT))] + fn filter_list_themes_with_view_context( + endpoint: ThemesRequestEndpoint, + #[case] params: ThemeListParams, + #[case] fields: &[SparseThemeFieldWithViewContext], + #[case] expected_path: &str, + ) { + validate_wp_v2_endpoint( + endpoint.filter_list_with_view_context(¶ms, fields), + expected_path, + ); + } + + #[rstest] + fn retrieve_theme(endpoint: ThemesRequestEndpoint) { + let theme_stylesheet: ThemeStylesheet = "foo".into(); + let expected_path = |context: &str| format!("/themes/foo?context={}", context); + validate_wp_v2_endpoint( + endpoint.retrieve_with_edit_context(&theme_stylesheet), + &expected_path("edit"), + ); + validate_wp_v2_endpoint( + endpoint.retrieve_with_embed_context(&theme_stylesheet), + &expected_path("embed"), + ); + validate_wp_v2_endpoint( + endpoint.retrieve_with_view_context(&theme_stylesheet), + &expected_path("view"), + ); + } + + #[rstest] + #[case(&[], "/themes/foo?context=edit&_fields=")] + #[case(&[SparseThemeFieldWithEditContext::Author], "/themes/foo?context=edit&_fields=author")] + #[case(ALL_SPARSE_THEME_FIELDS_WITH_EDIT_CONTEXT, &format!("/themes/foo?context=edit&{}", EXPECTED_QUERY_PAIRS_FOR_ALL_SPARSE_THEME_FIELDS_WITH_EDIT_CONTEXT))] + fn filter_retrieve_theme_with_edit_context( + endpoint: ThemesRequestEndpoint, + #[case] fields: &[SparseThemeFieldWithEditContext], + #[case] expected_path: &str, + ) { + validate_wp_v2_endpoint( + endpoint.filter_retrieve_with_edit_context(&"foo".into(), fields), + expected_path, + ); + } + + #[rstest] + #[case(&[], "/themes/foo?context=embed&_fields=")] + #[case(&[SparseThemeFieldWithEmbedContext::Author], "/themes/foo?context=embed&_fields=author")] + #[case(ALL_SPARSE_THEME_FIELDS_WITH_EMBED_CONTEXT, &format!("/themes/foo?context=embed&{}", EXPECTED_QUERY_PAIRS_FOR_ALL_SPARSE_THEME_FIELDS_WITH_EMBED_CONTEXT))] + fn filter_retrieve_theme_with_embed_context( + endpoint: ThemesRequestEndpoint, + #[case] fields: &[SparseThemeFieldWithEmbedContext], + #[case] expected_path: &str, + ) { + validate_wp_v2_endpoint( + endpoint.filter_retrieve_with_embed_context(&"foo".into(), fields), + expected_path, + ); + } + + #[rstest] + #[case(&[], "/themes/foo?context=view&_fields=")] + #[case(&[SparseThemeFieldWithViewContext::Author], "/themes/foo?context=view&_fields=author")] + #[case(ALL_SPARSE_THEME_FIELDS_WITH_VIEW_CONTEXT, &format!("/themes/foo?context=view&{}", EXPECTED_QUERY_PAIRS_FOR_ALL_SPARSE_THEME_FIELDS_WITH_VIEW_CONTEXT))] + fn filter_retrieve_theme_with_view_context( + endpoint: ThemesRequestEndpoint, + #[case] fields: &[SparseThemeFieldWithViewContext], + #[case] expected_path: &str, + ) { + validate_wp_v2_endpoint( + endpoint.filter_retrieve_with_view_context(&"foo".into(), fields), + expected_path, + ); + } + + const EXPECTED_QUERY_PAIRS_FOR_ALL_SPARSE_THEME_FIELDS_WITH_EDIT_CONTEXT: &str = "_fields=stylesheet%2Ctemplate%2Crequires_php%2Crequires_wp%2Ctextdomain%2Cversion%2Cscreenshot%2Cauthor%2Cauthor_uri%2Cdescription%2Cname%2Ctags%2Ctheme_uri%2Cstatus%2Cis_block_theme%2Cstylesheet_uri%2Ctemplate_uri%2Ctheme_supports"; + const ALL_SPARSE_THEME_FIELDS_WITH_EDIT_CONTEXT: &[SparseThemeFieldWithEditContext; 18] = &[ + SparseThemeFieldWithEditContext::Stylesheet, + SparseThemeFieldWithEditContext::Template, + SparseThemeFieldWithEditContext::RequiresPhp, + SparseThemeFieldWithEditContext::RequiresWp, + SparseThemeFieldWithEditContext::Textdomain, + SparseThemeFieldWithEditContext::Version, + SparseThemeFieldWithEditContext::Screenshot, + SparseThemeFieldWithEditContext::Author, + SparseThemeFieldWithEditContext::AuthorUri, + SparseThemeFieldWithEditContext::Description, + SparseThemeFieldWithEditContext::Name, + SparseThemeFieldWithEditContext::Tags, + SparseThemeFieldWithEditContext::ThemeUri, + SparseThemeFieldWithEditContext::Status, + SparseThemeFieldWithEditContext::IsBlockTheme, + SparseThemeFieldWithEditContext::StylesheetUri, + SparseThemeFieldWithEditContext::TemplateUri, + SparseThemeFieldWithEditContext::ThemeSupports, + ]; + + const EXPECTED_QUERY_PAIRS_FOR_ALL_SPARSE_THEME_FIELDS_WITH_EMBED_CONTEXT: &str = + "_fields=stylesheet%2Ctemplate%2Crequires_php%2Crequires_wp%2Ctextdomain%2Cversion%2Cscreenshot%2Cauthor%2Cauthor_uri%2Cdescription%2Cname%2Ctags%2Ctheme_uri%2Cstatus%2Cis_block_theme%2Cstylesheet_uri%2Ctemplate_uri%2Ctheme_supports"; + const ALL_SPARSE_THEME_FIELDS_WITH_EMBED_CONTEXT: &[SparseThemeFieldWithEmbedContext; 18] = &[ + SparseThemeFieldWithEmbedContext::Stylesheet, + SparseThemeFieldWithEmbedContext::Template, + SparseThemeFieldWithEmbedContext::RequiresPhp, + SparseThemeFieldWithEmbedContext::RequiresWp, + SparseThemeFieldWithEmbedContext::Textdomain, + SparseThemeFieldWithEmbedContext::Version, + SparseThemeFieldWithEmbedContext::Screenshot, + SparseThemeFieldWithEmbedContext::Author, + SparseThemeFieldWithEmbedContext::AuthorUri, + SparseThemeFieldWithEmbedContext::Description, + SparseThemeFieldWithEmbedContext::Name, + SparseThemeFieldWithEmbedContext::Tags, + SparseThemeFieldWithEmbedContext::ThemeUri, + SparseThemeFieldWithEmbedContext::Status, + SparseThemeFieldWithEmbedContext::IsBlockTheme, + SparseThemeFieldWithEmbedContext::StylesheetUri, + SparseThemeFieldWithEmbedContext::TemplateUri, + SparseThemeFieldWithEmbedContext::ThemeSupports, + ]; + + const EXPECTED_QUERY_PAIRS_FOR_ALL_SPARSE_THEME_FIELDS_WITH_VIEW_CONTEXT: &str = "_fields=stylesheet%2Ctemplate%2Crequires_php%2Crequires_wp%2Ctextdomain%2Cversion%2Cscreenshot%2Cauthor%2Cauthor_uri%2Cdescription%2Cname%2Ctags%2Ctheme_uri%2Cstatus%2Cis_block_theme%2Cstylesheet_uri%2Ctemplate_uri%2Ctheme_supports"; + const ALL_SPARSE_THEME_FIELDS_WITH_VIEW_CONTEXT: &[SparseThemeFieldWithViewContext; 18] = &[ + SparseThemeFieldWithViewContext::Stylesheet, + SparseThemeFieldWithViewContext::Template, + SparseThemeFieldWithViewContext::RequiresPhp, + SparseThemeFieldWithViewContext::RequiresWp, + SparseThemeFieldWithViewContext::Textdomain, + SparseThemeFieldWithViewContext::Version, + SparseThemeFieldWithViewContext::Screenshot, + SparseThemeFieldWithViewContext::Author, + SparseThemeFieldWithViewContext::AuthorUri, + SparseThemeFieldWithViewContext::Description, + SparseThemeFieldWithViewContext::Name, + SparseThemeFieldWithViewContext::Tags, + SparseThemeFieldWithViewContext::ThemeUri, + SparseThemeFieldWithViewContext::Status, + SparseThemeFieldWithViewContext::IsBlockTheme, + SparseThemeFieldWithViewContext::StylesheetUri, + SparseThemeFieldWithViewContext::TemplateUri, + SparseThemeFieldWithViewContext::ThemeSupports, + ]; + + #[fixture] + fn endpoint(fixture_api_base_url: Arc) -> ThemesRequestEndpoint { + ThemesRequestEndpoint::new(fixture_api_base_url) + } +} diff --git a/wp_api/src/themes.rs b/wp_api/src/themes.rs new file mode 100644 index 00000000..58422e19 --- /dev/null +++ b/wp_api/src/themes.rs @@ -0,0 +1,226 @@ +use crate::{ + impl_as_query_value_from_as_str, + url_query::{ + AppendUrlQueryPairs, FromUrlQueryPairs, QueryPairs, QueryPairsExtension, UrlQueryPairsMap, + }, + AsQueryValue, BoolOrVecString, EnumFromStrParsingError, +}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, fmt::Display, str::FromStr}; +use strum_macros::IntoStaticStr; +use wp_contextual::WpContextual; + +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, uniffi::Enum, +)] +#[serde(rename_all = "snake_case")] +pub enum ThemeStatus { + Active, + Inactive, + #[serde(untagged)] + Custom(String), +} + +impl_as_query_value_from_as_str!(ThemeStatus); + +impl ThemeStatus { + pub fn as_str(&self) -> &str { + match self { + Self::Active => "active", + Self::Inactive => "inactive", + Self::Custom(status) => status, + } + } +} + +impl FromStr for ThemeStatus { + type Err = EnumFromStrParsingError; + + fn from_str(s: &str) -> Result { + match s { + "active" => Ok(Self::Active), + "inactive" => Ok(Self::Inactive), + value => Ok(Self::Custom(value.to_string())), + } + } +} + +#[derive(Debug, Default, PartialEq, Eq, uniffi::Record)] +pub struct ThemeListParams { + /// Limit result set to themes assigned one or more statuses. + #[uniffi(default = None)] + pub status: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, IntoStaticStr)] +enum ThemeListParamsField { + #[strum(serialize = "status")] + Status, +} + +impl AppendUrlQueryPairs for ThemeListParams { + fn append_query_pairs(&self, query_pairs_mut: &mut QueryPairs) { + query_pairs_mut + .append_option_query_value_pair(ThemeListParamsField::Status, self.status.as_ref()); + } +} + +impl FromUrlQueryPairs for ThemeListParams { + fn from_url_query_pairs(query_pairs: UrlQueryPairsMap) -> Option { + Some(Self { + status: query_pairs.get(ThemeListParamsField::Status), + }) + } + + fn supports_pagination() -> bool { + true + } +} + +#[derive(Debug, Serialize, Deserialize, uniffi::Record, WpContextual)] +pub struct SparseTheme { + #[WpContext(edit, embed, view)] + pub stylesheet: Option, + #[WpContext(edit, embed, view)] + pub template: Option, + #[WpContext(edit, embed, view)] + pub requires_php: Option, + #[WpContext(edit, embed, view)] + pub requires_wp: Option, + #[WpContext(edit, embed, view)] + pub textdomain: Option, + #[WpContext(edit, embed, view)] + pub version: Option, + #[WpContext(edit, embed, view)] + pub screenshot: Option, + #[WpContext(edit, embed, view)] + pub author: Option, + #[WpContext(edit, embed, view)] + pub author_uri: Option, + #[WpContext(edit, embed, view)] + pub description: Option, + #[WpContext(edit, embed, view)] + pub name: Option, + #[WpContext(edit, embed, view)] + pub tags: Option, + #[WpContext(edit, embed, view)] + pub theme_uri: Option, + #[WpContext(edit, embed, view)] + pub status: Option, + #[WpContext(edit, embed, view)] + pub is_block_theme: Option, + #[WpContext(edit, embed, view)] + pub stylesheet_uri: Option, + #[WpContext(edit, embed, view)] + pub template_uri: Option, + #[WpContext(edit, embed, view)] + #[WpContextualOption] + pub theme_supports: Option>, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, uniffi::Record)] +#[serde(transparent)] +pub struct ThemeStylesheet { + pub value: String, +} + +impl ThemeStylesheet { + pub fn new(value: String) -> Self { + Self { value } + } +} + +impl From<&str> for ThemeStylesheet { + fn from(value: &str) -> Self { + Self { + value: value.to_string(), + } + } +} + +impl Display for ThemeStylesheet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.value) + } +} + +#[derive(Debug, Serialize, Deserialize, uniffi::Record)] +pub struct ThemeAuthor { + pub raw: String, + pub rendered: String, +} + +#[derive(Debug, Serialize, Deserialize, uniffi::Record)] +pub struct ThemeAuthorName { + pub raw: String, + pub rendered: String, +} + +#[derive(Debug, Serialize, Deserialize, uniffi::Record)] +pub struct ThemeAuthorUri { + pub raw: String, + pub rendered: String, +} + +#[derive(Debug, Serialize, Deserialize, uniffi::Record)] +pub struct ThemeAuthorDescription { + pub raw: String, + pub rendered: String, +} + +#[derive(Debug, Serialize, Deserialize, uniffi::Record)] +pub struct ThemeDescription { + pub raw: String, + pub rendered: String, +} + +#[derive(Debug, Serialize, Deserialize, uniffi::Record)] +pub struct ThemeName { + pub raw: String, + pub rendered: String, +} + +#[derive(Debug, Serialize, Deserialize, uniffi::Record)] +pub struct ThemeTags { + pub raw: Vec, + pub rendered: String, +} + +#[derive(Debug, Serialize, Deserialize, uniffi::Record)] +pub struct ThemeUri { + pub raw: String, + pub rendered: String, +} + +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, uniffi::Enum, +)] +#[serde(rename_all = "kebab-case")] +pub enum ThemeSupports { + AlignWide, + AutomaticFeedLinks, + BlockTemplates, + BlockTemplateParts, + CustomBackground, + CustomHeader, + CustomLogo, + CustomizeSelectiveRefreshWidgets, + DarkEditorStyle, + DisableCustomColors, + DisableCustomFontSizes, + DisableCustomGradients, + DisableLayoutStyles, + EditorColorPalette, + EditorFontSizes, + EditorGradientPresets, + EditorSpacingSizes, + EditorStyles, + Html5, + Formats, + PostThumbnails, + ResponsiveEmbeds, + TitleTag, + WpBlockStyles, + #[serde(untagged)] + Custom(String), +} diff --git a/wp_api_integration_tests/src/lib.rs b/wp_api_integration_tests/src/lib.rs index 710020ab..53684d2a 100644 --- a/wp_api_integration_tests/src/lib.rs +++ b/wp_api_integration_tests/src/lib.rs @@ -67,6 +67,9 @@ pub const CATEGORY_ID_INVALID: CategoryId = CategoryId(99999999); pub const TAG_ID_100: TagId = TagId(100); pub const TAG_ID_INVALID: TagId = TagId(99999999); pub const POST_TEMPLATE_SINGLE_WITH_SIDEBAR: &str = "single-with-sidebar"; +pub const THEME_TWENTY_TWENTY_FIVE: &str = "twentytwentyfive"; +pub const THEME_TWENTY_TWENTY_FOUR: &str = "twentytwentyfour"; +pub const THEME_TWENTY_TWENTY_THREE: &str = "twentytwentythree"; pub fn api_client() -> WpApiClient { let authentication = WpAuthentication::from_username_and_password( diff --git a/wp_api_integration_tests/tests/test_themes_err.rs b/wp_api_integration_tests/tests/test_themes_err.rs new file mode 100644 index 00000000..1e248768 --- /dev/null +++ b/wp_api_integration_tests/tests/test_themes_err.rs @@ -0,0 +1,50 @@ +use serial_test::parallel; +use wp_api::{ + themes::{ThemeListParams, ThemeStatus}, + WpErrorCode, +}; +use wp_api_integration_tests::{ + api_client, api_client_as_subscriber, AssertWpError, THEME_TWENTY_TWENTY_FIVE, +}; + +#[tokio::test] +#[parallel] +async fn list_err_cannot_view_active_theme() { + api_client_as_subscriber() + .themes() + .list_with_edit_context(&ThemeListParams { + status: Some(ThemeStatus::Active), + }) + .await + .assert_wp_error(WpErrorCode::CannotViewActiveTheme); +} + +#[tokio::test] +#[parallel] +async fn list_err_cannot_view_themes() { + api_client_as_subscriber() + .themes() + .list_with_edit_context(&ThemeListParams::default()) + .await + .assert_wp_error(WpErrorCode::CannotViewThemes); +} + +#[tokio::test] +#[parallel] +async fn retrieve_err_cannot_view_themes() { + api_client_as_subscriber() + .themes() + .retrieve_with_edit_context(&THEME_TWENTY_TWENTY_FIVE.into()) + .await + .assert_wp_error(WpErrorCode::CannotViewThemes); +} + +#[tokio::test] +#[parallel] +async fn retrieve_err_theme_not_found() { + api_client() + .themes() + .retrieve_with_edit_context(&"invalid_stylesheet".into()) + .await + .assert_wp_error(WpErrorCode::ThemeNotFound); +} diff --git a/wp_api_integration_tests/tests/test_themes_immut.rs b/wp_api_integration_tests/tests/test_themes_immut.rs new file mode 100644 index 00000000..2e3e5875 --- /dev/null +++ b/wp_api_integration_tests/tests/test_themes_immut.rs @@ -0,0 +1,281 @@ +use rstest::*; +use rstest_reuse::{self, apply, template}; +use serial_test::parallel; +use wp_api::generate; +use wp_api::themes::{ + SparseThemeFieldWithEditContext, SparseThemeFieldWithEmbedContext, + SparseThemeFieldWithViewContext, ThemeListParams, ThemeStatus, ThemeStylesheet, ThemeSupports, +}; +use wp_api_integration_tests::{ + api_client, AssertResponse, THEME_TWENTY_TWENTY_FIVE, THEME_TWENTY_TWENTY_FOUR, + THEME_TWENTY_TWENTY_THREE, +}; + +#[tokio::test] +#[apply(list_cases)] +#[parallel] +async fn list_with_edit_context(#[case] params: ThemeListParams) { + api_client() + .themes() + .list_with_edit_context(¶ms) + .await + .assert_response(); +} + +#[tokio::test] +#[apply(list_cases)] +#[parallel] +async fn list_with_embed_context(#[case] params: ThemeListParams) { + api_client() + .themes() + .list_with_embed_context(¶ms) + .await + .assert_response(); +} + +#[tokio::test] +#[apply(list_cases)] +#[parallel] +async fn list_with_view_context(#[case] params: ThemeListParams) { + api_client() + .themes() + .list_with_view_context(¶ms) + .await + .assert_response(); +} + +#[tokio::test] +#[apply(retrieve_cases)] +#[parallel] +async fn retrieve_with_edit_context(#[case] stylesheet: ThemeStylesheet) { + api_client() + .themes() + .retrieve_with_edit_context(&stylesheet) + .await + .assert_response(); +} + +#[tokio::test] +#[apply(retrieve_cases)] +#[parallel] +async fn retrieve_with_embed_context(#[case] stylesheet: ThemeStylesheet) { + api_client() + .themes() + .retrieve_with_embed_context(&stylesheet) + .await + .assert_response(); +} + +#[tokio::test] +#[apply(retrieve_cases)] +#[parallel] +async fn retrieve_with_view_context(#[case] stylesheet: ThemeStylesheet) { + api_client() + .themes() + .retrieve_with_view_context(&stylesheet) + .await + .assert_response(); +} + +#[tokio::test] +#[parallel] +async fn retrieve_theme_supports_is_a_bool() { + let theme = api_client() + .themes() + .retrieve_with_view_context(&THEME_TWENTY_TWENTY_FOUR.into()) + .await + .assert_response() + .data; + assert!(matches!( + theme + .theme_supports + .expect("'twentytwentyfour' theme includes the 'theme_supports' field") + .get(&ThemeSupports::BlockTemplates) + .expect( + "'twentytwentyfour' theme's 'theme_supports' field includes 'block-templates' type" + ), + wp_api::BoolOrVecString::Bool(true) + )); +} + +#[tokio::test] +#[parallel] +async fn retrieve_theme_supports_is_a_vec_string() { + let theme = api_client() + .themes() + .retrieve_with_view_context(&THEME_TWENTY_TWENTY_FOUR.into()) + .await + .assert_response() + .data; + assert!(matches!( + theme + .theme_supports + .expect("'twentytwentyfour' theme includes the 'theme_supports' field") + .get(&ThemeSupports::Html5) + .expect("'twentytwentyfour' theme's 'theme_supports' field includes 'html5' type"), + wp_api::BoolOrVecString::VecString(_) + )); +} + +#[template] +#[rstest] +#[case::default(ThemeListParams::default())] +#[case::status_active(generate!(ThemeListParams, (status, Some(ThemeStatus::Active))))] +#[case::status_inactive(generate!(ThemeListParams, (status, Some(ThemeStatus::Inactive))))] +pub fn list_cases(#[case] params: ThemeListParams) {} + +#[template] +#[rstest] +#[case::twentytwentyfive(THEME_TWENTY_TWENTY_FIVE.into())] +#[case::twentytwentyfour(THEME_TWENTY_TWENTY_FOUR.into())] +#[case::twentytwentythree(THEME_TWENTY_TWENTY_THREE.into())] +pub fn retrieve_cases(#[case] stylesheet: ThemeStylesheet) {} + +mod filter { + use super::*; + + wp_api::generate_sparse_theme_field_with_edit_context_test_cases!(); + wp_api::generate_sparse_theme_field_with_embed_context_test_cases!(); + wp_api::generate_sparse_theme_field_with_view_context_test_cases!(); + + #[apply(sparse_theme_field_with_edit_context_test_cases)] + #[case(&[SparseThemeFieldWithEditContext::Stylesheet, SparseThemeFieldWithEditContext::Author])] + #[tokio::test] + #[parallel] + async fn filter_list_with_edit_context( + #[case] fields: &[SparseThemeFieldWithEditContext], + #[values( + ThemeListParams::default(), + generate!(ThemeListParams, (status, Some(ThemeStatus::Active))), + generate!(ThemeListParams, (status, Some(ThemeStatus::Inactive))) + )] + params: ThemeListParams, + ) { + api_client() + .themes() + .filter_list_with_edit_context(¶ms, fields) + .await + .assert_response() + .data + .iter() + .for_each(|theme| { + theme.assert_that_instance_fields_nullability_match_provided_fields(fields) + }); + } + + #[apply(sparse_theme_field_with_edit_context_test_cases)] + #[case(&[SparseThemeFieldWithEditContext::Stylesheet, SparseThemeFieldWithEditContext::Author])] + #[tokio::test] + #[parallel] + async fn filter_retrieve_with_edit_context( + #[case] fields: &[SparseThemeFieldWithEditContext], + #[values( + THEME_TWENTY_TWENTY_FIVE, + THEME_TWENTY_TWENTY_FOUR, + THEME_TWENTY_TWENTY_THREE + )] + stylesheet: &str, + ) { + let theme = api_client() + .themes() + .filter_retrieve_with_edit_context(&stylesheet.into(), fields) + .await + .assert_response() + .data; + theme.assert_that_instance_fields_nullability_match_provided_fields(fields) + } + + #[apply(sparse_theme_field_with_embed_context_test_cases)] + #[case(&[SparseThemeFieldWithEmbedContext::Stylesheet, SparseThemeFieldWithEmbedContext::Author])] + #[tokio::test] + #[parallel] + async fn filter_list_with_embed_context( + #[case] fields: &[SparseThemeFieldWithEmbedContext], + #[values( + ThemeListParams::default(), + generate!(ThemeListParams, (status, Some(ThemeStatus::Active))), + generate!(ThemeListParams, (status, Some(ThemeStatus::Inactive))) + )] + params: ThemeListParams, + ) { + api_client() + .themes() + .filter_list_with_embed_context(¶ms, fields) + .await + .assert_response() + .data + .iter() + .for_each(|theme| { + theme.assert_that_instance_fields_nullability_match_provided_fields(fields) + }); + } + + #[apply(sparse_theme_field_with_embed_context_test_cases)] + #[case(&[SparseThemeFieldWithEmbedContext::Stylesheet, SparseThemeFieldWithEmbedContext::Author])] + #[tokio::test] + #[parallel] + async fn filter_retrieve_with_embed_context( + #[case] fields: &[SparseThemeFieldWithEmbedContext], + #[values( + THEME_TWENTY_TWENTY_FIVE, + THEME_TWENTY_TWENTY_FOUR, + THEME_TWENTY_TWENTY_THREE + )] + stylesheet: &str, + ) { + let theme = api_client() + .themes() + .filter_retrieve_with_embed_context(&stylesheet.into(), fields) + .await + .assert_response() + .data; + theme.assert_that_instance_fields_nullability_match_provided_fields(fields) + } + + #[apply(sparse_theme_field_with_view_context_test_cases)] + #[case(&[SparseThemeFieldWithViewContext::Stylesheet, SparseThemeFieldWithViewContext::Author])] + #[tokio::test] + #[parallel] + async fn filter_list_with_view_context( + #[case] fields: &[SparseThemeFieldWithViewContext], + #[values( + ThemeListParams::default(), + generate!(ThemeListParams, (status, Some(ThemeStatus::Active))), + generate!(ThemeListParams, (status, Some(ThemeStatus::Inactive))) + )] + params: ThemeListParams, + ) { + api_client() + .themes() + .filter_list_with_view_context(¶ms, fields) + .await + .assert_response() + .data + .iter() + .for_each(|theme| { + theme.assert_that_instance_fields_nullability_match_provided_fields(fields) + }); + } + + #[apply(sparse_theme_field_with_view_context_test_cases)] + #[case(&[SparseThemeFieldWithViewContext::Stylesheet, SparseThemeFieldWithViewContext::Author])] + #[tokio::test] + #[parallel] + async fn filter_retrieve_with_view_context( + #[case] fields: &[SparseThemeFieldWithViewContext], + #[values( + THEME_TWENTY_TWENTY_FIVE, + THEME_TWENTY_TWENTY_FOUR, + THEME_TWENTY_TWENTY_THREE + )] + stylesheet: &str, + ) { + let theme = api_client() + .themes() + .filter_retrieve_with_view_context(&stylesheet.into(), fields) + .await + .assert_response() + .data; + theme.assert_that_instance_fields_nullability_match_provided_fields(fields) + } +}