Skip to content
Draft
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
6 changes: 6 additions & 0 deletions .changeset/flat-turkeys-win.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@env-spec/parser": patch
"varlock": patch
---

added `replace()` function to replace a string with support for ref()
1 change: 1 addition & 0 deletions .cursor/rules/varlock.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ actions:
- concat() - For concatenated values
- exec() - For command output
- ref() - For referencing other variables
- replace() - For ref()-ing a variable and find-and-replace

examples:
- input: |
Expand Down
10 changes: 10 additions & 0 deletions packages/env-spec-parser/src/simple-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ export function simpleResolver(
} else {
throw new Error('Invalid `ref` args');
}
} else if (valOrFn.name === 'replace') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't actually need this defined here at all. This is just a very simple implementation of a few functions which are used to test the parser itself.

const args = valOrFn.simplifiedArgs;
if (Array.isArray(args)) {
const str = args[0];
const search = args[1];
const replace = args[2];
return str.replace(search, replace);
} else {
throw new Error('Invalid `replace` args');
}
} else if (valOrFn.name === 'concat') {
const args = valOrFn.data.args.values;
const resolvedArgs = args.map((i) => {
Expand Down
56 changes: 56 additions & 0 deletions packages/env-spec-parser/test/functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,21 @@ describe('function calls', functionValueTests({
ITEM: 'foo-val',
},
},
'replace()': {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is testing how the parser itself handles parsing functions, and we don't need to define or test the new resolver within the parser. The tests we want to add are in env-graph

input: 'ITEM=replace("foo", "f", "b")',
expected: { ITEM: 'boo' },
},
'nested function calls - array': {
input: 'OTHERVAL=d\nITEM=concat("a", fallback("", "b"), exec("echo c"), ref(OTHERVAL))',
expected: { ITEM: 'abcd' },
},
'nested function calls - replace()': {
input: 'FOO=foo-val\nITEM=replace(ref("FOO"), "f", "b")',
expected: {
FOO: 'foo-val',
ITEM: 'boo-val',
},
},
'nested function calls - key/value': {
input: 'ITEM=remap("foo", zzz=aaa, bar=fallback("", "foo"))',
expected: { ITEM: 'bar' },
Expand Down Expand Up @@ -135,6 +146,51 @@ describe('ref expansion', functionValueTests({
},
}));

describe('replace expansion', functionValueTests({
'ref expansion - unquoted': {
input: 'OTHER=foo\nITEM=replace($OTHER, "f", "b")',
expected: {
OTHER: 'foo',
ITEM: 'boo',
},
},
'ref expansion within quotes - double quotes': {
input: 'OTHER=foo\nITEM="${replace($OTHER, "f", "b")}"',
expected: {
OTHER: 'foo',
ITEM: 'boo',
},
},
'ref expansion within quotes - backtick': {
input: 'OTHER=foo\nITEM=`${replace($OTHER, "f", "b")}`',
expected: {
OTHER: 'foo',
ITEM: 'boo',
},
},
'ref expansion within quotes - single quotes (NOT EXPANDED)': {
input: "OTHER=foo\nITEM='${replace($OTHER, 'f', 'b')}'",
expected: {
OTHER: 'foo',
ITEM: '${replace($OTHER, "f", "b")}',
},
},
'ref expansion - simple (no brackets)': {
input: 'OTHER=foo\nITEM=replace($OTHER, "f", "b")',
expected: {
OTHER: 'foo',
ITEM: 'boo',
},
},
'ref expansion - with brackets': {
input: 'FOO=foo\nITEM=replace($FOO, "f", "b")',
expected: {
FOO: 'foo',
ITEM: 'boo',
},
},
}));

describe('complex cases', functionValueTests({
'multiple expansions': {
input: 'FOO=foo\nBAR=bar\nITEM=${FOO}-$BAR-$(echo baz)-${UNDEF:-qux}',
Expand Down
36 changes: 36 additions & 0 deletions packages/varlock/env-graph/lib/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,42 @@ export class RefResolver extends Resolver {
}
}

export class ReplaceResolver extends Resolver {
static fnName = 'replace';
label = 'replace';
icon = 'mdi-light:content-duplicate';

private originalString?: string;
private searchString?: string;
private replacementString?: string;

async _process() {
if (this.fnArgs.length !== 1) {
throw new SchemaError('replace() expects three child args');
}
if (!(this.fnArgs[0] instanceof StaticValueResolver)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is checking that the args are all static, as in replace("foo", "f", "z") but we want to support the args not being static - meaning they are other function calls (which includes using expansion) - ie replace($SOMEVAR, 'find', $OTHER).

So within _process, we are basically just checking if args look right, but without necessarily knowing what the actual values are. (We only can check values if the arg was static). Then within _resolve, we can get the actual values, do some additional error checks, and then return our result.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also note - this isnt getting tested yet because you had added the tests in the parser lib.

throw new SchemaError('replace() expects the first arg to be the original string');
}
if (!(this.fnArgs[1] instanceof StaticValueResolver)) {
throw new SchemaError('replace() expects the second arg to be the string to search for');
}
if (!(this.fnArgs[2] instanceof StaticValueResolver)) {
throw new SchemaError('replace() expects the third arg to be the replacement string');
}

this.originalString = String(await this.fnArgs[0].resolve());
this.searchString = String(await this.fnArgs[1].resolve());
this.replacementString = String(await this.fnArgs[2].resolve());
}

protected async _resolve() {
if (!this.originalString) throw new Error('expected originalString to be set');
if (!this.searchString) throw new Error('expected searchString to be set');
if (!this.replacementString) throw new Error('expected replacementString to be set');
return this.originalString.replace(this.searchString, this.replacementString);
}
}

// regex() is only used internally as function args to be used by other functions
// we will check final resoled values to make sure they are not regexes
export class RegexResolver extends Resolver {
Expand Down