|
| 1 | +import { DefinedTypeNode, InstructionNode, isNode, PathString, StructFieldTypeNode, TypeNode } from '@codama/nodes'; |
| 2 | + |
| 3 | +import { LinkableDictionary } from './LinkableDictionary'; |
| 4 | +import { getLastNodeFromPath, NodePath } from './NodePath'; |
| 5 | +import { NodeStack } from './NodeStack'; |
| 6 | + |
| 7 | +/** A struct field within an instruction's data, with its full path. */ |
| 8 | +export type InstructionDataField = { |
| 9 | + field: StructFieldTypeNode; |
| 10 | + /** The field's full path within `instructionNode.data`, e.g. `config.bump`. */ |
| 11 | + path: PathString; |
| 12 | +}; |
| 13 | + |
| 14 | +/** |
| 15 | + * List every struct field addressable within an instruction's `data`, with |
| 16 | + * its full path, following `definedTypeLinkNode`s. |
| 17 | + * |
| 18 | + * Fields are only addressable where the data type resolves to a struct, so |
| 19 | + * nested fields are listed for struct-typed (or struct-linked) fields only. |
| 20 | + * Each defined type is followed at most once, which guards against cycles. |
| 21 | + * |
| 22 | + * @param instructionPath - The path to the instruction. It must contain the |
| 23 | + * instruction's `programNode` for linked data to be followed; otherwise a |
| 24 | + * linked type contributes no fields. |
| 25 | + * @param linkables - Used to follow `definedTypeLinkNode`s. |
| 26 | + * |
| 27 | + * @example |
| 28 | + * ```ts |
| 29 | + * // data: struct { amount: u64, config: struct { fee: u16 } } |
| 30 | + * getInstructionDataFields(instructionPath, linkables).map(({ path }) => path); |
| 31 | + * // ['amount', 'config', 'config.fee'] |
| 32 | + * ``` |
| 33 | + */ |
| 34 | +export function getInstructionDataFields( |
| 35 | + instructionPath: NodePath<InstructionNode>, |
| 36 | + linkables: LinkableDictionary, |
| 37 | +): InstructionDataField[] { |
| 38 | + const fields: InstructionDataField[] = []; |
| 39 | + const stack = new NodeStack(instructionPath); |
| 40 | + // Keyed by node rather than identifier: same-named types of different programs are distinct. |
| 41 | + const walkedDefinedTypes = new Set<DefinedTypeNode>(); |
| 42 | + |
| 43 | + const walk = (type: TypeNode | undefined, prefix: string): void => { |
| 44 | + if (!type) return; |
| 45 | + if (isNode(type, 'definedTypeLinkNode')) { |
| 46 | + const linkedPath = linkables.getPath([...stack.getPath(), type]); |
| 47 | + if (!linkedPath) return; |
| 48 | + const definedType = getLastNodeFromPath(linkedPath); |
| 49 | + if (walkedDefinedTypes.has(definedType)) return; |
| 50 | + walkedDefinedTypes.add(definedType); |
| 51 | + stack.pushPath(linkedPath); |
| 52 | + walk(definedType.type, prefix); |
| 53 | + stack.popPath(); |
| 54 | + return; |
| 55 | + } |
| 56 | + if (!isNode(type, 'structTypeNode')) return; |
| 57 | + (type.fields ?? []).forEach(field => { |
| 58 | + const path = (prefix ? `${prefix}.${field.identifier}` : field.identifier) as PathString; |
| 59 | + fields.push({ field, path }); |
| 60 | + if (isNode(field.type, ['structTypeNode', 'definedTypeLinkNode'])) { |
| 61 | + walk(field.type, path); |
| 62 | + } |
| 63 | + }); |
| 64 | + }; |
| 65 | + |
| 66 | + walk(getLastNodeFromPath(instructionPath).data, ''); |
| 67 | + return fields; |
| 68 | +} |
0 commit comments