Skip to content
Draft
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
122 changes: 122 additions & 0 deletions .claude/skills/wp-api-endpoints/SKILL.md
Original file line number Diff line number Diff line change
@@ -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<UserId>,
#[WpContext(edit)]
pub username: Option<String>,
}
```

- Prefix type name with `Sparse`, all fields `Option<T>`
- `#[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<u32>,
#[uniffi(default = [])]
pub exclude: Vec<UserId>,
}
```

- `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<T>` with `#[uniffi(default = [])]`, NOT `Option<Vec<T>>`

**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<crate::SparseUser>, 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<crate::{mod}::{SparseType}>`
- 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: `<user_id>` 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`
154 changes: 2 additions & 152 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<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, WpDeriveParamsField)]
#[supports_pagination(true)] // or false if endpoint doesn't support pagination
pub struct UserListParams {
#[uniffi(default = None)]
pub page: Option<u32>,
#[uniffi(default = [])]
pub exclude: Vec<UserId>,
// ... 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<T>` with `#[uniffi(default = [])]`, NOT `Option<Vec<T>>` 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<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`
- `test_credentials.json` (WordPress.org)
- `wp_com_test_credentials.json` (WordPress.com)

## Important Files

Expand Down
Loading