Repository: aliyun/fun
Pull request #22 (2018-04-19 12:07:51)

初步实现 fun validate
1. 添加 validate 子命令
2. 使用 json schema 验证 template.yml, 测试覆盖了全部的 example。
3. 不符合 Spec 语法的 template.yml 会报错，但是报错信息不够精确，有待改进。


Patch:
diff --git a/bin/fun.js b/bin/fun.js
index 620534b..2236ca9 100755
--- a/bin/fun.js
+++ b/bin/fun.js
@@ -12,6 +12,7 @@ These are common Fun commands used in various situations:
 
 start a working area
    config      Configure the fun
+   validate    Validate a fun template
    deploy      Deploy a project to AliCloud
    build       Build the dependencies
    help        Print help information
@@ -35,6 +36,8 @@ var handle = function (err) {
 
 if (subcommand === 'config') {
   require('../lib/commands/config')(...args).catch(handle);
+} else if (subcommand === 'validate') {
+  require('../lib/commands/validate')(...args).catch(handle);
 } else if (subcommand === 'deploy') {
   require('../lib/commands/deploy')(...args).catch(handle);
 } else if (subcommand === 'build') {
diff --git a/docs/specs/2018-04-03.md b/docs/specs/2018-04-03.md
index a89b03a..c59aabb 100644
--- a/docs/specs/2018-04-03.md
+++ b/docs/specs/2018-04-03.md
@@ -168,7 +168,6 @@ When the logical ID of this resource is provided to the [Ref build-in function](
 ##### Example: Aliyun::Serverless::Api
 
 ```yaml
-StageName: prod
 DefinitionUri: swagger.yml
 ```
 
diff --git a/examples/datahub/template.yml b/examples/datahub/template.yml
index d13c613..83cf93a 100644
--- a/examples/datahub/template.yml
+++ b/examples/datahub/template.yml
@@ -7,7 +7,7 @@ Resources:
       Type: 'Aliyun::Serverless::Function'
       Properties:
         Handler: datahub.index
-        Runtime: nodejs6.10
+        Runtime: nodejs6
         CodeUri: './'
       Events:
         DhsTrigger:
diff --git a/examples/segment/template.yml b/examples/segment/template.yml
index b9a2594..c482e17 100644
--- a/examples/segment/template.yml
+++ b/examples/segment/template.yml
@@ -11,6 +11,7 @@ Resources:
         Handler: index.doSegment
         CodeUri: './'
         Description: 'do segment'
+        Runtime: nodejs8
       Events:
         GetApi:
           Type: API
diff --git a/examples/timer/template.yml b/examples/timer/template.yml
index 19c100e..d849ed1 100644
--- a/examples/timer/template.yml
+++ b/examples/timer/template.yml
@@ -7,7 +7,7 @@ Resources:
       Type: 'Aliyun::Serverless::Function'
       Properties:
         Handler: index.handler
-        Runtime: nodejs6.10
+        Runtime: nodejs8
         Description: 'send hangzhou weather'
         CodeUri: './'
       Events:
diff --git a/lib/commands/validate.js b/lib/commands/validate.js
new file mode 100644
index 0000000..269ee7c
--- /dev/null
+++ b/lib/commands/validate.js
@@ -0,0 +1,301 @@
+'use strict';
+
+const fs = require('fs');
+const util = require('util');
+
+const yaml = require('js-yaml');
+const Ajv = require('ajv');
+
+const exists = util.promisify(fs.exists);
+const readFile = util.promisify(fs.readFile);
+
+/**
+ * JSON Schema for template.yml
+ * http://json-schema.org/
+ */
+const rosSchema = {
+  'title': 'fun template',
+  'type': 'object',
+  'properties': {
+    'ROSTemplateFormatVersion': {
+      'type': 'string',
+      'enum': ['2015-09-01']
+    },
+    'Transform': {
+      'type': 'string',
+      'enum': ['Aliyun::Serverless-2018-04-03']
+    },
+    'Resources': {
+      'type': 'object',
+      'patternProperties': {
+        '^[a-zA-Z_][a-zA-Z0-9_-]{0,127}$': {
+          anyOf: [
+            {'$ref': '/Resources/Service'},
+            {'$ref': '/Resources/Api'}
+          ]         
+        }
+      }
+    }
+  },
+  'required': ['ROSTemplateFormatVersion', 'Resources']
+};
+const serviceResourceSchema = {
+  'type': 'object',
+  'description': 'Service',
+  'properties': {
+    'Type': {
+      'type': 'string',
+      'const': 'Aliyun::Serverless::Service'
+    },
+    'Properties': {
+      'type': 'object',
+      'properties': {
+        'Description': {
+          'type': 'string'
+        }
+      }
+    }
+  },
+  'patternProperties': {
+    '^(?!Type|Properties)[a-zA-Z_][a-zA-Z0-9_-]{0,127}$': {
+      '$ref': '/Resources/Service/Function'
+    }
+  },
+  'required': ['Type']
+};
+
+const functionSchema = {
+  'type': 'object',
+  'description': 'Function',
+  'properties': {
+    'Type': {
+      'type': 'string',
+      'const': 'Aliyun::Serverless::Function'
+    },
+    'Properties': {
+      'type': 'object',
+      'properties': {
+        'Handler': {
+          'type': 'string'
+        },
+        'Runtime': {
+          'type': 'string',
+          'enum': ['nodejs6', 'nodejs8', 'python2.7', 'python3', 'java8'],
+        },
+        'CodeUri': {
+          'type': 'string'
+        },
+        'Description': {
+          'type': 'string'
+        }
+      }
+    },
+    'Events': {
+      'type': 'object',
+      'patternProperties': {
+        '^[a-zA-Z_][a-zA-Z0-9_-]{0,127}$': {
+          'anyOf': [
+            { '$ref': '/Resources/Service/Function/Events/Datahub' },
+            { '$ref': '/Resources/Service/Function/Events/API' },
+            { '$ref': '/Resources/Service/Function/Events/OTS' },
+            { '$ref': '/Resources/Service/Function/Events/Timer' },
+          ]
+
+        }
+      }
+    },
+  },
+  'required': ['Type']
+};
+
+const datahubEventSchema = {
+  'type': 'object',
+  'properties': {
+    'Type': {
+      'type': 'string',
+      'const': 'Datahub'
+    },
+    'Properties': {
+      'type': 'object',
+      'properties': {
+        'Topic': {
+          'type': 'string'
+        },
+        'StartingPosition': {
+          'type': 'string',
+          'enum': ['LATEST', 'OLDEST', 'SYSTEM_TIME']
+        },
+        'BatchSize': {
+          'type': 'integer',
+          'minimum': 1
+        }
+      },
+      'required': ['Topic', 'StartingPosition']
+    }
+  }
+};
+
+const apiEventSchema = {
+  'type': 'object',
+  'properties': {
+    'Type': {
+      'type': 'string',
+      'const': 'API'
+    },
+    'Properties': {
+      'type': 'object',
+      'properties': {
+        'Path': {
+          'type': 'string'
+        },
+        'Method': {
+          'type': 'string',
+          'enum': ['get', 'head', 'post', 'put', 'delete', 'connect', 'options', 'trace', 'patch',
+            'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH',
+            'Get', 'Head', 'Post', 'Put', 'Delete', 'Connect', 'Options', 'Trace', 'Patch'
+          ]
+        },
+        'RestApiId': {
+          'oneOf': [
+            { 'type': 'string' },
+            {
+              'type': 'object',
+              'properties': {
+                'Ref': { 'type': 'string' }
+              }
+            }
+          ]
+
+        }
+      },
+      'required': ['Path', 'Method']
+    }
+  }
+};
+
+const apiRescoureSchema = {
+  'type': 'object',
+  'description': 'API',
+  'properties': {
+    'Type': {
+      'type': 'string',
+      'const': 'Aliyun::Serverless::Api'
+    },
+    'Properties': {
+      'type': 'object',
+      'properties': {
+        'Name': {
+          'type': 'string'
+        },
+        'DefinitionBody': {
+          'type': 'object'
+        },
+        'DefinitionUri': {
+          'type': 'string'
+        },
+        'Cors': {
+          oneOf: [
+            {'type': 'string'},
+            {'$ref': '/CORS'}
+          ]
+          
+        }
+      }
+    }
+  },
+  'required': ['Type']
+};
+
+const corsSchema = {
+  'type': 'object',
+  'properties': {
+    'AllowMethods':{
+      'type': 'string'
+    },
+    'AllowHeaders':{
+      'type': 'string'
+    },
+    'AllowOrigin':{
+      'type': 'string'
+    },
+    'MaxAge':{
+      'type': 'string'
+    }
+  },
+  'required': ['AllowOrigin']
+};
+
+const otsEventSchema = {
+  'type': 'object',
+  'properties': {
+    'Type': {
+      'type': 'string',
+      'const': 'OTS'
+    },
+    'Properties': {
+      'type': 'object',
+      'properties': {
+        'Stream': {
+          'type': 'string'
+        }
+      },
+      'required': ['Stream']
+    }
+  }
+};
+
+const timerEventSchema = {
+  'type': 'object',
+  'properties': {
+    'Type': {
+      'type': 'string',
+      'const': 'Timer'
+    },
+    'Properties': {
+      'type': 'object',
+      'properties': {
+        'Payload': {
+          'type': 'string'
+        },
+        'CronExpression': {
+          'type': 'string'
+        },
+        'Enable': {
+          'type': 'boolean'
+        },
+      },
+      'required': ['CronExpression']
+    }
+  }
+};
+
+
+async function validate() {
+  if (!(await exists('template.yml'))) {
+    console.error('Can\'t found template.yml in current dir.');
+    return;
+  }
+
+  const tplContent = await readFile('template.yml', 'utf8');
+  const tpl = yaml.safeLoad(tplContent);
+  const ajv = new Ajv();
+  const valid = ajv.addSchema(datahubEventSchema, '/Resources/Service/Function/Events/Datahub')
+    .addSchema(apiEventSchema, '/Resources/Service/Function/Events/API')
+    .addSchema(otsEventSchema, '/Resources/Service/Function/Events/OTS')
+    .addSchema(timerEventSchema, '/Resources/Service/Function/Events/Timer')
+    .addSchema(functionSchema, '/Resources/Service/Function')
+    .addSchema(serviceResourceSchema, '/Resources/Service')
+    .addSchema(apiRescoureSchema, '/Resources/Api')
+    .addSchema(corsSchema, '/CORS')
+    .validate(rosSchema, tpl);
+
+
+  if (valid) {
+    console.log(JSON.stringify(tpl, null, 2));
+  } else {
+    console.error(JSON.stringify(ajv.errorsText(), null, 2));
+  }
+  return valid;
+}
+
+module.exports = validate;
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 4636c6d..72ccc7c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -65,15 +65,14 @@
       }
     },
     "ajv": {
-      "version": "5.2.2",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.2.tgz",
-      "integrity": "sha1-R8aNaehvXZUxA7AHSpQw3GPaXjk=",
-      "dev": true,
+      "version": "6.4.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz",
+      "integrity": "sha1-06/3jpJ3VJdx2vAWTP9ISCt1T8Y=",
       "requires": {
-        "co": "4.6.0",
-        "fast-deep-equal": "1.0.0",
+        "fast-deep-equal": "1.1.0",
+        "fast-json-stable-stringify": "2.0.0",
         "json-schema-traverse": "0.3.1",
-        "json-stable-stringify": "1.0.1"
+        "uri-js": "3.0.2"
       }
     },
     "ajv-keywords": {
@@ -435,7 +434,7 @@
       "integrity": "sha1-/NfJY3a780yF7mftABKimWQrEI8=",
       "dev": true,
       "requires": {
-        "ajv": "5.2.2",
+        "ajv": "5.5.2",
         "babel-code-frame": "6.22.0",
         "chalk": "1.1.3",
         "concat-stream": "1.6.0",
@@ -473,6 +472,18 @@
         "text-table": "0.2.0"
       },
       "dependencies": {
+        "ajv": {
+          "version": "5.5.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+          "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+          "dev": true,
+          "requires": {
+            "co": "4.6.0",
+            "fast-deep-equal": "1.1.0",
+            "fast-json-stable-stringify": "2.0.0",
+            "json-schema-traverse": "0.3.1"
+          }
+        },
         "ansi-regex": {
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
@@ -617,10 +628,14 @@
       }
     },
     "fast-deep-equal": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz",
-      "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=",
-      "dev": true
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
+      "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ="
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
     },
     "fast-levenshtein": {
       "version": "2.0.6",
@@ -934,8 +949,7 @@
     "json-schema-traverse": {
       "version": "0.3.1",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
-      "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
-      "dev": true
+      "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A="
     },
     "json-stable-stringify": {
       "version": "1.0.1",
@@ -1323,6 +1337,11 @@
       "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
       "dev": true
     },
+    "punycode": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz",
+      "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0="
+    },
     "readable-stream": {
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
@@ -1594,6 +1613,14 @@
       "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
       "dev": true
     },
+    "uri-js": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz",
+      "integrity": "sha1-+QuFhQf4HepNz7s8TD2/orVX+qo=",
+      "requires": {
+        "punycode": "2.1.0"
+      }
+    },
     "util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/package.json b/package.json
index 2590a7d..f9cdad7 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
     "@alicloud/cloudapi": "^1.0.0",
     "@alicloud/fc": "^1.1.2",
     "@alicloud/ram": "^1.0.0",
+    "ajv": "^6.4.0",
     "archiver": "^2.0.0",
     "colors": "^1.1.2",
     "debug": "^2.6.8",


Test patch:
diff --git a/test/commands/validate.test.js b/test/commands/validate.test.js
new file mode 100644
index 0000000..21412d1
--- /dev/null
+++ b/test/commands/validate.test.js
@@ -0,0 +1,61 @@
+'use strict';
+
+const expect = require('expect.js');
+
+const validate = require('../../lib/commands/validate');
+
+describe('validate template', () => {
+  var prevCWD;
+  beforeEach(async () => {
+    prevCWD = process.cwd();
+  });
+  afterEach(async () => {
+    process.chdir(prevCWD);
+  });
+
+  it('validate datahub example', async () => {
+    process.chdir('./examples/datahub/');
+    expect(await validate()).to.be(true);
+  });
+
+  it('validate helloworld example', async () => {
+    process.chdir('./examples/helloworld/');
+    expect(await validate()).to.be(true);
+  });
+
+  it('validate java example', async () => {
+    process.chdir('./examples/java/');
+    expect(await validate()).to.be(true);
+  });
+
+  it('validate openid_connect example', async () => {
+    process.chdir('./examples/openid_connect/');
+    expect(await validate()).to.be(true);
+  });
+
+  it('validate ots_stream example', async () => {
+    process.chdir('./examples/ots_stream/');
+    expect(await validate()).to.be(true);
+  });
+
+  it('validate python example', async () => {
+    process.chdir('./examples/python/');
+    expect(await validate()).to.be(true);
+  });
+
+  it('validate segment example', async () => {
+    process.chdir('./examples/segment/');
+    expect(await validate()).to.be(true);
+  });
+
+  it('validate timer example', async () => {
+    process.chdir('./examples/timer/');
+    expect(await validate()).to.be(true);
+  });
+
+  it('validate wechat example', async () => {
+    process.chdir('./examples/wechat/');
+    expect(await validate()).to.be(true);
+  });
+
+});
\ No newline at end of file