diff --git a/.claude/skills/wp-api-endpoints/SKILL.md b/.claude/skills/wp-api-endpoints/SKILL.md new file mode 100644 index 000000000..3ad8f6595 --- /dev/null +++ b/.claude/skills/wp-api-endpoints/SKILL.md @@ -0,0 +1,122 @@ +--- +name: wp-api-endpoints +description: Guide for adding new WordPress REST API endpoints and types to the wordpress-rs codebase. Use when (1) adding new sparse types or context-aware API response types, (2) implementing new REST API endpoint request builders, (3) adding error handling and integration tests for endpoints, or (4) creating ID wrapper types or parameter types for API operations. +--- + +# Adding WordPress REST API Endpoints + +This codebase uses procedural macros to handle WordPress's context-aware responses (`view`, `edit`, `embed`) and maintain type safety. + +## 1. Adding New Types + +Create types in `wp_api/src/{endpoint_name}.rs`. + +### Sparse Types + +```rust +#[derive(Debug, Serialize, Deserialize, uniffi::Record, WpContextual)] +pub struct SparseUser { + #[WpContext(edit, embed, view)] + pub id: Option, + #[WpContext(edit)] + pub username: Option, +} +``` + +- Prefix type name with `Sparse`, all fields `Option` +- `#[WpContext(...)]` per API docs; `#[WpContextualOption]` keeps fields optional in generated types +- Omit `_links` and `_meta` fields (add a comment for `_meta`) + +### ID Wrapper Types + +```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); +``` + +### Parameter Types + +```rust +#[derive(Debug, Default, PartialEq, Eq, uniffi::Record, WpDeriveParamsField)] +#[supports_pagination(true)] +pub struct UserListParams { + #[uniffi(default = None)] + pub page: Option, + #[uniffi(default = [])] + pub exclude: Vec, +} +``` + +- `WpDeriveParamsField` generates field enum and query parameter handling (import from `wp_derive`) +- `#[supports_pagination(true/false)]` — `#[field_name("custom_name")]` if API name differs +- **Array params**: use `Vec` with `#[uniffi(default = [])]`, NOT `Option>` + +**Special parameter types:** +- Enum with partial serialization: `OptionFromStr` trait (see `WpApiParamUsersWho`) +- Complex params: custom `FromStr`/`Display` (see `WpApiParamUsersHasPublishedPosts`) +- Special serialization: serde attributes (see `UserAvatarSize`) + +## 2. Adding Endpoint Implementations + +Create `wp_api/src/request/endpoint/{endpoint_name}_endpoint.rs`: + +```rust +use wp_derive_request_builder::WpDerivedRequest; + +#[derive(WpDerivedRequest)] +enum UsersRequest { + #[contextual_paged(url = "/users", params = &UserListParams, output = Vec, filter_by = crate::SparseUserField)] + List, + #[post(url = "/users", params = &UserCreateParams, output = UserWithEditContext)] + Create, +} +``` + +**Attributes:** +- `#[contextual_paged]` — lists with pagination + context +- `#[contextual_get]` — GET with context +- `#[get]` / `#[post]` / `#[delete]` — without context + +**Output types:** +- Contextual lists: `Vec` +- Contextual single: `crate::{mod}::{SparseType}` +- Non-contextual: the concrete type directly + +**`filter_by`:** use `crate::{mod}::{SparseField}` — macro generates `SparseFieldWith{Edit,Embed,View}Context` + +**Special cases:** +- Delete vs Trash: `Delete` needs `force=true`, `Trash` needs `force=false` +- URL params: `` becomes `UserId` in generated functions + +**DerivedRequest trait:** + +```rust +impl DerivedRequest for UsersRequest { + fn namespace() -> impl AsNamespace { + WpNamespace::WpV2 + } +} +``` + +Override `additional_query_pairs()` only for special cases (e.g., Delete/Trash). + +**Unit tests:** test every variant with default and fully-populated params using `validate_wp_v2_endpoint()`. + +**Wire up:** add request builder & executor to `WpApiRequestBuilder` & `WpApiClient` in `wp_api/src/api_client.rs`. + +## 3. Error Handling and Integration Tests + +1. Add missing error codes to `crate::WpErrorCode` with `// Needs Triage` comment +2. Create `wp_api_integration_tests/tests/test_{endpoint_name}_err.rs` using: + - `api_client()` — admin + - `api_client_as_subscriber()` — limited permissions + - `api_client_with_auth_provider(WpAuthenticationProvider::none().into())` — unauthenticated +3. Add doc comments explaining test rationale; leave empty with explanation if unsure how to trigger an error + +## Reference Files + +- 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` diff --git a/CLAUDE.md b/CLAUDE.md index 4e4bbae04..48e5f598e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,158 +56,8 @@ cargo test -p wp_api_integration_tests ``` Test credentials are configured in: -- `wp_api_integration_tests/tests/test_credentials.json` (WordPress.org) -- `wp_api_integration_tests/tests/wp_com_test_credentials.json` (WordPress.com) - -### Common Development Tasks - -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`, 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, - #[WpContext(edit)] - pub username: Option, - // ... other fields - } - ``` - - Start type name with `Sparse` prefix - - All fields must be `Option` - - 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, WpDeriveParamsField)] - #[supports_pagination(true)] // or false if endpoint doesn't support pagination - pub struct UserListParams { - #[uniffi(default = None)] - pub page: Option, - #[uniffi(default = [])] - pub exclude: Vec, - // ... other fields - } - ``` - - Use `WpDeriveParamsField` macro to automatically generate field enum and query parameter handling - - Add `#[supports_pagination(true/false)]` attribute to indicate pagination support - - Use `#[field_name("custom_name")]` attribute if the API field name differs from the struct field name - - **IMPORTANT**: For array/list parameters, use `Vec` with `#[uniffi(default = [])]`, NOT `Option>` with `#[uniffi(default = None)]` - - Import: `use wp_derive::WpDeriveParamsField;` - -**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, 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`, i.e. `Vec` - - 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}` - -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**: `` 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` +- `test_credentials.json` (WordPress.org) +- `wp_com_test_credentials.json` (WordPress.com) ## Important Files