@codama/renderers-rust
Version:
Renders Rust clients for your programs
1 lines • 83.6 kB
Source Map (JSON)
{"version":3,"sources":["../src/ImportMap.ts","../src/getRenderMapVisitor.ts","../src/getTypeManifestVisitor.ts","../src/utils/codecs.ts","../src/utils/linkOverrides.ts","../src/utils/render.ts","../src/utils/traitOptions.ts","../src/renderValueNodeVisitor.ts","../src/renderVisitor.ts"],"sourcesContent":["import { TypeManifest } from './getTypeManifestVisitor';\n\nconst DEFAULT_MODULE_MAP: Record<string, string> = {\n generated: 'crate::generated',\n generatedAccounts: 'crate::generated::accounts',\n generatedErrors: 'crate::generated::errors',\n generatedInstructions: 'crate::generated::instructions',\n generatedTypes: 'crate::generated::types',\n hooked: 'crate::hooked',\n mplEssentials: 'mpl_toolbox',\n mplToolbox: 'mpl_toolbox',\n};\n\nexport class ImportMap {\n protected readonly _imports: Set<string> = new Set();\n\n protected readonly _aliases: Map<string, string> = new Map();\n\n get imports(): Set<string> {\n return this._imports;\n }\n\n get aliases(): Map<string, string> {\n return this._aliases;\n }\n\n add(imports: Set<string> | string[] | string): ImportMap {\n const newImports = typeof imports === 'string' ? [imports] : imports;\n newImports.forEach(i => this._imports.add(i));\n return this;\n }\n\n remove(imports: Set<string> | string[] | string): ImportMap {\n const importsToRemove = typeof imports === 'string' ? [imports] : imports;\n importsToRemove.forEach(i => this._imports.delete(i));\n return this;\n }\n\n mergeWith(...others: ImportMap[]): ImportMap {\n others.forEach(other => {\n this.add(other._imports);\n other._aliases.forEach((alias, importName) => this.addAlias(importName, alias));\n });\n return this;\n }\n\n mergeWithManifest(manifest: TypeManifest): ImportMap {\n return this.mergeWith(manifest.imports);\n }\n\n addAlias(importName: string, alias: string): ImportMap {\n this._aliases.set(importName, alias);\n return this;\n }\n\n isEmpty(): boolean {\n return this._imports.size === 0;\n }\n\n resolveDependencyMap(dependencies: Record<string, string>): ImportMap {\n const dependencyMap = { ...DEFAULT_MODULE_MAP, ...dependencies };\n const newImportMap = new ImportMap();\n const resolveDependency = (i: string): string => {\n const dependencyKey = Object.keys(dependencyMap).find(key => i.startsWith(`${key}::`));\n if (!dependencyKey) return i;\n const dependencyValue = dependencyMap[dependencyKey];\n return dependencyValue + i.slice(dependencyKey.length);\n };\n this._imports.forEach(i => newImportMap.add(resolveDependency(i)));\n this._aliases.forEach((alias, i) => newImportMap.addAlias(resolveDependency(i), alias));\n return newImportMap;\n }\n\n toString(dependencies: Record<string, string>): string {\n const resolvedMap = this.resolveDependencyMap(dependencies);\n const importStatements = [...resolvedMap.imports].map(i => {\n const alias = resolvedMap.aliases.get(i);\n if (alias) return `use ${i} as ${alias};`;\n return `use ${i};`;\n });\n return importStatements.join('\\n');\n }\n}\n","import { logWarn } from '@codama/errors';\nimport {\n getAllAccounts,\n getAllDefinedTypes,\n getAllInstructionsWithSubs,\n getAllPrograms,\n InstructionNode,\n isNode,\n isNodeFilter,\n pascalCase,\n ProgramNode,\n resolveNestedTypeNode,\n snakeCase,\n structTypeNodeFromInstructionArgumentNodes,\n VALUE_NODES,\n} from '@codama/nodes';\nimport { RenderMap } from '@codama/renderers-core';\nimport {\n extendVisitor,\n LinkableDictionary,\n NodeStack,\n pipe,\n recordLinkablesOnFirstVisitVisitor,\n recordNodeStackVisitor,\n staticVisitor,\n visit,\n} from '@codama/visitors-core';\n\nimport { getTypeManifestVisitor } from './getTypeManifestVisitor';\nimport { ImportMap } from './ImportMap';\nimport { renderValueNode } from './renderValueNodeVisitor';\nimport { getImportFromFactory, getTraitsFromNodeFactory, LinkOverrides, render, TraitOptions } from './utils';\n\nexport type GetRenderMapOptions = {\n anchorTraits?: boolean;\n defaultTraitOverrides?: string[];\n dependencyMap?: Record<string, string>;\n linkOverrides?: LinkOverrides;\n renderParentInstructions?: boolean;\n traitOptions?: TraitOptions;\n};\n\nexport function getRenderMapVisitor(options: GetRenderMapOptions = {}) {\n const linkables = new LinkableDictionary();\n const stack = new NodeStack();\n let program: ProgramNode | null = null;\n\n const renderParentInstructions = options.renderParentInstructions ?? false;\n const dependencyMap = options.dependencyMap ?? {};\n const getImportFrom = getImportFromFactory(options.linkOverrides ?? {});\n const getTraitsFromNode = getTraitsFromNodeFactory(options.traitOptions);\n const typeManifestVisitor = getTypeManifestVisitor({ getImportFrom, getTraitsFromNode });\n const anchorTraits = options.anchorTraits ?? true;\n\n return pipe(\n staticVisitor(() => new RenderMap(), {\n keys: ['rootNode', 'programNode', 'instructionNode', 'accountNode', 'definedTypeNode'],\n }),\n v =>\n extendVisitor(v, {\n visitAccount(node) {\n const typeManifest = visit(node, typeManifestVisitor);\n\n // Seeds.\n const seedsImports = new ImportMap();\n const pda = node.pda ? linkables.get([...stack.getPath(), node.pda]) : undefined;\n const pdaSeeds = pda?.seeds ?? [];\n const seeds = pdaSeeds.map(seed => {\n if (isNode(seed, 'variablePdaSeedNode')) {\n const seedManifest = visit(seed.type, typeManifestVisitor);\n seedsImports.mergeWith(seedManifest.imports);\n const resolvedType = resolveNestedTypeNode(seed.type);\n return { ...seed, resolvedType, typeManifest: seedManifest };\n }\n if (isNode(seed.value, 'programIdValueNode')) {\n return seed;\n }\n const seedManifest = visit(seed.type, typeManifestVisitor);\n const valueManifest = renderValueNode(seed.value, getImportFrom, true);\n seedsImports.mergeWith(valueManifest.imports);\n const resolvedType = resolveNestedTypeNode(seed.type);\n return { ...seed, resolvedType, typeManifest: seedManifest, valueManifest };\n });\n const hasVariableSeeds = pdaSeeds.filter(isNodeFilter('variablePdaSeedNode')).length > 0;\n const constantSeeds = seeds\n .filter(isNodeFilter('constantPdaSeedNode'))\n .filter(seed => !isNode(seed.value, 'programIdValueNode'));\n\n const { imports } = typeManifest;\n\n if (hasVariableSeeds) {\n imports.mergeWith(seedsImports);\n }\n\n return new RenderMap().add(\n `accounts/${snakeCase(node.name)}.rs`,\n render('accountsPage.njk', {\n account: node,\n anchorTraits,\n constantSeeds,\n hasVariableSeeds,\n imports: imports\n .remove(`generatedAccounts::${pascalCase(node.name)}`)\n .toString(dependencyMap),\n pda,\n program,\n seeds,\n typeManifest,\n }),\n );\n },\n\n visitDefinedType(node) {\n const typeManifest = visit(node, typeManifestVisitor);\n const imports = new ImportMap().mergeWithManifest(typeManifest);\n\n return new RenderMap().add(\n `types/${snakeCase(node.name)}.rs`,\n render('definedTypesPage.njk', {\n definedType: node,\n imports: imports.remove(`generatedTypes::${pascalCase(node.name)}`).toString(dependencyMap),\n typeManifest,\n }),\n );\n },\n\n visitInstruction(node) {\n // Imports.\n const imports = new ImportMap();\n\n // canMergeAccountsAndArgs\n const accountsAndArgsConflicts = getConflictsForInstructionAccountsAndArgs(node);\n if (accountsAndArgsConflicts.length > 0) {\n logWarn(\n `[Rust] Accounts and args of instruction [${node.name}] have the following ` +\n `conflicting attributes [${accountsAndArgsConflicts.join(', ')}]. ` +\n `Thus, the conflicting arguments will be suffixed with \"_arg\". ` +\n 'You may want to rename the conflicting attributes.',\n );\n }\n\n // Instruction args.\n const instructionArgs: {\n default: boolean;\n innerOptionType: string | null;\n name: string;\n optional: boolean;\n type: string;\n value: string | null;\n }[] = [];\n let hasArgs = false;\n let hasOptional = false;\n\n node.arguments.forEach(argument => {\n const argumentVisitor = getTypeManifestVisitor({\n getImportFrom,\n getTraitsFromNode,\n nestedStruct: true,\n parentName: `${pascalCase(node.name)}InstructionData`,\n });\n const manifest = visit(argument.type, argumentVisitor);\n imports.mergeWith(manifest.imports);\n const innerOptionType = isNode(argument.type, 'optionTypeNode')\n ? manifest.type.slice('Option<'.length, -1)\n : null;\n\n const hasDefaultValue = !!argument.defaultValue && isNode(argument.defaultValue, VALUE_NODES);\n let renderValue: string | null = null;\n if (hasDefaultValue) {\n const { imports: argImports, render: value } = renderValueNode(\n argument.defaultValue,\n getImportFrom,\n );\n imports.mergeWith(argImports);\n renderValue = value;\n }\n\n hasArgs = hasArgs || argument.defaultValueStrategy !== 'omitted';\n hasOptional = hasOptional || (hasDefaultValue && argument.defaultValueStrategy !== 'omitted');\n\n const name = accountsAndArgsConflicts.includes(argument.name)\n ? `${argument.name}_arg`\n : argument.name;\n\n instructionArgs.push({\n default: hasDefaultValue && argument.defaultValueStrategy === 'omitted',\n innerOptionType,\n name,\n optional: hasDefaultValue && argument.defaultValueStrategy !== 'omitted',\n type: manifest.type,\n value: renderValue,\n });\n });\n\n const struct = structTypeNodeFromInstructionArgumentNodes(node.arguments);\n const structVisitor = getTypeManifestVisitor({\n getImportFrom,\n getTraitsFromNode,\n parentName: `${pascalCase(node.name)}InstructionData`,\n });\n const typeManifest = visit(struct, structVisitor);\n\n const dataTraits = getTraitsFromNode(node);\n imports.mergeWith(dataTraits.imports);\n\n return new RenderMap().add(\n `instructions/${snakeCase(node.name)}.rs`,\n render('instructionsPage.njk', {\n dataTraits: dataTraits.render,\n hasArgs,\n hasOptional,\n imports: imports\n .remove(`generatedInstructions::${pascalCase(node.name)}`)\n .toString(dependencyMap),\n instruction: node,\n instructionArgs,\n program,\n typeManifest,\n }),\n );\n },\n\n visitProgram(node, { self }) {\n program = node;\n const renderMap = new RenderMap()\n .mergeWith(...node.accounts.map(account => visit(account, self)))\n .mergeWith(...node.definedTypes.map(type => visit(type, self)))\n .mergeWith(\n ...getAllInstructionsWithSubs(node, {\n leavesOnly: !renderParentInstructions,\n }).map(ix => visit(ix, self)),\n );\n\n // Errors.\n if (node.errors.length > 0) {\n renderMap.add(\n `errors/${snakeCase(node.name)}.rs`,\n render('errorsPage.njk', {\n errors: node.errors,\n imports: new ImportMap().toString(dependencyMap),\n program: node,\n }),\n );\n }\n\n program = null;\n return renderMap;\n },\n\n visitRoot(node, { self }) {\n const programsToExport = getAllPrograms(node);\n const accountsToExport = getAllAccounts(node);\n const instructionsToExport = getAllInstructionsWithSubs(node, {\n leavesOnly: !renderParentInstructions,\n });\n const definedTypesToExport = getAllDefinedTypes(node);\n const hasAnythingToExport =\n programsToExport.length > 0 ||\n accountsToExport.length > 0 ||\n instructionsToExport.length > 0 ||\n definedTypesToExport.length > 0;\n\n const ctx = {\n accountsToExport,\n definedTypesToExport,\n hasAnythingToExport,\n instructionsToExport,\n programsToExport,\n root: node,\n };\n\n const map = new RenderMap();\n if (accountsToExport.length > 0) {\n map.add('shared.rs', render('sharedPage.njk', ctx));\n }\n if (programsToExport.length > 0) {\n map.add('programs.rs', render('programsMod.njk', ctx)).add(\n 'errors/mod.rs',\n render('errorsMod.njk', ctx),\n );\n }\n if (accountsToExport.length > 0) {\n map.add('accounts/mod.rs', render('accountsMod.njk', ctx));\n }\n if (instructionsToExport.length > 0) {\n map.add('instructions/mod.rs', render('instructionsMod.njk', ctx));\n }\n if (definedTypesToExport.length > 0) {\n map.add('types/mod.rs', render('definedTypesMod.njk', ctx));\n }\n\n return map\n .add('mod.rs', render('rootMod.njk', ctx))\n .mergeWith(...getAllPrograms(node).map(p => visit(p, self)));\n },\n }),\n v => recordNodeStackVisitor(v, stack),\n v => recordLinkablesOnFirstVisitVisitor(v, linkables),\n );\n}\n\nfunction getConflictsForInstructionAccountsAndArgs(instruction: InstructionNode): string[] {\n const allNames = [\n ...instruction.accounts.map(account => account.name),\n ...instruction.arguments.map(argument => argument.name),\n ];\n const duplicates = allNames.filter((e, i, a) => a.indexOf(e) !== i);\n return [...new Set(duplicates)];\n}\n","import { CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE, CodamaError } from '@codama/errors';\nimport {\n arrayTypeNode,\n CountNode,\n definedTypeNode,\n fixedCountNode,\n isNode,\n NumberTypeNode,\n numberTypeNode,\n parseDocs,\n pascalCase,\n prefixedCountNode,\n REGISTERED_TYPE_NODE_KINDS,\n remainderCountNode,\n resolveNestedTypeNode,\n snakeCase,\n} from '@codama/nodes';\nimport { extendVisitor, mergeVisitor, pipe, visit } from '@codama/visitors-core';\n\nimport { ImportMap } from './ImportMap';\nimport { GetImportFromFunction, GetTraitsFromNodeFunction, rustDocblock } from './utils';\n\nexport type TypeManifest = {\n imports: ImportMap;\n nestedStructs: string[];\n type: string;\n};\n\nexport function getTypeManifestVisitor(options: {\n getImportFrom: GetImportFromFunction;\n getTraitsFromNode: GetTraitsFromNodeFunction;\n nestedStruct?: boolean;\n parentName?: string | null;\n}) {\n const { getImportFrom, getTraitsFromNode } = options;\n let parentName: string | null = options.parentName ?? null;\n let nestedStruct: boolean = options.nestedStruct ?? false;\n let inlineStruct: boolean = false;\n let parentSize: NumberTypeNode | number | null = null;\n\n return pipe(\n mergeVisitor(\n (): TypeManifest => ({ imports: new ImportMap(), nestedStructs: [], type: '' }),\n (_, values) => ({\n ...mergeManifests(values),\n type: values.map(v => v.type).join('\\n'),\n }),\n { keys: [...REGISTERED_TYPE_NODE_KINDS, 'definedTypeLinkNode', 'definedTypeNode', 'accountNode'] },\n ),\n v =>\n extendVisitor(v, {\n visitAccount(account, { self }) {\n parentName = pascalCase(account.name);\n const manifest = visit(account.data, self);\n const traits = getTraitsFromNode(account);\n manifest.imports.mergeWith(traits.imports);\n parentName = null;\n return {\n ...manifest,\n type: traits.render + manifest.type,\n };\n },\n\n visitArrayType(arrayType, { self }) {\n const childManifest = visit(arrayType.item, self);\n\n if (isNode(arrayType.count, 'fixedCountNode')) {\n return {\n ...childManifest,\n type: `[${childManifest.type}; ${arrayType.count.value}]`,\n };\n }\n\n if (isNode(arrayType.count, 'remainderCountNode')) {\n childManifest.imports.add('kaigan::types::RemainderVec');\n return {\n ...childManifest,\n type: `RemainderVec<${childManifest.type}>`,\n };\n }\n\n const prefix = resolveNestedTypeNode(arrayType.count.prefix);\n if (prefix.endian === 'le') {\n switch (prefix.format) {\n case 'u32':\n return {\n ...childManifest,\n type: `Vec<${childManifest.type}>`,\n };\n case 'u8':\n case 'u16':\n case 'u64': {\n const prefixFormat = prefix.format.toUpperCase();\n childManifest.imports.add(`kaigan::types::${prefixFormat}PrefixVec`);\n return {\n ...childManifest,\n type: `${prefixFormat}PrefixVec<${childManifest.type}>`,\n };\n }\n case 'shortU16': {\n childManifest.imports.add('solana_short_vec::ShortVec');\n return {\n ...childManifest,\n type: `ShortVec<${childManifest.type}>`,\n };\n }\n default:\n throw new Error(`Array prefix not supported: ${prefix.format}`);\n }\n }\n\n // TODO: Add to the Rust validator.\n throw new Error('Array size not supported by Borsh');\n },\n\n visitBooleanType(booleanType) {\n const resolvedSize = resolveNestedTypeNode(booleanType.size);\n if (resolvedSize.format === 'u8' && resolvedSize.endian === 'le') {\n return {\n imports: new ImportMap(),\n nestedStructs: [],\n type: 'bool',\n };\n }\n\n // TODO: Add to the Rust validator.\n throw new Error('Bool size not supported by Borsh');\n },\n\n visitBytesType(_bytesType, { self }) {\n let arraySize: CountNode = remainderCountNode();\n if (typeof parentSize === 'number') {\n arraySize = fixedCountNode(parentSize);\n } else if (parentSize && typeof parentSize === 'object') {\n arraySize = prefixedCountNode(parentSize);\n }\n const arrayType = arrayTypeNode(numberTypeNode('u8'), arraySize);\n return visit(arrayType, self);\n },\n\n visitDefinedType(definedType, { self }) {\n parentName = pascalCase(definedType.name);\n const manifest = visit(definedType.type, self);\n const traits = getTraitsFromNode(definedType);\n manifest.imports.mergeWith(traits.imports);\n parentName = null;\n\n const renderedType = isNode(definedType.type, ['enumTypeNode', 'structTypeNode'])\n ? manifest.type\n : `pub type ${pascalCase(definedType.name)} = ${manifest.type};`;\n\n return { ...manifest, type: `${traits.render}${renderedType}` };\n },\n\n visitDefinedTypeLink(node) {\n const pascalCaseDefinedType = pascalCase(node.name);\n const importFrom = getImportFrom(node);\n return {\n imports: new ImportMap().add(`${importFrom}::${pascalCaseDefinedType}`),\n nestedStructs: [],\n type: pascalCaseDefinedType,\n };\n },\n\n visitEnumEmptyVariantType(enumEmptyVariantType) {\n const name = pascalCase(enumEmptyVariantType.name);\n return {\n imports: new ImportMap(),\n nestedStructs: [],\n type: `${name},`,\n };\n },\n\n visitEnumStructVariantType(enumStructVariantType, { self }) {\n const name = pascalCase(enumStructVariantType.name);\n const originalParentName = parentName;\n\n if (!originalParentName) {\n throw new Error('Enum struct variant type must have a parent name.');\n }\n\n inlineStruct = true;\n parentName = pascalCase(originalParentName) + name;\n const typeManifest = visit(enumStructVariantType.struct, self);\n inlineStruct = false;\n parentName = originalParentName;\n\n return {\n ...typeManifest,\n type: `${name} ${typeManifest.type},`,\n };\n },\n\n visitEnumTupleVariantType(enumTupleVariantType, { self }) {\n const name = pascalCase(enumTupleVariantType.name);\n const originalParentName = parentName;\n\n if (!originalParentName) {\n throw new Error('Enum struct variant type must have a parent name.');\n }\n\n parentName = pascalCase(originalParentName) + name;\n const childManifest = visit(enumTupleVariantType.tuple, self);\n parentName = originalParentName;\n\n let derive = '';\n if (childManifest.type === '(Pubkey)') {\n derive =\n '#[cfg_attr(feature = \"serde\", serde(with = \"serde_with::As::<serde_with::DisplayFromStr>\"))]\\n';\n } else if (childManifest.type === '(Vec<Pubkey>)') {\n derive =\n '#[cfg_attr(feature = \"serde\", serde(with = \"serde_with::As::<Vec<serde_with::DisplayFromStr>>\"))]\\n';\n }\n\n return {\n ...childManifest,\n type: `${derive}${name}${childManifest.type},`,\n };\n },\n\n visitEnumType(enumType, { self }) {\n const originalParentName = parentName;\n if (!originalParentName) {\n // TODO: Add to the Rust validator.\n throw new Error('Enum type must have a parent name.');\n }\n\n const variants = enumType.variants.map(variant => visit(variant, self));\n const variantNames = variants.map(variant => variant.type).join('\\n');\n const mergedManifest = mergeManifests(variants);\n\n return {\n ...mergedManifest,\n type: `pub enum ${pascalCase(originalParentName)} {\\n${variantNames}\\n}`,\n };\n },\n\n visitFixedSizeType(fixedSizeType, { self }) {\n parentSize = fixedSizeType.size;\n const manifest = visit(fixedSizeType.type, self);\n parentSize = null;\n return manifest;\n },\n\n visitMapType(mapType, { self }) {\n const key = visit(mapType.key, self);\n const value = visit(mapType.value, self);\n const mergedManifest = mergeManifests([key, value]);\n mergedManifest.imports.add('std::collections::HashMap');\n return {\n ...mergedManifest,\n type: `HashMap<${key.type}, ${value.type}>`,\n };\n },\n\n visitNumberType(numberType) {\n if (numberType.endian !== 'le') {\n // TODO: Add to the Rust validator.\n throw new Error('Number endianness not supported by Borsh');\n }\n\n if (numberType.format === 'shortU16') {\n return {\n imports: new ImportMap().add('solana_short_vec::ShortU16'),\n nestedStructs: [],\n type: 'ShortU16',\n };\n }\n\n return {\n imports: new ImportMap(),\n nestedStructs: [],\n type: numberType.format,\n };\n },\n\n visitOptionType(optionType, { self }) {\n const childManifest = visit(optionType.item, self);\n\n const optionPrefix = resolveNestedTypeNode(optionType.prefix);\n if (optionPrefix.format === 'u8' && optionPrefix.endian === 'le') {\n return {\n ...childManifest,\n type: `Option<${childManifest.type}>`,\n };\n }\n\n // TODO: Add to the Rust validator.\n throw new Error('Option size not supported by Borsh');\n },\n\n visitPublicKeyType() {\n return {\n imports: new ImportMap().add('solana_pubkey::Pubkey'),\n nestedStructs: [],\n type: 'Pubkey',\n };\n },\n\n visitRemainderOptionType(node) {\n throw new CodamaError(CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE, { kind: node.kind, node });\n },\n\n visitSetType(setType, { self }) {\n const childManifest = visit(setType.item, self);\n childManifest.imports.add('std::collections::HashSet');\n return {\n ...childManifest,\n type: `HashSet<${childManifest.type}>`,\n };\n },\n\n visitSizePrefixType(sizePrefixType, { self }) {\n parentSize = resolveNestedTypeNode(sizePrefixType.prefix);\n const manifest = visit(sizePrefixType.type, self);\n parentSize = null;\n return manifest;\n },\n\n visitStringType() {\n if (!parentSize) {\n return {\n imports: new ImportMap().add(`kaigan::types::RemainderStr`),\n nestedStructs: [],\n type: `RemainderStr`,\n };\n }\n\n if (typeof parentSize === 'number') {\n return {\n imports: new ImportMap(),\n nestedStructs: [],\n type: `[u8; ${parentSize}]`,\n };\n }\n\n if (isNode(parentSize, 'numberTypeNode') && parentSize.endian === 'le') {\n switch (parentSize.format) {\n case 'u32':\n return {\n imports: new ImportMap(),\n nestedStructs: [],\n type: 'String',\n };\n case 'u8':\n case 'u16':\n case 'u64': {\n const prefix = parentSize.format.toUpperCase();\n return {\n imports: new ImportMap().add(`kaigan::types::${prefix}PrefixString`),\n nestedStructs: [],\n type: `${prefix}PrefixString`,\n };\n }\n default:\n throw new Error(`'String size not supported: ${parentSize.format}`);\n }\n }\n\n // TODO: Add to the Rust validator.\n throw new Error('String size not supported by Borsh');\n },\n\n visitStructFieldType(structFieldType, { self }) {\n const originalParentName = parentName;\n const originalInlineStruct = inlineStruct;\n const originalNestedStruct = nestedStruct;\n\n if (!originalParentName) {\n throw new Error('Struct field type must have a parent name.');\n }\n\n parentName = pascalCase(originalParentName) + pascalCase(structFieldType.name);\n nestedStruct = true;\n inlineStruct = false;\n\n const fieldManifest = visit(structFieldType.type, self);\n\n parentName = originalParentName;\n inlineStruct = originalInlineStruct;\n nestedStruct = originalNestedStruct;\n\n const fieldName = snakeCase(structFieldType.name);\n const docblock = rustDocblock(parseDocs(structFieldType.docs));\n const resolvedNestedType = resolveNestedTypeNode(structFieldType.type);\n\n let derive = '';\n if (fieldManifest.type === 'Pubkey') {\n derive =\n '#[cfg_attr(feature = \"serde\", serde(with = \"serde_with::As::<serde_with::DisplayFromStr>\"))]\\n';\n } else if (fieldManifest.type === 'Vec<Pubkey>') {\n derive =\n '#[cfg_attr(feature = \"serde\", serde(with = \"serde_with::As::<Vec<serde_with::DisplayFromStr>>\"))]\\n';\n } else if (\n isNode(resolvedNestedType, 'arrayTypeNode') &&\n isNode(resolvedNestedType.count, 'fixedCountNode') &&\n resolvedNestedType.count.value > 32\n ) {\n derive = '#[cfg_attr(feature = \"serde\", serde(with = \"serde_big_array::BigArray\"))]\\n';\n } else if (\n isNode(resolvedNestedType, ['bytesTypeNode', 'stringTypeNode']) &&\n isNode(structFieldType.type, 'fixedSizeTypeNode') &&\n structFieldType.type.size > 32\n ) {\n derive =\n '#[cfg_attr(feature = \"serde\", serde(with = \"serde_with::As::<serde_with::Bytes>\"))]\\n';\n }\n\n return {\n ...fieldManifest,\n type: inlineStruct\n ? `${docblock}${derive}${fieldName}: ${fieldManifest.type},`\n : `${docblock}${derive}pub ${fieldName}: ${fieldManifest.type},`,\n };\n },\n\n visitStructType(structType, { self }) {\n const originalParentName = parentName;\n\n if (!originalParentName) {\n // TODO: Add to the Rust validator.\n throw new Error('Struct type must have a parent name.');\n }\n\n const fields = structType.fields.map(field => visit(field, self));\n const fieldTypes = fields.map(field => field.type).join('\\n');\n const mergedManifest = mergeManifests(fields);\n\n if (nestedStruct) {\n const nestedTraits = getTraitsFromNode(\n definedTypeNode({ name: originalParentName, type: structType }),\n );\n mergedManifest.imports.mergeWith(nestedTraits.imports);\n return {\n ...mergedManifest,\n nestedStructs: [\n ...mergedManifest.nestedStructs,\n `${nestedTraits.render}pub struct ${pascalCase(originalParentName)} {\\n${fieldTypes}\\n}`,\n ],\n type: pascalCase(originalParentName),\n };\n }\n\n if (inlineStruct) {\n return { ...mergedManifest, type: `{\\n${fieldTypes}\\n}` };\n }\n\n return {\n ...mergedManifest,\n type: `pub struct ${pascalCase(originalParentName)} {\\n${fieldTypes}\\n}`,\n };\n },\n\n visitTupleType(tupleType, { self }) {\n const items = tupleType.items.map(item => visit(item, self));\n const mergedManifest = mergeManifests(items);\n\n return {\n ...mergedManifest,\n type: `(${items.map(item => item.type).join(', ')})`,\n };\n },\n\n visitZeroableOptionType(node) {\n throw new CodamaError(CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE, { kind: node.kind, node });\n },\n }),\n );\n}\n\nfunction mergeManifests(manifests: TypeManifest[]): Pick<TypeManifest, 'imports' | 'nestedStructs'> {\n return {\n imports: new ImportMap().mergeWith(...manifests.map(td => td.imports)),\n nestedStructs: manifests.flatMap(m => m.nestedStructs),\n };\n}\n","import { BytesValueNode } from '@codama/nodes';\nimport { getBase16Encoder, getBase58Encoder, getBase64Encoder, getUtf8Encoder } from '@solana/codecs-strings';\n\nexport function getBytesFromBytesValueNode(node: BytesValueNode): Uint8Array {\n switch (node.encoding) {\n case 'utf8':\n return getUtf8Encoder().encode(node.data) as Uint8Array;\n case 'base16':\n return getBase16Encoder().encode(node.data) as Uint8Array;\n case 'base58':\n return getBase58Encoder().encode(node.data) as Uint8Array;\n case 'base64':\n default:\n return getBase64Encoder().encode(node.data) as Uint8Array;\n }\n}\n","import { CODAMA_ERROR__UNEXPECTED_NODE_KIND, CodamaError } from '@codama/errors';\nimport {\n AccountLinkNode,\n DefinedTypeLinkNode,\n InstructionLinkNode,\n PdaLinkNode,\n ProgramLinkNode,\n ResolverValueNode,\n} from '@codama/nodes';\n\nexport type LinkOverrides = {\n accounts?: Record<string, string>;\n definedTypes?: Record<string, string>;\n instructions?: Record<string, string>;\n pdas?: Record<string, string>;\n programs?: Record<string, string>;\n resolvers?: Record<string, string>;\n};\n\ntype OverridableNodes =\n | AccountLinkNode\n | DefinedTypeLinkNode\n | InstructionLinkNode\n | PdaLinkNode\n | ProgramLinkNode\n | ResolverValueNode;\n\nexport type GetImportFromFunction = (node: OverridableNodes, fallback?: string) => string;\n\nexport function getImportFromFactory(overrides: LinkOverrides): GetImportFromFunction {\n const linkOverrides = {\n accounts: overrides.accounts ?? {},\n definedTypes: overrides.definedTypes ?? {},\n instructions: overrides.instructions ?? {},\n pdas: overrides.pdas ?? {},\n programs: overrides.programs ?? {},\n resolvers: overrides.resolvers ?? {},\n };\n\n return (node: OverridableNodes) => {\n const kind = node.kind;\n switch (kind) {\n case 'accountLinkNode':\n return linkOverrides.accounts[node.name] ?? 'generatedAccounts';\n case 'definedTypeLinkNode':\n return linkOverrides.definedTypes[node.name] ?? 'generatedTypes';\n case 'instructionLinkNode':\n return linkOverrides.instructions[node.name] ?? 'generatedInstructions';\n case 'pdaLinkNode':\n return linkOverrides.pdas[node.name] ?? 'generatedAccounts';\n case 'programLinkNode':\n return linkOverrides.programs[node.name] ?? 'generatedPrograms';\n case 'resolverValueNode':\n return linkOverrides.resolvers[node.name] ?? 'hooked';\n default:\n throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, {\n expectedKinds: [\n 'AccountLinkNode',\n 'DefinedTypeLinkNode',\n 'InstructionLinkNode',\n 'PdaLinkNode',\n 'ProgramLinkNode',\n 'resolverValueNode',\n ],\n kind: kind satisfies never,\n node,\n });\n }\n };\n}\n","import { dirname as pathDirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport { camelCase, kebabCase, pascalCase, snakeCase, titleCase } from '@codama/nodes';\nimport nunjucks, { ConfigureOptions as NunJucksOptions } from 'nunjucks';\n\nexport function rustDocblock(docs: string[]): string {\n if (docs.length <= 0) return '';\n const lines = docs.map(doc => `/// ${doc}`);\n return `${lines.join('\\n')}\\n`;\n}\n\nexport const render = (template: string, context?: object, options?: NunJucksOptions): string => {\n // @ts-expect-error import.meta will be used in the right environment.\n const dirname = __ESM__ ? pathDirname(fileURLToPath(import.meta.url)) : __dirname;\n const templates = __TEST__ ? join(dirname, '..', '..', 'public', 'templates') : join(dirname, 'templates'); // Path to templates from bundled output file.\n const env = nunjucks.configure(templates, { autoescape: false, trimBlocks: true, ...options });\n env.addFilter('pascalCase', pascalCase);\n env.addFilter('camelCase', camelCase);\n env.addFilter('snakeCase', snakeCase);\n env.addFilter('kebabCase', kebabCase);\n env.addFilter('titleCase', titleCase);\n env.addFilter('rustDocblock', rustDocblock);\n return env.render(template, context);\n};\n","import {\n AccountNode,\n assertIsNode,\n camelCase,\n DefinedTypeNode,\n InstructionNode,\n isNode,\n isScalarEnum,\n} from '@codama/nodes';\n\nimport { ImportMap } from '../ImportMap';\n\nexport type TraitOptions = {\n /** The default traits to implement for all types. */\n baseDefaults?: string[];\n /**\n * The default traits to implement for data enums only — on top of the base defaults.\n * Data enums are enums with at least one non-unit variant.\n */\n dataEnumDefaults?: string[];\n /**\n * The mapping of feature flags to traits.\n * For each entry, the traits will be rendered within a\n * `#[cfg_attr(feature = \"feature_name\", derive(Traits))]` attribute.\n */\n featureFlags?: Record<string, string[]>;\n /** The complete trait overrides of specific types. */\n overrides?: Record<string, string[]>;\n /**\n * The default traits to implement for scalar enums only — on top of the base defaults.\n * Scalar enums are enums with no variants or only unit variants.\n */\n scalarEnumDefaults?: string[];\n /** The default traits to implement for structs only — on top of the base defaults. */\n structDefaults?: string[];\n /** Whether or not to use the fully qualified name for traits, instead of importing them. */\n useFullyQualifiedName?: boolean;\n};\n\nexport const DEFAULT_TRAIT_OPTIONS: Required<TraitOptions> = {\n baseDefaults: [\n 'borsh::BorshSerialize',\n 'borsh::BorshDeserialize',\n 'serde::Serialize',\n 'serde::Deserialize',\n 'Clone',\n 'Debug',\n 'Eq',\n 'PartialEq',\n ],\n dataEnumDefaults: [],\n featureFlags: { serde: ['serde::Serialize', 'serde::Deserialize'] },\n overrides: {},\n scalarEnumDefaults: ['Copy', 'PartialOrd', 'Hash', 'num_derive::FromPrimitive'],\n structDefaults: [],\n useFullyQualifiedName: false,\n};\n\nexport type GetTraitsFromNodeFunction = (node: AccountNode | DefinedTypeNode | InstructionNode) => {\n imports: ImportMap;\n render: string;\n};\n\nexport function getTraitsFromNodeFactory(options: TraitOptions = {}): GetTraitsFromNodeFunction {\n return node => getTraitsFromNode(node, options);\n}\n\nexport function getTraitsFromNode(\n node: AccountNode | DefinedTypeNode | InstructionNode,\n userOptions: TraitOptions = {},\n): { imports: ImportMap; render: string } {\n assertIsNode(node, ['accountNode', 'definedTypeNode', 'instructionNode']);\n const options: Required<TraitOptions> = { ...DEFAULT_TRAIT_OPTIONS, ...userOptions };\n\n // Get the node type and return early if it's a type alias.\n const nodeType = getNodeType(node);\n if (nodeType === 'alias') {\n return { imports: new ImportMap(), render: '' };\n }\n\n // Find all the FQN traits for the node.\n const sanitizedOverrides = Object.fromEntries(\n Object.entries(options.overrides).map(([key, value]) => [camelCase(key), value]),\n );\n const nodeOverrides: string[] | undefined = sanitizedOverrides[node.name];\n const allTraits = nodeOverrides === undefined ? getDefaultTraits(nodeType, options) : nodeOverrides;\n\n // Wrap the traits in feature flags if necessary.\n const partitionedTraits = partitionTraitsInFeatures(allTraits, options.featureFlags);\n let unfeaturedTraits = partitionedTraits[0];\n const featuredTraits = partitionedTraits[1];\n\n // Import the traits if necessary.\n const imports = new ImportMap();\n if (!options.useFullyQualifiedName) {\n unfeaturedTraits = extractFullyQualifiedNames(unfeaturedTraits, imports);\n }\n\n // Render the trait lines.\n const traitLines: string[] = [\n ...(unfeaturedTraits.length > 0 ? [`#[derive(${unfeaturedTraits.join(', ')})]\\n`] : []),\n ...Object.entries(featuredTraits).map(([feature, traits]) => {\n return `#[cfg_attr(feature = \"${feature}\", derive(${traits.join(', ')}))]\\n`;\n }),\n ];\n\n return { imports, render: traitLines.join('') };\n}\n\nfunction getNodeType(\n node: AccountNode | DefinedTypeNode | InstructionNode,\n): 'alias' | 'dataEnum' | 'scalarEnum' | 'struct' {\n if (isNode(node, ['accountNode', 'instructionNode'])) return 'struct';\n if (isNode(node.type, 'structTypeNode')) return 'struct';\n if (isNode(node.type, 'enumTypeNode')) {\n return isScalarEnum(node.type) ? 'scalarEnum' : 'dataEnum';\n }\n return 'alias';\n}\n\nfunction getDefaultTraits(\n nodeType: 'dataEnum' | 'scalarEnum' | 'struct',\n options: Pick<\n Required<TraitOptions>,\n 'baseDefaults' | 'dataEnumDefaults' | 'scalarEnumDefaults' | 'structDefaults'\n >,\n): string[] {\n switch (nodeType) {\n case 'dataEnum':\n return [...options.baseDefaults, ...options.dataEnumDefaults];\n case 'scalarEnum':\n return [...options.baseDefaults, ...options.scalarEnumDefaults];\n case 'struct':\n return [...options.baseDefaults, ...options.structDefaults];\n }\n}\n\nfunction partitionTraitsInFeatures(\n traits: string[],\n featureFlags: Record<string, string[]>,\n): [string[], Record<string, string[]>] {\n // Reverse the feature flags option for quick lookup.\n // If there are any duplicate traits, the first one encountered will be used.\n const reverseFeatureFlags = Object.entries(featureFlags).reduce(\n (acc, [feature, traits]) => {\n for (const trait of traits) {\n if (!acc[trait]) acc[trait] = feature;\n }\n return acc;\n },\n {} as Record<string, string>,\n );\n\n const unfeaturedTraits: string[] = [];\n const featuredTraits: Record<string, string[]> = {};\n for (const trait of traits) {\n const feature: string | undefined = reverseFeatureFlags[trait];\n if (feature === undefined) {\n unfeaturedTraits.push(trait);\n } else {\n if (!featuredTraits[feature]) featuredTraits[feature] = [];\n featuredTraits[feature].push(trait);\n }\n }\n\n return [unfeaturedTraits, featuredTraits];\n}\n\nfunction extractFullyQualifiedNames(traits: string[], imports: ImportMap): string[] {\n return traits.map(trait => {\n const index = trait.lastIndexOf('::');\n if (index === -1) return trait;\n imports.add(trait);\n return trait.slice(index +