diff --git a/.gitignore b/.gitignore index 42ff7ce9..2d6664e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,47 +1,32 @@ -# Compiled source # -################### -*.com -*.class -*.dll -*.exe -*.o -*.so - -# Packages # -############ -# it's better to unpack these files and commit the raw source -# git has its own built in compression methods -*.7z -*.dmg -*.gz -*.iso -*.jar -*.rar -*.tar -*.zip - -# Logs and databases # -###################### -*.log -*.sql -*.sqlite - -# OS generated files # -###################### -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -Icon? -ehthumbs.db -Thumbs.db - -# Project Specific # -#################### -node_modules/ - -# IDE # -####### -*.iml +# IDE files .idea +.vscode + +# 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/.gitmodules b/.gitmodules deleted file mode 100644 index 52cf6368..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "swagger-ui"] - path = swagger-ui - url = https://github.com/wordnik/swagger-ui.git 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/.npmignore b/.npmignore deleted file mode 100644 index c5316eeb..00000000 --- a/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -sample-application/ -swagger-ui-*/ -.gitmodules -*-project diff --git a/.travis.yml b/.travis.yml index 20fd86b6..86850598 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,5 @@ language: node_js node_js: - - 0.10 + - "stable" + - "4" +sudo: false diff --git a/LICENSE b/LICENSE index 9f93e067..01abb442 100644 --- a/LICENSE +++ b/LICENSE @@ -1,11 +1,201 @@ -Copyright 2014 Reverb Technologies, Inc. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at [apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 SmartBear Software Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 222e6ca8..f6b98835 100644 --- a/README.md +++ b/README.md @@ -1,236 +1,118 @@ -# Swagger for Express and Node.js +# Archived -[![Build Status](https://travis-ci.org/wordnik/swagger-node-express.png)](https://travis-ci.org/wordnik/swagger-node-express) +This GitHub repository has been archived. +The npmjs package `swagger` may be re-used at a later date for a different project. +https://swagger.io/ may have similar or alternative functionality if you depend(ed) on this project. -This is a [Swagger](https://github.com/wordnik/swagger-spec) module for the [Express](http://expressjs.com) web application framework for Node.js. -Try a sample! The source for a [functional sample](https://github.com/wordnik/swagger-node-express/blob/master/SAMPLE.md) is available on github. +[![Build Status](https://travis-ci.org/swagger-api/swagger-node.svg?branch=master)](https://travis-ci.org/swagger-api/swagger-node) +[![NPM version](https://badge.fury.io/js/swagger.png)](http://badge.fury.io/js/swagger) +[![Dependency Status](https://david-dm.org/swagger-api/swagger-node/status.svg)](https://david-dm.org/swagger-api/swagger-node) +[![devDependency Status](https://david-dm.org/swagger-api/swagger-node/dev-status.svg)](https://david-dm.org/swagger-api/swagger-node#info=devDependencies) -## What's Swagger? -The goal of Swagger™ is to define a standard, language-agnostic interface to REST APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection. When properly defined via Swagger, a consumer can understand and interact with the remote service with a minimal amount of implementation logic. Similar to what interfaces have done for lower-level programming, Swager removes the guesswork in calling the service. +The `swagger` module provides tools for designing and building Swagger-compliant APIs entirely in Node.js. It integrates with popular Node.js servers, including Express, Hapi, Restify, and Sails, as well as any Connect-based middleware. With `swagger`, you can specify, build, and test your API from the very beginning, on your laptop. It allows you to change and iterate your design without rewriting the logic of your implementation. +![alt text](./docs/images/overview2.png) -Check out [Swagger-Spec](https://github.com/wordnik/swagger-spec) for additional information about the Swagger project, including additional libraries with support for other languages and more. +Remember, one great thing about this approach is that all of the Swagger validation logic is handled for you, and all of the routing logic is managed through the Swagger configuration. You don't have to code (or recode!) any of that stuff yourself. -## Installation +# Your swagger API in five steps -Using NPM, include the `swagger-node-express` module in your `package.json` dependencies. - -```json -{ - ... - "dependencies": { - "swagger-node-express": "~2.0", - ... - } -} -``` +## 1. Install the swagger module +Install using npm. For complete instructions, see the [install](./docs/install.md) page. -## Adding Swagger to an Express Application - -```js -// Load module dependencies. -var express = require("express") - , url = require("url") - , swagger = require("swagger-node-express"); - -// Create the application. -var app = express(); -app.use(express.json()); -app.use(express.urlencoded()); - -// Couple the application to the Swagger module. -swagger.setAppHandler(app); +```bash +$ npm install -g swagger ``` -You can optionally add a validator function, which is used to filter the swagger json and request operations: +## 2. Create a new swagger project -```js -// This is a sample validator. It simply says that for _all_ POST, DELETE, PUT methods, -// the header api_key OR query param api_key must be equal to the string literal -// special-key. All other HTTP ops are A-OK */ - -swagger.addValidator( - function validate(req, path, httpMethod) { - // example, only allow POST for api_key="special-key" - if ("POST" == httpMethod || "DELETE" == httpMethod || "PUT" == httpMethod) { - var apiKey = req.headers["api_key"]; - if (!apiKey) { - apiKey = url.parse(req.url,true).query["api_key"]; - } - if ("special-key" == apiKey) { - return true; - } - return false; - } - return true; - } -); +Use the [CLI](./docs/cli.md) to create and manage projects. Learn more on the [quick start](./docs/quick-start.md) page. +```bash +$ swagger project create hello-world ``` -You now add models to the swagger context. Models are described in a JSON format, per the [swagger model specification](https://github.com/wordnik/swagger-core/wiki/Datatypes). Most folks keep them in a separate file (see [here](https://github.com/wordnik/swagger-node-express/blob/master/Apps/petstore/models.js) for an example), or you can add them as such: +## 3. Design your API in the Swagger Editor -```js -swagger.addModels(models); +The interactive, browser-based [Swagger Editor](http://editor.swagger.io/) is built in. It provides Swagger 2.0 validation and endpoint routing, generates docs on the fly, and consumes easy-to-read YAML. +```bash +$ swagger project edit ``` -Next, add some resources. Each resource contains a swagger spec as well as the action to execute when called. The spec contains enough to describe the method, and adding the resource will do the rest. For example: +![screenshot of project editor](./docs/images/project-editor.png) +## 4. Write controller code in Node.js -```js -var findById = { - 'spec': { - "description" : "Operations about pets", - "path" : "/pet.{format}/{petId}", - "notes" : "Returns a pet based on ID", - "summary" : "Find pet by ID", - "method": "GET", - "parameters" : [swagger.pathParam("petId", "ID of pet that needs to be fetched", "string")], - "type" : "Pet", - "errorResponses" : [swagger.errors.invalid('id'), swagger.errors.notFound('pet')], - "nickname" : "getPetById" - }, - 'action': function (req,res) { - if (!req.params.petId) { - throw swagger.errors.invalid('id'); - } - var id = parseInt(req.params.petId); - var pet = petData.getPetById(id); - - if (pet) { - res.send(JSON.stringify(pet)); - } else { - throw swagger.errors.notFound('pet'); - } - } -}; - -swagger.addGet(findById); - -``` - -Adds an API route to express and provides all the necessary information to swagger. - -Finally, configure swagger with a `public` URL and version (note, this must be called after all the other swagger API calls): - -```js -swagger.configure("http://petstore.swagger.wordnik.com", "0.1"); -``` - -and the server can be started: +Code your API's business logic in Node.js. ```js -app.listen(8002); -``` - -Now you can open up a [swagger-ui](https://github.com/wordnik/swagger-ui) and browse your API, generate a client with [swagger-codegen](https://github.com/wordnik/swagger-codegen), and be happy. - - -## Additional Configurations - -### .{format} suffix removal - -If you don't like the .{format} or .json suffix, you can override this before configuring swagger: - -```js -swagger.configureSwaggerPaths("", "/api-docs", ""); +function hello(req, res) { + var name = req.swagger.params.name.value || 'stranger'; + var hello = util.format('Hello, %s!', name); + res.json({ "message": hello }); +} ``` -That will put the resource listing under `/api-docs`, and ditch the `.{format}` on each of the apis you're adding to. Make sure to set the paths correctly in your spec configuration though, like such: +If you look at the Swagger file in the editor (shown in step 3 above), the `x-swagger-router-controller` element (line 17 in the editor screenshot) specifies the name of the controller file associated with the `/hello` path. For example: -```js -// note the .{format} is removed from the path! -var findById = { - 'spec': { - "description" : "Operations about pets", - "path" : "/pet/{petId}", - "notes" : "Returns a pet based on ID", - ... +```yaml + paths: + /hello: + x-swagger-router-controller: hello_world ``` -### Mapping swagger to subpaths +Controller source code is always placed in `./api/controllers`. So, the controller source file for this project is `./api/controllers/hello_world.js`. -To add a subpath to the api (i.e. list your REST api under `/api` or `/v1`), you can configure express as follows: +The `operationId` element specifies which controller function to call. In this case (line 19), it is a function called `hello`. Learn [more](./docs/controllers.md). -```js -var app = express(); -var subpath = express(); +## 5. Run the server -app.use(express.json()); -app.use(express.urlencoded()); -app.use("/v1", subpath); +Run the project server. -swagger.setAppHandler(subpath); +```bash +$ swagger project start ``` -Now swagger and all apis configured through it will live under the `/v1` path (i.e. `/v1/api-docs`). +## Now, call the API! -### Allows special headers +It just works! -If you want to modify the default headers sent with every swagger-managed method, you can do so as follows: - -```js -swagger.setHeaders = function setHeaders(res) { - res.header("Access-Control-Allow-Headers", "Content-Type, X-API-KEY"); - res.header("Content-Type", "application/json; charset=utf-8"); -}; +```bash +$ curl http://127.0.0.1:10010/hello?name=Scott +{ "message": "Hello, Scott!" } ``` -If you have a special name for an api key (such as `X-API-KEY`, per above), this is where you can inject it. -### Enabling cors support using cors library +# Installing the swagger module -To enable cors support using cors express npm module (https://npmjs.org/package/cors) add the following to your app. +See the [Installing swagger](./docs/install.md) for details. -```js -var cors = require('cors'); - -var corsOptions = { - credentials: true, - origin: function(origin,callback) { - if(origin===undefined) { - callback(null,false); - } else { - // change wordnik.com to your allowed domain. - var match = origin.match("^(.*)?.wordnik.com(\:[0-9]+)?"); - var allowed = (match!==null && match.length > 0); - callback(null,allowed); - } - } -}; - -app.use(cors(corsOptions)); +# Using the swagger module -``` +Go to the [swagger module doc page](./docs/README.md). It includes all the information you need to get started. -### Configuring the Resource Listing Information +# About this project -The Swagger `info` node of the resource listing can be configured using the `configureDeclaration` method: +This initiative grew out of Apigee-127, an API design-first development framework using Swagger. +Apigee donated the code to create the swagger-node project in 2015. -```js -swagger.configureDeclaration('pet', { - description: 'Operations about Pets', - authorizations : ["oauth2"], - protocols : ["http"], - consumes: ['application/json'], - produces: ['application/json'] -}); -``` + >Copyright 2016 Apigee Corporation -## License + >Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -Copyright 2014 Reverb Technologies, Inc. + >http://www.apache.org/licenses/LICENSE-2.0 -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at [apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + >Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +--- + diff --git a/SAMPLE.md b/SAMPLE.md deleted file mode 100644 index bc6b1f41..00000000 --- a/SAMPLE.md +++ /dev/null @@ -1,86 +0,0 @@ -This is the Wordnik Swagger code for the express framework. For more on Swagger, please visit http://swagger.wordnik.com. For more on express, please visit https://github.com/visionmedia/express - -### To run the sample App - -You must first install dependencies: - -```js -npm install -``` - -To run the sample server: -```js -node Apps/petstore/main.js -``` - -Then visit the server directly from your browser: - -``` -http://localhost:8002/api-docs -``` - -or from [swagger UI](https://github.com/wordnik/swagger-ui), mounted at `/docs`: [http://localhost:8002/docs](http://localhost:8002/docs). - -### How it works - -The swagger.js file is included when configuring the express server. There -are a few additional steps to get the api to declare the swagger spec: - -
  • Define your input/output models in JSON schema format - -
  • Define a specification for operations against the API - -For the sample app, the models are defined here: [Apps/petstore/models.js](https://github.com/wordnik/swagger-node-express/blob/master/Apps/petstore/models.js) - -You could load this from a static file or generate them programatically as in the -sample. - -The operations and the callback functions are defined in this file: [Apps/petstore/petResources.js](https://github.com/wordnik/swagger-node-express/blob/master/Apps/petstore/petResources.js) - -Each spec defines input/output params with helper functions to generate the swagger -metadata. - -When the routes are added (see petstore.js: addGet, addPost...), the params -are validated and added to the schema. If they fail validation, the failure -will be logged to the console and they will not be added to the server. - -### Other notes - -The swagger.js code wraps exceptions and turns them into the appropriate HTTP -error response. To take advantage of this, you can throw exceptions as follows: - -```js -try{ - // some dangerous function -} -catch(ex){ - throw { - "code":401, - "description":"You forgot to log in!" - } -} -``` - -Also, the "Access-Control-Allow-Origin" is hard-coded to "*" to allow access from -localhost. This will become a configuration option at some point. - -#### Security - -You can secure the API by adding your own validator. These methods can read the -request object and extract cookies, headers, api-keys, etc. They also have -access to the HTTP method and path being requested. You can then decide for -yourself it the caller should have access to the resource. See the petstore.js -example: - -```js -swagger.addValidator( - function validate(req, path, httpMethod) { - ... -``` - - -### Current limitations - -
  • Only JSON is supported
  • - -
  • - There are probably (many) others
  • diff --git a/bin/swagger-project.js b/bin/swagger-project.js new file mode 100755 index 00000000..1647ac5a --- /dev/null +++ b/bin/swagger-project.js @@ -0,0 +1,81 @@ +#!/usr/bin/env node +/**************************************************************************** + Copyright 2016 Apigee Corporation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ****************************************************************************/ +'use strict'; + +var app = require('commander'); +var project = require('../lib/commands/project/project'); +var cli = require('../lib/util/cli'); +var execute = cli.execute; +var frameworks = Object.keys(project.frameworks).join('|'); +var assertiontypes = project.assertiontypes.join('|'); +var testmodules = project.testmodules.join('|'); + +app + .command('create [name]') + .description('Create a folder containing a Swagger project') + .option('-f, --framework ', 'one of: ' + frameworks) + .action(execute(project.create)); + +app + .command('start [directory]') + .description('Start the project in this or the specified directory') + .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 browser as client to the project') + .option('-n, --node-args ', 'run node with extra arguments (like --node-args \"--harmony\")') + .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') + .option('--host ', 'the hostname the editor is served from') + .option('-p, --port ', 'the port the editor is served from') + .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 + .command('generate-test [directory]') + .description('Generate the test template') + .option('-p, --path-name [path]', 'a specific path of the api, also supports regular expression') + .option('-f, --test-module ', 'one of: ' + testmodules) + .option('-t, --assertion-format ', 'one of: ' + assertiontypes) + .option('-o, --force', 'allow overwriting of all existing test files matching those generated') + .option('-l, --load-test [path]', 'generate load-tests for specified operations') + .action(execute(project.generateTest)); + +app.parse(process.argv); +cli.validate(app); diff --git a/bin/swagger.js b/bin/swagger.js new file mode 100755 index 00000000..211da4a1 --- /dev/null +++ b/bin/swagger.js @@ -0,0 +1,52 @@ +#!/usr/bin/env node +/**************************************************************************** + Copyright 2016 Apigee Corporation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ****************************************************************************/ +'use strict'; + +var app = require('commander'); +var browser = require('../lib/util/browser'); +var rootCommands = require('../lib/commands/commands'); +var cli = require('../lib/util/cli'); +var execute = cli.execute; + +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 + .command('validate [swaggerFile]') + .description('validate a Swagger document (supports unix piping)') + .option('-j, --json', 'output as JSON') + .action(execute(rootCommands.validate)); + +app + .command('convert [apiDeclarations...]') + .description('Converts Swagger 1.2 documents to a Swagger 2.0 document') + .option('-o, --output-file ', 'specify an output-file to write to') + .action(execute(rootCommands.convert)); + +app.parse(process.argv); +cli.validate(app); \ No newline at end of file diff --git a/config/index.js b/config/index.js new file mode 100644 index 00000000..38908e51 --- /dev/null +++ b/config/index.js @@ -0,0 +1,80 @@ +/**************************************************************************** + Copyright 2016 Apigee Corporation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ****************************************************************************/ +'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'), + editorConfig: { + analytics: { google: { id: null } }, + disableCodeGen: true, + disableNewUserIntro: true, + examplesFolder: '/spec-files/', + exampleFiles: [], + autocompleteExtension: {}, + useBackendForStorage: true, + backendEndpoint: '/editor/spec', + backendHealthCheckTimeout: 5000, + useYamlBackend: true, + disableFileMenu: true, + enableTryIt: true, + headerBranding: false, + brandingCssClass: null, + schemaUrl: '/schema/swagger.json', + importProxyUrl: 'https://cors-it.herokuapp.com/?url=' + } +}; + +// project // + +config.project = { + port: process.env.PORT || 10010, + skeletonsDir: path.resolve(__dirname, '..', 'project-skeletons') +}; + +// 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/docs/README.md b/docs/README.md new file mode 100644 index 00000000..400afa25 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,20 @@ + + + +## Welcome to Swagger! + +![alt text](./images/swagger-icon.png) + +* [Introduction](./introduction.md) +* [Installation](./install.md) +* [Quick start](./quick-start.md) +* [Configuration](./configuration.md) +* [CLI reference](./cli.md) +* [About Swagger](./swagger-about.md) +* [About the swagger.yaml file](./swagger-file.md) +* [Adding paths](./adding-paths.md) +* [Writing controllers](./controllers.md) +* [Using mock mode](./mock-mode.md) +* [Modules and dependencies](./modules.md) +* [Reporting issues](./report-issues.md) +* [Release Notes](./release-notes.md) diff --git a/docs/adding-paths.md b/docs/adding-paths.md new file mode 100644 index 00000000..feace925 --- /dev/null +++ b/docs/adding-paths.md @@ -0,0 +1,255 @@ + + +## Anatomy of a path + +This topic looks at how paths are constructed and wired to response objects in a Swagger project's Swagger configuration file. + +* [Simple path example](#simple) +* [More about route handling](#more) +* [Request and response models](#models) +* [Next steps](#nextstep) + +### Simple path example + +The `/hello` path in the original Quick Start example looks like this: + +```yaml + paths: + /hello: + # binds swagger 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" +``` + +The parts of the path definition include: + +* `x-swagger-router-controller` is a custom Swagger extension to the Swagger model that maps a path to a controller file. For instance, `/weather` gets mapped to `api/controller/weather.js`. See [More about route handling]() below. + +* `operationId` maps to a method name in the controller file. + +* `security:` can be used to apply security schemes such as OAuth, Basic authentication, and API keys. + +* `parameters:` specifies any parameters used by the path. They can be passed as query or form parameters, or headers. + +* The other keys conform to the Swagger 2.0 [specifications](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md). The parameters is a YAML array that defines all the parameters required for the call. The responses object defines the response specifications for response codes. + +### More about route handling + +As noted previously, `the x-swagger-router-controller` maps a path to a controller file. You can specify a router handler either at the path level or operation level in the Swagger file. For example, at the path level, like this: + +```yaml +paths: + /hello: + x-swagger-router-controller: hello_world + get: + description: Returns 'Hello' to the caller + # used as the method name of the controller + operationId: hello +``` + +or at the operation level, like this: + +```yaml +paths: + /hello: + get: + x-swagger-router-controller: hello_world + description: Returns 'Hello' to the caller + # used as the method name of the controller + operationId: hello +``` + +Routers applied at the operation level override routers specified at the path level. + +When you call your API with a given path, like `GET /hello`, the middleware checks to make sure there is an operation (e.g., "get") defined for that path. If not, then a 405 code is returned. If the path does have a corresponding operation defined, then a 200 response is returned. If you are running in mock mode, you'll get back a mock response, otherwise, you'll get back whatever response is returned by the controller. If you hit a path that is not specified in the Swagger file at all, then that's a 404. + +When you call your API, the middleware attempts to match a route defined in the Swagger file to a corresponding controller method. When you test your API, one of three possible results can occur: + +* A route defined in the Swagger file matches a controller file, and the controller has a method for the operation. In this case, either the route method is executed or, if you are in mock mode, a mock response is returned. + +* A route defined in the Swagger file matches a controller file, but there is no method in the controller for the operation. In this case, a 405 HTTP code is returned. + +* The requested route is not present in the Swagger file. In this case, a 404 code is returned. + +### Request and response models + +The OpenAPI Specification allows you to define both request and the response models (also called schemas). The `path` definition described in the previous section is an example of a request model. + +Here's an example of a weather API that returns a relatively complex object. + +The Open Weather API returns an object that looks like the following: + + ```json + { + "coord": { + "lon": -77.58, + "lat": 35.27 + }, + "sys": { + "type": 1, + "id": 1786, + "message": 0.1057, + "country": "United States of America", + "sunrise": 1409913972, + "sunset": 1409959883 + }, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + }, + { + "id": 211, + "main": "Thunderstorm", + "description": "thunderstorm", + "icon": "11n" + } + ], + "base": "cmc stations", + "main": { + "temp": 78.58, + "pressure": 1021, + "humidity": 88, + "temp_min": 73.4, + "temp_max": 82.4 + }, + "wind": { + "speed": 5.62, + "deg": 40 + }, + "clouds": { + "all": 90 + }, + "dt": 1409876198, + "id": 4474436, + "name": "Kinston", + "cod": 200 + } + ``` + + +To wire the path to the schema, you use the `responses` element to refer to the schema definition. In this example, we specify two responses, a 200 and an Error response. Note that you use `$ref` syntax to refer to each specific schema definition, listed in the `#/definitions` section (which we describe below). + +>Note: You must use explicit references of the form $ref: #/definitions/. For example: $ref: #/definitions/WeatherResponse. + +>Note: In this case, all other responses that are not 200 will be referred to the Error response schema. + +```yaml + paths: + /weather: + ... + responses: + "200": + description: Success + schema: + $ref: #/definitions/WeatherResponse + default: + description: Error + schema: + $ref: #/definitions/ErrorResponse +``` + +Then in the `#/definitions` section of the Swagger document we define the `WeatherResponse` schemas that we referenced from the `/paths` section. Here is the schema that represents the JSON returned by the weather API (shown previously). These schemas are primarily used to provide response objects for mock mode: + +```yaml +definitions: + WeatherResponse: + type: "object" + properties: + base: + type: "string" + clouds: + type: "object" + properties: + all: + type: "number" + cod: + type: "number" + coord: + type: "object" + properties: + lat: + type: "number" + lon: + type: "number" + dt: + type: "number" + id: + type: "number" + main: + type: "object" + properties: + humidity: + type: "number" + pressure: + type: "number" + temp_max: + type: "number" + temp_min: + type: "number" + temp: + type: "number" + name: + type: "string" + sys: + type: "object" + properties: + country: + type: "string" + id: + type: "number" + message: + type: "number" + sunrise: + type: "number" + sunset: + type: "number" + type: + type: "number" + weather: + type: "array" + items: + type: "object" + properties: + description: + type: "string" + icon: + type: "string" + id: + type: "number" + main: + type: "string" + wind: + type: "object" + properties: + deg: + type: "number" + speed: + type: "number" +``` + +### Next steps + +Now that you know have added a path, its time to [implement the actual controller](./controllers.md) diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 00000000..693a3e16 --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,152 @@ + + + +## Command-line interface reference + +Create, run, and manage Swagger projects from the command line. + +* [swagger](#swagger) +* [project create](#create) +* [project start](#start) +* [project verify](#verify) +* [project edit](#edit) +* [project open](#open) +* [project test](#test) +* [docs](#docs) + +#### swagger + +Options: + +* -h, --help: Outputs usage information. +* -V, --version: Outputs the swagger cli version number. + +Example: + + swagger -V + 0.2.0 + + +#### swagger project create [options] [name] + +Creates a folder with the specified [name] containing a new Swagger project. A project skeleton is downloaded from GitHub and installed in the new folder. + +Options: + +* -h, --help: Outputs usage information. +* -f, --framework : Specifies an API framework to use with the project. Choices are connect, express, hapi, restify, or sails. + +Example: + + swagger project create -f express sn-express + ls sn-express + README.md api app.js config node_modules package.json test + + +#### swagger project start [options] [directory] + +Starts the Swagger project in the current (or specified) directory. The server automatically restarts when you make changes to the project. You can also force a restart by typing `rs` on the server command line. + +Options: + +* -h, --help: Outputs usage information. +* -d, --debug : Start in remote debug mode so you can connect to it with a debugger. +* -b, --debug-brk : Start in remote debug mode, wait for debugger. +* -m, --mock: Start in mock mode. For more information, see [Running in mock mode](./mock-mode.md). +* -o, --open: Open the default browser as a client to the project. +* -n, --node-args : Pass extra arguments to node. E.g. `swagger project start --node-args "--harmony"` will run node with [ES6 a.k.a harmony features](https://github.com/joyent/node/wiki/ES6-%28a.k.a.-Harmony%29-Features-Implemented-in-V8-and-Available-in-Node) enabled. + +Example: + + cd ./myproject + swagger project start -m + + +#### swagger project verify [options] [project root directory] + +Verifies that the project in the current (or specified) directory is correct. Reports errors and warnings from the Swagger model, project configuration, etc. + +Options: + +* -h, --help: Outputs usage information. +* -j, --json: Output information in JSON format. + +Example: + + cd ./myproject + swagger project verify + Project Errors + -------------- + #/: Missing required property: paths + #/: Additional properties not allowed: aths + Results: 2 errors, 0 warnings + + + +#### swagger project edit [options] [directory] + +Opens the project in the current (or specified) directory in the [Swagger Editor](https://github.com/swagger-api/swagger-editor). + +![alt text](./images/swagger-editor.png) + +Options: + +* -h, --help: Outputs usage information. +* -s, --silent: Do not open the browser. +* --host : The hostname the editor is served from (default: 127.0.0.1). +* -p, --port : The port the editor is served from (default: random port). + +Example: + + cd ./myproject + swagger project edit + + +#### swagger project open [directory] + +Opens the browser as a client to the current or specified project. + +Options: + +* -h, --help: Outputs usage information. + +Example: + +`swagger project open ./myproject` + + + +#### swagger project test [options] [directory-or-file] + +Runs project tests. + +Options: + +* -h, --help: Outputs usage information. +* -d, --debug : Start in remote debug mode so you can connect to it with a debugger. +* -b, --debug-brk : Start in remote debug mode, wait for debugger. +* -m, --mock: Start in mock mode. For more information, see [Running in mock mode](./mock-mode.md). + +Example: + + `swagger project test` + + controllers + hello_world + GET /hello + ✓ should return a default string + ✓ should accept a name parameter + 2 passing (27ms) + + +##### swagger docs + +Opens the OpenAPI 2.0 Specification in your browser. + +Example: + + swagger docs + + + + diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 00000000..fc804b22 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,110 @@ +## Configuration + +** NOTE: The following applies to swagger-node apps replying on swagger-node-runner 0.5.x and better. (ie. Any app using swagger-connect 0.1.0, swagger-express-mw 0.1.0, swagger-hapi 0.1.0, swagger-restify 0.1.0, or swagger-sails 0.1.0 - or higher versions of the same.) ** + +Swagger-Node application configuration is driven by the file `default.yaml` (by default) in the application's `config` directory. Configuration is driven by the [config](https://github.com/lorenwest/node-config/wiki/Configuration-Files) module, so reference its documentation to understand how you may set up configuration per environment and perform configuration overrides. By default, the configuration file looks something like this: + +```yaml +# swagger configuration file + +# values in the swagger hash are system configuration for swagger-node +swagger: + + fittingsDirs: [ api/fittings, node_modules ] + defaultPipe: null + swaggerControllerPipe: swagger_controllers # defines the standard processing pipe for controllers + + # values defined in the bagpipes key are the bagpipes pipes and fittings definitions + # (see https://github.com/apigee-127/bagpipes) + bagpipes: + + _router: + name: swagger_router + mockMode: false + mockControllersDirs: [ api/mocks ] + controllersDirs: [ api/controllers ] + + _swagger_validate: + name: swagger_validator + validateResponse: true + + # pipe for all swagger-node controllers + swagger_controllers: + - onError: json_error_handler + - cors + - swagger_security + - _swagger_validate + - express_compatibility + - _router + + # pipe to serve swagger (endpoint is in swagger.yaml) + swagger_raw: + name: swagger_raw + +# any other values in this file are just loaded into the config for application access... +``` + +Important things to note: + +* All configuration for the Swagger-Node system is under the `swagger` key +* Overall system behavior is driven by configuring the [Bagpipes](https://github.com/apigee-127/bagpipes) library +* You may include other values and sections as you wish, they will just be loaded into the config for your application + to access. + +Let's walk through the configuration: + +### fittingsDirs + +Defines the directories Bagpipes will search for fittings that are defined or used in the bagpipes section below. Fittings are extension plugins that can either be installed (eg. https://www.npmjs.com/package/volos-swagger-oauth and https://www.npmjs.com/package/volos-swagger-apply) or written into your application directly. + +### defaultPipe + +If no pipe is explicitly declared for a path or operation, this pipe will be played when that endpoint is hit. + +### swaggerControllerPipe + +This names the standard pipe that plays for the swagger-node controllers (declared in the swagger.yaml with the +extension `x-swagger-router-controller`). We'll look at how that's defined in a second. + +### bagpipes + +This block is the configuration passed to the [bagpipes](https://github.com/apigee-127/bagpipes) underlying the application. As you can see, it defines not only the flow, but also the configuration of the elements. + +#### _router + +This configures the standard swagger-node router (currently swagger-tools). It tells it how to find your controllers, your mock controllers, and whether to run in mock mode. + +#### _swagger_validate + +This configures the swagger validator (currently swagger-tools). You can turn response validation on and off here. + +#### swagger_controllers + +Because this is specified as your controller pipe (in the `swaggerControllerPipe` setting above), this pipe plays for all paths and operations where you've specified a controller extension (`x-swagger-router-controller`). + +The default pipe is as follows: + +1. set an error handler that converts all errors to JSON +2. run the [cors](https://www.npmjs.com/package/cors) module +3. execute swagger security (currently swagger-tools) +4. run swagger validator (currently swagger-tools) +5. add a few commonly used Express functions (if not already present) to request (path, query, get) and response (json, + get, set, status). +6. run the router (currently swagger-tools) + +As you can see, you have full control over the pipeline and may add or remove elements you need for your specific application and environment. + +#### swagger_raw + +This serves your swagger file - on the path that is defined in your `api/swagger/swagger.yaml` and tagged with the `x-swagger-pipe` extension. It looks like this: + +```yaml + /swagger: + x-swagger-pipe: swagger_raw +``` + +Note: This automatically filters out all sections that are swagger extensions (`x-*`) by using a predefined regular expression: `/^(?!x-.*)/`. + +Naturally, if you don't wish to serve your swagger on this path or at all, you may change or remove this. + +This also conveniently serves as an example of how to map a path in your Swagger to a pipe. You may, of course, define and use any pipes you wish using any of the Bagpipes operations or add your own in your fittings directory. diff --git a/docs/controllers.md b/docs/controllers.md new file mode 100644 index 00000000..552f7587 --- /dev/null +++ b/docs/controllers.md @@ -0,0 +1,118 @@ + +## About controllers + +* [Implementing a controller](#implementing) +* [Using query parameters](#query) +* [Weather API example](#weather) + +### Implementing a controller + +This topic explains how to implement a controller. The `x-swagger-router-controller` Swagger extension element is used to specify the name of a controller file. The quick start example defines a `hello_world` controller file, which is by default in `api/controllers/hello_world.js`. + +```yaml +paths: + /hello: + # binds swagger app logic to a route + x-swagger-router-controller: hello_world +``` + +By default, controller method names map to the HTTP operation names, like get() or post(). But you can specify any name you wish with the `operationId` element. In the following example, a GET request results in calling the hello() method in the controller. + +```yaml + get: + description: Returns 'Hello' to the caller + # used as the method name of the controller + operationId: hello +``` + +Here is the `hello_world.js` implementation for the quick start example. It retrieves the query parameter value and returns a response. + +```javascript + var util = require('util'); + + module.exports = { + hello: hello + }; + + function hello(req, res) { + var name = req.swagger.params.name.value || 'stranger'; + var hello = util.format('Hello, %s!', name); + res.json({ "message": hello }); + } +``` + +### Using query parameters + +In the controller code, we obtained the value of a query parameter and echoed it back in the response. We used the `req.swagger` object to obtain access to the query parameters. You declare query parameters in the paths section of the project's Swagger definition. For example: + +```yaml + parameters: + - name: name + in: query + description: The name of the person to whom to say hello + required: false + type: string +``` + +The req.swagger object is populated by the swagger-tools middleware component of swagger. To read more about this object, see the [Swagger tools middleware documentation](https://github.com/apigee-127/swagger-tools/blob/master/docs/Middleware.md). + +### Weather API example + +Let's look at an example controller for a simple weather API. + +The Weather API requires a controller function that takes in request and response objects, queries the Open Weather Map API using the `city` query parameter and returns the current weather conditions. + +Note that Open Weather returns a JSON object. Also, we'll need to export the controller function so that it is available to the outside world. + +We will use the `request` library to make the request. So, ensure it is installed and added to `package.json`: + + ``` +npm install request --save + ``` + +>Note: If a controller requires additional Node.js modules, be sure to add them to your `package.json` file and execute `npm install`. + +In the Swagger file, you can see that when a GET is performed on `/weather`, the target controller file is `api/controllers/weather.js`, and the target method to call is `getWeatherByCity()`: + +```yaml + paths: + /weather: + x-swagger-router-controller: weather + get: + description: "Returns current weather in the specified city to the caller" + operationId: getWeatherByCity + parameters: + - name: city + in: query + description: "The city you want weather for in the form city,state,country" + required: true + type: "string" +``` + +Here is the controller implementation for the `getWeatherByCity` function: + +```javascript + 'use strict'; + + var util = require('util'); + var request = require('request'); + + module.exports = { + getWeatherByCity: getWeatherByCity + } + + function getWeatherByCity(req, res) { + var city = req.swagger.params.city.value; + var url = "http://api.openweathermap.org/data/2.5/weather?q="+city+"&units=imperial"; + console.log('Executing request: '+url); + request.get(url).pipe(res); + }; +``` + + +Here is how you call the Weather API, which returns data for a specified city. + + ```bash + curl http://localhost:10010/weather\?city\=San%20Jose,CA + ``` + diff --git a/docs/docco.css b/docs/docco.css deleted file mode 100644 index c36fdb41..00000000 --- a/docs/docco.css +++ /dev/null @@ -1,193 +0,0 @@ -/*--------------------- Layout and Typography ----------------------------*/ -body { - font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; - font-size: 15px; - line-height: 22px; - color: #252519; - margin: 0; padding: 0; -} -a { - color: #261a3b; -} - a:visited { - color: #261a3b; - } -p { - margin: 0 0 15px 0; -} -h1, h2, h3, h4, h5, h6 { - margin: 0px 0 15px 0; -} - h1 { - margin-top: 40px; - } -hr { - border: 0 none; - border-top: 1px solid #e5e5ee; - height: 1px; - margin: 20px 0; -} -#container { - position: relative; -} -#background { - position: fixed; - top: 0; left: 525px; right: 0; bottom: 0; - background: #f5f5ff; - border-left: 1px solid #e5e5ee; - z-index: -1; -} -#jump_to, #jump_page { - background: white; - -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; - -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; - font: 10px Arial; - text-transform: uppercase; - cursor: pointer; - text-align: right; -} -#jump_to, #jump_wrapper { - position: fixed; - right: 0; top: 0; - padding: 5px 10px; -} - #jump_wrapper { - padding: 0; - display: none; - } - #jump_to:hover #jump_wrapper { - display: block; - } - #jump_page { - padding: 5px 0 3px; - margin: 0 0 25px 25px; - } - #jump_page .source { - display: block; - padding: 5px 10px; - text-decoration: none; - border-top: 1px solid #eee; - } - #jump_page .source:hover { - background: #f5f5ff; - } - #jump_page .source:first-child { - } -table td { - border: 0; - outline: 0; -} - td.docs, th.docs { - max-width: 450px; - min-width: 450px; - min-height: 5px; - padding: 10px 25px 1px 50px; - overflow-x: hidden; - vertical-align: top; - text-align: left; - } - .docs pre { - margin: 15px 0 15px; - padding-left: 15px; - } - .docs p tt, .docs p code { - background: #f8f8ff; - border: 1px solid #dedede; - font-size: 12px; - padding: 0 0.2em; - } - .pilwrap { - position: relative; - } - .pilcrow { - font: 12px Arial; - text-decoration: none; - color: #454545; - position: absolute; - top: 3px; left: -20px; - padding: 1px 2px; - opacity: 0; - -webkit-transition: opacity 0.2s linear; - } - td.docs:hover .pilcrow { - opacity: 1; - } - td.code, th.code { - padding: 14px 15px 16px 25px; - width: 100%; - vertical-align: top; - background: #f5f5ff; - border-left: 1px solid #e5e5ee; - white-space: nowrap; - } - pre, tt, code { - font-size: 12px; line-height: 18px; - font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; - margin: 0; padding: 0; - } - - -/*---------------------- Syntax Highlighting -----------------------------*/ -td.linenos { background-color: #f0f0f0; padding-right: 10px; } -span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } -body .hll { background-color: #ffffcc } -body .c { color: #408080; font-style: italic } /* Comment */ -body .err { border: 1px solid #FF0000 } /* Error */ -body .k { color: #954121 } /* Keyword */ -body .o { color: #666666 } /* Operator */ -body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ -body .cp { color: #BC7A00 } /* Comment.Preproc */ -body .c1 { color: #408080; font-style: italic } /* Comment.Single */ -body .cs { color: #408080; font-style: italic } /* Comment.Special */ -body .gd { color: #A00000 } /* Generic.Deleted */ -body .ge { font-style: italic } /* Generic.Emph */ -body .gr { color: #FF0000 } /* Generic.Error */ -body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -body .gi { color: #00A000 } /* Generic.Inserted */ -body .go { color: #808080 } /* Generic.Output */ -body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ -body .gs { font-weight: bold } /* Generic.Strong */ -body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -body .gt { color: #0040D0 } /* Generic.Traceback */ -body .kc { color: #954121 } /* Keyword.Constant */ -body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */ -body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */ -body .kp { color: #954121 } /* Keyword.Pseudo */ -body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ -body .kt { color: #B00040 } /* Keyword.Type */ -body .m { color: #666666 } /* Literal.Number */ -body .s { color: #219161 } /* Literal.String */ -body .na { color: #7D9029 } /* Name.Attribute */ -body .nb { color: #954121 } /* Name.Builtin */ -body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ -body .no { color: #880000 } /* Name.Constant */ -body .nd { color: #AA22FF } /* Name.Decorator */ -body .ni { color: #999999; font-weight: bold } /* Name.Entity */ -body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ -body .nf { color: #0000FF } /* Name.Function */ -body .nl { color: #A0A000 } /* Name.Label */ -body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ -body .nt { color: #954121; font-weight: bold } /* Name.Tag */ -body .nv { color: #19469D } /* Name.Variable */ -body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ -body .w { color: #bbbbbb } /* Text.Whitespace */ -body .mf { color: #666666 } /* Literal.Number.Float */ -body .mh { color: #666666 } /* Literal.Number.Hex */ -body .mi { color: #666666 } /* Literal.Number.Integer */ -body .mo { color: #666666 } /* Literal.Number.Oct */ -body .sb { color: #219161 } /* Literal.String.Backtick */ -body .sc { color: #219161 } /* Literal.String.Char */ -body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ -body .s2 { color: #219161 } /* Literal.String.Double */ -body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ -body .sh { color: #219161 } /* Literal.String.Heredoc */ -body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ -body .sx { color: #954121 } /* Literal.String.Other */ -body .sr { color: #BB6688 } /* Literal.String.Regex */ -body .s1 { color: #219161 } /* Literal.String.Single */ -body .ss { color: #19469D } /* Literal.String.Symbol */ -body .bp { color: #954121 } /* Name.Builtin.Pseudo */ -body .vc { color: #19469D } /* Name.Variable.Class */ -body .vg { color: #19469D } /* Name.Variable.Global */ -body .vi { color: #19469D } /* Name.Variable.Instance */ -body .il { color: #666666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/images/overview.png b/docs/images/overview.png new file mode 100644 index 00000000..e9ba37b5 Binary files /dev/null and b/docs/images/overview.png differ diff --git a/docs/images/overview2.png b/docs/images/overview2.png new file mode 100644 index 00000000..cc238649 Binary files /dev/null and b/docs/images/overview2.png differ diff --git a/docs/images/project-call.png b/docs/images/project-call.png new file mode 100644 index 00000000..6f997982 Binary files /dev/null and b/docs/images/project-call.png differ diff --git a/docs/images/project-controller.png b/docs/images/project-controller.png new file mode 100644 index 00000000..9a815797 Binary files /dev/null and b/docs/images/project-controller.png differ diff --git a/docs/images/project-create.png b/docs/images/project-create.png new file mode 100644 index 00000000..cabc0a1b Binary files /dev/null and b/docs/images/project-create.png differ diff --git a/docs/images/project-editor.png b/docs/images/project-editor.png new file mode 100644 index 00000000..bac439a2 Binary files /dev/null and b/docs/images/project-editor.png differ diff --git a/docs/images/project-hello.png b/docs/images/project-hello.png new file mode 100644 index 00000000..fd56fc5d Binary files /dev/null and b/docs/images/project-hello.png differ diff --git a/docs/images/project-server.png b/docs/images/project-server.png new file mode 100644 index 00000000..ce0def85 Binary files /dev/null and b/docs/images/project-server.png differ diff --git a/docs/images/project-start-editor.png b/docs/images/project-start-editor.png new file mode 100644 index 00000000..f5518cf1 Binary files /dev/null and b/docs/images/project-start-editor.png differ diff --git a/docs/images/project-start.png b/docs/images/project-start.png new file mode 100644 index 00000000..54d5f722 Binary files /dev/null and b/docs/images/project-start.png differ diff --git a/docs/images/swagger-editor.png b/docs/images/swagger-editor.png new file mode 100644 index 00000000..71ba5757 Binary files /dev/null and b/docs/images/swagger-editor.png differ diff --git a/docs/images/swagger-icon.png b/docs/images/swagger-icon.png new file mode 100644 index 00000000..ce2150ce Binary files /dev/null and b/docs/images/swagger-icon.png differ diff --git a/docs/images/swagger-install.png b/docs/images/swagger-install.png new file mode 100644 index 00000000..3388c38e Binary files /dev/null and b/docs/images/swagger-install.png differ diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 00000000..7d20a4c7 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,77 @@ + +## Installing swagger + +* [Prerequisites](#prereqs) +* [Installing with npm](#install) +* [Using npm without sudo](#nosudo) +* [Configuring the default browser in Linux](#defaultbrowser) + +### Prerequisites + +* [Node.js](http://nodejs.org/download/) (v0.10.24+) +* [npm](https://docs.npmjs.com/getting-started/installing-node) (v1.3.0+) + +### Installing with npm + +The `swagger` module is designed for Node.js and is available through npm. + +#### Installing on Linux / Mac + +Here's how you install with `sudo`. If you do not wish to use `sudo`, see [Using npm without sudo](#nosudo) below. + +1. Open a terminal. +2. Run the install: + + `sudo npm install -g swagger` + +**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/bin` directory to your PATH manually. On Unix-based machines +the bin directory will often be found here: `/usr/local/lib/node_modules/swagger/bin`. + +#### Installing on Windows + +1. Open a terminal. +2. Run the install: + + `npm install -g swagger` + +## Using npm without sudo + +If you don't want to use sudo to install swagger on your system, follow the instructions in this section. + +#### Overview + +By default npm will place 'global' modules installed with the `-g` flag in `/usr/local/lib/node_modules` using the default prefix of `/usr/local`. Global executables would be placed in `/usr/local/bin` using the same default prefix, thereby putting them on the default PATH in most cases. In order to write to both of these directories root permissions are required. + +Many Node.js developers choose to use a different prefix such that they do not need to use root permissions to install modules using the `-g` flag (rightfully so - you should always be wary about things that 'require root permissions'!). Using root permissions is effectively a shortcut. In order to use executables installed using a different prefix you need to add an element to your path. + +#### Steps + +1. Set the 'prefix' for npm by using the following command (documented here: [npm-config](https://www.npmjs.org/doc/misc/npm-config.html). This will create a file `~/.npmrc` that contains configuration information for npm. + +```bash + npm set prefix ~/npm +``` + +2. Edit your `.bash_profile` or the appropriate shell initialization script to add `~/npm` to your `PATH` by adding the following line (or placing the single line in the new file if it does not exist): + + ```bash + PATH=~/npm/bin:$PATH + ``` + + This will enable you to easily use executable scripts installed using `-g` through npm - both for swagger and for other tools as well! + +###Configuring the default browser on Linux + +On Linux platforms, you need to specify your browser path before using the Swagger editor. + +1. Create or open the following file in a text editor: + + `~/.a127/config.js` + +2. Add the following contents to the file: + + ```javascript + module.exports = { + browser: 'the/path/to/your/browser' + }; + ``` diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 00000000..8532b295 --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,46 @@ +## What is the swagger module? +The swagger module provides tools for designing and building APIs entirely in Node.js. It integrates with popular Node.js servers, including Express, hapi, restify, and Sails, as well as any Connect-based middleware. With swagger, you can specify, build, and test your API from the very beginning, and it allows you to make changes to your design without rewriting the logic of your implementation. It explicitly isolates the design of your interfaces from writing your controller code, leading to a much better software development lifecycle. + +* [The API-First Development Process](#sdlc) +* [Reporting issues](#gethelp) + +### The API-First Development Process +API design is a discovery process. Swagger development begins with design tooling, and it expects that you will evolve your interface over time. It gracefully handles the routing of interfaces to controllers so that you can make changes to your interfaces without clobbering any of your implementation logic. + +Designing an API specification is like writing a contract. As you write the spec in YAML using [Swagger 2.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md), your API documentation is generated in real-time, allowing the client and server teams to agree on how their systems will work together. + +Once you have defined your first operation, it drives a mock server, which enables client development happen in parallel with server development. As you build the client, you will discover further changes you'll need to make to your APIs, meaning another iteration of the specification. + +* Defining your API specification using the Swagger Editor (included with swagger). + +*The Swagger Editor* +![alt text](./images/swagger-editor.png) + +Write your specification in YAML on the left, and see the API documentation in real-time on the right. Auto-completion makes it easy to learn the syntax, and validation helps you correct any syntactic or semantic errors you might make along the way. + +* Use the `x-swagger-router-controller` extension to annotating your paths and operations. This maps the interface onto the name of the controller file that implements the logic behind the operation. For example: + +```yaml +paths: + /hello: + x-swagger-router-controller: "hello_world" +``` + +* Use the operationId property to specify which controller method to call for the given path: + +```yaml + get: + description: "Returns 'Hello' to the caller" + operationId: "hello" +``` + +* Implement your controller files in Node.js and place them in `/api/controllers`. For example: `/api/controllers/hello_world.js` + +* Behind the scenes, swagger wires up your app, routing HTTP requests to specific Node.js controller files. + +* At runtime swagger-tools middleware validates the request before sending it to the `hello` operation of the `hello_world` controller. + +* Finally, the controller logic associated with the requested path is executed. + +### Reporting issues +Have an issue to report? See the [Reporting issues](./report-issues.md). diff --git a/docs/main.html b/docs/main.html deleted file mode 100644 index 4465df04..00000000 --- a/docs/main.html +++ /dev/null @@ -1,53 +0,0 @@ - main.js

    main.js

    Swagger Sample Application

    - -

    This is a sample application which uses the swagger-node-express -module. The application is organized in the following manner:

    - -

    petResources.js

    - -

    All API methods for this petstore implementation live in this file and are added to the swagger middleware.

    - -

    models.js

    - -

    This contains all model definitions which are sent & received from the API methods.

    - -

    petData.js

    - -

    This is the sample implementation which deals with data for this application

    Include express and swagger in the application.

    var express = require("express")
    - , url = require("url")
    - , swagger = require("../../lib/swagger.js");
    -
    -var petResources = require("./petResources.js");
    -
    -var app = express();
    -app.use(express.bodyParser());

    Set the main handler in swagger to the express app

    swagger.setAppHandler(app);

    This is a sample validator. It simply says that for all POST -methods, the header api_key OR query param api_key must be equal -to the string literal special-key. All other HTTP ops are A-OK

    swagger.addValidator(
    -  function validate(req, path, httpMethod) {

    example, only allow POST for api_key="special-key"

        if ("POST" == httpMethod) {
    -      var apiKey = req.headers["api_key"];
    -      if (!apiKey) {
    -        apiKey = url.parse(req.url,true).query["api_key"]; }
    -      if ("special-key" == apiKey) {
    -        return true; 
    -      }
    -      return false;
    -    }
    -    return true;
    -  }
    -);

    Add models and methods to swagger

    swagger.addModels(petResources.models)
    -  .addGet(petResources.findByTags)
    -  .addGet(petResources.findByStatus)
    -  .addGet(petResources.findById)
    -  .addPost(petResources.addPet)
    -  .addPut(petResources.updatePet)
    -  .addDelete(petResources.deletePet);

    Configures the app's base path and api version.

    swagger.configure("http://localhost:8002", "0.1");

    Serve up swagger ui at /docs via static route

    var docs_handler = express.static(__dirname + '/../../swagger-ui-1.1.1/');
    -app.get(/^\/docs(\/.*)?$/, function(req, res, next) {
    -  if (req.url === '/docs') { // express static barfs on root url w/o trailing slash
    -    res.writeHead(302, { 'Location' : req.url + '/' });
    -    res.end();
    -    return;
    -  }

    take off leading /docs so that connect locates file correctly

      req.url = req.url.substr('/docs'.length);
    -  return docs_handler(req, res, next);
    -});

    Start the server on port 8002

    app.listen(8002);
    -
    -
    diff --git a/docs/mock-mode.md b/docs/mock-mode.md new file mode 100644 index 00000000..f9066fd1 --- /dev/null +++ b/docs/mock-mode.md @@ -0,0 +1,349 @@ +## Running in mock mode + +Mock mode lets you "mock up" API routes/paths and response objects in the Swagger editor and test them without writing any controller code. By default, mock mode responses are system-generated; however, you can optionally implement custom mock controllers to return custom responses. + +* [When to use mock mode](#whentouse) +* [Starting a project in mock mode](#starting) +* [Quick example: mock mode in action](#quickexample) +* [Building and testing an API in mock mode](#buildtest) +* [Implementing mock controllers](#mockcontrollers) +* [Wiring up and implementing the API controller](#wireup) + +### When to use mock mode + +Mock mode is useful when you are designing your API model in the Swagger editor, but you're not ready to implement the API's handler/controllers. For example, you might use mock mode when you're trying to decide which API routes you need and what kind of data each API operation should return. Basically, mock mode let's you perfect your API design without writing any Node.js code. + +When you're happy with the overall API design, then you can implement your controller methods. + +### Starting a project in mock mode + +To start an swagger project in mock mode, use the `-m` flag: + +`swagger project start -m` + + +### Quick example: mock mode in action + +Here's a simple example where the API definition only has one path (`/weather`) and a response object called WeatherResponse. In this case, the WeatherResponse object returns a simple message of type string. Here's the Swagger YAML: + + +```yaml +swagger: '2.0' +info: + version: "0.0.1" + title: Mock mode test +host: localhost +basePath: / +schemes: + - http +consumes: + - application/json +produces: + - application/json +paths: + /weather: + x-swagger-router-controller: weather + get: + responses: + "200": + description: Success + schema: + $ref: "#/definitions/WeatherResponse" +definitions: + WeatherResponse: + required: + - message + properties: + message: + type: string +``` + + +To test this API configuration in mock mode, start your swagger project with the mock mode "-m" flag: + +`swagger project start -m` + +When you call the API, like this: + +`curl -i http://localhost:10010/weather` + +Mock mode returns this system-generated response: + + +```json + { + "message": "Sample text" + } +``` + +If you change the response object to return an integer... + +```yaml + WeatherResponse: + required: + - message + properties: + message: + type: integer +``` + +The mock response is an integer: + +```json + { + "message": 1 + } +``` + + +### Building and testing your API model in mock mode + +An actual weather API isn't only going to return a string; it's going to return a more complex response object consisting of objects, strings, and numbers. As you build your API, you can model and test the intended behavior entirely in mock mode. + +For example, here's a WeatherResponse object for a weather API. It's got strings, numbers, arrays, and objects representing various aspects of weather data. + +```yaml + WeatherResponse: + properties: + base: + type: "string" + clouds: + type: "object" + properties: + all: + type: "number" + cod: + type: "number" + coord: + type: "object" + properties: + lat: + type: "number" + lon: + type: "number" + dt: + type: "number" + id: + type: "number" + main: + type: "object" + properties: + humidity: + type: "number" + pressure: + type: "number" + temp_max: + type: "number" + temp_min: + type: "number" + temp: + type: "number" + name: + type: "string" + sys: + type: "object" + properties: + country: + type: "string" + id: + type: "number" + message: + type: "number" + sunrise: + type: "number" + sunset: + type: "number" + type: + type: "number" + weather: + type: "array" + items: + type: "object" + properties: + description: + type: "string" + icon: + type: "string" + id: + type: "number" + main: + type: "string" + wind: + type: "object" + properties: + deg: + type: "number" + speed: + type: "number" +``` + + +If you call this API in mock mode, it returns the following JSON. Objects, arrays, strings, and numbers are all "mocked up" with mock values of the appropriate data type: + +```yaml + { + "base": "Sample text", + "clouds": { + "all": 1 + }, + "cod": 1, + "coord": { + "lat": 1, + "lon": 1 + }, + "dt": 1, + "id": 1, + "main": { + "humidity": 1, + "pressure": 1, + "temp": 1, + "temp_max": 1, + "temp_min": 1 + }, + "name": "Sample text", + "sys": { + "country": "Sample text", + "id": 1, + "message": 1, + "sunrise": 1, + "sunset": 1, + "type": 1 + }, + "weather": [ + { + "description": "Sample text", + "icon": "Sample text", + "id": 1, + "main": "Sample text" + } + ], + "wind": { + "deg": 1, + "speed": 1 + } + } +``` + + +### Implementing mock mode controllers + +By default, mock mode returns programmed responses, like "Sample text" for a string, a number for an integer, and so on. + +But you can also create mock controllers with handler methods that return custom responses. + +Place these custom "mock" controllers in the `/api/mock` directory. + +Here's an example that returns some data whenever the `search()` handler method is called: + + +```javascript + 'use strict'; + + module.exports = { + search: search + }; + + function search(req, res, next) { + + res.json([{ user: 'mock', created: new Date(), text: 'this'}]); + } +``` + +### Wiring up and implementing the API controller + +After you're happy with your API design, you're ready to implement wire up the controller for the `/weather` path. + +You simply specify in the OpenAPI spec the route handler (`x-swagger-router-controller`) file, which method to call in the controller (`operationId`), and any query parameters you wish to pass: + +In weather sample's `swagger.yaml` file, it looks like this: + +```yaml + paths: + /weather: + x-swagger-router-controller: weather + get: + description: "Returns current weather in the specified city to the caller" + operationId: getWeatherByCity + parameters: + - name: city + in: query + description: "The city you want weather for in the form city,state,country" + required: true + type: "string" +``` + +Finally, implement the route's operation -- the `getWeatherByCity()` method in `api/controllers/weather.js` -- which calls the back-end service and returns the response. + +Here is the sample controller implementation for a weather API: + +```javascript +'use strict'; + +var util = require('util'); +var request = require('request'); + +module.exports = { + getWeatherByCity: getWeatherByCity +} + +function getWeatherByCity(req, res) { + var city = req.swagger.params.city.value; + var url = "http://api.openweathermap.org/data/2.5/weather?q="+city+"&units=imperial"; + console.log('Executing request: '+url); + request.get(url).pipe(res); + }; +``` + +In the case of this sample weather API, the controller calls the back-end service (the [OpenWeatherMap](http://openweathermap.org/) API). When you call the API like this: + +`curl http://localhost:10010/weather?city=Boulder,CO` + +The same response object that you previously modeled and tested in mock mode is filled in with the correct values: + + +```json + { + "base": "cmc stations", + "clouds": { + "all": 40 + }, + "cod": 200, + "coord": { + "lat": 40.02, + "lon": -105.28 + }, + "dt": 1411077635, + "id": 5574991, + "main": { + "humidity": 27, + "pressure": 1016, + "temp": 87.62, + "temp_max": 91.99, + "temp_min": 80.01 + }, + "name": "", + "sys": { + "country": "United States of America", + "id": 538, + "message": 0.0175, + "sunrise": 1411044334, + "sunset": 1411088663, + "type": 1 + }, + "weather": [ + { + "description": "scattered clouds", + "icon": "03d", + "id": 802, + "main": "Clouds" + } + ], + "wind": { + "deg": 160, + "speed": 7.78 + } + } +``` + + + diff --git a/docs/modules.md b/docs/modules.md new file mode 100644 index 00000000..0093ae81 --- /dev/null +++ b/docs/modules.md @@ -0,0 +1,69 @@ +## swagger modules and dependencies + +This topic briefly describes the relevant Node.js modules on which a swagger project depends. + +* [swagger](#swagger) +* [skeleton](#skeleton) +* [swagger-editor](#swagger-editor) +* [swagger-tools](#swagger-tools) + +### swagger + +The `swagger` npm module provides everything you need to create new projects, including the Swagger editor, Swagger Tools middleware, sample project skeletons, and the `swagger` command-line tools. + +#### Installation +For installation instructions, see "[Installation](./install.md)". + +#### Documentation + +The main source of documentation for the swagger module and related components is in the swagger-node repository on GitHub. + +#### Installation + +The swagger command-line tools are installed with swagger. + +#### Documentation + +[swagger command-line reference](./cli.md) + + +### skeleton + +A basic, "hello world" swagger project. This project automatically cloned when you create a new swagger project by executing `swagger project create`. Skeleton projects are implemented for specific API frameworks, such as express, restify, or others. + +#### Documentation + +See the swagger "[Quick start](./quick-start.md)" to see how easy it is to get a new swagger API project up and running. + +### swagger-editor + +The Swagger Editor lets you design your API specification and interactively preview its documentation for your swagger API project. + +#### Installation + +Standard npm install. Installed with swagger. + +#### Documentation + +See "[Swagger Editor](https://github.com/swagger-api/swagger-editor)" on GitHub. + +### swagger-tools + +Middleware for Node.js including Message Validation, Authorization and Routing. + +#### Installation + +Standard npm install. Installed with swagger. + +#### Documentation + +See the `swagger-tools` [README](https://github.com/apigee-127/swagger-tools) on GitHub. + + +#### Swagger Tools middleware components + +Swagger tools provides these middleware components. They provide services for message validation, authorization, and routing. + +* swagger-metadata +* swagger-router +* swagger-validator diff --git a/docs/paramTypes.html b/docs/paramTypes.html deleted file mode 100644 index d37cc811..00000000 --- a/docs/paramTypes.html +++ /dev/null @@ -1 +0,0 @@ - paramTypes.js

    paramTypes.js

    \ No newline at end of file diff --git a/docs/quick-start.md b/docs/quick-start.md new file mode 100644 index 00000000..162590e3 --- /dev/null +++ b/docs/quick-start.md @@ -0,0 +1,80 @@ +## Quick start + +Let's see how easily and quickly you can get a simple API up and running using swagger. + +* [Get an API up and running](#upandrunning) +* [Check out the main Node.js app file](#main) +* [Open the Swagger editor](#openeditor) +* [Windows users](#windows) + +### Get an API up and running + +First, we create a new swagger project and test a simple "hello world" API. + +1. Install swagger, as described in the [installation guide](install.md). + +2. Create swagger project directory and cd to it. This is where you'll create your first project. + +3. Execute the project create command: + + `swagger project create hello-world` + +4. Pick the API framework you want to use. We're going to pick express, but you can pick any of the listed frameworks: + ``` + ? Framework? (Use arrow keys) + connect + ❯ express + hapi + restify + sails + ``` + Note: You must enter the number of the option. + +5. swagger creates a skeleton project that's pre-configured to use your selected framework (in this example, Express). It then runs `npm install` to pick up the dependencies. + + Note: Windows users see the [note below](#windows-note) regarding npm. + +6. Change to the new project directory: `cd hello-world` + +7. Type `swagger project start` to start your API. You now have an API running with swagger! + +8. In another terminal, run this command: + + `curl http://127.0.0.1:10010/hello?name=Scott` + + And, you'll get back the response `{ "message": "Hello, Scott!" }`. + +That's it - You have now created, started and tested your first API project with swagger! + +### Check out the main Node.js app file + +Open /app.js in an editor. This is the main Node.js app that installs middleware and requires the API framework that you chose when you created your project. + +The middleware modules perform tasks like OpenAPI Specification validation and endpoint routing. For more information, see [swagger modules and dependencies](./modules.md). + +### Open the Swagger editor + +The Swagger editor lets you design and test your API interactively. While you design and test, the API documentation is generated automatically for you. + +Now that we've got our basic API running, let's open the Swagger editor. + +1. Be sure you're in your project root directory: `./hello-world`. + +2. Fire up the editor: `swagger project edit` + +*The Swagger editor* +![alt text](./images/swagger-editor.png) + +### Running Swagger Node in a Deployment + +When you run a swagger-node based application in a proper deployment, you should use `node ` (typically, `node app.js`) as opposed to using the swagger cli tooling. + +### Windows users +For some versions of npm on Windows will have problems on the `npm install` step of `swagger project create`. They are related to a `debug` module on npm not being managed properly. The following steps should resolve this issue: + +1. In the project directory, execute the following commands: + 1. `npm install yamljs` + 2. `npm install debug` + 3. `npm install swagger-tools` + +Now, when you run `swagger project start` your project should start successfully. diff --git a/docs/release-notes.md b/docs/release-notes.md new file mode 100644 index 00000000..717208be --- /dev/null +++ b/docs/release-notes.md @@ -0,0 +1,34 @@ +## Release Notes + +### swagger-node 0.7.0, swagger-node-runner 0.5.0 + +#### New features + + * Request handing pipeline is now fully configurable + * Application configuration is now driven by the [config module](https://github.com/lorenwest/node-config/wiki/Configuration-Files) to allow a ton of flexibility in setting up configurations and routes based on environment. + * Supports plugins such as [volos-swagger-oauth](https://www.npmjs.com/package/volos-swagger-oauth) and [volos-swagger-apply](https://www.npmjs.com/package/volos-swagger-apply) + * Custom security handlers can be declared in config in app.js. Example: + + ```javascript + config.swaggerSecurityHandlers = { + oauth2: function securityHandler1(req, authOrSecDef, scopesOrApiKey, cb) { + // your security code + cb(); + } + }; + ``` + +#### Bug Fixes + + * json_error_handler should work in all container environments (mapErrorsToJson did not) + +#### Breaking Changes + + * `mapErrorsToJson` config option is now configured as an onError handler: `onError: json_error_handler` + * `docEndpoints` raw config option is now declared in Swagger and handled via a pipe: `swagger_raw` + +#### Converting From Previous Version + + 1. Update your package.json to use the new middleware versions: "^0.1.0". (eg. `"swagger-express-mw": "^0.1.0"`) + 2. Update your application dependencies: `npm update`. + 3. Existing config should generally work, but you should update your config to the [new format](https://github.com/swagger-api/swagger-node/blob/master/docs/cli.md/configuration.md). diff --git a/docs/report-issues.md b/docs/report-issues.md new file mode 100644 index 00000000..e7782b72 --- /dev/null +++ b/docs/report-issues.md @@ -0,0 +1,14 @@ + + +## Reporting issues with swagger + +swagger is an open source project and we use GitHub issues for tracking problems with the code: + +### Swagger Editor +**Issue Tracker:** https://github.com/wordnik/swagger-editor/issues + +### Swagger Tools +**Issue Tracker:** https://github.com/apigee-127/swagger-tools/issues + +### Swagger Command Line +**Issue Tracker:** https://github.com/swagger-api/swagger-node/issues diff --git a/docs/swagger-about.md b/docs/swagger-about.md new file mode 100644 index 00000000..e8f46400 --- /dev/null +++ b/docs/swagger-about.md @@ -0,0 +1,47 @@ +## About swagger + +* [What is Swagger?](#whatisswagger) +* [How does the swagger module use Swagger?](#howdoes) +* [Help me with YAML](#helpwith) +* [Next step](#nextstep) + + +### What is Swagger? + +[Swagger™ ](http://swagger.io) is a specification and framework implementation for describing, producing, consuming, and visualizing RESTful web services. + +To read more about Swagger, refer to: + +* [The Swagger website](http://swagger.io) +* [Swagger on GitHub](https://github.com/swagger-api) + + +### How does the swagger module use Swagger? + +The Swagger Editor lets you design your API specification and preview its documentation for your swagger API. The editor is installed with swagger. + +A swagger.yaml file is installed into every new swagger project, and lives in `/api/swagger/swagger.yaml`. It conforms to the [OpenAPI 2.0 Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md). + +Behind the scenes, Swagger middleware validates and processes the Swagger configuration file, and routes API operation endpoints to controller files. All **you** need to do is implement your custom API controller logic. + +>Note: The editor is served locally and automatically saves your work *as you edit*. In addition, if the project is running (`swagger project start`), it is automatically restarted each time the editor saves. Just be careful if you kill the editor, that you do not lose unsaved work. + +**Try it:** + +1. `swagger project create test-project` +2. `cd test-project` +2. `swagger project edit` + +*The Swagger editor* +![alt text](./images/swagger-editor.png) + + +### Help me with YAML + +YAML is a data serialization/representation standard. If you're new to YAML, check out [www.yaml.org](http://www.yaml.org). Another excellent introduction is the [Wikipedia YAML entry](http://en.wikipedia.org/wiki/YAML). + +YAML is intended to be easy for humans to read. Every swagger project includes a Swagger 2.0 compliant configuration file that is written in YAML. + +### Next step + +For a more detailed look the Swagger configurations, see "[The OpenAPI Specification file](./swagger-file.md)". diff --git a/docs/swagger-config.png b/docs/swagger-config.png deleted file mode 100644 index f42b56e4..00000000 Binary files a/docs/swagger-config.png and /dev/null differ diff --git a/docs/swagger-file.md b/docs/swagger-file.md new file mode 100644 index 00000000..2f12da62 --- /dev/null +++ b/docs/swagger-file.md @@ -0,0 +1,84 @@ +## Understanding the OpenAPI Specification file + +* [Intro](#intro) +* [Default swagger project file](#default) +* [OpenAPI Specification elements](#reference) + +### Intro + +When you execute `swagger project create myproject`, a default Swagger model is placed in `myproject/api/swagger/swagger.yaml`. This model conforms to the [OpenAPI 2.0 Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md). + +>Note: For a quick intro to swagger, see "[What is Swagger](./swagger-about.md)". + +### Default swagger project file + +Here is the entire `swagger.yaml` file that is provisioned for a new swagger project: + +```yaml + 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 swagger 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 +``` + + +### OpenAPI Specification elements + +The Swagger file includes a number of standard OpenAPI 2.0 Specification elements. You can read about them in the [OpenAPI 2.0 Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md). + +Here's a brief description of the elements in a swagger project file: + +* **swagger: 2.0** - (Required) Identifies the version of the OpenAPI Specification (2.0). + +* **info:** - (Required) Provides metadata about the API. + +* **host:** - (Optional) The host serving the API. By default, a new project connects to a server running locally on port 10010. + +* **basePath:** - (Optional) The base path on which the API is served, which is relative to the host. + +* **schemes:** - (Optional) A list of transfer protocol(s) of the API. + +* **consumes:** - (Optional) A list of MIME types the APIs can consume. + +* **produces:** - (Optional) A list of MIME types the APIs can produce. + +* **paths:** - (Required) Defines the available operations on the API. You'll spend most of your time configuring the paths part of the file. You can read about the path element in the [OpenAPI 2.0 Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md). In general, the paths section specifies an operation's verb (like `get`), the endpoint for an API operation (like `/hello`), query parameters, and responses. + +* **definitions:** - (Optional) These represent the structure of complex objects such as request and response bodies. For example, you might have a collection of `/users` that returns an array of `user` objects. You would describe these with two definitions: 1) to describe the `User` object, and 2) the definition of the `Users` array. Swagger uses [JSON-schema](http://json-schema.org/). + +* **x-swagger-router-controller:** - (Optional) This extension specifies the name of the controller file (hello_world.js) that will execute when this API operation is called. Controller files reside in `apis/controllers` in your swagger project. This extension is provided through the [`swagger-tools`](https://github.com/apigee-127/swagger-tools) middleware module. + diff --git a/docs/swagger.html b/docs/swagger.html deleted file mode 100644 index 244d2f23..00000000 --- a/docs/swagger.html +++ /dev/null @@ -1,388 +0,0 @@ - swagger.js

    swagger.js

    var resourcePath = "/resources.json";
    -var basePath = "/";
    -var swaggerVersion = "1.1";
    -var apiVersion = "0.0";
    -var resources = {};
    -var validators = [];
    -var appHandler = null;
    -var allowedMethods = ['get', 'post', 'put', 'delete'];
    -var allowedDataTypes = ['string', 'int', 'long', 'double', 'boolean', 'date', 'array'];
    -var params = require(__dirname + '/paramTypes.js');
    -var allModels = {};

    Configuring swagger will set the basepath and api version for all -subdocuments. It should only be done once, and during bootstrap of the app

    function configure(bp, av) {
    -  basePath = bp;
    -  apiVersion = av;
    -  setResourceListingPaths(appHandler);
    -  appHandler.get(resourcePath, resourceListing);

    update resources if already configured

      for(key in resources) {
    -    var r = resources[key];
    -    r.apiVersion = av;
    -    r.basePath = bp;
    -  }
    -}

    Convenience to set default headers in each response.

    function setHeaders(res) {
    -  res.header('Access-Control-Allow-Origin', "*");
    -  res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
    -  res.header("Access-Control-Allow-Headers", "Content-Type, api_key");
    -  res.header("Content-Type", "application/json; charset=utf-8");
    -}

    creates declarations for each resource path.

    function setResourceListingPaths(app) {
    -  for (var key in resources) {
    -    app.get("/" + key.replace("\.\{format\}", ".json"), function(req, res) {
    -      var r = resources[req.url.substr(1).split('?')[0].replace('.json', '.{format}')];
    -      if (!r)
    -        return stopWithError(res, {'description': 'internal error', 'code': 500});
    -      else {
    -        setHeaders(res);
    -        var key = req.url.substr(1).replace('.json', '.{format}').split('?')[0];
    -        var data = filterApiListing(req, res, resources[key]);
    -        data.basePath = basePath;
    -        if (data.code) {
    -          res.send(data, data.code); }
    -        else {
    -          res.send(JSON.stringify(filterApiListing(req, res, r)));
    -        }
    -      }
    -    });
    -  }
    -}

    Applies a filter to an api listing. When done, the api listing will only contain -methods and models that the user actually has access to.

    function filterApiListing(req, res, r) {
    -  var route = req.route;
    -  var excludedPaths = [];
    -  
    -  if (!r || !r.apis) {
    -    return stopWithError(res, {'description': 'internal error', 'code': 500});
    -  }
    -
    -  for (var key in r.apis) {
    -    var api = r.apis[key];
    -    for (var opKey in api.operations) {
    -      var op = api.operations[opKey];
    -      var path = api.path.replace(/{.*\}/, "*");
    -      if (!canAccessResource(req, route + path, op.httpMethod)) {
    -        excludedPaths.push(op.httpMethod + ":" + api.path); }
    -    }
    -  }

    clone attributes in the resource

      var output = shallowClone(r);
    -  

    models required in the api listing

      var requiredModels = [];
    -  

    clone methods that user can access

      output.apis = [];
    -  var apis = JSON.parse(JSON.stringify(r.apis));
    -  for (var i in apis) {
    -    var api = apis[i];
    -    var clonedApi = shallowClone(api);
    -
    -    clonedApi.operations = [];
    -    var shouldAdd = true;
    -    for (var o in api.operations) {
    -      var operation = api.operations[o];
    -      if (excludedPaths.indexOf(operation.httpMethod + ":" + api.path) >= 0) {
    -        break;
    -      }
    -      else {
    -        clonedApi.operations.push(JSON.parse(JSON.stringify(operation)));
    -        addModelsFromResponse(operation, requiredModels);
    -      }
    -    }

    only add cloned api if there are operations

        if (clonedApi.operations.length > 0) {
    -      output.apis.push(clonedApi);
    -    }
    -  }

    add required models to output

      output.models = {};
    -  for (var i in requiredModels){
    -    var modelName = requiredModels[i];
    -    var model = allModels.models[modelName];
    -    if(model){
    -      output.models[requiredModels[i]] = model;
    -    }
    -  }

    look in object graph

      for (key in output.models) {
    -    var model = output.models[key];
    -    if (model && model.properties) {
    -      for (var key in model.properties) {
    -        var t = model.properties[key].type;
    -
    -        switch (t){
    -        case "Array":
    -          if (model.properties[key].items) {
    -            var ref = model.properties[key].items.$ref;
    -            if (ref && requiredModels.indexOf(ref) < 0) {
    -              requiredModels.push(ref);
    -            }
    -          }
    -          break;
    -        case "string":
    -        case "long":
    -          break;
    -        default:
    -          if (requiredModels.indexOf(t) < 0) {
    -            requiredModels.push(t);
    -          }
    -          break;
    -        }
    -      }
    -    }
    -  }
    -  for (var i in requiredModels){
    -    var modelName = requiredModels[i];
    -    if(!output[modelName]) {
    -      var model = allModels.models[modelName];
    -      if(model){
    -        output.models[requiredModels[i]] = model;
    -      }
    -    }
    -  }
    -  return output;
    -}

    Add model to list and parse List[model] elements

    function addModelsFromResponse(operation, models){
    -  var responseModel = operation.responseClass;
    -  if (responseModel) {
    -    responseModel = responseModel.replace(/^List\[/,"").replace(/\]/,"");
    -    if (models.indexOf(responseModel) < 0) {
    -      models.push(responseModel); 
    -    }
    -  }
    -}

    clone anything but objects to avoid shared references

    function shallowClone(obj) {
    -  var cloned = {};
    -  for (var i in obj) {
    -    if (typeof (obj[i]) != "object") {
    -      cloned[i] = obj[i];
    -    }
    -  }
    -  return cloned;
    -}

    function for filtering a resource. override this with your own implementation. -if consumer can access the resource, method returns true.

    function canAccessResource(req, path, httpMethod) {
    -  for (var i in validators) {
    -    if (!validators[i](req,path,httpMethod))
    -      return false;
    -  }
    -  return true;
    -}
    -
    -/**
    - * returns the json representation of a resource
    - * 
    - * @param request
    - * @param response
    - */
    -function resourceListing(req, res) {
    -  var r = {
    -    "apiVersion" : apiVersion, 
    -    "swaggerVersion" : swaggerVersion, 
    -    "basePath" : basePath, 
    -    "apis" : []
    -  };
    -
    -  for (var key in resources)
    -    r.apis.push({"path": "/" + key, "description": "none"}); 
    -
    -  setHeaders(res);
    -  res.write(JSON.stringify(r));
    -  res.end();
    -}

    Adds a method to the api along with a spec. If the spec fails to validate, it won't be added

    function addMethod(app, callback, spec) {
    -  var rootPath = spec.path.split("/")[1];
    -  var root = resources[rootPath];
    -  
    -  if (root && root.apis) {
    -    for (var key in root.apis) {
    -      var api = root.apis[key];
    -      if (api && api.path == spec.path && api.method == spec.method) {

    Add & return

            appendToApi(root, api, spec);
    -        return;
    -      }
    -    }
    -  }
    -
    -  var api = {"path" : spec.path};
    -  if (!resources[rootPath]) {
    -    if (!root) {
    -      var resourcePath = "/" + rootPath.replace("\.\{format\}", ""); 
    -      root = {
    -        "apiVersion" : apiVersion, "swaggerVersion": swaggerVersion, "basePath": basePath, "resourcePath": resourcePath, "apis": [], "models" : []
    -      };
    -    }
    -    resources[rootPath] = root;
    -  }
    -
    -  root.apis.push(api);
    -  appendToApi(root, api, spec);

    TODO: only supports json - convert .{format} to .json, make path params happy

      var fullPath = spec.path.replace("\.\{format\}", ".json").replace(/\/{/g, "/:").replace(/\}/g,"");
    -  var currentMethod = spec.method.toLowerCase();
    -  if (allowedMethods.indexOf(currentMethod)>-1) {
    -    app[currentMethod](fullPath, function(req,res) {
    -      setHeaders(res);
    -      if (!canAccessResource(req, req.url.substr(1).split('?')[0].replace('.json', '.*'), req.method)) {
    -        res.send(JSON.stringify({"description":"forbidden", "code":403}), 403);
    -      } else {    
    -        try {
    -          callback(req,res); 
    -        }
    -        catch (ex) {
    -          if (ex.code && ex.description)
    -            res.send(JSON.stringify(ex), ex.code); 
    -          else {
    -            console.error(spec.method + " failed for path '" + require('url').parse(req.url).href + "': " + ex);
    -            res.send(JSON.stringify({"description":"unknown error","code":500}), 500);
    -          }
    -        }
    -      }
    -    }); 
    -  } else {
    -    console.log('unable to add ' + currentMethod.toUpperCase() + ' handler');  
    -    return;
    -  }
    -}

    Set expressjs app handler

    function setAppHandler(app) {
    -  appHandler = app;
    -}

    Add swagger handlers to express

    function addHandlers(type, handlers) {
    -  for (var i = 0; i < handlers.length; i++) {
    -    var handler = handlers[i];
    -    handler.spec.method = type;
    -    addMethod(appHandler, handler.action, handler.spec);
    -  }
    -}

    Discover swagger handler from resource

    function discover(resource) {
    -  for (var key in resource) {
    -    if (resource[key].spec && resource[key].spec.method && allowedMethods.indexOf(resource[key].spec.method.toLowerCase())>-1) {
    -      addMethod(appHandler, resource[key].action, resource[key].spec); 
    -    } 
    -    else
    -      console.log('auto discover failed for: ' + key); 
    -  }
    -}

    Discover swagger handler from resource file path

    function discoverFile(file) {
    -  return discover(require(file));
    -}

    adds get handler

    function addGet() {
    -  addHandlers('GET', arguments);
    -  return this;
    -}

    adds post handler

    function addPost() {
    -  addHandlers('POST', arguments);
    -  return this;
    -}

    adds delete handler

    function addDelete() { 
    -  addHandlers('DELETE', arguments);
    -  return this;
    -}

    adds put handler

    function addPut() {
    -  addHandlers('PUT', arguments);
    -  return this;
    -}

    adds models to swagger

    function addModels(models) {
    -  allModels = models;
    -  return this;
    -}
    -
    -function wrap(callback, req, resp){
    -  callback(req,resp);
    -}
    -
    -function appendToApi(rootResource, api, spec) {
    -  if (!api.description) {
    -    api.description = spec.description; 
    -  }
    -  var validationErrors = [];
    -
    -  if(!spec.nickname || spec.nickname.indexOf(" ")>=0){

    nicknames don't allow spaces

        validationErrors.push({"path": api.path, "error": "invalid nickname '" + spec.nickname + "'"});
    -  } 

    validate params

      for ( var paramKey in spec.params) {
    -    var param = spec.params[paramKey];
    -    if(param.allowableValues) {
    -      var avs = param.allowableValues.toString();
    -      var type = avs.split('[')[0];
    -      if(type == 'LIST'){
    -        var values = avs.match(/\[(.*)\]/g).toString().replace('\[','').replace('\]', '').split(',');
    -        param.allowableValues = {valueType: type, values: values};
    -      }
    -      else if (type == 'RANGE') {
    -        var values = avs.match(/\[(.*)\]/g).toString().replace('\[','').replace('\]', '').split(',');
    -        param.allowableValues = {valueType: type, min: values[0], max: values[1]};
    -      }
    -    }
    -    
    -    switch (param.paramType) {
    -      case "path":
    -        if (api.path.indexOf("{" + param.name + "}") < 0) {
    -          validationErrors.push({"path": api.path, "name": param.name, "error": "invalid path"});
    -        }
    -        break;
    -      case "query":
    -        break;
    -      case "body":
    -        break;
    -      default:
    -        validationErrors.push({"path": api.path, "name": param.name, "error": "invalid param type " + param.paramType});
    -        break;
    -    }
    -  }
    -
    -  if (validationErrors.length > 0) {
    -    console.log(validationErrors);
    -    return;
    -  }
    -  
    -  if (!api.operations) {
    -    api.operations = []; }

    TODO: replace if existing HTTP operation in same api path

      var op = {
    -    "parameters" : spec.params,
    -    "httpMethod" : spec.method,
    -    "notes" : spec.notes,
    -    "errorResponses" : spec.errorResponses,
    -    "nickname" : spec.nickname,
    -    "summary" : spec.summary
    -  };
    -  
    -  if (spec.responseClass) {
    -    op.responseClass = spec.responseClass; 
    -  }
    -  else {
    -    op.responseClass = "void";
    -  }
    -  api.operations.push(op);
    -
    -  if (!rootResource.models) {
    -    rootResource.models = {}; 
    -  }
    -}
    -
    -function addValidator(v) {
    -  validators.push(v);
    -}

    Create Error JSON by code and text

    function error(code, description) {
    -  return {"code" : code, "description" : description};
    -}

    Stop express ressource with error code

    function stopWithError(res, error) {
    -  setHeaders(res);
    -  if (error && error.description && error.code)
    -    res.send(JSON.stringify(error), error.code);
    -  else
    -    res.send(JSON.stringify({'description': 'internal error', 'code': 500}), 500);
    -}

    Export most needed error types for easier handling

    exports.errors = {
    -  'notFound': function(field, res) { 
    -    if (!res) { 
    -      return {"code": 404, "description": field + ' not found'}; } 
    -    else { 
    -      res.send({"code": 404, "description": field + ' not found'}, 404); } 
    -  },
    -  'invalid': function(field, res) { 
    -    if (!res) { 
    -      return {"code": 400, "description": 'invalid ' + field}; } 
    -    else { 
    -      res.send({"code": 400, "description": 'invalid ' + field}, 404); } 
    -  },
    -  'forbidden': function(res) {
    -    if (!res) { 
    -      return {"code": 403, "description": 'forbidden' }; } 
    -    else { 
    -      res.send({"code": 403, "description": 'forbidden'}, 403); }
    -  }
    -};
    -
    -exports.params = params;
    -exports.queryParam = exports.params.query;
    -exports.pathParam = exports.params.path;
    -exports.postParam = exports.params.post;
    -exports.getModels = allModels;
    -
    -exports.error = error;
    -exports.stopWithError = stopWithError;
    -exports.stop = stopWithError;
    -exports.addValidator = addValidator;
    -exports.configure = configure;
    -exports.canAccessResource = canAccessResource;
    -exports.resourcePath = resourcePath;
    -exports.resourceListing = resourceListing;
    -exports.setHeaders = setHeaders;
    -exports.addGet = addGet;
    -exports.addPost = addPost;
    -exports.addPut = addPut;
    -exports.addDelete = addDelete;
    -exports.addGET = addGet;
    -exports.addPOST = addPost;
    -exports.addPUT = addPut;
    -exports.addDELETE = addDelete;
    -exports.addModels = addModels;
    -exports.setAppHandler = setAppHandler;
    -exports.discover = discover;
    -exports.discoverFile = discoverFile;
    -
    -
    \ No newline at end of file diff --git a/docs/toc.md b/docs/toc.md new file mode 100644 index 00000000..53f2334e --- /dev/null +++ b/docs/toc.md @@ -0,0 +1,16 @@ + +## Table of contents + +![alt text](./images/swagger-icon.png) + +* [Introduction](./introduction.md) +* [Installation](./install.md) +* [Quick start](./quick-start.md) +* [CLI reference](./cli.md) +* [About Swagger](./swagger-about.md) +* [About the swagger.yaml file](./swagger-file.md) +* [Adding paths](./adding-paths.md) +* [Writing controllers](./controllers.md) +* [Using mock mode](./mock-mode.md) +* [Modules and dependencies](./modules.md) +* [Reporting issues](./report-issues.md) \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index d1e26eba..00000000 --- a/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./lib/swagger.js"); diff --git a/lib/commands/commands.js b/lib/commands/commands.js new file mode 100644 index 00000000..9ea6f9fc --- /dev/null +++ b/lib/commands/commands.js @@ -0,0 +1,99 @@ +#!/usr/bin/env node +/**************************************************************************** + Copyright 2016 Apigee Corporation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ****************************************************************************/ +'use strict'; + +var swaggerConverter = require('swagger-converter'); +var swaggerSpec = require('../util/spec'); +var yaml = require('js-yaml'); +var join = require('path').join; +var fs = require('fs'); + +var convert = function convert(filePath, apiDeclarations, options, cb) { + if (filePath) { + if (!fs.existsSync(join(process.cwd(), filePath))) { + return cb(error); + } + + var resource = fs.readFileSync(join(process.cwd(), filePath), 'utf8'); + var json; + + try { + json = JSON.parse(resource); + } catch (error) { + return cb(error); + } + + var declarations = []; + var tempJson; + + apiDeclarations.forEach(function(currentValue) { + if (!fs.existsSync(join(process.cwd(), currentValue))) { + return cb(error); + } + + try { + tempJson = JSON.parse(fs.readFileSync(join(process.cwd(), currentValue), 'utf8')) + } catch (error) { + return cb(error); + } + + declarations.push(tempJson); + }); + + var swagger2 = yaml.safeDump(swaggerConverter(json, declarations)); + + if (options.outputFile) { + fs.writeFile(join(process.cwd(), options.outputFile), swagger2, function(err) { + if (err) {return cb(err);} + }); + } else { + cb(null, swagger2); + } + } +} + +var validate = function validate(file, options, cb) { + + if (!file) { // check stream + process.stdin.resume(); + process.stdin.setEncoding('utf8'); + process.stdin.on('data', function(data) { + if (!data) { process.exit(1); } + swaggerSpec.validateSwagger(parse(data), options, cb); + }); + } else { + var data = fs.readFileSync(file, 'utf8'); + swaggerSpec.validateSwagger(parse(data), options, cb); + } +} + +function parse(data) { + if (isJSON(data)) { + return JSON.parse(data); + } else { + return yaml.safeLoad(data); + } +} + +function isJSON(data) { + return data.match(/^\s*\{/); +} + +module.exports = { + convert: convert, + validate: validate +} diff --git a/lib/commands/project/project.js b/lib/commands/project/project.js new file mode 100644 index 00000000..f9fd5e7f --- /dev/null +++ b/lib/commands/project/project.js @@ -0,0 +1,557 @@ +/**************************************************************************** + Copyright 2016 Apigee Corporation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ****************************************************************************/ +'use strict'; + +var config = require('../../../config'); +var _ = require('lodash'); +var path = require('path'); +var fs = require('fs-extra'); +var emit = require('../../util/feedback').emit; +var netutil = require('../../util/net'); +var debug = require('debug')('swagger'); +var util = require('util'); +var cli = require('../../util/cli'); +var template = require('swagger-test-templates'); +var async = require('async'); +var swaggerSpec = require('../../util/spec'); +var spec = require('swagger-tools').specs.v2; +var inquirer = require('inquirer'); + +var FRAMEWORKS = { + connect: { source: 'connect' }, + express: { source: 'connect', overlay: 'express' }, + hapi: { source: 'connect', overlay: 'hapi' }, + restify: { source: 'connect', overlay: 'restify' }, + sails: { source: 'sails' } +}; + +var TEST_ASSERTION_TYPES = ['expect', 'should', 'assert']; +var TEST_MODULES = ['supertest', 'request']; +var TEST_DEPENDENCIES = { + 'z-schema': '^3.12.0', + request: '^2.58.0', + chai: '^3.0.0', + mocha: '^2.2.5', + dotenv: '^1.2.0' +}; + +module.exports = { + create: create, + start: start, + verify: verify, + edit: edit, + open: open, + test: test, + + // for internal use + frameworks: FRAMEWORKS, + read: readProject, + assertiontypes: TEST_ASSERTION_TYPES, + testmodules: TEST_MODULES, + + // for testing stub generating + generateTest: testGenerate +}; + +//.option('-f, --framework ', 'one of: connect | express') +function create(name, options, cb) { + function validateName(name) { + var targetDir = path.resolve(process.cwd(), name); + if (fs.existsSync(targetDir)) { + return 'Directory ' + targetDir + ' already exists.'; + } + return true; + } + + if (name) { + var valid = validateName(name); + if (typeof valid === 'string') { return cb(new Error(valid)); } + } + + if (options.framework && !FRAMEWORKS[options.framework]) { + return cb(new Error(util.format('Unknown framework: %j. Valid frameworks: %s', options.framework, Object.keys(FRAMEWORKS).join(', ')))); + } + + var questions = [ + { name: 'name', message: 'Project name?', validate: validateName }, + { name: 'framework', message: 'Framework?', type: 'list', choices: Object.keys(FRAMEWORKS) } + ]; + + var results = { + name: name, + framework: options.framework + }; + + cli.requireAnswers(questions, results, function(results) { + + var name = results.name; + var framework = results.framework; + var targetDir = path.resolve(process.cwd(), name); + + cloneSkeleton(name, framework, targetDir, function(err) { + if (err) { return cb(err); } + emit('Project %s created in %s', name, targetDir); + + var message = util.format('Success! You may start your new app by running: "swagger project start %s"', name); + + installDependencies(targetDir, message, cb); + }); + }); +} + +//.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); + var nodemonOpts = { + script: project.api.main, + ext: 'js,json,yaml,coffee', + nodeArgs: [] + }; + if (project.dirname) { nodemonOpts.cwd = project.dirname; } + if (options.debugBrk) { + var debugBrkArg = '--debug-brk'; + if (typeof options.debugBrk === 'string') { + debugBrkArg += '=' + options.debugBrk; + } + nodemonOpts.nodeArgs.push(debugBrkArg); + } + if (options.debug) { + var debugArg = '--debug'; + if (typeof options.debug === 'string') { + debugArg += '=' + options.debug; + } + nodemonOpts.nodeArgs.push(debugArg); + } + if (options.nodeArgs) { + nodemonOpts.nodeArgs = nodemonOpts.nodeArgs.concat(options.nodeArgs.split(' ')); + } + // https://www.npmjs.com/package/cors + nodemonOpts.env = { + swagger_corsOptions: '{}' // enable CORS so editor "try it" function can work + }; + if (options.mock) { + nodemonOpts.env.swagger_mockMode = true + } + 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); + }).on('quit', function () { + process.exit(0); + }); + }); +} + +//.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))); + } + } + testPath = path.resolve(testPath, 'test'); + debug('testPath: %s', testPath); + + if (fs.statSync(testPath).isFile()) { + if (testPath.substr(-3) !== '.js') { return cb(new Error('file is not a javascript file')); } + mocha.addFile(testPath); + debug('mocha addFile: %s', testPath); + } else { + MochaUtils.lookupFiles(testPath, ['js'], true) + .forEach(function(file) { + mocha.addFile(file); + debug('mocha addFile: %s', file); + }); + } + + emit('Running tests in: %s...', testPath); + + if (options.mock) { + process.env.swagger_mockMode = true; + } + mocha.run(function(failures) { + process.exit(failures); + }); + }); +} + +function verify(directory, options, cb) { + + readProject(directory, options, function(err, project) { + if (err) { return cb(err); } + + swaggerSpec.validateSwagger(project.api.swagger, options, cb); + }); +} + +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('js-yaml'); + 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, config.swagger.fileName); + project.api.swagger = yaml.safeLoad(fs.readFileSync(project.api.swaggerFile, 'utf8')); + + 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, framework, destDir, cb) { + + var skeletonsDir = config.project.skeletonsDir; + + framework = FRAMEWORKS[framework]; + var sourceDir = path.resolve(skeletonsDir, framework.source); + var overlayDir = (framework.overlay) ? path.resolve(skeletonsDir, framework.overlay) : null; + + var done = function(err) { + if (err) { return cb(err); } + customizeClonedFiles(name, framework, destDir, cb); + }; + + debug('copying source files from %s', sourceDir); + fs.copy(sourceDir, destDir, true, function(err) { + if (err) { return cb(err); } + if (overlayDir) { + debug('copying overlay files from %s', overlayDir); + fs.copy(overlayDir, destDir, false, done); + } else { + done(); + } + }); +} + +function customizeClonedFiles(name, framework, destDir, cb) { + + // npm renames .gitignore to .npmignore, change it back + 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, ' '), cb); + }); + }); +} + +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); + }); +} + + +//.option('-p, --path-name [path]', 'a sepecific path of the api') +//.option('-f, --test-module ', 'one of: ' + testmodules) +//.option('-t, --assertion-format ', 'one of: ' + assertiontypes) +//.option('-o, --force', 'allow overwriting of all existing test files matching those generated') +function testGenerate(directory, options, cb) { + var pathList = []; + var desiredPaths = []; + var testModule = options.testModule || TEST_MODULES[0]; + var assertionFormat = options.assertionFormat || TEST_ASSERTION_TYPES[0]; + var overwriteAll = options.force || false; + var loadTesting = options.loadTest || false; + directory = directory || process.cwd(); + + findProjectFile(directory, null, function(err, projPath) { + var projectFile; + var projectJson = require(projPath); + var jsonCopy = _.cloneDeep(projectJson); + var runInstall = false; + + if (err) { return cb(err); } + + if (!_.isEqual(jsonCopy.devDependencies, _.defaultsDeep(projectJson.devDependencies, TEST_DEPENDENCIES))) { + runInstall = true; + } + + _.defaultsDeep(projectJson, {scripts: {test: 'swagger project test'}}); + + projectFile = JSON.stringify(projectJson, null, 2); + + fs.writeFileSync(projPath, projectFile); + + if (!fs.existsSync(path.join(directory, 'test/api/client'))) { + fs.mkdirSync(path.join(directory, 'test/api/client')); + } + + //read the yaml file and validate it + readProject(directory, options, function(err, project) { + if (err) { return cb(err); } + swaggerSpec.validateSwagger(project.api.swagger, options, function(err) { + spec.resolve(project.api.swagger, function(err, result) { + // get the array of string paths from json object + pathList = Object.keys(result.paths); + + //check if the test frame is one of the two + if (options.testModule && !_.includes(TEST_MODULES, options.testModule)) { + return cb(new Error(util.format('Unknown type: %j. Valid types: %s', options.testModule, TEST_MODULES.join(', ')))); + } + + // check if the assertion-format is one of the three + if (options.assertionFormat && !_.includes(TEST_ASSERTION_TYPES, options.assertionFormat)) { + return cb(new Error(util.format('Unknown type: %j. Valid types: %s', options.assertionFormat, TEST_ASSERTION_TYPES.join(', ')))); + } + + // process the paths option + if (options.pathName){ + var reg = new RegExp(options.pathName); + desiredPaths = pathList.filter(function(val) { + return val.match(reg); + }); + } + + // pass the config to the module and get the result string array + var config = { + pathName: desiredPaths, + testModule: testModule, + assertionFormat: assertionFormat + }; + + // pass list of paths targeted for load testing + if (loadTesting) { + if ((typeof loadTesting) !== 'boolean' && fs.existsSync(path.join(directory, loadTesting))) { + config.loadTest = parseJsonFile(directory, loadTesting).loadTargets; + } else if (fs.existsSync(path.join(directory, 'load-config.json'))){ + config.loadTest = parseJsonFile(directory, 'load-config.json').loadTargets; + } else { + return cb(new Error('Config file not found. Please specify a load test config or add load-config.json file to your project directory.')); + } + } + + var finalResult = template.testGen(result, config); + var existingFiles = fs.readdirSync(path.join(directory, 'test/api/client')); + var skipAll = false; + + async.filterSeries(finalResult, function(file, cb) { + if (overwriteAll) { + cb(true); + } else if(skipAll){ + cb(false); + } else { + if (_.includes(existingFiles, file.name)) { + var prompt = util.format('Conflict on %s. Overwrite? (answer \'h\' for help):', file.name); + var question = {type: 'expand', message: prompt, name: 'overwrite', choices: [ + { + key: "y", + name: "Overwrite this one and show the next", + value: "overwrite" + }, + { + key: "a", + name: "Overwrite this one and all of the next", + value: "overwrite_all" + }, + { + key: 'n', + name: 'Skip this one and show the next', + value: 'overwrite_skip' + }, + { + key: 'x', + name: 'Skip this one and all of the next', + value: 'overwrite_skip_all' + } + ]}; + + inquirer.prompt(question, function(answers) { + if (answers.overwrite === 'overwrite') { + cb(true); + } else if (answers.overwrite === 'overwrite_all') { + overwriteAll = true; + cb(true); + } else if (answers.overwrite === 'overwrite_skip') { + cb(false); + } else { + skipAll = true; + cb(false); + } + }); + } else { + cb(true); + } + } + }, function(filteredResult) { + + async.each(filteredResult, function(file, cb) { + if (file.name === '.env') { + fs.outputFile(path.join(directory, file.name), file.test, cb); + } else { + fs.outputFile(path.join(directory, '/test/api/client', file.name), file.test, cb); + } + }, function(err) { + if (runInstall) { + installDependencies(directory, 'Success! You may now run your tests.', cb); + } + }); + }); + }); + }); + }); + }); +} + +function parseJsonFile(directory, filePath) { + return JSON.parse(fs.readFileSync(path.join(directory, filePath))); +} + +function installDependencies(directory, message, cb) { + spawn('npm', ['install'], directory, function(err) { + if (err) { + emit('"npm install" failed. Please run "npm install" in %s.', directory); + return cb(err); + } + cb(null, message); + }); +} diff --git a/lib/commands/project/swagger_editor.js b/lib/commands/project/swagger_editor.js new file mode 100644 index 00000000..d03247de --- /dev/null +++ b/lib/commands/project/swagger_editor.js @@ -0,0 +1,93 @@ +/**************************************************************************** + Copyright 2016 Apigee Corporation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ****************************************************************************/ +'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'; + +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) ); + + app.use(SWAGGER_EDITOR_CONFIG_PATH, function(req, res, next) { + if (req.method !== 'GET') { return next(); } + res.end(JSON.stringify(config.swagger.editorConfig)); + }); + + // serve swagger-editor + app.use(SWAGGER_EDITOR_SERVE_PATH, serveStatic(config.swagger.editorDir)); + + + // start // + + var http = require('http'); + var server = http.createServer(app); + var port = options.port || 0; + var hostname = options.host || '127.0.0.1'; + server.listen(port, hostname, function() { + port = server.address().port; + var editorUrl = util.format('http://%s:%d/#/edit', hostname, port); + var editApiUrl = util.format('http://%s:%d/editor/spec', hostname, 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/paramTypes.js b/lib/paramTypes.js deleted file mode 100644 index ae031d24..00000000 --- a/lib/paramTypes.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright 2013 Wordnik, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -exports.query = exports.q = function(name, description, type, required, allowableValuesEnum, defaultValue) { - return { - "name" : name, - "description" : description, - "type" : type, - "required" : required, - "enum" : allowableValuesEnum, - "defaultValue" : defaultValue, - "paramType" : "query" - }; -}; - -exports.path = function(name, description, type, allowableValuesEnum, defaultValue) { - return { - "name" : name, - "description" : description, - "type" : type, - "required" : true, - "enum" : allowableValuesEnum, - "paramType" : "path", - "defaultValue" : defaultValue - }; -}; - -exports.body = function(name, description, type, defaultValue) { - return { - "name" : name, - "description" : description, - "type" : type, - "required" : true, - "paramType" : "body", - "defaultValue" : defaultValue - }; -}; - -exports.form = function(name, description, type, required, allowableValuesEnum, defaultValue) { - return { - "name" : name, - "description" : description, - "type" : "string", - "required" : (typeof required !== 'undefined') ? required : true, - "enum" : allowableValuesEnum, - "paramType" : "form", - "defaultValue" : defaultValue - }; -}; - -exports.header = function(name, description, type, required) { - return { - "name" : name, - "description" : description, - "type" : type, - "required" : required, - "allowMultiple" : false, - "paramType" : "header" - }; -}; diff --git a/lib/shallowClone.js b/lib/shallowClone.js deleted file mode 100644 index 0efef443..00000000 --- a/lib/shallowClone.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -module.exports = shallowClone; - -// clone anything but objects to avoid shared references -function shallowClone(obj) { - var cloned = {}; - for (var i in obj) { - if (!obj.hasOwnProperty(i)) { - continue; - } - if (typeof (obj[i]) != "object") { - cloned[i] = obj[i]; - } - } - return cloned; -} diff --git a/lib/swagger.js b/lib/swagger.js deleted file mode 100644 index 2fbbde52..00000000 --- a/lib/swagger.js +++ /dev/null @@ -1,721 +0,0 @@ -/** - * Copyright 2014 Wordnik, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var _ = require('lodash'); -var allowedMethods = ['get', 'post', 'put', 'patch', 'delete']; -var allowedDataTypes = ['string', 'integer', 'boolean', 'array']; -var params = require(__dirname + '/paramTypes.js'); -var toJsonType = require('./toJsonType'); -var shallowClone = require('./shallowClone'); - - -function Swagger() { - - if (!(this instanceof Swagger)){ - return new Swagger(); - } - - this.formatString = ".{format}"; - this.resourcePath = "/api-docs" + this.formatString; - this.jsonSuffix = ".json"; - this.basePath = "/"; - this.apiInfo = null; - this.authorizations = null; - this.swaggerVersion = "1.2"; - this.apiVersion = "1.0"; - this.allModels = {}; - this.validators = []; - this.appHandler = null; - this.resources = {}; - - // Default error handler - - this.errorHandler = function (req, res, error) { - if (error.code && error.message) - res.send(JSON.stringify(error), error.code); - else { - console.error(req.method + " failed for path '" + require('url').parse(req.url).href + "': " + error); - res.send(JSON.stringify({ - "message": "unknown error", - "code": 500 - }), 500); - } - }; - - // For backwards compatability - this.getModels = this.allModels; -} - -/** - * returns a new instance of swagger - */ - -Swagger.prototype.createNew = function(){ - return new Swagger(); -}; - -Swagger.prototype.configureSwaggerPaths = function(format, path, suffix) { - if(path.indexOf("/") != 0) path = "/" + path; - this.formatString = format; - this.resourcePath = path; - this.jsonSuffix = suffix; -}; - -// Configuring swagger will set the basepath and api version for all -// subdocuments. It should only be done once, and during bootstrap of the app - -Swagger.prototype.configure = function(bp, av) { - var self = this; - self.basePath = bp; - self.apiVersion = av; - self.setResourceListingPaths(self.appHandler); - - // add the GET for resource listing - var resourceListing = _.bind(self.resourceListing, self); - self.appHandler.get(self.resourcePath.replace(self.formatString, self.jsonSuffix), resourceListing); - - // update resources if already configured - - _.forOwn(self.resources, function (resource) { - resource.apiVersion = av; - resource.basePath = bp; - }); -}; - -// Convenience to set default headers in each response. - -Swagger.prototype.setHeaders = function(res) { - res.header("Access-Control-Allow-Headers", "Content-Type, api_key"); - res.header("Content-Type", "application/json; charset=utf-8"); -}; - -// creates declarations for each resource path. - -Swagger.prototype.setResourceListingPaths = function(app) { - var self = this; - _.forOwn(this.resources, function (resource, key) { - - // pet.json => api-docs.json/pet - var path = self.baseApiFromPath(key); - app.get(path, function (req, res) { - // find the api base path from the request URL - // /api-docs.json/pet => /pet.json - - var p = self.basePathFromApi(req.url.split('?')[0]); - - // this handles the request - // api-docs.json/pet => pet.{format} - var r = self.resources[p] || self.resources[p.replace(self.formatString, "")]; - if (!r) { - console.error("unable to find listing"); - return stopWithError(res, { - 'message': 'internal error', - 'code': 500 - }); - } else { - self.setHeaders(res); - var data = self.filterApiListing(req, res, r); - data.basePath = self.basePath; - if (data.code) { - res.send(data, data.code); - } else { - res.send(JSON.stringify(data)); - } - } - }); - }); -}; - -Swagger.prototype.basePathFromApi = function(path) { - var l = this.resourcePath.replace(this.formatString, this.jsonSuffix); - var p = path.substring(l.length + 1) + this.formatString; - return p; -}; - -Swagger.prototype.baseApiFromPath = function(path) { - var p = this.resourcePath.replace(this.formatString, this.jsonSuffix) + "/" + path.replace(this.formatString, ""); - return p; -}; - -Swagger.prototype.addPropertiesToRequiredModels = function(properties, requiredModels) { - var self = this; - _.forOwn(properties, function (property) { - var type = property["type"]; - if(type) { - switch (type) { - case "array": - if (property.items) { - var ref = property.items.$ref; - if (ref && requiredModels.indexOf(ref) < 0) { - requiredModels.push(ref); - } - } - break; - case "string": - case "integer": - break; - default: - if (requiredModels.indexOf(type) < 0) { - requiredModels.push(type); - } - break; - } - } - else { - if (property["$ref"]){ - requiredModels.push(property["$ref"]); - } - } - if (property.properties) { - self.addPropertiesToRequiredModels(property.properties, requiredModels); - } - }); -}; - -// Applies a filter to an api listing. When done, the api listing will only contain -// methods and models that the user actually has access to. - -Swagger.prototype.filterApiListing = function(req, res, r) { - var self = this; - var excludedPaths = []; - - if (!r || !r.apis) { - return stopWithError(res, { - 'message': 'internal error', - 'code': 500 - }); - } - - _.forOwn(r.apis, function (api) { - for (var opKey in api.operations) { - if (!api.operations.hasOwnProperty(opKey)) { - continue; - } - var op = api.operations[opKey]; - var path = api.path.replace(self.formatString, "").replace(/{.*\}/, "*"); - if (!self.canAccessResource(req, path, op.method)) { - excludedPaths.push(op.method + ":" + api.path); - } - } - }); - - // clone attributes in the resource - var output = shallowClone(r); - - // clone arrays for - if(r["produces"]) output.produces = r["produces"].slice(0); - if(r["consumes"]) output.consumes = r["consumes"].slice(0); - if(r["authorizations"]) output.authorizations = r["authorizations"].slice(0); - if(r["protocols"]) output.protocols = r["protocols"].slice(0); - - // models required in the api listing - var requiredModels = []; - - // clone methods that user can access - output.apis = []; - var apis = JSON.parse(JSON.stringify(r.apis)); - _.forOwn(apis, function (api) { - var clonedApi = shallowClone(api); - - clonedApi.operations = []; - _.forOwn(api.operations, function (operation) { - if (excludedPaths.indexOf(operation.method + ":" + api.path) == -1) { - var co = JSON.parse(JSON.stringify(operation)); - delete co.path; - - var type = toJsonType(co.type); - if(type) { - for(var nm in type) { - delete co[nm]; - co[nm] = type[nm]; - } - } - clonedApi.operations.push(co); - self.addModelsFromBody(operation, requiredModels); - self.addModelsFromResponse(operation, requiredModels); - } - }); - // only add cloned api if there are operations - if (clonedApi.operations.length > 0) { - output.apis.push(clonedApi); - } - }); - - // add required models to output - output.models = {}; - _.forOwn(requiredModels, function (modelName) { - var model = self.allModels[modelName]; - if (model) { - output.models[modelName] = model; - } - }); - - // look in object graph - _.forOwn(output.models, function (model) { - if (model && model.properties) { - self.addPropertiesToRequiredModels(model.properties, requiredModels); - } - }); - _.forOwn(requiredModels, function (modelName) { - if (!output[modelName]) { - var model = self.allModels[modelName]; - if (model) { - output.models[modelName] = model; - } - } - }); - - return output; -}; - - - -// Add model to list and parse List[model] elements - -Swagger.prototype.addModelsFromBody = function(operation, models) { - var self = this; - if (operation.parameters) { - _.forOwn(operation.parameters, function (param) { - if (param.paramType == "body" && param.type) { - var model = param.type.replace(/^List\[/, "").replace(/\]/, ""); - models.push(model); - } - }); - } -}; - -// Add model to list and parse List[model] elements - -Swagger.prototype.addModelsFromResponse = function(operation, models) { - var responseModel = operation.type; - if(responseModel === "array" && operation.items) { - var items = operation.items; - if(items["$ref"]) { - models.push(items["$ref"]); - } - else if (items.type && allowedDataTypes.indexOf(items.type) == -1) { - models.push(items["type"]); - } - } - // if not void or a json-schema type, add the model - else if (responseModel != "void" && allowedDataTypes.indexOf(responseModel) == -1) { - models.push(responseModel); - } -}; - - -// function for filtering a resource. override this with your own implementation. -// if consumer can access the resource, method returns true. - -Swagger.prototype.canAccessResource = function(req, path, method) { - for (var i = 0; i < this.validators.length; i++) { - var validator = this.validators[i]; - if (_.isFunction(validator) && !validator(req, path, method)) { - return false; - } - } - return true; -}; - -/** - * returns the json representation of a resource - * - * @param request - * @param response - */ - -Swagger.prototype.resourceListing = function(req, res) { - var self = this; - var r = { - "apiVersion": self.apiVersion, - "swaggerVersion": self.swaggerVersion, - "apis": [] - }; - - if(self.authorizations != null) - r["authorizations"] = self.authorizations; - - if(self.apiInfo != null) - r["info"] = self.apiInfo; - - _.forOwn(self.resources, function (value, key) { - var p = "/" + key.replace(self.formatString, ""); - r.apis.push({ - "path": p, - "description": value.description - }); - }); - self.setHeaders(res); - res.write(JSON.stringify(r)); - res.end(); -}; - -// Adds a method to the api along with a spec. If the spec fails to validate, it won't be added - -Swagger.prototype.addMethod = function(app, callback, spec) { - var self = this; - var apiRootPath = spec.path.split(/[\/\(]/)[1]; - var root = self.resources[apiRootPath]; - - if (root && root.apis) { - // this path already exists in swagger resources - _.forOwn(root.apis, function (api) { - if (api && api.path == spec.path && api.method == spec.method) { - // add operation & return - appendToApi(root, api, spec); - return; - } - }); - } - - var api = { - "path": spec.path - }; - if (!self.resources[apiRootPath]) { - if (!root) { - // - var resourcePath = "/" + apiRootPath.replace(self.formatString, ""); - root = { - "apiVersion": self.apiVersion, - "swaggerVersion": self.swaggerVersion, - "basePath": self.basePath, - "resourcePath": resourcePath, - "apis": [], - "models": [] - }; - } - self.resources[apiRootPath] = root; - } - - root.apis.push(api); - appendToApi(root, api, spec); - - // convert .{format} to .json, make path params happy - var fullPath = spec.path.replace(self.formatString, self.jsonSuffix).replace(/\/{/g, "/:").replace(/\}/g, ""); - var currentMethod = spec.method.toLowerCase(); - if (allowedMethods.indexOf(currentMethod) > -1) { - app[currentMethod](fullPath, function (req, res, next) { - self.setHeaders(res); - - // todo: needs to do smarter matching against the defined paths - var path = req.url.split('?')[0].replace(self.jsonSuffix, "").replace(/{.*\}/, "*"); - if (!self.canAccessResource(req, path, req.method)) { - res.send(JSON.stringify({ - "message": "forbidden", - "code": 403 - }), 403); - } else { - try { - callback(req, res, next); - } catch (error) { - if (typeof self.errorHandler === "function") { - self.errorHandler(req, res, error); - } else if (self.errorHandler === "next") { - next(error); - } else { - throw error; - } - } - } - }); - } else { - console.error('unable to add ' + currentMethod.toUpperCase() + ' handler'); - return; - } -}; - -// Set expressjs app handler - -Swagger.prototype.setAppHandler = function(app) { - this.appHandler = app; -}; - -// Change error handler -// Error handler should be a function that accepts parameters req, res, error - -Swagger.prototype.setErrorHandler= function(handler) { - this.errorHandler = handler; -}; - -// Add swagger handlers to express - -Swagger.prototype.addHandlers = function(type, handlers) { - var self = this; - _.forOwn(handlers, function (handler) { - handler.spec.method = type; - self.addMethod(self.appHandler, handler.action, handler.spec); - }); -}; - -// Discover swagger handler from resource - -Swagger.prototype.discover = function(resource) { - var self = this; - _.forOwn(resource, function (handler, key) { - if (handler.spec && handler.spec.method && allowedMethods.indexOf(handler.spec.method.toLowerCase()) > -1) { - self.addMethod(self.appHandler, handler.action, handler.spec); - } else - console.error('auto discover failed for: ' + key); - }); -}; - -// Discover swagger handler from resource file path - -Swagger.prototype.discoverFile = function(file) { - return this.discover(require(file)); -}; - -// adds get handler - -Swagger.prototype.addGet = Swagger.prototype.addGET = function() { - this.addHandlers('GET', arguments); - return this; -}; - -// adds post handler - -Swagger.prototype.addPost = Swagger.prototype.addPOST = function() { - this.addHandlers('POST', arguments); - return this; -}; - -// adds delete handler - -Swagger.prototype.addDelete = Swagger.prototype.addDELETE = function() { - this.addHandlers('DELETE', arguments); - return this; -}; - -// adds put handler - -Swagger.prototype.addPut = Swagger.prototype.addPUT = function() { - this.addHandlers('PUT', arguments); - return this; -}; - -// adds patch handler - -Swagger.prototype.addPatch = Swagger.prototype.addPATCH = function() { - this.addHandlers('PATCH', arguments); - return this; -}; - -// adds models to swagger - -Swagger.prototype.addModels = function(models) { - models = _.cloneDeep(models).models; - var self = this; - if (!self.allModels) { - self.allModels = models; - } else { - _.forOwn(models, function (model, key) { - self.allModels[key] = model; - }); - } - return this; -}; - -function wrap(callback, req, resp) { - callback(req, resp); -} - -// appends a spec to an existing operation - -function appendToApi(rootResource, api, spec) { - var validationErrors = []; - - if (!spec.nickname || spec.nickname.indexOf(" ") >= 0) { - // nicknames don't allow spaces - validationErrors.push({ - "path": api.path, - "error": "invalid nickname '" + spec.nickname + "'" - }); - } - // validate params - _.forOwn(spec.parameters, function (parameter) { - - switch (parameter.paramType) { - case "path": - if (api.path.indexOf("{" + parameter.name + "}") < 0) { - validationErrors.push({ - "path": api.path, - "name": parameter.name, - "error": "invalid path" - }); - } - break; - case "query": - break; - case "body": - break; - case "form": - break; - case "header": - break; - default: - validationErrors.push({ - "path": api.path, - "name": parameter.name, - "error": "invalid param type " + parameter.paramType - }); - break; - } - }); - - if (validationErrors.length > 0) { - console.error(validationErrors); - return; - } - - if (!api.operations) { - api.operations = []; - } - - // TODO: replace if existing HTTP operation in same api path - var op = { - "parameters": spec.parameters, - "method": spec.method, - "notes": spec.notes, - "responseMessages": spec.responseMessages, - "nickname": spec.nickname, - "summary": spec.summary, - "consumes" : spec.consumes, - "produces" : spec.produces - }; - - // Add custom fields. - op = _.extend({}, spec, op); - - if (!spec.type) { - op.type = "void"; - } - api.operations.push(op); - - if (!rootResource.models) { - rootResource.models = {}; - } -} - -Swagger.prototype.addValidator = function(v) { - this.validators.push(v); -}; - -// Create Error JSON by code and text - -function error(code, description) { - return { - "code": code, - "message": description - }; -} - -// Stop express ressource with error code - -stopWithError = function(res, error) { - this.setHeaders(res); - if (error && error.message && error.code) - res.send(JSON.stringify(error), error.code); - else - res.send(JSON.stringify({ - 'message': 'internal error', - 'code': 500 - }), 500); -}; - -Swagger.prototype.setApiInfo = function(data) { - this.apiInfo = data; -}; - -Swagger.prototype.setAuthorizations = function(data) { - this.authorizations = data; -}; - -// Export most needed error types for easier handling -Swagger.prototype.errors = { - 'notFound': function (field, res) { - if (!res) { - return { - "code": 404, - "message": field + ' not found' - }; - } else { - res.send({ - "code": 404, - "message": field + ' not found' - }, 404); - } - }, - 'invalid': function (field, res) { - if (!res) { - return { - "code": 400, - "message": 'invalid ' + field - }; - } else { - res.send({ - "code": 400, - "message": 'invalid ' + field - }, 404); - } - }, - 'forbidden': function (res) { - if (!res) { - return { - "code": 403, - "message": 'forbidden' - }; - } else { - res.send({ - "code": 403, - "message": 'forbidden' - }, 403); - } - } -}; - -Swagger.prototype.configureDeclaration = function(resourceName, obj) { - if(this.resources[resourceName]) { - var resource = this.resources[resourceName]; - - if(obj["description"]) { - resource["description"] = obj["description"]; - } - if(obj["consumes"]) { - resource["consumes"] = obj["consumes"]; - } - if(obj["produces"]) { - resource["produces"] = obj["produces"]; - } - if(obj["protocols"]) { - resource["protocols"] = obj["protocols"]; - } - if(obj["authorizations"]) { - resource["authorizations"] = obj["authorizations"]; - } - } -}; - -// For backwards compatability, we just export a new instance of Swagger -module.exports = exports = Swagger(); - -exports.params = params; -exports.queryParam = exports.params.query; -exports.pathParam = exports.params.path; -exports.bodyParam = exports.params.body; -exports.formParam = exports.params.form; -exports.headerParam = exports.params.header; -exports.error = error; -exports.stopWithError = stopWithError; -exports.stop = stopWithError; diff --git a/lib/toJsonType.js b/lib/toJsonType.js deleted file mode 100644 index 3cd7a37e..00000000 --- a/lib/toJsonType.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -module.exports = toJsonType; - -var mappings = { - 'int': { - type: 'integer', - format: 'int32' - }, - 'long': { - type: 'integer', - format: 'int64' - }, - 'float': { - type: 'number', - format: 'float' - }, - 'double': { - type: 'number', - format: 'double' - }, - 'date': { - type: 'string', - format: 'date-time' - } -}; - -function toJsonType(model) { - if(model && mappings[model]) { - return mappings[model]; - } -} - diff --git a/lib/util/browser.js b/lib/util/browser.js new file mode 100644 index 00000000..223e69d9 --- /dev/null +++ b/lib/util/browser.js @@ -0,0 +1,85 @@ +/**************************************************************************** + Copyright 2016 Apigee Corporation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ****************************************************************************/ +'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..5caf7d06 --- /dev/null +++ b/lib/util/cli.js @@ -0,0 +1,189 @@ +/**************************************************************************** + Copyright 2016 Apigee Corporation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ****************************************************************************/ +'use strict'; + +var _ = require('lodash'); +var inquirer = require('inquirer'); +var feedback = require('./feedback'); +var config = require('../../config'); +var yaml = require('js-yaml'); +var util = require('util'); + +module.exports = { + requireAnswers: requireAnswers, + updateAnswers: updateAnswers, + printAndExit: printAndExit, + chooseOne: chooseOne, + validate: validate, + execute: execute, + confirm: confirm, + prompt: prompt, + version: version +}; + +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 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.safeDump(object, { indent: 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 (!_.includes(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..ed249043 --- /dev/null +++ b/lib/util/feedback.js @@ -0,0 +1,39 @@ +/**************************************************************************** + Copyright 2016 Apigee Corporation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ****************************************************************************/ +'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..f668b968 --- /dev/null +++ b/lib/util/net.js @@ -0,0 +1,50 @@ +/**************************************************************************** + Copyright 2016 Apigee Corporation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ****************************************************************************/ +'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 +}; + +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); + }); +} diff --git a/lib/util/spec.js b/lib/util/spec.js new file mode 100644 index 00000000..7c1c532f --- /dev/null +++ b/lib/util/spec.js @@ -0,0 +1,69 @@ +/**************************************************************************** + Copyright 2016 Apigee Corporation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ****************************************************************************/ +'use strict'; + +var emit = require('./feedback').emit; +var swaggerSpec = require('swagger-tools').specs.v2_0; + +module.exports = { + validateSwagger: validateSwagger +}; + +function validateSwagger(swagger, options, cb) { + + swaggerSpec.validate(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'); + } + } + }); +} diff --git a/package.json b/package.json index 1dcdee9f..f944b068 100644 --- a/package.json +++ b/package.json @@ -1,47 +1,57 @@ { - "name": "swagger-node-express", - "version": "2.0.3", - "author": { - "name": "Tony Tam", - "email": "fehguy@gmail.com", - "url": "http://developer.wordnik.com" - }, - "contributors": [ - { - "name": "Pauh Hill", - "email": "phill@kixeye.com" - } - ], - "description": "Wordnik swagger implementation for the express framework", - "repository": { - "type": "git", - "url": "https://github.com/wordnik/swagger-node-express" - }, + "name": "swagger", + "version": "0.7.5", + "description": "The Swagger command-line. Provides Swagger utilities and project lifecycle support.", "keywords": [ - "http", - "rest", "swagger", - "server" + "api", + "apis", + "connect", + "express" ], + "author": "Scott Ganyo ", + "license": "Apache 2.0", + "preferGlobal": true, "main": "index.js", - "engines": { - "node": ">= 0.8.x" + "repository": { + "type": "git", + "url": "https://github.com/swagger-api/swagger-node.git" }, "dependencies": { - "lodash": "1.3.1" + "async": "^2.1.4", + "commander": "^2.7.1", + "connect": "^3.3.5", + "debug": "^2.1.3", + "fs-extra": "^1.0.0", + "inquirer": "^1.2.3", + "js-yaml": "^3.3.0", + "lodash": "^4.17.2", + "mocha": "^3.2.0", + "nodemon": "^1.3.7", + "serve-static": "^1.9.2", + "swagger-converter": "^1.4.1", + "swagger-editor": "^2.9.2", + "swagger-test-templates": "^1.2.0", + "swagger-tools": "^0.10.1" }, "devDependencies": { - "express": "3.x", - "docco": "0.4.x", - "cors": "2.1.1", - "mocha": "~1.20.0", - "should": "~4.0.0", - "once": "~1.3.0", - "request": "~2.36.0" + "chai": "^3.0.0", + "mock-stdin": "^0.3.0", + "proxyquire": "^1.4.0", + "should": "^11.1.1", + "sinon": "^1.15.4", + "superagent": "^3.1.0", + "supertest": "^2.0.1", + "tmp": "^0.0.31", + "z-schema": "^3.14.0" }, - "license": "apache 2.0", "scripts": { - "test": "mocha -r should './test/**/*.js'", - "start": "node sample-application/app.js" + "test": "mocha -u exports -R spec test/config.js test/util test/commands test/commands/project test/project-skeletons", + "coverage": "istanbul cover _mocha -- -u exports -R spec test/config.js test/util test/commands test/commands/project test/project-skeletons", + "start": "node app.js" + }, + "bin": { + "swagger": "bin/swagger.js", + "swagger-project": "bin/swagger-project.js" } -} +} \ No newline at end of file diff --git a/project-skeletons/connect/.gitignore b/project-skeletons/connect/.gitignore new file mode 100644 index 00000000..65f91cd1 --- /dev/null +++ b/project-skeletons/connect/.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 for swagger app +config/runtime.yaml diff --git a/project-skeletons/connect/README.md b/project-skeletons/connect/README.md new file mode 100644 index 00000000..4c894e88 --- /dev/null +++ b/project-skeletons/connect/README.md @@ -0,0 +1 @@ +# Skeleton project for Swagger diff --git a/project-skeletons/connect/api/controllers/README.md b/project-skeletons/connect/api/controllers/README.md new file mode 100644 index 00000000..ddb3f648 --- /dev/null +++ b/project-skeletons/connect/api/controllers/README.md @@ -0,0 +1 @@ +Place your controllers in this directory. diff --git a/project-skeletons/connect/api/controllers/hello_world.js b/project-skeletons/connect/api/controllers/hello_world.js new file mode 100644 index 00000000..4023c5a4 --- /dev/null +++ b/project-skeletons/connect/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 + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode +*/ + +/* + 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({ "message": hello }); +} diff --git a/project-skeletons/connect/api/helpers/README.md b/project-skeletons/connect/api/helpers/README.md new file mode 100644 index 00000000..36a598fd --- /dev/null +++ b/project-skeletons/connect/api/helpers/README.md @@ -0,0 +1 @@ +Place helper files in this directory. diff --git a/project-skeletons/connect/api/mocks/README.md b/project-skeletons/connect/api/mocks/README.md new file mode 100644 index 00000000..51416cef --- /dev/null +++ b/project-skeletons/connect/api/mocks/README.md @@ -0,0 +1 @@ +Place controllers for mock mode in this directory. diff --git a/project-skeletons/connect/api/swagger/swagger.yaml b/project-skeletons/connect/api/swagger/swagger.yaml new file mode 100644 index 00000000..9947201b --- /dev/null +++ b/project-skeletons/connect/api/swagger/swagger.yaml @@ -0,0 +1,56 @@ +swagger: "2.0" +info: + version: "0.0.1" + title: Hello World App +# during dev, should point to your local machine +host: localhost:10010 +# 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" + /swagger: + x-swagger-pipe: swagger_raw +# complex objects have schema definitions +definitions: + HelloWorldResponse: + type: string + ErrorResponse: + required: + - message + properties: + message: + type: string diff --git a/project-skeletons/connect/app.js b/project-skeletons/connect/app.js new file mode 100644 index 00000000..5f4851e1 --- /dev/null +++ b/project-skeletons/connect/app.js @@ -0,0 +1,25 @@ +'use strict'; + +var SwaggerConnect = require('swagger-connect'); +var app = require('connect')(); +// export setup Promis for testing +module.exports = new Promise(function (resolve, reject) { + var config = { + appRoot: __dirname // required config + }; + + SwaggerConnect.create(config, function (err, swaggerConnect) { + if (err) { throw err; } + + // install middleware + swaggerConnect.register(app); + + var port = process.env.PORT || 10010; + app.listen(port); + + if (swaggerConnect.runner.swagger.paths['/hello']) { + console.log('try this:\ncurl http://127.0.0.1:' + port + '/hello?name=Scott'); + } + resolve(app); + }); +}); \ No newline at end of file diff --git a/project-skeletons/connect/config/README.md b/project-skeletons/connect/config/README.md new file mode 100644 index 00000000..d0535bda --- /dev/null +++ b/project-skeletons/connect/config/README.md @@ -0,0 +1 @@ +Place configuration files in this directory. diff --git a/project-skeletons/connect/config/default.yaml b/project-skeletons/connect/config/default.yaml new file mode 100644 index 00000000..fd82fa9f --- /dev/null +++ b/project-skeletons/connect/config/default.yaml @@ -0,0 +1,38 @@ +# swagger configuration file + +# values in the swagger hash are system configuration for swagger-node +swagger: + + fittingsDirs: [ api/fittings ] + defaultPipe: null + swaggerControllerPipe: swagger_controllers # defines the standard processing pipe for controllers + + # values defined in the bagpipes key are the bagpipes pipes and fittings definitions + # (see https://github.com/apigee-127/bagpipes) + bagpipes: + + _router: + name: swagger_router + mockMode: false + mockControllersDirs: [ api/mocks ] + controllersDirs: [ api/controllers ] + + _swagger_validate: + name: swagger_validator + validateResponse: true + + # pipe for all swagger-node controllers + swagger_controllers: + - onError: json_error_handler + - cors + - swagger_params_parser + - swagger_security + - _swagger_validate + - express_compatibility + - _router + + # pipe to serve swagger (endpoint is in swagger.yaml) + swagger_raw: + name: swagger_raw + +# any other values in this file are just loaded into the config for application access... diff --git a/project-skeletons/connect/package.json b/project-skeletons/connect/package.json new file mode 100644 index 00000000..d08243e2 --- /dev/null +++ b/project-skeletons/connect/package.json @@ -0,0 +1,22 @@ +{ + "name": "swagger-app", + "version": "0.0.1", + "private": true, + "description": "New Swagger API Project", + "keywords": [], + "author": "", + "license": "", + "main": "app.js", + "dependencies": { + "connect": "^3.6.0", + "swagger-connect": "^0.7.0" + }, + "devDependencies": { + "should": "^11.2.1", + "supertest": "^3.0.0" + }, + "scripts": { + "start": "node app.js", + "test": "swagger project test" + } +} diff --git a/project-skeletons/connect/test/api/controllers/README.md b/project-skeletons/connect/test/api/controllers/README.md new file mode 100644 index 00000000..16437ee1 --- /dev/null +++ b/project-skeletons/connect/test/api/controllers/README.md @@ -0,0 +1 @@ +Place your controller tests in this directory. diff --git a/project-skeletons/connect/test/api/controllers/hello_world.js b/project-skeletons/connect/test/api/controllers/hello_world.js new file mode 100644 index 00000000..68e9a6f9 --- /dev/null +++ b/project-skeletons/connect/test/api/controllers/hello_world.js @@ -0,0 +1,48 @@ +var should = require('should'); +var request = require('supertest'); +var server = require('../../../app'); + +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({ "message": "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({ "message": "Hello, Scott!" }); + + done(); + }); + }); + + }); + + }); + +}); diff --git a/project-skeletons/connect/test/api/helpers/README.md b/project-skeletons/connect/test/api/helpers/README.md new file mode 100644 index 00000000..8528f1b1 --- /dev/null +++ b/project-skeletons/connect/test/api/helpers/README.md @@ -0,0 +1 @@ +Place your helper tests in this directory. diff --git a/project-skeletons/express/app.js b/project-skeletons/express/app.js new file mode 100644 index 00000000..8745ebff --- /dev/null +++ b/project-skeletons/express/app.js @@ -0,0 +1,26 @@ +'use strict'; + +var SwaggerExpress = require('swagger-express-mw'); +var app = require('express')(); +// export setup Promis for testing +module.exports = new Promise(function (resolve, reject) { + + var config = { + appRoot: __dirname // required config + }; + + SwaggerExpress.create(config, function (err, swaggerExpress) { + if (err) { throw err; } + + // install middleware + swaggerExpress.register(app); + + var port = process.env.PORT || 10010; + app.listen(port, function() { + if (swaggerExpress.runner.swagger.paths['/hello']) { + console.log('try this:\ncurl http://127.0.0.1:' + port + '/hello?name=Scott'); + } + resolve(app); + }); + }); +}); \ No newline at end of file diff --git a/project-skeletons/express/package.json b/project-skeletons/express/package.json new file mode 100644 index 00000000..1a9f7c50 --- /dev/null +++ b/project-skeletons/express/package.json @@ -0,0 +1,22 @@ +{ + "name": "swagger-app", + "version": "0.0.1", + "private": true, + "description": "New Swagger API Project", + "keywords": [], + "author": "", + "license": "", + "main": "app.js", + "dependencies": { + "express": "^4.15.2", + "swagger-express-mw": "^0.7.0" + }, + "devDependencies": { + "should": "^11.2.1", + "supertest": "^3.0.0" + }, + "scripts": { + "start": "node app.js", + "test": "swagger project test" + } +} diff --git a/project-skeletons/hapi/app.js b/project-skeletons/hapi/app.js new file mode 100644 index 00000000..e1b3ecc0 --- /dev/null +++ b/project-skeletons/hapi/app.js @@ -0,0 +1,36 @@ +'use strict'; + +var SwaggerHapi = require('swagger-hapi'); +var Hapi = require('hapi'); +var app = new Hapi.Server(); + +// export setup Promis for testing +module.exports = new Promise(function (resolve, reject) { + + var config = { + appRoot: __dirname // required config + }; + + SwaggerHapi.create(config, function (err, swaggerHapi) { + if (err) { throw err; } + + var port = process.env.PORT || 10010; + app.connection({ port: port }); + app.address = function () { + return { port: port }; + }; + + app.register(swaggerHapi.plugin, function (err) { + if (err) { + console.error('Failed to load plugin:', err); + reject(err); + } + app.start(function () { + if (swaggerHapi.runner.swagger.paths['/hello']) { + console.log('try this:\ncurl http://127.0.0.1:' + port + '/hello?name=Scott'); + } + resolve(app); + }); + }); + }); +}); diff --git a/project-skeletons/hapi/package.json b/project-skeletons/hapi/package.json new file mode 100644 index 00000000..ebacabf5 --- /dev/null +++ b/project-skeletons/hapi/package.json @@ -0,0 +1,22 @@ +{ + "name": "swagger-app", + "version": "0.0.1", + "private": true, + "description": "New Swagger API Project", + "keywords": [], + "author": "", + "license": "", + "main": "app.js", + "dependencies": { + "hapi": "^16.1.1", + "swagger-hapi": "^0.7.0" + }, + "devDependencies": { + "should": "^11.2.1", + "supertest": "^3.0.0" + }, + "scripts": { + "start": "node app.js", + "test": "swagger project test" + } +} diff --git a/project-skeletons/restify/app.js b/project-skeletons/restify/app.js new file mode 100644 index 00000000..4c4f90c4 --- /dev/null +++ b/project-skeletons/restify/app.js @@ -0,0 +1,27 @@ +'use strict'; + +var SwaggerRestify = require('swagger-restify-mw'); +var restify = require('restify'); +var app = restify.createServer(); + +// export setup Promis for testing +module.exports = new Promise(function (resolve, reject) { + + var config = { + appRoot: __dirname // required config + }; + + SwaggerRestify.create(config, function (err, swaggerRestify) { + if (err) { throw err; } + + swaggerRestify.register(app); + + var port = process.env.PORT || 10010; + app.listen(port, function () { + if (swaggerRestify.runner.swagger.paths['/hello']) { + console.log('try this:\ncurl http://127.0.0.1:' + port + '/hello?name=Scott'); + } + resolve(app); + }); + }); +}); \ No newline at end of file diff --git a/project-skeletons/restify/package.json b/project-skeletons/restify/package.json new file mode 100644 index 00000000..fb3fdb21 --- /dev/null +++ b/project-skeletons/restify/package.json @@ -0,0 +1,22 @@ +{ + "name": "swagger-app", + "version": "0.0.1", + "private": true, + "description": "New Swagger API Project", + "keywords": [], + "author": "", + "license": "", + "main": "app.js", + "dependencies": { + "restify": "^4.3.0", + "swagger-restify-mw": "^0.7.0" + }, + "devDependencies": { + "should": "^11.2.1", + "supertest": "^3.0.0" + }, + "scripts": { + "start": "node app.js", + "test": "swagger project test" + } +} diff --git a/project-skeletons/sails/.editorconfig b/project-skeletons/sails/.editorconfig new file mode 100644 index 00000000..0f099897 --- /dev/null +++ b/project-skeletons/sails/.editorconfig @@ -0,0 +1,10 @@ +# editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/project-skeletons/sails/.gitignore b/project-skeletons/sails/.gitignore new file mode 100644 index 00000000..95f2d5dd --- /dev/null +++ b/project-skeletons/sails/.gitignore @@ -0,0 +1,120 @@ +################################################ +############### .gitignore ################## +################################################ +# +# This file is only relevant if you are using git. +# +# Files which match the splat patterns below will +# be ignored by git. This keeps random crap and +# sensitive credentials from being uploaded to +# your repository. It allows you to configure your +# app for your machine without accidentally +# committing settings which will smash the local +# settings of other developers on your team. +# +# Some reasonable defaults are included below, +# but, of course, you should modify/extend/prune +# to fit your needs! +################################################ + + + + +################################################ +# Local Configuration +# +# Explicitly ignore files which contain: +# +# 1. Sensitive information you'd rather not push to +# your git repository. +# e.g., your personal API keys or passwords. +# +# 2. Environment-specific configuration +# Basically, anything that would be annoying +# to have to change every time you do a +# `git pull` +# e.g., your local development database, or +# the S3 bucket you're using for file uploads +# development. +# +################################################ + +config/local.js + + + + + +################################################ +# Dependencies +# +# When releasing a production app, you may +# consider including your node_modules and +# bower_components directory in your git repo, +# but during development, its best to exclude it, +# since different developers may be working on +# different kernels, where dependencies would +# need to be recompiled anyway. +# +# More on that here about node_modules dir: +# http://www.futurealoof.com/posts/nodemodules-in-git.html +# (credit Mikeal Rogers, @mikeal) +# +# About bower_components dir, you can see this: +# http://addyosmani.com/blog/checking-in-front-end-dependencies/ +# (credit Addy Osmani, @addyosmani) +# +################################################ + +node_modules +bower_components + + + + +################################################ +# Sails.js / Waterline / Grunt +# +# Files generated by Sails and Grunt, or related +# tasks and adapters. +################################################ +.tmp +dump.rdb + + + + + +################################################ +# Node.js / NPM +# +# Common files generated by Node, NPM, and the +# related ecosystem. +################################################ +lib-cov +*.seed +*.log +*.out +*.pid +npm-debug.log + + + + + +################################################ +# Miscellaneous +# +# Common files generated by text editors, +# operating systems, file systems, etc. +################################################ + +*~ +*# +.DS_STORE +.netbeans +nbproject +.idea +.node_history +.vscode +.DS_STORE \ No newline at end of file diff --git a/project-skeletons/sails/.sailsrc b/project-skeletons/sails/.sailsrc new file mode 100644 index 00000000..fa89f5e1 --- /dev/null +++ b/project-skeletons/sails/.sailsrc @@ -0,0 +1,5 @@ +{ + "generators": { + "modules": {} + } +} \ No newline at end of file diff --git a/project-skeletons/sails/Gruntfile.js b/project-skeletons/sails/Gruntfile.js new file mode 100644 index 00000000..212c569c --- /dev/null +++ b/project-skeletons/sails/Gruntfile.js @@ -0,0 +1,82 @@ +/** + * Gruntfile + * + * This Node script is executed when you run `grunt` or `sails lift`. + * It's purpose is to load the Grunt tasks in your project's `tasks` + * folder, and allow you to add and remove tasks as you see fit. + * For more information on how this works, check out the `README.md` + * file that was generated in your `tasks` folder. + * + * WARNING: + * Unless you know what you're doing, you shouldn't change this file. + * Check out the `tasks` directory instead. + */ + +module.exports = function(grunt) { + + + // Load the include-all library in order to require all of our grunt + // configurations and task registrations dynamically. + var includeAll; + try { + includeAll = require('include-all'); + } catch (e0) { + try { + includeAll = require('sails/node_modules/include-all'); + } catch (e1) { + console.error('Could not find `include-all` module.'); + console.error('Skipping grunt tasks...'); + console.error('To fix this, please run:'); + console.error('npm install include-all --save`'); + console.error(); + + grunt.registerTask('default', []); + return; + } + } + + + /** + * Loads Grunt configuration modules from the specified + * relative path. These modules should export a function + * that, when run, should either load/configure or register + * a Grunt task. + */ + function loadTasks(relPath) { + return includeAll({ + dirname: require('path').resolve(__dirname, relPath), + filter: /(.+)\.js$/, + excludeDirs: /^\.(git|svn)$/ + }) || {}; + } + + /** + * Invokes the function from a Grunt configuration module with + * a single argument - the `grunt` object. + */ + function invokeConfigFn(tasks) { + for (var taskName in tasks) { + if (tasks.hasOwnProperty(taskName)) { + tasks[taskName](grunt); + } + } + } + + + + // Load task functions + var taskConfigurations = loadTasks('./tasks/config'), + registerDefinitions = loadTasks('./tasks/register'); + + // (ensure that a default task exists) + if (!registerDefinitions.default) { + registerDefinitions.default = function(grunt) { + grunt.registerTask('default', []); + }; + } + + // Run task functions to configure Grunt. + invokeConfigFn(taskConfigurations); + invokeConfigFn(registerDefinitions); + +}; diff --git a/project-skeletons/sails/README.md b/project-skeletons/sails/README.md new file mode 100644 index 00000000..e95c17e1 --- /dev/null +++ b/project-skeletons/sails/README.md @@ -0,0 +1 @@ +# A [Swagger](https://www.npmjs.com/package/swagger) / [Sails](http://sailsjs.org) application diff --git a/project-skeletons/sails/api/controllers/.gitkeep b/project-skeletons/sails/api/controllers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/project-skeletons/sails/api/controllers/hello_world.js b/project-skeletons/sails/api/controllers/hello_world.js new file mode 100644 index 00000000..4023c5a4 --- /dev/null +++ b/project-skeletons/sails/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 + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode +*/ + +/* + 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({ "message": hello }); +} diff --git a/project-skeletons/sails/api/mocks/.gitkeep b/project-skeletons/sails/api/mocks/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/project-skeletons/sails/api/models/.gitkeep b/project-skeletons/sails/api/models/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/project-skeletons/sails/api/policies/sessionAuth.js b/project-skeletons/sails/api/policies/sessionAuth.js new file mode 100644 index 00000000..8f9a2647 --- /dev/null +++ b/project-skeletons/sails/api/policies/sessionAuth.js @@ -0,0 +1,21 @@ +/** + * sessionAuth + * + * @module :: Policy + * @description :: Simple policy to allow any authenticated user + * Assumes that your login action in one of your controllers sets `req.session.authenticated = true;` + * @docs :: http://sailsjs.org/#!/documentation/concepts/Policies + * + */ +module.exports = function(req, res, next) { + + // User is allowed, proceed to the next policy, + // or if this is the last policy, the controller + if (req.session.authenticated) { + return next(); + } + + // User is not allowed + // (default res.forbidden() behavior can be overridden in `config/403.js`) + return res.forbidden('You are not permitted to perform this action.'); +}; diff --git a/project-skeletons/sails/api/responses/badRequest.js b/project-skeletons/sails/api/responses/badRequest.js new file mode 100644 index 00000000..0d37825c --- /dev/null +++ b/project-skeletons/sails/api/responses/badRequest.js @@ -0,0 +1,76 @@ +/** + * 400 (Bad Request) Handler + * + * Usage: + * return res.badRequest(); + * return res.badRequest(data); + * return res.badRequest(data, 'some/specific/badRequest/view'); + * + * e.g.: + * ``` + * return res.badRequest( + * 'Please choose a valid `password` (6-12 characters)', + * 'trial/signup' + * ); + * ``` + */ + +module.exports = function badRequest(data, options) { + + // Get access to `req`, `res`, & `sails` + var req = this.req; + var res = this.res; + var sails = req._sails; + + // Set status code + res.status(400); + + // Log error to console + if (data !== undefined) { + sails.log.verbose('Sending 400 ("Bad Request") response: \n',data); + } + else sails.log.verbose('Sending 400 ("Bad Request") response'); + + // Only include errors in response if application environment + // is not set to 'production'. In production, we shouldn't + // send back any identifying information about errors. + if (sails.config.environment === 'production' && sails.config.keepResponseErrors !== true) { + data = undefined; + } + + // If the user-agent wants JSON, always respond with JSON + // If views are disabled, revert to json + if (req.wantsJSON || sails.config.hooks.views === false) { + return res.jsonx(data); + } + + // If second argument is a string, we take that to mean it refers to a view. + // If it was omitted, use an empty object (`{}`) + options = (typeof options === 'string') ? { view: options } : options || {}; + + // Attempt to prettify data for views, if it's a non-error object + var viewData = data; + if (!(viewData instanceof Error) && 'object' == typeof viewData) { + try { + viewData = require('util').inspect(data, {depth: null}); + } + catch(e) { + viewData = undefined; + } + } + + // If a view was provided in options, serve it. + // Otherwise try to guess an appropriate view, or if that doesn't + // work, just send JSON. + if (options.view) { + return res.view(options.view, { data: viewData, title: 'Bad Request' }); + } + + // If no second argument provided, try to serve the implied view, + // but fall back to sending JSON(P) if no view can be inferred. + else return res.guessView({ data: viewData, title: 'Bad Request' }, function couldNotGuessView () { + return res.jsonx(data); + }); + +}; + diff --git a/project-skeletons/sails/api/responses/created.js b/project-skeletons/sails/api/responses/created.js new file mode 100644 index 00000000..31401134 --- /dev/null +++ b/project-skeletons/sails/api/responses/created.js @@ -0,0 +1,60 @@ +/** + * 201 (CREATED) Response + * + * Usage: + * return res.created(); + * return res.created(data); + * return res.created(data, 'auth/login'); + * + * @param {Object} data + * @param {String|Object} options + * - pass string to render specified view + */ + +module.exports = function created (data, options) { + + // Get access to `req`, `res`, & `sails` + var req = this.req; + var res = this.res; + var sails = req._sails; + + sails.log.silly('res.created() :: Sending 201 ("CREATED") response'); + + // Set status code + res.status(201); + + // If appropriate, serve data as JSON(P) + // If views are disabled, revert to json + if (req.wantsJSON || sails.config.hooks.views === false) { + return res.jsonx(data); + } + + // If second argument is a string, we take that to mean it refers to a view. + // If it was omitted, use an empty object (`{}`) + options = (typeof options === 'string') ? { view: options } : options || {}; + + // Attempt to prettify data for views, if it's a non-error object + var viewData = data; + if (!(viewData instanceof Error) && 'object' == typeof viewData) { + try { + viewData = require('util').inspect(data, {depth: null}); + } + catch(e) { + viewData = undefined; + } + } + + // If a view was provided in options, serve it. + // Otherwise try to guess an appropriate view, or if that doesn't + // work, just send JSON. + if (options.view) { + return res.view(options.view, { data: viewData, title: 'Created' }); + } + + // If no second argument provided, try to serve the implied view, + // but fall back to sending JSON(P) if no view can be inferred. + else return res.guessView({ data: viewData, title: 'Created' }, function couldNotGuessView () { + return res.jsonx(data); + }); + +}; diff --git a/project-skeletons/sails/api/responses/forbidden.js b/project-skeletons/sails/api/responses/forbidden.js new file mode 100644 index 00000000..ca94852f --- /dev/null +++ b/project-skeletons/sails/api/responses/forbidden.js @@ -0,0 +1,89 @@ +/** + * 403 (Forbidden) Handler + * + * Usage: + * return res.forbidden(); + * return res.forbidden(err); + * return res.forbidden(err, 'some/specific/forbidden/view'); + * + * e.g.: + * ``` + * return res.forbidden('Access denied.'); + * ``` + */ + +module.exports = function forbidden (data, options) { + + // Get access to `req`, `res`, & `sails` + var req = this.req; + var res = this.res; + var sails = req._sails; + + // Set status code + res.status(403); + + // Log error to console + if (data !== undefined) { + sails.log.verbose('Sending 403 ("Forbidden") response: \n',data); + } + else sails.log.verbose('Sending 403 ("Forbidden") response'); + + // Only include errors in response if application environment + // is not set to 'production'. In production, we shouldn't + // send back any identifying information about errors. + if (sails.config.environment === 'production' && sails.config.keepResponseErrors !== true) { + data = undefined; + } + + // If the user-agent wants JSON, always respond with JSON + // If views are disabled, revert to json + if (req.wantsJSON || sails.config.hooks.views === false) { + return res.jsonx(data); + } + + // If second argument is a string, we take that to mean it refers to a view. + // If it was omitted, use an empty object (`{}`) + options = (typeof options === 'string') ? { view: options } : options || {}; + + // Attempt to prettify data for views, if it's a non-error object + var viewData = data; + if (!(viewData instanceof Error) && 'object' == typeof viewData) { + try { + viewData = require('util').inspect(data, {depth: null}); + } + catch(e) { + viewData = undefined; + } + } + + // If a view was provided in options, serve it. + // Otherwise try to guess an appropriate view, or if that doesn't + // work, just send JSON. + if (options.view) { + return res.view(options.view, { data: viewData, title: 'Forbidden' }); + } + + // If no second argument provided, try to serve the default view, + // but fall back to sending JSON(P) if any errors occur. + else return res.view('403', { data: viewData, title: 'Forbidden' }, function (err, html) { + + // If a view error occured, fall back to JSON(P). + if (err) { + // + // Additionally: + // • If the view was missing, ignore the error but provide a verbose log. + if (err.code === 'E_VIEW_FAILED') { + sails.log.verbose('res.forbidden() :: Could not locate view for error page (sending JSON instead). Details: ',err); + } + // Otherwise, if this was a more serious error, log to the console with the details. + else { + sails.log.warn('res.forbidden() :: When attempting to render error page view, an error occured (sending JSON instead). Details: ', err); + } + return res.jsonx(data); + } + + return res.send(html); + }); + +}; + diff --git a/project-skeletons/sails/api/responses/notFound.js b/project-skeletons/sails/api/responses/notFound.js new file mode 100644 index 00000000..8f0cd035 --- /dev/null +++ b/project-skeletons/sails/api/responses/notFound.js @@ -0,0 +1,94 @@ +/** + * 404 (Not Found) Handler + * + * Usage: + * return res.notFound(); + * return res.notFound(err); + * return res.notFound(err, 'some/specific/notfound/view'); + * + * e.g.: + * ``` + * return res.notFound(); + * ``` + * + * NOTE: + * If a request doesn't match any explicit routes (i.e. `config/routes.js`) + * or route blueprints (i.e. "shadow routes", Sails will call `res.notFound()` + * automatically. + */ + +module.exports = function notFound (data, options) { + + // Get access to `req`, `res`, & `sails` + var req = this.req; + var res = this.res; + var sails = req._sails; + + // Set status code + res.status(404); + + // Log error to console + if (data !== undefined) { + sails.log.verbose('Sending 404 ("Not Found") response: \n',data); + } + else sails.log.verbose('Sending 404 ("Not Found") response'); + + // Only include errors in response if application environment + // is not set to 'production'. In production, we shouldn't + // send back any identifying information about errors. + if (sails.config.environment === 'production' && sails.config.keepResponseErrors !== true) { + data = undefined; + } + + // If the user-agent wants JSON, always respond with JSON + // If views are disabled, revert to json + if (req.wantsJSON || sails.config.hooks.views === false) { + return res.jsonx(data); + } + + // If second argument is a string, we take that to mean it refers to a view. + // If it was omitted, use an empty object (`{}`) + options = (typeof options === 'string') ? { view: options } : options || {}; + + // Attempt to prettify data for views, if it's a non-error object + var viewData = data; + if (!(viewData instanceof Error) && 'object' == typeof viewData) { + try { + viewData = require('util').inspect(data, {depth: null}); + } + catch(e) { + viewData = undefined; + } + } + + // If a view was provided in options, serve it. + // Otherwise try to guess an appropriate view, or if that doesn't + // work, just send JSON. + if (options.view) { + return res.view(options.view, { data: viewData, title: 'Not Found' }); + } + + // If no second argument provided, try to serve the default view, + // but fall back to sending JSON(P) if any errors occur. + else return res.view('404', { data: viewData, title: 'Not Found' }, function (err, html) { + + // If a view error occured, fall back to JSON(P). + if (err) { + // + // Additionally: + // • If the view was missing, ignore the error but provide a verbose log. + if (err.code === 'E_VIEW_FAILED') { + sails.log.verbose('res.notFound() :: Could not locate view for error page (sending JSON instead). Details: ',err); + } + // Otherwise, if this was a more serious error, log to the console with the details. + else { + sails.log.warn('res.notFound() :: When attempting to render error page view, an error occured (sending JSON instead). Details: ', err); + } + return res.jsonx(data); + } + + return res.send(html); + }); + +}; + diff --git a/project-skeletons/sails/api/responses/ok.js b/project-skeletons/sails/api/responses/ok.js new file mode 100644 index 00000000..eb701447 --- /dev/null +++ b/project-skeletons/sails/api/responses/ok.js @@ -0,0 +1,60 @@ +/** + * 200 (OK) Response + * + * Usage: + * return res.ok(); + * return res.ok(data); + * return res.ok(data, 'auth/login'); + * + * @param {Object} data + * @param {String|Object} options + * - pass string to render specified view + */ + +module.exports = function sendOK (data, options) { + + // Get access to `req`, `res`, & `sails` + var req = this.req; + var res = this.res; + var sails = req._sails; + + sails.log.silly('res.ok() :: Sending 200 ("OK") response'); + + // Set status code + res.status(200); + + // If appropriate, serve data as JSON(P) + // If views are disabled, revert to json + if (req.wantsJSON || sails.config.hooks.views === false) { + return res.jsonx(data); + } + + // If second argument is a string, we take that to mean it refers to a view. + // If it was omitted, use an empty object (`{}`) + options = (typeof options === 'string') ? { view: options } : options || {}; + + // Attempt to prettify data for views, if it's a non-error object + var viewData = data; + if (!(viewData instanceof Error) && 'object' == typeof viewData) { + try { + viewData = require('util').inspect(data, {depth: null}); + } + catch(e) { + viewData = undefined; + } + } + + // If a view was provided in options, serve it. + // Otherwise try to guess an appropriate view, or if that doesn't + // work, just send JSON. + if (options.view) { + return res.view(options.view, { data: viewData, title: 'OK' }); + } + + // If no second argument provided, try to serve the implied view, + // but fall back to sending JSON(P) if no view can be inferred. + else return res.guessView({ data: viewData, title: 'OK' }, function couldNotGuessView () { + return res.jsonx(data); + }); + +}; diff --git a/project-skeletons/sails/api/responses/serverError.js b/project-skeletons/sails/api/responses/serverError.js new file mode 100644 index 00000000..537c248c --- /dev/null +++ b/project-skeletons/sails/api/responses/serverError.js @@ -0,0 +1,89 @@ +/** + * 500 (Server Error) Response + * + * Usage: + * return res.serverError(); + * return res.serverError(err); + * return res.serverError(err, 'some/specific/error/view'); + * + * NOTE: + * If something throws in a policy or controller, or an internal + * error is encountered, Sails will call `res.serverError()` + * automatically. + */ + +module.exports = function serverError (data, options) { + + // Get access to `req`, `res`, & `sails` + var req = this.req; + var res = this.res; + var sails = req._sails; + + // Set status code + res.status(500); + + // Log error to console + if (data !== undefined) { + sails.log.error('Sending 500 ("Server Error") response: \n',data); + } + else sails.log.error('Sending empty 500 ("Server Error") response'); + + // Only include errors in response if application environment + // is not set to 'production'. In production, we shouldn't + // send back any identifying information about errors. + if (sails.config.environment === 'production' && sails.config.keepResponseErrors !== true) { + data = undefined; + } + + // If the user-agent wants JSON, always respond with JSON + // If views are disabled, revert to json + if (req.wantsJSON || sails.config.hooks.views === false) { + return res.jsonx(data); + } + + // If second argument is a string, we take that to mean it refers to a view. + // If it was omitted, use an empty object (`{}`) + options = (typeof options === 'string') ? { view: options } : options || {}; + + // Attempt to prettify data for views, if it's a non-error object + var viewData = data; + if (!(viewData instanceof Error) && 'object' == typeof viewData) { + try { + viewData = require('util').inspect(data, {depth: null}); + } + catch(e) { + viewData = undefined; + } + } + + // If a view was provided in options, serve it. + // Otherwise try to guess an appropriate view, or if that doesn't + // work, just send JSON. + if (options.view) { + return res.view(options.view, { data: viewData, title: 'Server Error' }); + } + + // If no second argument provided, try to serve the default view, + // but fall back to sending JSON(P) if any errors occur. + else return res.view('500', { data: viewData, title: 'Server Error' }, function (err, html) { + + // If a view error occured, fall back to JSON(P). + if (err) { + // + // Additionally: + // • If the view was missing, ignore the error but provide a verbose log. + if (err.code === 'E_VIEW_FAILED') { + sails.log.verbose('res.serverError() :: Could not locate view for error page (sending JSON instead). Details: ',err); + } + // Otherwise, if this was a more serious error, log to the console with the details. + else { + sails.log.warn('res.serverError() :: When attempting to render error page view, an error occured (sending JSON instead). Details: ', err); + } + return res.jsonx(data); + } + + return res.send(html); + }); + +}; + diff --git a/project-skeletons/sails/api/services/.gitkeep b/project-skeletons/sails/api/services/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/project-skeletons/sails/api/swagger/swagger.yaml b/project-skeletons/sails/api/swagger/swagger.yaml new file mode 100644 index 00000000..4a093a90 --- /dev/null +++ b/project-skeletons/sails/api/swagger/swagger.yaml @@ -0,0 +1,60 @@ +swagger: "2.0" +info: + version: "0.0.1" + title: Hello World App +# during dev, should point to your local machine +host: localhost:10010 +# 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" + /swagger: + x-swagger-pipe: swagger_raw +# complex objects have schema definitions +definitions: + HelloWorldResponse: + required: + - message + properties: + message: + type: string + ErrorResponse: + required: + - message + properties: + message: + type: string diff --git a/project-skeletons/sails/app.js b/project-skeletons/sails/app.js new file mode 100644 index 00000000..6b751708 --- /dev/null +++ b/project-skeletons/sails/app.js @@ -0,0 +1,60 @@ +/** + * app.js + * + * Use `app.js` to run your app without `sails lift`. + * To start the server, run: `node app.js`. + * + * This is handy in situations where the sails CLI is not relevant or useful. + * + * For example: + * => `node app.js` + * => `forever start app.js` + * => `node debug app.js` + * => `modulus deploy` + * => `heroku scale` + * + * + * The same command-line arguments are supported, e.g.: + * `node app.js --silent --port=80 --prod` + */ + + +// Ensure we're in the project directory, so cwd-relative paths work as expected +// no matter where we actually lift from. +// > Note: This is not required in order to lift, but it is a convenient default. +process.chdir(__dirname); + +// Attempt to import `sails`. +var sails; +try { + sails = require('sails'); +} catch (e) { + console.error('To run an app using `node app.js`, you usually need to have a version of `sails` installed in the same directory as your app.'); + console.error('To do that, run `npm install sails`'); + console.error(''); + console.error('Alternatively, if you have sails installed globally (i.e. you did `npm install -g sails`), you can use `sails lift`.'); + console.error('When you run `sails lift`, your app will still use a local `./node_modules/sails` dependency if it exists,'); + console.error('but if it doesn\'t, the app will run with the global sails instead!'); + return; +} + +// --• +// Try to get `rc` dependency (for loading `.sailsrc` files). +var rc; +try { + rc = require('rc'); +} catch (e0) { + try { + rc = require('sails/node_modules/rc'); + } catch (e1) { + console.error('Could not find dependency: `rc`.'); + console.error('Your `.sailsrc` file(s) will be ignored.'); + console.error('To resolve this, run:'); + console.error('npm install rc --save'); + rc = function () { return {}; }; + } +} + + +// Start server +sails.lift(rc('sails')); diff --git a/project-skeletons/sails/assets/favicon.ico b/project-skeletons/sails/assets/favicon.ico new file mode 100644 index 00000000..0092ec9c Binary files /dev/null and b/project-skeletons/sails/assets/favicon.ico differ diff --git a/project-skeletons/sails/assets/images/.gitkeep b/project-skeletons/sails/assets/images/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/project-skeletons/sails/assets/js/dependencies/sails.io.js b/project-skeletons/sails/assets/js/dependencies/sails.io.js new file mode 100644 index 00000000..3486b9a1 --- /dev/null +++ b/project-skeletons/sails/assets/js/dependencies/sails.io.js @@ -0,0 +1,1528 @@ +/** + * To use sails.io.js in an AMD environment (e.g. with require.js), + * replace this file with the sails.io.js file from the root of: + * https://github.com/balderdashy/sails.io.js + * and download a standalone copy of socket.io-client from: + * https://github.com/socketio/socket.io-client + * then follow the instructions at: + * https://github.com/balderdashy/sails.io.js#requirejsamd-usage + */ + +// socket.io-client version 1.4.4 +// https://github.com/socketio/socket.io-client + +!function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;b="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,b.io=a()}}(function(){var a;return function b(a,c,d){function e(g,h){if(!c[g]){if(!a[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};a[g][0].call(k.exports,function(b){var c=a[g][1][b];return e(c?c:b)},k,k.exports,b,a,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g0&&(this.extraHeaders=b.extraHeaders),this.open()}function e(a){var b={};for(var c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b}var f=a("./transports"),g=a("component-emitter"),h=a("debug")("engine.io-client:socket"),i=a("indexof"),j=a("engine.io-parser"),k=a("parseuri"),l=a("parsejson"),m=a("parseqs");b.exports=d,d.priorWebsocketSuccess=!1,g(d.prototype),d.protocol=j.protocol,d.Socket=d,d.Transport=a("./transport"),d.transports=a("./transports"),d.parser=a("engine.io-parser"),d.prototype.createTransport=function(a){h('creating transport "%s"',a);var b=e(this.query);b.EIO=j.protocol,b.transport=a,this.id&&(b.sid=this.id);var c=new f[a]({agent:this.agent,hostname:this.hostname,port:this.port,secure:this.secure,path:this.path,query:b,forceJSONP:this.forceJSONP,jsonp:this.jsonp,forceBase64:this.forceBase64,enablesXDR:this.enablesXDR,timestampRequests:this.timestampRequests,timestampParam:this.timestampParam,policyPort:this.policyPort,socket:this,pfx:this.pfx,key:this.key,passphrase:this.passphrase,cert:this.cert,ca:this.ca,ciphers:this.ciphers,rejectUnauthorized:this.rejectUnauthorized,perMessageDeflate:this.perMessageDeflate,extraHeaders:this.extraHeaders});return c},d.prototype.open=function(){var a;if(this.rememberUpgrade&&d.priorWebsocketSuccess&&-1!=this.transports.indexOf("websocket"))a="websocket";else{if(0===this.transports.length){var b=this;return void setTimeout(function(){b.emit("error","No transports available")},0)}a=this.transports[0]}this.readyState="opening";try{a=this.createTransport(a)}catch(c){return this.transports.shift(),void this.open()}a.open(),this.setTransport(a)},d.prototype.setTransport=function(a){h("setting transport %s",a.name);var b=this;this.transport&&(h("clearing existing transport %s",this.transport.name),this.transport.removeAllListeners()),this.transport=a,a.on("drain",function(){b.onDrain()}).on("packet",function(a){b.onPacket(a)}).on("error",function(a){b.onError(a)}).on("close",function(){b.onClose("transport close")})},d.prototype.probe=function(a){function b(){if(m.onlyBinaryUpgrades){var b=!this.supportsBinary&&m.transport.supportsBinary;l=l||b}l||(h('probe transport "%s" opened',a),k.send([{type:"ping",data:"probe"}]),k.once("packet",function(b){if(!l)if("pong"==b.type&&"probe"==b.data){if(h('probe transport "%s" pong',a),m.upgrading=!0,m.emit("upgrading",k),!k)return;d.priorWebsocketSuccess="websocket"==k.name,h('pausing current transport "%s"',m.transport.name),m.transport.pause(function(){l||"closed"!=m.readyState&&(h("changing transport and sending upgrade packet"),j(),m.setTransport(k),k.send([{type:"upgrade"}]),m.emit("upgrade",k),k=null,m.upgrading=!1,m.flush())})}else{h('probe transport "%s" failed',a);var c=new Error("probe error");c.transport=k.name,m.emit("upgradeError",c)}}))}function c(){l||(l=!0,j(),k.close(),k=null)}function e(b){var d=new Error("probe error: "+b);d.transport=k.name,c(),h('probe transport "%s" failed because of error: %s',a,b),m.emit("upgradeError",d)}function f(){e("transport closed")}function g(){e("socket closed")}function i(a){k&&a.name!=k.name&&(h('"%s" works - aborting "%s"',a.name,k.name),c())}function j(){k.removeListener("open",b),k.removeListener("error",e),k.removeListener("close",f),m.removeListener("close",g),m.removeListener("upgrading",i)}h('probing transport "%s"',a);var k=this.createTransport(a,{probe:1}),l=!1,m=this;d.priorWebsocketSuccess=!1,k.once("open",b),k.once("error",e),k.once("close",f),this.once("close",g),this.once("upgrading",i),k.open()},d.prototype.onOpen=function(){if(h("socket open"),this.readyState="open",d.priorWebsocketSuccess="websocket"==this.transport.name,this.emit("open"),this.flush(),"open"==this.readyState&&this.upgrade&&this.transport.pause){h("starting upgrade probes");for(var a=0,b=this.upgrades.length;b>a;a++)this.probe(this.upgrades[a])}},d.prototype.onPacket=function(a){if("opening"==this.readyState||"open"==this.readyState)switch(h('socket receive: type "%s", data "%s"',a.type,a.data),this.emit("packet",a),this.emit("heartbeat"),a.type){case"open":this.onHandshake(l(a.data));break;case"pong":this.setPing(),this.emit("pong");break;case"error":var b=new Error("server error");b.code=a.data,this.onError(b);break;case"message":this.emit("data",a.data),this.emit("message",a.data)}else h('packet received with socket readyState "%s"',this.readyState)},d.prototype.onHandshake=function(a){this.emit("handshake",a),this.id=a.sid,this.transport.query.sid=a.sid,this.upgrades=this.filterUpgrades(a.upgrades),this.pingInterval=a.pingInterval,this.pingTimeout=a.pingTimeout,this.onOpen(),"closed"!=this.readyState&&(this.setPing(),this.removeListener("heartbeat",this.onHeartbeat),this.on("heartbeat",this.onHeartbeat))},d.prototype.onHeartbeat=function(a){clearTimeout(this.pingTimeoutTimer);var b=this;b.pingTimeoutTimer=setTimeout(function(){"closed"!=b.readyState&&b.onClose("ping timeout")},a||b.pingInterval+b.pingTimeout)},d.prototype.setPing=function(){var a=this;clearTimeout(a.pingIntervalTimer),a.pingIntervalTimer=setTimeout(function(){h("writing ping packet - expecting pong within %sms",a.pingTimeout),a.ping(),a.onHeartbeat(a.pingTimeout)},a.pingInterval)},d.prototype.ping=function(){var a=this;this.sendPacket("ping",function(){a.emit("ping")})},d.prototype.onDrain=function(){this.writeBuffer.splice(0,this.prevBufferLen),this.prevBufferLen=0,0===this.writeBuffer.length?this.emit("drain"):this.flush()},d.prototype.flush=function(){"closed"!=this.readyState&&this.transport.writable&&!this.upgrading&&this.writeBuffer.length&&(h("flushing %d packets in socket",this.writeBuffer.length),this.transport.send(this.writeBuffer),this.prevBufferLen=this.writeBuffer.length,this.emit("flush"))},d.prototype.write=d.prototype.send=function(a,b,c){return this.sendPacket("message",a,b,c),this},d.prototype.sendPacket=function(a,b,c,d){if("function"==typeof b&&(d=b,b=void 0),"function"==typeof c&&(d=c,c=null),"closing"!=this.readyState&&"closed"!=this.readyState){c=c||{},c.compress=!1!==c.compress;var e={type:a,data:b,options:c};this.emit("packetCreate",e),this.writeBuffer.push(e),d&&this.once("flush",d),this.flush()}},d.prototype.close=function(){function a(){d.onClose("forced close"),h("socket closing - telling transport to close"),d.transport.close()}function b(){d.removeListener("upgrade",b),d.removeListener("upgradeError",b),a()}function c(){d.once("upgrade",b),d.once("upgradeError",b)}if("opening"==this.readyState||"open"==this.readyState){this.readyState="closing";var d=this;this.writeBuffer.length?this.once("drain",function(){this.upgrading?c():a()}):this.upgrading?c():a()}return this},d.prototype.onError=function(a){h("socket error %j",a),d.priorWebsocketSuccess=!1,this.emit("error",a),this.onClose("transport error",a)},d.prototype.onClose=function(a,b){if("opening"==this.readyState||"open"==this.readyState||"closing"==this.readyState){h('socket close with reason: "%s"',a);var c=this;clearTimeout(this.pingIntervalTimer),clearTimeout(this.pingTimeoutTimer),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),this.readyState="closed",this.id=null,this.emit("close",a,b),c.writeBuffer=[],c.prevBufferLen=0}},d.prototype.filterUpgrades=function(a){for(var b=[],c=0,d=a.length;d>c;c++)~i(this.transports,a[c])&&b.push(a[c]);return b}}).call(this,"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{})},{"./transport":4,"./transports":5,"component-emitter":15,debug:17,"engine.io-parser":19,indexof:23,parsejson:26,parseqs:27,parseuri:28}],4:[function(a,b,c){function d(a){this.path=a.path,this.hostname=a.hostname,this.port=a.port,this.secure=a.secure,this.query=a.query,this.timestampParam=a.timestampParam,this.timestampRequests=a.timestampRequests,this.readyState="",this.agent=a.agent||!1,this.socket=a.socket,this.enablesXDR=a.enablesXDR,this.pfx=a.pfx,this.key=a.key,this.passphrase=a.passphrase,this.cert=a.cert,this.ca=a.ca,this.ciphers=a.ciphers,this.rejectUnauthorized=a.rejectUnauthorized,this.extraHeaders=a.extraHeaders}var e=a("engine.io-parser"),f=a("component-emitter");b.exports=d,f(d.prototype),d.prototype.onError=function(a,b){var c=new Error(a);return c.type="TransportError",c.description=b,this.emit("error",c),this},d.prototype.open=function(){return("closed"==this.readyState||""==this.readyState)&&(this.readyState="opening",this.doOpen()),this},d.prototype.close=function(){return("opening"==this.readyState||"open"==this.readyState)&&(this.doClose(),this.onClose()),this},d.prototype.send=function(a){if("open"!=this.readyState)throw new Error("Transport not open");this.write(a)},d.prototype.onOpen=function(){this.readyState="open",this.writable=!0,this.emit("open")},d.prototype.onData=function(a){var b=e.decodePacket(a,this.socket.binaryType);this.onPacket(b)},d.prototype.onPacket=function(a){this.emit("packet",a)},d.prototype.onClose=function(){this.readyState="closed",this.emit("close")}},{"component-emitter":15,"engine.io-parser":19}],5:[function(a,b,c){(function(b){function d(a){var c,d=!1,h=!1,i=!1!==a.jsonp;if(b.location){var j="https:"==location.protocol,k=location.port;k||(k=j?443:80),d=a.hostname!=location.hostname||k!=a.port,h=a.secure!=j}if(a.xdomain=d,a.xscheme=h,c=new e(a),"open"in c&&!a.forceJSONP)return new f(a);if(!i)throw new Error("JSONP disabled");return new g(a)}var e=a("xmlhttprequest-ssl"),f=a("./polling-xhr"),g=a("./polling-jsonp"),h=a("./websocket");c.polling=d,c.websocket=h}).call(this,"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{})},{"./polling-jsonp":6,"./polling-xhr":7,"./websocket":9,"xmlhttprequest-ssl":10}],6:[function(a,b,c){(function(c){function d(){}function e(a){f.call(this,a),this.query=this.query||{},h||(c.___eio||(c.___eio=[]),h=c.___eio),this.index=h.length;var b=this;h.push(function(a){b.onData(a)}),this.query.j=this.index,c.document&&c.addEventListener&&c.addEventListener("beforeunload",function(){b.script&&(b.script.onerror=d)},!1)}var f=a("./polling"),g=a("component-inherit");b.exports=e;var h,i=/\n/g,j=/\\n/g;g(e,f),e.prototype.supportsBinary=!1,e.prototype.doClose=function(){this.script&&(this.script.parentNode.removeChild(this.script),this.script=null),this.form&&(this.form.parentNode.removeChild(this.form),this.form=null,this.iframe=null),f.prototype.doClose.call(this)},e.prototype.doPoll=function(){var a=this,b=document.createElement("script");this.script&&(this.script.parentNode.removeChild(this.script),this.script=null),b.async=!0,b.src=this.uri(),b.onerror=function(b){a.onError("jsonp poll error",b)};var c=document.getElementsByTagName("script")[0];c?c.parentNode.insertBefore(b,c):(document.head||document.body).appendChild(b),this.script=b;var d="undefined"!=typeof navigator&&/gecko/i.test(navigator.userAgent);d&&setTimeout(function(){var a=document.createElement("iframe");document.body.appendChild(a),document.body.removeChild(a)},100)},e.prototype.doWrite=function(a,b){function c(){d(),b()}function d(){if(e.iframe)try{e.form.removeChild(e.iframe)}catch(a){e.onError("jsonp polling iframe removal error",a)}try{var b='