Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/wise-otters-hum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@codama/node-types': patch
---

Bump `@codama/spec` from `1.6.0-rc.4` to `1.6.0-rc.6`. The encoded surface in `@codama/node-types` is functionally unchanged; one docstring paragraph on `NestedTypeNode` now reads `nestedTypeNode<T>` instead of `NestedTypeNode<T>` to mirror the spec's new camelCase nested-union alias name.

Behind the scenes, `@codama-internal/spec-generators` learns about the new `{ kind: 'address' }` `TypeExpr` (rendered as plain `string` on the v1 TS surface — a dedicated `Address` brand may follow in a future spec major), the camelCase rename of every union and enumeration name on the spec side (the generated PascalCase TS identifiers are unaffected since the generator runs each name through `pascalCase()` at render time), and a constructor-signature bug where an attribute that was both `optional` and supplied with a default would emit invalid TS (`param?: T = default`). The bug never triggered against the rc.4 v1 spec but would have surfaced once any future attribute combined `optional: true` with a configured default; the fix is to omit the `?` mark whenever an initializer is present.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { TypeNode } from './TypeNode';

/**
* A type, possibly wrapped in zero-or-more size, offset, sentinel, or hidden prefix/suffix modifiers.
* The wrapping is recursive: each modifier wraps another `NestedTypeNode<T>` until the inner `T` is reached.
* The wrapping is recursive: each modifier wraps another `nestedTypeNode<T>` until the inner `T` is reached.
*/
export type NestedTypeNode<TType extends TypeNode> =
| FixedSizeTypeNode<NestedTypeNode<TType>>
Expand Down
2 changes: 1 addition & 1 deletion packages/spec-generators/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"dependencies": {
"@codama/fragments": "workspace:*",
"@codama/spec": "1.6.0-rc.4"
"@codama/spec": "1.6.0-rc.6"
},
"devDependencies": {
"@types/node": "^25",
Expand Down
2 changes: 2 additions & 0 deletions packages/spec-generators/src/nodeTypes/fragments/typeExpr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { TypeExpr } from '@codama/spec';

export function getTypeExprFragment(expr: TypeExpr): Fragment {
switch (expr.kind) {
case 'address':
return fragment`string`;
case 'string':
return getStringExprFragment(expr);
case 'integer':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,13 @@ function renderParamForAttribute(
const override = config.attributes?.[attr.name];
const paramName = paramIdentifier(attr, override);
const baseTsType = renderParamTsType(attr, typeParameterAttribute, override);
const optionalMark = attr.optional ? '?' : '';
const defaultClause = renderParamDefaultClause(override, typeParameterAttribute);
// `param?: T = default` is a syntax error; an initializer already
// makes the parameter callable without an argument. Emit `?` only
// when the spec marks the attribute optional AND the config supplies
// no default value.
const hasDefault = defaultClause.content !== '';
const optionalMark = attr.optional && !hasDefault ? '?' : '';
return fragment`${paramName}${optionalMark}: ${baseTsType}${defaultClause}`;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/spec-generators/src/nodes/fragments/typeExpr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const NODE_TYPES_PACKAGE = '@codama/node-types';

export function getTypeExprFragment(expr: TypeExpr): Fragment {
switch (expr.kind) {
case 'address':
return fragment`string`;
case 'string':
return getStringExprFragment(expr);
case 'integer':
Expand Down
8 changes: 4 additions & 4 deletions packages/spec-generators/src/shared/unions.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type { NodeSpec, Spec, UnionSpec } from '@codama/spec';

/**
* Any union whose name starts with `Registered` is a category-registry
* union (e.g. `RegisteredContextualValueNode`). Derived from the spec
* Any union whose name starts with `registered` is a category-registry
* union (e.g. `registeredContextualValueNode`). Derived from the spec
* so future categories are picked up automatically.
*/
const REGISTERED_CATEGORY_UNION_PREFIX = 'Registered';
const REGISTERED_CATEGORY_UNION_PREFIX = 'registered';

/**
* Return the spec's per-category registry unions (those whose names
* start with `Registered`), sorted alphabetically by name.
* start with `registered`), sorted alphabetically by name.
*/
export function getRegisteredCategoryUnions(spec: Spec): readonly UnionSpec[] {
return spec.categories
Expand Down
20 changes: 10 additions & 10 deletions packages/spec-generators/src/visitorsCore/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,20 @@ export {
/**
* Map from a spec union name to the `@codama/nodes` plural-noun alias
* the visitors should reference in `assertIsNode` /
* `removeNullAndAssertIsNodeFilter` calls (e.g. `'TypeNode'` →
* `removeNullAndAssertIsNodeFilter` calls (e.g. `'typeNode'` →
* `'TYPE_NODES'`). Unions absent from this map are expanded inline as
* a literal kind array.
*/
export const UNION_ALIAS_NAMES: ReadonlyMap<string, string> = new Map([
['ContextualValueNode', 'CONTEXTUAL_VALUE_NODES'],
['CountNode', 'COUNT_NODES'],
['DiscriminatorNode', 'DISCRIMINATOR_NODES'],
['EnumVariantTypeNode', 'ENUM_VARIANT_TYPE_NODES'],
['InstructionInputValueNode', 'INSTRUCTION_INPUT_VALUE_NODES'],
['LinkNode', 'LINK_NODES'],
['PdaSeedNode', 'PDA_SEED_NODES'],
['TypeNode', 'TYPE_NODES'],
['ValueNode', 'VALUE_NODES'],
['contextualValueNode', 'CONTEXTUAL_VALUE_NODES'],
['countNode', 'COUNT_NODES'],
['discriminatorNode', 'DISCRIMINATOR_NODES'],
['enumVariantTypeNode', 'ENUM_VARIANT_TYPE_NODES'],
['instructionInputValueNode', 'INSTRUCTION_INPUT_VALUE_NODES'],
['linkNode', 'LINK_NODES'],
['pdaSeedNode', 'PDA_SEED_NODES'],
['typeNode', 'TYPE_NODES'],
['valueNode', 'VALUE_NODES'],
]);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest';

import { getNodeRegistryFragment } from '../../../src/nodeTypes/fragments/nodeRegistry';

// The renderer derives the registered-union list from any `Registered*`
// The renderer derives the registered-union list from any `registered*`
// union in the spec. This helper plumbs a minimum but complete spec:
// one stub node per registered category, each registered union
// referencing that node, plus extra top-level nodes the caller can
Expand All @@ -24,13 +24,13 @@ function buildSpecWithAllRegisteredUnions(extraTopLevelNodes: readonly string[]
defineCategory('topLevel', {
nodes: [...stubKinds, ...extraTopLevelNodes].map(k => defineNode(k, { attributes: [] })),
unions: [
defineUnion('RegisteredContextualValueNode', { members: ['someContextualValueNode'] }),
defineUnion('RegisteredCountNode', { members: ['someCountNode'] }),
defineUnion('RegisteredDiscriminatorNode', { members: ['someDiscriminatorNode'] }),
defineUnion('RegisteredLinkNode', { members: ['someLinkNode'] }),
defineUnion('RegisteredPdaSeedNode', { members: ['somePdaSeedNode'] }),
defineUnion('RegisteredTypeNode', { members: ['someTypeNode'] }),
defineUnion('RegisteredValueNode', { members: ['someValueNode'] }),
defineUnion('registeredContextualValueNode', { members: ['someContextualValueNode'] }),
defineUnion('registeredCountNode', { members: ['someCountNode'] }),
defineUnion('registeredDiscriminatorNode', { members: ['someDiscriminatorNode'] }),
defineUnion('registeredLinkNode', { members: ['someLinkNode'] }),
defineUnion('registeredPdaSeedNode', { members: ['somePdaSeedNode'] }),
defineUnion('registeredTypeNode', { members: ['someTypeNode'] }),
defineUnion('registeredValueNode', { members: ['someValueNode'] }),
],
}),
],
Expand All @@ -55,37 +55,37 @@ describe('getNodeRegistryFragment', () => {
expect(out).toContain('export type GetNodeFromKind<TKind extends NodeKind> = Extract<Node, { kind: TKind }>;');
});

it('lists every RegisteredXxxNode union as a Node member', () => {
it('lists every registeredXxxNode union as a Node member', () => {
const result = getNodeRegistryFragment(buildSpecWithAllRegisteredUnions());
const imports = [...result.imports.keys()].sort();
expect(imports).toContain('union:RegisteredContextualValueNode');
expect(imports).toContain('union:RegisteredCountNode');
expect(imports).toContain('union:RegisteredDiscriminatorNode');
expect(imports).toContain('union:RegisteredLinkNode');
expect(imports).toContain('union:RegisteredPdaSeedNode');
expect(imports).toContain('union:RegisteredTypeNode');
expect(imports).toContain('union:RegisteredValueNode');
expect(imports).toContain('union:registeredContextualValueNode');
expect(imports).toContain('union:registeredCountNode');
expect(imports).toContain('union:registeredDiscriminatorNode');
expect(imports).toContain('union:registeredLinkNode');
expect(imports).toContain('union:registeredPdaSeedNode');
expect(imports).toContain('union:registeredTypeNode');
expect(imports).toContain('union:registeredValueNode');
});

it('lists non-registered nodes as direct Node members', () => {
// `accountNode` and `rootNode` aren't inside any RegisteredXxxNode
// `accountNode` and `rootNode` aren't inside any registeredXxxNode
// union, so they should appear as direct members of `Node`.
const result = getNodeRegistryFragment(buildSpecWithAllRegisteredUnions(['accountNode', 'rootNode']));
const imports = [...result.imports.keys()];
expect(imports).toContain('node:accountNode');
expect(imports).toContain('node:rootNode');
});

it('omits nodes that are reachable through a RegisteredXxxNode union from direct membership', () => {
// `someTypeNode` is covered by `RegisteredTypeNode`; it must not
it('omits nodes that are reachable through a registeredXxxNode union from direct membership', () => {
// `someTypeNode` is covered by `registeredTypeNode`; it must not
// appear as a direct member of `Node`.
const result = getNodeRegistryFragment(buildSpecWithAllRegisteredUnions());
const imports = [...result.imports.keys()];
expect(imports).not.toContain('node:someTypeNode');
});

it('walks nested unions and excludes any node reachable through them from direct membership', () => {
// Nest a sub-union inside `RegisteredTypeNode`. The sub-union's
// Nest a sub-union inside `registeredTypeNode`. The sub-union's
// members must still be considered "covered" and excluded from
// direct `Node` membership.
const spec: Spec = {
Expand All @@ -101,15 +101,15 @@ describe('getNodeRegistryFragment', () => {
defineNode('deeplyNestedTypeNode', { attributes: [] }),
],
unions: [
defineUnion('RegisteredContextualValueNode', { members: ['someContextualValueNode'] }),
defineUnion('RegisteredCountNode', { members: ['someCountNode'] }),
defineUnion('RegisteredDiscriminatorNode', { members: ['someDiscriminatorNode'] }),
defineUnion('RegisteredLinkNode', { members: ['someLinkNode'] }),
defineUnion('RegisteredPdaSeedNode', { members: ['somePdaSeedNode'] }),
defineUnion('RegisteredValueNode', { members: ['someValueNode'] }),
defineUnion('InnerTypeNode', { members: ['deeplyNestedTypeNode'] }),
defineUnion('RegisteredTypeNode', {
members: [{ kind: 'union', name: 'InnerTypeNode' }],
defineUnion('registeredContextualValueNode', { members: ['someContextualValueNode'] }),
defineUnion('registeredCountNode', { members: ['someCountNode'] }),
defineUnion('registeredDiscriminatorNode', { members: ['someDiscriminatorNode'] }),
defineUnion('registeredLinkNode', { members: ['someLinkNode'] }),
defineUnion('registeredPdaSeedNode', { members: ['somePdaSeedNode'] }),
defineUnion('registeredValueNode', { members: ['someValueNode'] }),
defineUnion('innerTypeNode', { members: ['deeplyNestedTypeNode'] }),
defineUnion('registeredTypeNode', {
members: [{ kind: 'union', name: 'innerTypeNode' }],
}),
],
}),
Expand All @@ -123,23 +123,23 @@ describe('getNodeRegistryFragment', () => {
expect(imports).not.toContain('node:deeplyNestedTypeNode');
});

it('only emits the RegisteredXxxNode unions present in the spec', () => {
it('only emits the registeredXxxNode unions present in the spec', () => {
// The registry is derived from `spec` — unions whose names start
// with `Registered`. If the spec ships only some of them, the
// with `registered`. If the spec ships only some of them, the
// renderer emits exactly those and quietly omits the rest.
const spec: Spec = {
categories: [
defineCategory('topLevel', {
nodes: [defineNode('someContextualValueNode', { attributes: [] })],
unions: [defineUnion('RegisteredContextualValueNode', { members: ['someContextualValueNode'] })],
unions: [defineUnion('registeredContextualValueNode', { members: ['someContextualValueNode'] })],
}),
],
version: '1.0.0',
};
const result = getNodeRegistryFragment(spec);
const imports = [...result.imports.keys()];
expect(imports).toContain('union:RegisteredContextualValueNode');
expect(imports).not.toContain('union:RegisteredCountNode');
expect(imports).toContain('union:registeredContextualValueNode');
expect(imports).not.toContain('union:registeredCountNode');
});

it('sorts the Node members alphabetically for stable output', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
address,
array,
boolean,
codamaVersion,
Expand All @@ -23,6 +24,12 @@ describe('getTypeExprFragment', () => {
expect(getTypeExprFragment(string()).content).toBe('string');
});

it('renders address as plain string for v1 (no brand, no import)', () => {
const result = getTypeExprFragment(address());
expect(result.content).toBe('string');
expect(result.imports.size).toBe(0);
});

it('renders integer as number', () => {
expect(getTypeExprFragment(u32()).content).toBe('number');
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
address,
array,
boolean,
codamaVersion,
Expand All @@ -24,6 +25,12 @@ describe('getTypeExprFragment', () => {
expect(getTypeExprFragment(string()).content).toBe('string');
});

it('renders address as plain string for v1 (no import)', () => {
const result = getTypeExprFragment(address());
expect(result.content).toBe('string');
expect(result.imports.size).toBe(0);
});

it('renders integer as number', () => {
expect(getTypeExprFragment(u32()).content).toBe('number');
});
Expand Down
18 changes: 9 additions & 9 deletions packages/spec-generators/test/shared/unions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,26 @@ import { describe, expect, it } from 'vitest';
import { flattenNodeUnion, getRegisteredCategoryUnions } from '../../src/shared';

describe('getRegisteredCategoryUnions', () => {
it('returns only unions whose names start with `Registered`, sorted', () => {
it('returns only unions whose names start with `registered`, sorted', () => {
const spec: Spec = {
categories: [
defineCategory('type', {
nodes: [defineNode('aNode', { attributes: [] })],
unions: [
defineUnion('TypeNode', { members: ['aNode'] }),
defineUnion('RegisteredTypeNode', { members: ['aNode'] }),
defineUnion('typeNode', { members: ['aNode'] }),
defineUnion('registeredTypeNode', { members: ['aNode'] }),
],
}),
defineCategory('value', {
nodes: [defineNode('bNode', { attributes: [] })],
unions: [defineUnion('RegisteredValueNode', { members: ['bNode'] })],
unions: [defineUnion('registeredValueNode', { members: ['bNode'] })],
}),
],
version: '1.0.0',
};
expect(getRegisteredCategoryUnions(spec).map(u => u.name)).toEqual([
'RegisteredTypeNode',
'RegisteredValueNode',
'registeredTypeNode',
'registeredValueNode',
]);
});

Expand All @@ -33,7 +33,7 @@ describe('getRegisteredCategoryUnions', () => {
categories: [
defineCategory('type', {
nodes: [defineNode('aNode', { attributes: [] })],
unions: [defineUnion('TypeNode', { members: ['aNode'] })],
unions: [defineUnion('typeNode', { members: ['aNode'] })],
}),
],
version: '1.0.0',
Expand All @@ -46,13 +46,13 @@ describe('getRegisteredCategoryUnions', () => {
categories: [
defineCategory('type', {
nodes: [defineNode('aNode', { attributes: [] })],
unions: [defineUnion('RegisteredTypeNode', { members: ['aNode'] })],
unions: [defineUnion('registeredTypeNode', { members: ['aNode'] })],
}),
],
version: '1.0.0',
};
const [first] = getRegisteredCategoryUnions(spec);
expect(first.name).toBe('RegisteredTypeNode');
expect(first.name).toBe('registeredTypeNode');
expect(first.members).toEqual([{ kind: 'node', name: 'aNode' }]);
});
});
Expand Down
Loading
Loading