Browse Source

Add `RemainderOptionTypeNode` (#111)

Loris Leiva 1 năm trước cách đây
mục cha
commit
2f26050ddb

+ 13 - 0
.changeset/tame-files-wash.md

@@ -0,0 +1,13 @@
+---
+"@kinobi-so/renderers-js-umi": minor
+"@kinobi-so/renderers-rust": minor
+"@kinobi-so/visitors-core": minor
+"@kinobi-so/renderers-js": minor
+"@kinobi-so/node-types": minor
+"@kinobi-so/errors": minor
+"@kinobi-so/nodes": minor
+---
+
+Add `RemainderOptionTypeNode`
+
+A node that represents an optional item using a child `TypeNode` such that the item can either be present — i.e. `Some<T>` — or absent — i.e. `None` — depending on whether or not there are remaining bytes in the buffer.

+ 0 - 1
.github/workflows/main.yml

@@ -4,7 +4,6 @@ on:
   push:
     branches: [main]
   pull_request:
-    branches: [main]
 
 env:
   # Among other things, opts out of Turborepo telemetry. See https://consoledonottrack.com/.

+ 5 - 0
packages/errors/src/codes.ts

@@ -56,6 +56,10 @@ export const KINOBI_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING = 2100002 as const;
 export const KINOBI_ERROR__ANCHOR__TYPE_PATH_MISSING = 2100003 as const;
 export const KINOBI_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED = 2100004 as const;
 
+// Renderers-related errors.
+// Reserve error codes in the range [2800000-2800999].
+export const KINOBI_ERROR__RENDERERS__UNSUPPORTED_NODE = 2800000 as const;
+
 /**
  * A union of every Kinobi error code
  *
@@ -79,6 +83,7 @@ export type KinobiErrorCode =
     | typeof KINOBI_ERROR__ANCHOR__UNRECOGNIZED_IDL_TYPE
     | typeof KINOBI_ERROR__LINKED_NODE_NOT_FOUND
     | typeof KINOBI_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE
+    | typeof KINOBI_ERROR__RENDERERS__UNSUPPORTED_NODE
     | typeof KINOBI_ERROR__UNEXPECTED_NESTED_NODE_KIND
     | typeof KINOBI_ERROR__UNEXPECTED_NODE_KIND
     | typeof KINOBI_ERROR__UNRECOGNIZED_NODE_KIND

+ 5 - 0
packages/errors/src/context.ts

@@ -26,6 +26,7 @@ import {
     KINOBI_ERROR__ANCHOR__UNRECOGNIZED_IDL_TYPE,
     KINOBI_ERROR__LINKED_NODE_NOT_FOUND,
     KINOBI_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE,
+    KINOBI_ERROR__RENDERERS__UNSUPPORTED_NODE,
     KINOBI_ERROR__UNEXPECTED_NESTED_NODE_KIND,
     KINOBI_ERROR__UNEXPECTED_NODE_KIND,
     KINOBI_ERROR__UNRECOGNIZED_NODE_KIND,
@@ -78,6 +79,10 @@ export type KinobiErrorContext = DefaultUnspecifiedErrorContextToUndefined<{
     [KINOBI_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE]: {
         fsFunction: string;
     };
+    [KINOBI_ERROR__RENDERERS__UNSUPPORTED_NODE]: {
+        kind: NodeKind;
+        node: Node | undefined;
+    };
     [KINOBI_ERROR__UNEXPECTED_NESTED_NODE_KIND]: {
         expectedKinds: NodeKind[];
         kind: NodeKind | null;

+ 2 - 0
packages/errors/src/messages.ts

@@ -11,6 +11,7 @@ import {
     KINOBI_ERROR__ANCHOR__UNRECOGNIZED_IDL_TYPE,
     KINOBI_ERROR__LINKED_NODE_NOT_FOUND,
     KINOBI_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE,
+    KINOBI_ERROR__RENDERERS__UNSUPPORTED_NODE,
     KINOBI_ERROR__UNEXPECTED_NESTED_NODE_KIND,
     KINOBI_ERROR__UNEXPECTED_NODE_KIND,
     KINOBI_ERROR__UNRECOGNIZED_NODE_KIND,
@@ -47,6 +48,7 @@ export const KinobiErrorMessages: Readonly<{
     [KINOBI_ERROR__LINKED_NODE_NOT_FOUND]: 'Could not find linked node [$name] from [$kind].',
     [KINOBI_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE]:
         'Node.js filesystem function [$fsFunction] is not available in your environment.',
+    [KINOBI_ERROR__RENDERERS__UNSUPPORTED_NODE]: 'Cannot render the encountered node of kind [$kind].',
     [KINOBI_ERROR__UNEXPECTED_NESTED_NODE_KIND]: 'Expected nested node of kind [$expectedKinds], got [$kind]',
     [KINOBI_ERROR__UNEXPECTED_NODE_KIND]: 'Expected node of kind [$expectedKinds], got [$kind].',
     [KINOBI_ERROR__UNRECOGNIZED_NODE_KIND]: 'Unrecognized node kind [$kind].',

+ 8 - 0
packages/node-types/src/typeNodes/RemainderOptionTypeNode.ts

@@ -0,0 +1,8 @@
+import type { TypeNode } from './TypeNode';
+
+export interface RemainderOptionTypeNode<TItem extends TypeNode = TypeNode> {
+    readonly kind: 'remainderOptionTypeNode';
+
+    // Children.
+    readonly item: TItem;
+}

+ 2 - 0
packages/node-types/src/typeNodes/TypeNode.ts

@@ -17,6 +17,7 @@ import type { OptionTypeNode } from './OptionTypeNode';
 import type { PostOffsetTypeNode } from './PostOffsetTypeNode';
 import type { PreOffsetTypeNode } from './PreOffsetTypeNode';
 import type { PublicKeyTypeNode } from './PublicKeyTypeNode';
+import type { RemainderOptionTypeNode } from './RemainderOptionTypeNode';
 import type { SentinelTypeNode } from './SentinelTypeNode';
 import type { SetTypeNode } from './SetTypeNode';
 import type { SizePrefixTypeNode } from './SizePrefixTypeNode';
@@ -44,6 +45,7 @@ export type StandaloneTypeNode =
     | PostOffsetTypeNode
     | PreOffsetTypeNode
     | PublicKeyTypeNode
+    | RemainderOptionTypeNode
     | SentinelTypeNode
     | SetTypeNode
     | SizePrefixTypeNode

+ 1 - 0
packages/node-types/src/typeNodes/index.ts

@@ -18,6 +18,7 @@ export * from './OptionTypeNode';
 export * from './PostOffsetTypeNode';
 export * from './PreOffsetTypeNode';
 export * from './PublicKeyTypeNode';
+export * from './RemainderOptionTypeNode';
 export * from './SentinelTypeNode';
 export * from './SetTypeNode';
 export * from './SizePrefixTypeNode';

+ 1 - 0
packages/nodes/README.md

@@ -87,6 +87,7 @@ Below are all of the available nodes and their documentation. Also note that you
     -   [`PostOffsetTypeNode`](./docs/typeNodes/PostOffsetTypeNode.md)
     -   [`PreOffsetTypeNode`](./docs/typeNodes/PreOffsetTypeNode.md)
     -   [`PublicKeyTypeNode`](./docs/typeNodes/PublicKeyTypeNode.md)
+    -   [`RemainderOptionTypeNode`](./docs/typeNodes/RemainderOptionTypeNode.md)
     -   [`SentinelTypeNode`](./docs/typeNodes/SentinelTypeNode.md)
     -   [`SetTypeNode`](./docs/typeNodes/SetTypeNode.md)
     -   [`SizePrefixTypeNode`](./docs/typeNodes/SizePrefixTypeNode.md)

+ 1 - 0
packages/nodes/docs/typeNodes/README.md

@@ -18,6 +18,7 @@ The `TypeNode` type helper represents all the available type nodes as well as th
 -   [`PostOffsetTypeNode`](./PostOffsetTypeNode.md)
 -   [`PreOffsetTypeNode`](./PreOffsetTypeNode.md)
 -   [`PublicKeyTypeNode`](./PublicKeyTypeNode.md)
+-   [`RemainderOptionTypeNode`](./RemainderOptionTypeNode.md)
 -   [`SentinelTypeNode`](./SentinelTypeNode.md)
 -   [`SetTypeNode`](./SetTypeNode.md)
 -   [`SizePrefixTypeNode`](./SizePrefixTypeNode.md)

+ 38 - 0
packages/nodes/docs/typeNodes/RemainderOptionTypeNode.md

@@ -0,0 +1,38 @@
+# `RemainderOptionTypeNode`
+
+A node that represents an optional item using a child `TypeNode`. The item can either be present — i.e. `Some<T>` — or absent — i.e. `None` — depending on whether or not there are remaining bytes in the buffer. If there are remaining bytes, the item is present and the child node should be encoded/decoded accordingly. However, if there are no remaining bytes, the item is absent and no further encoding/decoding should be performed.
+
+## Attributes
+
+### Data
+
+| Attribute | Type                        | Description             |
+| --------- | --------------------------- | ----------------------- |
+| `kind`    | `"remainderOptionTypeNode"` | The node discriminator. |
+
+### Children
+
+| Attribute | Type                      | Description              |
+| --------- | ------------------------- | ------------------------ |
+| `item`    | [`TypeNode`](./README.md) | The item that may exist. |
+
+## Functions
+
+### `remainderOptionTypeNode(item)`
+
+Helper function that creates a `RemainderOptionTypeNode` object from the item `TypeNode`.
+
+```ts
+const node = remainderOptionTypeNode(publicKeyTypeNode());
+```
+
+## Examples
+
+### An optional UTF-8 string using remaining bytes
+
+```ts
+remainderOptionTypeNode(stringTypeNode('UTF-8'));
+
+// None          => 0x
+// Some("Hello") => 0x48656C6C6F
+```

+ 10 - 0
packages/nodes/src/typeNodes/RemainderOptionTypeNode.ts

@@ -0,0 +1,10 @@
+import type { RemainderOptionTypeNode, TypeNode } from '@kinobi-so/node-types';
+
+export function remainderOptionTypeNode<TItem extends TypeNode>(item: TItem): RemainderOptionTypeNode<TItem> {
+    return Object.freeze({
+        kind: 'remainderOptionTypeNode',
+
+        // Children.
+        item,
+    });
+}

+ 1 - 0
packages/nodes/src/typeNodes/TypeNode.ts

@@ -15,6 +15,7 @@ export const STANDALONE_TYPE_NODE_KINDS = [
     'postOffsetTypeNode' as const,
     'preOffsetTypeNode' as const,
     'publicKeyTypeNode' as const,
+    'remainderOptionTypeNode' as const,
     'sentinelTypeNode' as const,
     'setTypeNode' as const,
     'sizePrefixTypeNode' as const,

+ 1 - 0
packages/nodes/src/typeNodes/index.ts

@@ -18,6 +18,7 @@ export * from './OptionTypeNode';
 export * from './PostOffsetTypeNode';
 export * from './PreOffsetTypeNode';
 export * from './PublicKeyTypeNode';
+export * from './RemainderOptionTypeNode';
 export * from './SentinelTypeNode';
 export * from './SetTypeNode';
 export * from './SizePrefixTypeNode';

+ 13 - 0
packages/nodes/test/typeNodes/RemainderOptionTypeNode.test.ts

@@ -0,0 +1,13 @@
+import { expect, test } from 'vitest';
+
+import { numberTypeNode, remainderOptionTypeNode } from '../../src';
+
+test('it returns the right node kind', () => {
+    const node = remainderOptionTypeNode(numberTypeNode('u8'));
+    expect(node.kind).toBe('remainderOptionTypeNode');
+});
+
+test('it returns a frozen object', () => {
+    const node = remainderOptionTypeNode(numberTypeNode('u8'));
+    expect(Object.isFrozen(node)).toBe(true);
+});

+ 9 - 0
packages/renderers-js-umi/src/getTypeManifestVisitor.ts

@@ -1,3 +1,4 @@
+import { KINOBI_ERROR__RENDERERS__UNSUPPORTED_NODE, KinobiError } from '@kinobi-so/errors';
 import {
     ArrayTypeNode,
     camelCase,
@@ -600,6 +601,10 @@ export function getTypeManifestVisitor(input: {
                     };
                 },
 
+                visitRemainderOptionType(node) {
+                    throw new KinobiError(KINOBI_ERROR__RENDERERS__UNSUPPORTED_NODE, { kind: node.kind, node });
+                },
+
                 visitSetType(setType, { self }) {
                     const childManifest = visit(setType.item, self);
                     childManifest.serializerImports.add('umiSerializers', 'set');
@@ -825,6 +830,10 @@ export function getTypeManifestVisitor(input: {
                         valueImports: new ImportMap().mergeWith(...list.map(c => c.valueImports)),
                     };
                 },
+
+                visitZeroableOptionType(node) {
+                    throw new KinobiError(KINOBI_ERROR__RENDERERS__UNSUPPORTED_NODE, { kind: node.kind, node });
+                },
             }),
     );
 }

+ 22 - 0
packages/renderers-js/src/getTypeManifestVisitor.ts

@@ -624,6 +624,28 @@ export function getTypeManifestVisitor(input: {
                     return manifest;
                 },
 
+                visitRemainderOptionType(node, { self }) {
+                    const childManifest = visit(node.item, self);
+                    childManifest.strictType.mapRender(r => `Option<${r}>`).addImports('solanaOptions', 'type Option');
+                    childManifest.looseType
+                        .mapRender(r => `OptionOrNullable<${r}>`)
+                        .addImports('solanaOptions', 'type OptionOrNullable');
+                    const encoderOptions: string[] = ['prefix: null'];
+                    const decoderOptions: string[] = ['prefix: null'];
+
+                    const encoderOptionsAsString =
+                        encoderOptions.length > 0 ? `, { ${encoderOptions.join(', ')} }` : '';
+                    const decoderOptionsAsString =
+                        decoderOptions.length > 0 ? `, { ${decoderOptions.join(', ')} }` : '';
+                    childManifest.encoder
+                        .mapRender(r => `getOptionEncoder(${r + encoderOptionsAsString})`)
+                        .addImports('solanaOptions', 'getOptionEncoder');
+                    childManifest.decoder
+                        .mapRender(r => `getOptionDecoder(${r + decoderOptionsAsString})`)
+                        .addImports('solanaOptions', 'getOptionDecoder');
+                    return childManifest;
+                },
+
                 visitSentinelType(node, { self }) {
                     const manifest = visit(node.type, self);
                     const sentinel = visit(node.sentinel, self).value;

+ 37 - 0
packages/renderers-js/test/types/remainderOption.test.ts

@@ -0,0 +1,37 @@
+import { definedTypeNode, publicKeyTypeNode, remainderOptionTypeNode } from '@kinobi-so/nodes';
+import { visit } from '@kinobi-so/visitors-core';
+import { test } from 'vitest';
+
+import { getRenderMapVisitor } from '../../src';
+import { renderMapContains, renderMapContainsImports } from '../_setup';
+
+test('it renders remainder option codecs', () => {
+    // Given the following node.
+    const node = definedTypeNode({
+        name: 'myType',
+        type: remainderOptionTypeNode(publicKeyTypeNode()),
+    });
+
+    // When we render it.
+    const renderMap = visit(node, getRenderMapVisitor());
+
+    // Then we expect the following types and codecs to be exported.
+    renderMapContains(renderMap, 'types/myType.ts', [
+        'export type MyType = Option<Address>',
+        'export type MyTypeArgs = OptionOrNullable<Address>',
+        'getOptionEncoder( getAddressEncoder(), { prefix: null } )',
+        'getOptionDecoder( getAddressDecoder(), { prefix: null } )',
+    ]);
+
+    // And we expect the following codec imports.
+    renderMapContainsImports(renderMap, 'types/myType.ts', {
+        '@solana/web3.js': [
+            'getOptionEncoder',
+            'getOptionDecoder',
+            'getAddressEncoder',
+            'getAddressDecoder',
+            'Option',
+            'OptionOrNullable',
+        ],
+    });
+});

+ 9 - 0
packages/renderers-rust/src/getTypeManifestVisitor.ts

@@ -1,3 +1,4 @@
+import { KINOBI_ERROR__RENDERERS__UNSUPPORTED_NODE, KinobiError } from '@kinobi-so/errors';
 import {
     arrayTypeNode,
     CountNode,
@@ -291,6 +292,10 @@ export function getTypeManifestVisitor(options: { nestedStruct?: boolean; parent
                     };
                 },
 
+                visitRemainderOptionType(node) {
+                    throw new KinobiError(KINOBI_ERROR__RENDERERS__UNSUPPORTED_NODE, { kind: node.kind, node });
+                },
+
                 visitSetType(setType, { self }) {
                     const childManifest = visit(setType.item, self);
                     childManifest.imports.add('std::collections::HashSet');
@@ -443,6 +448,10 @@ export function getTypeManifestVisitor(options: { nestedStruct?: boolean; parent
                         type: `(${items.map(item => item.type).join(', ')})`,
                     };
                 },
+
+                visitZeroableOptionType(node) {
+                    throw new KinobiError(KINOBI_ERROR__RENDERERS__UNSUPPORTED_NODE, { kind: node.kind, node });
+                },
             }),
     );
 }

+ 10 - 0
packages/visitors-core/src/identityVisitor.ts

@@ -44,6 +44,7 @@ import {
     preOffsetTypeNode,
     programNode,
     REGISTERED_NODE_KINDS,
+    remainderOptionTypeNode,
     removeNullAndAssertIsNodeFilter,
     resolverValueNode,
     rootNode,
@@ -295,6 +296,15 @@ export function identityVisitor<TNodeKind extends NodeKind = NodeKind>(
         };
     }
 
+    if (castedNodeKeys.includes('remainderOptionTypeNode')) {
+        visitor.visitRemainderOptionType = function visitRemainderOptionType(node) {
+            const item = visit(this)(node.item);
+            if (item === null) return null;
+            assertIsNode(item, TYPE_NODES);
+            return remainderOptionTypeNode(item);
+        };
+    }
+
     if (castedNodeKeys.includes('booleanTypeNode')) {
         visitor.visitBooleanType = function visitBooleanType(node) {
             const size = visit(this)(node.size);

+ 6 - 0
packages/visitors-core/src/mergeVisitor.ts

@@ -138,6 +138,12 @@ export function mergeVisitor<TReturn, TNodeKind extends NodeKind = NodeKind>(
         };
     }
 
+    if (castedNodeKeys.includes('remainderOptionTypeNode')) {
+        visitor.visitRemainderOptionType = function visitRemainderOptionType(node) {
+            return merge(node, visit(this)(node.item));
+        };
+    }
+
     if (castedNodeKeys.includes('booleanTypeNode')) {
         visitor.visitBooleanType = function visitBooleanType(node) {
             return merge(node, visit(this)(node.size));

+ 33 - 0
packages/visitors-core/test/nodes/typeNodes/RemainderOptionTypeNode.test.ts

@@ -0,0 +1,33 @@
+import { publicKeyTypeNode, remainderOptionTypeNode } from '@kinobi-so/nodes';
+import { test } from 'vitest';
+
+import {
+    expectDebugStringVisitor,
+    expectDeleteNodesVisitor,
+    expectIdentityVisitor,
+    expectMergeVisitorCount,
+} from '../_setup';
+
+const node = remainderOptionTypeNode(publicKeyTypeNode());
+
+test('mergeVisitor', () => {
+    expectMergeVisitorCount(node, 2);
+});
+
+test('identityVisitor', () => {
+    expectIdentityVisitor(node);
+});
+
+test('deleteNodesVisitor', () => {
+    expectDeleteNodesVisitor(node, '[remainderOptionTypeNode]', null);
+    expectDeleteNodesVisitor(node, '[publicKeyTypeNode]', null);
+});
+
+test('debugStringVisitor', () => {
+    expectDebugStringVisitor(
+        node,
+        `
+remainderOptionTypeNode
+|   publicKeyTypeNode`,
+    );
+});