Skip to content

/widget-types endpoint #785

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: widgets
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 155 additions & 16 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,28 +57,167 @@ Test credentials are configured in:

### Common Development Tasks

1. **Adding new types that uses WordPress REST API endpoint**:
- The API returns different fields depending on the `context` parameter which can be `edit`, `embed` or `view` and defaults to `view`. To support this, we use the `wp_contextual::WpContextual` derive macro and add `#[WpContext(edit, embed, view)]` attribute to each field, using the available contexts.
- Each contextual type's name has to start with `Sparse` prefix and all of its fields has to be `Option<T>`. `wp_contextual::WpContextual` derive macro will generate new types from it with the `WithEditContext`, `WithEmbedContext` & `WithViewContext` suffices. These new types will not use `Option<T>` for its fields unless a field in the original `Sparse` type is marked with `#[WpContextualOption]` attribute.
- Most types should have the following derive macros `#[derive(Debug, serde::Serialize, serde::Deserialize, uniffi::Record)]`.
- To implement new types, use `wp_api/src/posts.rs` as a reference and follow the same style
- For a new endpoint, a set of JSONs should be provided to you for each context type, so you can compare them and figure out which field is returned for which contexts.

2. **Error handling for an endpoint**:
- The server will return a `crate::WpErrorCode` instance for most error types.
- While implementing the errors for a new endpoint, if it's missing from the `crate::WpErrorCode` variants, it needs to be added there. If you need to do this, please add the new variants to the very top of the type and add a comment on top with `// Needs Triage`.
- Integration tests for error cases go into `wp_api_integration_tests/tests/test_{endpoint_name}_err.rs` file.
- The implementation should follow a similar approach to `wp_api_integration_tests/tests/test_users_err.rs`.
- There are several `api_client` helper functions available. The default `api_client` function is authenticated with an admin users. This should be the preferred client if creating the error case doesn't require an inauthenticated or a separate user. There is also `api_client_as_subscriber` function that is authenticated with a subscriber user. Most authentication error types can be triggered using this client type. Another possibility is `api_client_with_auth_provider(WpAuthenticationProvider::none().into())` which doesn't have any authentication headers, so it's useful in specific cases.
- Implementing these tests can be difficult without having a full understanding of how to trigger them. So, if you are not sure how to implement it, generate a test function following existing patterns, but leave the implementation empty. Instead, add a comment about what you can find from the implementation related to how one might be able to trigger this error.
- The existing tests don't have much documentation, because the test implementation can act as one. However, when you are implementing the test, please add a documentation. This is because we need some context about why you implemented a test in a specific way. If you include a documentation, we can check if what you are trying to do is correct, before reviewing the implementation.
This section explains how to add new WordPress REST API endpoints and types to this codebase. The implementation follows a specific pattern to handle WordPress's context-aware responses and maintain type safety.

#### 1. Adding new types for WordPress REST API endpoints

WordPress REST API returns different fields depending on the `context` parameter (`view`, `edit`, or `embed`). We handle this using a procedural macro that generates context-specific types.

**Core concepts:**
- **Sparse types**: Base types with all fields as `Option<T>`, prefixed with `Sparse`
- **Context-specific types**: Generated types with appropriate fields for each context
- **Type safety**: New type wrappers for IDs and strongly-typed parameter enums

**Implementation steps:**

1. **Create the Sparse type** in `wp_api/src/{endpoint_name}.rs`:
```rust
#[derive(Debug, Serialize, Deserialize, uniffi::Record, WpContextual)]
pub struct SparseUser {
#[WpContext(edit, embed, view)]
pub id: Option<UserId>,
#[WpContext(edit)]
pub username: Option<String>,
// ... other fields
}
```
- Start type name with `Sparse` prefix
- All fields must be `Option<T>`
- Add `#[WpContext(...)]` attributes based on API documentation
- Fields marked with `#[WpContextualOption]` remain optional in generated types
- Omit `_links` and `_meta` fields (add a comment for `_meta`)

2. **Create ID wrapper types** for type safety:
```rust
impl_as_query_value_for_new_type!(UserId);
uniffi::custom_newtype!(UserId, i64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserId(pub i64);
```

3. **Define parameter types** for list/create/update operations:
```rust
#[derive(Debug, Default, PartialEq, Eq, uniffi::Record)]
pub struct UserListParams {
#[uniffi(default = None)]
pub page: Option<u32>,
// ... other fields
}
```

4. **Implement query parameter handling**:
- Create a `{Type}ListParamsField` enum:
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, IntoStaticStr)]
enum UserListParamsField {
#[strum(serialize = "page")]
Page,
// ... other fields
}
```
- Implement `AppendUrlQueryPairs` and `FromUrlQueryPairs` traits
- Import helpers: `crate::url_query::{AppendUrlQueryPairs, FromUrlQueryPairs, QueryPairs, QueryPairsExtension, UrlQueryPairsMap}`

**Special parameter types:**

Some parameters require custom handling:
- **Enum parameters with partial serialization**: Use `OptionFromStr` trait (see `WpApiParamUsersWho`)
- **Complex parameters**: Implement custom `FromStr`/`Display` (see `WpApiParamUsersHasPublishedPosts`)
- **Parameters with special serialization**: Use serde attributes (see `UserAvatarSize`)

#### 2. Adding WordPress REST API endpoint implementations

Endpoints are implemented using a derive macro that generates the request builder functions.

**Implementation steps:**

1. **Create endpoint file** `wp_api/src/request/endpoint/{endpoint_name}_endpoint.rs`:
```rust
use crate::{/* imports */};
use wp_derive_request_builder::WpDerivedRequest;

#[derive(WpDerivedRequest)]
enum UsersRequest {
#[contextual_paged(url = "/users", params = &UserListParams, output = Vec<crate::SparseUser>, filter_by = crate::SparseUserField)]
List,
#[post(url = "/users", params = &UserCreateParams, output = UserWithEditContext)]
Create,
// ... other variants
}
```

2. **Choose appropriate attributes**:
- `#[contextual_paged]` - For lists with pagination and context support
- `#[contextual_get]` - For `GET` operations with context support
- `#[get]` - For `GET` operations without context support
- `#[post]` - For `POST` operations
- `#[delete]` - For `DELETE` operations
- `filter_by` parameter enables `_fields` query parameter support

3. **Use appropriate `output` types**
- For lists with contextual types: `Vec<crate::{endpoint_name}::{sparse_endpoint_type}>`, i.e. `Vec<crate::posts::SparsePost>`
- For single items with contextual types: `crate::{endpoint_name}::{sparse_endpoint_type}`, i.e. `crate::posts::SparsePost`
- For non contextual types: `Vec<crate::{endpoint_name}::{return_type}>` & `crate::{endpoint_name}::{return_type}`

4. **Use appropriate `filter_by` types**
- For lists with contextual types: `crate::{endpoint_name}::{sparse_field_type}`, i.e. `crate::posts::SparsePostField`
- Procedural macro will turn `SparsePostField` into `SparsePostFieldWithEditContext`, `SparsePostFieldWithEmbedContext` & `SparsePostFieldWithViewContext`

5. **Handle special cases**:
- **Delete vs Trash**: `Delete` requires `force=true`, `Trash` requires `force=false`
- **URL parameters**: `<user_id>` becomes `UserId` parameter in generated functions

6. **Implement DerivedRequest trait**:
```rust
impl DerivedRequest for UsersRequest {
fn namespace() -> impl AsNamespace {
WpNamespace::WpV2 // For /wp/v2 endpoints
}
}
```
- Override `additional_query_pairs()` only for special cases (e.g., Delete/Trash)

7. **Add comprehensive unit tests**:
- Test every endpoint variant
- Test with default parameters
- Test with all parameters populated
- Use `validate_wp_v2_endpoint()` helper

8. **Add the new request builder & executor to `WpApiRequestBuilder` & `WpApiClient` in `wp_api/src/api_client.rs`**

#### 3. Error handling and integration tests

WordPress REST API returns specific error codes that need to be handled and tested.

**Implementation steps:**

1. **Add missing error codes** to `crate::WpErrorCode`:
- Add new variants at the top with `// Needs Triage` comment for each one
- Match the error codes from API responses

2. **Create error tests** in `wp_api_integration_tests/tests/test_{endpoint_name}_err.rs`:
- Use appropriate client helpers:
- `api_client()` - Admin authenticated (default)
- `api_client_as_subscriber()` - Limited permissions
- `api_client_with_auth_provider(WpAuthenticationProvider::none().into())` - Unauthenticated

3. **Document test rationale**:
- Add doc comments explaining why tests are implemented a specific way
- If unsure how to trigger an error, leave implementation empty with explanation

**Example references:**
- Types: `wp_api/src/posts.rs`, `wp_api/src/categories.rs`
- Endpoints: `wp_api/src/request/endpoint/posts_endpoint.rs`
- Error tests: `wp_api_integration_tests/tests/test_posts_err.rs`

## Important Files

- `Makefile` - Build automation and platform-specific targets
- `wp_api/src/lib.rs` - Main library entry point
- `wp_api/src/request.rs` - Core request/response handling
- `wp_api/src/wp_error.rs` - Error types and handling
- `wp_api/src/api_client.rs` - Request builder & executor wrapper API client types
- `wp_api/src/api_error.rs` - Error types and handling
- `wp_api_integration_tests/src/lib.rs` - Helpers for integration tests

## Development Tips

Expand Down
6 changes: 6 additions & 0 deletions wp_api/src/api_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crate::{
templates_endpoint::{TemplatesRequestBuilder, TemplatesRequestExecutor},
themes_endpoint::{ThemesRequestBuilder, ThemesRequestExecutor},
users_endpoint::{UsersRequestBuilder, UsersRequestExecutor},
widget_types_endpoint::{WidgetTypesRequestBuilder, WidgetTypesRequestExecutor},
widgets_endpoint::{WidgetsRequestBuilder, WidgetsRequestExecutor},
wp_site_health_tests_endpoint::{
WpSiteHealthTestsRequestBuilder, WpSiteHealthTestsRequestExecutor,
Expand Down Expand Up @@ -67,6 +68,7 @@ pub struct WpApiRequestBuilder {
templates: Arc<TemplatesRequestBuilder>,
themes: Arc<ThemesRequestBuilder>,
users: Arc<UsersRequestBuilder>,
widget_types: Arc<WidgetTypesRequestBuilder>,
widgets: Arc<WidgetsRequestBuilder>,
wp_site_health_tests: Arc<WpSiteHealthTestsRequestBuilder>,
}
Expand Down Expand Up @@ -94,6 +96,7 @@ impl WpApiRequestBuilder {
templates,
themes,
users,
widget_types,
widgets,
wp_site_health_tests
)
Expand Down Expand Up @@ -131,6 +134,7 @@ pub struct WpApiClient {
templates: Arc<TemplatesRequestExecutor>,
themes: Arc<ThemesRequestExecutor>,
users: Arc<UsersRequestExecutor>,
widget_types: Arc<WidgetTypesRequestExecutor>,
widgets: Arc<WidgetsRequestExecutor>,
wp_site_health_tests: Arc<WpSiteHealthTestsRequestExecutor>,
}
Expand All @@ -155,6 +159,7 @@ impl WpApiClient {
templates,
themes,
users,
widget_types,
widgets,
wp_site_health_tests
)
Expand Down Expand Up @@ -189,6 +194,7 @@ api_client_generate_endpoint_impl!(WpApi, taxonomies);
api_client_generate_endpoint_impl!(WpApi, templates);
api_client_generate_endpoint_impl!(WpApi, themes);
api_client_generate_endpoint_impl!(WpApi, users);
api_client_generate_endpoint_impl!(WpApi, widget_types);
api_client_generate_endpoint_impl!(WpApi, widgets);
api_client_generate_endpoint_impl!(WpApi, wp_site_health_tests);

Expand Down
2 changes: 2 additions & 0 deletions wp_api/src/api_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ pub enum WpErrorCode {
UserInvalidSlug,
#[serde(rename = "rest_widget_not_found")]
WidgetNotFound,
#[serde(rename = "rest_widget_type_invalid")]
WidgetTypeInvalid,
// ------------------------------------------------------------------------------------
// Untested, because we are unable to create the necessary conditions for them
// ------------------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions wp_api/src/request/endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub mod taxonomies_endpoint;
pub mod templates_endpoint;
pub mod themes_endpoint;
pub mod users_endpoint;
pub mod widget_types_endpoint;
pub mod widgets_endpoint;
pub mod wp_site_health_tests_endpoint;

Expand Down
3 changes: 1 addition & 2 deletions wp_api/src/request/endpoint/posts_endpoint.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::{AsNamespace, DerivedRequest, WpNamespace};
use crate::{
SparseField,
posts::{
Expand All @@ -8,8 +9,6 @@ use crate::{
};
use wp_derive_request_builder::WpDerivedRequest;

use super::{AsNamespace, DerivedRequest, WpNamespace};

#[derive(WpDerivedRequest)]
enum PostsRequest {
#[contextual_paged(url = "/posts", params = &PostListParams, output = Vec<crate::posts::SparsePost>, filter_by = crate::posts::SparsePostField)]
Expand Down
124 changes: 124 additions & 0 deletions wp_api/src/request/endpoint/widget_types_endpoint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
use super::{AsNamespace, DerivedRequest, WpNamespace};
use crate::{
SparseField,
widget_types::{
SparseWidgetTypeFieldWithEditContext, SparseWidgetTypeFieldWithEmbedContext,
SparseWidgetTypeFieldWithViewContext, WidgetTypeId,
},
};
use wp_derive_request_builder::WpDerivedRequest;

#[derive(WpDerivedRequest)]
enum WidgetTypesRequest {
#[contextual_get(url = "/widget-types", output = Vec<crate::widget_types::SparseWidgetType>, filter_by = crate::widget_types::SparseWidgetTypeField)]
List,
#[contextual_get(url = "/widget-types/<widget_type_id>", output = crate::widget_types::SparseWidgetType, filter_by = crate::widget_types::SparseWidgetTypeField)]
Retrieve,
}

impl DerivedRequest for WidgetTypesRequest {
fn namespace() -> impl AsNamespace {
WpNamespace::WpV2
}
}

super::macros::default_sparse_field_implementation_from_field_name!(
SparseWidgetTypeFieldWithEditContext
);
super::macros::default_sparse_field_implementation_from_field_name!(
SparseWidgetTypeFieldWithEmbedContext
);
super::macros::default_sparse_field_implementation_from_field_name!(
SparseWidgetTypeFieldWithViewContext
);

#[cfg(test)]
mod tests {
use super::*;
use crate::request::endpoint::ApiUrlResolver;
use crate::request::endpoint::tests::{
fixture_wp_org_site_api_url_resolver, validate_wp_v2_endpoint,
};
use rstest::*;
use std::sync::Arc;

#[rstest]
fn list_widget_types(endpoint: WidgetTypesRequestEndpoint) {
validate_wp_v2_endpoint(
endpoint.list_with_edit_context(),
"/widget-types?context=edit",
);
validate_wp_v2_endpoint(
endpoint.list_with_embed_context(),
"/widget-types?context=embed",
);
validate_wp_v2_endpoint(
endpoint.list_with_view_context(),
"/widget-types?context=view",
);
}

#[rstest]
#[case(&[], "/widget-types?context=edit&_fields=")]
#[case(&[SparseWidgetTypeFieldWithEditContext::Description], "/widget-types?context=edit&_fields=description")]
#[case(ALL_SPARSE_WIDGET_TYPE_FIELDS_WITH_EDIT_CONTEXT, &format!("/widget-types?context=edit&{}", EXPECTED_QUERY_PAIRS_FOR_ALL_SPARSE_WIDGET_TYPE_FIELDS_WITH_EDIT_CONTEXT))]
fn filter_list_widget_types(
endpoint: WidgetTypesRequestEndpoint,
#[case] fields: &[SparseWidgetTypeFieldWithEditContext],
#[case] expected_path: &str,
) {
validate_wp_v2_endpoint(
endpoint.filter_list_with_edit_context(fields),
expected_path,
);
}

#[rstest]
fn retrieve_widget_type(endpoint: WidgetTypesRequestEndpoint) {
let widget_type = &WidgetTypeId("calendar".to_string());
validate_wp_v2_endpoint(
endpoint.retrieve_with_edit_context(widget_type),
"/widget-types/calendar?context=edit",
);
validate_wp_v2_endpoint(
endpoint.retrieve_with_embed_context(widget_type),
"/widget-types/calendar?context=embed",
);
validate_wp_v2_endpoint(
endpoint.retrieve_with_view_context(widget_type),
"/widget-types/calendar?context=view",
);
}

#[rstest]
fn filter_retrieve_widget_type_with_embed_context(endpoint: WidgetTypesRequestEndpoint) {
validate_wp_v2_endpoint(
endpoint.filter_retrieve_with_embed_context(
&WidgetTypeId("recent-posts".to_string()),
&[
SparseWidgetTypeFieldWithEmbedContext::Name,
SparseWidgetTypeFieldWithEmbedContext::IsMulti,
],
),
"/widget-types/recent-posts?context=embed&_fields=name%2Cis_multi",
);
}

const EXPECTED_QUERY_PAIRS_FOR_ALL_SPARSE_WIDGET_TYPE_FIELDS_WITH_EDIT_CONTEXT: &str =
"_fields=id%2Cname%2Cdescription%2Cis_multi%2Cclassname";
const ALL_SPARSE_WIDGET_TYPE_FIELDS_WITH_EDIT_CONTEXT: &[SparseWidgetTypeFieldWithEditContext;
5] = &[
SparseWidgetTypeFieldWithEditContext::Id,
SparseWidgetTypeFieldWithEditContext::Name,
SparseWidgetTypeFieldWithEditContext::Description,
SparseWidgetTypeFieldWithEditContext::IsMulti,
SparseWidgetTypeFieldWithEditContext::Classname,
];

#[fixture]
fn endpoint(
fixture_wp_org_site_api_url_resolver: Arc<dyn ApiUrlResolver>,
) -> WidgetTypesRequestEndpoint {
WidgetTypesRequestEndpoint::new(fixture_wp_org_site_api_url_resolver)
}
}
Loading