├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── examples ├── FakeHapi.js ├── README.md ├── basicAuth.js ├── basicAuth.raml ├── controllers │ └── SimpleController.js ├── es6 │ ├── FakeHapi.js │ ├── README.md │ ├── controllers │ │ └── SimpleController.js │ ├── es6.js │ └── es6run.js ├── simple.js └── simple.raml ├── index.js ├── package.json ├── src ├── index.js └── utils │ └── RAML.js ├── test ├── authTest.raml ├── dotTest.raml ├── index.test.js ├── mocha.opts ├── rootTest.raml ├── test.raml └── utils │ └── RAML.test.js └── wallaby.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["transform-runtime"] 4 | } 5 | 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module" 5 | } 6 | } 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | lib/ 4 | npm-debug.log 5 | 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .npmignore 2 | .idea/ 3 | src/ 4 | .travis.yml 5 | .eslintrc 6 | .babelrc 7 | wallaby.js 8 | 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "6" 5 | sudo: false 6 | cache: 7 | directories: 8 | - node_modules 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/dave-irvine/node-hapi-raml.svg?branch=master)](https://travis-ci.org/dave-irvine/node-hapi-raml) 2 | [![NPM](https://nodei.co/npm/hapi-raml.png?mini=true)](https://nodei.co/npm/hapi-raml/) 3 | hapi-raml 4 | ---- 5 | 6 | This library will take your RAML, parse it using `raml-parser`, and then set up routes in `hapi` to match your RAML. 7 | 8 | ##Installation 9 | 10 | ```npm install hapi-raml``` 11 | 12 | ##Usage 13 | 14 | Require in hapi-raml, and pass it a reference to your hapi server, a hashmap of your Controllers, and the path to your 15 | RAML. 16 | 17 | When you're ready, call `hookup()`, and hapi-raml will link up your hapi routes to your Controllers, and return a 18 | Promise that will tell you when it has finished. Then you can start your hapi server. 19 | 20 | ``` 21 | var HapiRaml = require('hapi-raml'); 22 | var server = new Hapi.Server(); 23 | 24 | var controllers = {}; 25 | controllers.MyController = new MyController(); 26 | 27 | var hapiRaml = new HapiRaml(server, controllers, './my.raml'); 28 | 29 | server.connection({ port: 3000 }); 30 | 31 | hapiRaml.hookup().then(function () { 32 | server.start(); 33 | }); 34 | ``` 35 | 36 | ##Conventions 37 | 38 | We borrow the collection/collection-item pattern from RAML and layer Controllers on top. 39 | 40 | Your Controllers should feature at least the following functions, which should expect to receive the Hapi object 41 | `request`, and a callback `reply`. 42 | 43 | #####GET (API Root)/collection maps to `list()` 44 | 45 | Returns an Array of instances of the matching Model for this Controller. 46 | 47 | #####POST (API Root)/collection maps to `create()` 48 | 49 | Create a single instance of the matching Model for this Controller. 50 | 51 | #####GET (API Root)/collection/{id} maps to `fetch()` 52 | 53 | Returns a single instance of the matching Model for this Controller. 54 | 55 | #####DELETE (API Root)/collection/{id} maps to `delete()` 56 | 57 | Deletes the matching Model for this Controller. 58 | 59 | #####POST||PATCH (API Root)/collection/{id} maps to `update()` 60 | 61 | Updates the matching Model for this Controller. 62 | 63 | #####ANY (API Root)/collection/{id}/(anything) maps to `(anything)()` 64 | 65 | Whatever you decide! 66 | -------------------------------------------------------------------------------- /examples/FakeHapi.js: -------------------------------------------------------------------------------- 1 | var FakeHapi = function () { 2 | } 3 | 4 | FakeHapi.Server = function () { 5 | }; 6 | 7 | FakeHapi.Server.prototype.route = function () { 8 | }; 9 | 10 | FakeHapi.Server.prototype.connection = function () { 11 | }; 12 | 13 | module.exports = FakeHapi; 14 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Simple Example 2 | ==== 3 | 4 | This example creates a single route (/simple). 5 | 6 | We use a fake Hapi because otherwise Hapi would become a dependency of this library. 7 | 8 | Basic Auth Example 9 | ==== 10 | 11 | This example creates a single route (/simple) that is secured by a Basic Auth strategy. 12 | 13 | This expects a real instance of Hapi, so copy this example out somewhere and install. 14 | -------------------------------------------------------------------------------- /examples/basicAuth.js: -------------------------------------------------------------------------------- 1 | var Hapi = require('hapi'); 2 | var HapiRaml = require('hapi-raml'); 3 | var Basic = require('hapi-auth-basic'); 4 | 5 | //Load and instantiate our Controllers 6 | var SimpleController = require('./controllers/SimpleController'); 7 | 8 | var controllers = { 9 | SimpleController: new SimpleController() 10 | }; 11 | 12 | //Create a new Hapi Server. 13 | var server = new Hapi.Server(); 14 | 15 | //Instantiate HapiRaml, passing our Hapi Server, Controllers, and path to a RAML file. 16 | var hapiRaml = new HapiRaml(server, controllers, './basicAuth.raml'); 17 | 18 | server.connection({ port: 3000 }); 19 | 20 | server.register(Basic, function (err) { 21 | server.auth.strategy('myBasicAuth', 'basic', { validateFunc: function () {} }); 22 | 23 | //Hookup the routes. 24 | hapiRaml.hookup().then(function () { 25 | //We could now start our Hapi Server 26 | console.log('All routes hooked up'); 27 | }) 28 | .catch(function (err) { 29 | //There was some problem doing the hookups. 30 | console.log(err); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /examples/basicAuth.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | --- 3 | title: Test API 4 | baseUri: https://test.raml.net/api/{version} 5 | version: v1 6 | mediaType: application/json 7 | resourceTypes: 8 | - collection: 9 | get: 10 | responses: 11 | 200: 12 | body: 13 | securitySchemes: 14 | - myBasicAuth: 15 | type: 'Basic Authentication' 16 | /simple: 17 | type: collection 18 | displayName: Simple 19 | get: 20 | securedBy: ['myBasicAuth'] 21 | -------------------------------------------------------------------------------- /examples/controllers/SimpleController.js: -------------------------------------------------------------------------------- 1 | var SimpleController = function () { 2 | }; 3 | 4 | SimpleController.prototype.list = function () { 5 | }; 6 | 7 | module.exports = SimpleController; 8 | -------------------------------------------------------------------------------- /examples/es6/FakeHapi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Server = class { 4 | route() { 5 | } 6 | 7 | connection() { 8 | } 9 | } 10 | 11 | exports.Server = Server; 12 | -------------------------------------------------------------------------------- /examples/es6/README.md: -------------------------------------------------------------------------------- 1 | ES6 Example 2 | ==== 3 | 4 | This will only work if you run: 5 | 6 | ``` 7 | npm install babel-core 8 | ``` 9 | 10 | Then: 11 | 12 | ``` 13 | node es6run.js 14 | ``` 15 | -------------------------------------------------------------------------------- /examples/es6/controllers/SimpleController.js: -------------------------------------------------------------------------------- 1 | export default class SimpleController { 2 | list() { 3 | } 4 | } 5 | 6 | -------------------------------------------------------------------------------- /examples/es6/es6.js: -------------------------------------------------------------------------------- 1 | import SimpleController from './controllers/SimpleController'; 2 | import HapiRaml from '../../'; 3 | import Hapi from './FakeHapi'; 4 | 5 | export default () => { 6 | //Instantiate our Controllers 7 | let controllers = { 8 | SimpleController: new SimpleController() 9 | }; 10 | 11 | //Create a new Hapi Server. 12 | let server = new Hapi.Server(); 13 | 14 | //Instantiate HapiRaml, passing our Hapi Server, Controllers, and path to a RAML file. 15 | let hapiRaml = new HapiRaml(server, controllers, '../simple.raml'); 16 | 17 | server.connection({ port: 3000 }); 18 | 19 | //Hookup the routes. 20 | hapiRaml.hookup().then(() => { 21 | //We could now start our Hapi Server 22 | console.log('All routes hooked up'); 23 | }) 24 | .catch((err) => { 25 | //There was some problem doing the hookups. 26 | console.log(err); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /examples/es6/es6run.js: -------------------------------------------------------------------------------- 1 | require("babel-core/register"); 2 | 3 | var es6 = require('./es6').default; 4 | 5 | es6(); 6 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | //Load Hapi (Fake in this case) 2 | var Hapi = require('./FakeHapi'); 3 | //Load HapiRaml (usually 'hapi-raml') 4 | var HapiRaml = require('../'); 5 | 6 | //Load and instantiate our Controllers 7 | var SimpleController = require('./controllers/SimpleController'); 8 | 9 | var controllers = { 10 | SimpleController: new SimpleController() 11 | }; 12 | 13 | //Create a new Hapi Server. 14 | var server = new Hapi.Server(); 15 | 16 | //Instantiate HapiRaml, passing our Hapi Server, Controllers, and path to a RAML file. 17 | var hapiRaml = new HapiRaml(server, controllers, './simple.raml'); 18 | 19 | server.connection({ port: 3000 }); 20 | 21 | //Hookup the routes. 22 | hapiRaml.hookup().then(function () { 23 | //We could now start our Hapi Server 24 | console.log('All routes hooked up'); 25 | }) 26 | .catch(function (err) { 27 | //There was some problem doing the hookups. 28 | console.log(err); 29 | }); 30 | -------------------------------------------------------------------------------- /examples/simple.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | --- 3 | title: Test API 4 | baseUri: https://test.raml.net/api/{version} 5 | version: v1 6 | mediaType: application/json 7 | resourceTypes: 8 | - collection: 9 | get: 10 | responses: 11 | 200: 12 | body: 13 | /simple: 14 | type: collection 15 | displayName: Simple 16 | get: 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/index.js').default; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-raml", 3 | "version": "4.0.0", 4 | "description": "Create HAPI bindings from RAML specs automatically", 5 | "main": "index.js", 6 | "scripts": { 7 | "preversion": "npm test && npm run build && git diff --exit-code --quiet", 8 | "postversion": "git push && git push --tags", 9 | "pretest": "eslint src/", 10 | "test": "mocha --compilers js:babel-core/register", 11 | "build": "babel src --out-dir lib" 12 | }, 13 | "keywords": [ 14 | "hapi", 15 | "raml" 16 | ], 17 | "author": "Dave Irvine ", 18 | "license": "MIT", 19 | "dependencies": { 20 | "babel-runtime": "^6.11.6", 21 | "debug": "^2.2.0", 22 | "lodash": "^4.16.4", 23 | "pluralize": "^3.0.0", 24 | "raml-1-parser": "^1.1.6", 25 | "string": "^3.3.3" 26 | }, 27 | "devDependencies": { 28 | "babel-cli": "^6.16.0", 29 | "babel-core": "^6.17.0", 30 | "babel-plugin-transform-runtime": "^6.15.0", 31 | "babel-preset-es2015": "^6.16.0", 32 | "chai": "^3.5.0", 33 | "chai-as-promised": "^6.0.0", 34 | "eslint": "^3.8.1", 35 | "mocha": "^3.1.2", 36 | "mock-fs": "^3.11.0", 37 | "sinon": "^1.17.6", 38 | "sinon-as-promised": "^4.0.2", 39 | "sinon-chai": "^2.8.0" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "git+ssh://git@github.com/dave-irvine/node-hapi-raml.git" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import fs from 'fs'; 4 | import RAML from './utils/RAML'; 5 | import ramlParser from 'raml-1-parser'; 6 | import _ from 'lodash'; 7 | import dbg from 'debug'; 8 | 9 | let debug = dbg('Hapi-RAML'); 10 | 11 | export default class HapiRaml { 12 | constructor(server, controllersMap, ramlPath) { 13 | debug('constructor()'); 14 | if (server === undefined || server.route === undefined || typeof server.route !== 'function') { 15 | throw new Error('Missing `server` dependency.'); 16 | } 17 | 18 | if (controllersMap === undefined || typeof controllersMap === 'string') { 19 | throw new Error('Missing `controllersMap` dependency.'); 20 | } 21 | 22 | this.server = server; 23 | this.controllersMap = controllersMap; 24 | 25 | this.raml = new RAML(fs, ramlParser, ramlPath); 26 | } 27 | 28 | hookup() { 29 | return new Promise((resolve, reject) => { 30 | let controllersMap = this.controllersMap, 31 | server = this.server; 32 | 33 | this.raml.getRouteMap() 34 | .then(function (routeMap) { 35 | _.each(routeMap, (route) => { 36 | let routeOptions = {}; 37 | 38 | //The controllersMap must contain the Controller class for this route 39 | let controller = controllersMap[route.className]; 40 | 41 | if (controller === undefined) { 42 | return reject(new Error(`Tried to find Controller '${route.className}' but it did not exist.`)); 43 | } 44 | 45 | let controllerFunction = controller[route.classFunction]; 46 | 47 | //The Controller class for this route must contain the function for this route 48 | if (controllerFunction === undefined || typeof controllerFunction !== 'function') { 49 | return reject(new Error(`Tried to find '${route.classFunction}' on Controller '${route.className}' but it did not exist.`)); 50 | } 51 | 52 | if (route.authStrategy !== undefined) { 53 | /* 54 | If the route has any authStrategies, then our auth mode is required. 55 | We might override this later if the route has a null authStrategy because this will mean 56 | that it is possible to access the route without auth. 57 | */ 58 | let authOptions = { 59 | mode: 'required' 60 | }; 61 | 62 | //Filter out any authStrategies that are null 63 | authOptions.strategies = _.filter(route.authStrategy, (authStrategy) => { 64 | return authStrategy !== null; 65 | }); 66 | 67 | //The route has a null authStrategy, so our auth mode for the entire route is optional 68 | if (authOptions.strategies.length < route.authStrategy.length) { 69 | authOptions.mode = 'optional'; 70 | } 71 | 72 | //The only authStrategy is null, so auth should be false 73 | if (authOptions.strategies.length === 0) { 74 | routeOptions.auth = false; 75 | } else { 76 | routeOptions.auth = authOptions; 77 | } 78 | } 79 | 80 | server.route({ 81 | method: route.method, 82 | path: route.uri, 83 | config: routeOptions, 84 | handler: (request, reply) => { 85 | controllerFunction.apply(controller, [request, reply]); 86 | } 87 | }); 88 | }); 89 | 90 | return resolve(); 91 | }) 92 | .catch((err) => { 93 | reject(err); 94 | }); 95 | }); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/utils/RAML.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import path from 'path'; 4 | import pluralize from 'pluralize'; 5 | import string from 'string'; 6 | import _ from 'lodash'; 7 | import dbg from 'debug'; 8 | 9 | let debug = dbg('Hapi-RAML:RAML'); 10 | 11 | export default class RAML { 12 | constructor(fs, parser, ramlPath) { 13 | debug('constructor()'); 14 | if (fs === undefined) { 15 | throw new Error('Missing `fs` dependency.'); 16 | } 17 | 18 | if (parser === undefined) { 19 | throw new Error('Missing `parser` dependency.'); 20 | } 21 | 22 | if (ramlPath === undefined) { 23 | throw new Error('Missing path to RAML file'); 24 | } 25 | 26 | this.fs = fs; 27 | this.parser = parser; 28 | this.resolvedRamlPath = path.resolve(process.cwd(), ramlPath); 29 | 30 | if (!fs.existsSync(this.resolvedRamlPath)) { 31 | throw new Error('path to RAML file does not exist'); 32 | } 33 | } 34 | 35 | loadRAMLFile() { 36 | return new Promise((resolve, reject) => { 37 | this.parser.loadApi(this.resolvedRamlPath) 38 | .then((parsedData) => { 39 | if (parsedData && !parsedData.baseUri) { 40 | return reject(new Error('Missing `baseUri` property')); 41 | } 42 | 43 | return resolve(parsedData); 44 | }) 45 | .catch((parseErr) => { 46 | return reject(new Error('Parsing error: ' + parseErr)); 47 | }); 48 | }); 49 | } 50 | 51 | getRouteMap() { 52 | function camelize(str) { 53 | return string(str).camelize().s; 54 | } 55 | 56 | function singularize(str) { 57 | return pluralize.singular(str); 58 | } 59 | 60 | function uppercase(str) { 61 | return str.toUpperCase(); 62 | } 63 | 64 | //Can be called recursively 65 | function resourcesParser(resources, parentResource, ast) { 66 | let routeMap = [], 67 | defaultAuthStrategies; 68 | 69 | //Shift over some parameters if the parentResource looks like an ast 70 | if (parentResource && parentResource.baseUri) { 71 | ast = parentResource; 72 | parentResource = undefined; 73 | } 74 | 75 | if (ast === undefined) { 76 | throw new Error('Resource is not parseable, ast was not passed'); 77 | } else { 78 | //A default auth strategy is found at the top of the ast 79 | //This can be overriden by an authStrategy on a resource 80 | debug(`Checking for default auth strategy`); 81 | debug(ast.securedBy); 82 | debug(ast); 83 | if (ast.securedBy !== undefined) { 84 | defaultAuthStrategies = ast.securedBy; 85 | } 86 | } 87 | 88 | resources.forEach((_resource) => { 89 | let baseClassName, 90 | className, 91 | classFunction, 92 | strippedRelativeUri; 93 | 94 | let resource = { 95 | relativeUri: _resource.relativeUri().value(), 96 | methods: _resource.methods() 97 | }; 98 | 99 | if (_resource.type && _resource.type()) { 100 | resource.type = _resource.type().name(); 101 | } 102 | 103 | if (_resource.resources && _resource.resources()) { 104 | resource.resources = _resource.resources(); 105 | } 106 | 107 | if (resource.relativeUri === undefined) { 108 | throw new Error('Resource is not parseable, no relativeUri'); 109 | } 110 | 111 | debug(`Parsing resource ${resource.relativeUri}`); 112 | 113 | resource.hapi = {}; 114 | classFunction = ''; 115 | 116 | //Drop the preceding '/' 117 | strippedRelativeUri = resource.relativeUri.substring(1); 118 | resource.hapi.resourceUri = resource.relativeUri; 119 | 120 | //If parent resource is the root resource, we effectively don't have a parent 121 | //If resource is a collection, it should have its own Controller, not use the parent. 122 | if (parentResource !== undefined && parentResource.relativeUri !== '/' && resource.type !== 'collection') { 123 | //If there is a parent resource, we can infer the needed className from its relativeUri 124 | className = parentResource.relativeUri.substring(1); 125 | if (className.indexOf('/') > 0) { 126 | className = className.substring(0, className.indexOf('/')); 127 | } 128 | } else { 129 | className = strippedRelativeUri; 130 | if (className.indexOf('/') > 0) { 131 | className = className.substring(0, className.indexOf('/')); 132 | } 133 | 134 | if (className === '') { 135 | //This resource is likely the root resource. 136 | className = 'root'; 137 | strippedRelativeUri = 'root'; 138 | } 139 | } 140 | 141 | if (parentResource !== undefined && parentResource.relativeUri !== '/') { 142 | //A sub resource's uri is based upon its parent resource uri 143 | resource.hapi.resourceUri = `${parentResource.hapi.resourceUri}${resource.relativeUri}`; 144 | } 145 | 146 | baseClassName = className; 147 | className = singularize(className); 148 | className = camelize(`Controller-${className}-`); 149 | 150 | debug(`Resource classname is ${className}`); 151 | 152 | debug(`Resource type: ${resource.type}`); 153 | 154 | //Determine the function to call on the Class 155 | //This is based on the resource type, as well as the relativeUri 156 | switch (resource.type) { 157 | case 'collection': 158 | if (strippedRelativeUri === baseClassName) { 159 | debug(`Special case, resource is top level`); 160 | classFunction = 'list'; 161 | } else { 162 | classFunction = strippedRelativeUri.substring(strippedRelativeUri.lastIndexOf('/') + 1); 163 | } 164 | break; 165 | case 'collection-item': 166 | default: 167 | switch (strippedRelativeUri) { 168 | case '{id}': 169 | debug(`Special case, resource is an {id} match`); 170 | classFunction = 'fetch'; 171 | break; 172 | default: 173 | classFunction = strippedRelativeUri.substring(strippedRelativeUri.lastIndexOf('/') + 1); 174 | } 175 | } 176 | 177 | //Camelize class functions to remove "-" from function names 178 | classFunction = camelize(classFunction); 179 | //Remove "." from function names and replace with "-" 180 | classFunction = classFunction.replace('.', '_'); 181 | 182 | debug(`Resource classFunction is ${classFunction}`); 183 | 184 | resource.hapi.className = className; 185 | resource.hapi.classFunction = classFunction; 186 | 187 | if (resource.methods === undefined) { 188 | throw new Error('Resource is not parseable, no methods'); 189 | } 190 | 191 | resource.methods.forEach((_method) => { 192 | let method = { 193 | method: _method.method() 194 | }; 195 | 196 | if (_method.responses && _method.responses()) { 197 | method.responses = _method.responses(); 198 | } 199 | 200 | if (_method.securedBy && _method.securedBy()) { 201 | method.securedBy = _method.securedBy(); 202 | } 203 | 204 | debug(`Parsing resource method ${method.method}`); 205 | let methodClassFunction = resource.hapi.classFunction; 206 | 207 | resource.hapi.method = uppercase(method.method); 208 | resource.hapi.responses = method.responses; 209 | 210 | debug(`Checking method securedBy property`); 211 | debug(method.securedBy); 212 | 213 | if (method.securedBy && method.securedBy.length === 1) { 214 | debug(`Method has an authStrategy`); 215 | debug(method.securedBy); 216 | resource.hapi.authStrategy = method.securedBy[0].securitySchemeName(); 217 | } else { 218 | if (defaultAuthStrategies !== undefined) { 219 | if (defaultAuthStrategies.length === 1) { 220 | let defaultAuthStrategy = defaultAuthStrategies[0]; 221 | debug(`There is a default authStrategy that will affect this resource ${defaultAuthStrategy.securitySchemeName()}`); 222 | resource.hapi.authStrategy = defaultAuthStrategy.securitySchemeName(); 223 | } else { 224 | debug(`Multiple default auth strategies.`); 225 | } 226 | } else { 227 | debug(`There is no authStrategy for this resource`); 228 | resource.hapi.authStrategy = [null]; 229 | } 230 | } 231 | 232 | //Special http methods can cause a remapping of the Class function we need to call 233 | switch(method.method) { 234 | case 'patch': 235 | case 'post': 236 | if (method.method === 'post') { 237 | //Catch POST methods only 238 | if (methodClassFunction === 'list') { 239 | debug(`Special case, this is a POST on collection, which is a create()`); 240 | methodClassFunction = 'create'; 241 | } 242 | } 243 | 244 | //POST or PATCH 245 | if (methodClassFunction === 'fetch') { 246 | debug(`Special case, this is a POST or PATCH at {id}, which is an update()`); 247 | methodClassFunction = 'update'; 248 | } 249 | break; 250 | case 'delete': 251 | if (methodClassFunction === 'fetch') { 252 | debug(`Special case, this is a DELETE at {id}, which is a delete()`); 253 | methodClassFunction = 'delete'; 254 | } 255 | break; 256 | } 257 | 258 | let route = { 259 | 'className': resource.hapi.className, 260 | 'classFunction': methodClassFunction, 261 | 'uri': resource.hapi.resourceUri, 262 | 'method': resource.hapi.method, 263 | 'responses': resource.hapi.responses, 264 | 'authStrategy': resource.hapi.authStrategy 265 | }; 266 | 267 | debug(`Adding route to route map`); 268 | routeMap.push(route); 269 | }); 270 | 271 | if (resource.resources) { 272 | debug(`Resource has sub resources`); 273 | debug(resource.resources); 274 | let mappedRoutes = resourcesParser(resource.resources, resource, ast); 275 | routeMap = _.flatten([routeMap, mappedRoutes]); 276 | } 277 | }); 278 | 279 | return routeMap; 280 | } 281 | 282 | return new Promise((resolve, reject) => { 283 | this.loadRAMLFile() 284 | .then((_ast) => { 285 | try { 286 | let ast = { 287 | resources: _ast.resources(), 288 | baseUri: _ast.baseUri().value() 289 | }; 290 | 291 | if (_ast.securedBy && _ast.securedBy()) { 292 | ast.securedBy = _ast.securedBy(); 293 | } 294 | 295 | var parsedResources = resourcesParser(ast.resources, ast); 296 | resolve(parsedResources); 297 | } catch (e) { 298 | reject(e); 299 | } 300 | }) 301 | .catch((e) => { 302 | reject(e); 303 | }); 304 | }); 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /test/authTest.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | --- 3 | title: Test API 4 | baseUri: https://test.raml.net/api/{version} 5 | version: v1 6 | mediaType: application/json 7 | resourceTypes: 8 | - collection: 9 | get: 10 | responses: 11 | 200: 12 | body: 13 | - collection-item: 14 | get: 15 | responses: 16 | 200: 17 | body: 18 | securitySchemes: 19 | - jwt: 20 | type: 'x-jwt' 21 | describedBy: 22 | headers: 23 | Authorization: 24 | type: string 25 | 26 | /test: 27 | type: collection 28 | displayName: Users 29 | get: 30 | securedBy: [ 'jwt' ] 31 | -------------------------------------------------------------------------------- /test/dotTest.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | --- 3 | title: Test API 4 | baseUri: https://test.raml.net/api/{version} 5 | version: v1 6 | mediaType: application/json 7 | resourceTypes: 8 | - collection: 9 | get: 10 | responses: 11 | 200: 12 | body: 13 | - collection-item: 14 | get: 15 | responses: 16 | 200: 17 | body: 18 | 19 | /test: 20 | type: collection 21 | displayName: Users 22 | get: 23 | /test.type: 24 | type: collection-item 25 | get: 26 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | /*eslint-env mocha */ 2 | /*eslint-disable no-unused-expressions*/ 3 | 'use strict'; 4 | 5 | import chai, {expect} from 'chai'; 6 | import chaiAsPromised from 'chai-as-promised'; 7 | import sinon from 'sinon'; 8 | import sinonChai from 'sinon-chai'; 9 | import mockFS from 'mock-fs'; 10 | import fs from 'fs'; 11 | import path from 'path'; 12 | 13 | chai.use(sinonChai); 14 | chai.use(chaiAsPromised); 15 | 16 | import HapiRaml from '../src'; 17 | 18 | describe('hapi-raml', () => { 19 | let hapiRaml; 20 | 21 | describe('constructor()', () => { 22 | it('should throw if not passed a hapi-server', () => { 23 | expect(() => { 24 | hapiRaml = new HapiRaml(); 25 | }).to.throw(/Missing `server` dependency/); 26 | }); 27 | 28 | it('should throw if hapi-server is missing its route() function', () => { 29 | expect(() => { 30 | let fakeServer = () => {}; 31 | 32 | hapiRaml = new HapiRaml(fakeServer); 33 | }).to.throw(/Missing `server` dependency/); 34 | }); 35 | 36 | it('should throw if not passed a controllers map', () => { 37 | expect(() => { 38 | let fakeServer = { 39 | route: () => {} 40 | }; 41 | 42 | hapiRaml = new HapiRaml(fakeServer); 43 | }).to.throw(/Missing `controllersMap` dependency/); 44 | }); 45 | 46 | it('should throw if not passed a path to a raml file', () => { 47 | expect(() => { 48 | let fakeServer = { 49 | route: () => {} 50 | }, 51 | fakeControllersMap = {}; 52 | 53 | hapiRaml = new HapiRaml(fakeServer, fakeControllersMap); 54 | }).to.throw('Missing path to RAML file'); 55 | }); 56 | 57 | it('should not throw if passed all required properties', () => { 58 | expect(() => { 59 | let fakeServer = { 60 | route: () => {} 61 | }, 62 | fakeControllersMap = {}, 63 | ramlPath = './test.raml'; 64 | 65 | mockFS({ 66 | 'test.raml': 'test' 67 | }); 68 | 69 | hapiRaml = new HapiRaml(fakeServer, fakeControllersMap, ramlPath); 70 | 71 | mockFS.restore(); 72 | }).to.not.throw(); 73 | }); 74 | }); 75 | 76 | describe('hookup()', () => { 77 | let fakeServer, 78 | fakeControllersMap, 79 | ramlPath; 80 | 81 | beforeEach(() => { 82 | fakeServer = () => {}; 83 | fakeServer.route = () => {}; 84 | fakeControllersMap = {}; 85 | ramlPath = './test.raml'; 86 | 87 | let testRAML = fs.readFileSync(path.resolve(__dirname, './test.raml')); 88 | let rootTestRAML = fs.readFileSync(path.resolve(__dirname, './rootTest.raml')); 89 | let dotTestRAML = fs.readFileSync(path.resolve(__dirname, './dotTest.raml')); 90 | 91 | mockFS({ 92 | 'test.raml': testRAML, 93 | 'rootTest.raml': rootTestRAML, 94 | 'dotTest.raml': dotTestRAML 95 | }); 96 | 97 | hapiRaml = new HapiRaml(fakeServer, fakeControllersMap, ramlPath); 98 | }); 99 | 100 | afterEach(() => { 101 | mockFS.restore(); 102 | }); 103 | 104 | it('should return a Promise', () => { 105 | expect(hapiRaml.hookup()).to.be.an.instanceOf(Promise); 106 | }); 107 | 108 | it('should reject if the RAML file is not parseable', () => { 109 | mockFS.restore(); 110 | 111 | mockFS({ 112 | 'test.raml': 'unparseable' 113 | }); 114 | 115 | return expect(hapiRaml.hookup()).to.be.rejectedWith(/Parsing error/); 116 | }); 117 | 118 | it('should reject if any controllers listed in the RAML are not found', () => { 119 | return expect(hapiRaml.hookup()).to.be.rejectedWith(/Tried to find Controller '\w+' but it did not exist/); 120 | }); 121 | 122 | it('should map root level collections to ControllerRoot', () => { 123 | hapiRaml = new HapiRaml(fakeServer, fakeControllersMap, './rootTest.raml'); 124 | return expect(hapiRaml.hookup()).to.be.rejectedWith(/Tried to find Controller 'ControllerRoot' but it did not exist/); 125 | }); 126 | 127 | it('should route resources to the correct relativeUri', () => { 128 | let routeStub = sinon.stub(fakeServer, 'route', () => {}); 129 | 130 | fakeControllersMap = { 131 | 'ControllerRoot': { 132 | 'list': () => {} 133 | }, 134 | 'ControllerTest': { 135 | 'list': () => {} 136 | }, 137 | 'ControllerSubTest': { 138 | 'list': () => {}, 139 | 'item': () => {} 140 | } 141 | }; 142 | 143 | hapiRaml = new HapiRaml(fakeServer, fakeControllersMap, './rootTest.raml'); 144 | return hapiRaml.hookup() 145 | .then(() => { 146 | return expect(routeStub).to.have.been.calledWith(sinon.match({ 147 | path: "/test/subTest/item" 148 | })); 149 | }); 150 | }); 151 | 152 | it('should rename routes with "." to "-"', () => { 153 | let routeStub = sinon.stub(fakeServer, 'route', () => {}); 154 | 155 | fakeControllersMap = { 156 | 'ControllerTest': { 157 | 'list': () => {} 158 | } 159 | }; 160 | 161 | hapiRaml = new HapiRaml(fakeServer, fakeControllersMap, './dotTest.raml'); 162 | return expect(hapiRaml.hookup()).to.be.rejectedWith(/Tried to find 'test_type' on Controller '\w+' but it did not exist/); 163 | }); 164 | 165 | it('should map a sub level collection to the Controller for that collection name', () => { 166 | fakeControllersMap = { 167 | 'ControllerRoot': { 168 | 'list': () => {} 169 | } 170 | }; 171 | 172 | hapiRaml = new HapiRaml(fakeServer, fakeControllersMap, './rootTest.raml'); 173 | 174 | return expect(hapiRaml.hookup()).to.be.rejectedWith(/Tried to find Controller 'ControllerTest' but it did not exist/); 175 | }); 176 | 177 | it('should map a sub level collection-item to the Controller for the parent collection name', () => { 178 | fakeControllersMap = { 179 | 'ControllerRoot': { 180 | 'list': () => {} 181 | }, 182 | 'ControllerTest': { 183 | 'list': () => {} 184 | }, 185 | 'ControllerSubTest': { 186 | 'list': () => {} 187 | } 188 | }; 189 | 190 | hapiRaml = new HapiRaml(fakeServer, fakeControllersMap, './rootTest.raml'); 191 | 192 | return expect(hapiRaml.hookup()).to.be.rejectedWith(/Tried to find '\w+' on Controller 'ControllerSubTest' but it did not exist/); 193 | }); 194 | 195 | it('should reject if any functions listed in the RAML are not found on the Controller', () => { 196 | fakeControllersMap = { 197 | 'ControllerTest': {} 198 | }; 199 | 200 | hapiRaml = new HapiRaml(fakeServer, fakeControllersMap, ramlPath); 201 | 202 | return expect(hapiRaml.hookup()).to.be.rejectedWith(/Tried to find '\w+' on Controller '\w+' but it did not exist/); 203 | }); 204 | 205 | it('should reject if any functions listed in the RAML are not functions on the Controller', () => { 206 | fakeControllersMap = { 207 | 'ControllerTest': { 208 | 'list': () => {}, 209 | 'action': () => {} 210 | } 211 | }; 212 | 213 | hapiRaml = new HapiRaml(fakeServer, fakeControllersMap, ramlPath); 214 | 215 | return expect(hapiRaml.hookup()).to.be.rejectedWith(/Tried to find 'fetch()' on Controller '\w+' but it did not exist/); 216 | }); 217 | 218 | it('should call server.route() for any functions listed in the RAML that are present on the Controller', () => { 219 | let routeStub = sinon.stub(fakeServer, 'route', () => { 220 | 221 | }); 222 | 223 | fakeControllersMap = { 224 | 'ControllerTest': { 225 | 'list': () => {}, 226 | 'fetch': () => {}, 227 | 'action': () => {} 228 | } 229 | }; 230 | 231 | hapiRaml = new HapiRaml(fakeServer, fakeControllersMap, ramlPath); 232 | 233 | return hapiRaml.hookup() 234 | .then(() => { 235 | return expect(routeStub).to.have.been.called; 236 | }); 237 | }); 238 | 239 | describe('auth', () => { 240 | let routeStub, 241 | routeMap; 242 | 243 | beforeEach(() => { 244 | routeStub = sinon.stub(fakeServer, 'route', () => {}); 245 | 246 | mockFS({ 247 | 'test.raml': '' 248 | }); 249 | 250 | fakeControllersMap = { 251 | 'ControllerTest': { 252 | 'list': () => {}, 253 | 'fetch': () => {}, 254 | 'action': () => {} 255 | } 256 | }; 257 | 258 | hapiRaml = new HapiRaml(fakeServer, fakeControllersMap, ramlPath); 259 | 260 | sinon.stub(hapiRaml.raml, 'getRouteMap', () => { 261 | return new Promise((resolve, reject) => { 262 | resolve(routeMap); 263 | }); 264 | }); 265 | }); 266 | 267 | it('should call server.route() with an auth config when a route has an associated authStrategy', () => { 268 | routeMap = [ 269 | { 270 | 'className': 'ControllerTest', 271 | 'classFunction': 'list', 272 | 'uri': '/', 273 | 'method': 'GET', 274 | 'authStrategy': ['jwt'] 275 | } 276 | ]; 277 | 278 | return hapiRaml.hookup() 279 | .then(() => { 280 | let expectedArgs = sinon.match({ 281 | config: sinon.match({ 282 | auth: sinon.match.truthy 283 | }) 284 | }); 285 | 286 | return expect(routeStub).to.have.been.calledWith(expectedArgs); 287 | }); 288 | }); 289 | 290 | it('should set the auth config mode to be required when the authStrategy does not contain `null`', () => { 291 | routeMap = [ 292 | { 293 | 'className': 'ControllerTest', 294 | 'classFunction': 'list', 295 | 'uri': '/', 296 | 'method': 'GET', 297 | 'authStrategy': ['jwt'] 298 | } 299 | ]; 300 | 301 | return hapiRaml.hookup() 302 | .then(() => { 303 | let expectedArgs = sinon.match({ 304 | config: sinon.match({ 305 | auth: sinon.match({ 306 | mode: 'required' 307 | }) 308 | }) 309 | }); 310 | 311 | return expect(routeStub).to.have.been.calledWith(expectedArgs); 312 | }); 313 | }); 314 | 315 | it('should set the auth config mode to be optional when the authStrategy contains null', () => { 316 | routeMap = [ 317 | { 318 | 'className': 'ControllerTest', 319 | 'classFunction': 'list', 320 | 'uri': '/', 321 | 'method': 'GET', 322 | 'authStrategy': [null, 'jwt'] 323 | } 324 | ]; 325 | 326 | return hapiRaml.hookup() 327 | .then(() => { 328 | let expectedArgs = sinon.match({ 329 | config: sinon.match({ 330 | auth: sinon.match({ 331 | mode: 'optional' 332 | }) 333 | }) 334 | }); 335 | 336 | return expect(routeStub).to.have.been.calledWith(expectedArgs); 337 | }); 338 | }); 339 | 340 | it('should set the auth config strategies to match all the authStrategy elements', () => { 341 | routeMap = [ 342 | { 343 | 'className': 'ControllerTest', 344 | 'classFunction': 'list', 345 | 'uri': '/', 346 | 'method': 'GET', 347 | 'authStrategy': ['jwt', 'oauth'] 348 | } 349 | ]; 350 | 351 | return hapiRaml.hookup() 352 | .then(() => { 353 | let expectedArgs = sinon.match({ 354 | config: sinon.match({ 355 | auth: sinon.match({ 356 | strategies: ['jwt', 'oauth'] 357 | }) 358 | }) 359 | }); 360 | 361 | return expect(routeStub).to.have.been.calledWith(expectedArgs); 362 | }); 363 | }); 364 | 365 | it('should not include `null` in the auth config strategies', () => { 366 | routeMap = [ 367 | { 368 | 'className': 'ControllerTest', 369 | 'classFunction': 'list', 370 | 'uri': '/', 371 | 'method': 'GET', 372 | 'authStrategy': [null, 'jwt'] 373 | } 374 | ]; 375 | 376 | return hapiRaml.hookup() 377 | .then(() => { 378 | let expectedArgs = sinon.match({ 379 | config: sinon.match({ 380 | auth: sinon.match({ 381 | strategies: ['jwt'] 382 | }) 383 | }) 384 | }); 385 | 386 | return expect(routeStub).to.have.been.calledWith(expectedArgs); 387 | }); 388 | }); 389 | 390 | it('should set the auth config to be false if null is the only authStrategy', () => { 391 | routeMap = [ 392 | { 393 | 'className': 'ControllerTest', 394 | 'classFunction': 'list', 395 | 'uri': '/', 396 | 'method': 'GET', 397 | 'authStrategy': [null] 398 | } 399 | ]; 400 | 401 | return hapiRaml.hookup() 402 | .then(() => { 403 | let expectedArgs = sinon.match({ 404 | config: sinon.match({ 405 | auth: false 406 | }) 407 | }); 408 | 409 | return expect(routeStub).to.have.been.calledWith(expectedArgs); 410 | }); 411 | }); 412 | 413 | it('should not set the auth config if there are no authStrategys', () => { 414 | routeMap = [ 415 | { 416 | 'className': 'ControllerTest', 417 | 'classFunction': 'list', 418 | 'uri': '/', 419 | 'method': 'GET' 420 | } 421 | ]; 422 | 423 | return hapiRaml.hookup() 424 | .then(() => { 425 | let expectedArgs = sinon.match({ 426 | config: sinon.match({ 427 | auth: sinon.falsy 428 | }) 429 | }); 430 | 431 | return expect(routeStub).to.have.been.calledWith(expectedArgs); 432 | }); 433 | }); 434 | }); 435 | }); 436 | }); 437 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | test 2 | --recursive 3 | -------------------------------------------------------------------------------- /test/rootTest.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | --- 3 | title: Test API 4 | baseUri: https://test.raml.net/api/{version} 5 | version: v1 6 | mediaType: application/json 7 | resourceTypes: 8 | - collection: 9 | get: 10 | responses: 11 | 200: 12 | body: 13 | - collection-item: 14 | get: 15 | responses: 16 | 200: 17 | body: 18 | 19 | /: 20 | type: collection 21 | get: 22 | /test: 23 | type: collection 24 | get: 25 | /subTest: 26 | type: collection 27 | get: 28 | /item: 29 | type: collection-item 30 | get: 31 | -------------------------------------------------------------------------------- /test/test.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | --- 3 | title: Test API 4 | baseUri: https://test.raml.net/api/{version} 5 | version: v1 6 | mediaType: application/json 7 | resourceTypes: 8 | - collection: 9 | get: 10 | responses: 11 | 200: 12 | body: 13 | - collection-item: 14 | get: 15 | responses: 16 | 200: 17 | body: 18 | 19 | /test: 20 | type: collection 21 | displayName: Users 22 | get: 23 | /{id}: 24 | type: collection-item 25 | get: 26 | /{id}/action: 27 | post: 28 | -------------------------------------------------------------------------------- /test/utils/RAML.test.js: -------------------------------------------------------------------------------- 1 | /*eslint-env mocha */ 2 | /*eslint-disable no-unused-expressions*/ 3 | 'use strict'; 4 | 5 | import chai, {expect} from 'chai'; 6 | import chaiAsPromised from 'chai-as-promised'; 7 | import sinon from 'sinon'; 8 | import sinonChai from 'sinon-chai'; 9 | import mock_fs from 'mock-fs'; 10 | 11 | chai.use(sinonChai); 12 | chai.use(chaiAsPromised); 13 | 14 | import RAML from '../../src/utils/RAML'; 15 | 16 | describe('RAML', () => { 17 | let newRAML; 18 | 19 | describe('constructor', () => { 20 | let mockFS, 21 | mockParser, 22 | ramlPath; 23 | 24 | beforeEach(() => { 25 | ramlPath = './test.raml'; 26 | mockFS = mock_fs.fs({ 27 | 'test.raml': 'test' 28 | }); 29 | mockParser = {}; 30 | }); 31 | 32 | it('should throw if not passed fs dependency', () => { 33 | expect(() => { 34 | newRAML = new RAML(); 35 | }).to.throw(/Missing `\w+` dependency/); 36 | }); 37 | 38 | it('should throw if not passed parser dependency', () => { 39 | expect(() => { 40 | newRAML = new RAML(mockFS); 41 | }).to.throw(/Missing `\w+` dependency/); 42 | }); 43 | 44 | it('should throw if not passed a path to RAML file', () => { 45 | expect(() => { 46 | newRAML = new RAML(mockFS, mockParser); 47 | }).to.throw('Missing path to RAML file'); 48 | }); 49 | 50 | it('should throw if the path to the raml file does not exist', () => { 51 | let mockFSMissingRAML = mock_fs.fs({}); 52 | 53 | expect(() => { 54 | newRAML = new RAML(mockFSMissingRAML, mockParser, ramlPath); 55 | }).to.throw('path to RAML file does not exist'); 56 | }); 57 | 58 | it('should not throw if passed all required properties', () => { 59 | expect(() => { 60 | newRAML = new RAML(mockFS, mockParser, ramlPath); 61 | }).to.not.throw(); 62 | }); 63 | }); 64 | 65 | describe('loadRAMLFile()', () => { 66 | let mockParser; 67 | 68 | beforeEach(() => { 69 | let ramlPath = './test.raml'; 70 | 71 | let mockFS = mock_fs.fs({ 72 | 'test.raml': 'test' 73 | }); 74 | 75 | mockParser = { 76 | loadApi: () => { 77 | return new Promise((resolve) => {}); 78 | } 79 | }; 80 | 81 | newRAML = new RAML(mockFS, mockParser, ramlPath); 82 | }); 83 | 84 | it('should return a Promise', () => { 85 | expect(newRAML.loadRAMLFile()).to.be.an.instanceOf(Promise); 86 | }); 87 | 88 | it('should load the file content and pass it to the parser', () => { 89 | let loadStub = sinon.stub(mockParser, 'loadApi', () => { 90 | return new Promise((resolve, reject) => { 91 | resolve(); 92 | }); 93 | }); 94 | 95 | return newRAML.loadRAMLFile() 96 | .then(() => { 97 | return expect(loadStub).to.have.been.called; 98 | }); 99 | }); 100 | 101 | it('should reject if the raml does not parse with a baseUri property', () => { 102 | let loadStub = sinon.stub(mockParser, 'loadApi', () => { 103 | return new Promise((resolve, reject) => { 104 | resolve('parsed'); 105 | }); 106 | }); 107 | 108 | return expect(newRAML.loadRAMLFile()).to.eventually.be.rejectedWith('Missing `baseUri` property'); 109 | }); 110 | 111 | it('should resolve with the parsed raml', () => { 112 | let expectedAST = { 113 | 'baseUri': () => { return 'http://'; } 114 | }; 115 | 116 | let loadStub = sinon.stub(mockParser, 'loadApi', () => { 117 | return new Promise((resolve, reject) => { 118 | resolve(expectedAST); 119 | }); 120 | }); 121 | 122 | return expect(newRAML.loadRAMLFile()).to.eventually.deep.equal(expectedAST); 123 | }); 124 | 125 | it('should reject if the raml fails to parse', () => { 126 | let loadStub = sinon.stub(mockParser, 'loadApi', () => { 127 | return new Promise((resolve, reject) => { 128 | reject(); 129 | }); 130 | }); 131 | 132 | return (expect(newRAML.loadRAMLFile())).to.eventually.be.rejected; 133 | }); 134 | }); 135 | 136 | describe('getRouteMap()', () => { 137 | let mockParser; 138 | 139 | beforeEach(() => { 140 | let ramlPath = './test.raml'; 141 | 142 | let mockFS = mock_fs.fs({ 143 | 'test.raml': 'test' 144 | }); 145 | 146 | mockParser = { 147 | loadApi: () => { 148 | return new Promise((resolve) => { 149 | resolve({ 150 | baseUri: () => { return { value: () => { return 'http://'; } } }, 151 | resources: () => { 152 | return []; 153 | } 154 | }); 155 | }); 156 | } 157 | }; 158 | 159 | newRAML = new RAML(mockFS, mockParser, ramlPath); 160 | }); 161 | 162 | it('should return a Promise', () => { 163 | expect(newRAML.getRouteMap()).to.be.an.instanceOf(Promise); 164 | }); 165 | 166 | it('should eventually resolve with an Array', () => { 167 | return expect(newRAML.getRouteMap()).to.eventually.be.an('Array'); 168 | }); 169 | 170 | it('should reject if given invalid resources', () => { 171 | let resources = [{}]; 172 | 173 | sinon.stub(mockParser, 'loadApi', () => { 174 | return new Promise((resolve) => { 175 | resolve({ 176 | baseUri: () => { return { value: () => { return 'http://'; } } }, 177 | resources: () => { return resources; } 178 | }); 179 | }); 180 | }); 181 | 182 | return expect(newRAML.getRouteMap()).to.eventually.be.rejected; 183 | }); 184 | 185 | it('should reject if given resources with no methods', () => { 186 | let resources = [{ 187 | relativeUri: () => { return { value: () => { return '/objects'; } } } 188 | }]; 189 | 190 | sinon.stub(mockParser, 'loadApi', () => { 191 | return new Promise((resolve) => { 192 | resolve({ 193 | baseUri: () => { return { value: () => { return 'http://'; } } }, 194 | resources: () => { return resources; } 195 | }); 196 | }); 197 | }); 198 | 199 | return expect(newRAML.getRouteMap()).to.eventually.be.rejected; 200 | }); 201 | 202 | it('should reject if given a resource with invalid sub-resources', () => { 203 | let resources = [{ 204 | relativeUri: () => { return { value: () => { return '/objects'; } } }, 205 | methods: () => { return [{ method: () => { return 'get'; } }]; }, 206 | resources: () => { return [{}]; } 207 | }]; 208 | 209 | sinon.stub(mockParser, 'loadApi', () => { 210 | return new Promise((resolve) => { 211 | resolve({ 212 | baseUri: () => { return { value: () => { return 'http://'; } } }, 213 | resources: () => { return resources; } 214 | }); 215 | }); 216 | }); 217 | 218 | return expect(newRAML.getRouteMap()).to.eventually.be.rejected; 219 | }); 220 | 221 | it('should parse only the resources it is given', () => { 222 | let resources = [ 223 | { 224 | relativeUri: () => { return { value: () => { return '/objects'; } } }, 225 | type: () => { return { name: () => { return 'collection'; } } }, 226 | methods: () => { return [{ method: () => { return 'get'; } }]; } 227 | }]; 228 | 229 | sinon.stub(mockParser, 'loadApi', () => { 230 | return new Promise((resolve) => { 231 | resolve({ 232 | baseUri: () => { return { value: () => { return 'http://'; } } }, 233 | resources: () => { return resources; } 234 | }); 235 | }); 236 | }); 237 | 238 | return newRAML.getRouteMap() 239 | .then((mappedRoutes) => { 240 | return expect(mappedRoutes).to.be.of.length(resources.length); 241 | }); 242 | }); 243 | 244 | it('should convert URIs of `collection` resources to be Controller classes', () => { 245 | let expectedClass = 'ControllerObject'; 246 | let resources = [ 247 | { 248 | relativeUri: () => { return { value: () => { return '/objects'; } } }, 249 | type: () => { return { name: () => { return 'collection'; } } }, 250 | methods: () => { return [{ method: () => { return 'get'; } }]; } 251 | }]; 252 | 253 | sinon.stub(mockParser, 'loadApi', () => { 254 | return new Promise((resolve) => { 255 | resolve({ 256 | baseUri: () => { return { value: () => { return 'http://'; } } }, 257 | resources: () => { return resources; } 258 | }); 259 | }); 260 | }); 261 | 262 | return newRAML.getRouteMap() 263 | .then((mappedRoutes) => { 264 | let mappedRoute = mappedRoutes[0]; 265 | 266 | return expect(mappedRoute.className).to.equal(expectedClass); 267 | }); 268 | }); 269 | 270 | it('should convert URIs of `collection-item` resources to be Controller classes', () => { 271 | let expectedClass = 'ControllerObject'; 272 | let resources = [ 273 | { 274 | relativeUri: () => { return { value: () => { return '/objects'; } } }, 275 | type: () => { return { name: () => { return 'collection-item'; } } }, 276 | methods: () => { return [{ method: () => { return 'get'; } }]; } 277 | }]; 278 | 279 | sinon.stub(mockParser, 'loadApi', () => { 280 | return new Promise((resolve) => { 281 | resolve({ 282 | baseUri: () => { return { value: () => { return 'http://'; } } }, 283 | resources: () => { return resources; } 284 | }); 285 | }); 286 | }); 287 | 288 | return newRAML.getRouteMap() 289 | .then((mappedRoutes) => { 290 | let mappedRoute = mappedRoutes[0]; 291 | 292 | return expect(mappedRoute.className).to.equal(expectedClass); 293 | }); 294 | }); 295 | 296 | it('should convert URIs with no resource type to be Controller classes', () => { 297 | let expectedClass = 'ControllerObject'; 298 | let resources = [ 299 | { 300 | relativeUri: () => { return { value: () => { return '/objects'; } } }, 301 | methods: () => { return [{ method: () => { return 'get'; } }]; } 302 | }]; 303 | 304 | sinon.stub(mockParser, 'loadApi', () => { 305 | return new Promise((resolve) => { 306 | resolve({ 307 | baseUri: () => { return { value: () => { return 'http://'; } } }, 308 | resources: () => { return resources; } 309 | }); 310 | }); 311 | }); 312 | 313 | return newRAML.getRouteMap() 314 | .then((mappedRoutes) => { 315 | let mappedRoute = mappedRoutes[0]; 316 | 317 | return expect(mappedRoute.className).to.equal(expectedClass); 318 | }); 319 | }); 320 | 321 | it('should convert URIs of `collection` resources with no subpath in the URI to be `list()` commands', () => { 322 | let expectedClassFunction = 'list'; 323 | let resources = [ 324 | { 325 | relativeUri: () => { return { value: () => { return '/objects'; } } }, 326 | type: () => { return { name: () => { return 'collection'; } } }, 327 | methods: () => { return [{ method: () => { return 'get'; } }]; } 328 | }]; 329 | 330 | sinon.stub(mockParser, 'loadApi', () => { 331 | return new Promise((resolve) => { 332 | resolve({ 333 | baseUri: () => { return { value: () => { return 'http://'; } } }, 334 | resources: () => { return resources; } 335 | }); 336 | }); 337 | }); 338 | 339 | return newRAML.getRouteMap() 340 | .then((mappedRoutes) => { 341 | let mappedRoute = mappedRoutes[0]; 342 | 343 | return expect(mappedRoute.classFunction).to.equal(expectedClassFunction); 344 | }); 345 | }); 346 | 347 | it('should convert URIs of `collection` resources with no subpath in the URI an a method of `post` to be `create()` commands', () => { 348 | let expectedClassFunction = 'create'; 349 | let resources = [ 350 | { 351 | relativeUri: () => { return { value: () => { return '/objects'; } } }, 352 | type: () => { return { name: () => { return 'collection'; } } }, 353 | methods: () => { return [{ method: () => { return 'post'; } }]; } 354 | }]; 355 | 356 | sinon.stub(mockParser, 'loadApi', () => { 357 | return new Promise((resolve) => { 358 | resolve({ 359 | baseUri: () => { return { value: () => { return 'http://'; } } }, 360 | resources: () => { return resources; } 361 | }); 362 | }); 363 | }); 364 | 365 | return newRAML.getRouteMap() 366 | .then((mappedRoutes) => { 367 | let mappedRoute = mappedRoutes[0]; 368 | 369 | return expect(mappedRoute.classFunction).to.equal(expectedClassFunction); 370 | }); 371 | }); 372 | 373 | it('should convert URIs of `collection` resources with a subpath in the URI to be commands matching the subpath', () => { 374 | let expectedClassFunction = 'create'; 375 | let resources = [ 376 | { 377 | relativeUri: () => { return { value: () => { return '/objects/create'; } } }, 378 | type: () => { return { name: () => { return 'collection'; } } }, 379 | methods: () => { return [{ method: () => { return 'get'; } }]; } 380 | }]; 381 | 382 | sinon.stub(mockParser, 'loadApi', () => { 383 | return new Promise((resolve) => { 384 | resolve({ 385 | baseUri: () => { return { value: () => { return 'http://'; } } }, 386 | resources: () => { return resources; } 387 | }); 388 | }); 389 | }); 390 | 391 | return newRAML.getRouteMap() 392 | .then((mappedRoutes) => { 393 | let mappedRoute = mappedRoutes[0]; 394 | 395 | return expect(mappedRoute.classFunction).to.equal(expectedClassFunction); 396 | }); 397 | }); 398 | 399 | it('should convert URIs of `collection-item` resources with a subpath in the URI to be commands matching the subpath', () => { 400 | let expectedClassFunction = 'create'; 401 | let resources = [ 402 | { 403 | relativeUri: () => { return { value: () => { return '/objects/create'; } } }, 404 | type: () => { return { name: () => { return 'collection-item'; } } }, 405 | methods: () => { return [{ method: () => { return 'get'; } }]; } 406 | }]; 407 | 408 | sinon.stub(mockParser, 'loadApi', () => { 409 | return new Promise((resolve) => { 410 | resolve({ 411 | baseUri: () => { return { value: () => { return 'http://'; } } }, 412 | resources: () => { return resources; } 413 | }); 414 | }); 415 | }); 416 | 417 | return newRAML.getRouteMap() 418 | .then((mappedRoutes) => { 419 | let mappedRoute = mappedRoutes[0]; 420 | 421 | return expect(mappedRoute.classFunction).to.equal(expectedClassFunction); 422 | }); 423 | }); 424 | 425 | it('should convert URIs of resources with no type and a subpath in the URI to be commands matching the subpath', () => { 426 | let expectedClassFunction = 'create'; 427 | let resources = [ 428 | { 429 | relativeUri: () => { return { value: () => { return '/create'; } } }, 430 | methods: () => { return [{ method: () => { return 'get'; } }]; } 431 | }]; 432 | 433 | sinon.stub(mockParser, 'loadApi', () => { 434 | return new Promise((resolve) => { 435 | resolve({ 436 | baseUri: () => { return { value: () => { return 'http://'; } } }, 437 | resources: () => { return resources; } 438 | }); 439 | }); 440 | }); 441 | 442 | return newRAML.getRouteMap() 443 | .then((mappedRoutes) => { 444 | let mappedRoute = mappedRoutes[0]; 445 | 446 | return expect(mappedRoute.classFunction).to.equal(expectedClassFunction); 447 | }); 448 | }); 449 | 450 | it('should convert URIs of resource type `collection-item` and a subpath of {id} and a method of `get` to be `fetch()` commands', () => { 451 | let expectedClassFunction = 'fetch'; 452 | let resources = [ 453 | { 454 | relativeUri: () => { return { value: () => { return '/{id}'; } } }, 455 | type: () => { return { name: () => { return 'collection-item'; } } }, 456 | methods: () => { return [{ method: () => { return 'get'; } }]; } 457 | }]; 458 | 459 | sinon.stub(mockParser, 'loadApi', () => { 460 | return new Promise((resolve) => { 461 | resolve({ 462 | baseUri: () => { return { value: () => { return 'http://'; } } }, 463 | resources: () => { return resources; } 464 | }); 465 | }); 466 | }); 467 | 468 | return newRAML.getRouteMap() 469 | .then((mappedRoutes) => { 470 | let mappedRoute = mappedRoutes[0]; 471 | 472 | return expect(mappedRoute.classFunction).to.equal(expectedClassFunction); 473 | }); 474 | }); 475 | 476 | it('should convert URIs of resource type `collection-item` and a subpath of {id} and a method of `delete` to be `delete()` commands', () => { 477 | let expectedClassFunction = 'delete'; 478 | let resources = [ 479 | { 480 | relativeUri: () => { return { value: () => { return '/{id}'; } } }, 481 | type: () => { return { name: () => { return 'collection-item'; } } }, 482 | methods: () => { return [{ method: () => { return 'delete'; } }]; } 483 | }]; 484 | 485 | sinon.stub(mockParser, 'loadApi', () => { 486 | return new Promise((resolve) => { 487 | resolve({ 488 | baseUri: () => { return { value: () => { return 'http://'; } } }, 489 | resources: () => { return resources; } 490 | }); 491 | }); 492 | }); 493 | 494 | return newRAML.getRouteMap() 495 | .then((mappedRoutes) => { 496 | let mappedRoute = mappedRoutes[0]; 497 | 498 | return expect(mappedRoute.classFunction).to.equal(expectedClassFunction); 499 | }); 500 | }); 501 | 502 | it('should convert URIs of resource type `collection-item` and a subpath of {id} and a method of `post` to be `update()` commands', () => { 503 | let expectedClassFunction = 'update'; 504 | let resources = [ 505 | { 506 | relativeUri: () => { return { value: () => { return '/{id}'; } } }, 507 | type: () => { return { name: () => { return 'collection-item'; } } }, 508 | methods: () => { return [{ method: () => { return 'post'; } }]; } 509 | }]; 510 | 511 | sinon.stub(mockParser, 'loadApi', () => { 512 | return new Promise((resolve) => { 513 | resolve({ 514 | baseUri: () => { return { value: () => { return 'http://'; } } }, 515 | resources: () => { return resources; } 516 | }); 517 | }); 518 | }); 519 | 520 | return newRAML.getRouteMap() 521 | .then((mappedRoutes) => { 522 | let mappedRoute = mappedRoutes[0]; 523 | 524 | return expect(mappedRoute.classFunction).to.equal(expectedClassFunction); 525 | }); 526 | }); 527 | 528 | it('should convert URIs of resource type `collection-item` and a subpath of {id} and a method of `patch` to be `update()` commands', () => { 529 | let expectedClassFunction = 'update'; 530 | let resources = [ 531 | { 532 | relativeUri: () => { return { value: () => { return '/{id}'; } } }, 533 | type: () => { return { name: () => { return 'collection-item'; } } }, 534 | methods: () => { return [{ method: () => { return 'patch'; } }]; } 535 | }]; 536 | 537 | sinon.stub(mockParser, 'loadApi', () => { 538 | return new Promise((resolve) => { 539 | resolve({ 540 | baseUri: () => { return { value: () => { return 'http://'; } } }, 541 | resources: () => { return resources; } 542 | }); 543 | }); 544 | }); 545 | 546 | return newRAML.getRouteMap() 547 | .then((mappedRoutes) => { 548 | let mappedRoute = mappedRoutes[0]; 549 | 550 | return expect(mappedRoute.classFunction).to.equal(expectedClassFunction); 551 | }); 552 | }); 553 | 554 | it('should parse sub resources of a resource', () => { 555 | let resources = [ 556 | { 557 | relativeUri: () => { return { value: () => { return '/objects'; } } }, 558 | type: () => { return { name: () => { return 'collection'; } } }, 559 | methods: () => { return [{ method: () => { return 'get'; } }]; }, 560 | resources: () => { 561 | return [{ 562 | relativeUri: () => { 563 | return { 564 | value: () => { 565 | return '/{id}'; 566 | } 567 | } 568 | }, 569 | methods: () => { return [{ method: () => { return 'get'; } }]; } 570 | }] 571 | } 572 | }]; 573 | 574 | sinon.stub(mockParser, 'loadApi', () => { 575 | return new Promise((resolve) => { 576 | resolve({ 577 | baseUri: () => { return { value: () => { return 'http://'; } } }, 578 | resources: () => { return resources; } 579 | }); 580 | }); 581 | }); 582 | 583 | return newRAML.getRouteMap() 584 | .then((mappedRoutes) => { 585 | return expect(mappedRoutes).to.be.of.length(2); 586 | }); 587 | }); 588 | 589 | it('should set the auth strategy for a route to be null by default', () => { 590 | let resources = [ 591 | { 592 | relativeUri: () => { return { value: () => { return '/objects'; } } }, 593 | type: () => { return { name: () => { return 'collection'; } } }, 594 | methods: () => { return [{ method: () => { return 'get'; } }]; } 595 | }]; 596 | 597 | let expectedAuthStrategies = [null]; 598 | 599 | sinon.stub(mockParser, 'loadApi', () => { 600 | return new Promise((resolve) => { 601 | resolve({ 602 | baseUri: () => { return { value: () => { return 'http://'; } } }, 603 | resources: () => { return resources; } 604 | }); 605 | }); 606 | }); 607 | 608 | return newRAML.getRouteMap() 609 | .then((mappedRoutes) => { 610 | let mappedRoute = mappedRoutes[0]; 611 | return expect(mappedRoute.authStrategy).to.deep.equal(expectedAuthStrategies); 612 | }); 613 | }); 614 | 615 | it('should set the auth strategy to be the default strategy for the API if not specifically defined for a route', () => { 616 | let resources = [ 617 | { 618 | relativeUri: () => { return { value: () => { return '/objects'; } } }, 619 | type: () => { return { name: () => { return 'collection'; } } }, 620 | methods: () => { return [{ method: () => { return 'patch'; } }]; } 621 | }]; 622 | 623 | let expectedAuthStrategies = [{ 624 | securitySchemeName: () => { return 'jwt'; } 625 | }]; 626 | 627 | sinon.stub(mockParser, 'loadApi', () => { 628 | return new Promise((resolve) => { 629 | resolve({ 630 | baseUri: () => { return { value: () => { return 'https://'; } } }, 631 | securedBy: () => { return expectedAuthStrategies; }, 632 | resources: () => { return resources; } 633 | }); 634 | }); 635 | }); 636 | 637 | return newRAML.getRouteMap() 638 | .then((mappedRoutes) => { 639 | let mappedRoute = mappedRoutes[0]; 640 | return expect(mappedRoute.authStrategy).to.deep.equal('jwt'); 641 | }); 642 | }); 643 | 644 | it('should override the default strategy for the API if defined for a route', () => { 645 | let expectedAuthStrategies = [{ 646 | securitySchemeName: () => { return 'oauth'; } 647 | }]; 648 | 649 | let resources = [ 650 | { 651 | relativeUri: () => { return { value: () => { return '/objects'; } } }, 652 | type: () => { return { name: () => { return 'collection'; } } }, 653 | methods: () => { return [{ method: () => { return 'get'; }, securedBy: () => { return expectedAuthStrategies; } }]; } 654 | }]; 655 | 656 | sinon.stub(mockParser, 'loadApi', () => { 657 | return new Promise((resolve) => { 658 | resolve({ 659 | baseUri: () => { return { value: () => { return 'http://'; } } }, 660 | securedBy: () => { return [{ securitySchemeName: () => { return 'jwt'; } }]; }, 661 | resources: () => { return resources; } 662 | }); 663 | }); 664 | }); 665 | 666 | return newRAML.getRouteMap() 667 | .then((mappedRoutes) => { 668 | let mappedRoute = mappedRoutes[0]; 669 | return expect(mappedRoute.authStrategy).to.deep.equal('oauth'); 670 | }); 671 | }); 672 | }); 673 | }); 674 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | var babel = require('babel-core'); 2 | 3 | module.exports = function (wallaby) { 4 | return { 5 | files: [ 6 | 'src/**/*.js', 7 | 'test/*.raml' 8 | ], 9 | 10 | tests: [ 11 | 'test/**/*.test.js' 12 | ], 13 | 14 | compilers: { 15 | '**/*.js': wallaby.compilers.babel({ 16 | babel: babel, 17 | presets: ['es2015'], 18 | plugins: ['transform-runtime'] 19 | }) 20 | }, 21 | env: { 22 | type: 'node' 23 | }, 24 | workers: { 25 | recycle: true 26 | } 27 | }; 28 | }; 29 | --------------------------------------------------------------------------------