Repository: serverless/serverless (JavaScript)
Date: 2020-04-28 20:48:56+00:00
Task type: Bug Fix

Issue:
Variables: Lack of support for (non string) default plain values
Ex:
```
service:
  name: test
# app and org for use with dashboard.serverless.com
#app: your-app-name
#org: your-org-name

# Add the serverless-webpack plugin
plugins:
  - serverless-webpack

provider:
  name: aws
  runtime: nodejs10.x

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          method: get
          path: hello
          enabled: ${self:custom.doesnotexist, false}
```

Using `sls print` I get:
```
service:
  name: test
plugins:
  - serverless-webpack
provider:
  name: aws
  runtime: nodejs10.x
functions:
  hello:
    handler: handler.hello
    events:
      - http:
          method: get
          path: hello
    name: test-dev-hello
```

**Note**: the `enabled` key is missing entirely

This happens for custom values and keys other than `enabled` as well.

Seems to be related to https://github.com/serverless/serverless/pull/3891

Framework Core: 1.66.0
Plugin: 3.5.0
SDK: 2.3.0
Components: 2.22.3


Issue discussion:
My workaround for now:

```
service:
  name: test
# app and org for use with dashboard.serverless.com
#app: your-app-name
#org: your-org-name

# Add the serverless-webpack plugin
plugins:
  - serverless-webpack

provider:
  name: aws
  runtime: nodejs10.x

custom:
  false: false

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          method: get
          path: hello
          enabled: ${self:custom.doesnotexist, self:custom.false}
```
@jbsulli thanks for report. I think in general _plain_ default values are not supported (it doesn't seem specific to `false`).

I agree it's a weird characteristics, and it'll definitely be great to improve that. PR is welcome!

(@erikerikson  you may have a better insight on why it's the case).
I think the exception to that is that the quoted string type added in [#4097](https://github.com/serverless/serverless/pull/4097).  I think its simply a matter of a narrow change and no one ever expanding upon or generalizing it.
@jbsulli you can use `strToBool` method in your overwrite parameter (see https://serverless.com/framework/docs/providers/aws/guide/variables#read-string-variable-values-as-boolean-values).

In your example, this would be something like :
```yml
enabled: ${self:custom.doesnotexist, strToBool(false)}
```

@medikoo @erikerikson not sure this was what you meant for non-string overwrite, did you had another idea on this subject ?

Made a small PR anyway with additional doc and a test providing proof this work.
Not quite what I was referencing @fredericbarthelet but a good expansion of the docs and testing of the capability.  The changes in #4097 added identification of a quoted string but no other basic types (e.g. booleans or integers) so that only known variables are accepted.  An expansion of the capability might identify other basic types to allow them.  So, by narrow, I meant that the changes in #4097 added strings but no other non-variable type arguments to fallbacks.
> @medikoo @erikerikson not sure this was what you meant for non-string overwrite, did you had another idea on this subject ?

I think `false` and other common primitives should be supported directly, but indeed until it's the case, the `strToBool(false)` can be used as workaround (for boolean case)

@medikoo @erikerikson thanks for your feedback. I'll close my PR and work on this more definitive solution of common primitives native support :)
Thank you @fredericbarthelet !

Solution patch:
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;


Test patch:
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',
         },