From f22bffc2b49e0badef8a3253478337808222964c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Barthelet?= Date: Mon, 4 May 2020 13:00:07 +0200 Subject: [PATCH] feat(Variables): Support boolean and integer fallbacks (#7632) Additionally improved string related regex --- docs/providers/aws/guide/variables.md | 2 + lib/classes/Variables.js | 23 ++++++++++-- lib/classes/Variables.test.js | 54 +++++++++++++++++++++++++-- 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/docs/providers/aws/guide/variables.md b/docs/providers/aws/guide/variables.md index 6c38984922c..a9cd81c0bed 100644 --- a/docs/providers/aws/guide/variables.md +++ b/docs/providers/aws/guide/variables.md @@ -615,6 +615,8 @@ provider: custom: myStage: ${opt:stage, self:provider.stage} myRegion: ${opt:region, 'us-west-1'} + myCfnRole: ${opt:role, false} + myLambdaMemory: ${opt:memory, 1024} functions: hello: diff --git a/lib/classes/Variables.js b/lib/classes/Variables.js index 9a7994d0c89..ff53732ea2d 100644 --- a/lib/classes/Variables.js +++ b/lib/classes/Variables.js @@ -53,7 +53,9 @@ class Variables { this.envRefSyntax = RegExp(/^env:/g); this.optRefSyntax = RegExp(/^opt:/g); this.selfRefSyntax = RegExp(/^self:/g); - this.stringRefSyntax = RegExp(/(?:('|").*?\1)/g); + this.stringRefSyntax = RegExp(/(?:^('|").*?\1$)/g); + this.boolRefSyntax = RegExp(/(?:^(true|false)$)/g); + this.intRefSyntax = RegExp(/(?:^\d+$)/g); this.s3RefSyntax = RegExp(/^(?:\${)?s3:(.+?)\/(.+)$/); this.cfRefSyntax = RegExp(/^(?:\${)?cf(?:\.([a-zA-Z0-9-]+))?:(.+?)\.(.+)$/); this.ssmRefSyntax = RegExp( @@ -80,6 +82,8 @@ class Variables { serviceName: 'S3', }, { regex: this.stringRefSyntax, resolver: this.getValueFromString.bind(this) }, + { regex: this.boolRefSyntax, resolver: this.getValueFromBool.bind(this) }, + { regex: this.intRefSyntax, resolver: this.getValueFromInt.bind(this) }, { regex: this.ssmRefSyntax, resolver: this.getValueFromSsm.bind(this), @@ -476,15 +480,16 @@ class Variables { * @param string The string to split by comma. */ splitByComma(string) { + const quotedWordSyntax = RegExp(/(?:('|").*?\1)/g); const input = string.trim(); const stringMatches = []; - let match = this.stringRefSyntax.exec(input); + let match = quotedWordSyntax.exec(input); while (match) { stringMatches.push({ start: match.index, - end: this.stringRefSyntax.lastIndex, + end: quotedWordSyntax.lastIndex, }); - match = this.stringRefSyntax.exec(input); + match = quotedWordSyntax.exec(input); } const commaReplacements = []; const contained = ( @@ -621,6 +626,16 @@ class Variables { return BbPromise.resolve(valueToPopulate); } + getValueFromBool(variableString) { + const valueToPopulate = variableString === 'true'; + return BbPromise.resolve(valueToPopulate); + } + + getValueFromInt(variableString) { + const valueToPopulate = parseInt(variableString, 10); + return BbPromise.resolve(valueToPopulate); + } + getValueFromOptions(variableString) { const requestedOption = variableString.split(':')[1]; let valueToPopulate; diff --git a/lib/classes/Variables.test.js b/lib/classes/Variables.test.js index 17b8266029a..f7c97b48cb5 100644 --- a/lib/classes/Variables.test.js +++ b/lib/classes/Variables.test.js @@ -1224,6 +1224,54 @@ module.exports = { .should.eventually.eql('my stage is prod'); }); + it('should not allow partially double-quoted string', () => { + const property = '${opt:stage, prefix"prod"suffix}'; + serverless.variables.options = {}; + const warnIfNotFoundSpy = sinon.spy(serverless.variables, 'warnIfNotFound'); + return serverless.variables + .populateProperty(property) + .should.become(undefined) + .then(() => { + expect(warnIfNotFoundSpy.callCount).to.equal(1); + }) + .finally(() => { + warnIfNotFoundSpy.restore(); + }); + }); + + it('should allow a boolean with value true if overwrite syntax provided', () => { + const property = '${opt:stage, true}'; + serverless.variables.options = {}; + return serverless.variables.populateProperty(property).should.eventually.eql(true); + }); + + it('should allow a boolean with value false if overwrite syntax provided', () => { + const property = '${opt:stage, false}'; + serverless.variables.options = {}; + return serverless.variables.populateProperty(property).should.eventually.eql(false); + }); + + it('should not match a boolean with value containing word true or false if overwrite syntax provided', () => { + const property = '${opt:stage, foofalsebar}'; + serverless.variables.options = {}; + const warnIfNotFoundSpy = sinon.spy(serverless.variables, 'warnIfNotFound'); + return serverless.variables + .populateProperty(property) + .should.become(undefined) + .then(() => { + expect(warnIfNotFoundSpy.callCount).to.equal(1); + }) + .finally(() => { + warnIfNotFoundSpy.restore(); + }); + }); + + it('should allow an integer if overwrite syntax provided', () => { + const property = '${opt:quantity, 123}'; + serverless.variables.options = {}; + return serverless.variables.populateProperty(property).should.eventually.eql(123); + }); + it('should call getValueFromSource if no overwrite syntax provided', () => { // eslint-disable-next-line no-template-curly-in-string const property = 'my stage is ${opt:stage}'; @@ -1520,7 +1568,7 @@ module.exports = { .stub(serverless.variables.variableResolvers[6], 'resolver') .resolves('variableValue'); getValueFromSsmStub = sinon - .stub(serverless.variables.variableResolvers[8], 'resolver') + .stub(serverless.variables.variableResolvers[10], 'resolver') .resolves('variableValue'); }); @@ -1532,7 +1580,7 @@ module.exports = { serverless.variables.variableResolvers[4].resolver.restore(); serverless.variables.variableResolvers[5].resolver.restore(); serverless.variables.variableResolvers[6].resolver.restore(); - serverless.variables.variableResolvers[8].resolver.restore(); + serverless.variables.variableResolvers[10].resolver.restore(); }); it('should call getValueFromSls if referencing sls var', () => @@ -1630,7 +1678,7 @@ module.exports = { variableString: 's3:test-bucket/path/to/ke', }, { - functionIndex: 8, + functionIndex: 10, function: 'getValueFromSsm', variableString: 'ssm:/test/path/to/param', },