Skip to content

Commit

Permalink
Create isNode and assertIsNode helper functions (#125)
Browse files Browse the repository at this point in the history
* Create isNode helper function

* Add typetests

* Create assertIsNode helper function

* Add isNodeFilter and assertIsNodeFilter helpers

* Test removeNullAndAssertIsNodeFilter helper

* Add typetests for filter functions

* Refactor type guards for PDA seed nodes

* Refactor type guards for size nodes

* Refactor type guards for type nodes

* Refactor type guards for value nodes

* Refactor type guards for top-level nodes

* Add changeset
lorisleiva authored Dec 31, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 5fb8ca2 commit 22ce9ee
Showing 98 changed files with 555 additions and 1,081 deletions.
5 changes: 5 additions & 0 deletions .changeset/afraid-numbers-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@metaplex-foundation/kinobi': patch
---

Create isNode and assertIsNode helper functions
4 changes: 2 additions & 2 deletions src/Kinobi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
assertRootNode,
assertIsNode,
IdlInputs,
Node,
rootNode,
@@ -36,7 +36,7 @@ export function createFromRoot(
},
update(visitor: Visitor<Node | null>): void {
const newRoot = visit(currentRoot, visitor);
assertRootNode(newRoot);
assertIsNode(newRoot, 'rootNode');
currentRoot = newRoot;
},
clone(): Kinobi {
13 changes: 0 additions & 13 deletions src/nodes/AccountDataNode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { InvalidKinobiTreeError, MainCaseString, mainCase } from '../shared';
import { LinkTypeNode } from './typeNodes/LinkTypeNode';
import type { Node } from './Node';
import { StructTypeNode } from './typeNodes/StructTypeNode';

export type AccountDataNode = {
@@ -27,15 +26,3 @@ export function accountDataNode(input: AccountDataNodeInput): AccountDataNode {
link: input.link,
};
}

export function isAccountDataNode(node: Node | null): node is AccountDataNode {
return !!node && node.kind === 'accountDataNode';
}

export function assertAccountDataNode(
node: Node | null
): asserts node is AccountDataNode {
if (!isAccountDataNode(node)) {
throw new Error(`Expected accountDataNode, got ${node?.kind ?? 'null'}.`);
}
}
17 changes: 2 additions & 15 deletions src/nodes/AccountNode.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import {
mainCase,
} from '../shared';
import { AccountDataNode, accountDataNode } from './AccountDataNode';
import type { Node } from './Node';
import { assertIsNode } from './Node';
import {
PdaSeedNode,
constantPdaSeedNode,
@@ -17,7 +17,6 @@ import {
import { remainderSizeNode } from './sizeNodes';
import { bytesTypeNode } from './typeNodes/BytesTypeNode';
import { stringTypeNode } from './typeNodes/StringTypeNode';
import { assertStructTypeNode } from './typeNodes/StructTypeNode';
import { TypeNode, createTypeNodeFromIdl } from './typeNodes/TypeNode';
import {
booleanValueNode,
@@ -66,7 +65,7 @@ export function accountNodeFromIdl(idl: Partial<IdlAccount>): AccountNode {
const name = mainCase(idlName);
const idlStruct = idl.type ?? { kind: 'struct', fields: [] };
const struct = createTypeNodeFromIdl(idlStruct);
assertStructTypeNode(struct);
assertIsNode(struct, 'structTypeNode');
const seeds = (idl.seeds ?? []).map((seed): PdaSeedNode => {
if (seed.kind === 'constant') {
const value = (() => {
@@ -102,15 +101,3 @@ export function accountNodeFromIdl(idl: Partial<IdlAccount>): AccountNode {
seeds,
});
}

export function isAccountNode(node: Node | null): node is AccountNode {
return !!node && node.kind === 'accountNode';
}

export function assertAccountNode(
node: Node | null
): asserts node is AccountNode {
if (!isAccountNode(node)) {
throw new Error(`Expected accountNode, got ${node?.kind ?? 'null'}.`);
}
}
13 changes: 0 additions & 13 deletions src/nodes/DefinedTypeNode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { IdlDefinedType } from '../idl';
import { InvalidKinobiTreeError, MainCaseString, mainCase } from '../shared';
import type { Node } from './Node';
import { TypeNode, createTypeNodeFromIdl } from './typeNodes/TypeNode';

export type DefinedTypeNode = {
@@ -42,15 +41,3 @@ export function definedTypeNodeFromIdl(
const data = createTypeNodeFromIdl(idlType);
return definedTypeNode({ name, data, idlName: name, docs: idl.docs });
}

export function isDefinedTypeNode(node: Node | null): node is DefinedTypeNode {
return !!node && node.kind === 'definedTypeNode';
}

export function assertDefinedTypeNode(
node: Node | null
): asserts node is DefinedTypeNode {
if (!isDefinedTypeNode(node)) {
throw new Error(`Expected definedTypeNode, got ${node?.kind ?? 'null'}.`);
}
}
11 changes: 0 additions & 11 deletions src/nodes/ErrorNode.ts
Original file line number Diff line number Diff line change
@@ -5,7 +5,6 @@ import {
PartialExcept,
mainCase,
} from '../shared';
import type { Node } from './Node';

export type ErrorNode = {
readonly kind: 'errorNode';
@@ -51,13 +50,3 @@ export function errorNodeFromIdl(idl: Partial<IdlError>): ErrorNode {
docs: idl.docs ?? [msg ? `${name}: ${msg}` : `${name}`],
});
}

export function isErrorNode(node: Node | null): node is ErrorNode {
return !!node && node.kind === 'errorNode';
}

export function assertErrorNode(node: Node | null): asserts node is ErrorNode {
if (!isErrorNode(node)) {
throw new Error(`Expected errorNode, got ${node?.kind ?? 'null'}.`);
}
}
17 changes: 0 additions & 17 deletions src/nodes/InstructionAccountNode.ts
Original file line number Diff line number Diff line change
@@ -5,7 +5,6 @@ import {
PartialExcept,
mainCase,
} from '../shared';
import type { Node } from './Node';

export type InstructionAccountNode = {
readonly kind: 'instructionAccountNode';
@@ -51,19 +50,3 @@ export function instructionAccountNodeFromIdl(
docs: idl.docs ?? desc ?? [],
});
}

export function isInstructionAccountNode(
node: Node | null
): node is InstructionAccountNode {
return !!node && node.kind === 'instructionAccountNode';
}

export function assertInstructionAccountNode(
node: Node | null
): asserts node is InstructionAccountNode {
if (!isInstructionAccountNode(node)) {
throw new Error(
`Expected instructionAccountNode, got ${node?.kind ?? 'null'}.`
);
}
}
17 changes: 0 additions & 17 deletions src/nodes/InstructionDataArgsNode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { InvalidKinobiTreeError, MainCaseString, mainCase } from '../shared';
import { LinkTypeNode } from './typeNodes/LinkTypeNode';
import type { Node } from './Node';
import { StructTypeNode } from './typeNodes/StructTypeNode';

export type InstructionDataArgsNode = {
@@ -32,19 +31,3 @@ export function instructionDataArgsNode(
link: input.link,
};
}

export function isInstructionDataArgsNode(
node: Node | null
): node is InstructionDataArgsNode {
return !!node && node.kind === 'instructionDataArgsNode';
}

export function assertInstructionDataArgsNode(
node: Node | null
): asserts node is InstructionDataArgsNode {
if (!isInstructionDataArgsNode(node)) {
throw new Error(
`Expected instructionDataArgsNode, got ${node?.kind ?? 'null'}.`
);
}
}
17 changes: 0 additions & 17 deletions src/nodes/InstructionExtraArgsNode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { InvalidKinobiTreeError, MainCaseString, mainCase } from '../shared';
import { LinkTypeNode } from './typeNodes/LinkTypeNode';
import type { Node } from './Node';
import { StructTypeNode } from './typeNodes/StructTypeNode';

export type InstructionExtraArgsNode = {
@@ -32,19 +31,3 @@ export function instructionExtraArgsNode(
link: input.link,
};
}

export function isInstructionExtraArgsNode(
node: Node | null
): node is InstructionExtraArgsNode {
return !!node && node.kind === 'instructionExtraArgsNode';
}

export function assertInstructionExtraArgsNode(
node: Node | null
): asserts node is InstructionExtraArgsNode {
if (!isInstructionExtraArgsNode(node)) {
throw new Error(
`Expected instructionExtraArgsNode, got ${node?.kind ?? 'null'}.`
);
}
}
20 changes: 4 additions & 16 deletions src/nodes/InstructionNode.ts
Original file line number Diff line number Diff line change
@@ -20,8 +20,8 @@ import {
InstructionExtraArgsNode,
instructionExtraArgsNode,
} from './InstructionExtraArgsNode';
import type { Node } from './Node';
import { ProgramNode, isProgramNode } from './ProgramNode';
import { isNode } from './Node';
import { ProgramNode } from './ProgramNode';
import { RootNode } from './RootNode';
import { structFieldTypeNode } from './typeNodes/StructFieldTypeNode';
import {
@@ -128,31 +128,19 @@ export function getAllInstructionsWithSubs(
node: ProgramNode | RootNode | InstructionNode,
leavesOnly = false
): InstructionNode[] {
if (isInstructionNode(node)) {
if (isNode(node, 'instructionNode')) {
if (node.subInstructions.length === 0) return [node];
const subInstructions = node.subInstructions.flatMap((sub) =>
getAllInstructionsWithSubs(sub, leavesOnly)
);
return leavesOnly ? subInstructions : [node, ...subInstructions];
}

const instructions = isProgramNode(node)
const instructions = isNode(node, 'programNode')
? node.instructions
: node.programs.flatMap((program) => program.instructions);

return instructions.flatMap((instruction) =>
getAllInstructionsWithSubs(instruction, leavesOnly)
);
}

export function isInstructionNode(node: Node | null): node is InstructionNode {
return !!node && node.kind === 'instructionNode';
}

export function assertInstructionNode(
node: Node | null
): asserts node is InstructionNode {
if (!isInstructionNode(node)) {
throw new Error(`Expected instructionNode, got ${node?.kind ?? 'null'}.`);
}
}
54 changes: 42 additions & 12 deletions src/nodes/Node.ts
Original file line number Diff line number Diff line change
@@ -13,6 +13,8 @@ import { REGISTERED_SIZE_NODES } from './sizeNodes';
import { REGISTERED_TYPE_NODES } from './typeNodes';
import { REGISTERED_VALUE_NODES } from './valueNodes';

// Node Registration.

const REGISTERED_NODES = {
rootNode: {} as RootNode,
programNode: {} as ProgramNode,
@@ -38,22 +40,50 @@ export const REGISTERED_NODES_KEYS = Object.keys(

export type RegisteredNodes = typeof REGISTERED_NODES;

// Node Helpers.

export type Node = RegisteredNodes[keyof RegisteredNodes];

export const assertNodeFilter =
<T extends Node>(
assertCallback: (node: Node | null) => asserts node is T
): ((node: Node | null | null) => node is T) =>
(node): node is T => {
assertCallback(node);
export function isNode<TKeys extends keyof RegisteredNodes>(
node: Node | null,
key: TKeys | TKeys[]
): node is RegisteredNodes[TKeys] {
const keys = Array.isArray(key) ? key : [key];
return !!node && (keys as (keyof RegisteredNodes)[]).includes(node.kind);
}

export function assertIsNode<TKeys extends keyof RegisteredNodes>(
node: Node | null,
key: TKeys | TKeys[]
): asserts node is RegisteredNodes[TKeys] {
const keys = Array.isArray(key) ? key : [key];
if (!isNode(node, keys)) {
throw new Error(
`Expected ${keys.join(' | ')}, got ${node?.kind ?? 'null'}.`
);
}
}

export function isNodeFilter<TKeys extends keyof RegisteredNodes>(
key: TKeys | TKeys[]
): (node: Node | null) => node is RegisteredNodes[TKeys] {
return (node): node is RegisteredNodes[TKeys] => isNode(node, key);
}

export function assertIsNodeFilter<TKeys extends keyof RegisteredNodes>(
key: TKeys | TKeys[]
): (node: Node | null) => node is RegisteredNodes[TKeys] {
return (node): node is RegisteredNodes[TKeys] => {
assertIsNode(node, key);
return true;
};
}

export const removeNullAndAssertNodeFilter =
<T extends Node>(
assertCallback: (node: Node | null) => asserts node is T
): ((node: Node | null) => node is T) =>
(node): node is T => {
if (node) assertCallback(node);
export function removeNullAndAssertIsNodeFilter<
TKeys extends keyof RegisteredNodes
>(key: TKeys | TKeys[]): (node: Node | null) => node is RegisteredNodes[TKeys] {
return (node): node is RegisteredNodes[TKeys] => {
if (node) assertIsNode(node, key);
return node !== null;
};
}
13 changes: 0 additions & 13 deletions src/nodes/ProgramNode.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,6 @@ import { AccountNode, accountNodeFromIdl } from './AccountNode';
import { DefinedTypeNode, definedTypeNodeFromIdl } from './DefinedTypeNode';
import { ErrorNode, errorNodeFromIdl } from './ErrorNode';
import { InstructionNode, instructionNodeFromIdl } from './InstructionNode';
import type { Node } from './Node';

export type ProgramNode = {
readonly kind: 'programNode';
@@ -74,15 +73,3 @@ export function programNodeFromIdl(idl: Partial<Idl>): ProgramNode {
origin,
});
}

export function isProgramNode(node: Node | null): node is ProgramNode {
return !!node && node.kind === 'programNode';
}

export function assertProgramNode(
node: Node | null
): asserts node is ProgramNode {
if (!isProgramNode(node)) {
throw new Error(`Expected programNode, got ${node?.kind ?? 'null'}.`);
}
}
11 changes: 0 additions & 11 deletions src/nodes/RootNode.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,6 @@ import type { AccountNode } from './AccountNode';
import type { DefinedTypeNode } from './DefinedTypeNode';
import type { ErrorNode } from './ErrorNode';
import type { InstructionNode } from './InstructionNode';
import type { Node } from './Node';
import { ProgramNode, programNodeFromIdl } from './ProgramNode';

export type IdlInputs = string | Partial<Idl> | (string | Partial<Idl>)[];
@@ -41,13 +40,3 @@ export function getAllInstructions(node: RootNode): InstructionNode[] {
export function getAllErrors(node: RootNode): ErrorNode[] {
return node.programs.flatMap((program) => program.errors);
}

export function isRootNode(node: Node | null): node is RootNode {
return !!node && node.kind === 'rootNode';
}

export function assertRootNode(node: Node | null): asserts node is RootNode {
if (!isRootNode(node)) {
throw new Error(`Expected rootNode, got ${node?.kind ?? 'null'}.`);
}
}
Loading

0 comments on commit 22ce9ee

Please sign in to comment.