Repository: serverless/serverless (JavaScript)
Date: 2016-10-18 11:05:31+00:00
Task type: Bug Fix

Issue:
Function event configuration can't be moved to a separate file anymore
# This is a Bug Report
## Description

We used to be able to specify a "file variable" for function event configuration, which was quite useful for a service with multiple handlers. This allowed each handler to be in their own directory, along with the event configuration. This looked something like this:

```
functions:
  users:
    handler: handlers/users/handler.users
    events: ${file(./handlers/users/config.yml):events}
```

The `config.yml` file would contain a valid event configuration.

I'm not sure exactly when this started but using the same configuration I can no longer use variables for event configurations and instead get the following error:

`Events for "users" must be an array, not an string`

The event configuration should have loaded properly as it used to
## Workaround

If I comment out the following lines from the `./lib/classes/Service.js` file everything works as expected, but I'm not sure of the impact of removing those lines. They must be there for a reason:

```
if (!_.isArray(functionObj.events)) {
   throw new SError(`Events for "${functionName}" must be an array,` +
                    ` not an ${typeof functionObj.events}`);
 }
```

I'd be happy to submit a PR to fix this, but I'm not sure what the best approach would be. Removing those lines would work, but then this opens the door for a bunch of validation issues and potential errors.

What do you think?
## Additional Data
- **_Serverless Framework Version you're using**_: 1.0.2
- **_Operating System**_: Windows 10
- **_Stack Trace**_: N/A
- **_Provider Error messages**_: N/A



Issue discussion:
Actually, on second thought, I could just change the check to look for an array OR a string using the variable syntax. That should be relatively easy given the variable syntax pattern is available in Service.js

I'll give that a crack when I get back from work


Solution patch:
diff --git a/lib/Serverless.js b/lib/Serverless.js
index 61675bad987..376cd871965 100644
--- a/lib/Serverless.js
+++ b/lib/Serverless.js
@@ -84,6 +84,9 @@ class Serverless {
     // (https://github.com/serverless/serverless/issues/2041)
     this.variables.populateService(this.pluginManager.cliOptions);
 
+    // validate the service configuration, now that variables are loaded
+    this.service.validate();
+
     // trigger the plugin lifecycle when there's something which should be processed
     return this.pluginManager.run(this.processedInput.commands);
   }
diff --git a/lib/classes/Service.js b/lib/classes/Service.js
index 9cff995f22b..411dd30db79 100644
--- a/lib/classes/Service.js
+++ b/lib/classes/Service.js
@@ -152,10 +152,6 @@ class Service {
           if (!functionObj.events) {
             that.functions[functionName].events = [];
           }
-          if (!_.isArray(functionObj.events)) {
-            throw new SError(`Events for "${functionName}" must be an array,` +
-                             ` not an ${typeof functionObj.events}`);
-          }
 
           if (!functionObj.name) {
             that.functions[functionName].name =
@@ -167,6 +163,17 @@ class Service {
       });
   }
 
+  validate() {
+    _.forEach(this.functions, (functionObj, functionName) => {
+      if (!_.isArray(functionObj.events)) {
+        throw new SError(`Events for "${functionName}" must be an array,` +
+                          ` not an ${typeof functionObj.events}`);
+      }
+    });
+
+    return this;
+  }
+
   update(data) {
     return _.merge(this, data);
   }


Test patch:
diff --git a/lib/classes/Service.test.js b/lib/classes/Service.test.js
index 2a9ce568d60..a5844275313 100644
--- a/lib/classes/Service.test.js
+++ b/lib/classes/Service.test.js
@@ -392,21 +392,32 @@ describe('Service', () => {
         YAML.dump(serverlessYml));
 
       const serverless = new Serverless({ servicePath: tmpDirPath });
-      serviceInstance = new Service(serverless);
+      serverless.service = new Service(serverless);
+      serverless.variables.service = serverless.service;
 
-      return serviceInstance.load().then(() => {
-        expect(serverless.service.functions).to.deep.equal({});
+      return serverless.service.load().then(() => {
+        // if we reach this, then no error was thrown
+        // populate variables in service configuration
+        serverless.variables.populateService();
+
+        // validate the service configuration, now that variables are loaded
+        serviceInstance.validate();
+
+        expect(serviceInstance.functions.functionA.events).to.deep.equal({});
+      }).catch(() => {
+        // make assertion fail intentionally to let us know something is wrong
+        expect(1).to.equal(2);
       });
     });
 
-    it("should throw error if a function's event is not an array", () => {
+    it('should throw error if a function\'s event is not an array or a variable', () => {
       const SUtils = new Utils();
       const serverlessYml = {
         service: 'service-name',
         provider: 'aws',
         functions: {
           functionA: {
-            events: {},
+            events: 'not an array or a variable',
           },
         },
       };
@@ -414,14 +425,19 @@ describe('Service', () => {
         YAML.dump(serverlessYml));
 
       const serverless = new Serverless({ servicePath: tmpDirPath });
-      serviceInstance = new Service(serverless);
-
-      return serviceInstance.load().then(() => {
-        // if we reach this, then no error was thrown as expected
-        // so make assertion fail intentionally to let us know something is wrong
-        expect(1).to.equal(2);
-      }).catch(e => {
-        expect(e.name).to.be.equal('ServerlessError');
+      serverless.service = new Service(serverless);
+
+      return serverless.service.load().then(() => {
+        // validate the service configuration, now that variables are loaded
+        try {
+          serverless.service.validate();
+
+          // if we reach this, then no error was thrown as expected
+          // so make assertion fail intentionally to let us know something is wrong
+          expect(1).to.equal(2);
+        } catch (e) {
+          expect(e.name).to.be.equal('ServerlessError');
+        }
       });
     });