Repository: serverless/serverless (JavaScript)
Date: 2016-10-25 17:07:08+00:00
Task type: Bug Fix

Issue:
Self referenced variables don't allow for fallback
# This is a Bug Report
## Description

For bug reports:
- What went wrong? 

When referencing a `self:` variable the `getDeepValue` function returns an empty object that means it doesn't fallback to the other option. 
- What did you expect should have happened?

It load the fallback value
- What was the config you used?

```
default:
  CorsOrigins: 
    - 'http://test.com'
currentVars:
  CorsOrigins: ${self:custom.${self:custom.currentStage}.CorsOrigins, self:custom.default.CorsOrigins}
  cors:
    origins: ${self:custom.currentVars.CorsOrigins}
```
- What stacktrace or error message from your provider did you see?

```
  Type Error ---------------------------------------------

     cors.origins.join is not a function
```

After changing https://github.com/serverless/serverless/blob/master/lib/classes/Variables.js#L172 to `valueToPopulate = undefined;` the fallback option was loaded correctly. It appears that the `overwrite` function is checking for `undefined` or `null`. https://github.com/serverless/serverless/blob/master/lib/classes/Variables.js#L95
## Additional Data
- **_Serverless Framework Version you're using**_: Master
- **_Operating System**_: Win 10
- **_Stack Trace**_: 
- **_Provider Error messages**_:



Issue discussion:
@andymac4182 thanks for reporting this. This is an issue, but I think the solution is in the overwrite method to also overwrite if object is empty, something like this:

``` js
return (typeof finalValue !== 'undefined' && finalValue !== null) && !_.isEmpty(finalValue);
```

could you try something like that in your PR instead? 😊 

This reverting to empty object when it's undefined "feature" is a fix for an issue reported by @flomotlik a while back that allows us to deeply/recursively check non existing sub properties without throwing `can't read property of undefined` error. In other words, it makes the following overwrite works:

``` yml
custom:
  testA: i am a string not an object
  testB: hello world
  testC: ${self:custom.testA.some.invalid.property, self:custom.testB}
```

Without this reverting to an empty object, we'd be trying to access the `invalid` sub property of an undefined `some` sub property, which of course throws an Error.

Thanks @eahefnawy. I will give that a go. I am still getting my head around the way the variables are implemented. 

@eahefnawy This broke 2 tests. My guess would be to do something like check that it is an object and if so then do `Object.keys().length > 0` to show it has some properties. 

```
  1) Variables #overwrite() should not overwrite 0 values:
     AssertionError: expected 'variableValue' to equal 0
      at Context.<anonymous> (tests/classes/Variables.js:180:34)

  2) Variables #overwrite() should not overwrite false values:
     AssertionError: expected 'variableValue' to equal false
      at Context.<anonymous> (tests/classes/Variables.js:196:34)
```

Edit: I just tried 

``` js
return (typeof finalValue !== 'undefined' && finalValue !== null) && !_.isEqual(finalValue, {});
```

and that seems to be good for the current tests and my project. 


Solution patch:
diff --git a/lib/classes/Variables.js b/lib/classes/Variables.js
index 0ff2b4b82b6..e0c6977c989 100644
--- a/lib/classes/Variables.js
+++ b/lib/classes/Variables.js
@@ -98,7 +98,8 @@ class Variables {
     const variableStringsArray = variableStringsString.split(',');
     variableStringsArray.find(variableString => {
       finalValue = this.getValueFromSource(variableString);
-      return (typeof finalValue !== 'undefined' && finalValue !== null);
+      return (finalValue !== null && typeof finalValue !== 'undefined') &&
+        !(typeof finalValue === 'object' && _.isEmpty(finalValue));
     });
 
     return finalValue;


Test patch:
diff --git a/lib/classes/Variables.test.js b/lib/classes/Variables.test.js
index bfef5ecd2d8..5a18b00edb6 100644
--- a/lib/classes/Variables.test.js
+++ b/lib/classes/Variables.test.js
@@ -194,6 +194,21 @@ describe('Variables', () => {
       serverless.variables.getValueFromSource.restore();
     });
 
+    it('should overwrite empty object values', () => {
+      const serverless = new Serverless();
+      const getValueFromSourceStub = sinon
+        .stub(serverless.variables, 'getValueFromSource');
+
+      getValueFromSourceStub.onCall(0).returns({});
+      getValueFromSourceStub.onCall(1).returns('variableValue');
+
+      const valueToPopulate = serverless.variables
+        .overwrite('opt:stage,env:stage');
+      expect(valueToPopulate).to.equal('variableValue');
+      expect(getValueFromSourceStub.callCount).to.equal(2);
+      serverless.variables.getValueFromSource.restore();
+    });
+
     it('should not overwrite 0 values', () => {
       const serverless = new Serverless();
       const getValueFromSourceStub = sinon