Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/nodes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ pnpm install @codama/nodes

The Codama IDL is composed of various nodes that describe different aspects of a Solana program. Some nodes are categorised together as they share a similar purpose. For instance, all the nodes that describe a data structure that can be encoded and decoded into buffers are grouped under the `TypeNode` category.

The nodes themselves are defined by the [Codama specification](https://github.com/codama-idl/spec), which is the canonical reference for this package. Every node has its own generated documentation page describing its attributes and providing worked TypeScript examples. Head over to the [spec documentation](https://github.com/codama-idl/spec/blob/main/v1/docs/README.md) to explore all available nodes and their categories.
The nodes themselves are defined by the [Codama specification](https://github.com/codama-idl/spec), which is the canonical reference for this package. Every node has its own generated documentation page describing its attributes and providing worked TypeScript examples. Head over to the [spec documentation](https://github.com/codama-idl/spec/blob/main/docs/README.md) to explore all available nodes and their categories.

## Helpers

For every concrete node in the spec, this package exports a factory function named after it — e.g. `accountNode(input)` creates an `AccountNode` and `numberTypeNode('u64')` creates a `NumberTypeNode`. The worked examples on each documentation page use these helpers directly. The package also exports the matching TypeScript types, including the `Node` union type representing all available nodes and category unions such as `TypeNode` and `ValueNode`.
For every concrete node in the spec, this package exports a factory function named after it — e.g. `accountNode(input)` creates an `AccountNode` and `integerTypeNode('u64')` creates an `IntegerTypeNode`. The worked examples on each documentation page use these helpers directly. The package also exports the matching TypeScript types, including the `Node` union type representing all available nodes and category unions such as `TypeNode` and `ValueNode`.
2 changes: 1 addition & 1 deletion packages/nodes/src/EnumTypeNode.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { EnumTypeNode } from '@codama/node-types';

export function isScalarEnum(node: EnumTypeNode): boolean {
return (node.variants ?? []).every(variant => variant.kind === 'enumEmptyVariantTypeNode');
return (node.variants ?? []).every(variant => variant.data === undefined);
}

export function isDataEnum(node: EnumTypeNode): boolean {
Expand Down
21 changes: 0 additions & 21 deletions packages/nodes/src/InstructionArgumentNode.ts

This file was deleted.

12 changes: 1 addition & 11 deletions packages/nodes/src/InstructionNode.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import type {
InstructionArgumentNode,
InstructionNode,
OptionalAccountStrategy,
ProgramNode,
RootNode,
} from '@codama/node-types';
import type { InstructionNode, OptionalAccountStrategy, ProgramNode, RootNode } from '@codama/node-types';

import { isNode } from './Node';
import { getAllInstructions } from './ProgramNode';
Expand All @@ -15,10 +9,6 @@ export function parseOptionalAccountStrategy(
return optionalAccountStrategy ?? 'programId';
}

export function getAllInstructionArguments(node: InstructionNode): InstructionArgumentNode[] {
return [...(node.arguments ?? []), ...(node.extraArguments ?? [])];
}

export function getAllInstructionsWithSubs(
node: InstructionNode | ProgramNode | RootNode,
config: { leavesOnly?: boolean; subInstructionsFirst?: boolean } = {},
Expand Down
65 changes: 0 additions & 65 deletions packages/nodes/src/NestedTypeNode.ts

This file was deleted.

17 changes: 0 additions & 17 deletions packages/nodes/src/NumberTypeNode.ts

This file was deleted.

34 changes: 34 additions & 0 deletions packages/nodes/src/TypeNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { TransformNode, TypeNode } from '@codama/node-types';

/**
* Return a copy of `typeNode` with the given `transforms` appended after
* any it already carries.
*
* Transforms apply in array order, the first being the innermost, so
* appending keeps the node's existing transforms innermost and layers the
* new ones on the outside. This is the v2 counterpart of re-wrapping a
* type in its original wrapper nodes: a consumer that replaces a type node
* (e.g. rebuilds a `structTypeNode`) can carry the original's transforms
* across with `addTypeNodeTransforms(newNode, oldNode.transforms ?? [])`
* instead of a double spread.
*
* Only pass transforms the target node does not already carry. A node
* derived by spreading the original (`{ ...oldNode, fields }`) already has
* `oldNode.transforms`, so calling `addTypeNodeTransforms(rebuilt,
* oldNode.transforms ?? [])` on it would duplicate them.
*
* When `transforms` is empty the node is returned unchanged. The result
* omits the `transforms` attribute entirely when there is nothing to
* carry, matching the generated constructors.
*
* The return type keeps `T` for ergonomics; the node's `TTransforms` type
* parameter is not re-derived, as with the generated constructors' own
* `as TTransforms` casts.
*/
export function addTypeNodeTransforms<T extends TypeNode>(typeNode: T, transforms: readonly TransformNode[]): T {
if (transforms.length === 0) return typeNode;
const merged = [...(typeNode.transforms ?? []), ...transforms];
// Spreading a generic `T` widens to an index-signature object, so the
// narrowed node type has to be reasserted through `unknown`.
return Object.freeze({ ...typeNode, transforms: merged }) as unknown as T;
}
Comment thread
lorisleiva marked this conversation as resolved.
5 changes: 1 addition & 4 deletions packages/nodes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,16 @@ export * from './shared';
export * from './ConstantPdaSeedNode';
export * from './ConstantValueNode';
export * from './EnumTypeNode';
export * from './InstructionArgumentNode';
export * from './InstructionNode';
export * from './NestedTypeNode';
export * from './Node';
export * from './NumberTypeNode';
export * from './ProgramNode';
export * from './TypeNode';

// Legacy plural-noun aliases preserved for API stability. Each maps to
// the canonical `*_NODE_KINDS` name generated from the matching spec
// union.
export {
CONTEXTUAL_VALUE_NODE_KINDS as CONTEXTUAL_VALUE_NODES,
ENUM_VARIANT_TYPE_NODE_KINDS as ENUM_VARIANT_TYPE_NODES,
INSTRUCTION_INPUT_VALUE_NODE_KINDS as INSTRUCTION_INPUT_VALUE_NODES,
REGISTERED_COUNT_NODE_KINDS as COUNT_NODES,
REGISTERED_DISCRIMINATOR_NODE_KINDS as DISCRIMINATOR_NODES,
Expand Down
22 changes: 19 additions & 3 deletions packages/nodes/test/AccountNode.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import { expect, test } from 'vitest';

import { accountNode } from '../src';
import { accountNode, integerTypeNode, structFieldTypeNode, structTypeNode } from '../src';

test('it returns the right node kind', () => {
const node = accountNode({ name: 'foo' });
const node = accountNode({ identifier: 'foo' });
expect(node.kind).toBe('accountNode');
});

test('it returns a frozen object', () => {
const node = accountNode({ name: 'foo' });
const node = accountNode({ identifier: 'foo' });
expect(Object.isFrozen(node)).toBe(true);
});

test('it defaults the data to an empty struct', () => {
const node = accountNode({ identifier: 'foo' });
expect(node.data).toEqual(structTypeNode([]));
});

test('it keeps the provided data', () => {
const data = structTypeNode([structFieldTypeNode({ identifier: 'amount', type: integerTypeNode('u64') })]);
const node = accountNode({ data, identifier: 'foo' });
expect(node.data).toBe(data);
});

test('it preserves the identifier casing', () => {
const node = accountNode({ identifier: 'MyAccount' });
expect(node.identifier).toBe('MyAccount');
});
43 changes: 29 additions & 14 deletions packages/nodes/test/ConstantNode.test.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,52 @@
import { CODAMA_ERROR__INVALID_BRANDED_STRING, CodamaError } from '@codama/errors';
import { expect, test } from 'vitest';

import { constantNode, numberTypeNode, numberValueNode, stringTypeNode, stringValueNode } from '../src';
import { constantNode, integerTypeNode, integerValueNode, stringTypeNode, stringValueNode } from '../src';

test('it returns the right node kind', () => {
const node = constantNode('myConstant', numberTypeNode('u32'), numberValueNode(42));
const node = constantNode('myConstant', integerTypeNode('u32'), integerValueNode('42'));
expect(node.kind).toBe('constantNode');
});

test('it returns a frozen object', () => {
const node = constantNode('myConstant', numberTypeNode('u32'), numberValueNode(42));
const node = constantNode('myConstant', integerTypeNode('u32'), integerValueNode('42'));
expect(Object.isFrozen(node)).toBe(true);
});

test('it creates a constant with a number type and value', () => {
const node = constantNode('maxItems', numberTypeNode('u64'), numberValueNode(100));
expect(node.name).toBe('maxItems');
expect(node.type.kind).toBe('numberTypeNode');
expect(node.value.kind).toBe('numberValueNode');
test('it creates a constant with an integer type and value', () => {
const node = constantNode('maxItems', integerTypeNode('u64'), integerValueNode('100'));
expect(node.identifier).toBe('maxItems');
expect(node.type.kind).toBe('integerTypeNode');
expect(node.value.kind).toBe('integerValueNode');
});

test('it creates a constant with a string type and value', () => {
const node = constantNode('appName', stringTypeNode('utf8'), stringValueNode('MyApp'));
expect(node.name).toBe('appName');
expect(node.identifier).toBe('appName');
expect(node.type.kind).toBe('stringTypeNode');
expect(node.value.kind).toBe('stringValueNode');
});

test('it converts name to camelCase', () => {
const node = constantNode('my_constant', numberTypeNode('u8'), numberValueNode(1));
expect(node.name).toBe('myConstant');
test('it preserves the identifier casing', () => {
const node = constantNode('my_constant', integerTypeNode('u8'), integerValueNode('1'));
expect(node.identifier).toBe('my_constant');
});

test('it rejects invalid identifiers', () => {
expect(() => constantNode('my-constant', integerTypeNode('u8'), integerValueNode('1'))).toThrow(
new CodamaError(CODAMA_ERROR__INVALID_BRANDED_STRING, {
actual: 'my-constant',
expected: 'identifier (letters, digits and underscores; no leading digit)',
}),
);
});

test('it can have documentation', () => {
const node = constantNode('myConstant', numberTypeNode('u32'), numberValueNode(42), ['My docs']);
expect(node.docs).toEqual(['My docs']);
const node = constantNode('myConstant', integerTypeNode('u32'), integerValueNode('42'), { docs: 'My docs' });
expect(node.docs).toBe('My docs');
});

test('it omits documentation when not provided', () => {
const node = constantNode('myConstant', integerTypeNode('u32'), integerValueNode('42'));
expect('docs' in node).toBe(false);
});
9 changes: 7 additions & 2 deletions packages/nodes/test/DefinedTypeNode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ import { expect, test } from 'vitest';
import { definedTypeNode, structTypeNode } from '../src';

test('it returns the right node kind', () => {
const node = definedTypeNode({ name: 'foo', type: structTypeNode([]) });
const node = definedTypeNode({ identifier: 'foo', type: structTypeNode([]) });
expect(node.kind).toBe('definedTypeNode');
});

test('it returns a frozen object', () => {
const node = definedTypeNode({ name: 'foo', type: structTypeNode([]) });
const node = definedTypeNode({ identifier: 'foo', type: structTypeNode([]) });
expect(Object.isFrozen(node)).toBe(true);
});

test('it can have documentation', () => {
const node = definedTypeNode({ docs: 'line one\nline two', identifier: 'foo', type: structTypeNode([]) });
expect(node.docs).toBe('line one\nline two');
});
18 changes: 15 additions & 3 deletions packages/nodes/test/ErrorNode.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { expect, test } from 'vitest';

import { errorNode } from '../src';
import { errorNode, textNode } from '../src';

test('it returns the right node kind', () => {
const node = errorNode({ name: 'foo', code: 42, message: 'error message' });
const node = errorNode({ code: 42, identifier: 'foo', message: 'error message' });
expect(node.kind).toBe('errorNode');
});

test('it returns a frozen object', () => {
const node = errorNode({ name: 'foo', code: 42, message: 'error message' });
const node = errorNode({ code: 42, identifier: 'foo', message: 'error message' });
expect(Object.isFrozen(node)).toBe(true);
});

test('it keeps the code and message', () => {
const node = errorNode({ code: 42, identifier: 'foo', message: 'error message' });
expect(node.code).toBe(42);
expect(node.message).toBe('error message');
});

test('it accepts a text node as message', () => {
const message = textNode({ content: 'error message' });
const node = errorNode({ code: 42, identifier: 'foo', message });
expect(node.message).toBe(message);
});
12 changes: 8 additions & 4 deletions packages/nodes/test/EventNode.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { expect, test } from 'vitest';

import { structTypeNode } from '../src';
import { eventNode } from '../src';
import { eventNode, structTypeNode } from '../src';

test('it returns the right node kind', () => {
const node = eventNode({ data: structTypeNode([]), name: 'foo' });
const node = eventNode({ data: structTypeNode([]), identifier: 'foo' });
expect(node.kind).toBe('eventNode');
});

test('it returns a frozen object', () => {
const node = eventNode({ data: structTypeNode([]), name: 'foo' });
const node = eventNode({ data: structTypeNode([]), identifier: 'foo' });
expect(Object.isFrozen(node)).toBe(true);
});

test('it omits discriminators when the array is empty', () => {
const node = eventNode({ data: structTypeNode([]), discriminators: [], identifier: 'foo' });
expect('discriminators' in node).toBe(false);
});
Loading