diff --git a/.changeset/empty-knives-remain.md b/.changeset/empty-knives-remain.md new file mode 100644 index 00000000..4006829a --- /dev/null +++ b/.changeset/empty-knives-remain.md @@ -0,0 +1,5 @@ +--- +"@metaplex-foundation/kinobi": minor +--- + +Add ZeroableOptionTypeNode diff --git a/src/nodes/typeNodes/TypeNode.ts b/src/nodes/typeNodes/TypeNode.ts index 205b0d77..57e80607 100644 --- a/src/nodes/typeNodes/TypeNode.ts +++ b/src/nodes/typeNodes/TypeNode.ts @@ -29,6 +29,7 @@ import { StringTypeNode, stringTypeNode } from './StringTypeNode'; import { StructFieldTypeNode } from './StructFieldTypeNode'; import { StructTypeNode, structTypeNodeFromIdl } from './StructTypeNode'; import { TupleTypeNode, tupleTypeNodeFromIdl } from './TupleTypeNode'; +import { ZeroableOptionTypeNode } from './ZeroableOptionTypeNode'; // Standalone Type Node Registration. export type StandaloneTypeNode = @@ -53,7 +54,8 @@ export type StandaloneTypeNode = | SolAmountTypeNode | StringTypeNode | StructTypeNode - | TupleTypeNode; + | TupleTypeNode + | ZeroableOptionTypeNode; export const STANDALONE_TYPE_NODE_KINDS = [ 'amountTypeNode', 'arrayTypeNode', @@ -77,6 +79,7 @@ export const STANDALONE_TYPE_NODE_KINDS = [ 'stringTypeNode', 'structTypeNode', 'tupleTypeNode', + 'zeroableOptionTypeNode', ] satisfies readonly StandaloneTypeNode['kind'][]; null as unknown as StandaloneTypeNode['kind'] satisfies (typeof STANDALONE_TYPE_NODE_KINDS)[number]; diff --git a/src/nodes/typeNodes/ZeroableOptionTypeNode.ts b/src/nodes/typeNodes/ZeroableOptionTypeNode.ts new file mode 100644 index 00000000..984398ae --- /dev/null +++ b/src/nodes/typeNodes/ZeroableOptionTypeNode.ts @@ -0,0 +1,25 @@ +import { ConstantValueNode } from '../valueNodes'; +import { TypeNode } from './TypeNode'; + +export interface ZeroableOptionTypeNode< + TItem extends TypeNode = TypeNode, + TZeroValue extends ConstantValueNode | undefined = + | ConstantValueNode + | undefined, +> { + readonly kind: 'zeroableOptionTypeNode'; + + // Children. + readonly item: TItem; + readonly zeroValue?: TZeroValue; +} + +export function zeroableOptionTypeNode< + TItem extends TypeNode, + TZeroValue extends ConstantValueNode | undefined, +>( + item: TItem, + zeroValue?: TZeroValue +): ZeroableOptionTypeNode { + return { kind: 'zeroableOptionTypeNode', item, zeroValue }; +} diff --git a/src/nodes/typeNodes/index.ts b/src/nodes/typeNodes/index.ts index afcdc8d6..db55601e 100644 --- a/src/nodes/typeNodes/index.ts +++ b/src/nodes/typeNodes/index.ts @@ -27,3 +27,4 @@ export * from './StructFieldTypeNode'; export * from './StructTypeNode'; export * from './TupleTypeNode'; export * from './TypeNode'; +export * from './ZeroableOptionTypeNode'; diff --git a/src/visitors/identityVisitor.ts b/src/visitors/identityVisitor.ts index ac1959e0..f8c8e56a 100644 --- a/src/visitors/identityVisitor.ts +++ b/src/visitors/identityVisitor.ts @@ -63,6 +63,7 @@ import { tupleTypeNode, tupleValueNode, variablePdaSeedNode, + zeroableOptionTypeNode, } from '../nodes'; import { staticVisitor } from './staticVisitor'; import { Visitor, visit as baseVisit } from './visitor'; @@ -324,6 +325,19 @@ export function identityVisitor( }; } + if (castedNodeKeys.includes('zeroableOptionTypeNode')) { + visitor.visitZeroableOptionType = function visitZeroableOptionType(node) { + const item = visit(this)(node.item); + if (item === null) return null; + assertIsNode(item, TYPE_NODES); + const zeroValue = node.zeroValue + ? visit(this)(node.zeroValue) ?? undefined + : undefined; + if (zeroValue) assertIsNode(zeroValue, 'constantValueNode'); + return zeroableOptionTypeNode(item, zeroValue); + }; + } + if (castedNodeKeys.includes('booleanTypeNode')) { visitor.visitBooleanType = function visitBooleanType(node) { const size = visit(this)(node.size); diff --git a/src/visitors/mergeVisitor.ts b/src/visitors/mergeVisitor.ts index 7fcd8ee0..1f14b6fd 100644 --- a/src/visitors/mergeVisitor.ts +++ b/src/visitors/mergeVisitor.ts @@ -153,6 +153,15 @@ export function mergeVisitor( }; } + if (castedNodeKeys.includes('zeroableOptionTypeNode')) { + visitor.visitZeroableOptionType = function visitZeroableOptionType(node) { + return merge(node, [ + ...visit(this)(node.item), + ...(node.zeroValue ? visit(this)(node.zeroValue) : []), + ]); + }; + } + if (castedNodeKeys.includes('booleanTypeNode')) { visitor.visitBooleanType = function visitBooleanType(node) { return merge(node, visit(this)(node.size)); diff --git a/test/visitors/nodes/typeNodes/ZeroableOptionTypeNode.test.ts b/test/visitors/nodes/typeNodes/ZeroableOptionTypeNode.test.ts new file mode 100644 index 00000000..bd913fcf --- /dev/null +++ b/test/visitors/nodes/typeNodes/ZeroableOptionTypeNode.test.ts @@ -0,0 +1,48 @@ +import test from 'ava'; +import { + constantValueNodeFromBytes, + publicKeyTypeNode, + zeroableOptionTypeNode, +} from '../../../../src'; +import { + deleteNodesVisitorMacro, + getDebugStringVisitorMacro, + identityVisitorMacro, + mergeVisitorMacro, +} from '../_setup'; + +const node = zeroableOptionTypeNode( + publicKeyTypeNode(), + constantValueNodeFromBytes('base16', 'ff'.repeat(32)) +); + +test(mergeVisitorMacro, node, 5); +test(identityVisitorMacro, node); +test(deleteNodesVisitorMacro, node, '[zeroableOptionTypeNode]', null); +test(deleteNodesVisitorMacro, node, '[publicKeyTypeNode]', null); +test( + deleteNodesVisitorMacro, + node, + '[constantValueNode]', + zeroableOptionTypeNode(publicKeyTypeNode()) +); +test( + getDebugStringVisitorMacro, + node, + ` +zeroableOptionTypeNode +| publicKeyTypeNode +| constantValueNode +| | bytesTypeNode +| | bytesValueNode [base16.ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff]` +); + +// No zero value. +test( + 'getDebugStringVisitor: different strategy', + getDebugStringVisitorMacro, + zeroableOptionTypeNode(publicKeyTypeNode()), + ` +zeroableOptionTypeNode +| publicKeyTypeNode` +);