diff --git a/bin/fun.js b/bin/fun.js index 620534bd5..2236ca919 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 a89b03a63..c59aabbf0 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 d13c6137e..83cf93aa9 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 b9a259410..c482e176f 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 19c100ec1..d849ed16d 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 000000000..269ee7c2e --- /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 4636c6db5..72ccc7c86 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 a0f3e028d..07d2028c8 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", diff --git a/test/commands/validate.test.js b/test/commands/validate.test.js new file mode 100644 index 000000000..21412d1aa --- /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