From dd380a73163c46a437cc46f0b587dffff9bf5050 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Mon, 5 Oct 2026 12:22:53 +0100 Subject: [PATCH] Carry resolver docs and default value strategies in the official plugins --- .changeset/lazy-regions-turn.md | 14 ++++++++++++++ docs/PluginNode.md | 4 ++-- docs/typeNodes/StructFieldTypeNode.md | 16 ++++++++-------- spec.json | 6 +++--- src/spec/nodes/PluginNode.ts | 4 ++-- src/spec/nodes/typeNodes/StructFieldTypeNode.ts | 2 +- 6 files changed, 30 insertions(+), 16 deletions(-) create mode 100644 .changeset/lazy-regions-turn.md diff --git a/.changeset/lazy-regions-turn.md b/.changeset/lazy-regions-turn.md new file mode 100644 index 0000000..c994102 --- /dev/null +++ b/.changeset/lazy-regions-turn.md @@ -0,0 +1,14 @@ +--- +'@codama/spec': patch +--- + +Carry resolver docs and default value strategies in the official plugins. The `codama.resolver` payload gains an optional `docs` field describing the resolver, and the `codama.extraArgument` payload gains an optional `defaultValueStrategy`, as for struct fields. A struct field resolved by a `codama.resolver` plugin may now carry a `defaultValueStrategy`, which applies to the resolved value as it would to a `defaultValue`, e.g. `omitted` keeps the field out of generated inputs. + +```ts +structFieldTypeNode({ + identifier: 'tags', + type: integerTypeNode('u8'), + defaultValueStrategy: 'omitted', + plugins: [pluginNode('codama.resolver', { payload: { name: 'resolveTags', docs: 'Derives tags from the name.' } })], +}); +``` diff --git a/docs/PluginNode.md b/docs/PluginNode.md index 3ae82ee..bc686cd 100644 --- a/docs/PluginNode.md +++ b/docs/PluginNode.md @@ -6,8 +6,8 @@ Every node can carry plugins via the `plugins` base attribute. The `codama.*` namespace is reserved for official plugins, defined by this specification for information that is renderer-specific by nature: -- `codama.resolver` — on a node whose value renderers resolve with custom code rather than from the IDL, e.g. an instruction account, a struct field, remaining accounts or a byte delta. Its payload is `{ name, dependsOn? }`: the `name` of the resolver function, and the inputs it depends on as `accounts.` or `data.` strings, e.g. `["accounts.mint", "data.config.fee"]`, where `data.*` covers both the instruction data and its extra arguments. `dependsOn` is interpreted relative to the enclosing instruction, so it must be omitted on nodes outside an instruction. -- `codama.extraArgument` — on an instruction node, one per client input that is not serialised in the instruction data, e.g. to feed a resolver. Its payload is `{ identifier, type, defaultValue?, docs? }`, where `type` and `defaultValue` are the JSON of a type node and an instruction input value node — as for instruction account defaults. Its `identifier` must not collide with a top-level field of the instruction data. +- `codama.resolver` — on a node whose value renderers resolve with custom code rather than from the IDL, e.g. an instruction account, a struct field, remaining accounts or a byte delta. Its payload is `{ name, dependsOn?, docs? }`: the `name` of the resolver function, the inputs it depends on as `accounts.` or `data.` strings, e.g. `["accounts.mint", "data.config.fee"]`, where `data.*` covers both the instruction data and its extra arguments. `dependsOn` is interpreted relative to the enclosing instruction, so it must be omitted on nodes outside an instruction, and optional `docs` describing the resolver. On a struct field, the `defaultValueStrategy` of the field applies to the resolved value as it would to a `defaultValue`. +- `codama.extraArgument` — on an instruction node, one per client input that is not serialised in the instruction data, e.g. to feed a resolver. Its payload is `{ identifier, type, defaultValue?, defaultValueStrategy?, docs? }`, where `type` and `defaultValue` are the JSON of a type node and an instruction input value node — as for instruction account defaults — and `defaultValueStrategy` is a `defaultValueStrategy` value, as for struct fields. Its `identifier` must not collide with a top-level field of the instruction data. Like any payload, these are inert: renaming the nodes they reference does not update them. diff --git a/docs/typeNodes/StructFieldTypeNode.md b/docs/typeNodes/StructFieldTypeNode.md index 0235736..9f3390f 100644 --- a/docs/typeNodes/StructFieldTypeNode.md +++ b/docs/typeNodes/StructFieldTypeNode.md @@ -13,14 +13,14 @@ A named field within a struct type. ### Children -| Attribute | Type | Description | -| ---------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `defaultValueStrategy` | [`DefaultValueStrategy`](../sharedNodes/DefaultValueStrategy.md) _(optional)_ | How a configured default value is exposed in generated APIs. Only relevant when `defaultValue` is set — a strategy without a default value is meaningless. When absent, `optional` is assumed. | -| `docs` | `string` \| [`TextNode`](../TextNode.md) _(optional)_ | Markdown documentation for the field. | -| `type` | [`TypeNode`](./TypeNode.md) | The type of the field. | -| `defaultValue` | [`ValueNode`](../valueNodes/ValueNode.md) _(optional)_ | A default value used when the field is omitted by callers. | -| `display` | [`StructFieldDisplayNode`](../displayNodes/StructFieldDisplayNode.md) _(optional)_ | Display metadata describing how the field is presented. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. | +| Attribute | Type | Description | +| ---------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `defaultValueStrategy` | [`DefaultValueStrategy`](../sharedNodes/DefaultValueStrategy.md) _(optional)_ | How a configured default value is exposed in generated APIs. Only relevant when `defaultValue` is set or a `codama.resolver` plugin resolves the field — a strategy without a default value is meaningless. When absent, `optional` is assumed. | +| `docs` | `string` \| [`TextNode`](../TextNode.md) _(optional)_ | Markdown documentation for the field. | +| `type` | [`TypeNode`](./TypeNode.md) | The type of the field. | +| `defaultValue` | [`ValueNode`](../valueNodes/ValueNode.md) _(optional)_ | A default value used when the field is omitted by callers. | +| `display` | [`StructFieldDisplayNode`](../displayNodes/StructFieldDisplayNode.md) _(optional)_ | Display metadata describing how the field is presented. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. | ## Examples diff --git a/spec.json b/spec.json index e5de3e8..775fa3b 100644 --- a/spec.json +++ b/spec.json @@ -1252,7 +1252,7 @@ "optional": true, "docs": [ "How a configured default value is exposed in generated APIs.", - "Only relevant when `defaultValue` is set — a strategy without a default value is meaningless. When absent, `optional` is assumed." + "Only relevant when `defaultValue` is set or a `codama.resolver` plugin resolves the field — a strategy without a default value is meaningless. When absent, `optional` is assumed." ] }, { @@ -7177,8 +7177,8 @@ "", "The `codama.*` namespace is reserved for official plugins, defined by this specification for information that is renderer-specific by nature:", "", - "- `codama.resolver` — on a node whose value renderers resolve with custom code rather than from the IDL, e.g. an instruction account, a struct field, remaining accounts or a byte delta. Its payload is `{ name, dependsOn? }`: the `name` of the resolver function, and the inputs it depends on as `accounts.` or `data.` strings, e.g. `[\"accounts.mint\", \"data.config.fee\"]`, where `data.*` covers both the instruction data and its extra arguments. `dependsOn` is interpreted relative to the enclosing instruction, so it must be omitted on nodes outside an instruction.", - "- `codama.extraArgument` — on an instruction node, one per client input that is not serialised in the instruction data, e.g. to feed a resolver. Its payload is `{ identifier, type, defaultValue?, docs? }`, where `type` and `defaultValue` are the JSON of a type node and an instruction input value node — as for instruction account defaults. Its `identifier` must not collide with a top-level field of the instruction data.", + "- `codama.resolver` — on a node whose value renderers resolve with custom code rather than from the IDL, e.g. an instruction account, a struct field, remaining accounts or a byte delta. Its payload is `{ name, dependsOn?, docs? }`: the `name` of the resolver function, the inputs it depends on as `accounts.` or `data.` strings, e.g. `[\"accounts.mint\", \"data.config.fee\"]`, where `data.*` covers both the instruction data and its extra arguments. `dependsOn` is interpreted relative to the enclosing instruction, so it must be omitted on nodes outside an instruction, and optional `docs` describing the resolver. On a struct field, the `defaultValueStrategy` of the field applies to the resolved value as it would to a `defaultValue`.", + "- `codama.extraArgument` — on an instruction node, one per client input that is not serialised in the instruction data, e.g. to feed a resolver. Its payload is `{ identifier, type, defaultValue?, defaultValueStrategy?, docs? }`, where `type` and `defaultValue` are the JSON of a type node and an instruction input value node — as for instruction account defaults — and `defaultValueStrategy` is a `defaultValueStrategy` value, as for struct fields. Its `identifier` must not collide with a top-level field of the instruction data.", "", "Like any payload, these are inert: renaming the nodes they reference does not update them." ], diff --git a/src/spec/nodes/PluginNode.ts b/src/spec/nodes/PluginNode.ts index c0960aa..bc53744 100644 --- a/src/spec/nodes/PluginNode.ts +++ b/src/spec/nodes/PluginNode.ts @@ -9,8 +9,8 @@ export const pluginNode = defineNode('pluginNode', { '', 'The `codama.*` namespace is reserved for official plugins, defined by this specification for information that is renderer-specific by nature:', '', - '- `codama.resolver` — on a node whose value renderers resolve with custom code rather than from the IDL, e.g. an instruction account, a struct field, remaining accounts or a byte delta. Its payload is `{ name, dependsOn? }`: the `name` of the resolver function, and the inputs it depends on as `accounts.` or `data.` strings, e.g. `["accounts.mint", "data.config.fee"]`, where `data.*` covers both the instruction data and its extra arguments. `dependsOn` is interpreted relative to the enclosing instruction, so it must be omitted on nodes outside an instruction.', - '- `codama.extraArgument` — on an instruction node, one per client input that is not serialised in the instruction data, e.g. to feed a resolver. Its payload is `{ identifier, type, defaultValue?, docs? }`, where `type` and `defaultValue` are the JSON of a type node and an instruction input value node — as for instruction account defaults. Its `identifier` must not collide with a top-level field of the instruction data.', + '- `codama.resolver` — on a node whose value renderers resolve with custom code rather than from the IDL, e.g. an instruction account, a struct field, remaining accounts or a byte delta. Its payload is `{ name, dependsOn?, docs? }`: the `name` of the resolver function, the inputs it depends on as `accounts.` or `data.` strings, e.g. `["accounts.mint", "data.config.fee"]`, where `data.*` covers both the instruction data and its extra arguments. `dependsOn` is interpreted relative to the enclosing instruction, so it must be omitted on nodes outside an instruction, and optional `docs` describing the resolver. On a struct field, the `defaultValueStrategy` of the field applies to the resolved value as it would to a `defaultValue`.', + '- `codama.extraArgument` — on an instruction node, one per client input that is not serialised in the instruction data, e.g. to feed a resolver. Its payload is `{ identifier, type, defaultValue?, defaultValueStrategy?, docs? }`, where `type` and `defaultValue` are the JSON of a type node and an instruction input value node — as for instruction account defaults — and `defaultValueStrategy` is a `defaultValueStrategy` value, as for struct fields. Its `identifier` must not collide with a top-level field of the instruction data.', '', 'Like any payload, these are inert: renaming the nodes they reference does not update them.', ], diff --git a/src/spec/nodes/typeNodes/StructFieldTypeNode.ts b/src/spec/nodes/typeNodes/StructFieldTypeNode.ts index 161a81c..75999f5 100644 --- a/src/spec/nodes/typeNodes/StructFieldTypeNode.ts +++ b/src/spec/nodes/typeNodes/StructFieldTypeNode.ts @@ -19,7 +19,7 @@ export const structFieldTypeNode = defineNode('structFieldTypeNode', { optionalAttribute('defaultValueStrategy', enumeration('defaultValueStrategy'), { docs: [ 'How a configured default value is exposed in generated APIs.', - 'Only relevant when `defaultValue` is set — a strategy without a default value is meaningless. When absent, `optional` is assumed.', + 'Only relevant when `defaultValue` is set or a `codama.resolver` plugin resolves the field — a strategy without a default value is meaningless. When absent, `optional` is assumed.', ], }), optionalAttribute('docs', docs(), {