From f8ed84574a813722ec0cfa783a2807060c815eca Mon Sep 17 00:00:00 2001 From: Scott Ganyo Date: Fri, 27 Mar 2015 17:13:24 -0700 Subject: [PATCH] light it up --- .gitignore | 31 ++ .jshintrc | 24 ++ .travis.yml | 5 + LICENSE | 23 ++ README.md | 135 +++++++ bin/swagger | 52 +++ bin/swagger-project | 72 ++++ config/editor-branding/branding.css | 10 + config/editor-branding/left.html | 5 + config/editor-branding/right.html | 3 + config/index.js | 71 ++++ config/swagger-editor.config.json | 22 ++ lib/commands/project/project.js | 371 ++++++++++++++++++ lib/commands/project/swagger_editor.js | 107 +++++ lib/util/browser.js | 93 +++++ lib/util/cli.js | 207 ++++++++++ lib/util/feedback.js | 47 +++ lib/util/net.js | 96 +++++ package.json | 47 +++ project-skeleton/.gitignore | 34 ++ project-skeleton/README.md | 1 + project-skeleton/api/controllers/README.md | 1 + .../api/controllers/hello_world.js | 44 +++ project-skeleton/api/helpers/README.md | 3 + project-skeleton/api/mocks/README.md | 1 + project-skeleton/api/swagger/swagger.yaml | 58 +++ project-skeleton/app.js | 31 ++ project-skeleton/config/README.md | 5 + project-skeleton/package.json | 18 + .../test/api/controllers/README.md | 1 + .../test/api/controllers/hello_world.js | 50 +++ project-skeleton/test/api/helpers/README.md | 1 + test/commands/project/badswagger.yaml | 62 +++ test/commands/project/project.js | 320 +++++++++++++++ test/commands/project/swagger_editor.js | 110 ++++++ test/config.js | 58 +++ test/helpers.js | 92 +++++ test/util/browser.js | 124 ++++++ test/util/cli.js | 299 ++++++++++++++ test/util/net.js | 114 ++++++ 40 files changed, 2848 insertions(+) create mode 100644 .gitignore create mode 100644 .jshintrc create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100755 bin/swagger create mode 100755 bin/swagger-project create mode 100644 config/editor-branding/branding.css create mode 100644 config/editor-branding/left.html create mode 100644 config/editor-branding/right.html create mode 100644 config/index.js create mode 100644 config/swagger-editor.config.json create mode 100644 lib/commands/project/project.js create mode 100644 lib/commands/project/swagger_editor.js create mode 100644 lib/util/browser.js create mode 100644 lib/util/cli.js create mode 100644 lib/util/feedback.js create mode 100644 lib/util/net.js create mode 100644 package.json create mode 100644 project-skeleton/.gitignore create mode 100755 project-skeleton/README.md create mode 100755 project-skeleton/api/controllers/README.md create mode 100755 project-skeleton/api/controllers/hello_world.js create mode 100755 project-skeleton/api/helpers/README.md create mode 100755 project-skeleton/api/mocks/README.md create mode 100755 project-skeleton/api/swagger/swagger.yaml create mode 100755 project-skeleton/app.js create mode 100755 project-skeleton/config/README.md create mode 100755 project-skeleton/package.json create mode 100755 project-skeleton/test/api/controllers/README.md create mode 100644 project-skeleton/test/api/controllers/hello_world.js create mode 100755 project-skeleton/test/api/helpers/README.md create mode 100644 test/commands/project/badswagger.yaml create mode 100644 test/commands/project/project.js create mode 100644 test/commands/project/swagger_editor.js create mode 100644 test/config.js create mode 100644 test/helpers.js create mode 100644 test/util/browser.js create mode 100644 test/util/cli.js create mode 100644 test/util/net.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..be222a27 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# IDE files +.idea + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# Commenting this out is preferred by some people, see +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules + +# Users Environment Variables +.lock-wscript diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 00000000..e21f1655 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,24 @@ +{ + "bitwise":true, + "curly":true, + "eqeqeq":true, + "forin":true, + "newcap":true, + "noarg":true, + "noempty":true, + "nonew":true, + "undef":true, + "strict":true, + "node":true, + "indent":2, + "expr":true, + "globals" : { + /* MOCHA */ + "describe" : false, + "it" : false, + "before" : false, + "beforeEach" : false, + "after" : false, + "afterEach" : false + } +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..5737015a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: + - "0.12" + - "0.10" + - "iojs" diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..53a21cdb --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +/**************************************************************************** + The MIT License (MIT) + + Copyright (c) 2014 Apigee Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ****************************************************************************/ diff --git a/README.md b/README.md new file mode 100644 index 00000000..26a23fd4 --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +# swagger-node reference + +This is the installation guide and command reference for `swagger`, the command-line interface for swagger-node. + +* Prerequisites +* Installation +* Commands + +# Prerequisites + +* [Node.js](http://nodejs.org/download/) (v0.10.24+) +* [npm](https://docs.npmjs.com/getting-started/installing-node) (v1.3.0+) + +# Installation + +You can install `swagger-node` either through npm or by cloning and linking the code from GitHub. +This document covers the installation details for installing from npm. + +## Installation from npm + +The `swagger-node` module and its dependencies are designed for Node.js and is available through npm. + +### Linux / Mac from a Terminal Window: + + sudo npm install -g swagger-node + +NOTE: `sudo` may or may not be required with the `-g` option depending on your configuration. If you do not +use `-g`, you may need to add the `swagger-node/bin` directory to your PATH manually. On unix-based machines +the bin directory will often be found here: `/usr/local/lib/node_modules/swagger-node/bin`. + +### Windows, from a Command Prompt: + + npm install -g swagger-node + +# Command reference + +To print a list of valid commands, just run `swagger` with no options or -h: + + $ swagger -h + + Usage: swagger [options] [command] + + + Commands: + + project project actions + docs open Swagger documentation + help [cmd] display help for [cmd] + + Options: + + -h, --help output usage information + -V, --version output the version number + +docs links: + +* [project](#project) +* [docs](#docs) + +## project + +Create and manage swagger-node projects on your local machine. + + $ swagger project -h + + Usage: swagger-project [options] [command] + + Commands: + + create Create a folder containing a Swagger project + start [options] [directory] Start the project in this or the specified directory + verify [options] [directory] Verify that the project is correct (swagger, config, etc.) + edit [options] [directory] open Swagger editor for this project + open [directory] open browser as client to the project + test [options] [directory_or_file] Run project tests + +docs links: + +* [create](#create) +* [start](#start) +* [verify](#verify) +* [edit](#edit) +* [open](#open) +* [test](#test) + +### create + +Create a new swagger-node project with the given name in a folder of the same name. + +### start + +Start the API server in the directory you are in - or, optionally, another directory. The server +will automatically restart when you make changes to the project. + + $ swagger project start -h + + Usage: start [options] [directory] + + Start the project in this or the specified directory + + Options: + + -h, --help output usage information + -d, --debug start in remote debug mode + -b, --debug-brk start in remote debug mode, wait for debugger connect + -m, --mock start in mock mode + -o, --open open browser as client to the project + +`-debug` and `-debug-brk` will start the project in debug mode so that you can connect to it via a debugger. + +`-mock` will choose controllers from your mock directory instead of your controllers directory. If you have +no controller defined in the mock directory, the system will generate an appropriate response for you based on +the modules you have defined in your Swagger. + +`-open` will start the app and then open a browser as a client to it. + +### verify + +Verify the project's swagger. + +### edit + +Open the project in the swagger-editor in your default browser. + +### open + +Open your default browser as a client of the project. + +### test + +Run the tests for your project using mocha. + +## docs + +Opens the Swagger 2.0 documentation web page in your default browser. diff --git a/bin/swagger b/bin/swagger new file mode 100755 index 00000000..1114b372 --- /dev/null +++ b/bin/swagger @@ -0,0 +1,52 @@ +#!/usr/bin/env node +/**************************************************************************** + The MIT License (MIT) + + Copyright (c) 2015 Apigee Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ****************************************************************************/ +'use strict'; + +var app = require('commander'); +var browser = require('../lib/util/browser'); + +app.version(require('../lib/util/cli').version()); + +app + .command('project ', 'project actions'); + +app + .command('docs') + .description('open Swagger documentation') + .action(function() { + browser.open('https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md', function() { + process.exit(0); + }); + }); + +app.parse(process.argv); + +if (!app.runningCommand) { + if (app.args.length > 0) { + console.log(); + console.log('error: invalid command: ' + app.args[0]); + } + app.help(); +} diff --git a/bin/swagger-project b/bin/swagger-project new file mode 100755 index 00000000..aee42d19 --- /dev/null +++ b/bin/swagger-project @@ -0,0 +1,72 @@ +#!/usr/bin/env node +/**************************************************************************** + The MIT License (MIT) + + Copyright (c) 2015 Apigee Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ****************************************************************************/ +'use strict'; + +var app = require('commander'); +var project = require('../lib/commands/project/project'); +var cli = require('../lib/util/cli'); +var execute = cli.execute; + +app + .command('create ') + .description('Create a folder containing a Swagger project') + .action(execute(project.create)); + +app + .command('start [directory]') + .description('Start the project in this or the specified directory') + .option('-d, --debug ', 'start in remote debug mode') + .option('-b, --debug-brk ', 'start in remote debug mode, wait for debugger connect') + .option('-m, --mock', 'start in mock mode') + .option('-o, --open', 'open browser as client to the project') + .action(execute(project.start)); + +app + .command('verify [directory]') + .description('Verify that the project is correct (swagger, config, etc)') + .option('-j, --json', 'output as JSON') + .action(execute(project.verify)); + +app + .command('edit [directory]') + .description('open Swagger editor for this project or the specified project directory') + .option('-s --silent', 'do not open the browser') + .action(execute(project.edit)); + +app + .command('open [directory]') + .description('open browser as client to the project') + .action(execute(project.open)); + +app + .command('test [directory_or_file]') + .description('Run project tests') + .option('-d, --debug [port]', 'start in remote debug mode') + .option('-b, --debug-brk [port]', 'start in remote debug mode, wait for debugger connect') + .option('-m, --mock', 'run in mock mode') + .action(execute(project.test)); + +app.parse(process.argv); +cli.validate(app); diff --git a/config/editor-branding/branding.css b/config/editor-branding/branding.css new file mode 100644 index 00000000..7ca7f255 --- /dev/null +++ b/config/editor-branding/branding.css @@ -0,0 +1,10 @@ +.a127 .branding.left { + float: left; +} +.a127 .branding.left .logo { + width: 75px; + padding: 5px; +} +.a127 .branding.right { + float: right; +} \ No newline at end of file diff --git a/config/editor-branding/left.html b/config/editor-branding/left.html new file mode 100644 index 00000000..172a7c06 --- /dev/null +++ b/config/editor-branding/left.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/config/editor-branding/right.html b/config/editor-branding/right.html new file mode 100644 index 00000000..ca598e59 --- /dev/null +++ b/config/editor-branding/right.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/config/index.js b/config/index.js new file mode 100644 index 00000000..4bf723a5 --- /dev/null +++ b/config/index.js @@ -0,0 +1,71 @@ +/**************************************************************************** + The MIT License (MIT) + + Copyright (c) 2015 Apigee Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ****************************************************************************/ +'use strict'; + +var path = require('path'); +var _ = require('lodash'); +var debug = require('debug')('swagger'); + +var config = { + rootDir: path.resolve(__dirname, '..'), + userHome: process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'], + debug: !!process.env.DEBUG +}; +config.nodeModules = path.resolve(config.rootDir, 'node_modules'); + +module.exports = config; + +// swagger editor // + +config.swagger = { + fileName: 'api/swagger/swagger.yaml', + editorDir: path.resolve(config.nodeModules, 'swagger-editor') +}; + +// project // + +config.project = { + port: process.env.PORT || 10010, + skeletonDir: path.resolve(__dirname, '..', 'project-skeleton') +}; + +// load env vars // + +_.each(process.env, function(value, key) { + var split = key.split('_'); + if (split[0] === 'swagger') { + var configItem = config; + for (var i = 1; i < split.length; i++) { + var subKey = split[i]; + if (i < split.length - 1) { + if (!configItem[subKey]) { configItem[subKey] = {}; } + configItem = configItem[subKey]; + } else { + configItem[subKey] = value; + } + } + debug('loaded env var: %s = %s', split.slice(1).join('.'), value); + } +}); + diff --git a/config/swagger-editor.config.json b/config/swagger-editor.config.json new file mode 100644 index 00000000..26c60975 --- /dev/null +++ b/config/swagger-editor.config.json @@ -0,0 +1,22 @@ +{ + "analytics": { + "google": { + "id": null + } + }, + "disableCodeGen": true, + "disableNewUserIntro": true, + "examplesFolder": "/spec-files/", + "exampleFiles": [], + "autocompleteExtension": {}, + "useBackendForStorage": true, + "backendEndpoint": "/editor/spec", + "backendHelathCheckTimeout": 5000, + "useYamlBackend": true, + "disableFileMenu": true, + "headerBranding": true, + "enableTryIt": true, + "brandingCssClass": "a127", + "schemaUrl": "/schema/swagger.json", + "importProxyUrl": "https://cors-it.herokuapp.com/?url=" +} \ No newline at end of file diff --git a/lib/commands/project/project.js b/lib/commands/project/project.js new file mode 100644 index 00000000..a07b16cf --- /dev/null +++ b/lib/commands/project/project.js @@ -0,0 +1,371 @@ +/**************************************************************************** + The MIT License (MIT) + + Copyright (c) 2015 Apigee Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ****************************************************************************/ +'use strict'; + +var config = require('../../../config'); +var _ = require('lodash'); +var path = require('path'); +var fs = require('fs'); +var emit = require('../../util/feedback').emit; +var netutil = require('../../util/net'); +var debug = require('debug')('swagger'); +var util = require('util'); + +module.exports = { + create: create, + start: start, + verify: verify, + edit: edit, + open: open, + test: test, + + // for internal use + read: readProject +}; + +function create(name, ignore, cb) { + + var targetDir = path.resolve(process.cwd(), name); + if (fs.existsSync(targetDir)) { + return cb(new Error('Directory ' + targetDir + ' already exists.')); + } + cloneSkeleton(name, targetDir, function(err) { + if (err) { cb(err); } + spawn('npm', ['install'], targetDir, function(err) { + if (err) { + emit('\'npm install\' failed. Please run \'npm install\' from the project directory.') + } else { + emit('Project %s created in %s', name, targetDir); + } + cb(err); + }); + }); +} + +//.option('-d, --debug [port]', 'start in remote debug mode') +//.option('-b, --debug-brk [port]', 'start in remote debug mode, wait for debugger connect') +//.option('-m, --mock', 'start in mock mode') +//.option('-o, --open', 'open in browser') +function start(directory, options, cb) { + + readProject(directory, options, function(err, project) { + if (err) { throw err; } + + var fullPath = path.join(project.dirname, project.api.main); + emit('Starting: %s...', fullPath); + if (project.dirname) { process.chdir(project.dirname); } + var nodemonOpts = { + script: project.api.main, + ext: 'js,json,yaml,coffee' + }; + if (options.debugBrk) { + nodemonOpts.nodeArgs = '--debug-brk'; + if (typeof(options.debugBrk == 'String')) { + nodemonOpts.nodeArgs += '=' + options.debugBrk; + } + } + if (options.debug) { + nodemonOpts.nodeArgs = '--debug'; + if (typeof(options.debug == 'String')) { + nodemonOpts.nodeArgs += '=' + options.debug; + } + } + var nodemon = require('nodemon'); + // hack to enable proxyquire stub for testing... + if (_.isFunction(nodemon)) { + nodemon(nodemonOpts); + } else { + nodemon._init(nodemonOpts, cb); + } + nodemon.on('start', function () { + emit(' project started here: ' + project.api.localUrl); + emit(' project will restart on changes.'); + emit(' to restart at any time, enter `rs`'); + + if (options.open) { + setTimeout(function() { + open(directory, options, cb); + }, 500); + } + }).on('restart', function (files) { + emit('Project restarted. Files changed: ', files); + }); + }); +} + +//.option('-d, --debug [port]', 'start in remote debug mode') +//.option('-b, --debug-brk [port]', 'start in remote debug mode, wait for debugger connect') +//.option('-m, --mock', 'start in mock mode') +//.option('-o, --open', 'open in browser') +function test(directory, options, cb) { + + var Mocha = require('mocha'); + var MochaUtils = require('mocha/lib/utils'); + + readProject(directory, options, function(err, project) { + + if (err) { return cb(err); } + + var mocha = new Mocha(); + var testPath = project.dirname; + if (directory) { + try { + testPath = fs.realpathSync(directory); + } catch (err) { + return cb(new Error(util.format('no such file or directory %s', directory))); + } + } + if (testPath === project.dirname) { + testPath = path.resolve(testPath, 'test'); + } + + if (fs.statSync(testPath).isFile()) { + if (testPath.substr(-3) !== '.js') { return cb(new Error('file is not a javascript file')); } + mocha.addFile(testPath); + } else { + MochaUtils.lookupFiles(testPath, ['js'], true) + .forEach(function(file) { + mocha.addFile(file); + }); + } + + var fullPath = path.join(project.dirname, project.api.main); + emit('Loading server: %s...', fullPath); + var app = require(fullPath); + if (!Object.keys(app).length) { + return cb(new Error(util.format('Ensure %s exports the server. eg. "module.exports = app;"', project.api.main))); + } + + emit('Running tests in: %s...', testPath); + + mocha.run(function(failures) { + process.exit(failures); + }); + }); +} + +function verify(directory, options, cb) { + + readProject(directory, options, function(err, project) { + if (err) { return cb(err); } + + var swaggerSpec = require('swagger-tools').specs.v2_0; + swaggerSpec.validate(project.api.swagger, function(err, results) { + if (err) { return cb(err); } + + var toJsonPointer = function (path) { + // http://tools.ietf.org/html/rfc6901#section-4 + return '#/' + path.map(function (part) { + return part.replace(/\//g, '~1'); + }).join('/'); + }; + + if (results) { + if (options.json) { + cb(null, JSON.stringify(results, null, ' ')); + } else { + if (results.errors.length > 0) { + emit('\nProject Errors'); + emit('--------------'); + + results.errors.forEach(function (vErr) { + emit(toJsonPointer(vErr.path) + ': ' + vErr.message); + }); + } + + if (results.warnings.length > 0) { + emit('\nProject Warnings'); + emit('----------------'); + + results.warnings.forEach(function (vWarn) { + emit(toJsonPointer(vWarn.path) + ': ' + vWarn.message); + }); + } + + cb(null, 'Results: ' + results.errors.length + ' errors, ' + results.warnings.length + ' warnings'); + } + } else { + if (options.json) { + cb(null, ''); + } else { + cb(null, 'Results: 0 errors, 0 warnings'); + } + } + }); + }); +} + +function edit(directory, options, cb) { + + readProject(directory, options, function(err, project) { + if (err) { return cb(err); } + var editor = require('./swagger_editor'); + editor.edit(project, options, cb); + }); +} + +function open(directory, options, cb) { + + readProject(directory, options, function(err, project) { + if (err) { return cb(err); } + + netutil.isPortOpen(project.api.port, function(err, isOpen) { + if (err) { return cb(err); } + if (isOpen) { + var browser = require('../../util/browser'); + browser.open(project.api.localUrl, cb); + } else { + emit('Project does not appear to be listening on port %d.', project.api.port); + } + }); + }); +} + +// Utility + +function readProject(directory, options, cb) { + + findProjectFile(directory, options, function(err, fileName) { + if (err) { return cb(err); } + + var YAML = require('yamljs'); + var Url = require('url'); + + var string = fs.readFileSync(fileName, { encoding: 'utf8' }); + var project = JSON.parse(string); + + project.filename = fileName; + project.dirname = path.dirname(fileName); + + if (!project.api) { project.api = {}; } + + project.api.swaggerFile = path.resolve(project.dirname, 'api', 'swagger', 'swagger.yaml'); + project.api.swagger = YAML.load(project.api.swaggerFile); + + project.api.name = project.name; + project.api.main = project.main; + project.api.host = project.api.swagger.host; + project.api.basePath = project.api.swagger.basePath; + + project.api.localUrl = 'http://' + project.api.host + project.api.swagger.basePath; + project.api.port = Url.parse(project.api.localUrl).port || 80; + + debug('project.api: %j', _.omit(project.api, 'swagger')); + cb(null, project); + }); +} + +// .option('-p, --project', 'use specified project file') +function findProjectFile(startDir, options, cb) { + + var parent = startDir = startDir || process.cwd(); + var maxDepth = 50; + var current = null; + while (current !== parent && maxDepth-- > 0) { + current = parent; + var projectFile = path.resolve(current, 'package.json'); + if (fs.existsSync(projectFile)) { + return cb(null, projectFile); + } + parent = path.join(current, '..'); + } + cb(new Error('Project root not found in or above: ' + startDir)); +} + +function cloneSkeleton(name, destDir, cb) { + + var sourceDir = config.project.skeletonDir; + + var filter = function(fileName) { + fileName = fileName.substr(sourceDir.length + 1); + if (fileName.length > 0) { emit('creating: ' + fileName); } + return true; + }; + + var options = { + clobber: false, + filter: filter + }; + + emit('Copying files to %s...', destDir); + var ncp = require('ncp'); + ncp(sourceDir, destDir, options, function (err) { + if (err) { return cb(err); } + customizeClonedFiles(name, destDir, cb); + }); +} + +function customizeClonedFiles(name, destDir, cb) { + + // ensure .npmignore is renamed to .gitignore (damn you, npm!) + var npmignore = path.resolve(destDir, '.npmignore'); + var gitignore = path.resolve(destDir, '.gitignore'); + fs.rename(npmignore, gitignore, function(err) { + if (err && !fs.existsSync(gitignore)) { return cb(err); } + + // rewrite package.json + var fileName = path.resolve(destDir, 'package.json'); + fs.readFile(fileName, { encoding: 'utf8' }, function(err, string) { + if (err) { return cb(err); } + var project = JSON.parse(string); + project.name = name; + debug('writing project: %j', project); + fs.writeFile(fileName, JSON.stringify(project, null, ' '), function(err) { + if (err) { return cb(error); } + cb(null, 'done!'); + }); + }); + }); +} + +function spawn(command, options, cwd, cb) { + + var cp = require('child_process'); + var os = require('os'); + + var isWin = /^win/.test(os.platform()); + + emit('Running \'%s %s\'...', command, options.join(' ')); + + var npm = cp.spawn(isWin ? + process.env.comspec : + command, + isWin ? + ['/c'].concat(command, options) : + options, + { cwd: cwd }); + npm.stdout.on('data', function (data) { + emit(data); + }); + npm.stderr.on('data', function(data) { + emit('%s', data); + }); + npm.on('close', function(exitCode) { + if (exitCode !== 0) { var err = new Error('exit code: ' + exitCode); } + cb(err); + }); + npm.on('error', function(err) { + cb(err); + }); +} diff --git a/lib/commands/project/swagger_editor.js b/lib/commands/project/swagger_editor.js new file mode 100644 index 00000000..4924bee2 --- /dev/null +++ b/lib/commands/project/swagger_editor.js @@ -0,0 +1,107 @@ +/**************************************************************************** + The MIT License (MIT) + + Copyright (c) 2015 Apigee Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ****************************************************************************/ +'use strict'; + +var config = require('../../../config'); +var emit = require('../../util/feedback').emit; +var browser = require('../../util/browser'); +var util = require('util'); +var path = require('path'); +var serveStatic = require('serve-static'); +var fs = require('fs'); + +// swagger-editor must be served from root +var SWAGGER_EDITOR_SERVE_PATH = '/'; + +// swagger-editor expects to GET the file here +var SWAGGER_EDITOR_LOAD_PATH = '/editor/spec'; + +// swagger-editor PUTs the file back here +var SWAGGER_EDITOR_SAVE_PATH = '/editor/spec'; + +// swagger-editor GETs the configuration files +var SWAGGER_EDITOR_CONFIG_PATH = '/config/defaults.json'; +var SWAGGER_EDITOR_BRANDING_LEFT = '/templates/branding-left.html'; +var SWAGGER_EDITOR_BRANDING_RIGHT = '/templates/branding-right.html'; +var SWAGGER_EDITOR_BRANDING_CSS = '/styles/branding.css'; + +module.exports = { + edit: edit +}; + +function edit(project, options, cb) { + + var swaggerFile = path.resolve(project.dirname, config.swagger.fileName); + var app = require('connect')(); + + // save the file from swagger-editor + app.use(SWAGGER_EDITOR_SAVE_PATH, function(req, res, next) { + if (req.method !== 'PUT') { return next(); } + var stream = fs.createWriteStream(swaggerFile); + req.pipe(stream); + + stream.on('finish', function() { + res.end('ok'); + }) + }); + + // retrieve the project swagger file for the swagger-editor + app.use(SWAGGER_EDITOR_LOAD_PATH, serveStatic(swaggerFile) ); + + // server swagger-editor configuration JSON and branding HTML and CSS files + var configFilePath = path.join(config.rootDir, 'config', 'swagger-editor.config.json'); + var brandingDir = path.join(config.rootDir, 'config', 'editor-branding'); + + app.use(SWAGGER_EDITOR_CONFIG_PATH, serveStatic(configFilePath)); + app.use(SWAGGER_EDITOR_BRANDING_LEFT, serveStatic(path.join(brandingDir, 'left.html'))); + app.use(SWAGGER_EDITOR_BRANDING_RIGHT, serveStatic(path.join(brandingDir, 'right.html'))); + app.use(SWAGGER_EDITOR_BRANDING_CSS, serveStatic(path.join(brandingDir, 'branding.css'))); + + + // serve swagger-editor + app.use(SWAGGER_EDITOR_SERVE_PATH, serveStatic(config.swagger.editorDir)); + + + // start // + + var http = require('http'); + var server = http.createServer(app); + server.listen(0, '127.0.0.1', function() { + var port = server.address().port; + var editorUrl = util.format('http://127.0.0.1:%d/#/edit', port); + var editApiUrl = util.format('http://127.0.0.1:%d/editor/spec', port); + var dontKillMessage = 'Do not terminate this process or close this window until finished editing.'; + emit('Starting Swagger Editor.'); + + if (!options.silent) { + browser.open(editorUrl, function(err) { + if (err) { return cb(err); } + emit(dontKillMessage); + }); + } else { + emit('Running Swagger Editor API server. You can make GET and PUT calls to %s', editApiUrl); + emit(dontKillMessage) + } + }); +} diff --git a/lib/util/browser.js b/lib/util/browser.js new file mode 100644 index 00000000..820db42b --- /dev/null +++ b/lib/util/browser.js @@ -0,0 +1,93 @@ +/**************************************************************************** + The MIT License (MIT) + + Copyright (c) 2015 Apigee Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ****************************************************************************/ +'use strict'; + +var Child = require('child_process'); +var config = require('../../config'); +var emit = require('./feedback').emit; + +var platformOpeners = { + + darwin: + function(url, cb) { + var browser = escape(config.browser); + if (browser) { + open('open -a ' + browser, url, cb); + } else { + open('open', url, cb); + } + }, + + win32: + function(url, cb) { + var browser = escape(config.browser); + if (browser) { + open('start "" "' + browser + '"', url, cb); + } else { + open('start ""', url, cb); + } + }, + + linux: + function(url, cb) { + var browser = escape(config.browser); + if (browser) { + open(browser, url, cb); + } else { + open('xdg-open', url, cb); + } + }, + + other: + function(url, cb) { + var browser = escape(config.browser); + if (browser) { + open(browser, url, cb); + } else { + cb(new Error('must specify browser in config')); + } + } +}; + +module.exports = { + open: platformOpen +}; + +// note: platform parameter is just for testing... +function platformOpen(url, cb, platform) { + platform = platform || process.platform; + if (!platformOpeners[platform]) { platform = 'other'; } + platformOpeners[platform](url, cb); +} + +function open(command, url, cb) { + if (config.debug) { emit('command: ' + command); } + emit('Opening browser to: ' + url); + Child.exec(command + ' "' + escape(url) + '"', cb); +} + +function escape(s) { + if (!s) { return s; } + return s.replace(/"/g, '\\\"'); +} diff --git a/lib/util/cli.js b/lib/util/cli.js new file mode 100644 index 00000000..40e3b80f --- /dev/null +++ b/lib/util/cli.js @@ -0,0 +1,207 @@ +/**************************************************************************** + The MIT License (MIT) + + Copyright (c) 2015 Apigee Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ****************************************************************************/ +'use strict'; + +var _ = require('lodash'); +var inquirer = require('inquirer'); +var feedback = require('./feedback'); +var config = require('../../config'); +var yaml = require('yamljs'); +var util = require('util'); + +module.exports = { + requireAnswers: requireAnswers, + updateAnswers: updateAnswers, + printAndExit: printAndExit, + chooseOne: chooseOne, + validate: validate, + execute: execute, + confirm: confirm, + prompt: prompt, + version: version, + updateDefaultValue: updateDefaultValue +}; + +function version() { + return require('../../package.json').version; +} + +// questions are array of objects like these: +// { name: 'key', message: 'Your prompt?' } +// { name: 'key', message: 'Your prompt?', type: 'password' } +// { name: 'key', message: 'Your prompt?', type: 'list', choices: ['1', '2'] } +// results is an (optional) object containing existing results like this: { key: value } +function requireAnswers(questions, results, cb) { + if (!cb) { cb = results; results = {}; } + var unanswered = getUnanswered(questions, results); + if (unanswered.length === 0) { + return cb(results); + } + inquirer.prompt(unanswered, function(answers) { + _.extend(results, answers); + requireAnswers(questions, results, cb); + }); +} + +function updateAnswers(questions, results, cb) { + if (!cb) { cb = results; results = {}; } + for (var i = 0; i < questions.length; i++) { + var question = questions[i]; + if (question.type !== 'password') { + question.default = results[question.name]; + } + } + inquirer.prompt(questions, function(answers) { + _.extend(results, answers); + requireAnswers(questions, results, cb); + }); +} + +function updateDefaultValue(questions, questionName, defaultValue) { + for (var i = 0; i < questions.length; i++) { + var question = questions[i]; + if (question.name === questionName) { + question.default = defaultValue; + } + } +} + +function getUnanswered(questions, results) { + var unanswered = []; + for (var i = 0; i < questions.length; i++) { + var question = questions[i]; + if (!results[question.name]) { + unanswered.push(question); + } + } + return unanswered; +} + +function printAndExit(err, output, code) { + if (err) { + print(err); + code = code || 1; + } else if (output !== null && output !== undefined) { + print(output); + } + process.exit(code || 0); +} + +function print(object) { + if (util.isError(object)) { + console.log(config.debug ? object.stack : object); + } else if (_.isObject(object)) { + if (object.password) { + object.password = '******'; + } + console.log(yaml.stringify(object, 100, 2)); + } else if (object !== null && object !== undefined) { + console.log(object); + } else { + console.log(); + } +} + +// prompt: 'Your prompt?', choices: ['1', '2'] } +// result passed to cb() is the choice selected +function chooseOne(prompt, choices, cb) { + var questions = { name: 'x', message: prompt, type: 'list', choices: choices }; + inquirer.prompt(questions, function(answers) { + cb(answers.x); + }); +} + +// defaultBool is optional (default == true) +// result passed to cb() is the choice selected +function confirm(prompt, defaultBool, cb) { + if (!cb) { cb = defaultBool; defaultBool = true; } + var question = { name: 'x', message: prompt, type: 'confirm', default: defaultBool}; + inquirer.prompt(question, function(answers) { + cb(answers.x); + }); +} + +// defaultValue is optional +// result passed to cb() is the response +function prompt(prompt, defaultValue, cb) { + if (!cb) { cb = defaultValue; defaultValue = undefined; } + var question = { name: 'x', message: prompt, default: defaultValue}; + inquirer.prompt(question, function(answers) { + cb(answers.x); + }); +} + +function validate(app) { + var commands = app.commands.map(function(command) { return command._name; }); + if (!_.contains(commands, app.rawArgs[2])) { + if (app.rawArgs[2]) { + console.log(); + console.log('error: invalid command: ' + app.rawArgs[2]); + } + app.help(); + } +} + +function execute(command, header) { + var cb = function(err, reply) { + if (header && !err) { + print(header); + print(Array(header.length + 1).join('=')); + } + if (!reply && !err) { reply = 'done'; } + printAndExit(err, reply); + }; + return function() { + try { + var args = Array.prototype.slice.call(arguments); + args.push(cb); + if (!command) { + return cb(new Error('missing command method')); + } + if (args.length !== command.length) { + return cb(new Error('incorrect arguments')); + } + var reply = command.apply(this, args); + if (reply) { + cb(null, reply); + } + } catch (err) { + cb(err); + } + } +} + +if (typeof String.prototype.endsWith !== 'function') { + String.prototype.endsWith = function(suffix) { + return this.indexOf(suffix, this.length - suffix.length) !== -1; + }; +} + +feedback.on(function(feedback) { + if (_.isString(feedback) && feedback.endsWith('\\')) { + process.stdout.write(feedback.substr(0, feedback.length - 1)); + } else { + print(feedback); + } +}); diff --git a/lib/util/feedback.js b/lib/util/feedback.js new file mode 100644 index 00000000..cc9ef42a --- /dev/null +++ b/lib/util/feedback.js @@ -0,0 +1,47 @@ +/**************************************************************************** + The MIT License (MIT) + + Copyright (c) 2015 Apigee Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ****************************************************************************/ +'use strict'; + +var EventEmitter = require('events').EventEmitter; +var feedback = new EventEmitter(); +var CHANNEL = 'feedback'; +var util = require('util'); +var _ = require('lodash'); + +module.exports = { + + on: function(cb) { + feedback.on(CHANNEL, function(feedback) { + cb(feedback); + }); + }, + + emit: function(string) { + if (Buffer.isBuffer(string)) { string = string.toString(); } + if (arguments.length > 1 && _.isString(string)) { + string = util.format.apply(this, arguments); + } + feedback.emit(CHANNEL, string); + } +}; diff --git a/lib/util/net.js b/lib/util/net.js new file mode 100644 index 00000000..92bb5434 --- /dev/null +++ b/lib/util/net.js @@ -0,0 +1,96 @@ +/**************************************************************************** + The MIT License (MIT) + + Copyright (c) 2015 Apigee Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ****************************************************************************/ +'use strict'; + +var net = require('net'); +var debug = require('debug')('swagger'); +var http = require('http'); +var https = require('https'); +var fs = require('fs'); +var _ = require('lodash'); + +var DEFAULT_TIMEOUT = 100; + +module.exports = { + isPortOpen: isPortOpen, + download: download +}; + +function isPortOpen(port, timeout, cb) { + if (!cb) { cb = timeout; timeout = DEFAULT_TIMEOUT; } + cb = _.once(cb); + + var s = new net.Socket(); + + s.setTimeout(timeout, function() { + s.destroy(); + cb(null, false); + }); + s.connect(port, function() { + cb(null, true); + }); + + s.on('error', function(err) { + s.destroy(); + if (err.code === 'ECONNREFUSED') { err = null; } + cb(err, false); + }); +} + +// returns final file size if successful (or -1 if unknown) +function download(url, destFile, cb) { + + var proto = url.substr(0, url.indexOf(':')) == 'https' ? https : http; + var tmpFile = destFile + '.tmp'; + + var error = function(err) { + debug(err); + debug('error: removing downloaded file'); + fs.unlink(tmpFile); + cb(err); + }; + + var file = fs.createWriteStream(tmpFile); + file.on('error', error); + + proto.get(url, function(res) { + + var size = 0; + var count = 0; + + res.on('data', function(chunk) { + file.write(chunk); + size += chunk.length; + if (++count % 70 === 0) { process.stdout.write('.'); } + if (debug.enabled) { debug('downloaded ' + size + ' bytes'); } + }) + .on('end', function() { + fs.rename(tmpFile, destFile, function(err) { + if (err) { return error(err); } + cb(null, size); + }); + }) + .on('error', error); + }) +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..6c8d134e --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "name": "swagger-node", + "version": "0.0.1", + "description": "The Swagger command-line. Provides Swagger utilities and project support.", + "keywords": [ + "swagger", + "api", + "apis", + "connect", + "express" + ], + "author": "Scott Ganyo ", + "license": "MIT", + "preferGlobal": true, + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/swagger-node/swagger-node.git" + }, + "dependencies": { + "commander": "^2.7.1", + "connect": "^3.3.5", + "debug": "^2.1.3", + "inquirer": "^0.8.2", + "lodash": "^3.6.0", + "ncp": "^2.0.0", + "nodemon": "^1.3.7", + "serve-static": "^1.9.2", + "swagger-editor": "^2.9.2", + "swagger-tools": "^0.8.5", + "yamljs": "^0.2.1" + }, + "devDependencies": { + "superagent": "^1.1.0", + "should": "^5.2.0", + "proxyquire": "^1.4.0", + "tmp": "^0.0.25" + }, + "scripts": { + "test": "mocha -u exports -R spec test/config.js test/util test/commands test/commands/project", + "coverage": "istanbul cover _mocha -- -u exports -R spec test/config.js test/util test/commands test/commands/project" + }, + "bin": { + "swagger": "bin/swagger", + "swagger-project": "bin/swagger-project" + } +} diff --git a/project-skeleton/.gitignore b/project-skeleton/.gitignore new file mode 100644 index 00000000..8bae4f3c --- /dev/null +++ b/project-skeleton/.gitignore @@ -0,0 +1,34 @@ +# IDE files +.idea + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# Commenting this out is preferred by some people, see +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules + +# Users Environment Variables +.lock-wscript + +# Runtime configuration generated by swagger-node +config/runtime.yaml diff --git a/project-skeleton/README.md b/project-skeleton/README.md new file mode 100755 index 00000000..0656e0a2 --- /dev/null +++ b/project-skeleton/README.md @@ -0,0 +1 @@ +# Skeleton project for Swagger-Node diff --git a/project-skeleton/api/controllers/README.md b/project-skeleton/api/controllers/README.md new file mode 100755 index 00000000..ddb3f648 --- /dev/null +++ b/project-skeleton/api/controllers/README.md @@ -0,0 +1 @@ +Place your controllers in this directory. diff --git a/project-skeleton/api/controllers/hello_world.js b/project-skeleton/api/controllers/hello_world.js new file mode 100755 index 00000000..83df4039 --- /dev/null +++ b/project-skeleton/api/controllers/hello_world.js @@ -0,0 +1,44 @@ +'use strict'; +/* + 'use strict' is not required but helpful for turning syntactical errors into true errors in the program flow + http://www.w3schools.com/js/js_strict.asp +*/ + +/* + Modules make it possible to import JavaScript files into your application. Modules are imported + using 'require' statements that give you a reference to the module. + + It is a good idea to list the modules that your application depends on in the package.json in the project root + */ +var util = require('util'); + +/* + Once you 'require' a module you can reference the things that it exports. These are defined in module.exports. + + For a controller in a127 (which this is) you should export the functions referenced in your Swagger document by name. + + Either: + - The HTTP Verb of the corresponding operation (get, put, post, delete, etc) + - Or the operationId associated with the operation in your Swagger document + + In the starter/skeleton project the 'get' operation on the '/hello' path has an operationId named 'hello'. Here, + we specify that in the exports of this module that 'hello' maps to the function named 'hello' + */ +module.exports = { + hello: hello +}; + +/* + Functions in a127 controllers used for operations should take two parameters: + + Param 1: a handle to the request object + Param 2: a handle to the response object + */ +function hello(req, res) { + // variables defined in the Swagger document can be referenced using req.swagger.params.{parameter_name} + var name = req.swagger.params.name.value || 'stranger'; + var hello = util.format('Hello, %s!', name); + + // this sends back a JSON response which is a single string + res.json(hello); +} diff --git a/project-skeleton/api/helpers/README.md b/project-skeleton/api/helpers/README.md new file mode 100755 index 00000000..1c1e88d6 --- /dev/null +++ b/project-skeleton/api/helpers/README.md @@ -0,0 +1,3 @@ +Place helper files in this directory. + +This is also the directory that will be checked for Volos functions. diff --git a/project-skeleton/api/mocks/README.md b/project-skeleton/api/mocks/README.md new file mode 100755 index 00000000..78b0e18b --- /dev/null +++ b/project-skeleton/api/mocks/README.md @@ -0,0 +1 @@ +Place controllers for a127 mock mode in this directory. diff --git a/project-skeleton/api/swagger/swagger.yaml b/project-skeleton/api/swagger/swagger.yaml new file mode 100755 index 00000000..10eb4ab3 --- /dev/null +++ b/project-skeleton/api/swagger/swagger.yaml @@ -0,0 +1,58 @@ +swagger: "2.0" +info: + version: "0.0.1" + title: Hello World App +# during dev, should point to your local machine +host: localhost +# basePath prefixes all resource paths +basePath: / +# +schemes: + # tip: remove http to make production-grade + - http + - https +# format of bodies a client can send (Content-Type) +consumes: + - application/json +# format of the responses to the client (Accepts) +produces: + - application/json +paths: + /hello: + # binds a127 app logic to a route + x-swagger-router-controller: hello_world + get: + description: Returns 'Hello' to the caller + # used as the method name of the controller + operationId: hello + parameters: + - name: name + in: query + description: The name of the person to whom to say hello + required: false + type: string + responses: + "200": + description: Success + schema: + # a pointer to a definition + $ref: "#/definitions/HelloWorldResponse" + # responses may fall through to errors + default: + description: Error + schema: + $ref: "#/definitions/ErrorResponse" +# complex objects have schema definitions +definitions: + HelloWorldResponse: + required: + - message + properties: + message: + type: string + ErrorResponse: + required: + - message + properties: + message: + type: string diff --git a/project-skeleton/app.js b/project-skeleton/app.js new file mode 100755 index 00000000..5af7488d --- /dev/null +++ b/project-skeleton/app.js @@ -0,0 +1,31 @@ +'use strict'; + +var SwaggerRunner = require('swagger-node-runner'); +var app = require('connect')(); +module.exports = app; // for testing + +var config = { + appRoot: __dirname // required config +}; + +SwaggerRunner.create(config, function(err, runner) { + if (err) { throw err; } + + app.use(runner.connectMiddleware().chain()); + app.use(errorHandler); + + var port = process.env.PORT || 10010; + app.listen(port); + + console.log('try this:\ncurl http://127.0.0.1:' + port + '/hello?name=Scott'); +}); + +// this just emits errors to the client as a json string +function errorHandler(err, req, res, next) { + + if (err && typeof err === 'object') { + Object.defineProperty(err, 'message', { enumerable: true }); // include message property in response + res.end(JSON.stringify(err)); + } + next(err); +} diff --git a/project-skeleton/config/README.md b/project-skeleton/config/README.md new file mode 100755 index 00000000..29c273e6 --- /dev/null +++ b/project-skeleton/config/README.md @@ -0,0 +1,5 @@ +# Place configuration files in this directory. + +A generated file called "runtime.yaml" will be placed in this directory by swagger-node. + +You may also include a file called "default.yaml" that will be diff --git a/project-skeleton/package.json b/project-skeleton/package.json new file mode 100755 index 00000000..5246ed91 --- /dev/null +++ b/project-skeleton/package.json @@ -0,0 +1,18 @@ +{ + "name": "swagger-skeleton", + "version": "0.0.1", + "private": "true", + "description": "My New Swagger API Project", + "keywords": [], + "author": "", + "license": "", + "main": "app.js", + "dependencies": { + "connect": "^3.3.5", + "swagger-node-runner": "^0.0.1" + }, + "devDependencies": { + "should": "^5.2.0", + "supertest": "^0.15.0" + } +} diff --git a/project-skeleton/test/api/controllers/README.md b/project-skeleton/test/api/controllers/README.md new file mode 100755 index 00000000..16437ee1 --- /dev/null +++ b/project-skeleton/test/api/controllers/README.md @@ -0,0 +1 @@ +Place your controller tests in this directory. diff --git a/project-skeleton/test/api/controllers/hello_world.js b/project-skeleton/test/api/controllers/hello_world.js new file mode 100644 index 00000000..730e642a --- /dev/null +++ b/project-skeleton/test/api/controllers/hello_world.js @@ -0,0 +1,50 @@ +var should = require('should'); +var request = require('supertest'); +var server = require('../../../app'); + +process.env.A127_ENV = 'test'; + +describe('controllers', function() { + + describe('hello_world', function() { + + describe('GET /hello', function() { + + it('should return a default string', function(done) { + + request(server) + .get('/hello') + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res) { + should.not.exist(err); + + res.body.should.eql('Hello, stranger!'); + + done(); + }); + }); + + it('should accept a name parameter', function(done) { + + request(server) + .get('/hello') + .query({ name: 'Scott'}) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res) { + should.not.exist(err); + + res.body.should.eql('Hello, Scott!'); + + done(); + }); + }); + + }); + + }); + +}); diff --git a/project-skeleton/test/api/helpers/README.md b/project-skeleton/test/api/helpers/README.md new file mode 100755 index 00000000..8528f1b1 --- /dev/null +++ b/project-skeleton/test/api/helpers/README.md @@ -0,0 +1 @@ +Place your helper tests in this directory. diff --git a/test/commands/project/badswagger.yaml b/test/commands/project/badswagger.yaml new file mode 100644 index 00000000..5b8a493f --- /dev/null +++ b/test/commands/project/badswagger.yaml @@ -0,0 +1,62 @@ +swagger: 2 +info: + version: "0.0.1" + title: Hello World App +# during dev, should point to your local machine +host: localhost +# basePath prefixes all resource paths +basePath: / +# +schemes: + # tip: remove http to make production-grade + - http + - https +# format of bodies a client can send (Content-Type) +consumes: + - application/json +# format of the responses to the client (Accepts) +produces: + - application/json +x-a127-config: {} +x-volos-resources: {} +paths: + /hello: + # binds a127 app logic to a route + x-swagger-router-controller: hello_world + x-volos-authorizations: {} + x-volos-apply: {} + get: + description: Returns 'Hello' to the caller + # used as the method name of the controller + operationId: hello + parameters: + - name: name + in: query + description: The name of the person to whom to say hello + required: false + type: string + responses: + "200": + description: Success + schema: + # a pointer to a definition + $ref: "#/definitions/HelloWorldResponse" + # responses may fall through to errors + default: + description: Error + schema: + $ref: "#/definitions/ErrorResponse" +# complex objects have schema definitions +definitions: + HelloWorldResponse: + required: + - message + properties: + message: + type: string + ErrorResponse: + required: + - message + properties: + message: + type: string diff --git a/test/commands/project/project.js b/test/commands/project/project.js new file mode 100644 index 00000000..9f7cbe90 --- /dev/null +++ b/test/commands/project/project.js @@ -0,0 +1,320 @@ +/**************************************************************************** + The MIT License (MIT) + + Copyright (c) 2015 Apigee Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ****************************************************************************/ +'use strict'; + +var should = require('should'); +var util = require('util'); +var config = require('../../../config'); +var path = require('path'); +var proxyquire = require('proxyquire'); +var tmp = require('tmp'); +var fs = require('fs'); +var yaml = require('yamljs'); +var helpers = require('../../helpers'); +var _ = require('lodash'); + +/* + create: create, + start: start, + verify: verify, + edit: edit, + open: open, + docs: docs + */ + +describe('project', function() { + + var tmpDir; + var spawn = {}; + + before(function(done) { + tmp.setGracefulCleanup(); + + // set up project dir + tmp.dir({ unsafeCleanup: true }, function(err, path) { + should.not.exist(err); + tmpDir = path; + process.chdir(tmpDir); + done(); + }); + }); + + + var capture; + beforeEach(function() { + capture = helpers.captureOutput(); + }); + + afterEach(function() { + capture.release(); + }); + + var didEdit, didOpen; + var nodemonOpts = {}; + var projectStubs = { + 'child_process': { + spawn: function(command, args, options) { + spawn.command = command; + spawn.args = args; + spawn.options = options; + + var ret = {}; + ret.stdout = { + on: function() {} + }; + ret.stderr = { + on: function() {} + }; + ret.on = function(name, cb) { + if (name === 'close') { + setTimeout(function() { cb(0); }, 0); + } + return ret; + }; + return ret; + } + }, + 'nodemon': { + on: function(name, cb) { + if (name === 'start') { + setTimeout(function() { cb(); nodemonOpts.cb(); }, 0); + } + return this; + }, + _init: function(opts, cb) { + nodemonOpts = opts; + nodemonOpts.cb = cb; + }, + '@noCallThru': true + }, + './swagger_editor': { + edit: function(directory, options, cb) { + didEdit = true; + cb(); + } + }, + '../../util/browser': { + open: function(url, cb) { + didOpen = true; + cb(); + } + }, + '../../util/net': { + isPortOpen: function(port, cb) { + cb(null, true); + } + } + }; + var project = proxyquire('../../../lib/commands/project/project', projectStubs); + + describe('create', function() { + + it('should err if project directory already exists', function(done) { + var name = 'create_err'; + var projPath = path.resolve(tmpDir, name); + fs.mkdirSync(projPath); + process.chdir(tmpDir); + project.create(name, {}, function(err) { + should.exist(err); + done(); + }); + }); + + it('should create a new project', function(done) { + var name = 'create'; + var projPath = path.resolve(tmpDir, name); + process.chdir(tmpDir); + project.create(name, {}, function(err) { + should.not.exist(err); + // check a couple of files + var packageJson = path.resolve(projPath, 'package.json'); + fs.existsSync(packageJson).should.be.ok; + fs.existsSync(path.resolve(projPath, 'node_modules')).should.not.be.ok; + fs.existsSync(path.resolve(projPath, '.gitignore')).should.be.ok; + + // check spawn `npm install` + spawn.command.should.equal('npm'); + spawn.args.should.containEql('install'); + spawn.options.should.have.property('cwd', fs.realpathSync(projPath)); + + // check package.json customization + fs.readFile(packageJson, { encoding: 'utf8' }, function(err, string) { + if (err) { return cb(err); } + var project = JSON.parse(string); + project.name.should.equal(name); + done(); + }); + }); + }); + }); + + describe('start', function() { + + var name = 'start'; + var projPath; + + before(function(done) { + projPath = path.resolve(tmpDir, name); + process.chdir(tmpDir); + project.create(name, {}, done); + }); + + it('should pass debug options', function(done) { + var options = { debug: 'true,test' }; + project.start(projPath, options, function(err) { + should.not.exist(err); + nodemonOpts.nodeArgs.should.containDeep('--debug=' + options.debug); + done(); + }); + }); + + it('should start in debug break mode', function(done) { + var options = { debugBrk: true }; + project.start(projPath, options, function(err) { + should.not.exist(err); + nodemonOpts.nodeArgs.should.containDeep('--debug-brk'); + done(); + }); + }); + }); + + describe('verify', function() { + + describe('no errors', function() { + + var name = 'verifyGood'; + var projPath; + + before(function(done) { + projPath = path.resolve(tmpDir, name); + process.chdir(tmpDir); + project.create(name, {}, done); + }); + + it('should emit nothing, return summary', function(done) { + + project.verify(projPath, {}, function(err, reply) { + should.not.exist(err); + + capture.output().should.equal(''); + reply.should.equal('Results: 0 errors, 0 warnings'); + done(); + }) + }); + + it('w/ json option should emit nothing, return nothing', function(done) { + + project.verify(projPath, { json: true }, function(err, reply) { + should.not.exist(err); + + capture.output().should.equal(''); + reply.should.equal(''); + done(); + }) + }) + }); + + + describe('with errors', function() { + + var name = 'verifyBad'; + var projPath; + + before(function(done) { + projPath = path.resolve(tmpDir, name); + process.chdir(tmpDir); + project.create(name, {}, function() { + var sourceFile = path.join(__dirname, 'badswagger.yaml'); + var destFile = path.join(projPath, 'api', 'swagger', 'swagger.yaml'); + helpers.copyFile(sourceFile, destFile, done); + }); + }); + + it('should emit errors, return summary', function(done) { + + project.verify(projPath, {}, function(err, reply) { + should.not.exist(err); + + capture.output().should.containDeep('\nProject Errors\n--------------\n#/swagger:'); + reply.should.containDeep('Results:'); + done(); + }) + }); + + it('json option should emit as json', function(done) { + + project.verify(projPath, { json: true }, function(err, reply) { + should.not.exist(err); + + var json = JSON.parse(reply); + json.should.have.keys('errors', 'warnings') + json.errors.should.be.an.Array; + var error = json.errors[0]; + error.should.have.property('code', 'INVALID_TYPE'); + error.should.have.property('message'); + error.should.have.property('path', [ 'swagger' ]); + error.should.have.property('description', 'The Swagger version of this document.'); + done(); + }) + }) + }); + }); + + describe('basic functions', function() { + + var name = 'basic'; + var projPath; + + before(function(done) { + projPath = path.resolve(tmpDir, name); + process.chdir(tmpDir); + project.create(name, {}, done); + }); + + it('edit should exec editor', function(done) { + project.edit(projPath, {}, function(err) { + should.not.exist(err); + should(didEdit).true; + done(); + }); + }); + + it('edit should exec editor with --silent flag', function(done) { + project.edit(projPath, {silent: true}, function(err) { + should.not.exist(err); + should(didEdit).true; + done(); + }); + }); + + it('open should exec browser', function(done) { + project.open(projPath, {}, function(err) { + should.not.exist(err); + should(didOpen).true; + done(); + }); + }); + }); + +}); diff --git a/test/commands/project/swagger_editor.js b/test/commands/project/swagger_editor.js new file mode 100644 index 00000000..588dbcc4 --- /dev/null +++ b/test/commands/project/swagger_editor.js @@ -0,0 +1,110 @@ +/**************************************************************************** + The MIT License (MIT) + + Copyright (c) 2015 Apigee Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ****************************************************************************/ +'use strict'; + +var should = require('should'); +var util = require('util'); +var path = require('path'); +var proxyquire = require('proxyquire'); +var tmp = require('tmp'); +var fs = require('fs'); +var config = require('../../../config/index'); +var project = require('../../../lib/commands/project/project'); +var request = require('superagent'); +var Url = require('url'); + +var SWAGGER_EDITOR_LOAD_PATH = '/editor/spec'; // swagger-editor expects to GET the file here +var SWAGGER_EDITOR_SAVE_PATH = '/editor/spec'; // swagger-editor PUTs the file back here + +describe('swagger editor', function() { + + var name = 'basic'; + var projPath; + var swaggerFile; + var baseUrl; + + before(function(done) { + tmp.setGracefulCleanup(); + tmp.dir({ unsafeCleanup: true }, function(err, dir) { + should.not.exist(err); + var tmpDir = dir; + process.chdir(tmpDir); + + projPath = path.resolve(tmpDir, name); + swaggerFile = path.resolve(projPath, config.swagger.fileName); + process.chdir(tmpDir); + project.create(name, {}, done); + }); + }); + + var editorStubs = { + '../../util/browser': { + open: function(editorUrl, cb) { + var url = Url.parse(editorUrl); + url.hash = null; + url.query = null; + baseUrl = Url.format(url); + cb(new Error()); + } + } + }; + var editor = proxyquire('../../../lib/commands/project/swagger_editor', editorStubs); + + it('should be able to load swagger', function(done) { + var fileContents = fs.readFileSync(swaggerFile, 'utf8'); + + project.read(projPath, {}, function(err, project) { + if (err) { cb(err); } + + editor.edit(project, {}, function() { + var url = Url.resolve(baseUrl, SWAGGER_EDITOR_LOAD_PATH); + request + .get(url) + .buffer() + .end(function(err, res) { + should.not.exist(err); + res.status.should.eql(200); + res.text.should.equal(fileContents); + done(); + }); + }); + }); + }); + + it('should be able to save swagger', function(done) { + var url = Url.resolve(baseUrl, SWAGGER_EDITOR_SAVE_PATH); + request + .put(url) + .send('success!') + .end(function(err, res) { + should.not.exist(err); + res.status.should.eql(200); + + var fileContents = fs.readFileSync(swaggerFile, 'utf8'); + fileContents.should.equal('success!'); + + done(); + }); + }); +}); diff --git a/test/config.js b/test/config.js new file mode 100644 index 00000000..eab362b4 --- /dev/null +++ b/test/config.js @@ -0,0 +1,58 @@ +/**************************************************************************** + The MIT License (MIT) + + Copyright (c) 2015 Apigee Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ****************************************************************************/ +'use strict'; + +var should = require('should'); +var proxyquire = require('proxyquire').noPreserveCache(); + +describe('config', function() { + + describe('swagger env var', function() { + + it('should load', function(done) { + + var config = proxyquire('../config', {}); + should.not.exist(config.test); + process.env['swagger_test'] = 'test'; + config = proxyquire('../config', {}); + should.exist(config.test); + config.test.should.equal('test'); + done(); + }); + + it('should load subkeys', function(done) { + + var config = proxyquire('../config', {}); + should.not.exist(config.sub); + process.env['swagger_sub_key'] = 'test'; + process.env['swagger_sub_key2'] = 'test2'; + config = proxyquire('../config', {}); + should.exist(config.sub); + config.sub.should.have.property('key', 'test'); + config.sub.should.have.property('key2', 'test2'); + done(); + }); + }); + +}); diff --git a/test/helpers.js b/test/helpers.js new file mode 100644 index 00000000..b1c5dcd9 --- /dev/null +++ b/test/helpers.js @@ -0,0 +1,92 @@ +/**************************************************************************** + The MIT License (MIT) + + Copyright (c) 2015 Apigee Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ****************************************************************************/ +'use strict'; + +var fs = require('fs'); +var _ = require('lodash'); +var util = require('util'); + +module.exports.copyFile = function(source, target, cb) { + cb = _.once(cb); + + var rd = fs.createReadStream(source); + rd.on('error', function(err) { + cb(err); + }); + + var wr = fs.createWriteStream(target); + wr.on('error', function(err) { + cb(err); + }); + wr.on('close', function(err) { + cb(err); + }); + rd.pipe(wr); +}; + +// intercepts stdout and stderr +// returns object with methods: +// output() : returns captured string +// release() : must be called when done, returns captured string +module.exports.captureOutput = function captureOutput() { + var old_stdout_write = process.stdout.write; + var old_console_error = console.error; + + var captured = ''; + var callback = function(string) { + captured += string; + }; + + process.stdout.write = (function(write) { + return function(string, encoding, fd) { + var args = _.toArray(arguments); + write.apply(process.stdout, args); + + // only intercept the string + callback.call(callback, string); + }; + }(process.stdout.write)); + + console.error = (function(log) { + return function() { + var args = _.toArray(arguments); + args.unshift('[ERROR]'); + console.log.apply(console.log, args); + + // string here encapsulates all the args + callback.call(callback, util.format(args)); + }; + }(console.error)); + + return { + output: function output(err, reply) { + return captured; + }, + release: function done(err, reply) { + process.stdout.write = old_stdout_write; + console.error = old_console_error; + return captured; + } + } +}; diff --git a/test/util/browser.js b/test/util/browser.js new file mode 100644 index 00000000..b6d1a3cb --- /dev/null +++ b/test/util/browser.js @@ -0,0 +1,124 @@ +/**************************************************************************** + The MIT License (MIT) + + Copyright (c) 2015 Apigee Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ****************************************************************************/ +'use strict'; + +var should = require('should'); +var util = require('util'); +var config = require('../../config'); +var proxyquire = require('proxyquire'); + +describe('browser', function() { + + var URL = 'abc123'; + var browserStubs = { + 'child_process': { + exec: function(name, cb) { + cb(null, name); + } + } + }; + var browser = proxyquire('../../lib/util/browser', browserStubs); + + beforeEach(function() { + config.browser = undefined; + }); + + describe('Windows', function() { + + it('should start', function(done) { + browser.open(URL, function(err, command) { + command.should.equal(util.format('start "" "%s"', URL)); + done(); + }, 'win32') + }); + + it('should honor config', function(done) { + var browserPath = config.browser = '/my/browser'; + browser.open(URL, function(err, command) { + command.should.equal(util.format("start \"\" \"%s\" \"%s\"", browserPath, URL)); + done(); + }, 'win32') + }); + + }); + + describe('OS X', function() { + + it('should open', function(done) { + browser.open(URL, function(err, command) { + command.should.equal(util.format('open "%s"', URL)); + done(); + }, 'darwin') + }); + + it('should honor config', function(done) { + var browserPath = config.browser = '/my/browser'; + browser.open(URL, function(err, command) { + command.should.equal(util.format("open -a %s \"%s\"", browserPath, URL)); + done(); + }, 'darwin') + }); + + }); + + describe('Linux', function () { + + it('should open', function(done) { + browser.open(URL, function(err, command) { + command.should.equal(util.format('xdg-open "%s"', URL)); + done(); + }, 'linux'); + }); + + it('should honor config', function(done) { + var browserPath = config.browser = '/usr/bin/x-www-browser'; + browser.open(URL, function(err, command) { + command.should.equal(util.format('%s "%s"', browserPath, URL)); + done(); + }, 'linux') + }); + + }); + + describe('other Unix', function() { + + it('should err if not configured', function(done) { + config.browser = undefined; + browser.open(URL, function(err, command) { + should.exist(err); + done(); + }, 'foo') + }); + + it('should honor config', function(done) { + var browserPath = config.browser = '/my/browser'; + browser.open(URL, function(err, command) { + command.should.equal(util.format('%s "%s"', browserPath, URL)); + done(); + }, 'foo') + }); + + }); + +}); diff --git a/test/util/cli.js b/test/util/cli.js new file mode 100644 index 00000000..25133af4 --- /dev/null +++ b/test/util/cli.js @@ -0,0 +1,299 @@ +/**************************************************************************** + The MIT License (MIT) + + Copyright (c) 2015 Apigee Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ****************************************************************************/ +'use strict'; + +var should = require('should'); +var config = require('../../config'); +var proxyquire = require('proxyquire'); +var _ = require('lodash'); +var helpers = require('../helpers'); + +describe('cli', function() { + + var cliStubs = { + 'inquirer': { + prompt: function(questions, cb) { + var results = {}; + if (!_.isArray(questions)) { questions = [questions]; } + _.each(questions, function(question) { + results[question.name] = + (question.hasOwnProperty('default') && question.default !== undefined) + ? question.default + : (question.type === 'list') ? question.choices[0] : 'XXX'; + }); + cb(results); + } + } + }; + var cli = proxyquire('../../lib/util/cli', cliStubs); + + beforeEach(function() { + config.browser = undefined; + }); + + var FIELDS = [ + { name: 'baseuri', message: 'Base URI?', default: 'https://api.enterprise.apigee.com' }, + { name: 'organization', message: 'Organization?' }, + { name: 'password', message: 'Password?', type: 'password' } + ]; + + describe('requireAnswers', function() { + + it('should ensure all questions are answered', function(done) { + + cli.requireAnswers(FIELDS, {}, function(results) { + results.baseuri.should.equal('https://api.enterprise.apigee.com'); + results.organization.should.equal('XXX'); + results.password.should.equal('XXX'); + done(); + }); + }); + + it('should not ask for questions already answered', function(done) { + + var results = { + password: 'password' + }; + + cli.requireAnswers(FIELDS, results, function(results) { + results.password.should.equal('password'); + done(); + }); + }); + + }); + + describe('updateAnswers', function() { + + it('should ask all questions', function(done) { + + cli.updateAnswers(FIELDS, {}, function(results) { + results.organization.should.equal('XXX'); + results.baseuri.should.equal('XXX'); + results.password.should.equal('XXX'); + done(); + }); + }); + + + it('should default to existing answers', function(done) { + + var results = { + baseuri: 'baseuri' + }; + + cli.updateAnswers(FIELDS, results, function(results) { + results.organization.should.equal('XXX'); + results.baseuri.should.equal('baseuri'); + done(); + }); + }); + + it('should not default password', function(done) { + + var results = { + password: 'password' + }; + + cli.updateAnswers(FIELDS, results, function(results) { + results.password.should.equal('XXX'); + done(); + }); + }); + + }); + + describe('confirm', function() { + + it('should default true', function(done) { + + cli.confirm('true?', function(result) { + result.should.equal(true); + done(); + }); + }); + + it('should default false', function(done) { + + cli.confirm('false?', false, function(result) { + result.should.equal(false); + done(); + }); + }); + + }); + + describe('chooseOne', function() { + + it('should return one', function(done) { + + cli.chooseOne('choose?', ['1', '2'], function(result) { + result.should.equal('1'); + done(); + }); + }); + + }); + + describe('printAndExit', function() { + + var oldExit = process.exit; + var exitCode; + + before(function() { + process.exit = (function() { + return function(code) { + exitCode = code; + } + })(); + }); + + after(function() { + process.exit = oldExit; + }); + + var capture; + beforeEach(function() { + capture = helpers.captureOutput(); + exitCode = undefined; + }); + + afterEach(function() { + capture.release(); + }); + + it('should log errors', function() { + + cli.printAndExit(new Error('test')); + exitCode.should.equal(1); + }); + + it('should log strings', function() { + + cli.printAndExit(null, 'test'); + exitCode.should.equal(0); + capture.output().should.equal('test\n'); + }); + + it('should log simple objects', function() { + + cli.printAndExit(null, { test: 1 }); + exitCode.should.equal(0); + capture.output().should.equal('test: 1\n\n'); + }); + + it('should log complex objects', function() { + + cli.printAndExit(null, { test: { test: 1 } }); + exitCode.should.equal(0); + capture.output().should.equal('test:\n test: 1\n\n'); + }); + + it('should hide passwords', function() { + + cli.printAndExit(null, { password: 1 }); + exitCode.should.equal(0); + capture.output().should.equal("password: '******'\n\n"); + }); + + describe('execute', function() { + + var executeNoError = function(arg1, cb) { + cb(null, arg1); + }; + + it('should error if no command', function() { + cli.execute(null, 'whatever')(); + exitCode.should.equal(1); + capture.output().should.match(/Error: missing command method/); + }); + + it("should error if arguments don't match", function() { + + cli.execute(executeNoError, 'whatever')(); + exitCode.should.equal(1); + capture.output().should.match(/Error: incorrect arguments/); + }); + + it('should print the result of the command', function() { + + cli.execute(executeNoError)(1); + exitCode.should.equal(0); + capture.output().should.equal('1\n'); + }); + + it('should print the result with header', function() { + + cli.execute(executeNoError, 'whatever')(1); + exitCode.should.equal(0); + capture.output().should.equal('whatever\n========\n1\n'); + }); + + }); + + }); + + describe('validate', function() { + + var helpCalled = false; + var app = { + commands: [ + { _name: '1' }, + { _name: '2' } + ], + help: function() { + helpCalled = true; + }, + rawArgs: new Array(3) + }; + + beforeEach(function() { + helpCalled = false; + }); + + it('should do nothing if valid command', function() { + + app.rawArgs[2] = '1'; + cli.validate(app); + helpCalled.should.be.false; + }); + + it('should error if invalid command', function() { + + app.rawArgs[2] = '3'; + cli.validate(app); + helpCalled.should.be.true; + }); + }); + + describe('version', function() { + + it('should return the version', function() { + + var version = require('../../package.json').version; + cli.version().should.eql(version); + }); + }); + +}); diff --git a/test/util/net.js b/test/util/net.js new file mode 100644 index 00000000..677c9175 --- /dev/null +++ b/test/util/net.js @@ -0,0 +1,114 @@ +/**************************************************************************** + The MIT License (MIT) + + Copyright (c) 2015 Apigee Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ****************************************************************************/ +'use strict'; + +var should = require('should'); +var path = require('path'); +var tmp = require('tmp'); +var fs = require('fs'); +var _ = require('lodash'); +var http = require('http'); +var netutil = require('../../lib/util/net'); + +describe('net', function() { + + describe('isPortOpen', function() { + + var port, server; + + it('should recognize an open port', function(done) { + + serve(null, function(p, s) { + port = p; server = s; + netutil.isPortOpen(port, function(err, open) { + should.not.exist(err); + should(open).be.true; + done(); + }); + }) + }); + + it('should recognize an unopened port', function(done) { + + server.close(); + netutil.isPortOpen(port, function(err, open) { + should.not.exist(err); + should(open).be.false; + done(); + }); + }); + }); + + describe('download', function() { + + it('should download a file', function(done) { + + tmp.setGracefulCleanup(); + tmp.dir({ unsafeCleanup: true }, function(err, tmpDir) { + should.not.exist(err); + + var sourceFile = path.resolve(tmpDir, 'source'); + var outputLines = ''; + for (var i = 0; i < 20; i++) { + outputLines += 'line' + i + '\n'; + } + fs.writeFileSync(sourceFile, outputLines); + var destFile = path.resolve(tmpDir, 'dest'); + + serve(sourceFile, function(port, server) { + var url = 'http://localhost:' + port; + + netutil.download(url, destFile, function(err, size) { + server.close(); + should.not.exist(err); + + var destContent = fs.readFileSync(destFile, 'utf8'); + size.should.eql(destContent.length); + destContent.should.eql(outputLines); + done(); + }); + }); + }); + }); + }); +}); + +// return port, server +function serve(file, cb) { + var server = http.createServer(function(req, res) { + fs.readFile(file, function(err, data) { + if (err) { + res.writeHead(404); + res.end(JSON.stringify(err)); + return; + } + res.writeHead(200); + res.end(data); + }); + }); + server.listen(0, 'localhost', function() { + var port = server.address().port; + cb(port, server); + }); +}