Skip to content

Commit

Permalink
feat(recursive): full recursive support (#37)
Browse files Browse the repository at this point in the history
* feat(recursive): support for evaluating expressions surrounded by arbitary non-expression strings

* feat(recursive): support for evaluating expressions surrounded by arbitary non-expression strings

* feat(recursive): full recursion support

* feat(recursive): full recursion support
  • Loading branch information
b3ross authored Jun 23, 2019
1 parent f48e595 commit bfccb6a
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 11 deletions.
4 changes: 3 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"type": "node",
"request": "launch",
"args": [
"${relativeFile}"
"${relativeFile}",
"-sstaging",
"-fexample/env.yml"
],
"runtimeArgs": [
"--nolazy",
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ default_env: &default_env
OPTIONAL_VARIABLE: ## Optional variable syntax. Undefined variables will otherwise cause failures
value: ${env:SOME_POSSIBLY_UNDEFINED_VARIABLE}
optional: true
ADVANCED_VALUE: test-${env:SOME_ENV_VARIABLE}

development:
<<: *default_env
Expand All @@ -49,6 +50,16 @@ Then, run `yarn dotenvi -s <stage>` to generate a `.env` file for the stage desi
Note that stages are not required in your yaml file - you can also define it without stages, in which case you should not specify a stage with the `-s` option when you run `dotenvi`.


## Recursion

Dotenvi now supports recursion. You can specify an expression that returns another expression. For example, if the following environment variables are defined:

* `RECURSIVE_OUTER`: `${env:RECURSIVE_MIDDLE}-test`
* `RECURSIVE_MIDDLE`: `foo${env:RECURSIVE_INNER}bar${env:RECURSIVE_INNER}`
* `RECURSIVE_INNER`: `test`

If you evaluate `${env:RECURSIVE_OUTER}`, it will return `footestbartest-test`.

## Configuration

In order to override default configuration, you can supply a `env.js` located next to your `env.yml`. The format of this file is as follows:
Expand Down Expand Up @@ -84,5 +95,3 @@ The reference syntax used in `env.yml` is inspired by [serverless](https://githu
## Possible Future Work

1. Allow for `dotenvi` to replace `dotenv`, if desired, by skipping the `.env`-generation step.
2. Support for references embedded within a configuration value (e.g., `foo-${env:BAR}` --> `foo-bar` if BAR=bar)
3. Support recursive reference calls (e.g., `${env:${env:FOO}}`)
1 change: 1 addition & 0 deletions example/env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ default_env: &default_env
INTEGER_VALUE: 3000
BOOLEAN_TRUE_VALUE: true
BOOLEAN_FALSE_VALUE: false
VALUE_WITH_SURROUNDING_STRINGS: test-${IA_ENV}-test

sandbox:
<<: *default_env
Expand Down
45 changes: 45 additions & 0 deletions src/rewriter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,49 @@ describe('Rewriter', () => {
});
});

it('Rewrites variables with surrounding strings', () => {
process.env['TEST'] = 'hello';
const document = {
'test': {
'value': 'test-${env:TEST}-test'
}
};

const rewriter = new Rewriter({ resolvers: resolvers });
return rewriter.rewrite(document).then((output) => {
expect(output['test']).toBe('test-hello-test');
});
});

it('Handles recursive rewrites', () => {
process.env['RECURSIVE_OUTER'] = '${env:RECURSIVE_INNER}';
process.env['RECURSIVE_INNER'] = 'test';
const document = {
'test': {
'value': '${env:RECURSIVE_OUTER}'
}
};

const rewriter = new Rewriter({ resolvers: resolvers });
return rewriter.rewrite(document).then((output) => {
expect(output['test']).toBe('test');
});
});


it('Handles complex recursive rewrites', () => {
process.env['RECURSIVE_OUTER2'] = '${env:RECURSIVE_MIDDLE2}-test';
process.env['RECURSIVE_MIDDLE2'] = 'foo${env:RECURSIVE_INNER2}bar${env:RECURSIVE_INNER2}'
process.env['RECURSIVE_INNER2'] = 'test';
const document = {
'test': {
'value': '${env:RECURSIVE_OUTER2}'
}
};

const rewriter = new Rewriter({ resolvers: resolvers });
return rewriter.rewrite(document).then((output) => {
expect(output['test']).toBe('footestbartest-test');
});
})
});
36 changes: 28 additions & 8 deletions src/rewriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,35 @@ export class Rewriter {

private async rewriteValue(value: Primitive): Promise<Primitive> {
if (typeof value === 'string') {
const regex = new RegExp('\\${([a-z]+):(.*)}');
const results = value.match(regex);
const resolverName = results && results[1];
const innerValue = results ? results[2] : value;
const resolver = this.getResolver(resolverName);
if (!resolver) {
throw new Error(`Could not locate resolver for value ${value}`);
let result = '';
let capture = '';

for (let i = 0; i < value.length; ++i) {
const c = value.charAt(i);
if (c === '$') {
capture += c;
} else if (c == '}') {
capture += c;
const regex = new RegExp('\\${([a-z]+):(.*)}');
const matchResults = capture.match(regex);
const resolverName = matchResults && matchResults[1];
const resolver = this.getResolver(resolverName);
if (!resolver) {
throw new Error(`Could not locate resolver for value ${value}`);
}
const innerValue = matchResults ? matchResults[2] : value;
let innerResult = await resolver(innerValue, this.config);
if (!innerResult) {
throw new Error(`Resolver ${resolverName} didn't return any value`);
}
result += await this.rewriteValue(innerResult);
capture = '';
} else if (capture) {
capture += c;
} else {
result += c;
}
}
const result = await resolver(innerValue, this.config);
return result;
} else {
return value;
Expand Down

0 comments on commit bfccb6a

Please sign in to comment.