diff --git a/e2e/anchor/src/generated/constants.rs b/e2e/anchor/src/generated/constants.rs new file mode 100644 index 0000000..a0f507b --- /dev/null +++ b/e2e/anchor/src/generated/constants.rs @@ -0,0 +1,17 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +pub const EXTRA_ACCOUNT_METAS: [u8; 19] = [ + 101, 120, 116, 114, 97, 45, 97, 99, 99, 111, 117, 110, 116, 45, 109, 101, 116, 97, 115, +]; + +pub const GUARD_V1: [u8; 8] = [103, 117, 97, 114, 100, 95, 118, 49]; + +pub const WEN_TOKEN_GUARD: [u8; 24] = [ + 119, 101, 110, 95, 116, 111, 107, 101, 110, 95, 116, 114, 97, 110, 115, 102, 101, 114, 95, 103, + 117, 97, 114, 100, +]; diff --git a/e2e/anchor/src/generated/mod.rs b/e2e/anchor/src/generated/mod.rs index e0d740a..c787e64 100644 --- a/e2e/anchor/src/generated/mod.rs +++ b/e2e/anchor/src/generated/mod.rs @@ -6,6 +6,7 @@ //! pub mod accounts; +pub mod constants; pub mod errors; pub mod instructions; pub mod programs; diff --git a/public/templates/constantsPage.njk b/public/templates/constantsPage.njk new file mode 100644 index 0000000..b685ceb --- /dev/null +++ b/public/templates/constantsPage.njk @@ -0,0 +1,13 @@ +{% extends "layout.njk" %} +{% import "macros.njk" as macros %} + +{% block main %} + +{{ imports }} + +{% for constant in constants | sort(false, false, 'name') %} +{{ macros.docblock(constant.docs) }} +pub const {{ constant.name | snakeCase | upper }}: {{ constant.type }} = {{ constant.value }}; +{% endfor %} + +{% endblock %} diff --git a/public/templates/rootMod.njk b/public/templates/rootMod.njk index d792bcb..567c9e5 100644 --- a/public/templates/rootMod.njk +++ b/public/templates/rootMod.njk @@ -6,6 +6,9 @@ {% if accountsToExport.length > 0 %} pub mod accounts; {% endif %} + {% if constantsToExport.length > 0 %} + pub mod constants; + {% endif %} {% if programsToExport.length > 0 %} pub mod errors; {% endif %} diff --git a/src/getRenderMapVisitor.ts b/src/getRenderMapVisitor.ts index 9abba5b..9a72fab 100644 --- a/src/getRenderMapVisitor.ts +++ b/src/getRenderMapVisitor.ts @@ -1,6 +1,7 @@ import { logWarn } from '@codama/errors'; import { getAllAccounts, + getAllConstants, getAllDefinedTypes, getAllInstructionsWithSubs, getAllPrograms, @@ -284,6 +285,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) { visitRoot(node, { self }) { const programsToExport = getAllPrograms(node); const accountsToExport = getAllAccounts(node); + const constantsToExport = getAllConstants(node); const instructionsToExport = getAllInstructionsWithSubs(node, { leavesOnly: !renderParentInstructions, }); @@ -291,11 +293,36 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) { const hasAnythingToExport = programsToExport.length > 0 || accountsToExport.length > 0 || + constantsToExport.length > 0 || instructionsToExport.length > 0 || definedTypesToExport.length > 0; + const constants = constantsToExport.map(constant => { + const isBytes = isNode(constant.type, 'bytesTypeNode'); + const isString = isNode(constant.type, 'stringTypeNode'); + const fixedByteArray = + isNode(constant.type, 'fixedSizeTypeNode') && + isNode(constant.type.type, 'bytesTypeNode') && + isNode(constant.value, 'stringValueNode') + ? parseByteArrayConstant(constant.value.string, constant.type.size) + : null; + const typeManifest = isBytes || isString ? null : visit(constant.type, typeManifestVisitor); + const valueManifest = renderValueNode(constant.value, getImportFrom, true); + + return { + ...constant, + imports: new ImportMap() + .mergeWith(typeManifest?.imports ?? new ImportMap()) + .mergeWith(valueManifest.imports), + type: isBytes ? "&'static [u8]" : isString ? "&'static str" : typeManifest!.type, + value: fixedByteArray ?? `${isBytes ? '&' : ''}${valueManifest.render}`, + }; + }); + const constantsImports = new ImportMap().mergeWith(...constants.map(constant => constant.imports)); + const ctx = { accountsToExport, + constantsToExport, definedTypesToExport, hasAnythingToExport, instructionsToExport, @@ -309,6 +336,16 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) { accountsToExport.length > 0 ? { content: render('accountsMod.njk', ctx), imports: new ImportMap() } : undefined, + ['constants.rs']: + constantsToExport.length > 0 + ? { + content: render('constantsPage.njk', { + constants, + imports: constantsImports.toString(dependencyMap), + }), + imports: constantsImports, + } + : undefined, ['errors/mod.rs']: programsToExport.length > 0 ? { content: render('errorsMod.njk', ctx), imports: new ImportMap() } @@ -340,6 +377,23 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) { ); } +function parseByteArrayConstant(value: string, expectedLength: number): string | null { + try { + const bytes: unknown = JSON.parse(value); + if ( + !Array.isArray(bytes) || + bytes.length !== expectedLength || + !bytes.every(byte => typeof byte === 'number' && Number.isInteger(byte) && byte >= 0 && byte <= 255) + ) { + return null; + } + + return `[${bytes.join(', ')}]`; + } catch { + return null; + } +} + function getConflictsForInstructionAccountsAndArgs(instruction: InstructionNode): string[] { const allNames = [ ...(instruction.accounts ?? []).map(account => account.name), diff --git a/test/constantsPage.test.ts b/test/constantsPage.test.ts new file mode 100644 index 0000000..005b569 --- /dev/null +++ b/test/constantsPage.test.ts @@ -0,0 +1,55 @@ +import { + bytesTypeNode, + bytesValueNode, + constantNode, + fixedSizeTypeNode, + numberTypeNode, + numberValueNode, + programNode, + rootNode, + stringValueNode, +} from '@codama/nodes'; +import { getFromRenderMap } from '@codama/renderers-core'; +import { visit } from '@codama/visitors-core'; +import { test } from 'vitest'; + +import { getRenderMapVisitor } from '../src'; +import { codeContains } from './_setup'; + +test('it renders program constants in a top-level constants page', () => { + const node = rootNode( + programNode({ + constants: [ + constantNode('maxOption', numberTypeNode('u8'), numberValueNode(10), ['Maximum options.']), + constantNode('seed', bytesTypeNode(), bytesValueNode('utf8', 'seed')), + ], + name: 'governance', + publicKey: 'GovER5Lthms3bLBqWub97yVrQm9WLZ7YgRrxYQYy2f', + }), + ); + + const renderMap = visit(node, getRenderMapVisitor()); + + codeContains(getFromRenderMap(renderMap, 'constants.rs').content, [ + '/// Maximum options.', + 'pub const MAX_OPTION: u8 = 10;', + "pub const SEED: &'static [u8] = &[115, 101, 101, 100];", + ]); + codeContains(getFromRenderMap(renderMap, 'mod.rs').content, 'pub mod constants;'); +}); + +test('it renders JSON byte arrays from Anchor constants as fixed byte arrays', () => { + const node = rootNode( + programNode({ + constants: [ + constantNode('guardV1', fixedSizeTypeNode(bytesTypeNode(), 3), stringValueNode('[103, 117, 97]')), + ], + name: 'tokenGuard', + publicKey: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', + }), + ); + + const renderMap = visit(node, getRenderMapVisitor()); + + codeContains(getFromRenderMap(renderMap, 'constants.rs').content, 'pub const GUARD_V1: [u8; 3] = [103, 117, 97];'); +});