From f4d3892b742bcef6f5e6e89f86662ce2f5bace60 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 22 Jun 2026 10:57:33 +0100 Subject: [PATCH] Integrate @codama/spec@1.7.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrates `@codama/spec@1.7.0-rc.0` into the TS codegen pipeline and regenerates `@codama/node-types`, `@codama/nodes`, and `@codama/visitors-core` with the new presentation layer (display nodes, provide/inject, account-field selector). Held against `main` for CI and review but not merged until the spec stabilises at `1.7.0` — letting us bump to later RCs here as the spec evolves. No changeset is included for the same reason. Key changes: - `anyNode` TypeExpr: both `typeExpr.ts` fragments render it as the top-level `Node` type; the visitor generator visits-and-asserts it against `REGISTERED_NODE_KINDS`. - Config maps (`CATEGORY_DIRECTORIES`, `GENERIC_PARAM_ORDER`, walk-order maps, `NODE_CONFIGS`) absorb the new `display`/`provides` attributes; a `paramName`-only `AttributeOverride` variant renames `providedNode.node` so it doesn't shadow the visitor's `node` parameter. - Input-type generator now widens every `stringIdentifier()` attribute to `string` (not just `name`). - Visitor fixes: `getByteSizeVisitor`/`getMaxByteSizeVisitor` add `visitStringType => null` now that `stringTypeNode` is no longer a leaf; `getDebugStringVisitor` drops the trailing `()` for childless nodes; `dynamic-codecs` adds `visitInjectedValue` that throws (injected values must be resolved before codec time). - Tests: 11 new per-node fixtures for the new spec kinds, plus `anyNode` unit tests. Lint, build, and test pass across all 17 packages. --- packages/dynamic-codecs/src/values.ts | 16 +++- .../test/values/InjectedValueNode.test.ts | 22 +++++ .../src/generated/InstructionAccountNode.ts | 11 +++ .../src/generated/InstructionArgumentNode.ts | 4 + .../src/generated/InstructionNode.ts | 11 +++ .../InstructionRemainingAccountsNode.ts | 4 + packages/node-types/src/generated/Node.ts | 4 + .../node-types/src/generated/ProvidedNode.ts | 18 ++++ .../AccountFieldValueNode.ts | 19 ++++ .../StandaloneContextualValueNode.ts | 2 + .../generated/contextualValueNodes/index.ts | 1 + .../displayNodes/AmountNumberDisplayNode.ts | 26 +++++ .../displayNodes/DateTimeNumberDisplayNode.ts | 14 +++ .../src/generated/displayNodes/DisplayNode.ts | 4 + .../displayNodes/DurationNumberDisplayNode.ts | 15 +++ .../displayNodes/EnumVariantDisplayNode.ts | 16 ++++ .../InstructionAccountDisplayNode.ts | 15 +++ .../displayNodes/InstructionDisplayNode.ts | 17 ++++ .../displayNodes/NumberDisplayNode.ts | 6 ++ .../displayNodes/RegisteredDisplayNode.ts | 19 ++++ .../displayNodes/StringDisplayNode.ts | 19 ++++ .../displayNodes/StructFieldDisplayNode.ts | 29 ++++++ .../src/generated/displayNodes/index.ts | 11 +++ packages/node-types/src/generated/index.ts | 2 + .../src/generated/shared/codamaVersion.ts | 2 +- .../src/generated/shared/displaySkip.ts | 14 +++ .../node-types/src/generated/shared/index.ts | 1 + .../typeNodes/EnumEmptyVariantTypeNode.ts | 9 +- .../typeNodes/EnumStructVariantTypeNode.ts | 4 + .../typeNodes/EnumTupleVariantTypeNode.ts | 4 + .../src/generated/typeNodes/NumberTypeNode.ts | 10 +- .../src/generated/typeNodes/StringTypeNode.ts | 10 +- .../typeNodes/StructFieldTypeNode.ts | 4 + .../valueNodes/InjectableNumberValueNode.ts | 5 + .../valueNodes/InjectableStringValueNode.ts | 5 + .../generated/valueNodes/InjectedValueNode.ts | 22 +++++ .../valueNodes/StandaloneValueNode.ts | 2 + .../src/generated/valueNodes/index.ts | 3 + .../src/generated/InstructionAccountNode.ts | 23 ++++- .../src/generated/InstructionArgumentNode.ts | 16 +++- .../nodes/src/generated/InstructionNode.ts | 20 +++- .../InstructionRemainingAccountsNode.ts | 15 ++- packages/nodes/src/generated/ProvidedNode.ts | 18 ++++ packages/nodes/src/generated/codamaVersion.ts | 2 +- .../AccountFieldValueNode.ts | 22 +++++ .../StandaloneContextualValueNode.ts | 1 + .../generated/contextualValueNodes/index.ts | 1 + .../displayNodes/AmountNumberDisplayNode.ts | 23 +++++ .../displayNodes/DateTimeNumberDisplayNode.ts | 16 ++++ .../src/generated/displayNodes/DisplayNode.ts | 4 + .../displayNodes/DurationNumberDisplayNode.ts | 17 ++++ .../displayNodes/EnumVariantDisplayNode.ts | 14 +++ .../InstructionAccountDisplayNode.ts | 16 ++++ .../displayNodes/InstructionDisplayNode.ts | 17 ++++ .../displayNodes/NumberDisplayNode.ts | 6 ++ .../displayNodes/RegisteredDisplayNode.ts | 11 +++ .../displayNodes/StringDisplayNode.ts | 17 ++++ .../displayNodes/StructFieldDisplayNode.ts | 19 ++++ .../nodes/src/generated/displayNodes/index.ts | 11 +++ packages/nodes/src/generated/index.ts | 2 + packages/nodes/src/generated/nodeKinds.ts | 3 + .../typeNodes/EnumEmptyVariantTypeNode.ts | 13 ++- .../typeNodes/EnumStructVariantTypeNode.ts | 18 +++- .../typeNodes/EnumTupleVariantTypeNode.ts | 18 +++- .../src/generated/typeNodes/NumberTypeNode.ts | 15 ++- .../src/generated/typeNodes/StringTypeNode.ts | 15 ++- .../typeNodes/StructFieldTypeNode.ts | 11 ++- .../valueNodes/InjectableNumberValueNode.ts | 2 + .../valueNodes/InjectableStringValueNode.ts | 2 + .../generated/valueNodes/InjectedValueNode.ts | 28 ++++++ .../valueNodes/StandaloneValueNode.ts | 1 + .../nodes/src/generated/valueNodes/index.ts | 3 + packages/spec-generators/package.json | 2 +- .../src/nodeTypes/fragments/typeExpr.ts | 2 + packages/spec-generators/src/nodes/config.ts | 14 +++ .../src/nodes/fragments/inputType.ts | 16 +++- .../src/nodes/fragments/typeExpr.ts | 2 + .../spec-generators/src/shared/defaults.ts | 5 +- .../fragments/identityWalkStep.ts | 7 ++ .../visitorsCore/fragments/mergeCollect.ts | 1 + .../src/visitorsCore/options.ts | 4 + .../src/visitorsCore/walkStep.ts | 5 + .../test/nodeTypes/fragments/typeExpr.test.ts | 7 ++ .../fragments/nodeTypeParameters.test.ts | 16 ++-- .../test/nodes/fragments/typeExpr.test.ts | 7 ++ .../src/generated/identityVisitor.ts | 94 +++++++++++++++++-- .../src/generated/mergeVisitor.ts | 55 ++++++++++- .../src/generated/nodeTestPaths.ts | 11 +++ .../visitors-core/src/getByteSizeVisitor.ts | 6 ++ .../src/getDebugStringVisitor.ts | 2 +- .../src/getMaxByteSizeVisitor.ts | 6 ++ .../visitors-core/test/mapVisitor.test.ts | 4 +- .../visitors-core/test/mergeVisitor.test.ts | 4 +- .../test/nodes/ProvidedNode.test.ts | 33 +++++++ .../AccountFieldValueNode.test.ts | 27 ++++++ .../AmountNumberDisplayNode.test.ts | 42 +++++++++ .../DateTimeNumberDisplayNode.test.ts | 27 ++++++ .../DurationNumberDisplayNode.test.ts | 27 ++++++ .../EnumVariantDisplayNode.test.ts | 27 ++++++ .../InstructionAccountDisplayNode.test.ts | 27 ++++++ .../InstructionDisplayNode.test.ts | 30 ++++++ .../displayNodes/StringDisplayNode.test.ts | 27 ++++++ .../StructFieldDisplayNode.test.ts | 27 ++++++ .../valueNodes/InjectedValueNode.test.ts | 27 ++++++ pnpm-lock.yaml | 10 +- 105 files changed, 1351 insertions(+), 70 deletions(-) create mode 100644 packages/dynamic-codecs/test/values/InjectedValueNode.test.ts create mode 100644 packages/node-types/src/generated/ProvidedNode.ts create mode 100644 packages/node-types/src/generated/contextualValueNodes/AccountFieldValueNode.ts create mode 100644 packages/node-types/src/generated/displayNodes/AmountNumberDisplayNode.ts create mode 100644 packages/node-types/src/generated/displayNodes/DateTimeNumberDisplayNode.ts create mode 100644 packages/node-types/src/generated/displayNodes/DisplayNode.ts create mode 100644 packages/node-types/src/generated/displayNodes/DurationNumberDisplayNode.ts create mode 100644 packages/node-types/src/generated/displayNodes/EnumVariantDisplayNode.ts create mode 100644 packages/node-types/src/generated/displayNodes/InstructionAccountDisplayNode.ts create mode 100644 packages/node-types/src/generated/displayNodes/InstructionDisplayNode.ts create mode 100644 packages/node-types/src/generated/displayNodes/NumberDisplayNode.ts create mode 100644 packages/node-types/src/generated/displayNodes/RegisteredDisplayNode.ts create mode 100644 packages/node-types/src/generated/displayNodes/StringDisplayNode.ts create mode 100644 packages/node-types/src/generated/displayNodes/StructFieldDisplayNode.ts create mode 100644 packages/node-types/src/generated/displayNodes/index.ts create mode 100644 packages/node-types/src/generated/shared/displaySkip.ts create mode 100644 packages/node-types/src/generated/valueNodes/InjectableNumberValueNode.ts create mode 100644 packages/node-types/src/generated/valueNodes/InjectableStringValueNode.ts create mode 100644 packages/node-types/src/generated/valueNodes/InjectedValueNode.ts create mode 100644 packages/nodes/src/generated/ProvidedNode.ts create mode 100644 packages/nodes/src/generated/contextualValueNodes/AccountFieldValueNode.ts create mode 100644 packages/nodes/src/generated/displayNodes/AmountNumberDisplayNode.ts create mode 100644 packages/nodes/src/generated/displayNodes/DateTimeNumberDisplayNode.ts create mode 100644 packages/nodes/src/generated/displayNodes/DisplayNode.ts create mode 100644 packages/nodes/src/generated/displayNodes/DurationNumberDisplayNode.ts create mode 100644 packages/nodes/src/generated/displayNodes/EnumVariantDisplayNode.ts create mode 100644 packages/nodes/src/generated/displayNodes/InstructionAccountDisplayNode.ts create mode 100644 packages/nodes/src/generated/displayNodes/InstructionDisplayNode.ts create mode 100644 packages/nodes/src/generated/displayNodes/NumberDisplayNode.ts create mode 100644 packages/nodes/src/generated/displayNodes/RegisteredDisplayNode.ts create mode 100644 packages/nodes/src/generated/displayNodes/StringDisplayNode.ts create mode 100644 packages/nodes/src/generated/displayNodes/StructFieldDisplayNode.ts create mode 100644 packages/nodes/src/generated/displayNodes/index.ts create mode 100644 packages/nodes/src/generated/valueNodes/InjectableNumberValueNode.ts create mode 100644 packages/nodes/src/generated/valueNodes/InjectableStringValueNode.ts create mode 100644 packages/nodes/src/generated/valueNodes/InjectedValueNode.ts create mode 100644 packages/visitors-core/test/nodes/ProvidedNode.test.ts create mode 100644 packages/visitors-core/test/nodes/contextualValueNodes/AccountFieldValueNode.test.ts create mode 100644 packages/visitors-core/test/nodes/displayNodes/AmountNumberDisplayNode.test.ts create mode 100644 packages/visitors-core/test/nodes/displayNodes/DateTimeNumberDisplayNode.test.ts create mode 100644 packages/visitors-core/test/nodes/displayNodes/DurationNumberDisplayNode.test.ts create mode 100644 packages/visitors-core/test/nodes/displayNodes/EnumVariantDisplayNode.test.ts create mode 100644 packages/visitors-core/test/nodes/displayNodes/InstructionAccountDisplayNode.test.ts create mode 100644 packages/visitors-core/test/nodes/displayNodes/InstructionDisplayNode.test.ts create mode 100644 packages/visitors-core/test/nodes/displayNodes/StringDisplayNode.test.ts create mode 100644 packages/visitors-core/test/nodes/displayNodes/StructFieldDisplayNode.test.ts create mode 100644 packages/visitors-core/test/nodes/valueNodes/InjectedValueNode.test.ts diff --git a/packages/dynamic-codecs/src/values.ts b/packages/dynamic-codecs/src/values.ts index e506f49a1..d9fe0260b 100644 --- a/packages/dynamic-codecs/src/values.ts +++ b/packages/dynamic-codecs/src/values.ts @@ -1,4 +1,8 @@ -import { CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND, CodamaError } from '@codama/errors'; +import { + CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, + CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND, + CodamaError, +} from '@codama/errors'; import { assertIsNode, bytesTypeNode, isNode, isScalarEnum, pascalCase, ValueNode } from '@codama/nodes'; import { LinkableDictionary, NodeStack, pipe, recordNodeStackVisitor, visit, Visitor } from '@codama/visitors-core'; @@ -60,6 +64,16 @@ export function getValueNodeVisitor( } return kind; }, + visitInjectedValue(node) { + // `injectedValueNode` is a placeholder resolved by the provide/inject graph + // at instruction-build / presentation time. By codec time the resolution + // pass should have replaced it with a concrete value; reaching this layer + // unresolved indicates an upstream pipeline bug, not a value this visitor + // can decode without ancestor context. + throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, { + message: `injectedValueNode (key "${node.key}") reached codec evaluation; the provide/inject resolution pass must run first.`, + }); + }, visitMapValue(node) { return Object.fromEntries( node.entries.map(entry => { diff --git a/packages/dynamic-codecs/test/values/InjectedValueNode.test.ts b/packages/dynamic-codecs/test/values/InjectedValueNode.test.ts new file mode 100644 index 000000000..bf115ba27 --- /dev/null +++ b/packages/dynamic-codecs/test/values/InjectedValueNode.test.ts @@ -0,0 +1,22 @@ +import { CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, isCodamaError } from '@codama/errors'; +import { injectedValueNode } from '@codama/nodes'; +import { LinkableDictionary, visit } from '@codama/visitors-core'; +import { expect, test } from 'vitest'; + +import { getValueNodeVisitor } from '../../src'; + +test('it throws when an injectedValueNode reaches codec evaluation unresolved', () => { + // Given an injectedValueNode that the provide/inject pass never replaced with a concrete value. + const node = injectedValueNode({ key: 'decimals' }); + + // When we visit it with the value-node codec visitor. + const visitInjectedValue = () => visit(node, getValueNodeVisitor(new LinkableDictionary())); + + // Then it throws a pipeline invariant-violation error naming the unresolved node. + expect(visitInjectedValue).toThrow(/injectedValueNode \(key "decimals"\) reached codec evaluation/); + try { + visitInjectedValue(); + } catch (error) { + expect(isCodamaError(error, CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION)).toBe(true); + } +}); diff --git a/packages/node-types/src/generated/InstructionAccountNode.ts b/packages/node-types/src/generated/InstructionAccountNode.ts index ec017e34f..7a6e58d49 100644 --- a/packages/node-types/src/generated/InstructionAccountNode.ts +++ b/packages/node-types/src/generated/InstructionAccountNode.ts @@ -1,10 +1,14 @@ import type { CamelCaseString } from '../brands'; import type { Docs } from '../Docs'; import type { InstructionInputValueNode } from './contextualValueNodes/InstructionInputValueNode'; +import type { InstructionAccountDisplayNode } from './displayNodes/InstructionAccountDisplayNode'; +import type { AccountLinkNode } from './linkNodes/AccountLinkNode'; /** An account participating in an instruction, with its name, signing/writability flags, and an optional default value. */ export interface InstructionAccountNode< TDefaultValue extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined, + TAccountLink extends AccountLinkNode | undefined = AccountLinkNode | undefined, + TDisplay extends InstructionAccountDisplayNode | undefined = InstructionAccountDisplayNode | undefined, > { readonly kind: 'instructionAccountNode'; @@ -26,4 +30,11 @@ export interface InstructionAccountNode< // Children. /** A default value used to fill the slot when the caller does not provide one. */ readonly defaultValue?: TDefaultValue; + /** + * A reference to the account's data layout. Required for consumers (e.g. `accountFieldValueNode`) to read fields from the account. + * The link's optional `program` allows cross-program references via the root's `additionalPrograms`. + */ + readonly accountLink?: TAccountLink; + /** Display metadata describing how the account is presented. */ + readonly display?: TDisplay; } diff --git a/packages/node-types/src/generated/InstructionArgumentNode.ts b/packages/node-types/src/generated/InstructionArgumentNode.ts index 707eb7d71..a28f0d932 100644 --- a/packages/node-types/src/generated/InstructionArgumentNode.ts +++ b/packages/node-types/src/generated/InstructionArgumentNode.ts @@ -1,6 +1,7 @@ import type { CamelCaseString } from '../brands'; import type { Docs } from '../Docs'; import type { InstructionInputValueNode } from './contextualValueNodes/InstructionInputValueNode'; +import type { StructFieldDisplayNode } from './displayNodes/StructFieldDisplayNode'; import type { DefaultValueStrategy } from './shared/defaultValueStrategy'; import type { TypeNode } from './typeNodes/TypeNode'; @@ -8,6 +9,7 @@ import type { TypeNode } from './typeNodes/TypeNode'; export interface InstructionArgumentNode< TDefaultValue extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined, TType extends TypeNode = TypeNode, + TDisplay extends StructFieldDisplayNode | undefined = StructFieldDisplayNode | undefined, > { readonly kind: 'instructionArgumentNode'; @@ -24,4 +26,6 @@ export interface InstructionArgumentNode< readonly type: TType; /** A default value used when the argument is omitted by callers. */ readonly defaultValue?: TDefaultValue; + /** Display metadata describing how the argument is presented. */ + readonly display?: TDisplay; } diff --git a/packages/node-types/src/generated/InstructionNode.ts b/packages/node-types/src/generated/InstructionNode.ts index cb48240c8..f2a395765 100644 --- a/packages/node-types/src/generated/InstructionNode.ts +++ b/packages/node-types/src/generated/InstructionNode.ts @@ -1,11 +1,13 @@ import type { CamelCaseString } from '../brands'; import type { Docs } from '../Docs'; import type { DiscriminatorNode } from './discriminatorNodes/DiscriminatorNode'; +import type { InstructionDisplayNode } from './displayNodes/InstructionDisplayNode'; import type { InstructionAccountNode } from './InstructionAccountNode'; import type { InstructionArgumentNode } from './InstructionArgumentNode'; import type { InstructionByteDeltaNode } from './InstructionByteDeltaNode'; import type { InstructionRemainingAccountsNode } from './InstructionRemainingAccountsNode'; import type { InstructionStatusNode } from './InstructionStatusNode'; +import type { ProvidedNode } from './ProvidedNode'; import type { OptionalAccountStrategy } from './shared/optionalAccountStrategy'; type SelfInstructionNode = InstructionNode; @@ -22,6 +24,8 @@ export interface InstructionNode< TDiscriminators extends Array | undefined = Array | undefined, TSubInstructions extends Array | undefined = Array | undefined, TStatus extends InstructionStatusNode | undefined = InstructionStatusNode | undefined, + TProvides extends Array | undefined = Array | undefined, + TDisplay extends InstructionDisplayNode | undefined = InstructionDisplayNode | undefined, > { readonly kind: 'instructionNode'; @@ -53,4 +57,11 @@ export interface InstructionNode< readonly status?: TStatus; /** Inner instructions invoked through CPI as part of executing this instruction. */ readonly subInstructions?: TSubInstructions; + /** + * Named nodes exposed to consumers in the surrounding scope. + * Each entry pairs with an `injectedValueNode` that references it by key, so reusable types can pull contextual values without naming siblings directly. + */ + readonly provides?: TProvides; + /** Display metadata describing how the instruction is presented. */ + readonly display?: TDisplay; } diff --git a/packages/node-types/src/generated/InstructionRemainingAccountsNode.ts b/packages/node-types/src/generated/InstructionRemainingAccountsNode.ts index 822b04565..421432669 100644 --- a/packages/node-types/src/generated/InstructionRemainingAccountsNode.ts +++ b/packages/node-types/src/generated/InstructionRemainingAccountsNode.ts @@ -1,9 +1,11 @@ import type { Docs } from '../Docs'; +import type { InstructionAccountDisplayNode } from './displayNodes/InstructionAccountDisplayNode'; import type { InstructionRemainingAccountsValue } from './InstructionRemainingAccountsValue'; /** A "remaining accounts" slot in an instruction — a variable-length tail of accounts derived from a value. */ export interface InstructionRemainingAccountsNode< TValue extends InstructionRemainingAccountsValue = InstructionRemainingAccountsValue, + TDisplay extends InstructionAccountDisplayNode | undefined = InstructionAccountDisplayNode | undefined, > { readonly kind: 'instructionRemainingAccountsNode'; @@ -23,4 +25,6 @@ export interface InstructionRemainingAccountsNode< // Children. /** The source of the remaining-accounts list — a referenced argument or a resolver. */ readonly value: TValue; + /** Display metadata describing how the remaining-accounts group is presented as a whole. */ + readonly display?: TDisplay; } diff --git a/packages/node-types/src/generated/Node.ts b/packages/node-types/src/generated/Node.ts index d3cc8eca0..d80696824 100644 --- a/packages/node-types/src/generated/Node.ts +++ b/packages/node-types/src/generated/Node.ts @@ -4,6 +4,7 @@ import type { RegisteredContextualValueNode } from './contextualValueNodes/Regis import type { RegisteredCountNode } from './countNodes/RegisteredCountNode'; import type { DefinedTypeNode } from './DefinedTypeNode'; import type { RegisteredDiscriminatorNode } from './discriminatorNodes/RegisteredDiscriminatorNode'; +import type { RegisteredDisplayNode } from './displayNodes/RegisteredDisplayNode'; import type { ErrorNode } from './ErrorNode'; import type { EventNode } from './EventNode'; import type { InstructionAccountNode } from './InstructionAccountNode'; @@ -16,6 +17,7 @@ import type { RegisteredLinkNode } from './linkNodes/RegisteredLinkNode'; import type { PdaNode } from './PdaNode'; import type { RegisteredPdaSeedNode } from './pdaSeedNodes/RegisteredPdaSeedNode'; import type { ProgramNode } from './ProgramNode'; +import type { ProvidedNode } from './ProvidedNode'; import type { RootNode } from './RootNode'; import type { RegisteredTypeNode } from './typeNodes/RegisteredTypeNode'; import type { RegisteredValueNode } from './valueNodes/RegisteredValueNode'; @@ -36,9 +38,11 @@ export type Node = | InstructionStatusNode | PdaNode | ProgramNode + | ProvidedNode | RegisteredContextualValueNode | RegisteredCountNode | RegisteredDiscriminatorNode + | RegisteredDisplayNode | RegisteredLinkNode | RegisteredPdaSeedNode | RegisteredTypeNode diff --git a/packages/node-types/src/generated/ProvidedNode.ts b/packages/node-types/src/generated/ProvidedNode.ts new file mode 100644 index 000000000..b4e285499 --- /dev/null +++ b/packages/node-types/src/generated/ProvidedNode.ts @@ -0,0 +1,18 @@ +import type { CamelCaseString } from '../brands'; +import type { Node } from './Node'; + +/** + * Exposes a node under a name so consumers in the surrounding scope can resolve it by that key. + * Sits inside a host's `provides` list and pairs with `injectedValueNode` on the consumer side: an injection with the matching key resolves to this entry's `node`. + */ +export interface ProvidedNode { + readonly kind: 'providedNode'; + + // Data. + /** The key under which the node is exposed to consumers. */ + readonly name: CamelCaseString; + + // Children. + /** The exposed node. The provider is a transparent pipe — any node may be supplied; the family check happens at the injection point against the consumer's expected family. */ + readonly node: TNode; +} diff --git a/packages/node-types/src/generated/contextualValueNodes/AccountFieldValueNode.ts b/packages/node-types/src/generated/contextualValueNodes/AccountFieldValueNode.ts new file mode 100644 index 000000000..25086148f --- /dev/null +++ b/packages/node-types/src/generated/contextualValueNodes/AccountFieldValueNode.ts @@ -0,0 +1,19 @@ +import type { CamelCaseString } from '../../brands'; + +/** + * Refers to a field of a named account's decoded data. + * The referenced account must carry an `accountLink` so the account's layout is known. + * Resolving the value requires reading the account state at presentation time. + */ +export interface AccountFieldValueNode { + readonly kind: 'accountFieldValueNode'; + + // Data. + /** The name of the referenced account in the surrounding instruction. */ + readonly account: CamelCaseString; + /** + * The name of the field within the account's decoded data. + * When absent, the value is the whole decoded account data. + */ + readonly path?: CamelCaseString; +} diff --git a/packages/node-types/src/generated/contextualValueNodes/StandaloneContextualValueNode.ts b/packages/node-types/src/generated/contextualValueNodes/StandaloneContextualValueNode.ts index 2b7fee423..045b90b08 100644 --- a/packages/node-types/src/generated/contextualValueNodes/StandaloneContextualValueNode.ts +++ b/packages/node-types/src/generated/contextualValueNodes/StandaloneContextualValueNode.ts @@ -1,4 +1,5 @@ import type { AccountBumpValueNode } from './AccountBumpValueNode'; +import type { AccountFieldValueNode } from './AccountFieldValueNode'; import type { AccountValueNode } from './AccountValueNode'; import type { ArgumentValueNode } from './ArgumentValueNode'; import type { ConditionalValueNode } from './ConditionalValueNode'; @@ -11,6 +12,7 @@ import type { ResolverValueNode } from './ResolverValueNode'; /** Every contextual-value node usable as a top-level value. */ export type StandaloneContextualValueNode = | AccountBumpValueNode + | AccountFieldValueNode | AccountValueNode | ArgumentValueNode | ConditionalValueNode diff --git a/packages/node-types/src/generated/contextualValueNodes/index.ts b/packages/node-types/src/generated/contextualValueNodes/index.ts index 4df98fc0a..d1b006142 100644 --- a/packages/node-types/src/generated/contextualValueNodes/index.ts +++ b/packages/node-types/src/generated/contextualValueNodes/index.ts @@ -1,4 +1,5 @@ export * from './AccountBumpValueNode'; +export * from './AccountFieldValueNode'; export * from './AccountValueNode'; export * from './ArgumentValueNode'; export * from './ConditionalValueCondition'; diff --git a/packages/node-types/src/generated/displayNodes/AmountNumberDisplayNode.ts b/packages/node-types/src/generated/displayNodes/AmountNumberDisplayNode.ts new file mode 100644 index 000000000..38dc5c988 --- /dev/null +++ b/packages/node-types/src/generated/displayNodes/AmountNumberDisplayNode.ts @@ -0,0 +1,26 @@ +import type { InjectableNumberValueNode } from '../valueNodes/InjectableNumberValueNode'; +import type { InjectableStringValueNode } from '../valueNodes/InjectableStringValueNode'; + +/** + * Display metadata that presents a number as a scaled amount with an optional unit. + * The value is divided by `10 ^ decimals` and rendered alongside `unit` (e.g. `"USDC"`, `"%"`, `"bps"`). + */ +export interface AmountNumberDisplayNode< + TDecimals extends InjectableNumberValueNode | undefined = InjectableNumberValueNode | undefined, + TUnit extends InjectableStringValueNode | undefined = InjectableStringValueNode | undefined, +> { + readonly kind: 'amountNumberDisplayNode'; + + // Children. + /** + * How many decimal places scale the underlying integer. Resolved as a number value: either a literal `numberValueNode` or a key resolved from a surrounding provider. + * A value of `1000000` with `decimals` resolving to `6` renders as `1`. + * When this input cannot resolve, renderers should fall back to presenting the raw value rather than guess the scale. + */ + readonly decimals?: TDecimals; + /** + * A label appended after the scaled value (e.g. `"USDC"`, `"%"`, `"bps"`). Resolved as a string value: either a literal `stringValueNode` or a key resolved from a surrounding provider. + * When this input cannot resolve, renderers should present the scaled value without a unit. + */ + readonly unit?: TUnit; +} diff --git a/packages/node-types/src/generated/displayNodes/DateTimeNumberDisplayNode.ts b/packages/node-types/src/generated/displayNodes/DateTimeNumberDisplayNode.ts new file mode 100644 index 000000000..bbae64d82 --- /dev/null +++ b/packages/node-types/src/generated/displayNodes/DateTimeNumberDisplayNode.ts @@ -0,0 +1,14 @@ +/** + * Display metadata that presents a number as a point in time. + * The underlying value counts ticks since the Unix epoch; `ticksPerSecond` is the divisor that converts those ticks back to seconds. + */ +export interface DateTimeNumberDisplayNode { + readonly kind: 'dateTimeNumberDisplayNode'; + + // Data. + /** + * How many ticks make one second. Defaults to `1` (the value is already in seconds). + * Common choices are `1000` (milliseconds), `1000000` (microseconds), and `1000000000` (nanoseconds). + */ + readonly ticksPerSecond?: number; +} diff --git a/packages/node-types/src/generated/displayNodes/DisplayNode.ts b/packages/node-types/src/generated/displayNodes/DisplayNode.ts new file mode 100644 index 000000000..389fe323c --- /dev/null +++ b/packages/node-types/src/generated/displayNodes/DisplayNode.ts @@ -0,0 +1,4 @@ +import type { RegisteredDisplayNode } from './RegisteredDisplayNode'; + +/** The composable form: any registered display node. */ +export type DisplayNode = RegisteredDisplayNode; diff --git a/packages/node-types/src/generated/displayNodes/DurationNumberDisplayNode.ts b/packages/node-types/src/generated/displayNodes/DurationNumberDisplayNode.ts new file mode 100644 index 000000000..47cccd061 --- /dev/null +++ b/packages/node-types/src/generated/displayNodes/DurationNumberDisplayNode.ts @@ -0,0 +1,15 @@ +/** + * Display metadata that presents a number as an elapsed duration. + * The underlying value counts ticks; `ticksPerSecond` is the divisor that converts those ticks back to seconds. + * Renderers typically format the result as `HH:mm:ss` or a coarser human-readable form. + */ +export interface DurationNumberDisplayNode { + readonly kind: 'durationNumberDisplayNode'; + + // Data. + /** + * How many ticks make one second. Defaults to `1` (the value is already in seconds). + * Common choices are `1000` (milliseconds), `1000000` (microseconds), and `1000000000` (nanoseconds). + */ + readonly ticksPerSecond?: number; +} diff --git a/packages/node-types/src/generated/displayNodes/EnumVariantDisplayNode.ts b/packages/node-types/src/generated/displayNodes/EnumVariantDisplayNode.ts new file mode 100644 index 000000000..992fed1e7 --- /dev/null +++ b/packages/node-types/src/generated/displayNodes/EnumVariantDisplayNode.ts @@ -0,0 +1,16 @@ +/** Display metadata for an enum variant: its label and whether to hide its inner payload. */ +export interface EnumVariantDisplayNode { + readonly kind: 'enumVariantDisplayNode'; + + // Data. + /** + * An override label shown for the variant (e.g. `"Buy"`). + * When absent, renderers derive a label from the variant `name`. + */ + readonly label?: string; + /** + * When `true`, the variant's payload is hidden — only the label is rendered. + * Useful for tuple payloads that have no per-field handle, or when the payload is purely structural. + */ + readonly skipInnerData?: boolean; +} diff --git a/packages/node-types/src/generated/displayNodes/InstructionAccountDisplayNode.ts b/packages/node-types/src/generated/displayNodes/InstructionAccountDisplayNode.ts new file mode 100644 index 000000000..0ced8b5d7 --- /dev/null +++ b/packages/node-types/src/generated/displayNodes/InstructionAccountDisplayNode.ts @@ -0,0 +1,15 @@ +import type { DisplaySkip } from '../shared/displaySkip'; + +/** Display metadata for an instruction account: its label in the fallback list and whether it is shown. */ +export interface InstructionAccountDisplayNode { + readonly kind: 'instructionAccountDisplayNode'; + + // Data. + /** + * An override label shown in the fallback list (e.g. `"To"`). + * When absent, renderers derive a label from the account `name`. + */ + readonly label?: string; + /** Whether the account is shown in the fallback list. Defaults to `"never"` (always shown). */ + readonly skip?: DisplaySkip; +} diff --git a/packages/node-types/src/generated/displayNodes/InstructionDisplayNode.ts b/packages/node-types/src/generated/displayNodes/InstructionDisplayNode.ts new file mode 100644 index 000000000..a8261c497 --- /dev/null +++ b/packages/node-types/src/generated/displayNodes/InstructionDisplayNode.ts @@ -0,0 +1,17 @@ +/** + * Display metadata for an instruction: a short intent label and an interpolated sentence template. + * Either form may be absent; presentation strategy is left to the renderer. + */ +export interface InstructionDisplayNode { + readonly kind: 'instructionDisplayNode'; + + // Data. + /** A short imperative label describing what the instruction does (e.g. `"Transfer"`). */ + readonly intent?: string; + /** + * A sentence template that composes the instruction into prose with `${root.path}` placeholders. + * Roots are `data.` (an instruction argument) and `accounts.` (an instruction account); the path is flat after the root (e.g. `${data.amount}`, `${accounts.destination}`). + * A placeholder renders through its referent's own presentation; the `skip` rule governs the fallback list only and never the sentence. + */ + readonly interpolatedIntent?: string; +} diff --git a/packages/node-types/src/generated/displayNodes/NumberDisplayNode.ts b/packages/node-types/src/generated/displayNodes/NumberDisplayNode.ts new file mode 100644 index 000000000..d0e05293d --- /dev/null +++ b/packages/node-types/src/generated/displayNodes/NumberDisplayNode.ts @@ -0,0 +1,6 @@ +import type { AmountNumberDisplayNode } from './AmountNumberDisplayNode'; +import type { DateTimeNumberDisplayNode } from './DateTimeNumberDisplayNode'; +import type { DurationNumberDisplayNode } from './DurationNumberDisplayNode'; + +/** The presentation forms a number may take. Raw rendering is expressed by the absence of a display attribute. */ +export type NumberDisplayNode = AmountNumberDisplayNode | DateTimeNumberDisplayNode | DurationNumberDisplayNode; diff --git a/packages/node-types/src/generated/displayNodes/RegisteredDisplayNode.ts b/packages/node-types/src/generated/displayNodes/RegisteredDisplayNode.ts new file mode 100644 index 000000000..16f2ed0dd --- /dev/null +++ b/packages/node-types/src/generated/displayNodes/RegisteredDisplayNode.ts @@ -0,0 +1,19 @@ +import type { AmountNumberDisplayNode } from './AmountNumberDisplayNode'; +import type { DateTimeNumberDisplayNode } from './DateTimeNumberDisplayNode'; +import type { DurationNumberDisplayNode } from './DurationNumberDisplayNode'; +import type { EnumVariantDisplayNode } from './EnumVariantDisplayNode'; +import type { InstructionAccountDisplayNode } from './InstructionAccountDisplayNode'; +import type { InstructionDisplayNode } from './InstructionDisplayNode'; +import type { StringDisplayNode } from './StringDisplayNode'; +import type { StructFieldDisplayNode } from './StructFieldDisplayNode'; + +/** Every node tagged as display metadata. */ +export type RegisteredDisplayNode = + | AmountNumberDisplayNode + | DateTimeNumberDisplayNode + | DurationNumberDisplayNode + | EnumVariantDisplayNode + | InstructionAccountDisplayNode + | InstructionDisplayNode + | StringDisplayNode + | StructFieldDisplayNode; diff --git a/packages/node-types/src/generated/displayNodes/StringDisplayNode.ts b/packages/node-types/src/generated/displayNodes/StringDisplayNode.ts new file mode 100644 index 000000000..06728a7cd --- /dev/null +++ b/packages/node-types/src/generated/displayNodes/StringDisplayNode.ts @@ -0,0 +1,19 @@ +/** + * Display metadata for a string value. + * The string's wire encoding is carried by `stringTypeNode.encoding`; this node only addresses presentation. + */ +export interface StringDisplayNode { + readonly kind: 'stringDisplayNode'; + + // Data. + /** + * The start index of the displayed slice, inclusive. Defaults to the start of the string. + * Indices apply to the decoded character sequence. + */ + readonly sliceStart?: number; + /** + * The end index of the displayed slice, exclusive. Defaults to the end of the string. + * Indices apply to the decoded character sequence. + */ + readonly sliceEnd?: number; +} diff --git a/packages/node-types/src/generated/displayNodes/StructFieldDisplayNode.ts b/packages/node-types/src/generated/displayNodes/StructFieldDisplayNode.ts new file mode 100644 index 000000000..564059e0e --- /dev/null +++ b/packages/node-types/src/generated/displayNodes/StructFieldDisplayNode.ts @@ -0,0 +1,29 @@ +import type { DisplaySkip } from '../shared/displaySkip'; + +/** + * Display metadata for a named member: its label, whether it is shown in the fallback list, and whether it is flattened into its parent. + * Value presentation is carried by the member's type; this node only addresses naming and composition. + */ +export interface StructFieldDisplayNode { + readonly kind: 'structFieldDisplayNode'; + + // Data. + /** + * An override label shown for the member (e.g. `"Amount"`). + * When absent, renderers derive a label from the member `name`. + */ + readonly label?: string; + /** Whether the member is shown in the fallback list. Defaults to `"never"` (always shown). */ + readonly skip?: DisplaySkip; + /** + * When `true`, the member's type is expected to be a struct and its fields are lifted into the parent's context, dropping the field name as an extra level of nesting. + * Flattening lives on the field rather than on the struct so the same struct can be flattened in one place and nested in another. + * Meaningful only when the member's type is structurally a struct; renderers ignore it otherwise. + */ + readonly flatten?: boolean; + /** + * A literal prefix prepended to each flattened member's label (e.g. `"args."`). + * Meaningful only when `flatten` is `true`. Useful to disambiguate when two flattened children might collide. + */ + readonly flattenPrefix?: string; +} diff --git a/packages/node-types/src/generated/displayNodes/index.ts b/packages/node-types/src/generated/displayNodes/index.ts new file mode 100644 index 000000000..ef6b97d31 --- /dev/null +++ b/packages/node-types/src/generated/displayNodes/index.ts @@ -0,0 +1,11 @@ +export * from './AmountNumberDisplayNode'; +export * from './DateTimeNumberDisplayNode'; +export * from './DisplayNode'; +export * from './DurationNumberDisplayNode'; +export * from './EnumVariantDisplayNode'; +export * from './InstructionAccountDisplayNode'; +export * from './InstructionDisplayNode'; +export * from './NumberDisplayNode'; +export * from './RegisteredDisplayNode'; +export * from './StringDisplayNode'; +export * from './StructFieldDisplayNode'; diff --git a/packages/node-types/src/generated/index.ts b/packages/node-types/src/generated/index.ts index 14e1d6761..656c4bfcd 100644 --- a/packages/node-types/src/generated/index.ts +++ b/packages/node-types/src/generated/index.ts @@ -14,11 +14,13 @@ export * from './InstructionStatusNode'; export * from './Node'; export * from './PdaNode'; export * from './ProgramNode'; +export * from './ProvidedNode'; export * from './RootNode'; export * from './contextualValueNodes'; export * from './countNodes'; export * from './discriminatorNodes'; +export * from './displayNodes'; export * from './linkNodes'; export * from './pdaSeedNodes'; export * from './shared'; diff --git a/packages/node-types/src/generated/shared/codamaVersion.ts b/packages/node-types/src/generated/shared/codamaVersion.ts index e7936cd11..e4bca3fac 100644 --- a/packages/node-types/src/generated/shared/codamaVersion.ts +++ b/packages/node-types/src/generated/shared/codamaVersion.ts @@ -3,4 +3,4 @@ * version of the spec at generation time; documents conforming to this * version of the spec carry this exact string. */ -export type CodamaVersion = '1.6.0'; +export type CodamaVersion = '1.7.0'; diff --git a/packages/node-types/src/generated/shared/displaySkip.ts b/packages/node-types/src/generated/shared/displaySkip.ts new file mode 100644 index 000000000..7133577ec --- /dev/null +++ b/packages/node-types/src/generated/shared/displaySkip.ts @@ -0,0 +1,14 @@ +/** + * Whether a member should be hidden from the fallback display list. + * The interpolated sentence on `instructionDisplayNode` is governed separately — a member may be referenced there regardless of its skip value. + */ +export type DisplaySkip = + /** The member is never shown in the fallback list. Use for purely structural fields like discriminators. */ + | 'always' + /** The member is always shown in the fallback list. This is the default. */ + | 'never' + /** + * The member is shown only when its value was not already surfaced elsewhere through the provide/inject graph. + * When the value was pulled via injection, the member is hidden as redundant; when nothing surfaced it, the member appears under its label as a backup. + */ + | 'whenInjected'; diff --git a/packages/node-types/src/generated/shared/index.ts b/packages/node-types/src/generated/shared/index.ts index 173d8155b..485884519 100644 --- a/packages/node-types/src/generated/shared/index.ts +++ b/packages/node-types/src/generated/shared/index.ts @@ -1,6 +1,7 @@ export * from './bytesEncoding'; export * from './codamaVersion'; export * from './defaultValueStrategy'; +export * from './displaySkip'; export * from './endianness'; export * from './instructionLifecycle'; export * from './numberFormat'; diff --git a/packages/node-types/src/generated/typeNodes/EnumEmptyVariantTypeNode.ts b/packages/node-types/src/generated/typeNodes/EnumEmptyVariantTypeNode.ts index 3663e9598..0170c550e 100644 --- a/packages/node-types/src/generated/typeNodes/EnumEmptyVariantTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/EnumEmptyVariantTypeNode.ts @@ -1,7 +1,10 @@ import type { CamelCaseString } from '../../brands'; +import type { EnumVariantDisplayNode } from '../displayNodes/EnumVariantDisplayNode'; /** A unit-style variant of an enum that carries no payload. */ -export interface EnumEmptyVariantTypeNode { +export interface EnumEmptyVariantTypeNode< + TDisplay extends EnumVariantDisplayNode | undefined = EnumVariantDisplayNode | undefined, +> { readonly kind: 'enumEmptyVariantTypeNode'; // Data. @@ -9,4 +12,8 @@ export interface EnumEmptyVariantTypeNode { readonly name: CamelCaseString; /** Explicit discriminator value. When omitted, the discriminator is inferred from the variant position. */ readonly discriminator?: number; + + // Children. + /** Display metadata describing how the variant is presented. */ + readonly display?: TDisplay; } diff --git a/packages/node-types/src/generated/typeNodes/EnumStructVariantTypeNode.ts b/packages/node-types/src/generated/typeNodes/EnumStructVariantTypeNode.ts index d6ced0411..c1a40cf5d 100644 --- a/packages/node-types/src/generated/typeNodes/EnumStructVariantTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/EnumStructVariantTypeNode.ts @@ -1,10 +1,12 @@ import type { CamelCaseString } from '../../brands'; +import type { EnumVariantDisplayNode } from '../displayNodes/EnumVariantDisplayNode'; import type { NestedTypeNode } from './NestedTypeNode'; import type { StructTypeNode } from './StructTypeNode'; /** A variant of an enum that carries a struct payload (named fields). */ export interface EnumStructVariantTypeNode< TStruct extends NestedTypeNode = NestedTypeNode, + TDisplay extends EnumVariantDisplayNode | undefined = EnumVariantDisplayNode | undefined, > { readonly kind: 'enumStructVariantTypeNode'; @@ -17,4 +19,6 @@ export interface EnumStructVariantTypeNode< // Children. /** The struct of named fields carried by the variant. */ readonly struct: TStruct; + /** Display metadata describing how the variant is presented. */ + readonly display?: TDisplay; } diff --git a/packages/node-types/src/generated/typeNodes/EnumTupleVariantTypeNode.ts b/packages/node-types/src/generated/typeNodes/EnumTupleVariantTypeNode.ts index ce019eadd..72dc6bd0e 100644 --- a/packages/node-types/src/generated/typeNodes/EnumTupleVariantTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/EnumTupleVariantTypeNode.ts @@ -1,10 +1,12 @@ import type { CamelCaseString } from '../../brands'; +import type { EnumVariantDisplayNode } from '../displayNodes/EnumVariantDisplayNode'; import type { NestedTypeNode } from './NestedTypeNode'; import type { TupleTypeNode } from './TupleTypeNode'; /** A variant of an enum that carries a tuple payload (positional fields). */ export interface EnumTupleVariantTypeNode< TTuple extends NestedTypeNode = NestedTypeNode, + TDisplay extends EnumVariantDisplayNode | undefined = EnumVariantDisplayNode | undefined, > { readonly kind: 'enumTupleVariantTypeNode'; @@ -17,4 +19,6 @@ export interface EnumTupleVariantTypeNode< // Children. /** The tuple of positional fields carried by the variant. */ readonly tuple: TTuple; + /** Display metadata describing how the variant is presented. */ + readonly display?: TDisplay; } diff --git a/packages/node-types/src/generated/typeNodes/NumberTypeNode.ts b/packages/node-types/src/generated/typeNodes/NumberTypeNode.ts index 8364373d4..818476652 100644 --- a/packages/node-types/src/generated/typeNodes/NumberTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/NumberTypeNode.ts @@ -1,8 +1,12 @@ +import type { NumberDisplayNode } from '../displayNodes/NumberDisplayNode'; import type { Endianness } from '../shared/endianness'; import type { NumberFormat } from '../shared/numberFormat'; /** A numeric type with a fixed wire format and byte order. */ -export interface NumberTypeNode { +export interface NumberTypeNode< + TFormat extends NumberFormat = NumberFormat, + TDisplay extends NumberDisplayNode | undefined = NumberDisplayNode | undefined, +> { readonly kind: 'numberTypeNode'; // Data. @@ -10,4 +14,8 @@ export interface NumberTypeNode { readonly format: TFormat; /** The byte order used to serialise the number. */ readonly endian: Endianness; + + // Children. + /** Display metadata describing how the number is presented. */ + readonly display?: TDisplay; } diff --git a/packages/node-types/src/generated/typeNodes/StringTypeNode.ts b/packages/node-types/src/generated/typeNodes/StringTypeNode.ts index fe28ef5c6..087247d95 100644 --- a/packages/node-types/src/generated/typeNodes/StringTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/StringTypeNode.ts @@ -1,3 +1,4 @@ +import type { StringDisplayNode } from '../displayNodes/StringDisplayNode'; import type { BytesEncoding } from '../shared/bytesEncoding'; /** @@ -5,10 +6,17 @@ import type { BytesEncoding } from '../shared/bytesEncoding'; * The encoding describes how its bytes are written. * The byte length is determined by an enclosing wrapper such as `sizePrefixTypeNode` or `fixedSizeTypeNode`. */ -export interface StringTypeNode { +export interface StringTypeNode< + TEncoding extends BytesEncoding = BytesEncoding, + TDisplay extends StringDisplayNode | undefined = StringDisplayNode | undefined, +> { readonly kind: 'stringTypeNode'; // Data. /** The byte encoding used to serialise the string. */ readonly encoding: TEncoding; + + // Children. + /** Display metadata describing how the string is presented. */ + readonly display?: TDisplay; } diff --git a/packages/node-types/src/generated/typeNodes/StructFieldTypeNode.ts b/packages/node-types/src/generated/typeNodes/StructFieldTypeNode.ts index ee5e5e4a3..41662bb0f 100644 --- a/packages/node-types/src/generated/typeNodes/StructFieldTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/StructFieldTypeNode.ts @@ -1,5 +1,6 @@ import type { CamelCaseString } from '../../brands'; import type { Docs } from '../../Docs'; +import type { StructFieldDisplayNode } from '../displayNodes/StructFieldDisplayNode'; import type { DefaultValueStrategy } from '../shared/defaultValueStrategy'; import type { ValueNode } from '../valueNodes/ValueNode'; import type { TypeNode } from './TypeNode'; @@ -8,6 +9,7 @@ import type { TypeNode } from './TypeNode'; export interface StructFieldTypeNode< TType extends TypeNode = TypeNode, TDefaultValue extends ValueNode | undefined = ValueNode | undefined, + TDisplay extends StructFieldDisplayNode | undefined = StructFieldDisplayNode | undefined, > { readonly kind: 'structFieldTypeNode'; @@ -24,4 +26,6 @@ export interface StructFieldTypeNode< readonly type: TType; /** A default value used when the field is omitted by callers. */ readonly defaultValue?: TDefaultValue; + /** Display metadata describing how the field is presented. */ + readonly display?: TDisplay; } diff --git a/packages/node-types/src/generated/valueNodes/InjectableNumberValueNode.ts b/packages/node-types/src/generated/valueNodes/InjectableNumberValueNode.ts new file mode 100644 index 000000000..125b0ca07 --- /dev/null +++ b/packages/node-types/src/generated/valueNodes/InjectableNumberValueNode.ts @@ -0,0 +1,5 @@ +import type { InjectedValueNode } from './InjectedValueNode'; +import type { NumberValueNode } from './NumberValueNode'; + +/** A concrete number value, or a key resolved at presentation time from a surrounding provider. */ +export type InjectableNumberValueNode = InjectedValueNode | NumberValueNode; diff --git a/packages/node-types/src/generated/valueNodes/InjectableStringValueNode.ts b/packages/node-types/src/generated/valueNodes/InjectableStringValueNode.ts new file mode 100644 index 000000000..cc67bd380 --- /dev/null +++ b/packages/node-types/src/generated/valueNodes/InjectableStringValueNode.ts @@ -0,0 +1,5 @@ +import type { InjectedValueNode } from './InjectedValueNode'; +import type { StringValueNode } from './StringValueNode'; + +/** A concrete string value, or a key resolved at presentation time from a surrounding provider. */ +export type InjectableStringValueNode = InjectedValueNode | StringValueNode; diff --git a/packages/node-types/src/generated/valueNodes/InjectedValueNode.ts b/packages/node-types/src/generated/valueNodes/InjectedValueNode.ts new file mode 100644 index 000000000..a05a1eb3a --- /dev/null +++ b/packages/node-types/src/generated/valueNodes/InjectedValueNode.ts @@ -0,0 +1,22 @@ +import type { CamelCaseString } from '../../brands'; +import type { ValueNode } from './ValueNode'; + +/** + * A value resolved by key from a surrounding provider. + * A `providedNode` higher in the resolution tree supplies the actual value; the consumer references only the `key`, so the same type stays portable across instructions that wire the key differently. + * Resolution is a per-context property: a value with the same key may resolve in one instruction and not another. + */ +export interface InjectedValueNode { + readonly kind: 'injectedValueNode'; + + // Data. + /** The key looked up against the surrounding provide/inject graph. */ + readonly key: CamelCaseString; + + // Children. + /** + * A value used when no provider supplies the key. + * When absent, the key is required: a provider must supply it for the surrounding context to be valid. + */ + readonly fallback?: TFallback; +} diff --git a/packages/node-types/src/generated/valueNodes/StandaloneValueNode.ts b/packages/node-types/src/generated/valueNodes/StandaloneValueNode.ts index bf3a4f660..d10571be0 100644 --- a/packages/node-types/src/generated/valueNodes/StandaloneValueNode.ts +++ b/packages/node-types/src/generated/valueNodes/StandaloneValueNode.ts @@ -3,6 +3,7 @@ import type { BooleanValueNode } from './BooleanValueNode'; import type { BytesValueNode } from './BytesValueNode'; import type { ConstantValueNode } from './ConstantValueNode'; import type { EnumValueNode } from './EnumValueNode'; +import type { InjectedValueNode } from './InjectedValueNode'; import type { MapValueNode } from './MapValueNode'; import type { NoneValueNode } from './NoneValueNode'; import type { NumberValueNode } from './NumberValueNode'; @@ -20,6 +21,7 @@ export type StandaloneValueNode = | BytesValueNode | ConstantValueNode | EnumValueNode + | InjectedValueNode | MapValueNode | NoneValueNode | NumberValueNode diff --git a/packages/node-types/src/generated/valueNodes/index.ts b/packages/node-types/src/generated/valueNodes/index.ts index f9c35d7fb..5a7fb1d8b 100644 --- a/packages/node-types/src/generated/valueNodes/index.ts +++ b/packages/node-types/src/generated/valueNodes/index.ts @@ -4,6 +4,9 @@ export * from './BytesValueNode'; export * from './ConstantValueNode'; export * from './EnumValueNode'; export * from './EnumValuePayload'; +export * from './InjectableNumberValueNode'; +export * from './InjectableStringValueNode'; +export * from './InjectedValueNode'; export * from './MapEntryValueNode'; export * from './MapValueNode'; export * from './NoneValueNode'; diff --git a/packages/nodes/src/generated/InstructionAccountNode.ts b/packages/nodes/src/generated/InstructionAccountNode.ts index 6e5123964..4785c48c1 100644 --- a/packages/nodes/src/generated/InstructionAccountNode.ts +++ b/packages/nodes/src/generated/InstructionAccountNode.ts @@ -1,17 +1,28 @@ -import type { InstructionAccountNode, InstructionInputValueNode } from '@codama/node-types'; +import type { + AccountLinkNode, + InstructionAccountDisplayNode, + InstructionAccountNode, + InstructionInputValueNode, +} from '@codama/node-types'; import { camelCase, DocsInput, parseDocs } from '../shared'; export type InstructionAccountNodeInput< TDefaultValue extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined, -> = Omit, 'docs' | 'kind' | 'name'> & { + TAccountLink extends AccountLinkNode | undefined = AccountLinkNode | undefined, + TDisplay extends InstructionAccountDisplayNode | undefined = InstructionAccountDisplayNode | undefined, +> = Omit, 'docs' | 'kind' | 'name'> & { readonly name: string; readonly docs?: DocsInput; }; /** An account participating in an instruction, with its name, signing/writability flags, and an optional default value. */ -export function instructionAccountNode( - input: InstructionAccountNodeInput, -): InstructionAccountNode { +export function instructionAccountNode< + const TDefaultValue extends InstructionInputValueNode | undefined = undefined, + const TAccountLink extends AccountLinkNode | undefined = undefined, + const TDisplay extends InstructionAccountDisplayNode | undefined = undefined, +>( + input: InstructionAccountNodeInput, +): InstructionAccountNode { const parsedDocs = parseDocs(input.docs); return Object.freeze({ kind: 'instructionAccountNode', @@ -25,5 +36,7 @@ export function instructionAccountNode = Omit, 'docs' | 'kind' | 'name'> & { + TDisplay extends StructFieldDisplayNode | undefined = StructFieldDisplayNode | undefined, +> = Omit, 'docs' | 'kind' | 'name'> & { readonly name: string; readonly docs?: DocsInput; }; @@ -13,7 +19,10 @@ export type InstructionArgumentNodeInput< export function instructionArgumentNode< const TDefaultValue extends InstructionInputValueNode | undefined = undefined, const TType extends TypeNode = TypeNode, ->(input: InstructionArgumentNodeInput): InstructionArgumentNode { + const TDisplay extends StructFieldDisplayNode | undefined = undefined, +>( + input: InstructionArgumentNodeInput, +): InstructionArgumentNode { const parsedDocs = parseDocs(input.docs); return Object.freeze({ kind: 'instructionArgumentNode', @@ -26,5 +35,6 @@ export function instructionArgumentNode< // Children. type: input.type, ...(input.defaultValue !== undefined && { defaultValue: input.defaultValue }), + ...(input.display !== undefined && { display: input.display }), }); } diff --git a/packages/nodes/src/generated/InstructionNode.ts b/packages/nodes/src/generated/InstructionNode.ts index 7885d19b6..29e076b38 100644 --- a/packages/nodes/src/generated/InstructionNode.ts +++ b/packages/nodes/src/generated/InstructionNode.ts @@ -3,9 +3,11 @@ import type { InstructionAccountNode, InstructionArgumentNode, InstructionByteDeltaNode, + InstructionDisplayNode, InstructionNode, InstructionRemainingAccountsNode, InstructionStatusNode, + ProvidedNode, } from '@codama/node-types'; import { camelCase, DocsInput, parseDocs } from '../shared'; @@ -20,6 +22,8 @@ export type InstructionNodeInput< TDiscriminators extends Array | undefined = Array | undefined, TSubInstructions extends Array | undefined = Array | undefined, TStatus extends InstructionStatusNode | undefined = InstructionStatusNode | undefined, + TProvides extends Array | undefined = Array | undefined, + TDisplay extends InstructionDisplayNode | undefined = InstructionDisplayNode | undefined, > = Omit< Partial< InstructionNode< @@ -30,7 +34,9 @@ export type InstructionNodeInput< TByteDeltas, TDiscriminators, TSubInstructions, - TStatus + TStatus, + TProvides, + TDisplay > >, 'docs' | 'kind' | 'name' @@ -49,6 +55,8 @@ export function instructionNode< const TDiscriminators extends Array | undefined = undefined, const TSubInstructions extends Array | undefined = undefined, const TStatus extends InstructionStatusNode | undefined = undefined, + const TProvides extends Array | undefined = undefined, + const TDisplay extends InstructionDisplayNode | undefined = undefined, >( input: InstructionNodeInput< TAccounts, @@ -58,7 +66,9 @@ export function instructionNode< TByteDeltas, TDiscriminators, TSubInstructions, - TStatus + TStatus, + TProvides, + TDisplay >, ): InstructionNode< TAccounts, @@ -68,7 +78,9 @@ export function instructionNode< TByteDeltas, TDiscriminators, TSubInstructions, - TStatus + TStatus, + TProvides, + TDisplay > { const parsedDocs = parseDocs(input.docs); return Object.freeze({ @@ -88,5 +100,7 @@ export function instructionNode< ...(input.discriminators !== undefined && { discriminators: input.discriminators }), ...(input.status !== undefined && { status: input.status }), ...(input.subInstructions !== undefined && { subInstructions: input.subInstructions }), + ...(input.provides !== undefined && { provides: input.provides }), + ...(input.display !== undefined && { display: input.display }), }); } diff --git a/packages/nodes/src/generated/InstructionRemainingAccountsNode.ts b/packages/nodes/src/generated/InstructionRemainingAccountsNode.ts index 58dff1beb..ab447d800 100644 --- a/packages/nodes/src/generated/InstructionRemainingAccountsNode.ts +++ b/packages/nodes/src/generated/InstructionRemainingAccountsNode.ts @@ -1,16 +1,24 @@ -import type { InstructionRemainingAccountsNode, InstructionRemainingAccountsValue } from '@codama/node-types'; +import type { + InstructionAccountDisplayNode, + InstructionRemainingAccountsNode, + InstructionRemainingAccountsValue, +} from '@codama/node-types'; import { DocsInput, parseDocs } from '../shared'; /** A "remaining accounts" slot in an instruction — a variable-length tail of accounts derived from a value. */ -export function instructionRemainingAccountsNode( +export function instructionRemainingAccountsNode< + const TValue extends InstructionRemainingAccountsValue, + const TDisplay extends InstructionAccountDisplayNode | undefined = undefined, +>( value: TValue, options: { isOptional?: boolean; isSigner?: boolean | 'either'; isWritable?: boolean; docs?: DocsInput; + display?: TDisplay; } = {}, -): InstructionRemainingAccountsNode { +): InstructionRemainingAccountsNode { const parsedDocs = parseDocs(options.docs); return Object.freeze({ kind: 'instructionRemainingAccountsNode', @@ -23,5 +31,6 @@ export function instructionRemainingAccountsNode(name: string, value: TNode): ProvidedNode { + return Object.freeze({ + kind: 'providedNode', + + // Data. + name: camelCase(name), + + // Children. + node: value, + }); +} diff --git a/packages/nodes/src/generated/codamaVersion.ts b/packages/nodes/src/generated/codamaVersion.ts index e20913c9a..bfcdbd578 100644 --- a/packages/nodes/src/generated/codamaVersion.ts +++ b/packages/nodes/src/generated/codamaVersion.ts @@ -8,4 +8,4 @@ import type { CodamaVersion } from '@codama/node-types'; * that need to compare an IDL document's `version` against the spec * shape `@codama/nodes` understands. */ -export const CODAMA_VERSION: CodamaVersion = '1.6.0'; +export const CODAMA_VERSION: CodamaVersion = '1.7.0'; diff --git a/packages/nodes/src/generated/contextualValueNodes/AccountFieldValueNode.ts b/packages/nodes/src/generated/contextualValueNodes/AccountFieldValueNode.ts new file mode 100644 index 000000000..eb7629285 --- /dev/null +++ b/packages/nodes/src/generated/contextualValueNodes/AccountFieldValueNode.ts @@ -0,0 +1,22 @@ +import type { AccountFieldValueNode } from '@codama/node-types'; +import { camelCase } from '../../shared'; + +export type AccountFieldValueNodeInput = Omit & { + readonly account: string; + readonly path?: string; +}; + +/** + * Refers to a field of a named account's decoded data. + * The referenced account must carry an `accountLink` so the account's layout is known. + * Resolving the value requires reading the account state at presentation time. + */ +export function accountFieldValueNode(input: AccountFieldValueNodeInput): AccountFieldValueNode { + return Object.freeze({ + kind: 'accountFieldValueNode', + + // Data. + account: camelCase(input.account), + ...(input.path !== undefined && { path: camelCase(input.path) }), + }); +} diff --git a/packages/nodes/src/generated/contextualValueNodes/StandaloneContextualValueNode.ts b/packages/nodes/src/generated/contextualValueNodes/StandaloneContextualValueNode.ts index 63ad96dda..a4e499b35 100644 --- a/packages/nodes/src/generated/contextualValueNodes/StandaloneContextualValueNode.ts +++ b/packages/nodes/src/generated/contextualValueNodes/StandaloneContextualValueNode.ts @@ -1,6 +1,7 @@ /** Every contextual-value node usable as a top-level value. */ export const STANDALONE_CONTEXTUAL_VALUE_NODE_KINDS = [ 'accountBumpValueNode' as const, + 'accountFieldValueNode' as const, 'accountValueNode' as const, 'argumentValueNode' as const, 'conditionalValueNode' as const, diff --git a/packages/nodes/src/generated/contextualValueNodes/index.ts b/packages/nodes/src/generated/contextualValueNodes/index.ts index 4df98fc0a..d1b006142 100644 --- a/packages/nodes/src/generated/contextualValueNodes/index.ts +++ b/packages/nodes/src/generated/contextualValueNodes/index.ts @@ -1,4 +1,5 @@ export * from './AccountBumpValueNode'; +export * from './AccountFieldValueNode'; export * from './AccountValueNode'; export * from './ArgumentValueNode'; export * from './ConditionalValueCondition'; diff --git a/packages/nodes/src/generated/displayNodes/AmountNumberDisplayNode.ts b/packages/nodes/src/generated/displayNodes/AmountNumberDisplayNode.ts new file mode 100644 index 000000000..cf5394b9d --- /dev/null +++ b/packages/nodes/src/generated/displayNodes/AmountNumberDisplayNode.ts @@ -0,0 +1,23 @@ +import type { AmountNumberDisplayNode, InjectableNumberValueNode, InjectableStringValueNode } from '@codama/node-types'; + +export type AmountNumberDisplayNodeInput< + TDecimals extends InjectableNumberValueNode | undefined = InjectableNumberValueNode | undefined, + TUnit extends InjectableStringValueNode | undefined = InjectableStringValueNode | undefined, +> = Omit, 'kind'>; + +/** + * Display metadata that presents a number as a scaled amount with an optional unit. + * The value is divided by `10 ^ decimals` and rendered alongside `unit` (e.g. `"USDC"`, `"%"`, `"bps"`). + */ +export function amountNumberDisplayNode< + const TDecimals extends InjectableNumberValueNode | undefined = undefined, + const TUnit extends InjectableStringValueNode | undefined = undefined, +>(input: AmountNumberDisplayNodeInput): AmountNumberDisplayNode { + return Object.freeze({ + kind: 'amountNumberDisplayNode', + + // Children. + ...(input.decimals !== undefined && { decimals: input.decimals }), + ...(input.unit !== undefined && { unit: input.unit }), + }); +} diff --git a/packages/nodes/src/generated/displayNodes/DateTimeNumberDisplayNode.ts b/packages/nodes/src/generated/displayNodes/DateTimeNumberDisplayNode.ts new file mode 100644 index 000000000..88fa188fa --- /dev/null +++ b/packages/nodes/src/generated/displayNodes/DateTimeNumberDisplayNode.ts @@ -0,0 +1,16 @@ +import type { DateTimeNumberDisplayNode } from '@codama/node-types'; + +export type DateTimeNumberDisplayNodeInput = Omit; + +/** + * Display metadata that presents a number as a point in time. + * The underlying value counts ticks since the Unix epoch; `ticksPerSecond` is the divisor that converts those ticks back to seconds. + */ +export function dateTimeNumberDisplayNode(input: DateTimeNumberDisplayNodeInput): DateTimeNumberDisplayNode { + return Object.freeze({ + kind: 'dateTimeNumberDisplayNode', + + // Data. + ...(input.ticksPerSecond !== undefined && { ticksPerSecond: input.ticksPerSecond }), + }); +} diff --git a/packages/nodes/src/generated/displayNodes/DisplayNode.ts b/packages/nodes/src/generated/displayNodes/DisplayNode.ts new file mode 100644 index 000000000..2f4019b81 --- /dev/null +++ b/packages/nodes/src/generated/displayNodes/DisplayNode.ts @@ -0,0 +1,4 @@ +import { REGISTERED_DISPLAY_NODE_KINDS } from './RegisteredDisplayNode'; + +/** The composable form: any registered display node. */ +export const DISPLAY_NODE_KINDS = [...REGISTERED_DISPLAY_NODE_KINDS]; diff --git a/packages/nodes/src/generated/displayNodes/DurationNumberDisplayNode.ts b/packages/nodes/src/generated/displayNodes/DurationNumberDisplayNode.ts new file mode 100644 index 000000000..c10509fab --- /dev/null +++ b/packages/nodes/src/generated/displayNodes/DurationNumberDisplayNode.ts @@ -0,0 +1,17 @@ +import type { DurationNumberDisplayNode } from '@codama/node-types'; + +export type DurationNumberDisplayNodeInput = Omit; + +/** + * Display metadata that presents a number as an elapsed duration. + * The underlying value counts ticks; `ticksPerSecond` is the divisor that converts those ticks back to seconds. + * Renderers typically format the result as `HH:mm:ss` or a coarser human-readable form. + */ +export function durationNumberDisplayNode(input: DurationNumberDisplayNodeInput): DurationNumberDisplayNode { + return Object.freeze({ + kind: 'durationNumberDisplayNode', + + // Data. + ...(input.ticksPerSecond !== undefined && { ticksPerSecond: input.ticksPerSecond }), + }); +} diff --git a/packages/nodes/src/generated/displayNodes/EnumVariantDisplayNode.ts b/packages/nodes/src/generated/displayNodes/EnumVariantDisplayNode.ts new file mode 100644 index 000000000..50c21433d --- /dev/null +++ b/packages/nodes/src/generated/displayNodes/EnumVariantDisplayNode.ts @@ -0,0 +1,14 @@ +import type { EnumVariantDisplayNode } from '@codama/node-types'; + +export type EnumVariantDisplayNodeInput = Omit; + +/** Display metadata for an enum variant: its label and whether to hide its inner payload. */ +export function enumVariantDisplayNode(input: EnumVariantDisplayNodeInput): EnumVariantDisplayNode { + return Object.freeze({ + kind: 'enumVariantDisplayNode', + + // Data. + ...(input.label !== undefined && { label: input.label }), + ...(input.skipInnerData !== undefined && { skipInnerData: input.skipInnerData }), + }); +} diff --git a/packages/nodes/src/generated/displayNodes/InstructionAccountDisplayNode.ts b/packages/nodes/src/generated/displayNodes/InstructionAccountDisplayNode.ts new file mode 100644 index 000000000..65c156cb8 --- /dev/null +++ b/packages/nodes/src/generated/displayNodes/InstructionAccountDisplayNode.ts @@ -0,0 +1,16 @@ +import type { InstructionAccountDisplayNode } from '@codama/node-types'; + +export type InstructionAccountDisplayNodeInput = Omit; + +/** Display metadata for an instruction account: its label in the fallback list and whether it is shown. */ +export function instructionAccountDisplayNode( + input: InstructionAccountDisplayNodeInput, +): InstructionAccountDisplayNode { + return Object.freeze({ + kind: 'instructionAccountDisplayNode', + + // Data. + ...(input.label !== undefined && { label: input.label }), + ...(input.skip !== undefined && { skip: input.skip }), + }); +} diff --git a/packages/nodes/src/generated/displayNodes/InstructionDisplayNode.ts b/packages/nodes/src/generated/displayNodes/InstructionDisplayNode.ts new file mode 100644 index 000000000..953d3e4da --- /dev/null +++ b/packages/nodes/src/generated/displayNodes/InstructionDisplayNode.ts @@ -0,0 +1,17 @@ +import type { InstructionDisplayNode } from '@codama/node-types'; + +export type InstructionDisplayNodeInput = Omit; + +/** + * Display metadata for an instruction: a short intent label and an interpolated sentence template. + * Either form may be absent; presentation strategy is left to the renderer. + */ +export function instructionDisplayNode(input: InstructionDisplayNodeInput): InstructionDisplayNode { + return Object.freeze({ + kind: 'instructionDisplayNode', + + // Data. + ...(input.intent !== undefined && { intent: input.intent }), + ...(input.interpolatedIntent !== undefined && { interpolatedIntent: input.interpolatedIntent }), + }); +} diff --git a/packages/nodes/src/generated/displayNodes/NumberDisplayNode.ts b/packages/nodes/src/generated/displayNodes/NumberDisplayNode.ts new file mode 100644 index 000000000..cc7cca91b --- /dev/null +++ b/packages/nodes/src/generated/displayNodes/NumberDisplayNode.ts @@ -0,0 +1,6 @@ +/** The presentation forms a number may take. Raw rendering is expressed by the absence of a display attribute. */ +export const NUMBER_DISPLAY_NODE_KINDS = [ + 'amountNumberDisplayNode' as const, + 'dateTimeNumberDisplayNode' as const, + 'durationNumberDisplayNode' as const, +]; diff --git a/packages/nodes/src/generated/displayNodes/RegisteredDisplayNode.ts b/packages/nodes/src/generated/displayNodes/RegisteredDisplayNode.ts new file mode 100644 index 000000000..fa744aefb --- /dev/null +++ b/packages/nodes/src/generated/displayNodes/RegisteredDisplayNode.ts @@ -0,0 +1,11 @@ +/** Every node tagged as display metadata. */ +export const REGISTERED_DISPLAY_NODE_KINDS = [ + 'amountNumberDisplayNode' as const, + 'dateTimeNumberDisplayNode' as const, + 'durationNumberDisplayNode' as const, + 'enumVariantDisplayNode' as const, + 'instructionAccountDisplayNode' as const, + 'instructionDisplayNode' as const, + 'stringDisplayNode' as const, + 'structFieldDisplayNode' as const, +]; diff --git a/packages/nodes/src/generated/displayNodes/StringDisplayNode.ts b/packages/nodes/src/generated/displayNodes/StringDisplayNode.ts new file mode 100644 index 000000000..0da0165f6 --- /dev/null +++ b/packages/nodes/src/generated/displayNodes/StringDisplayNode.ts @@ -0,0 +1,17 @@ +import type { StringDisplayNode } from '@codama/node-types'; + +export type StringDisplayNodeInput = Omit; + +/** + * Display metadata for a string value. + * The string's wire encoding is carried by `stringTypeNode.encoding`; this node only addresses presentation. + */ +export function stringDisplayNode(input: StringDisplayNodeInput): StringDisplayNode { + return Object.freeze({ + kind: 'stringDisplayNode', + + // Data. + ...(input.sliceStart !== undefined && { sliceStart: input.sliceStart }), + ...(input.sliceEnd !== undefined && { sliceEnd: input.sliceEnd }), + }); +} diff --git a/packages/nodes/src/generated/displayNodes/StructFieldDisplayNode.ts b/packages/nodes/src/generated/displayNodes/StructFieldDisplayNode.ts new file mode 100644 index 000000000..905f00f70 --- /dev/null +++ b/packages/nodes/src/generated/displayNodes/StructFieldDisplayNode.ts @@ -0,0 +1,19 @@ +import type { StructFieldDisplayNode } from '@codama/node-types'; + +export type StructFieldDisplayNodeInput = Omit; + +/** + * Display metadata for a named member: its label, whether it is shown in the fallback list, and whether it is flattened into its parent. + * Value presentation is carried by the member's type; this node only addresses naming and composition. + */ +export function structFieldDisplayNode(input: StructFieldDisplayNodeInput): StructFieldDisplayNode { + return Object.freeze({ + kind: 'structFieldDisplayNode', + + // Data. + ...(input.label !== undefined && { label: input.label }), + ...(input.skip !== undefined && { skip: input.skip }), + ...(input.flatten !== undefined && { flatten: input.flatten }), + ...(input.flattenPrefix !== undefined && { flattenPrefix: input.flattenPrefix }), + }); +} diff --git a/packages/nodes/src/generated/displayNodes/index.ts b/packages/nodes/src/generated/displayNodes/index.ts new file mode 100644 index 000000000..ef6b97d31 --- /dev/null +++ b/packages/nodes/src/generated/displayNodes/index.ts @@ -0,0 +1,11 @@ +export * from './AmountNumberDisplayNode'; +export * from './DateTimeNumberDisplayNode'; +export * from './DisplayNode'; +export * from './DurationNumberDisplayNode'; +export * from './EnumVariantDisplayNode'; +export * from './InstructionAccountDisplayNode'; +export * from './InstructionDisplayNode'; +export * from './NumberDisplayNode'; +export * from './RegisteredDisplayNode'; +export * from './StringDisplayNode'; +export * from './StructFieldDisplayNode'; diff --git a/packages/nodes/src/generated/index.ts b/packages/nodes/src/generated/index.ts index 49811402e..822fb55de 100644 --- a/packages/nodes/src/generated/index.ts +++ b/packages/nodes/src/generated/index.ts @@ -15,11 +15,13 @@ export * from './InstructionStatusNode'; export * from './nodeKinds'; export * from './PdaNode'; export * from './ProgramNode'; +export * from './ProvidedNode'; export * from './RootNode'; export * from './contextualValueNodes'; export * from './countNodes'; export * from './discriminatorNodes'; +export * from './displayNodes'; export * from './linkNodes'; export * from './pdaSeedNodes'; export * from './typeNodes'; diff --git a/packages/nodes/src/generated/nodeKinds.ts b/packages/nodes/src/generated/nodeKinds.ts index d9b78697f..e926e1a7e 100644 --- a/packages/nodes/src/generated/nodeKinds.ts +++ b/packages/nodes/src/generated/nodeKinds.ts @@ -1,6 +1,7 @@ import { REGISTERED_CONTEXTUAL_VALUE_NODE_KINDS } from './contextualValueNodes/RegisteredContextualValueNode'; import { REGISTERED_COUNT_NODE_KINDS } from './countNodes/RegisteredCountNode'; import { REGISTERED_DISCRIMINATOR_NODE_KINDS } from './discriminatorNodes/RegisteredDiscriminatorNode'; +import { REGISTERED_DISPLAY_NODE_KINDS } from './displayNodes/RegisteredDisplayNode'; import { REGISTERED_LINK_NODE_KINDS } from './linkNodes/RegisteredLinkNode'; import { REGISTERED_PDA_SEED_NODE_KINDS } from './pdaSeedNodes/RegisteredPdaSeedNode'; import { REGISTERED_TYPE_NODE_KINDS } from './typeNodes/RegisteredTypeNode'; @@ -21,10 +22,12 @@ export const REGISTERED_NODE_KINDS = [ 'instructionStatusNode' as const, 'pdaNode' as const, 'programNode' as const, + 'providedNode' as const, 'rootNode' as const, ...REGISTERED_CONTEXTUAL_VALUE_NODE_KINDS, ...REGISTERED_COUNT_NODE_KINDS, ...REGISTERED_DISCRIMINATOR_NODE_KINDS, + ...REGISTERED_DISPLAY_NODE_KINDS, ...REGISTERED_LINK_NODE_KINDS, ...REGISTERED_PDA_SEED_NODE_KINDS, ...REGISTERED_TYPE_NODE_KINDS, diff --git a/packages/nodes/src/generated/typeNodes/EnumEmptyVariantTypeNode.ts b/packages/nodes/src/generated/typeNodes/EnumEmptyVariantTypeNode.ts index 5b3d86f6a..d9559be5e 100644 --- a/packages/nodes/src/generated/typeNodes/EnumEmptyVariantTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/EnumEmptyVariantTypeNode.ts @@ -1,13 +1,22 @@ -import type { EnumEmptyVariantTypeNode } from '@codama/node-types'; +import type { EnumEmptyVariantTypeNode, EnumVariantDisplayNode } from '@codama/node-types'; import { camelCase } from '../../shared'; /** A unit-style variant of an enum that carries no payload. */ -export function enumEmptyVariantTypeNode(name: string, discriminator?: number): EnumEmptyVariantTypeNode { +export function enumEmptyVariantTypeNode( + name: string, + discriminator?: number, + options: { + display?: TDisplay; + } = {}, +): EnumEmptyVariantTypeNode { return Object.freeze({ kind: 'enumEmptyVariantTypeNode', // Data. name: camelCase(name), ...(discriminator !== undefined && { discriminator }), + + // Children. + ...(options.display !== undefined && { display: options.display }), }); } diff --git a/packages/nodes/src/generated/typeNodes/EnumStructVariantTypeNode.ts b/packages/nodes/src/generated/typeNodes/EnumStructVariantTypeNode.ts index d22cf7210..7d3dbcdfa 100644 --- a/packages/nodes/src/generated/typeNodes/EnumStructVariantTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/EnumStructVariantTypeNode.ts @@ -1,12 +1,23 @@ -import type { EnumStructVariantTypeNode, NestedTypeNode, StructTypeNode } from '@codama/node-types'; +import type { + EnumStructVariantTypeNode, + EnumVariantDisplayNode, + NestedTypeNode, + StructTypeNode, +} from '@codama/node-types'; import { camelCase } from '../../shared'; /** A variant of an enum that carries a struct payload (named fields). */ -export function enumStructVariantTypeNode>( +export function enumStructVariantTypeNode< + const TStruct extends NestedTypeNode, + const TDisplay extends EnumVariantDisplayNode | undefined = undefined, +>( name: string, struct: TStruct, discriminator?: number, -): EnumStructVariantTypeNode { + options: { + display?: TDisplay; + } = {}, +): EnumStructVariantTypeNode { return Object.freeze({ kind: 'enumStructVariantTypeNode', @@ -16,5 +27,6 @@ export function enumStructVariantTypeNode>( +export function enumTupleVariantTypeNode< + const TTuple extends NestedTypeNode, + const TDisplay extends EnumVariantDisplayNode | undefined = undefined, +>( name: string, tuple: TTuple, discriminator?: number, -): EnumTupleVariantTypeNode { + options: { + display?: TDisplay; + } = {}, +): EnumTupleVariantTypeNode { return Object.freeze({ kind: 'enumTupleVariantTypeNode', @@ -16,5 +27,6 @@ export function enumTupleVariantTypeNode( +export function numberTypeNode< + const TFormat extends NumberFormat = NumberFormat, + const TDisplay extends NumberDisplayNode | undefined = undefined, +>( format: TFormat, endian: Endianness = 'le', -): NumberTypeNode { + options: { + display?: TDisplay; + } = {}, +): NumberTypeNode { return Object.freeze({ kind: 'numberTypeNode', // Data. format, endian, + + // Children. + ...(options.display !== undefined && { display: options.display }), }); } diff --git a/packages/nodes/src/generated/typeNodes/StringTypeNode.ts b/packages/nodes/src/generated/typeNodes/StringTypeNode.ts index 358510b6e..c1f18b578 100644 --- a/packages/nodes/src/generated/typeNodes/StringTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/StringTypeNode.ts @@ -1,17 +1,26 @@ -import type { BytesEncoding, StringTypeNode } from '@codama/node-types'; +import type { BytesEncoding, StringDisplayNode, StringTypeNode } from '@codama/node-types'; /** * A string value. * The encoding describes how its bytes are written. * The byte length is determined by an enclosing wrapper such as `sizePrefixTypeNode` or `fixedSizeTypeNode`. */ -export function stringTypeNode( +export function stringTypeNode< + const TEncoding extends BytesEncoding = BytesEncoding, + const TDisplay extends StringDisplayNode | undefined = undefined, +>( encoding: TEncoding, -): StringTypeNode { + options: { + display?: TDisplay; + } = {}, +): StringTypeNode { return Object.freeze({ kind: 'stringTypeNode', // Data. encoding, + + // Children. + ...(options.display !== undefined && { display: options.display }), }); } diff --git a/packages/nodes/src/generated/typeNodes/StructFieldTypeNode.ts b/packages/nodes/src/generated/typeNodes/StructFieldTypeNode.ts index 19f618975..54df7e938 100644 --- a/packages/nodes/src/generated/typeNodes/StructFieldTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/StructFieldTypeNode.ts @@ -1,10 +1,11 @@ -import type { StructFieldTypeNode, TypeNode, ValueNode } from '@codama/node-types'; +import type { StructFieldDisplayNode, StructFieldTypeNode, TypeNode, ValueNode } from '@codama/node-types'; import { camelCase, DocsInput, parseDocs } from '../../shared'; export type StructFieldTypeNodeInput< TType extends TypeNode = TypeNode, TDefaultValue extends ValueNode | undefined = ValueNode | undefined, -> = Omit, 'docs' | 'kind' | 'name'> & { + TDisplay extends StructFieldDisplayNode | undefined = StructFieldDisplayNode | undefined, +> = Omit, 'docs' | 'kind' | 'name'> & { readonly name: string; readonly docs?: DocsInput; }; @@ -13,7 +14,10 @@ export type StructFieldTypeNodeInput< export function structFieldTypeNode< const TType extends TypeNode, const TDefaultValue extends ValueNode | undefined = undefined, ->(input: StructFieldTypeNodeInput): StructFieldTypeNode { + const TDisplay extends StructFieldDisplayNode | undefined = undefined, +>( + input: StructFieldTypeNodeInput, +): StructFieldTypeNode { const parsedDocs = parseDocs(input.docs); return Object.freeze({ kind: 'structFieldTypeNode', @@ -26,5 +30,6 @@ export function structFieldTypeNode< // Children. type: input.type, ...(input.defaultValue !== undefined && { defaultValue: input.defaultValue }), + ...(input.display !== undefined && { display: input.display }), }); } diff --git a/packages/nodes/src/generated/valueNodes/InjectableNumberValueNode.ts b/packages/nodes/src/generated/valueNodes/InjectableNumberValueNode.ts new file mode 100644 index 000000000..2d4e61dfd --- /dev/null +++ b/packages/nodes/src/generated/valueNodes/InjectableNumberValueNode.ts @@ -0,0 +1,2 @@ +/** A concrete number value, or a key resolved at presentation time from a surrounding provider. */ +export const INJECTABLE_NUMBER_VALUE_NODE_KINDS = ['injectedValueNode' as const, 'numberValueNode' as const]; diff --git a/packages/nodes/src/generated/valueNodes/InjectableStringValueNode.ts b/packages/nodes/src/generated/valueNodes/InjectableStringValueNode.ts new file mode 100644 index 000000000..51697332d --- /dev/null +++ b/packages/nodes/src/generated/valueNodes/InjectableStringValueNode.ts @@ -0,0 +1,2 @@ +/** A concrete string value, or a key resolved at presentation time from a surrounding provider. */ +export const INJECTABLE_STRING_VALUE_NODE_KINDS = ['injectedValueNode' as const, 'stringValueNode' as const]; diff --git a/packages/nodes/src/generated/valueNodes/InjectedValueNode.ts b/packages/nodes/src/generated/valueNodes/InjectedValueNode.ts new file mode 100644 index 000000000..6f0975964 --- /dev/null +++ b/packages/nodes/src/generated/valueNodes/InjectedValueNode.ts @@ -0,0 +1,28 @@ +import type { InjectedValueNode, ValueNode } from '@codama/node-types'; +import { camelCase } from '../../shared'; + +export type InjectedValueNodeInput = Omit< + InjectedValueNode, + 'key' | 'kind' +> & { + readonly key: string; +}; + +/** + * A value resolved by key from a surrounding provider. + * A `providedNode` higher in the resolution tree supplies the actual value; the consumer references only the `key`, so the same type stays portable across instructions that wire the key differently. + * Resolution is a per-context property: a value with the same key may resolve in one instruction and not another. + */ +export function injectedValueNode( + input: InjectedValueNodeInput, +): InjectedValueNode { + return Object.freeze({ + kind: 'injectedValueNode', + + // Data. + key: camelCase(input.key), + + // Children. + ...(input.fallback !== undefined && { fallback: input.fallback }), + }); +} diff --git a/packages/nodes/src/generated/valueNodes/StandaloneValueNode.ts b/packages/nodes/src/generated/valueNodes/StandaloneValueNode.ts index 246eaa643..002358d61 100644 --- a/packages/nodes/src/generated/valueNodes/StandaloneValueNode.ts +++ b/packages/nodes/src/generated/valueNodes/StandaloneValueNode.ts @@ -5,6 +5,7 @@ export const STANDALONE_VALUE_NODE_KINDS = [ 'bytesValueNode' as const, 'constantValueNode' as const, 'enumValueNode' as const, + 'injectedValueNode' as const, 'mapValueNode' as const, 'noneValueNode' as const, 'numberValueNode' as const, diff --git a/packages/nodes/src/generated/valueNodes/index.ts b/packages/nodes/src/generated/valueNodes/index.ts index f9c35d7fb..5a7fb1d8b 100644 --- a/packages/nodes/src/generated/valueNodes/index.ts +++ b/packages/nodes/src/generated/valueNodes/index.ts @@ -4,6 +4,9 @@ export * from './BytesValueNode'; export * from './ConstantValueNode'; export * from './EnumValueNode'; export * from './EnumValuePayload'; +export * from './InjectableNumberValueNode'; +export * from './InjectableStringValueNode'; +export * from './InjectedValueNode'; export * from './MapEntryValueNode'; export * from './MapValueNode'; export * from './NoneValueNode'; diff --git a/packages/spec-generators/package.json b/packages/spec-generators/package.json index 98caf7ccd..b7d8619e4 100644 --- a/packages/spec-generators/package.json +++ b/packages/spec-generators/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@codama/fragments": "workspace:*", - "@codama/spec": "1.6.0" + "@codama/spec": "1.7.0-rc.0" }, "devDependencies": { "@types/node": "^25", diff --git a/packages/spec-generators/src/nodeTypes/fragments/typeExpr.ts b/packages/spec-generators/src/nodeTypes/fragments/typeExpr.ts index 64e6e11fa..4a62bb2aa 100644 --- a/packages/spec-generators/src/nodeTypes/fragments/typeExpr.ts +++ b/packages/spec-generators/src/nodeTypes/fragments/typeExpr.ts @@ -5,6 +5,8 @@ export function getTypeExprFragment(expr: TypeExpr): Fragment { switch (expr.kind) { case 'address': return fragment`string`; + case 'anyNode': + return use('type Node', 'registry:Node'); case 'string': return getStringExprFragment(expr); case 'integer': diff --git a/packages/spec-generators/src/nodes/config.ts b/packages/spec-generators/src/nodes/config.ts index d0319b60a..4fcdbca45 100644 --- a/packages/spec-generators/src/nodes/config.ts +++ b/packages/spec-generators/src/nodes/config.ts @@ -52,6 +52,7 @@ export type AttributeOverride = readonly paramName?: string; } | { readonly coerce: Fragment; readonly paramName?: string } + | { readonly paramName: string } | { readonly value: Fragment }; export interface NodeConstructorConfig { @@ -348,6 +349,19 @@ export const NODE_CONFIGS: ReadonlyMap = new Map< ], ['programLinkNode', { positionalArgs: ['name'] }], + [ + 'providedNode', + { + attributes: { + // Rename the local identifier used in generated bodies (constructor positional + // arg, visitor walk-step local) so it does not shadow the outer `node` + // parameter that every visitor receives. + node: { paramName: 'value' }, + }, + positionalArgs: ['name', 'node'], + }, + ], + ['constantPdaSeedNode', { positionalArgs: ['type', 'value'] }], ['variablePdaSeedNode', { positionalArgs: ['name', 'type', 'docs'] }], ]); diff --git a/packages/spec-generators/src/nodes/fragments/inputType.ts b/packages/spec-generators/src/nodes/fragments/inputType.ts index 815f6a3b3..92d0419a5 100644 --- a/packages/spec-generators/src/nodes/fragments/inputType.ts +++ b/packages/spec-generators/src/nodes/fragments/inputType.ts @@ -16,6 +16,7 @@ import type { AttributeSpec, NodeSpec } from '@codama/spec'; import { getTypeParameterIdentifierFragment, getTypeParameterIdentifierListFragment } from '../../shared'; import type { NodeConstructorConfig } from '../config'; +import { isStringIdentifierAttr } from '../paramIdentifier'; import { getNodeTypeParameterConstraint } from './nodeTypeParameters'; export function getInputTypeFragment( @@ -80,15 +81,22 @@ function nodeHasDefaultedRequiredAttribute(node: NodeSpec, config: NodeConstruct /** * Fields the Input type re-asserts in the intersection block: * - * - `name: string` (relaxed from `CamelCaseString`) - * - `docs?: DocsInput` (relaxed from `Docs`) + * - Every `stringIdentifier()`-typed attribute relaxes from the branded + * `CamelCaseString` to plain `string` (the constructor body runs + * `camelCase(...)` on the value). This covers `name` and any other + * identifier-shaped attribute (e.g. `accountFieldValueNode.account`, + * `injectedValueNode.key`). + * - `docs?: DocsInput` (relaxed from `Docs`). * - `publicKey: ProgramNode['publicKey']` for `programNode` — a * required-not-defaulted field that survives the `Partial<>` wrap. */ function collectReassertedFields(node: NodeSpec): readonly { readonly name: string; readonly tsType: Fragment }[] { const out: { name: string; tsType: Fragment }[] = []; - if (node.attributes.some(a => a.name === 'name')) { - out.push({ name: 'name', tsType: fragment`string` }); + for (const attr of node.attributes) { + if (isStringIdentifierAttr(attr)) { + const fieldName = attr.optional ? `${attr.name}?` : attr.name; + out.push({ name: fieldName, tsType: fragment`string` }); + } } if (node.attributes.some(a => a.name === 'docs')) { const docsInput = use('DocsInput', 'shared:DocsInput'); diff --git a/packages/spec-generators/src/nodes/fragments/typeExpr.ts b/packages/spec-generators/src/nodes/fragments/typeExpr.ts index a227a66a0..9b2e9a6ef 100644 --- a/packages/spec-generators/src/nodes/fragments/typeExpr.ts +++ b/packages/spec-generators/src/nodes/fragments/typeExpr.ts @@ -17,6 +17,8 @@ export function getTypeExprFragment(expr: TypeExpr): Fragment { switch (expr.kind) { case 'address': return fragment`string`; + case 'anyNode': + return use('type Node', NODE_TYPES_PACKAGE); case 'string': return getStringExprFragment(expr); case 'integer': diff --git a/packages/spec-generators/src/shared/defaults.ts b/packages/spec-generators/src/shared/defaults.ts index c260d32c4..c175feda1 100644 --- a/packages/spec-generators/src/shared/defaults.ts +++ b/packages/spec-generators/src/shared/defaults.ts @@ -25,7 +25,7 @@ export const NARROWABLE_DATA_ATTRIBUTES: ReadonlySet = new Set([ export const GENERIC_PARAM_ORDER: ReadonlyMap = new Map([ ['programNode', ['pdas', 'accounts', 'instructions', 'definedTypes', 'errors', 'events', 'constants']], ['pdaValueNode', ['seeds', 'programId', 'pda']], - ['instructionArgumentNode', ['defaultValue', 'type']], + ['instructionArgumentNode', ['defaultValue', 'type', 'display']], [ 'instructionNode', [ @@ -37,6 +37,8 @@ export const GENERIC_PARAM_ORDER: ReadonlyMap = new M 'discriminators', 'subInstructions', 'status', + 'provides', + 'display', ], ], ]); @@ -50,6 +52,7 @@ export const CATEGORY_DIRECTORIES: ReadonlyMap = new Map([ ['contextualValue', 'contextualValueNodes'], ['count', 'countNodes'], ['discriminator', 'discriminatorNodes'], + ['display', 'displayNodes'], ['link', 'linkNodes'], ['pdaSeed', 'pdaSeedNodes'], ['shared', 'shared'], diff --git a/packages/spec-generators/src/visitorsCore/fragments/identityWalkStep.ts b/packages/spec-generators/src/visitorsCore/fragments/identityWalkStep.ts index 909cca1d2..3262bc79a 100644 --- a/packages/spec-generators/src/visitorsCore/fragments/identityWalkStep.ts +++ b/packages/spec-generators/src/visitorsCore/fragments/identityWalkStep.ts @@ -50,6 +50,13 @@ export function getIdentityWalkStep( case 'data': return { preStatement: undefined, rebuildExpr: fragment`${fieldAccess}` }; + case 'anyNode': + return buildSingleNodeStep(localName, fieldAccess, optional, { + assertCall: use('assertIsNode', '@codama/nodes'), + kindArg: use('REGISTERED_NODE_KINDS', '@codama/nodes'), + visitThis, + }); + case 'node': return buildSingleNodeStep(localName, fieldAccess, optional, { assertCall: use('assertIsNode', '@codama/nodes'), diff --git a/packages/spec-generators/src/visitorsCore/fragments/mergeCollect.ts b/packages/spec-generators/src/visitorsCore/fragments/mergeCollect.ts index 81d9ddd71..8d2b2f51f 100644 --- a/packages/spec-generators/src/visitorsCore/fragments/mergeCollect.ts +++ b/packages/spec-generators/src/visitorsCore/fragments/mergeCollect.ts @@ -22,6 +22,7 @@ export function getMergeCollectFragment(attr: AttributeSpec): Fragment | undefin case 'data': return undefined; + case 'anyNode': case 'node': case 'nestedNode': case 'union': diff --git a/packages/spec-generators/src/visitorsCore/options.ts b/packages/spec-generators/src/visitorsCore/options.ts index d0b0c60aa..ce925e3ba 100644 --- a/packages/spec-generators/src/visitorsCore/options.ts +++ b/packages/spec-generators/src/visitorsCore/options.ts @@ -60,6 +60,8 @@ export const MERGE_VISITOR_WALK_ORDER: ReadonlyMap = 'byteDeltas', 'discriminators', 'subInstructions', + 'provides', + 'display', ], ], ['arrayTypeNode', ['count', 'item']], @@ -97,6 +99,8 @@ export const IDENTITY_VISITOR_WALK_ORDER: ReadonlyMap 'extraArguments', 'remainingAccounts', 'subInstructions', + 'provides', + 'display', ], ], ]); diff --git a/packages/spec-generators/src/visitorsCore/walkStep.ts b/packages/spec-generators/src/visitorsCore/walkStep.ts index 35951beb3..af6aee7a9 100644 --- a/packages/spec-generators/src/visitorsCore/walkStep.ts +++ b/packages/spec-generators/src/visitorsCore/walkStep.ts @@ -13,6 +13,8 @@ import { type ResolvedRenderOptions } from './options'; * assert nested ``. * - `union` — `union('')`. Walk once, assert against the * resolved kind-list. + * - `anyNode` — `anyNode()`. Walk once, assert against the full + * registered-node kind-list. * - `arrayNode` — `array(node(''))`. Walk each, filter. * - `arrayUnion` — `array(union(''))`. Walk each, filter * against the resolved kind-list. @@ -22,6 +24,7 @@ import { type ResolvedRenderOptions } from './options'; * would need a new variant here. */ export type ChildShape = + | { readonly kind: 'anyNode' } | { readonly kind: 'arrayNode'; readonly nodeKind: string } | { readonly kind: 'arrayUnion'; readonly unionName: string } | { readonly kind: 'data' } @@ -32,6 +35,8 @@ export type ChildShape = /** Classify a {@link TypeExpr} into the matching {@link ChildShape}. */ export function getChildShape(typeExpr: TypeExpr): ChildShape { switch (typeExpr.kind) { + case 'anyNode': + return { kind: 'anyNode' }; case 'node': return { kind: 'node', nodeKind: typeExpr.name }; case 'nestedUnion': diff --git a/packages/spec-generators/test/nodeTypes/fragments/typeExpr.test.ts b/packages/spec-generators/test/nodeTypes/fragments/typeExpr.test.ts index ac39ec1c2..ae4c2b16e 100644 --- a/packages/spec-generators/test/nodeTypes/fragments/typeExpr.test.ts +++ b/packages/spec-generators/test/nodeTypes/fragments/typeExpr.test.ts @@ -1,5 +1,6 @@ import { address, + anyNode, array, boolean, codamaVersion, @@ -98,6 +99,12 @@ describe('getTypeExprFragment', () => { expect([...result.imports.keys()]).toEqual(['node:innerTypeNode']); }); + it('renders anyNode as Node imported from the registry', () => { + const result = getTypeExprFragment(anyNode()); + expect(result.content).toBe('Node'); + expect([...result.imports.keys()]).toEqual(['registry:Node']); + }); + it('renders union references with a union: import key', () => { const result = getTypeExprFragment(union('TypeNode')); expect(result.content).toBe('TypeNode'); diff --git a/packages/spec-generators/test/nodes/fragments/nodeTypeParameters.test.ts b/packages/spec-generators/test/nodes/fragments/nodeTypeParameters.test.ts index f9cddcc99..d16d9829e 100644 --- a/packages/spec-generators/test/nodes/fragments/nodeTypeParameters.test.ts +++ b/packages/spec-generators/test/nodes/fragments/nodeTypeParameters.test.ts @@ -44,17 +44,19 @@ describe('getNodeTypeParameterAttributes', () => { }); it('uses genericParamOrder overrides to reorder type-parameter attributes', () => { - // `instructionArgumentNode` surfaces both `type` (child) and - // `defaultValue` (child) as type parameters; the - // GENERIC_PARAM_ORDER table says `defaultValue` must come first. + // `instructionArgumentNode` surfaces `type` (child), `defaultValue` + // (child), and `display` (child) as type parameters; the + // GENERIC_PARAM_ORDER table pins the order to + // `[defaultValue, type, display]`. const n = defineNode('instructionArgumentNode', { attributes: [ attribute('type', union('TypeNode')), optionalAttribute('defaultValue', union('InstructionInputValueNode')), + optionalAttribute('display', node('StructFieldDisplayNode')), ], }); const attrs = getNodeTypeParameterAttributes(n, scope); - expect(attrs.map(a => a.name)).toEqual(['defaultValue', 'type']); + expect(attrs.map(a => a.name)).toEqual(['defaultValue', 'type', 'display']); }); it('throws when GENERIC_PARAM_ORDER references attributes the spec does not surface', () => { @@ -68,9 +70,9 @@ describe('getNodeTypeParameterAttributes', () => { }); it('throws when the spec surfaces a type-parameter attribute that GENERIC_PARAM_ORDER does not list', () => { - // `instructionArgumentNode`'s order is `[defaultValue, type]`. - // Synthesise a third type-parameter attribute the override - // doesn't know about; the strict check should reject it. + // `instructionArgumentNode`'s order is `[defaultValue, type, display]`. + // Synthesise an unrelated type-parameter attribute (`extra`) and + // omit `display`; the strict check should reject the mismatch. const n = defineNode('instructionArgumentNode', { attributes: [ attribute('type', union('TypeNode')), diff --git a/packages/spec-generators/test/nodes/fragments/typeExpr.test.ts b/packages/spec-generators/test/nodes/fragments/typeExpr.test.ts index 8cd58c6ee..ef04fddb3 100644 --- a/packages/spec-generators/test/nodes/fragments/typeExpr.test.ts +++ b/packages/spec-generators/test/nodes/fragments/typeExpr.test.ts @@ -1,5 +1,6 @@ import { address, + anyNode, array, boolean, codamaVersion, @@ -99,6 +100,12 @@ describe('getTypeExprFragment', () => { expect([...result.imports.keys()]).toEqual(['@codama/node-types']); }); + it('routes anyNode to the Node registry type imported from @codama/node-types', () => { + const result = getTypeExprFragment(anyNode()); + expect(result.content).toBe('Node'); + expect([...result.imports.keys()]).toEqual(['@codama/node-types']); + }); + it('routes union references to @codama/node-types under their PascalCase name', () => { const result = getTypeExprFragment(union('TypeNode')); expect(result.content).toBe('TypeNode'); diff --git a/packages/visitors-core/src/generated/identityVisitor.ts b/packages/visitors-core/src/generated/identityVisitor.ts index 499d94374..931a914d6 100644 --- a/packages/visitors-core/src/generated/identityVisitor.ts +++ b/packages/visitors-core/src/generated/identityVisitor.ts @@ -1,6 +1,7 @@ import { accountLinkNode, accountNode, + amountNumberDisplayNode, amountTypeNode, arrayTypeNode, arrayValueNode, @@ -18,6 +19,7 @@ import { definedTypeNode, DISCRIMINATOR_NODES, ENUM_VARIANT_TYPE_NODES, + enumEmptyVariantTypeNode, enumStructVariantTypeNode, enumTupleVariantTypeNode, enumTypeNode, @@ -26,6 +28,7 @@ import { fixedSizeTypeNode, hiddenPrefixTypeNode, hiddenSuffixTypeNode, + injectedValueNode, INSTRUCTION_INPUT_VALUE_NODES, instructionAccountLinkNode, instructionAccountNode, @@ -38,6 +41,7 @@ import { mapEntryValueNode, mapTypeNode, mapValueNode, + numberTypeNode, optionTypeNode, PDA_SEED_NODES, pdaLinkNode, @@ -48,6 +52,7 @@ import { prefixedCountNode, preOffsetTypeNode, programNode, + providedNode, REGISTERED_NODE_KINDS, remainderOptionTypeNode, removeNullAndAssertIsNodeFilter, @@ -59,6 +64,7 @@ import { sizePrefixTypeNode, solAmountTypeNode, someValueNode, + stringTypeNode, structFieldTypeNode, structFieldValueNode, structTypeNode, @@ -130,12 +136,22 @@ export function identityVisitor( }; } + if (keys.includes('enumEmptyVariantTypeNode')) { + visitor.visitEnumEmptyVariantType = function visitEnumEmptyVariantType(node) { + const display = node.display ? (visit(this)(node.display) ?? undefined) : undefined; + if (display) assertIsNode(display, 'enumVariantDisplayNode'); + return enumEmptyVariantTypeNode(node.name, node.discriminator, { ...node, display }); + }; + } + if (keys.includes('enumStructVariantTypeNode')) { visitor.visitEnumStructVariantType = function visitEnumStructVariantType(node) { const struct = visit(this)(node.struct); if (struct === null) return null; assertIsNestedTypeNode(struct, 'structTypeNode'); - return enumStructVariantTypeNode(node.name, struct, node.discriminator); + const display = node.display ? (visit(this)(node.display) ?? undefined) : undefined; + if (display) assertIsNode(display, 'enumVariantDisplayNode'); + return enumStructVariantTypeNode(node.name, struct, node.discriminator, { ...node, display }); }; } @@ -144,7 +160,9 @@ export function identityVisitor( const tuple = visit(this)(node.tuple); if (tuple === null) return null; assertIsNestedTypeNode(tuple, 'tupleTypeNode'); - return enumTupleVariantTypeNode(node.name, tuple, node.discriminator); + const display = node.display ? (visit(this)(node.display) ?? undefined) : undefined; + if (display) assertIsNode(display, 'enumVariantDisplayNode'); + return enumTupleVariantTypeNode(node.name, tuple, node.discriminator, { ...node, display }); }; } @@ -208,6 +226,19 @@ export function identityVisitor( }; } + if (keys.includes('numberTypeNode')) { + visitor.visitNumberType = function visitNumberType(node) { + const display = node.display ? (visit(this)(node.display) ?? undefined) : undefined; + if (display) + assertIsNode(display, [ + 'amountNumberDisplayNode', + 'dateTimeNumberDisplayNode', + 'durationNumberDisplayNode', + ]); + return numberTypeNode(node.format, node.endian, { ...node, display }); + }; + } + if (keys.includes('optionTypeNode')) { visitor.visitOptionType = function visitOptionType(node) { const prefix = visit(this)(node.prefix); @@ -292,6 +323,14 @@ export function identityVisitor( }; } + if (keys.includes('stringTypeNode')) { + visitor.visitStringType = function visitStringType(node) { + const display = node.display ? (visit(this)(node.display) ?? undefined) : undefined; + if (display) assertIsNode(display, 'stringDisplayNode'); + return stringTypeNode(node.encoding, { ...node, display }); + }; + } + if (keys.includes('structFieldTypeNode')) { visitor.visitStructFieldType = function visitStructFieldType(node) { const type = visit(this)(node.type); @@ -299,7 +338,9 @@ export function identityVisitor( assertIsNode(type, TYPE_NODES); const defaultValue = node.defaultValue ? (visit(this)(node.defaultValue) ?? undefined) : undefined; if (defaultValue) assertIsNode(defaultValue, VALUE_NODES); - return structFieldTypeNode({ ...node, type, defaultValue }); + const display = node.display ? (visit(this)(node.display) ?? undefined) : undefined; + if (display) assertIsNode(display, 'structFieldDisplayNode'); + return structFieldTypeNode({ ...node, type, defaultValue, display }); }; } @@ -361,6 +402,14 @@ export function identityVisitor( }; } + if (keys.includes('injectedValueNode')) { + visitor.visitInjectedValue = function visitInjectedValue(node) { + const fallback = node.fallback ? (visit(this)(node.fallback) ?? undefined) : undefined; + if (fallback) assertIsNode(fallback, VALUE_NODES); + return injectedValueNode({ ...node, fallback }); + }; + } + if (keys.includes('mapEntryValueNode')) { visitor.visitMapEntryValue = function visitMapEntryValue(node) { const key = visit(this)(node.key); @@ -510,6 +559,16 @@ export function identityVisitor( }; } + if (keys.includes('amountNumberDisplayNode')) { + visitor.visitAmountNumberDisplay = function visitAmountNumberDisplay(node) { + const decimals = node.decimals ? (visit(this)(node.decimals) ?? undefined) : undefined; + if (decimals) assertIsNode(decimals, ['numberValueNode', 'injectedValueNode']); + const unit = node.unit ? (visit(this)(node.unit) ?? undefined) : undefined; + if (unit) assertIsNode(unit, ['stringValueNode', 'injectedValueNode']); + return amountNumberDisplayNode({ ...node, decimals, unit }); + }; + } + if (keys.includes('conditionalValueNode')) { visitor.visitConditionalValue = function visitConditionalValue(node) { const condition = visit(this)(node.condition); @@ -620,7 +679,11 @@ export function identityVisitor( visitor.visitInstructionAccount = function visitInstructionAccount(node) { const defaultValue = node.defaultValue ? (visit(this)(node.defaultValue) ?? undefined) : undefined; if (defaultValue) assertIsNode(defaultValue, INSTRUCTION_INPUT_VALUE_NODES); - return instructionAccountNode({ ...node, defaultValue }); + const accountLink = node.accountLink ? (visit(this)(node.accountLink) ?? undefined) : undefined; + if (accountLink) assertIsNode(accountLink, 'accountLinkNode'); + const display = node.display ? (visit(this)(node.display) ?? undefined) : undefined; + if (display) assertIsNode(display, 'instructionAccountDisplayNode'); + return instructionAccountNode({ ...node, defaultValue, accountLink, display }); }; } @@ -631,7 +694,9 @@ export function identityVisitor( assertIsNode(type, TYPE_NODES); const defaultValue = node.defaultValue ? (visit(this)(node.defaultValue) ?? undefined) : undefined; if (defaultValue) assertIsNode(defaultValue, INSTRUCTION_INPUT_VALUE_NODES); - return instructionArgumentNode({ ...node, type, defaultValue }); + const display = node.display ? (visit(this)(node.display) ?? undefined) : undefined; + if (display) assertIsNode(display, 'structFieldDisplayNode'); + return instructionArgumentNode({ ...node, type, defaultValue, display }); }; } @@ -648,6 +713,8 @@ export function identityVisitor( visitor.visitInstruction = function visitInstruction(node) { const status = node.status ? (visit(this)(node.status) ?? undefined) : undefined; if (status) assertIsNode(status, 'instructionStatusNode'); + const display = node.display ? (visit(this)(node.display) ?? undefined) : undefined; + if (display) assertIsNode(display, 'instructionDisplayNode'); return instructionNode({ ...node, status, @@ -678,6 +745,10 @@ export function identityVisitor( subInstructions: node.subInstructions ? node.subInstructions.map(visit(this)).filter(removeNullAndAssertIsNodeFilter('instructionNode')) : undefined, + provides: node.provides + ? node.provides.map(visit(this)).filter(removeNullAndAssertIsNodeFilter('providedNode')) + : undefined, + display, }); }; } @@ -687,7 +758,9 @@ export function identityVisitor( const value = visit(this)(node.value); if (value === null) return null; assertIsNode(value, ['argumentValueNode', 'resolverValueNode']); - return instructionRemainingAccountsNode(value, { ...node }); + const display = node.display ? (visit(this)(node.display) ?? undefined) : undefined; + if (display) assertIsNode(display, 'instructionAccountDisplayNode'); + return instructionRemainingAccountsNode(value, { ...node, display }); }; } @@ -721,6 +794,15 @@ export function identityVisitor( }; } + if (keys.includes('providedNode')) { + visitor.visitProvided = function visitProvided(node) { + const value = visit(this)(node.node); + if (value === null) return null; + assertIsNode(value, REGISTERED_NODE_KINDS); + return providedNode(node.name, value); + }; + } + if (keys.includes('rootNode')) { visitor.visitRoot = function visitRoot(node) { const program = visit(this)(node.program); diff --git a/packages/visitors-core/src/generated/mergeVisitor.ts b/packages/visitors-core/src/generated/mergeVisitor.ts index 9e5e48db2..5f2a13f95 100644 --- a/packages/visitors-core/src/generated/mergeVisitor.ts +++ b/packages/visitors-core/src/generated/mergeVisitor.ts @@ -44,15 +44,21 @@ export function mergeVisitor( }; } + if (keys.includes('enumEmptyVariantTypeNode')) { + visitor.visitEnumEmptyVariantType = function visitEnumEmptyVariantType(node) { + return merge(node, node.display ? visit(this)(node.display) : []); + }; + } + if (keys.includes('enumStructVariantTypeNode')) { visitor.visitEnumStructVariantType = function visitEnumStructVariantType(node) { - return merge(node, visit(this)(node.struct)); + return merge(node, [...visit(this)(node.struct), ...(node.display ? visit(this)(node.display) : [])]); }; } if (keys.includes('enumTupleVariantTypeNode')) { visitor.visitEnumTupleVariantType = function visitEnumTupleVariantType(node) { - return merge(node, visit(this)(node.tuple)); + return merge(node, [...visit(this)(node.tuple), ...(node.display ? visit(this)(node.display) : [])]); }; } @@ -86,6 +92,12 @@ export function mergeVisitor( }; } + if (keys.includes('numberTypeNode')) { + visitor.visitNumberType = function visitNumberType(node) { + return merge(node, node.display ? visit(this)(node.display) : []); + }; + } + if (keys.includes('optionTypeNode')) { visitor.visitOptionType = function visitOptionType(node) { return merge(node, [...visit(this)(node.prefix), ...visit(this)(node.item)]); @@ -134,11 +146,18 @@ export function mergeVisitor( }; } + if (keys.includes('stringTypeNode')) { + visitor.visitStringType = function visitStringType(node) { + return merge(node, node.display ? visit(this)(node.display) : []); + }; + } + if (keys.includes('structFieldTypeNode')) { visitor.visitStructFieldType = function visitStructFieldType(node) { return merge(node, [ ...visit(this)(node.type), ...(node.defaultValue ? visit(this)(node.defaultValue) : []), + ...(node.display ? visit(this)(node.display) : []), ]); }; } @@ -179,6 +198,12 @@ export function mergeVisitor( }; } + if (keys.includes('injectedValueNode')) { + visitor.visitInjectedValue = function visitInjectedValue(node) { + return merge(node, node.fallback ? visit(this)(node.fallback) : []); + }; + } + if (keys.includes('mapEntryValueNode')) { visitor.visitMapEntryValue = function visitMapEntryValue(node) { return merge(node, [...visit(this)(node.key), ...visit(this)(node.value)]); @@ -281,6 +306,15 @@ export function mergeVisitor( }; } + if (keys.includes('amountNumberDisplayNode')) { + visitor.visitAmountNumberDisplay = function visitAmountNumberDisplay(node) { + return merge(node, [ + ...(node.decimals ? visit(this)(node.decimals) : []), + ...(node.unit ? visit(this)(node.unit) : []), + ]); + }; + } + if (keys.includes('conditionalValueNode')) { visitor.visitConditionalValue = function visitConditionalValue(node) { return merge(node, [ @@ -344,7 +378,11 @@ export function mergeVisitor( if (keys.includes('instructionAccountNode')) { visitor.visitInstructionAccount = function visitInstructionAccount(node) { - return merge(node, node.defaultValue ? visit(this)(node.defaultValue) : []); + return merge(node, [ + ...(node.defaultValue ? visit(this)(node.defaultValue) : []), + ...(node.accountLink ? visit(this)(node.accountLink) : []), + ...(node.display ? visit(this)(node.display) : []), + ]); }; } @@ -353,6 +391,7 @@ export function mergeVisitor( return merge(node, [ ...visit(this)(node.type), ...(node.defaultValue ? visit(this)(node.defaultValue) : []), + ...(node.display ? visit(this)(node.display) : []), ]); }; } @@ -374,13 +413,15 @@ export function mergeVisitor( ...(node.byteDeltas ?? []).flatMap(visit(this)), ...(node.discriminators ?? []).flatMap(visit(this)), ...(node.subInstructions ?? []).flatMap(visit(this)), + ...(node.provides ?? []).flatMap(visit(this)), + ...(node.display ? visit(this)(node.display) : []), ]); }; } if (keys.includes('instructionRemainingAccountsNode')) { visitor.visitInstructionRemainingAccounts = function visitInstructionRemainingAccounts(node) { - return merge(node, visit(this)(node.value)); + return merge(node, [...visit(this)(node.value), ...(node.display ? visit(this)(node.display) : [])]); }; } @@ -404,6 +445,12 @@ export function mergeVisitor( }; } + if (keys.includes('providedNode')) { + visitor.visitProvided = function visitProvided(node) { + return merge(node, visit(this)(node.node)); + }; + } + if (keys.includes('rootNode')) { visitor.visitRoot = function visitRoot(node) { return merge(node, [...visit(this)(node.program), ...(node.additionalPrograms ?? []).flatMap(visit(this))]); diff --git a/packages/visitors-core/src/generated/nodeTestPaths.ts b/packages/visitors-core/src/generated/nodeTestPaths.ts index 08493d86c..e899e18a5 100644 --- a/packages/visitors-core/src/generated/nodeTestPaths.ts +++ b/packages/visitors-core/src/generated/nodeTestPaths.ts @@ -7,9 +7,11 @@ import type { NodeKind } from '@codama/nodes'; */ export const NODE_TEST_PATHS: Readonly> = { accountBumpValueNode: 'contextualValueNodes/AccountBumpValueNode', + accountFieldValueNode: 'contextualValueNodes/AccountFieldValueNode', accountLinkNode: 'linkNodes/AccountLinkNode', accountNode: 'AccountNode', accountValueNode: 'contextualValueNodes/AccountValueNode', + amountNumberDisplayNode: 'displayNodes/AmountNumberDisplayNode', amountTypeNode: 'typeNodes/AmountTypeNode', argumentValueNode: 'contextualValueNodes/ArgumentValueNode', arrayTypeNode: 'typeNodes/ArrayTypeNode', @@ -23,14 +25,17 @@ export const NODE_TEST_PATHS: Readonly> = { constantNode: 'ConstantNode', constantPdaSeedNode: 'pdaSeedNodes/ConstantPdaSeedNode', constantValueNode: 'valueNodes/ConstantValueNode', + dateTimeNumberDisplayNode: 'displayNodes/DateTimeNumberDisplayNode', dateTimeTypeNode: 'typeNodes/DateTimeTypeNode', definedTypeLinkNode: 'linkNodes/DefinedTypeLinkNode', definedTypeNode: 'DefinedTypeNode', + durationNumberDisplayNode: 'displayNodes/DurationNumberDisplayNode', enumEmptyVariantTypeNode: 'typeNodes/EnumEmptyVariantTypeNode', enumStructVariantTypeNode: 'typeNodes/EnumStructVariantTypeNode', enumTupleVariantTypeNode: 'typeNodes/EnumTupleVariantTypeNode', enumTypeNode: 'typeNodes/EnumTypeNode', enumValueNode: 'valueNodes/EnumValueNode', + enumVariantDisplayNode: 'displayNodes/EnumVariantDisplayNode', errorNode: 'ErrorNode', eventNode: 'EventNode', fieldDiscriminatorNode: 'discriminatorNodes/FieldDiscriminatorNode', @@ -39,11 +44,14 @@ export const NODE_TEST_PATHS: Readonly> = { hiddenPrefixTypeNode: 'typeNodes/HiddenPrefixTypeNode', hiddenSuffixTypeNode: 'typeNodes/HiddenSuffixTypeNode', identityValueNode: 'contextualValueNodes/IdentityValueNode', + injectedValueNode: 'valueNodes/InjectedValueNode', + instructionAccountDisplayNode: 'displayNodes/InstructionAccountDisplayNode', instructionAccountLinkNode: 'linkNodes/InstructionAccountLinkNode', instructionAccountNode: 'InstructionAccountNode', instructionArgumentLinkNode: 'linkNodes/InstructionArgumentLinkNode', instructionArgumentNode: 'InstructionArgumentNode', instructionByteDeltaNode: 'InstructionByteDeltaNode', + instructionDisplayNode: 'displayNodes/InstructionDisplayNode', instructionLinkNode: 'linkNodes/InstructionLinkNode', instructionNode: 'InstructionNode', instructionRemainingAccountsNode: 'InstructionRemainingAccountsNode', @@ -66,6 +74,7 @@ export const NODE_TEST_PATHS: Readonly> = { programIdValueNode: 'contextualValueNodes/ProgramIdValueNode', programLinkNode: 'linkNodes/ProgramLinkNode', programNode: 'ProgramNode', + providedNode: 'ProvidedNode', publicKeyTypeNode: 'typeNodes/PublicKeyTypeNode', publicKeyValueNode: 'valueNodes/PublicKeyValueNode', remainderCountNode: 'countNodes/RemainderCountNode', @@ -79,8 +88,10 @@ export const NODE_TEST_PATHS: Readonly> = { sizePrefixTypeNode: 'typeNodes/SizePrefixTypeNode', solAmountTypeNode: 'typeNodes/SolAmountTypeNode', someValueNode: 'valueNodes/SomeValueNode', + stringDisplayNode: 'displayNodes/StringDisplayNode', stringTypeNode: 'typeNodes/StringTypeNode', stringValueNode: 'valueNodes/StringValueNode', + structFieldDisplayNode: 'displayNodes/StructFieldDisplayNode', structFieldTypeNode: 'typeNodes/StructFieldTypeNode', structFieldValueNode: 'valueNodes/StructFieldValueNode', structTypeNode: 'typeNodes/StructTypeNode', diff --git a/packages/visitors-core/src/getByteSizeVisitor.ts b/packages/visitors-core/src/getByteSizeVisitor.ts index c699922c0..37d6a412b 100644 --- a/packages/visitors-core/src/getByteSizeVisitor.ts +++ b/packages/visitors-core/src/getByteSizeVisitor.ts @@ -167,6 +167,12 @@ export function getByteSizeVisitor( return getArrayLikeSize(node.count, visit(node.item, self), self); }, + visitStringType() { + // Strings have no fixed byte size; size is determined by an enclosing + // wrapper such as `sizePrefixTypeNode` or `fixedSizeTypeNode`. + return null; + }, + visitZeroableOptionType(node, { self }) { const itemSize = visit(node.item, self); if (!node.zeroValue) return itemSize; diff --git a/packages/visitors-core/src/getDebugStringVisitor.ts b/packages/visitors-core/src/getDebugStringVisitor.ts index c8fbd86c9..9f869774d 100644 --- a/packages/visitors-core/src/getDebugStringVisitor.ts +++ b/packages/visitors-core/src/getDebugStringVisitor.ts @@ -27,7 +27,7 @@ export function getDebugStringVisitor(options: { indent?: boolean; indentSeparat ...values, ].join('\n'); } - return `${node.kind}${details ? `[${details}]` : ''}(${values.join(', ')})`; + return `${node.kind}${details ? `[${details}]` : ''}${values.length > 0 ? `(${values.join(', ')})` : ''}`; }, ), v => diff --git a/packages/visitors-core/src/getMaxByteSizeVisitor.ts b/packages/visitors-core/src/getMaxByteSizeVisitor.ts index 669d0cf8c..cbb0d1970 100644 --- a/packages/visitors-core/src/getMaxByteSizeVisitor.ts +++ b/packages/visitors-core/src/getMaxByteSizeVisitor.ts @@ -157,6 +157,12 @@ export function getMaxByteSizeVisitor( return getArrayLikeSize(node.count, visit(node.item, self), self); }, + visitStringType() { + // Strings have no fixed maximum byte size; size is determined by an + // enclosing wrapper such as `sizePrefixTypeNode` or `fixedSizeTypeNode`. + return null; + }, + visitZeroableOptionType(node, { self }) { const itemSize = visit(node.item, self); if (!node.zeroValue) return itemSize; diff --git a/packages/visitors-core/test/mapVisitor.test.ts b/packages/visitors-core/test/mapVisitor.test.ts index 707ef6d6c..dfa445a33 100644 --- a/packages/visitors-core/test/mapVisitor.test.ts +++ b/packages/visitors-core/test/mapVisitor.test.ts @@ -17,8 +17,8 @@ test('it maps the return value of a visitor to another', () => { const visitorB = mapVisitor(visitorA, value => value.length); // Then we expect the following results when visiting different nodes. - expect(visit(node, visitorB)).toBe(47); - expect(visit(node.items[0], visitorB)).toBe(14); + expect(visit(node, visitorB)).toBe(49); + expect(visit(node.items[0], visitorB)).toBe(16); expect(visit(node.items[1], visitorB)).toBe(17); }); diff --git a/packages/visitors-core/test/mergeVisitor.test.ts b/packages/visitors-core/test/mergeVisitor.test.ts index fbdb55c50..de698f3d2 100644 --- a/packages/visitors-core/test/mergeVisitor.test.ts +++ b/packages/visitors-core/test/mergeVisitor.test.ts @@ -18,7 +18,7 @@ test('it sets a value for all leaves and merges node values together', () => { const result = visit(node, visitor); // Then we get the following result. - expect(result).toBe('tupleTypeNode(numberTypeNode,publicKeyTypeNode)'); + expect(result).toBe('tupleTypeNode(numberTypeNode(),publicKeyTypeNode)'); }); test('it can be used to count nodes', () => { @@ -51,7 +51,7 @@ test('it can create partial visitors', () => { const result = visit(node, visitor); // Then the unsupported node is not included in the result. - expect(result).toBe('tupleTypeNode(numberTypeNode)'); + expect(result).toBe('tupleTypeNode(numberTypeNode())'); // And the unsupported node cannot be visited. // @ts-expect-error PublicKeyTypeNode is not supported. diff --git a/packages/visitors-core/test/nodes/ProvidedNode.test.ts b/packages/visitors-core/test/nodes/ProvidedNode.test.ts new file mode 100644 index 000000000..51e331c7a --- /dev/null +++ b/packages/visitors-core/test/nodes/ProvidedNode.test.ts @@ -0,0 +1,33 @@ +import { argumentValueNode, providedNode } from '@codama/nodes'; +import { test } from 'vitest'; + +import { + expectDebugStringVisitor, + expectDeleteNodesVisitor, + expectIdentityVisitor, + expectMergeVisitorCount, +} from './_setup'; + +const node = providedNode('decimals', argumentValueNode('decimals')); + +test('mergeVisitor', () => { + expectMergeVisitorCount(node, 2); +}); + +test('identityVisitor', () => { + expectIdentityVisitor(node); +}); + +test('deleteNodesVisitor', () => { + expectDeleteNodesVisitor(node, '[providedNode]', null); + expectDeleteNodesVisitor(node, '[argumentValueNode]', null); +}); + +test('debugStringVisitor', () => { + expectDebugStringVisitor( + node, + ` +providedNode [decimals] +| argumentValueNode [decimals]`, + ); +}); diff --git a/packages/visitors-core/test/nodes/contextualValueNodes/AccountFieldValueNode.test.ts b/packages/visitors-core/test/nodes/contextualValueNodes/AccountFieldValueNode.test.ts new file mode 100644 index 000000000..a2d477974 --- /dev/null +++ b/packages/visitors-core/test/nodes/contextualValueNodes/AccountFieldValueNode.test.ts @@ -0,0 +1,27 @@ +import { accountFieldValueNode } from '@codama/nodes'; +import { test } from 'vitest'; + +import { + expectDebugStringVisitor, + expectDeleteNodesVisitor, + expectIdentityVisitor, + expectMergeVisitorCount, +} from '../_setup'; + +const node = accountFieldValueNode({ account: 'mint', path: 'decimals' }); + +test('mergeVisitor', () => { + expectMergeVisitorCount(node, 1); +}); + +test('identityVisitor', () => { + expectIdentityVisitor(node); +}); + +test('deleteNodesVisitor', () => { + expectDeleteNodesVisitor(node, '[accountFieldValueNode]', null); +}); + +test('debugStringVisitor', () => { + expectDebugStringVisitor(node, `accountFieldValueNode`); +}); diff --git a/packages/visitors-core/test/nodes/displayNodes/AmountNumberDisplayNode.test.ts b/packages/visitors-core/test/nodes/displayNodes/AmountNumberDisplayNode.test.ts new file mode 100644 index 000000000..ee97512cd --- /dev/null +++ b/packages/visitors-core/test/nodes/displayNodes/AmountNumberDisplayNode.test.ts @@ -0,0 +1,42 @@ +import { amountNumberDisplayNode, injectedValueNode, numberValueNode } from '@codama/nodes'; +import { test } from 'vitest'; + +import { + expectDebugStringVisitor, + expectDeleteNodesVisitor, + expectIdentityVisitor, + expectMergeVisitorCount, +} from '../_setup'; + +const node = amountNumberDisplayNode({ + decimals: numberValueNode(6), + unit: injectedValueNode({ key: 'symbol' }), +}); + +test('mergeVisitor', () => { + expectMergeVisitorCount(node, 3); +}); + +test('identityVisitor', () => { + expectIdentityVisitor(node); +}); + +test('deleteNodesVisitor', () => { + expectDeleteNodesVisitor(node, '[amountNumberDisplayNode]', null); + expectDeleteNodesVisitor( + node, + '[numberValueNode]', + amountNumberDisplayNode({ unit: injectedValueNode({ key: 'symbol' }) }), + ); + expectDeleteNodesVisitor(node, '[injectedValueNode]', amountNumberDisplayNode({ decimals: numberValueNode(6) })); +}); + +test('debugStringVisitor', () => { + expectDebugStringVisitor( + node, + ` +amountNumberDisplayNode +| numberValueNode [6] +| injectedValueNode`, + ); +}); diff --git a/packages/visitors-core/test/nodes/displayNodes/DateTimeNumberDisplayNode.test.ts b/packages/visitors-core/test/nodes/displayNodes/DateTimeNumberDisplayNode.test.ts new file mode 100644 index 000000000..b070d9302 --- /dev/null +++ b/packages/visitors-core/test/nodes/displayNodes/DateTimeNumberDisplayNode.test.ts @@ -0,0 +1,27 @@ +import { dateTimeNumberDisplayNode } from '@codama/nodes'; +import { test } from 'vitest'; + +import { + expectDebugStringVisitor, + expectDeleteNodesVisitor, + expectIdentityVisitor, + expectMergeVisitorCount, +} from '../_setup'; + +const node = dateTimeNumberDisplayNode({ ticksPerSecond: 1000 }); + +test('mergeVisitor', () => { + expectMergeVisitorCount(node, 1); +}); + +test('identityVisitor', () => { + expectIdentityVisitor(node); +}); + +test('deleteNodesVisitor', () => { + expectDeleteNodesVisitor(node, '[dateTimeNumberDisplayNode]', null); +}); + +test('debugStringVisitor', () => { + expectDebugStringVisitor(node, `dateTimeNumberDisplayNode`); +}); diff --git a/packages/visitors-core/test/nodes/displayNodes/DurationNumberDisplayNode.test.ts b/packages/visitors-core/test/nodes/displayNodes/DurationNumberDisplayNode.test.ts new file mode 100644 index 000000000..160f2d599 --- /dev/null +++ b/packages/visitors-core/test/nodes/displayNodes/DurationNumberDisplayNode.test.ts @@ -0,0 +1,27 @@ +import { durationNumberDisplayNode } from '@codama/nodes'; +import { test } from 'vitest'; + +import { + expectDebugStringVisitor, + expectDeleteNodesVisitor, + expectIdentityVisitor, + expectMergeVisitorCount, +} from '../_setup'; + +const node = durationNumberDisplayNode({ ticksPerSecond: 1 }); + +test('mergeVisitor', () => { + expectMergeVisitorCount(node, 1); +}); + +test('identityVisitor', () => { + expectIdentityVisitor(node); +}); + +test('deleteNodesVisitor', () => { + expectDeleteNodesVisitor(node, '[durationNumberDisplayNode]', null); +}); + +test('debugStringVisitor', () => { + expectDebugStringVisitor(node, `durationNumberDisplayNode`); +}); diff --git a/packages/visitors-core/test/nodes/displayNodes/EnumVariantDisplayNode.test.ts b/packages/visitors-core/test/nodes/displayNodes/EnumVariantDisplayNode.test.ts new file mode 100644 index 000000000..0f644b9c5 --- /dev/null +++ b/packages/visitors-core/test/nodes/displayNodes/EnumVariantDisplayNode.test.ts @@ -0,0 +1,27 @@ +import { enumVariantDisplayNode } from '@codama/nodes'; +import { test } from 'vitest'; + +import { + expectDebugStringVisitor, + expectDeleteNodesVisitor, + expectIdentityVisitor, + expectMergeVisitorCount, +} from '../_setup'; + +const node = enumVariantDisplayNode({ label: 'Buy', skipInnerData: false }); + +test('mergeVisitor', () => { + expectMergeVisitorCount(node, 1); +}); + +test('identityVisitor', () => { + expectIdentityVisitor(node); +}); + +test('deleteNodesVisitor', () => { + expectDeleteNodesVisitor(node, '[enumVariantDisplayNode]', null); +}); + +test('debugStringVisitor', () => { + expectDebugStringVisitor(node, `enumVariantDisplayNode`); +}); diff --git a/packages/visitors-core/test/nodes/displayNodes/InstructionAccountDisplayNode.test.ts b/packages/visitors-core/test/nodes/displayNodes/InstructionAccountDisplayNode.test.ts new file mode 100644 index 000000000..b7ced5f7f --- /dev/null +++ b/packages/visitors-core/test/nodes/displayNodes/InstructionAccountDisplayNode.test.ts @@ -0,0 +1,27 @@ +import { instructionAccountDisplayNode } from '@codama/nodes'; +import { test } from 'vitest'; + +import { + expectDebugStringVisitor, + expectDeleteNodesVisitor, + expectIdentityVisitor, + expectMergeVisitorCount, +} from '../_setup'; + +const node = instructionAccountDisplayNode({ label: 'To', skip: 'never' }); + +test('mergeVisitor', () => { + expectMergeVisitorCount(node, 1); +}); + +test('identityVisitor', () => { + expectIdentityVisitor(node); +}); + +test('deleteNodesVisitor', () => { + expectDeleteNodesVisitor(node, '[instructionAccountDisplayNode]', null); +}); + +test('debugStringVisitor', () => { + expectDebugStringVisitor(node, `instructionAccountDisplayNode`); +}); diff --git a/packages/visitors-core/test/nodes/displayNodes/InstructionDisplayNode.test.ts b/packages/visitors-core/test/nodes/displayNodes/InstructionDisplayNode.test.ts new file mode 100644 index 000000000..efb469b19 --- /dev/null +++ b/packages/visitors-core/test/nodes/displayNodes/InstructionDisplayNode.test.ts @@ -0,0 +1,30 @@ +import { instructionDisplayNode } from '@codama/nodes'; +import { test } from 'vitest'; + +import { + expectDebugStringVisitor, + expectDeleteNodesVisitor, + expectIdentityVisitor, + expectMergeVisitorCount, +} from '../_setup'; + +const node = instructionDisplayNode({ + intent: 'Transfer', + interpolatedIntent: 'Transfer ${data.amount} to ${accounts.destination}', +}); + +test('mergeVisitor', () => { + expectMergeVisitorCount(node, 1); +}); + +test('identityVisitor', () => { + expectIdentityVisitor(node); +}); + +test('deleteNodesVisitor', () => { + expectDeleteNodesVisitor(node, '[instructionDisplayNode]', null); +}); + +test('debugStringVisitor', () => { + expectDebugStringVisitor(node, `instructionDisplayNode`); +}); diff --git a/packages/visitors-core/test/nodes/displayNodes/StringDisplayNode.test.ts b/packages/visitors-core/test/nodes/displayNodes/StringDisplayNode.test.ts new file mode 100644 index 000000000..15e66e534 --- /dev/null +++ b/packages/visitors-core/test/nodes/displayNodes/StringDisplayNode.test.ts @@ -0,0 +1,27 @@ +import { stringDisplayNode } from '@codama/nodes'; +import { test } from 'vitest'; + +import { + expectDebugStringVisitor, + expectDeleteNodesVisitor, + expectIdentityVisitor, + expectMergeVisitorCount, +} from '../_setup'; + +const node = stringDisplayNode({ sliceEnd: 4, sliceStart: 0 }); + +test('mergeVisitor', () => { + expectMergeVisitorCount(node, 1); +}); + +test('identityVisitor', () => { + expectIdentityVisitor(node); +}); + +test('deleteNodesVisitor', () => { + expectDeleteNodesVisitor(node, '[stringDisplayNode]', null); +}); + +test('debugStringVisitor', () => { + expectDebugStringVisitor(node, `stringDisplayNode`); +}); diff --git a/packages/visitors-core/test/nodes/displayNodes/StructFieldDisplayNode.test.ts b/packages/visitors-core/test/nodes/displayNodes/StructFieldDisplayNode.test.ts new file mode 100644 index 000000000..d165e614f --- /dev/null +++ b/packages/visitors-core/test/nodes/displayNodes/StructFieldDisplayNode.test.ts @@ -0,0 +1,27 @@ +import { structFieldDisplayNode } from '@codama/nodes'; +import { test } from 'vitest'; + +import { + expectDebugStringVisitor, + expectDeleteNodesVisitor, + expectIdentityVisitor, + expectMergeVisitorCount, +} from '../_setup'; + +const node = structFieldDisplayNode({ label: 'Amount', skip: 'never' }); + +test('mergeVisitor', () => { + expectMergeVisitorCount(node, 1); +}); + +test('identityVisitor', () => { + expectIdentityVisitor(node); +}); + +test('deleteNodesVisitor', () => { + expectDeleteNodesVisitor(node, '[structFieldDisplayNode]', null); +}); + +test('debugStringVisitor', () => { + expectDebugStringVisitor(node, `structFieldDisplayNode`); +}); diff --git a/packages/visitors-core/test/nodes/valueNodes/InjectedValueNode.test.ts b/packages/visitors-core/test/nodes/valueNodes/InjectedValueNode.test.ts new file mode 100644 index 000000000..09fbd2995 --- /dev/null +++ b/packages/visitors-core/test/nodes/valueNodes/InjectedValueNode.test.ts @@ -0,0 +1,27 @@ +import { injectedValueNode } from '@codama/nodes'; +import { test } from 'vitest'; + +import { + expectDebugStringVisitor, + expectDeleteNodesVisitor, + expectIdentityVisitor, + expectMergeVisitorCount, +} from '../_setup'; + +const node = injectedValueNode({ key: 'decimals' }); + +test('mergeVisitor', () => { + expectMergeVisitorCount(node, 1); +}); + +test('identityVisitor', () => { + expectIdentityVisitor(node); +}); + +test('deleteNodesVisitor', () => { + expectDeleteNodesVisitor(node, '[injectedValueNode]', null); +}); + +test('debugStringVisitor', () => { + expectDebugStringVisitor(node, `injectedValueNode`); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75f94525b..81ffe0c13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -322,8 +322,8 @@ importers: specifier: workspace:* version: link:../fragments '@codama/spec': - specifier: 1.6.0 - version: 1.6.0 + specifier: 1.7.0-rc.0 + version: 1.7.0-rc.0 devDependencies: '@types/node': specifier: ^25 @@ -613,8 +613,8 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@codama/spec@1.6.0': - resolution: {integrity: sha512-/lgvuwVUBc6wgBrdajDIV4Hazlx6BcuuqCkoTnm/SPUaDJKTXzuIBe/+Sd5Rd03TJxDORGW1ywZgju9Da4PPdg==} + '@codama/spec@1.7.0-rc.0': + resolution: {integrity: sha512-KT8KLoobMAUq7xybXoeSRgRwgGhdKdalnOhZVQehGu7yw4s2EcV5w+1P2E/9zLf+lihTDrYkcaltSDNd4HNJyA==} engines: {node: '>=22.12.0'} '@esbuild/aix-ppc64@0.27.0': @@ -4836,7 +4836,7 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 - '@codama/spec@1.6.0': {} + '@codama/spec@1.7.0-rc.0': {} '@esbuild/aix-ppc64@0.27.0': optional: true