├── .gitignore ├── LICENSE-APACHE2 ├── README.md ├── example ├── example.png ├── petstore-cli └── petstore-schema.json ├── package.json └── src ├── client.js ├── columnLayout.js ├── print.js ├── printOperation.js ├── printOperations.js └── printResources.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /LICENSE-APACHE2: -------------------------------------------------------------------------------- 1 | Copyright 2014 SignalFuse 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swagger CLI Client 2 | 3 | Generates a command-line interface for any 4 | [Swagger Specification](https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md) so you can do things like: 5 | 6 | ![Example usage](https://i.imgur.com/IVhxFlE.png) 7 | 8 | ## Usage 9 | This intended to be embedded within a wrapper application which can provide it the schema object (which is generated using [fetch-swagger-schema](https://github.com/signalfx/fetch-swagger-schema)). For example, here's the petstore-cli file: 10 | 11 | ```javascript 12 | #!/usr/bin/env node 13 | 14 | var swaggerCli = require('../'), 15 | schema = require('./petstore-schema.json'); 16 | 17 | swaggerCli(schema); 18 | ``` 19 | 20 | To create a cli app for your schema, just require your schema instead of the petstore schema. 21 | 22 | ## Auth lookup strategy 23 | By default the cli will first use the `--auth` param (if defined), then it'll use the `_AUTH` (e.g., PETSTORECLI_AUTH) env variable (if defined), and finally a yaml/json file called `.` (e.g. ~/.petstore-cli which may contain "`auth: MY_TOKEN`"). 24 | 25 | ## Overriding the base path 26 | You can override your api base path via the same lookup strategy as auth keys, this is useful for testing and development. Pass in `--basePathOverride ` or defined a `_BASE_PATH` or a `basePath` key-value pair in the `.` config file. 27 | -------------------------------------------------------------------------------- /example/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/signalfx/swagger-cli-client/aafeb340c8a74f9dc7cc11d3dfdd0914608c6d3e/example/example.png -------------------------------------------------------------------------------- /example/petstore-cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var swaggerCli = require('../'), 4 | schema = require('./petstore-schema.json'); 5 | 6 | swaggerCli(schema); -------------------------------------------------------------------------------- /example/petstore-schema.json: -------------------------------------------------------------------------------- 1 | {"apiVersion":"1.0.0","swaggerVersion":"1.2","apis":[{"path":"/pet","description":"Operations about pets","apiDeclaration":{"apiVersion":"1.0.0","swaggerVersion":"1.2","basePath":"http://petstore.swagger.wordnik.com/api","resourcePath":"/pet","produces":["application/json","application/xml","text/plain","text/html"],"apis":[{"path":"/pet/{petId}","operations":[{"method":"GET","summary":"Find pet by ID","notes":"Returns a pet based on ID","type":"Pet","nickname":"getPetById","authorizations":{},"parameters":[{"name":"petId","description":"ID of pet that needs to be fetched","required":true,"type":"integer","format":"int64","paramType":"path","allowMultiple":false,"minimum":"1.0","maximum":"100000.0"}],"responseMessages":[{"code":400,"message":"Invalid ID supplied"},{"code":404,"message":"Pet not found"}]},{"method":"DELETE","summary":"Deletes a pet","notes":"","type":"void","nickname":"deletePet","authorizations":{"oauth2":[{"scope":"write:pets","description":"modify pets in your account"}]},"parameters":[{"name":"petId","description":"Pet id to delete","required":true,"type":"string","paramType":"path","allowMultiple":false}],"responseMessages":[{"code":400,"message":"Invalid pet value"}]},{"method":"PATCH","summary":"partial updates to a pet","notes":"","type":"array","items":{"$ref":"Pet"},"nickname":"partialUpdate","produces":["application/json","application/xml"],"consumes":["application/json","application/xml"],"authorizations":{"oauth2":[{"scope":"write:pets","description":"modify pets in your account"}]},"parameters":[{"name":"petId","description":"ID of pet that needs to be fetched","required":true,"type":"string","paramType":"path","allowMultiple":false},{"name":"body","description":"Pet object that needs to be added to the store","required":true,"type":"Pet","paramType":"body","allowMultiple":false}],"responseMessages":[{"code":400,"message":"Invalid tag value"}]},{"method":"POST","summary":"Updates a pet in the store with form data","notes":"","type":"void","nickname":"updatePetWithForm","consumes":["application/x-www-form-urlencoded"],"authorizations":{"oauth2":[{"scope":"write:pets","description":"modify pets in your account"}]},"parameters":[{"name":"petId","description":"ID of pet that needs to be updated","required":true,"type":"string","paramType":"path","allowMultiple":false},{"name":"name","description":"Updated name of the pet","required":false,"type":"string","paramType":"form","allowMultiple":false},{"name":"status","description":"Updated status of the pet","required":false,"type":"string","paramType":"form","allowMultiple":false}],"responseMessages":[{"code":405,"message":"Invalid input"}]}]},{"path":"/pet","operations":[{"method":"POST","summary":"Add a new pet to the store","notes":"","type":"void","nickname":"addPet","consumes":["application/json","application/xml"],"authorizations":{"oauth2":[{"scope":"write:pets","description":"modify pets in your account"}]},"parameters":[{"name":"body","description":"Pet object that needs to be added to the store","required":true,"type":"Pet","paramType":"body","allowMultiple":false}],"responseMessages":[{"code":405,"message":"Invalid input"}]},{"method":"PUT","summary":"Update an existing pet","notes":"","type":"void","nickname":"updatePet","authorizations":{},"parameters":[{"name":"body","description":"Pet object that needs to be updated in the store","required":true,"type":"Pet","paramType":"body","allowMultiple":false}],"responseMessages":[{"code":400,"message":"Invalid ID supplied"},{"code":404,"message":"Pet not found"},{"code":405,"message":"Validation exception"}]}]},{"path":"/pet/findByStatus","operations":[{"method":"GET","summary":"Finds Pets by status","notes":"Multiple status values can be provided with comma seperated strings","type":"array","items":{"$ref":"Pet"},"nickname":"findPetsByStatus","authorizations":{},"parameters":[{"name":"status","description":"Status values that need to be considered for filter","defaultValue":"available","required":true,"type":"string","paramType":"query","allowMultiple":true,"enum":["available","pending","sold"]}],"responseMessages":[{"code":400,"message":"Invalid status value"}]}]},{"path":"/pet/findByTags","operations":[{"method":"GET","summary":"Finds Pets by tags","notes":"Muliple tags can be provided with comma seperated strings. Use tag1, tag2, tag3 for testing.","type":"array","items":{"$ref":"Pet"},"nickname":"findPetsByTags","authorizations":{},"parameters":[{"name":"tags","description":"Tags to filter by","required":true,"type":"string","paramType":"query","allowMultiple":true}],"responseMessages":[{"code":400,"message":"Invalid tag value"}],"deprecated":"true"}]},{"path":"/pet/uploadImage","operations":[{"method":"POST","summary":"uploads an image","notes":"","type":"void","nickname":"uploadFile","consumes":["multipart/form-data"],"authorizations":{"oauth2":[{"scope":"write:pets","description":"modify pets in your account"},{"scope":"read:pets","description":"read your pets"}]},"parameters":[{"name":"additionalMetadata","description":"Additional data to pass to server","required":false,"type":"string","paramType":"form","allowMultiple":false},{"name":"file","description":"file to upload","required":false,"type":"File","paramType":"form","allowMultiple":false}]}]}],"models":{"Tag":{"id":"Tag","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}}},"Pet":{"id":"Pet","required":["id","name"],"properties":{"id":{"type":"integer","format":"int64","description":"unique identifier for the pet","minimum":"0.0","maximum":"100.0"},"category":{"$ref":"Category"},"name":{"type":"string"},"photoUrls":{"type":"array","items":{"type":"string"}},"tags":{"type":"array","items":{"$ref":"Tag"}},"status":{"type":"string","description":"pet status in the store","enum":["available","pending","sold"]}}},"Category":{"id":"Category","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}}}}}},{"path":"/user","description":"Operations about user","apiDeclaration":{"apiVersion":"1.0.0","swaggerVersion":"1.2","basePath":"http://petstore.swagger.wordnik.com/api","resourcePath":"/user","produces":["application/json"],"apis":[{"path":"/user/createWithArray","operations":[{"method":"POST","summary":"Creates list of users with given input array","notes":"","type":"void","nickname":"createUsersWithArrayInput","authorizations":{"oauth2":[{"scope":"test:anything","description":"anything"}]},"parameters":[{"name":"body","description":"List of user object","required":true,"type":"array","items":{"$ref":"User"},"paramType":"body","allowMultiple":false}]}]},{"path":"/user/createWithList","operations":[{"method":"POST","summary":"Creates list of users with given list input","notes":"","type":"void","nickname":"createUsersWithListInput","authorizations":{"oauth2":[{"scope":"test:anything","description":"anything"}]},"parameters":[{"name":"body","description":"List of user object","required":true,"type":"array","items":{"$ref":"User"},"paramType":"body","allowMultiple":false}]}]},{"path":"/user","operations":[{"method":"POST","summary":"Create user","notes":"This can only be done by the logged in user.","type":"void","nickname":"createUser","authorizations":{"oauth2":[{"scope":"test:anything","description":"anything"}]},"parameters":[{"name":"body","description":"Created user object","required":true,"type":"User","paramType":"body","allowMultiple":false}]}]},{"path":"/user/{username}","operations":[{"method":"PUT","summary":"Updated user","notes":"This can only be done by the logged in user.","type":"void","nickname":"updateUser","authorizations":{"oauth2":[{"scope":"test:anything","description":"anything"}]},"parameters":[{"name":"username","description":"name that need to be deleted","required":true,"type":"string","paramType":"path","allowMultiple":false},{"name":"body","description":"Updated user object","required":true,"type":"User","paramType":"body","allowMultiple":false}],"responseMessages":[{"code":400,"message":"Invalid username supplied"},{"code":404,"message":"User not found"}]},{"method":"DELETE","summary":"Delete user","notes":"This can only be done by the logged in user.","type":"void","nickname":"deleteUser","authorizations":{"oauth2":[{"scope":"test:anything","description":"anything"}]},"parameters":[{"name":"username","description":"The name that needs to be deleted","required":true,"type":"string","paramType":"path","allowMultiple":false}],"responseMessages":[{"code":400,"message":"Invalid username supplied"},{"code":404,"message":"User not found"}]},{"method":"GET","summary":"Get user by user name","notes":"","type":"User","nickname":"getUserByName","authorizations":{},"parameters":[{"name":"username","description":"The name that needs to be fetched. Use user1 for testing.","required":true,"type":"string","paramType":"path","allowMultiple":false}],"responseMessages":[{"code":400,"message":"Invalid username supplied"},{"code":404,"message":"User not found"}]}]},{"path":"/user/login","operations":[{"method":"GET","summary":"Logs user into the system","notes":"","type":"string","nickname":"loginUser","authorizations":{},"parameters":[{"name":"username","description":"The user name for login","required":true,"type":"string","paramType":"query","allowMultiple":false},{"name":"password","description":"The password for login in clear text","required":true,"type":"string","paramType":"query","allowMultiple":false}],"responseMessages":[{"code":400,"message":"Invalid username and password combination"}]}]},{"path":"/user/logout","operations":[{"method":"GET","summary":"Logs out current logged in user session","notes":"","type":"void","nickname":"logoutUser","authorizations":{},"parameters":[]}]}],"models":{"User":{"id":"User","properties":{"id":{"type":"integer","format":"int64"},"firstName":{"type":"string"},"username":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"},"phone":{"type":"string"},"userStatus":{"type":"integer","format":"int32","description":"User Status","enum":["1-registered","2-active","3-closed"]}}}}}},{"path":"/store","description":"Operations about store","apiDeclaration":{"apiVersion":"1.0.0","swaggerVersion":"1.2","basePath":"http://petstore.swagger.wordnik.com/api","resourcePath":"/store","produces":["application/json"],"apis":[{"path":"/store/order/{orderId}","operations":[{"method":"DELETE","summary":"Delete purchase order by ID","notes":"For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors","type":"void","nickname":"deleteOrder","authorizations":{"oauth2":[{"scope":"write:pets","description":"write to your pets"}]},"parameters":[{"name":"orderId","description":"ID of the order that needs to be deleted","required":true,"type":"string","paramType":"path","allowMultiple":false}],"responseMessages":[{"code":400,"message":"Invalid ID supplied"},{"code":404,"message":"Order not found"}]},{"method":"GET","summary":"Find purchase order by ID","notes":"For valid response try integer IDs with value <= 5. Anything above 5 or nonintegers will generate API errors","type":"Order","nickname":"getOrderById","authorizations":{},"parameters":[{"name":"orderId","description":"ID of pet that needs to be fetched","required":true,"type":"string","paramType":"path","allowMultiple":false}],"responseMessages":[{"code":400,"message":"Invalid ID supplied"},{"code":404,"message":"Order not found"}]}]},{"path":"/store/order","operations":[{"method":"POST","summary":"Place an order for a pet","notes":"","type":"void","nickname":"placeOrder","authorizations":{"oauth2":[{"scope":"write:pets","description":"write to your pets"}]},"parameters":[{"name":"body","description":"order placed for purchasing the pet","required":true,"type":"Order","paramType":"body","allowMultiple":false}],"responseMessages":[{"code":400,"message":"Invalid order"}]}]}],"models":{"Order":{"id":"Order","properties":{"id":{"type":"integer","format":"int64"},"petId":{"type":"integer","format":"int64"},"quantity":{"type":"integer","format":"int32"},"status":{"type":"string","description":"Order Status","enum":["placed"," approved"," delivered"]},"shipDate":{"type":"string","format":"date-time"}}}}}}],"authorizations":{"oauth2":{"type":"oauth2","scopes":[{"scope":"write:pets","description":"Modify pets in your account"},{"scope":"read:pets","description":"Read your pets"}],"grantTypes":{"implicit":{"loginEndpoint":{"url":"http://petstore.swagger.wordnik.com/api/oauth/dialog"},"tokenName":"access_token"},"authorization_code":{"tokenRequestEndpoint":{"url":"http://petstore.swagger.wordnik.com/api/oauth/requestToken","clientIdName":"client_id","clientSecretName":"client_secret"},"tokenEndpoint":{"url":"http://petstore.swagger.wordnik.com/api/oauth/token","tokenName":"auth_code"}}}}},"info":{"title":"Swagger Sample App","description":"This is a sample server Petstore server. You can find out more about Swagger \n at http://swagger.wordnik.com or on irc.freenode.net, #swagger. For this sample,\n you can use the api key \"special-key\" to test the authorization filters","termsOfServiceUrl":"http://helloreverb.com/terms/","contact":"apiteam@wordnik.com","license":"Apache 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.html"}} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swagger-cli-client", 3 | "version": "0.1.12", 4 | "description": "Generates a command-line interface for any Swagger Specification", 5 | "keywords": [], 6 | "main": "./src/client.js", 7 | "devDependencies": {}, 8 | "directories": {}, 9 | "author": { 10 | "name": "Ozan Turgut", 11 | "email": "ozanturgut@gmail.com", 12 | "url": "http://ozan.io" 13 | }, 14 | "scripts": { 15 | "test": "exit 0" 16 | }, 17 | "licenses": [ 18 | { 19 | "type": "Apache License 2.0", 20 | "url": "https://github.com/signalfx/swagger-cli/blob/master/LICENSE-APACHE2" 21 | } 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git://github.com/signalfx/swagger-cli.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/signalfx/swagger-cli/issues" 29 | }, 30 | "homepage": "https://github.com/signalfx/swagger-cli", 31 | "dependencies": { 32 | "swagger-node-client": "^0.2.2", 33 | "commander": "^2.3.0", 34 | "minimist": "^0.2.0", 35 | "colors": "^0.6.2", 36 | "cli-table": "^0.3.0", 37 | "sprintfjs": "^1.1.4", 38 | "sprintf-js": "0.0.7", 39 | "strip-ansi": "^1.0.0", 40 | "js-yaml": "^3.1.0", 41 | "swagger-validate": "^0.1.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | var clientGenerator = require('swagger-node-client'), 2 | minimist = require('minimist'), 3 | path = require('path'), 4 | colors = require('colors'), 5 | yaml = require('js-yaml'), 6 | fs = require('fs'), 7 | 8 | columnLayout = require('./columnLayout'), 9 | print = require('./print'), 10 | printOperationFactory = require('./printOperation'), 11 | printOperationsFactory = require('./printOperations'), 12 | printResources = require('./printResources'); 13 | 14 | module.exports = function(schema, argv, env){ 15 | argv = argv || process.argv; 16 | env = env || process.env; 17 | 18 | var args = minimist(argv.slice(2)); 19 | var appName = path.basename(argv[1]); 20 | var resourceName = args._.shift(); 21 | var operationName = args._.shift(); 22 | 23 | var printOperation = printOperationFactory(appName); 24 | var printOperations = printOperationsFactory(appName); 25 | 26 | var basePathOverride = args.basePathOverride || tryToGetBasePathOverride(); 27 | if(basePathOverride) { 28 | print.ln(); 29 | print.ln('Overridding base API path to %s'.red, basePathOverride); 30 | print.ln(); 31 | // Override the base path 32 | schema.apis.forEach(function(api){ 33 | api.apiDeclaration.basePath = basePathOverride; 34 | }); 35 | } 36 | 37 | var api = clientGenerator(schema); 38 | var authMethodName = api.authorization ? 'authorization' : 'auth'; 39 | var authMethod = api[authMethodName]; 40 | 41 | if(args.v) return printVersion(schema); 42 | 43 | var auth = args.auth || tryToGetAuth(); 44 | if(auth) authMethod(auth); 45 | 46 | if(resourceName){ 47 | var resource = api[resourceName]; 48 | 49 | if(!resource){ 50 | printUsage(schema, api, new Error('Unknown resource: ' + resourceName)); 51 | return; 52 | } 53 | 54 | if(operationName){ 55 | var operationHandler = resource[operationName]; 56 | if(!operationHandler){ 57 | printOperations(api, resourceName, new Error('Unknown operation: ' + operationName)); 58 | return; 59 | } 60 | 61 | printOperation(operationHandler, args); 62 | } else { 63 | printOperations(api, resourceName); 64 | } 65 | } else { 66 | printUsage(schema, api); 67 | } 68 | 69 | function printVersion(schema){ 70 | print(appName); 71 | 72 | if(schema.info && schema.info.title){ 73 | if(schema.apiVersion) print(' v%s', schema.apiVersion); 74 | print(' (%s)', schema.info.title); 75 | } 76 | print.ln(); 77 | } 78 | 79 | function tryToGetBasePathOverride(){ 80 | var appName = path.basename(argv[1]); 81 | 82 | // Attempt to get it from the env 83 | var envVar = appName.replace(/\W/g, '').toUpperCase() + '_BASE_PATH'; 84 | var auth = env[envVar]; 85 | if(auth) return auth; 86 | 87 | // Attempt to get it form the ~/. json file 88 | var homeDir = env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME']; 89 | var configFile = path.resolve(homeDir, '.' + appName); 90 | try { 91 | var config = yaml.safeLoad(fs.readFileSync(configFile)); 92 | return config.basePath; 93 | } catch(e){ 94 | // it's ok to fail here 95 | } 96 | } 97 | 98 | function tryToGetAuth(){ 99 | var appName = path.basename(argv[1]); 100 | 101 | // Attempt to get it from the env 102 | var envVar = appName.replace(/\W/g, '').toUpperCase() + '_AUTH'; 103 | var auth = env[envVar]; 104 | if(auth) return auth; 105 | 106 | // Attempt to get it form the ~/. json file 107 | var homeDir = env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME']; 108 | var configFile = path.resolve(homeDir, '.' + appName); 109 | try { 110 | var config = yaml.safeLoad(fs.readFileSync(configFile)); 111 | return config.auth; 112 | } catch(e){ 113 | // it's ok to fail here 114 | } 115 | } 116 | 117 | function printUsage(schema, api, error){ 118 | var appName = path.basename(argv[1]); 119 | 120 | print.ln('usage: %s [-v] [--auth ] []', appName); 121 | print.ln() 122 | 123 | if(error){ 124 | print.ln(colors.red(error.toString())); 125 | print.ln(); 126 | } 127 | 128 | printInfo(schema); 129 | printResources(api); 130 | } 131 | 132 | function printInfo(schema){ 133 | if(schema.info && schema.info.title){ 134 | print(schema.info.title.bold); 135 | if(schema.apiVersion) print(' v' + schema.apiVersion.bold); 136 | print.ln(); 137 | 138 | print.ln(columnLayout.wrap(schema.info.description, 80)); 139 | } 140 | 141 | print.ln(); 142 | } 143 | }; 144 | -------------------------------------------------------------------------------- /src/columnLayout.js: -------------------------------------------------------------------------------- 1 | var stripAnsi = require('strip-ansi'), 2 | sprintf = require('sprintf-js').sprintf, 3 | colors = require('colors'); 4 | 5 | function strlen(str){ 6 | if(!str) return 0; 7 | return stripAnsi(str.toString()).length; 8 | } 9 | 10 | function columnLayout(paddingWidth, columnWidthOption){ 11 | var api, 12 | options, 13 | paddingWidth, 14 | columnWidths, 15 | maxColumnWidths, 16 | columnMappers, 17 | rowMappers, 18 | maxColumnWidth; 19 | 20 | if(Array.isArray(columnWidthOption)){ 21 | maxColumnWidths = columnWidthOption; 22 | } else if(typeof columnWidthOption === 'number'){ 23 | maxColumnWidth = columnWidthOption; 24 | } 25 | 26 | if(typeof paddingWidth === 'object') { 27 | options = paddingWidth; 28 | paddingWidth = options.padding; 29 | columnWidths = options.columnWidths; 30 | maxColumnWidths = options.maxColumnWidths; 31 | maxColumnWidth = options.maxColumnWidth; 32 | columnMappers = options.columnMappers; 33 | rowMappers = options.rowMappers; 34 | } 35 | 36 | columnMappers = columnMappers || []; 37 | rowMappers = rowMappers || []; 38 | paddingWidth = paddingWidth || 0; 39 | columnWidths = columnWidths || []; 40 | maxColumnWidths = maxColumnWidths || []; 41 | maxColumnWidth = maxColumnWidth || Infinity; 42 | 43 | var padding = sprintf('%' + paddingWidth + 's', ''); 44 | var rows = []; 45 | var columns = []; 46 | 47 | function getColumnWidth(index){ 48 | var maxColumnWidth = maxColumnWidths[index] || maxColumnWidth || Infinity; 49 | var columnWidth = columnWidths[index] || 'auto'; 50 | 51 | if(columnWidth === 'auto'){ 52 | return Math.min(maxStringLength(columns[index]), maxColumnWidth); 53 | } else { 54 | return columnWidth; 55 | } 56 | } 57 | 58 | function addRow(){ 59 | var row = [].slice.call(arguments); 60 | 61 | row.forEach(function(element, index){ 62 | var column = columns[index]; 63 | if(!column) column = columns[index] = []; 64 | 65 | if(Array.isArray(element)){ 66 | column.push(element[0]); 67 | } else { 68 | column.push(element); 69 | } 70 | }); 71 | 72 | rows.push(row); 73 | } 74 | api = addRow; 75 | api.row = addRow; 76 | 77 | function addColoredRow(){ 78 | var row = [].slice.call(arguments), 79 | colorString = row.shift(0); 80 | 81 | function colorMapper(string){ 82 | colorString.split('.').forEach(function(colorName){ 83 | string = colors[colorName](string); 84 | }); 85 | 86 | return string; 87 | } 88 | 89 | rowMappers[rows.length] = colorMapper; 90 | 91 | addRow.apply(null, row); 92 | } 93 | api.colored = addColoredRow; 94 | 95 | api.toString = function(){ 96 | function offset(columnIndex){ 97 | var offset = 0, 98 | index = 0, 99 | columnWidth; 100 | 101 | for(; index <= columnIndex; index++){ 102 | offset += getColumnWidth(columnIndex); 103 | } 104 | 105 | return offset; 106 | } 107 | 108 | return rows.map(function(row, rowIndex){ 109 | var rowMapper = rowMappers[rowIndex] || identity; 110 | var elementMappers = []; 111 | 112 | var renderedRow = row.map(function(element, columnIndex){ 113 | // Unpack elements with mappers ['text', mapper] 114 | if(Array.isArray(element)){ 115 | elementMappers[columnIndex] = element[1]; 116 | element = element[0]; 117 | } else if(element === undefined){ 118 | element = ''; 119 | } 120 | 121 | var string = element.toString(), 122 | columnWidth = getColumnWidth(columnIndex); 123 | 124 | if(strlen(string) > columnWidth){ 125 | return wrap(string, columnWidth).split('\n'); 126 | } else { 127 | return [sprintf('%-' + columnWidth + 's', string)]; 128 | } 129 | }); 130 | 131 | var subrowCount = renderedRow.map(function(subrows){ 132 | return subrows.length; 133 | }).reduce(function(a, b){ 134 | return Math.max(a, b); 135 | }, 0); 136 | 137 | var index = 0; 138 | var subrows = []; 139 | 140 | for(; index < subrowCount; index++){ 141 | subrows.push(padding + renderedRow.map(function(subrows, columnIndex){ 142 | if(subrows[index] !== undefined){ 143 | var columnMapper = columnMappers[columnIndex] || identity; 144 | var elementMapper = elementMappers[columnIndex] || identity; 145 | return columnMapper(elementMapper(subrows[index])); 146 | } else { 147 | return sprintf('%' + offset(columnIndex) + 's', ''); 148 | } 149 | }).join(index? '': padding)); // only add padding to first row 150 | } 151 | 152 | return rowMapper(subrows.join('\n')); 153 | }).join('\n'); 154 | }; 155 | 156 | return addRow; 157 | }; 158 | 159 | module.exports = columnLayout; 160 | 161 | function maxStringLength(strArr){ 162 | return strArr.map(function(item){ return strlen(item); }) 163 | .reduce(function(a, b){ 164 | return Math.max(a, b); 165 | }, 0); 166 | } 167 | 168 | function identity(item){ return item; }; 169 | 170 | function wrap(string, width){ 171 | var index = 0, 172 | substr, 173 | chunks = [], 174 | chunk; 175 | 176 | string = string.replace(/\n/g, ''); 177 | var length = strlen(string); 178 | for(index; index < length; index += width){ 179 | substr = string.substr(index, width).trim(); 180 | chunks.push(sprintf('%-' + width + 's', substr)); 181 | } 182 | 183 | return chunks.join('\n'); 184 | } 185 | columnLayout.wrap = wrap; -------------------------------------------------------------------------------- /src/print.js: -------------------------------------------------------------------------------- 1 | var sprintf = require('sprintf-js').sprintf; 2 | 3 | function print(){ 4 | var result = sprintf.apply(null, arguments); 5 | process.stdout.write(result); 6 | } 7 | 8 | function println(){ 9 | print.apply(null, arguments); 10 | process.stdout.write('\n'); 11 | } 12 | 13 | module.exports = print; 14 | print.ln = println; -------------------------------------------------------------------------------- /src/printOperation.js: -------------------------------------------------------------------------------- 1 | var print = require('./print'), 2 | yaml = require('js-yaml'), 3 | path = require('path'), 4 | colors = require('colors'), 5 | validate = require('swagger-validate'), 6 | columnLayout = require('./columnLayout'); 7 | 8 | module.exports = function(appName){ 9 | function handleOperation(operationHandler, args, models){ 10 | var operation = operationHandler.operation; 11 | var models = operation.apiObject.apiDeclaration.models; 12 | 13 | if(args.h){ 14 | return printOperation(operationHandler); 15 | } 16 | 17 | Object.keys(args).forEach(function(arg){ 18 | if(arg === '_') return; 19 | try { 20 | args[arg] = yaml.safeLoad(args[arg]); 21 | } catch(e){ 22 | // It's ok to have an error here, this is just a helper 23 | // and is expected to fail sometimes 24 | } 25 | }); 26 | 27 | var data = args._.shift(); 28 | if(data !== undefined){ 29 | try { data = yaml.safeLoad(data); } catch(e) {} 30 | data = singleParamConvenienceProcessor(operation, data); 31 | } else { 32 | data = args; 33 | } 34 | 35 | var errors; 36 | try { 37 | e = operationHandler.validate(data, operation, models); 38 | } catch (e) { 39 | errors = new Error('Invalid query'); 40 | } 41 | 42 | if(errors || args.h){ 43 | printOperation(operationHandler, errors); 44 | } else { 45 | operationHandler(data).then(function(response){ 46 | print.ln(response); 47 | }).catch(function(response){ 48 | printOperation(operationHandler, response.errors || response); 49 | }); 50 | } 51 | } 52 | 53 | // Enables data to be passed directly for single param operations. 54 | function singleParamConvenienceProcessor(operation, data){ 55 | // If there are more than one params, bail 56 | var requiredParams = operation.parameters.filter(function(param){ 57 | return param.required; 58 | }); 59 | 60 | // If there are more than one required params, or if there is no required param 61 | // and there are many optional params, bail 62 | if(requiredParams.length > 1) return data; 63 | 64 | if(requiredParams.length !== 1 && operation.parameters.length !== 1) return data; 65 | 66 | var param = requiredParams[0] || operation.parameters[0]; 67 | 68 | // If the param is already defined explicitly, bail 69 | if(typeof data === 'object' && (param.name in data)) return data; 70 | 71 | var models = operation.apiObject.apiDeclaration.models; 72 | 73 | // If the data passed is is not valid for the param data type, bail 74 | var error; 75 | if(typeof data === 'object'){ 76 | try { 77 | error = validate.dataType(data, param, models); 78 | } catch(e) { 79 | error = e; 80 | } 81 | } 82 | 83 | // If the data passed is a valid param data type, bail 84 | if(!error){ 85 | var wrapper = {}; 86 | wrapper[param.name] = data; 87 | return wrapper; 88 | } else { 89 | return data; 90 | } 91 | } 92 | 93 | function printOperation(operationHandler, error){ 94 | var operation = operationHandler.operation; 95 | var resourceName = getResourceApiName(operation.apiObject); 96 | 97 | print.ln('usage: %s %s %s [--auth ] [-- ] []', appName, resourceName); 11 | print.ln() 12 | 13 | if(error){ 14 | print.ln(colors.red(error.toString())); 15 | print.ln(); 16 | } 17 | 18 | var columns = columnLayout({ 19 | padding: 3, 20 | maxColumnWidths: [20, 60] 21 | }); 22 | columns.colored('bold', 'Operation', 'Description'); 23 | 24 | Object.keys(resourceApi).forEach(function(operationName){ 25 | var operationHandler = resourceApi[operationName]; 26 | if(!(operationHandler.auth || operationHandler.authorization)) return; 27 | 28 | columns(operationName, operationHandler.operation.summary); 29 | }); 30 | 31 | print.ln(columns.toString()); 32 | }; 33 | 34 | return printOperations; 35 | }; -------------------------------------------------------------------------------- /src/printResources.js: -------------------------------------------------------------------------------- 1 | var columnLayout = require('./columnLayout'), 2 | print = require('./print'); 3 | 4 | function printResources(api){ 5 | var columns = columnLayout({ 6 | padding: 3, 7 | maxColumnWidths: [20, 60] 8 | }); 9 | 10 | columns.colored('bold', 'Resource', 'Description'); 11 | 12 | Object.keys(api).forEach(function(resourceName){ 13 | var resource = api[resourceName]; 14 | if(!(resource.auth || resource.authorization)) return; 15 | 16 | var description = getResourceDescription(api[resourceName]); 17 | columns(resourceName, description); 18 | }); 19 | 20 | print.ln(columns.toString()); 21 | } 22 | 23 | module.exports = printResources; 24 | 25 | 26 | function getResourceDescription(resourceApi){ 27 | var apiObject; 28 | 29 | Object.keys(resourceApi).some(function(operationHandlerName){ 30 | var operationHandler = resourceApi[operationHandlerName]; 31 | 32 | if(operationHandler.auth || operationHandler.authorization){ 33 | apiObject = operationHandler.operation.apiObject; 34 | return true; 35 | } 36 | }); 37 | 38 | // Since api declaration resource paths are chosen preferentially for naming 39 | // of the api (instead of apiObject paths), we'll use the description for it 40 | // if it has a resource path 41 | if(apiObject.apiDeclaration.resourcePath){ 42 | return apiObject.resourceObject.description; 43 | } else { 44 | return apiObject.description; 45 | } 46 | } 47 | --------------------------------------------------------------------------------