Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/builder/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export async function createInitialContext(
imports: {},

settings: _.clone(opts),

vars: {},
};
}

Expand Down
29 changes: 27 additions & 2 deletions packages/builder/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,31 @@ export const diamondSchema = z
})
.strict();

/**
* Schema for individual var definitions within a var namespace.
* Each var has a required type, optional value, and optional description.
*/
const varDefinitionSchema = z.object({
/**
* The value of the variable. If not provided, must be supplied externally at build time.
*/
value: z.string().optional().describe('The value of the variable. If not provided, must be supplied externally at build time.'),
/**
* The type of the variable. Required. Used for validation.
*/
type: z.enum(['string', 'address', 'number', 'boolean', 'bytes']).describe('The type of the variable. Used for validation.'),
/**
* Description of what this variable is used for.
*/
description: z.string().optional().describe('Description of what this variable is used for.'),
});

/**
* Schema for var namespace sections (e.g., [var.main]).
* Each namespace contains var definitions keyed by var name.
*/
export const varNamespaceSchema = z.record(z.string(), varDefinitionSchema);

export const varSchema = z
.object({
/**
Expand All @@ -878,7 +903,7 @@ export const varSchema = z
'List of operations that this operation depends on, which Cannon will execute first. If unspecified, Cannon automatically detects dependencies.'
),
})
.catchall(z.string());
.catchall(z.union([z.string(), varDefinitionSchema]));

/**
* Shared schema for cannonfile actions (operations).
Expand Down Expand Up @@ -973,7 +998,7 @@ const cannonfileActionsSchema = z.object({
/**
* @internal
*/
var: z.record(varSchema).describe('Apply a setting or intermediate value.'),
var: z.record(varNamespaceSchema).describe('Define namespaced variables with type information and descriptions.'),
// ... there may be others that come from plugins
});

Expand Down
234 changes: 230 additions & 4 deletions packages/builder/src/steps/var.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { fixtureCtx } from '../../test/fixtures';
import { fakeRuntime } from './utils.test.helper';
import { PackageReference } from '../package-reference';
import action from './var';
import { VarDefinition } from './var';

describe('steps/var.ts', () => {
describe('configInject()', () => {
it('injects all fields', async () => {
it('injects all fields (legacy format)', async () => {
const result = action.configInject(
fixtureCtx({
settings: { a: 'A', b: 'B', from: 'FROM', salt: 'SALT' },
Expand All @@ -26,10 +27,39 @@ describe('steps/var.ts', () => {
depends: ['<%= settings.A %>'],
});
});

it('injects values in new var namespace format', async () => {
const result = action.configInject(
fixtureCtx({
settings: { owner: '0x1234567890123456789012345678901234567890' },
}),
{
foobar: {
value: '<%= settings.owner %>',
type: 'address' as const,
description: 'Contract owner',
},
baz: {
type: 'string' as const,
},
}
);

expect(result).toStrictEqual({
foobar: {
value: '0x1234567890123456789012345678901234567890',
type: 'address',
description: 'Contract owner',
},
baz: {
type: 'string',
},
});
});
});

describe('getState()', () => {
it('resolves correct properties on state', async () => {
it('resolves correct properties on state (legacy format)', async () => {
const runtime = fakeRuntime;

const ctx = fixtureCtx({ settings: { woot: 'w' } });
Expand All @@ -50,7 +80,7 @@ describe('steps/var.ts', () => {
});

describe('exec()', () => {
it('works forwards compatible', async () => {
it('works forwards compatible (legacy setting format)', async () => {
const step = {
ref: new PackageReference('var-test:0.0.0'),
currentLabel: 'setting.something',
Expand All @@ -63,7 +93,7 @@ describe('steps/var.ts', () => {
expect(arts).toStrictEqual({ settings: { something: 'shoutout' } });
});

it('works backwards compatible', async () => {
it('works backwards compatible (legacy var format)', async () => {
const step = {
ref: new PackageReference('var-test:0.0.0'),
currentLabel: 'var.something',
Expand All @@ -75,5 +105,201 @@ describe('steps/var.ts', () => {
const arts = await action.exec(runtime, fixtureCtx(), config, step);
expect(arts).toStrictEqual({ settings: { else: 'shoutout' } });
});

describe('new var namespace format', () => {
it('returns vars in namespaced format', async () => {
const step = {
ref: new PackageReference('var-test:0.0.0'),
currentLabel: 'var.main',
};

const runtime = fakeRuntime;
const config: Record<string, VarDefinition> = {
foobar: {
value: 'my super variable',
type: 'string',
description: 'Super important variable documentation',
},
};

const arts = await action.exec(runtime, fixtureCtx(), config as any, step);
expect(arts).toStrictEqual({
settings: { foobar: 'my super variable' },
vars: { main: { foobar: 'my super variable' } },
});
});

it('validates address type', async () => {
const step = {
ref: new PackageReference('var-test:0.0.0'),
currentLabel: 'var.main',
};

const runtime = fakeRuntime;
const config: Record<string, VarDefinition> = {
owner: {
value: '0x1234567890123456789012345678901234567890',
type: 'address',
},
};

const arts = await action.exec(runtime, fixtureCtx(), config as any, step);
expect(arts.settings.owner).toBe('0x1234567890123456789012345678901234567890');
});

it('throws on invalid address type', async () => {
const step = {
ref: new PackageReference('var-test:0.0.0'),
currentLabel: 'var.main',
};

const runtime = fakeRuntime;
const config: Record<string, VarDefinition> = {
owner: {
value: 'not-an-address',
type: 'address',
},
};

await expect(action.exec(runtime, fixtureCtx(), config as any, step)).rejects.toThrow(
'Variable "owner" in namespace "main" has invalid value "not-an-address" for type "address"'
);
});

it('throws when value is missing and no override provided', async () => {
const step = {
ref: new PackageReference('var-test:0.0.0'),
currentLabel: 'var.main',
};

const runtime = fakeRuntime;
const config: Record<string, VarDefinition> = {
requiredVar: {
type: 'string',
},
};

await expect(action.exec(runtime, fixtureCtx(), config as any, step)).rejects.toThrow(
'Variable "requiredVar" in namespace "main" has no value defined'
);
});

it('uses override when value is missing', async () => {
const step = {
ref: new PackageReference('var-test:0.0.0'),
currentLabel: 'var.main',
};

const runtime = fakeRuntime;
const config: Record<string, VarDefinition> = {
overrideableVar: {
type: 'string',
description: 'Can be overridden',
},
};

const ctx = fixtureCtx({
overrideSettings: {
'main.overrideableVar': 'overridden-value',
},
});

const arts = await action.exec(runtime, ctx, config as any, step);
expect(arts.settings.overrideableVar).toBe('overridden-value');
expect(arts.vars!.main.overrideableVar).toBe('overridden-value');
});

it('validates number type', async () => {
const step = {
ref: new PackageReference('var-test:0.0.0'),
currentLabel: 'var.main',
};

const runtime = fakeRuntime;
const config: Record<string, VarDefinition> = {
amount: {
value: '12345',
type: 'number',
},
};

const arts = await action.exec(runtime, fixtureCtx(), config as any, step);
expect(arts.settings.amount).toBe('12345');
});

it('throws on invalid number type', async () => {
const step = {
ref: new PackageReference('var-test:0.0.0'),
currentLabel: 'var.main',
};

const runtime = fakeRuntime;
const config: Record<string, VarDefinition> = {
amount: {
value: 'not-a-number',
type: 'number',
},
};

await expect(action.exec(runtime, fixtureCtx(), config as any, step)).rejects.toThrow(
'Variable "amount" in namespace "main" has invalid value "not-a-number" for type "number"'
);
});

it('validates boolean type', async () => {
const step = {
ref: new PackageReference('var-test:0.0.0'),
currentLabel: 'var.main',
};

const runtime = fakeRuntime;
const config: Record<string, VarDefinition> = {
enabled: {
value: 'true',
type: 'boolean',
},
};

const arts = await action.exec(runtime, fixtureCtx(), config as any, step);
expect(arts.settings.enabled).toBe('true');
});

it('validates bytes type', async () => {
const step = {
ref: new PackageReference('var-test:0.0.0'),
currentLabel: 'var.main',
};

const runtime = fakeRuntime;
const config: Record<string, VarDefinition> = {
data: {
value: '0x1234',
type: 'bytes',
},
};

const arts = await action.exec(runtime, fixtureCtx(), config as any, step);
expect(arts.settings.data).toBe('0x1234');
});
});
});

describe('getOutputs()', () => {
it('returns settings format for legacy var', () => {
const config = { foo: 'bar' };
const outputs = action.getOutputs(config, { ref: null, currentLabel: 'var.something' });
expect(outputs).toContain('settings.foo');
});

it('returns namespaced var format for new format', () => {
const config: Record<string, VarDefinition> = {
foobar: {
value: 'test',
type: 'string',
},
};
const outputs = action.getOutputs(config as any, { ref: null, currentLabel: 'var.main' });
expect(outputs).toContain('var.main.foobar');
});
});
});
Loading
Loading