Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .bumpy/fix-enum-env-string-coerce.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
varlock: patch
---

Coerce string env overrides to numeric/boolean enum members
17 changes: 15 additions & 2 deletions packages/varlock/src/env-graph/lib/data-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,8 +468,21 @@ const EnumDataType = createEnvGraphDataType(
icon: 'material-symbols-light:category', // a few shapes... not sure about this one
coercedType: { enum: enumOptions },
coerce(val) {
if (_.isString(val) || _.isNumber(val) || _.isBoolean(val)) return val;
return new CoercionError('Value must be a string, number, or boolean');
if (_.isNumber(val) || _.isBoolean(val)) return val;
if (!_.isString(val)) {
return new CoercionError('Value must be a string, number, or boolean');
}
// Exact string member (e.g. enum(dev, prod) + "dev")
if (enumOptions.includes(val)) return val;
// process.env / overrideValues are always strings. Schema file values like
// LEVEL=2 are auto-coerced to numbers by the parser, but CI overrides stay
// as "2" / "true" and must still match numeric/boolean members.
for (const opt of enumOptions) {
if (_.isNumber(opt) && String(opt) === val) return opt;
if (opt === true && val === 'true') return true;
if (opt === false && val === 'false') return false;
}
return val;
},
validate(val) {
const possibleValues: Array<any> = enumOptions || [];
Expand Down
35 changes: 34 additions & 1 deletion packages/varlock/src/env-graph/test/data-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ import { describe, it, expect } from 'vitest';
import { outdent } from 'outdent';
import { DotEnvFileDataSource, EnvGraph, CoercionError } from '../index';

async function loadAndResolve(envFileContent: string) {
async function loadAndResolve(
envFileContent: string,
opts?: { overrideValues?: Record<string, string> },
) {
const g = new EnvGraph();
if (opts?.overrideValues) g.overrideValues = opts.overrideValues;
const testDataSource = new DotEnvFileDataSource('.env.schema', {
overrideContents: outdent`
# @defaultRequired=false
Expand Down Expand Up @@ -44,6 +48,35 @@ describe('number data type - Infinity coercion', () => {
});
});

describe('enum data type - process.env string overrides', () => {
it('accepts numeric enum members from schema file values', async () => {
const g = await loadAndResolve(outdent`
# @type=enum(1, 2, 3)
LEVEL=2
`);
expect(g.configSchema.LEVEL.isValid).toBe(true);
expect(g.configSchema.LEVEL.resolvedValue).toBe(2);
});

it('accepts numeric enum members from string overrides', async () => {
const g = await loadAndResolve(outdent`
# @type=enum(1, 2, 3)
LEVEL=2
`, { overrideValues: { LEVEL: '1' } });
expect(g.configSchema.LEVEL.isValid).toBe(true);
expect(g.configSchema.LEVEL.resolvedValue).toBe(1);
});

it('accepts boolean enum members from string overrides', async () => {
const g = await loadAndResolve(outdent`
# @type=enum(true, false)
FLAG=false
`, { overrideValues: { FLAG: 'true' } });
expect(g.configSchema.FLAG.isValid).toBe(true);
expect(g.configSchema.FLAG.resolvedValue).toBe(true);
});
});

describe('url data type', () => {
describe('prependHttps', () => {
it('prepends https:// when missing', async () => {
Expand Down
Loading