Repository: serverless/serverless (JavaScript)
Date: 2017-04-26 13:49:42+00:00
Task type: Feature

Issue:
Allow for service property to be an object
# This is a Feature Proposal

## Description

Allow for the the `service` property in `serverless.yml` to be an object instead of just a string. This way we can add additional service specific properties such as `version`, `license`, `description`, `email`, `repository`, etc.

```yaml
service:
  name: 'my-service'
  version: 1.0.0
  license: 'MIT'
  description: 'My Awesome Service'
  email: 'project@hostname.com'
  repository: 'https://github.com/serverless/serverless.git'
```


Issue discussion:
This could be a breaking change as all provider plugins have to implement a fallback mechanism to set the service name from "service" in case it is a string type.
Currently the service name is referenced directly in the implementation. Here's an example from provider.aws.naming:
```
getStackName() {
    return `${this.provider.serverless.service.service}-${this.provider.getStage()}`;
  },
```
I propose to add "BREAKING" to the issue subject, as it needs modifications in all providers and yaml files created with objects won't work with older serverless versions too. Additionally 3rd party plugins might cease to function correctly with service specified as object.

### Alternative solution
Maybe a different solution would be better: If we do not modify the "service" property, but invent a new `metadata` property in the YAML, that can be set with arbitrary properties, that do not affect the current implementation but can be referenced or used by plugins more easily.
@HyperBrain thanks for chiming in!

Isn't it possible to intercept this at the YAML parsing level (here: https://github.com/serverless/serverless/blob/master/lib/classes/Service.js) so that it's a string by default but will be an object if the user decides to define it like that.

This way nothing should break if the user does not use the object syntax.

---

**BTW:** The plan is to support both. Simple string representation but also object representation.

We had the same with the `provider` property when we started with the first sketches for Serverless v1. It started with a string representation of the provider name and was expanded into an object representation later on.

Now the object representation is the default.
Yes, agree. This can (and should) be handled centrally in the service YAML parser. The service name (which currently is stored in `service.service`) could be left as is (to keep compatibility). I.e. if the parser encounters an object typed service property it would just set `this.service = serverlessFile.service.name` and store the object additionally with `this.serviceObject = serverlessFile.service`. In case service is a string, the parser should set `serviceObject`just to `{ name: serverlessFile.service}`. A new method in the Service object `getServiceObject()` could expose the service as object.
With this approach nothing would break and plugins can switch to `getServiceObject()` one after each other, without the need to adapt their implementations right now.

The only mandatory property for the object typed version would be `name` as it is necessary for things to work. Everything else can be added arbitrarily.

@pmuens If you agree I can prepare a PR for this in the very near future... Should be quite easy.
@HyperBrain Thanks for the implementation proposal! Sounds reasoneable 👌 

> @pmuens If you agree I can prepare a PR for this in the very near future... Should be quite easy.

That would be super nice! Looking forward to it. Let us know if you need any help with that!

Solution patch:
diff --git a/lib/classes/Service.js b/lib/classes/Service.js
index 6d9d7d11615..eeba0a5b6bb 100644
--- a/lib/classes/Service.js
+++ b/lib/classes/Service.js
@@ -13,6 +13,7 @@ class Service {
 
     // Default properties
     this.service = null;
+    this.serviceObject = null;
     this.provider = {
       stage: 'dev',
       region: 'us-east-1',
@@ -64,6 +65,9 @@ class Service {
         if (!serverlessFile.service) {
           throw new ServerlessError('"service" property is missing in serverless.yml');
         }
+        if (_.isObject(serverlessFile.service) && !serverlessFile.service.name) {
+          throw new ServerlessError('"service" is missing the "name" property in serverless.yml');
+        }
         if (!serverlessFile.provider) {
           throw new ServerlessError('"provider" property is missing in serverless.yml');
         }
@@ -91,7 +95,13 @@ class Service {
           , {});
         }
 
-        that.service = serverlessFile.service;
+        if (_.isObject(serverlessFile.service)) {
+          that.serviceObject = serverlessFile.service;
+          that.service = serverlessFile.service.name;
+        } else {
+          that.serviceObject = { name: serverlessFile.service };
+          that.service = serverlessFile.service;
+        }
         that.custom = serverlessFile.custom;
         that.plugins = serverlessFile.plugins;
         that.resources = serverlessFile.resources;
@@ -148,6 +158,14 @@ class Service {
     return _.merge(this, data);
   }
 
+  getServiceName() {
+    return this.serviceObject.name;
+  }
+
+  getServiceObject() {
+    return this.serviceObject;
+  }
+
   getAllFunctions() {
     return Object.keys(this.functions);
   }


Test patch:
diff --git a/lib/classes/Service.test.js b/lib/classes/Service.test.js
index 94163ef5021..0493cbf7824 100644
--- a/lib/classes/Service.test.js
+++ b/lib/classes/Service.test.js
@@ -2,6 +2,7 @@
 
 const path = require('path');
 const YAML = require('js-yaml');
+const _ = require('lodash');
 const expect = require('chai').expect;
 const sinon = require('sinon');
 const Service = require('../../lib/classes/Service');
@@ -22,6 +23,7 @@ describe('Service', () => {
       const serviceInstance = new Service(serverless);
 
       expect(serviceInstance.service).to.be.equal(null);
+      expect(serviceInstance.serviceObject).to.be.equal(null);
       expect(serviceInstance.provider).to.deep.equal({
         stage: 'dev',
         region: 'us-east-1',
@@ -199,6 +201,52 @@ describe('Service', () => {
       });
     });
 
+    it('should fail when the service name is missing', () => {
+      const SUtils = new Utils();
+      const serverlessYaml = {
+        service: {},
+        provider: 'aws',
+      };
+
+      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'),
+        YAML.dump(serverlessYaml));
+
+      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');
+      });
+    });
+
+    it('should support service objects', () => {
+      const SUtils = new Utils();
+      const serverlessYaml = {
+        service: {
+          name: 'my-service',
+          foo: 'bar',
+        },
+        provider: 'aws',
+      };
+
+      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'),
+        YAML.dump(serverlessYaml));
+
+      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(serviceInstance.service).to.equal('my-service');
+        expect(serviceInstance.serviceObject).to.deep.equal(serverlessYaml.service);
+      });
+    });
+
     it('should support Serverless file with a non-aws provider', () => {
       const SUtils = new Utils();
       const serverlessYaml = {
@@ -526,6 +574,37 @@ describe('Service', () => {
     });
   });
 
+  describe('#getServiceName()', () => {
+    it('should return the service name', () => {
+      const serverless = new Serverless();
+      const serviceInstance = new Service(serverless);
+      serviceInstance.serviceObject = {
+        name: 'my-service',
+      };
+
+      const serviceName = serviceInstance.getServiceName();
+
+      expect(serviceName).to.equal('my-service');
+    });
+  });
+
+  describe('#getServiceObject()', () => {
+    it('should return the service object with all properties', () => {
+      const serverless = new Serverless();
+      const serviceInstance = new Service(serverless);
+      const testObject = {
+        name: 'my-service',
+        foo: 'bar',
+      };
+      // Use a clone here to check for implicit reference errors
+      serviceInstance.serviceObject = _.cloneDeep(testObject);
+
+      const serviceObject = serviceInstance.getServiceObject();
+
+      expect(serviceObject).to.deep.equal(testObject);
+    });
+  });
+
   describe('#getFunction()', () => {
     let serviceInstance;
     before(() => {