├── .gitignore ├── apiblueprint-group.mustache ├── Screenshot.png ├── package.json ├── .travis.yml ├── apiblueprint-action.mustache ├── LICENSE ├── README.md ├── Cakefile ├── APIBlueprintGenerator.js └── APIBlueprintGenerator.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | build/ 4 | -------------------------------------------------------------------------------- /apiblueprint-group.mustache: -------------------------------------------------------------------------------- 1 | {{{ level }}} Group {{{ name }}} 2 | -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apiaryio/Paw-APIBlueprintGenerator/HEAD/Screenshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Paw-APIBlueprintGenerator", 3 | "version": "1.0.1", 4 | "devDependencies": { 5 | "coffee-script": "latest", 6 | "mkdirp": "~0.5.0", 7 | "ncp": "~1.0.1" 8 | }, 9 | "dependencies": { 10 | "mustache": "~2.2.1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:apiaryio/Paw-APIBlueprintGenerator.git" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_install: 5 | - npm install coffee-script 6 | before_script: 7 | - ./node_modules/.bin/cake archive 8 | script: 9 | - ./node_modules/.bin/cake test 10 | deploy: 11 | provider: releases 12 | api_key: 13 | secure: Gro51LKeOVYrDg+nGHb2glhbGDP9SbfXryvY6Ql3F5Edfx3kTN5NAVsAN60FTrYreL6czv05jvzT0Y8+PTM9jUMNQbK3Dblln5DNaD5SOs4UCewfOe2z7+Ksi9Zk/iJHAku61j5qwnJSR1EEekVUM8XF79qs5sfIwV3/RKU6IbQ= 14 | file: build/APIBlueprintGenerator.zip 15 | skip_cleanup: true 16 | on: 17 | tags: true 18 | repo: apiaryio/Paw-APIBlueprintGenerator 19 | -------------------------------------------------------------------------------- /apiblueprint-action.mustache: -------------------------------------------------------------------------------- 1 | {{#name}} 2 | {{{ level }}} {{{ name }}} [{{{ method }}} {{{ path }}}] 3 | {{/name}} 4 | {{^name}} 5 | {{{ level }}} {{{ method }}} {{{ path }}} 6 | {{/name}} 7 | 8 | {{#request.description?}} 9 | {{{request.description}}} 10 | {{/request.description?}} 11 | {{#request}} 12 | + Request{{#contentType}} ({{{contentType}}}){{/contentType}} 13 | 14 | {{#headers?}} 15 | + Headers 16 | 17 | {{#headers}} 18 | {{{key}}}: {{{value}}} 19 | {{/headers}} 20 | 21 | {{/headers?}} 22 | {{#body?}} 23 | + Body 24 | 25 | {{/body?}} 26 | {{{body}}} 27 | 28 | {{/request}} 29 | {{#response}} 30 | + Response {{{statusCode}}}{{#contentType}} ({{{contentType}}}){{/contentType}} 31 | 32 | {{#headers?}} 33 | + Headers 34 | 35 | {{#headers}} 36 | {{{key}}}: {{{value}}} 37 | {{/headers}} 38 | 39 | {{/headers?}} 40 | {{#body?}} 41 | + Body 42 | 43 | {{/body?}} 44 | {{{body}}} 45 | {{/response}} 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014 Apiary Inc. . 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Paw API Blueprint Generator Extension 2 | ==================================== 3 | 4 | [![Build Status](https://travis-ci.org/apiaryio/Paw-APIBlueprintGenerator.svg?branch=master?style=flat)](https://travis-ci.org/apiaryio/Paw-APIBlueprintGenerator) 5 | 6 | Paw extension providing support to export API Blueprint as a code generator. 7 | 8 | ![](Screenshot.png) 9 | 10 | ### Installation 11 | 12 | The [Paw extension](http://luckymarmot.com/paw/extensions/APIBlueprintGenerator) can be installed with one simple step by clicking [here](paw://extensions/io.apiary.PawExtensions.APIBlueprintGenerator?install). 13 | 14 | #### Development Instructions 15 | 16 | If you would like to develop the extension, you have follow these steps to get a development environment setup. 17 | 18 | ##### Clone 19 | 20 | First of all, clone this repository in any convenient location (e.g `~/Desktop`). 21 | 22 | ```bash 23 | $ git clone https://github.com/apiaryio/Paw-APIBlueprintGenerator 24 | ``` 25 | 26 | ##### Prerequisites 27 | 28 | Install `npm` if needed (e.g. below using [Homebrew](http://brew.sh/)): 29 | 30 | ```bash 31 | $ brew install npm 32 | ``` 33 | 34 | Install dependencies using `npm`: 35 | 36 | ```bash 37 | $ npm install 38 | ``` 39 | 40 | ##### Development Installation 41 | 42 | During development, build the `.js` script using: 43 | 44 | ```bash 45 | $ cake build 46 | ``` 47 | 48 | To install into the Paw Extension directory: 49 | 50 | ```bash 51 | $ cake install 52 | ``` 53 | 54 | Alternatively, use the `watch` command to automatically build and install when a file has been modified: 55 | 56 | ```bash 57 | $ cake watch 58 | ``` 59 | 60 | ### License 61 | 62 | MIT License. See the [LICENSE](LICENSE) file. 63 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | {spawn} = require 'child_process' 2 | {ncp} = require 'ncp' 3 | mkdirp = require 'mkdirp' 4 | fs = require 'fs' 5 | 6 | file = 'APIBlueprintGenerator.coffee' 7 | identifier = 'io.apiary.PawExtensions.APIBlueprintGenerator' 8 | 9 | extensions_dir = "#{ process.env.HOME }/Library/Containers/com.luckymarmot.Paw/Data/Library/Application Support/com.luckymarmot.Paw/Extensions/" 10 | build_root_dir = "build" 11 | build_dir = "#{ build_root_dir }/#{ identifier }" 12 | 13 | # compile CoffeeScript 14 | build_coffee = (callback) -> 15 | coffee = spawn 'coffee', ['-c', '-o', build_dir, file] 16 | coffee.stderr.on 'data', (data) -> 17 | process.stderr.write data.toString() 18 | coffee.stdout.on 'data', (data) -> 19 | process.stdout.write data.toString() 20 | coffee.on 'exit', (code) -> 21 | if code is 0 22 | callback?() 23 | else 24 | console.error "Build failed with error: #{ code }" 25 | 26 | # copy files to build directory 27 | build_copy = () -> 28 | fs.writeFileSync "#{ build_dir }/README.md", fs.readFileSync("./README.md") 29 | fs.writeFileSync "#{ build_dir }/LICENSE", fs.readFileSync("./LICENSE") 30 | fs.writeFileSync "#{ build_dir }/apiblueprint-action.mustache", fs.readFileSync("./apiblueprint-action.mustache") 31 | fs.writeFileSync "#{ build_dir }/apiblueprint-group.mustache", fs.readFileSync("./apiblueprint-group.mustache") 32 | fs.writeFileSync "#{ build_dir }/mustache.js", fs.readFileSync("./node_modules/mustache/mustache.js") 33 | 34 | # build: build CoffeeScript and copy files to build directory 35 | build = (callback) -> 36 | # mkdir build dir 37 | mkdirp build_dir, (err) -> 38 | if err 39 | console.error err 40 | else 41 | build_coffee () -> 42 | build_copy() 43 | callback?() 44 | 45 | # install: copy files to Extensions directory 46 | install = (callback) -> 47 | ncp build_dir, "#{ extensions_dir }/#{ identifier }", (err) -> 48 | if err 49 | console.error err 50 | else 51 | callback?() 52 | 53 | # archive: create a zip archive from the build 54 | archive = (callback) -> 55 | zip_file = "#{ identifier.split('.').pop() }.zip" 56 | 57 | # go to build dir 58 | process.chdir "#{ build_root_dir }/" 59 | 60 | # delete any previous zip 61 | if fs.existsSync zip_file 62 | fs.unlinkSync zip_file 63 | 64 | # zip 65 | zip = spawn 'zip', ["-r", zip_file, "#{ identifier }/"] 66 | zip.stderr.on 'data', (data) -> 67 | process.stderr.write data.toString() 68 | zip.stdout.on 'data', (data) -> 69 | process.stdout.write data.toString() 70 | zip.on 'exit', (code) -> 71 | if code is 0 72 | callback?() 73 | else 74 | console.error "zip returned with error code: #{ code }" 75 | 76 | task 'build', -> 77 | build() 78 | 79 | task 'test', -> 80 | build () -> 81 | # no test to run 82 | 83 | task 'install', -> 84 | build () -> 85 | install() 86 | 87 | task 'archive', -> 88 | build () -> 89 | archive() 90 | 91 | task 'watch', -> 92 | # find all files in directory 93 | for filename in fs.readdirSync '.' 94 | # only watch non-hidden files 95 | if not filename.match(/^\./) and fs.lstatSync("./#{ filename }").isFile() 96 | fs.watchFile "./#{ filename }", {persistent:true, interval:500}, (_event, _filename) -> 97 | # when a file is changed, build and install 98 | build () -> 99 | install() 100 | -------------------------------------------------------------------------------- /APIBlueprintGenerator.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.9.3 2 | (function() { 3 | var APIBlueprintGenerator; 4 | 5 | require("mustache.js"); 6 | 7 | APIBlueprintGenerator = function() { 8 | this.response = function(exchange) { 9 | var body, body_indentation, has_body, has_headers, headers, is_json, key, ref, value; 10 | if (!exchange) { 11 | return null; 12 | } 13 | headers = []; 14 | is_json = false; 15 | ref = exchange.responseHeaders; 16 | for (key in ref) { 17 | value = ref[key]; 18 | if (key === 'Content-Type' || key === 'Connection' || key === 'Date' || key === 'Via' || key === 'Server' || key === 'Content-Length') { 19 | is_json = key === 'Content-Type' && value.search(/(json)/i) > -1; 20 | continue; 21 | } 22 | headers.push({ 23 | key: key, 24 | value: value 25 | }); 26 | } 27 | has_headers = headers.length > 0; 28 | body = exchange.responseBody; 29 | has_body = body.length > 0; 30 | if (has_body) { 31 | if (is_json) { 32 | body = JSON.stringify(JSON.parse(body), null, 4); 33 | } 34 | body_indentation = ' '; 35 | if (has_headers) { 36 | body_indentation += ' '; 37 | } 38 | body = body.replace(/^/gm, body_indentation); 39 | } 40 | return { 41 | statusCode: exchange.responseStatusCode, 42 | contentType: exchange.responseHeaders['Content-Type'], 43 | "headers?": has_headers, 44 | headers: headers, 45 | "body?": has_headers && has_body, 46 | body: body 47 | }; 48 | }; 49 | this.request = function(paw_request) { 50 | var body, body_indentation, has_body, has_headers, headers, is_json, key, ref, value; 51 | headers = []; 52 | is_json = false; 53 | ref = paw_request.headers; 54 | for (key in ref) { 55 | value = ref[key]; 56 | if (key === 'Content-Type') { 57 | is_json = value.search(/(json)/i) > -1; 58 | continue; 59 | } 60 | headers.push({ 61 | key: key, 62 | value: value 63 | }); 64 | } 65 | has_headers = headers.length > 0; 66 | body = paw_request.body; 67 | has_body = body.length > 0; 68 | if (has_body) { 69 | if (is_json) { 70 | body = JSON.stringify(JSON.parse(body), null, 4); 71 | } 72 | body_indentation = ' '; 73 | if (has_headers) { 74 | body_indentation += ' '; 75 | } 76 | body = body.replace(/^/gm, body_indentation); 77 | } 78 | if (has_headers || has_body || paw_request.headers['Content-Type']) { 79 | return { 80 | "headers?": has_headers, 81 | headers: headers, 82 | contentType: paw_request.headers['Content-Type'], 83 | "body?": has_headers && has_body, 84 | body: body 85 | }; 86 | } 87 | }; 88 | this.path = function(url) { 89 | var path; 90 | path = url.replace(/^https?:\/\/[^\/]+/i, ''); 91 | if (!path) { 92 | path = '/'; 93 | } 94 | return path; 95 | }; 96 | this.generate = function(context) { 97 | var paw_request, template, url; 98 | paw_request = context.getCurrentRequest(); 99 | url = paw_request.url; 100 | template = readFile("apiblueprint.mustache"); 101 | return Mustache.render(template, { 102 | method: paw_request.method, 103 | path: this.path(url), 104 | request: this.request(paw_request), 105 | response: this.response(paw_request.getLastExchange()) 106 | }); 107 | }; 108 | }; 109 | 110 | APIBlueprintGenerator.identifier = "io.apiary.PawExtensions.APIBlueprintGenerator"; 111 | 112 | APIBlueprintGenerator.title = "API Blueprint Generator"; 113 | 114 | APIBlueprintGenerator.fileExtension = "md"; 115 | 116 | registerCodeGenerator(APIBlueprintGenerator); 117 | 118 | }).call(this); 119 | -------------------------------------------------------------------------------- /APIBlueprintGenerator.coffee: -------------------------------------------------------------------------------- 1 | # in API v0.2.0 and below (Paw 2.2.2 and below), require had no return value 2 | ((root) -> 3 | if root.bundle?.minApiVersion('0.2.0') 4 | root.Mustache = require("./mustache") 5 | else 6 | require("mustache.js") 7 | )(this) 8 | 9 | APIBlueprintGenerator = -> 10 | templateAction = readFile("apiblueprint-action.mustache") 11 | templateGroup = readFile("apiblueprint-group.mustache") 12 | 13 | # Generate a response dictionary for the mustache context from a paw HTTPExchange 14 | # 15 | # @param [HTTPExchange] exchange The paw HTTP exchange for the response 16 | # 17 | # @return [Object] The template context object 18 | # 19 | @response = (exchange) -> 20 | if !exchange 21 | return null 22 | 23 | headers = [] 24 | is_json = false 25 | for key, value of exchange.responseHeaders 26 | if key in ['Content-Type', 'Connection', 'Date', 'Via', 'Server', 'Content-Length'] 27 | if key == 'Content-Type' 28 | is_json = value.search(/(json)/i) > -1 29 | continue 30 | 31 | headers.push({ key: key, value: value }) 32 | has_headers = (headers.length > 0) 33 | 34 | body = exchange.responseBody 35 | has_body = body.length > 0 36 | if has_body 37 | if is_json 38 | body = JSON.stringify(JSON.parse(body), null, 4) 39 | body_indentation = ' ' 40 | if has_headers 41 | body_indentation += ' ' 42 | body = body.replace(/^/gm, body_indentation) 43 | 44 | return { 45 | statusCode: exchange.responseStatusCode, 46 | contentType: exchange.responseHeaders['Content-Type'], 47 | "headers?": has_headers, 48 | headers: headers 49 | "body?": has_headers && has_body, 50 | body: body, 51 | } 52 | 53 | # Generate a request dictionary for the mustache context from a paw Request 54 | # 55 | # @param [Request] exchange The paw HTTP request 56 | # 57 | # @return [Object] The template context object 58 | # 59 | @request = (paw_request) -> 60 | headers = [] 61 | is_json = false 62 | for key, value of paw_request.headers 63 | if key in ['Content-Type'] 64 | is_json = (value.search(/(json)/i) > -1) 65 | continue 66 | 67 | headers.push({ key: key, value: value }) 68 | has_headers = (headers.length > 0) 69 | 70 | body = paw_request.body 71 | has_body = body.length > 0 72 | if has_body 73 | if is_json 74 | body = JSON.stringify(JSON.parse(body), null, 4) 75 | body_indentation = ' ' 76 | if has_headers 77 | body_indentation += ' ' 78 | body = body.replace(/^/gm, body_indentation) 79 | 80 | description = paw_request.description 81 | has_description = description && description.length > 0 82 | 83 | if has_headers || has_body || paw_request.headers['Content-Type'] 84 | return { 85 | "headers?": has_headers, 86 | headers: headers, 87 | contentType: paw_request.headers['Content-Type'], 88 | "body?": has_headers && has_body, 89 | body: body, 90 | "description?": has_description, 91 | description: description, 92 | } 93 | 94 | # Get a path from a URL 95 | # 96 | # @param [String] url The given URL 97 | # 98 | # @return [String] The path from the URL 99 | @path = (url) -> 100 | path = url.replace(/^https?:\/\/[^\/]+/i, '') 101 | if !path 102 | path = '/' 103 | 104 | path 105 | 106 | @renderPawItems = (items, level = 1) -> 107 | sections = for item in items 108 | @renderPawItem(item, level) 109 | 110 | sections.join("\n") 111 | 112 | @renderPawItem = (item, level = 1) -> 113 | if item.toString().match(/^RequestGroup/) 114 | @renderPawGroup(item, level) 115 | else 116 | @renderPawRequest(item, level) 117 | 118 | @renderPawGroup = (paw_group, level = 1) -> 119 | sections = [] 120 | 121 | sections.push Mustache.render(templateGroup, 122 | level: "#".repeat(level), 123 | name: paw_group.name 124 | ) 125 | 126 | children = paw_group.getChildren().sort (a, b) -> a.order - b.order 127 | 128 | sections = sections.concat @renderPawItems(children, level + 1) 129 | 130 | sections.join("\n") 131 | 132 | @renderPawRequest = (paw_request, level = 1) -> 133 | url = paw_request.url 134 | Mustache.render(templateAction, 135 | level: "#".repeat(level), 136 | name: paw_request.name.replace(/[\[\]\(\)]/g, ''), 137 | method: paw_request.method, 138 | path: @path(url), 139 | request: @request(paw_request), 140 | response: @response(paw_request.getLastExchange()), 141 | ) 142 | 143 | @generate = (context, requests, options) -> 144 | if context.runtimeInfo.task == 'exportAllRequests' 145 | @renderPawItems(context.getRootRequestTreeItems()) 146 | else 147 | @renderPawItems(requests) 148 | 149 | return 150 | 151 | APIBlueprintGenerator.identifier = "io.apiary.PawExtensions.APIBlueprintGenerator" 152 | APIBlueprintGenerator.title = "API Blueprint Generator" 153 | APIBlueprintGenerator.fileExtension = "md" 154 | 155 | registerCodeGenerator APIBlueprintGenerator 156 | --------------------------------------------------------------------------------