Skip to content

Commit

Permalink
Add new RpcRequest, RpcResponse types and their transformer types (
Browse files Browse the repository at this point in the history
…#3147)

Now that we've made room for these new types, we add the following:

- `RpcRequest`: The method name and the params are all we need to create a request. Combining them into a single object makes it easier to play with RPC requests. For instance, we can now have a `RpcRequestTransformer` instead of a `RpcParamsTransformer`.
- `RpcResponse`: This is the response abstraction that all the layers of the RPC packages will use moving forward. Instead of a simple `unknown` piece of data, the `RpcResponse` object contains two async methods:
  - `json`: Returns the parsed piece of data.
  - `text`: Returns the unparsed piece of data (as a string).
- `RpcRequestTransformer` and `RpcResponseTransformer`: Functions that take a request/response and return a new request/response respectively. Note that the response transformer also provides the associated request as a second argument.
- `RpcResponseTransformerFor`: Same as `RpcResponseTransformer` but we know exactly the type of the return data we expect.
- `createJsonRpcResponseTransformer`: This function is a helper that transforms a function of type `(json: unknown) => T` to a `RpcResponseTransformerFor<T>` by wrapping it in a `json` async function.
  • Loading branch information
lorisleiva authored Sep 1, 2024
1 parent 628177f commit 4f87d12
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/light-bugs-compare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solana/rpc-spec': patch
---

Add new `RpcRequest` and `RpcResponse` types with `RpcRequestTransformer`, `RpcResponseTransformer` and `createJsonRpcResponseTransformer` functions
42 changes: 41 additions & 1 deletion packages/rpc-spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,32 @@ Calling the `send(options)` method on a `PendingRpcRequest` will trigger the req

An object that exposes all of the functions described by `TRpcMethods`, and fulfils them using `TRpcTransport`. Calling each method returns a `PendingRpcRequest<TResponse>` where `TResponse` is that method's response type.

### `RpcRequest`

An object that describes the elements of a JSON RPC request. It consists of the following properties:

- `methodName`: The name of the JSON RPC method to be called.
- `params`: The parameters to be passed to the JSON RPC method.

### `RpcRequestTransformer`

A function that accepts an `RpcRequest` and returns another `RpcRequest`. This allows the `RpcApi` to transform the request before it is sent to the JSON RPC server.

### `RpcResponse`

An object that represents the response from a JSON RPC server. It contains two asynchronous methods that can be used to access the response data:

- `await response.json()`: Returns the data as a JSON object.
- `await response.text()`: Returns the data, unparsed, as a JSON string.

This allows the `RpcApi` to decide whether they want the parsed JSON object or the raw JSON string. Ultimately, the `json` method will be used by the `Rpc` to provide the final response to the caller.

### `RpcResponseTransformer`

A function that accepts an `RpcResponse` and returns another `RpcResponse`. This allows the `RpcApi` to transform the response before it is returned to the caller.

Note that a `RpcResponseTransformerFor<T>` generic function type is also available to ensure the response data returned by the transformer matches the expected type `T`.

### `RpcApi<TRpcMethods>`

For each of `TRpcMethods` this object exposes a method with the same name that maps between its input arguments and a `RpcApiRequestPlan<TResponse>` that describes how to prepare a JSON RPC request to fetch `TResponse`.
Expand All @@ -51,7 +77,7 @@ This is a marker interface that all RPC method definitions must extend to be acc

### `RpcApiRequestPlan`

This type describes how a particular request should be issued to the JSON RPC server. Given a function that was called on a `Rpc`, this object gives you the opportunity to:
This type allows an `RpcApi` to describe how a particular request should be issued to the JSON RPC server. Given a function that was called on a `Rpc`, this object gives you the opportunity to:

- customize the JSON RPC method name in the case that it's different than the name of that function
- define the shape of the JSON RPC params in case they are different than the arguments provided to that function
Expand Down Expand Up @@ -116,3 +142,17 @@ A config object with the following properties:

- `parametersTransformer<T>(params: T, methodName): unknown`: An optional function that maps between the shape of the arguments an RPC method was called with and the shape of the params expected by the JSON RPC server.
- `responseTransformer<T>(response, methodName): T`: An optional function that maps between the shape of the JSON RPC server response for a given method and the shape of the response expected by the `RpcApi`.

### `createJsonRpcResponseTransformer<T>(jsonTransformer)`

Creates an `RpcResponseTransformerFor<T>` function from a function that transforms any JSON value to a value of type `T` by wrapping it in a `json` async function.

```ts
const getResultTransformer = createJsonRpcResponseTransformer((json: unknown) => {
return (json as { result: TResponse }).result;
});
```

#### Arguments

- `jsonTransformer: (json: unknown, request: RpcRequest) => T`: A function that transforms an unknown JSON value to a value of type `T`.
56 changes: 56 additions & 0 deletions packages/rpc-spec/src/__tests__/rpc-shared-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import '@solana/test-matchers/toBeFrozenObject';

import { createJsonRpcResponseTransformer, RpcRequest, RpcResponse } from '../rpc-shared';

describe('createJsonRpcResponseTransformer', () => {
it('can alter the value of the json Promise', async () => {
expect.assertions(1);

// Given a request and a response that returns a number.
const request = { methodName: 'someMethod', params: [123] };
const response = {
json: () => Promise.resolve(123),
text: () => Promise.resolve('123'),
};

// When we create a JSON transformer that doubles the number.
const transformer = createJsonRpcResponseTransformer((json: unknown) => (json as number) * 2);

// Then the transformed response should return the doubled number.
const transformedResponse = transformer(response, request);
transformedResponse satisfies RpcResponse<number>;
await expect(transformedResponse.json()).resolves.toBe(246);
});

it('does not alter the value of the text Promise', async () => {
expect.assertions(1);

// Given a request and a response that returns a number.
const request = { methodName: 'someMethod', params: [123] };
const response = {
json: () => Promise.resolve(123),
text: () => Promise.resolve('123'),
};

// When we create a JSON transformer that doubles the number.
const transformer = createJsonRpcResponseTransformer((json: unknown) => (json as number) * 2);

// Then the text should function should return the original string.
const transformedResponse = transformer(response, request);
await expect(transformedResponse.text()).resolves.toBe('123');
});

it('returns a frozen object as the Reponse', () => {
// Given any response.
const response = {
json: () => Promise.resolve(123),
text: () => Promise.resolve('123'),
};

// When we pass it through a JSON transformer.
const transformedResponse = createJsonRpcResponseTransformer(x => x)(response, {} as RpcRequest);

// Then we expect the transformed response to be frozen.
expect(transformedResponse).toBeFrozenObject();
});
});
1 change: 1 addition & 0 deletions packages/rpc-spec/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './rpc';
export * from './rpc-api';
export * from './rpc-request';
export * from './rpc-shared';
export * from './rpc-transport';
35 changes: 35 additions & 0 deletions packages/rpc-spec/src/rpc-shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export type RpcRequest<TParams = unknown> = {
readonly methodName: string;
readonly params: TParams;
};

export type RpcResponse<TResponse = unknown> = {
readonly json: () => Promise<TResponse>;
readonly text: () => Promise<string>;
};

export type RpcRequestTransformer = {
<TParams>(request: RpcRequest<unknown>): RpcRequest<TParams>;
};

export type RpcResponseTransformer = {
<TResponse>(response: RpcResponse<unknown>, request: RpcRequest<unknown>): RpcResponse<TResponse>;
};

export type RpcResponseTransformerFor<TResponse> = {
(response: RpcResponse<unknown>, request: RpcRequest<unknown>): RpcResponse<TResponse>;
};

export function createJsonRpcResponseTransformer<TResponse>(
jsonTransformer: (json: unknown, request: RpcRequest) => TResponse,
): RpcResponseTransformerFor<TResponse> {
return function (response: RpcResponse, request: RpcRequest): RpcResponse<TResponse> {
return Object.freeze({
...response,
json: async () => {
const json = await response.json();
return jsonTransformer(json, request);
},
});
};
}

0 comments on commit 4f87d12

Please sign in to comment.