Skip to content

Commit

Permalink
Merge pull request #441 from WoltLab/rpc-api
Browse files Browse the repository at this point in the history
Add the documentation for the RPC API
  • Loading branch information
dtdesign authored Jun 24, 2024
2 parents a3362de + b7b3713 commit 4c1f61f
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 0 deletions.
70 changes: 70 additions & 0 deletions docs/javascript/components_rpc_api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# RPC API

The [PHP RPC API](../php/api/rpc_api.md) has a companion implementation in TypeScript that greatly simplifies the communication with the server and provides helper methods to process responses.

# Implementing the API

## Naming Schema

The module should roughly map to the route parameters for simplicity, for example, `WoltLabSuite/Core/Api/Sessions/DeleteSession` maps to the `DELETE /sessions/:id` endpoint.

## Using `WoltLabSuite/Core/Api/Result`

The `Result` module provides a consistent interface to interact with the API.
A comprehensive implementation can be found in `WoltLabSuite/Core/Api/Comments/RenderComment.ts`:

```ts
import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result";

type Response = {
template: string;
response: string | undefined;
};

export async function renderComment(
commentId: number,
responseId: number | undefined = undefined,
messageOnly: boolean = false,
objectTypeId: number | undefined = undefined
): Promise<ApiResult<Response>> {
const url = new URL(
`${window.WSC_API_URL}index.php?api/rpc/core/comments/${commentId}/render`
);
url.searchParams.set("messageOnly", messageOnly.toString());
if (responseId !== undefined) {
url.searchParams.set("responseID", responseId.toString());
}
if (objectTypeId !== undefined) {
url.searchParams.set("objectTypeID", objectTypeId.toString());
}

let response: Response;
try {
response = (await prepareRequest(url).get().fetchAsJson()) as Response;
} catch (e) {
return apiResultFromError(e);
}

return apiResultFromValue(response);
}
```

For `GET` and `DELETE` parameters it is possibly to provide additional parameters using the query string.
You should use the native `URL` class to (conditionally) set those parameters as shown in the example above.

### `ApiResult`

The type `ApiResult` represents the two possible states of the response, either an error or the actual result.
The callee should evaluate the `ok` property to distinguish between the two cases to properly handle any rejections.

Sometimes an action is infallible by design in which case the shortcut `.unwrap()` should be used.
It will return the result value or fail hard when there is an actual error.

#### `apiResultFromError(error: unknown)`

This function checks if the `error` represents a server response that is not in the 2xx range.
Any network errors or other kind of client errors will fail hard.

The returned value from the server will be attempted to be parsed into an `WoltLabSuite/Core/Api/Error` that represents the well-defined error response from the PHP RPC API.
Validation errors can easily be detected through the `.getValidationError()` method of `ApiError` which returns `undefined` for all other error classes.
149 changes: 149 additions & 0 deletions docs/php/api/rpc_api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# RPC API

The RPC API provides a consistent and predictable JSON API for the interaction with the server.
Authentication is entirely handled through the existing cookie-backed session system.

The current implementation covers a few reference endpoints that replaced existing PSR-based actions.
It is designed to be a replacement for the existing `AbstractDatabaseObjectAction` actions in the mid to long term.
You are more than welcome to share any feedback, including but not limited to suggestions for improvements, on GitHub.

The implementation of the RPC API is built in a way that it could support a token based authentication in the future but this is entirely out of scope for the current iteration.

# Predictable API

## Idempotency Whenever Reasonably Possible

Some endpoints will perform a specific action, such as following a user or reacting to a message. These endpoints SHOULD be idempotent whenever possible, treating the request to be the “should” state.

For example, following a user for the first time should produce the same response as attempting to follow them again while still following. Reacting to a message that has already been reacted to using a difference reaction should implicitly revoke the previous reaction. If the user has already reacted using the same reaction then no change should be made. However, in both cases the response should be indistinguishable from reacting to a message for the first time.

## HTTP Verbs

We will only support three basic verbs: `GET`, `POST` and `DELETE`.

Some implementations also make use of `PATCH` and `PUT`, but this would make it much more complex and adds little benefit plus its quite verbose since the verb implies the semantics of the endpoint. The same can be achieved by using distinct endpoints and using `POST` instead. Plus historically the support for `PATCH` and `PUT` was rather poor.

## Response Format

The PSR messaging interface allows for a host of useful response types, such as `204 No Content`, returning a plain “HTML” response or any format that is suitable. While this is more efficient, it also makes the API much more complicated and less predictable, requiring different response handling based on the endpoint being targeted.

## HTTP Status Codes

| Code | Name | Meaning |
| ---- | --------------------- | --------------------------------------------------------- |
| 200 | OK | The request was successful. |
| 400 | Bad Request | The provided parameters are insufficient. |
| 403 | Permission Denied | The callee is not authorized to execute this request. |
| 404 | Not Found | The requested endpoint does not exist. |
| 405 | Method Not Allowed | Using any HTTP verb other than `GET`, `POST` or `DELETE`. |
| 500 | Internal Server Error | The server failed to process the request. |
| 503 | Service Unavailable | The API is currently unavailable. |

## Error Format

Whenever a request is rejected with the error code 400, the response will match the following data structure:

```ts
type ErrorResponse = {
type: "api_error" | "invalid_request_error";
code: string;
message: string;
param: string;
};
```

A typical response could look like this:

```json
{
"type": "invalid_request_error",
"code": "value_too_short",
"message": "",
"param": "username"
}
```

### `type`

The error type is used to tell apart issues caused by the request being made by the client or by unexpected errors taking place on the server side processing.

### `code`

The error code is all lowercase and using snake case to describe the type of error, for example, `missing_api_key`.

### `message`

The message can be empty but when present contains a non-localized string explaining the cause of the error with the intention of assisting a developer to resolve the issue.

You MUST NOT present this message to the end user.

### `param`

Validation errors may refer to a specific parameter that has caused the request to be rejected. A common use case is to point to a specific input field, allowing for contextual error message presented to the user.

## Examples

The endpoints below may or may not exist at any point and are only used for illustration purposes.

| Verb | Endpoint | Explanation |
| -------- | ---------------------------------------- | ------------------------------------------------------------------------------- |
| `GET` | `/core/users` | Retrieve a list of users, may include query parameters to control the response. |
| `GET` | `/core/users/{id:\d+}` | Retrieve a user, may include query parameters to control the response. |
| `GET` | `/core/users/{id:\d+}/following` | Retrieve a list of users that the target user is following. |
| - | - | - |
| `POST` | `/core/users` | Creates a new user, parameters are always presented in the body. |
| `POST` | `/core/users/{id:\d+}` | (Partially) updates a user. |
| `POST` | `/core/moderationqueues/{id:\d+}/assign` | Assigns a user. |
| - | - | - |
| `DELETE` | `/forum/threads/{id:\d+}` | Deletes a thread. |

# Implementation of an Endpoint

## Namespaces for Endpoints

Endpoints are defined using a strict rule set:

- The first path segment is a namespace that holds other objects.
- The namespaces `core`, `forum`, `blog`, `filebase` and `gallery` are reserved and MUST NOT be used by third parties.
- Segments MUST be lowercase ASCII and use the plural form.
- Parameters MAY appear starting with the third path segment and are defined with a leading colon.
- The name of a parameter MUST be unique within one endpoint.

## Convention for File Name and Location

It is strongly recommended to place the files in `lib/system/endpoint/<namespace>/<objects>/<nameOfTheAction>.class.php`.
The file name should reflect the action itself, following the pattern `<Verb><Object>`, for example, `DeleteFile` or `CreatePost`.

## Registering the Route of an Endpoint

Every endpoint needs to implement `wcf\system\endpoint\IController` which defines the `__invoke()` method that will receive the `ServerRequestInterface` and an array containing any defined route parameters.

Any endpoint can only ever serve a single verb, registered through the use of the `wcf\system\endpoint\GetRequest`, `wcf\system\endpoint\PostRequest` or `wcf\system\endpoint\DeleteRequest` class attribute.
The attribute expects a single parameter to define the endpoint’s route.

### Placeholders

The route implementation uses [FastRoute](https://github.com/nikic/FastRoute) which supports named placeholders through the `{name}` syntax. Optionally, a validation pattern can be specified to further narrow down the valid value of the placeholder: `{id:\d+}`

## Available Helper Methods

The `wcf\http\Helper` class offers a few helpful methods that simplify the validation and processing of request parameters.

### `mapApiParameters(ServerRequestInterface $request, string $className)`

Takes the `$request` from the `__invoke()` call and maps the parameters against the provided class name.
By convention it is recommended to use an internal class that is defined at the end of the class file and uses a `Parameters` suffix.

For `GET` and `DELETE` requests the query string is used as the source.
For `POST` requests the request body is mapped to the parameters.

### `fetchObjectFromRequestParameter(int|string $objectID, string $className)`

Expects `$className` to be derived from `wcf\data\DatabaseObject` and attempts to fetch it using the `$objectID` parameter.
Afterwards the object is tested to have a non-falsy object id, otherwise a `UserInputException` is raised.

Returns the fetched object on success.

# Interacting with the PHP RPC API in TypeScript

You can find an introduction to the [TypeScript API for the RPC API](../../javascript/components_rpc_api.md) in the documentation.
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ nav:
- "Validation and Data": "php/api/form_builder/validation_data.md"
- "Dependencies": "php/api/form_builder/dependencies.md"
- "Package Installation Plugins": "php/api/package_installation_plugins.md"
- "RPC API": "php/api/rpc_api.md"
- "User Activity Points": "php/api/user_activity_points.md"
- "User Notifications": "php/api/user_notifications.md"
- "RSS Feeds": "php/api/rss_feeds.md"
Expand All @@ -64,6 +65,7 @@ nav:
- "Google Maps": "javascript/components_google_maps.md"
- "Notices": "javascript/components_notice.md"
- "Pagination": "javascript/components_pagination.md"
- "RPC API": "javascript/components/rpc_api.md"
- "New API":
- "Writing a module": "javascript/new-api_writing-a-module.md"
- "Core Functions": "javascript/new-api_core.md"
Expand Down

0 comments on commit 4c1f61f

Please sign in to comment.