├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── package.json ├── src └── index.js └── test ├── index.js └── support └── ServerlessBuilder.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | install: 4 | - travis_retry npm install 5 | 6 | node_js: 7 | - '4.4' 8 | - '5.11' 9 | - '6.2' 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.2.0 (12.06.2016) 2 | 3 | ## Features 4 | * [Unit tests](https://github.com/andrewcurioso/raml-serverless/issues/2) (#2) 5 | * [Populate baseUri](https://github.com/andrewcurioso/raml-serverless/issues/3) (#3) 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 nfour 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RAML-Serverless Plugin 2 | 3 | [![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) 4 | [![npm version](https://badge.fury.io/js/raml-serverless.svg)](https://badge.fury.io/js/raml-serverless) 5 | [![Build Status](https://travis-ci.org/andrewcurioso/raml-serverless.svg?branch=master)](https://travis-ci.org/andrewcurioso/raml-serverless) 6 | 7 | Work with [RAML](http://raml.org/) documentation for [Serverless v1.0](https://serverless.com/) projects. 8 | 9 | Currently it can be used to generate RAML documentation. Future versions will be able to deploy RAML documentents, stub API endpoints based on the RAML and even generate/update serverless.yml files from RAML specifications. For more information, take a look at the [Feature Roadmap](https://github.com/andrewcurioso/raml-serverless/milestones). 10 | 11 | ## Installation 12 | 1. Open a terminal to your Serverless project 13 | 2. `npm install --save raml-serverless` 14 | 3. Add `raml-serverless` to your `serverless.yml` file (see [Serverless docs](https://serverless.com/framework/docs/providers/aws/guide/plugins/#installing-plugins)) 15 | 16 | ## Usage 17 | 18 | Example usage: 19 | 20 | ```bash 21 | sls raml > docs.raml 22 | ``` 23 | 24 | RAML-Serverless will automatically create a section of the documentation for each HTTP endpoint you have in your `serverless.yml` file. 25 | 26 | You can put global documentation in the `custom:` object in your Yaml file and it will be copied as is into the output RAML. Anything that can go into a RAML file can go here. For example: 27 | 28 | ```yaml 29 | custom: 30 | documentation: 31 | raml: 32 | title: My Awesome API 33 | version: v1.0 34 | ``` 35 | 36 | Which will result in a RAML file that starts with: 37 | 38 | ```yaml 39 | #%RAML 1.0 40 | title: My Awsome API 41 | version: v1.0 42 | ``` 43 | 44 | You can also put RAML on individual HTTP event endpoints and they will be included in the output. For example: 45 | 46 | ```yaml 47 | functions: 48 | index: 49 | handler: handlers.index 50 | events: 51 | - http: 52 | path: /hello/world 53 | method: get 54 | cors: true 55 | documentation: 56 | raml: 57 | description: Say hello to the world 58 | ``` 59 | 60 | Will produce this output in your RAML file: 61 | ```yaml 62 | /hello: 63 | /world: 64 | get: 65 | description: Say hello to the world 66 | ``` 67 | 68 | You can also include your RAML in a seperate file and import it into your `serverless.yml` using variables: 69 | 70 | ```yaml 71 | custom: 72 | documentation: 73 | raml: ${file(raml-base.yml)} 74 | ``` 75 | 76 | ## Contributing 77 | 78 | This plugin is a work in progress. If you would like to contribute, go to Github issues ([/andrewcurioso/raml-serverless/issues](https://github.com/andrewcurioso/raml-serverless/issues)) and pick an issue to work on or create a new issue. 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "raml-serverless", 3 | "version": "0.2.0", 4 | "description": "Generate RAML documents from Serverless projects", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "mocha test" 8 | }, 9 | "engines": { 10 | "node": ">=4.0" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/andrewcurioso/raml-serverless.git" 15 | }, 16 | "keywords": [ 17 | "raml", 18 | "serverless", 19 | "plugin" 20 | ], 21 | "author": "Andrew Curioso", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/andrewcurioso/raml-serverless/issues" 25 | }, 26 | "homepage": "https://github.com/andrewcurioso/raml-serverless#readme", 27 | "dependencies": { 28 | "js-yaml": "^3.7.0" 29 | }, 30 | "devDependencies": { 31 | "chai": "^3.5.0", 32 | "chai-as-promised": "^6.0.0", 33 | "lodash": "^4.17.2", 34 | "mocha": "^3.2.0", 35 | "sinon": "^1.17.6", 36 | "sinon-chai": "^2.8.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var yaml = require('js-yaml'); 4 | 5 | class Raml { 6 | constructor(serverless, options) { 7 | this.serverless = serverless; 8 | this.options = options; 9 | 10 | /* Note: this will not work once non-AWS providers are added */ 11 | this.provider = this.serverless.getProvider('aws'); 12 | 13 | this.commands = { 14 | raml: { 15 | lifecycleEvents: [ 16 | 'serverless' 17 | ], 18 | }, 19 | }; 20 | 21 | this.hooks = { 22 | 'raml:serverless': this.ramlCommand.bind(this) 23 | } 24 | } 25 | 26 | ramlCommand() { 27 | this.getEndpointAsync() 28 | .then((endpoint) => console.log(this.getRaml(endpoint))); 29 | } 30 | 31 | getEndpointAsync() { 32 | const stackName = this.provider.naming.getStackName(this.options.stage); 33 | 34 | return this.provider.request('CloudFormation', 35 | 'describeStacks', 36 | { StackName: stackName }, 37 | this.options.stage, 38 | this.options.region) 39 | 40 | .then((result) => { 41 | let outputs; 42 | if ( result && result.Stacks.length ) { 43 | outputs = result.Stacks[0].Outputs; 44 | const serviceEndpointOutputRegex = this.provider.naming.getServiceEndpointRegex(); 45 | return outputs.filter(x => x.OutputKey.match(serviceEndpointOutputRegex)).reduce(x => x); 46 | } 47 | 48 | }) 49 | 50 | .then((endpoint) => { 51 | return endpoint && endpoint.OutputValue; 52 | }) 53 | 54 | .catch((e) => { 55 | return null; 56 | }); 57 | 58 | } 59 | 60 | getRaml(endpoint) { 61 | 62 | var service = this.serverless.service; 63 | var docs = service.custom && service.custom.documentation && service.custom.documentation.raml; 64 | 65 | var spec = docs || {}; 66 | 67 | !spec.protocols && (spec.protocols = [ 'HTTPS' ]); 68 | !spec.mediaType && (spec.mediaType = 'application/json' ); 69 | !spec.baseUri && endpoint && (spec.baseUri = endpoint); 70 | 71 | service.getAllFunctions().map((f) => { 72 | var events = service.getFunction(f).events; 73 | 74 | return events 75 | ? events 76 | .filter((e) => !!e.http) 77 | .map((e) => { 78 | return { 79 | path : e.http.path, 80 | method : e.http.method, 81 | split_path : e.http.path.split('/'), 82 | docs : e.http.documentation && e.http.documentation.raml, 83 | }; 84 | }) 85 | : []; 86 | 87 | }) 88 | 89 | .reduce((a, b) => a.concat(b),[]) 90 | 91 | .forEach((e) => { 92 | var root = spec; 93 | 94 | 95 | if ( e.path == '/' ) { 96 | 97 | if ( !root['/'] ) root['/'] = {}; 98 | root = root['/']; 99 | 100 | } else { 101 | 102 | e.split_path.forEach((p) => { 103 | if ( p.length ) { 104 | p = '/' + p; 105 | if ( !root[p] ) root[p] = {}; 106 | root = root[p]; 107 | } 108 | }); 109 | 110 | } 111 | 112 | root[e.method] = e.docs || {}; 113 | 114 | }); 115 | 116 | return [ 117 | '#%RAML 1.0', 118 | yaml.safeDump(spec) 119 | ].join("\n"); 120 | } 121 | } 122 | 123 | module.exports = Raml; 124 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const expect = require('chai').expect; 5 | const yaml = require('js-yaml'); 6 | const sinon = require('sinon'); 7 | const chaiAsPromised = require('chai-as-promised'); 8 | const sinonChai = require("sinon-chai"); 9 | 10 | chai.use(sinonChai); 11 | chai.use(chaiAsPromised); 12 | 13 | const RamlServerless = require('../src/index.js'); 14 | const ServerlessBuilder = require('./support/ServerlessBuilder.js'); 15 | 16 | describe('RAML-Serverless', () => { 17 | let serverless, plugin; 18 | 19 | beforeEach(() => { 20 | serverless = new ServerlessBuilder({ service: { custom: {} } }); 21 | plugin = new RamlServerless(serverless.serverless, serverless.serverless.config); 22 | }); 23 | 24 | describe('.getRaml', function() { 25 | 26 | it('writes "#%RAML 1.0" to the first line', function() { 27 | 28 | let out = plugin.getRaml(); 29 | expect(out).to.match(/^#%RAML 1.0\n/); 30 | 31 | }); 32 | 33 | it('writes the contents of custom.documentation.raml to output as is', function() { 34 | 35 | serverless.addCustom('documentation',{ 36 | raml: { 37 | title: 'My Awesome API', 38 | version: 'v1.0', 39 | }, 40 | }); 41 | 42 | let out = plugin.getRaml(); 43 | let outObj = yaml.safeLoad(out); 44 | expect(outObj).to.contain.all.keys('title','version') 45 | expect(outObj.title).to.equal('My Awesome API'); 46 | expect(outObj.version).to.equal('v1.0'); 47 | 48 | }); 49 | 50 | it('creates a object for each HTTP event path', function() { 51 | 52 | serverless.addFunction('func1',{ 53 | events: [ 54 | {http: { 55 | method: 'get', 56 | path: '/one', 57 | }}, 58 | ], 59 | }); 60 | 61 | serverless.addFunction('func2',{ 62 | events: [ 63 | {http: { 64 | method: 'post', 65 | path: '/one', 66 | }}, 67 | {http: { 68 | method: 'get', 69 | path: '/two', 70 | }}, 71 | {http: { 72 | method: 'get', 73 | path: '/three', 74 | }}, 75 | ], 76 | }); 77 | 78 | let out = plugin.getRaml(); 79 | let outObj = yaml.safeLoad(out); 80 | expect(outObj).to.contain.all.keys('/one','/two','/three') 81 | expect(outObj['/one']).to.have.keys('get','post'); 82 | expect(outObj['/two']).to.have.keys('get'); 83 | expect(outObj['/three']).to.have.keys('get'); 84 | 85 | }); 86 | 87 | it('splits paths', function() { 88 | 89 | serverless.addFunction('func1',{ 90 | events: [ 91 | {http: { 92 | method: 'get', 93 | path: '/one', 94 | }}, 95 | ], 96 | }); 97 | 98 | serverless.addFunction('func2',{ 99 | events: [ 100 | {http: { 101 | method: 'get', 102 | path: '/one/two', 103 | }}, 104 | {http: { 105 | method: 'get', 106 | path: '/one/two/three', 107 | }}, 108 | ], 109 | }); 110 | 111 | let out = plugin.getRaml(); 112 | let outObj = yaml.safeLoad(out); 113 | expect(outObj).to.contain.all.keys('/one') 114 | expect(outObj['/one']).to.have.keys('get','/two'); 115 | expect(outObj['/one']['/two']).to.have.keys('get','/three'); 116 | expect(outObj['/one']['/two']['/three']).to.have.keys('get'); 117 | 118 | }); 119 | 120 | it('works with root path [/]', function() { 121 | 122 | serverless.addFunction('func1',{ 123 | events: [ 124 | {http: { 125 | method: 'get', 126 | path: '/', 127 | }}, 128 | ], 129 | }); 130 | 131 | let out = plugin.getRaml(); 132 | let outObj = yaml.safeLoad(out); 133 | expect(outObj).to.contain.all.keys('/') 134 | expect(outObj['/']).to.have.keys('get'); 135 | 136 | }); 137 | 138 | it('defaults to protocol = HTTPS', function() { 139 | 140 | let out = plugin.getRaml(); 141 | let outObj = yaml.safeLoad(out); 142 | 143 | expect(outObj.protocols).to.deep.equal(['HTTPS']); 144 | 145 | }); 146 | 147 | it('defaults to mediaType = application/json', function() { 148 | 149 | let out = plugin.getRaml(); 150 | let outObj = yaml.safeLoad(out); 151 | 152 | expect(outObj.mediaType).to.deep.equal('application/json'); 153 | 154 | }); 155 | 156 | }); 157 | 158 | describe('.getEndpointAsync', function() { 159 | 160 | it('returns a promise that resolves to an endpoint', function() { 161 | 162 | let provider = { 163 | request: sinon.stub().returns( 164 | Promise.resolve({ 165 | Stacks: [{ 166 | Outputs: [{ Description: "", OutputKey: "ServiceEndpoint", OutputValue: "expected" }], 167 | }] 168 | }) 169 | ), 170 | naming: { 171 | getServiceEndpointRegex: sinon.stub().returns(/./), 172 | getStackName: sinon.stub().returns('somestack'), 173 | } 174 | }; 175 | 176 | plugin.serverless.getProvider = sinon.stub().returns(provider); 177 | plugin = new RamlServerless(serverless.serverless, serverless.serverless.config); 178 | 179 | let endpoint = plugin.getEndpointAsync(); 180 | 181 | return expect(endpoint).to.eventually.equal('expected') 182 | .then(function() { 183 | 184 | expect(plugin.serverless.getProvider).to.have.been.calledOnce; 185 | expect(provider.request).to.have.been.calledOnce; 186 | expect(provider.naming.getServiceEndpointRegex).to.have.been.calledOnce; 187 | expect(provider.naming.getStackName).to.have.been.calledOnce; 188 | 189 | expect(plugin.serverless.getProvider).to.be.calledWith('aws'); 190 | expect(provider.request).to.be.calledWith( 191 | 'CloudFormation', 192 | 'describeStacks', 193 | { StackName: 'somestack' }, 194 | plugin.options.stage, 195 | plugin.options.region 196 | ); 197 | 198 | }); 199 | 200 | }); 201 | 202 | it('fails gracefully when it cannot get the CloudFormation stack', function() { 203 | 204 | let provider = { 205 | request: sinon.stub().returns(new Promise((x,y) => { throw new Error(); })), 206 | naming: { 207 | getServiceEndpointRegex: sinon.stub().returns(/./), 208 | getStackName: sinon.stub().returns('somestack'), 209 | } 210 | }; 211 | 212 | plugin.serverless.getProvider = sinon.stub().returns(provider); 213 | plugin = new RamlServerless(serverless.serverless, serverless.serverless.config); 214 | 215 | let endpoint = plugin.getEndpointAsync(); 216 | 217 | return expect(endpoint).to.eventually.be.null 218 | .then(function() { 219 | expect(plugin.serverless.getProvider).to.have.been.calledOnce; 220 | expect(provider.request).to.have.been.calledOnce; 221 | expect(provider.naming.getStackName).to.have.been.calledOnce; 222 | 223 | expect(plugin.serverless.getProvider).to.be.calledWith('aws'); 224 | expect(provider.request).to.be.calledWith( 225 | 'CloudFormation', 226 | 'describeStacks', 227 | { StackName: 'somestack' }, 228 | plugin.options.stage, 229 | plugin.options.region 230 | ); 231 | }); 232 | 233 | }); 234 | 235 | }); 236 | 237 | }); 238 | -------------------------------------------------------------------------------- /test/support/ServerlessBuilder.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * ServerlessBulder origionally copied from the serverless-offline project 5 | * Date: 2016-12-06 6 | * Url: https://github.com/dherault/serverless-offline/blob/c21b332f99c73ffda7caa7a1108f30b93d89fa42/test/support/ServerlessBuilder.js 7 | * License: MIT 8 | */ 9 | 10 | const _ = require('lodash'); 11 | const sinon = require('sinon'); 12 | 13 | module.exports = class ServerlessBuilder { 14 | constructor(serverless) { 15 | const serverlessDefaults = { 16 | service: { 17 | provider: { 18 | name: 'aws', 19 | stage: 'dev', 20 | region: 'us-east-1', 21 | runtime: 'nodejs4.3', 22 | }, 23 | functions: {}, 24 | getFunction(functionName) { 25 | return this.functions[functionName]; 26 | }, 27 | getAllFunctions() { 28 | return Object.keys(this.functions); 29 | }, 30 | }, 31 | cli: { 32 | log: sinon.stub(), 33 | }, 34 | version: '1.0.2', 35 | config: { 36 | servicePath: '', 37 | }, 38 | getProvider() { 39 | return null; 40 | }, 41 | }; 42 | this.serverless = _.merge(serverlessDefaults, serverless); 43 | 44 | this.serverless.service.getFunction = this.serverless.service.getFunction.bind(this.serverless.service); 45 | this.serverless.service.getAllFunctions = this.serverless.service.getAllFunctions.bind(this.serverless.service); 46 | this.serverless.getProvider = this.serverless.getProvider.bind(this.serverless.service); 47 | 48 | } 49 | 50 | addApiKeys(keys) { 51 | this.serverless.service.provider = Object.assign(this.serverless.service.provider, { apiKeys: keys }); 52 | } 53 | 54 | addFunction(functionName, functionConfig) { 55 | this.serverless.service.functions[functionName] = functionConfig; 56 | } 57 | 58 | addCustom(prop, value) { 59 | const newCustomProp = {}; 60 | newCustomProp[prop] = value; 61 | this.serverless.service.custom = Object.assign(this.serverless.service.custom || {}, newCustomProp); 62 | } 63 | 64 | toObject() { 65 | return this.serverless; 66 | } 67 | }; 68 | --------------------------------------------------------------------------------