├── .gitignore ├── package.json ├── .travis.yml ├── LICENSE ├── README.md ├── curl.mustache ├── Cakefile └── cURLCodeGenerator.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | build/ 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Paw-cURLCodeGenerator", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "coffee-script": "latest", 6 | "mkdirp": "~0.5.0", 7 | "ncp": "~1.0.1" 8 | }, 9 | "dependencies": { 10 | "Base64": "^1.0.0", 11 | "mustache": "~0.8.2" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com:luckymarmot/Paw-cURLCodeGenerator.git" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.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: 0yzJucOYgtgVHKDJ0f2knmnygeo5BakxRdiIQkyckXkoJ1q0O8Uqs0Q+uM+lF2s+NIuFteeBegwGtV0uFVqHqfRea2BZvLTu3TiN4rDXMCYoWjJ9rGJ9QKUom1o9j/ZH1vVs3asEFjaIB4GNBQ1hXgLbLQufbpJLwVIvKSVmoPs= 14 | file: build/cURLCodeGenerator.zip 15 | skip_cleanup: true 16 | on: 17 | tags: true 18 | all_branches: true 19 | repo: luckymarmot/Paw-cURLCodeGenerator 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Paw Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/luckymarmot/Paw-cURLCodeGenerator.svg?branch=master)](https://travis-ci.org/luckymarmot/Paw-cURLCodeGenerator) 2 | 3 | # cURL Code Generator (Paw Extension) 4 | 5 | A [Paw Extension](http://luckymarmot.com/paw/extensions/) that generates [cURL](http://en.wikipedia.org/wiki/CURL) command line code. 6 | 7 | ## Installation 8 | 9 | Easily install this Paw Extension: [Install cURL Code Generator](http://luckymarmot.com/paw/extensions/cURLCodeGenerator) 10 | 11 | ## Development 12 | 13 | ### Build & Install 14 | 15 | ```shell 16 | npm install 17 | cake build 18 | cake install 19 | ``` 20 | 21 | ### Watch 22 | 23 | During development, watch for changes: 24 | 25 | ```shell 26 | cake watch 27 | ``` 28 | 29 | ## License 30 | 31 | This Paw Extension is released under the [MIT License](LICENSE). Feel free to fork, and modify! 32 | 33 | Copyright © 2014 Paw Inc. 34 | 35 | ## Contributors 36 | 37 | See [Contributors](https://github.com/luckymarmot/Paw-cURLCodeGenerator/graphs/contributors). 38 | 39 | ## Credits 40 | 41 | * [Mustache.js](https://github.com/janl/mustache.js/), also released under the MIT License 42 | * [URI.js](http://medialize.github.io/URI.js/), also released under the MIT License 43 | -------------------------------------------------------------------------------- /curl.mustache: -------------------------------------------------------------------------------- 1 | {{#request.name}}## {{{request.name}}} 2 | {{/request.name}}{{#request.cURLDescription}} 3 | {{{request.cURLDescription}}} 4 | {{/request.cURLDescription}}curl {{#request_is_head}}--head {{/request_is_head}}{{#specify_method}}-X "{{{request.method}}}" {{/specify_method}}"{{{request.url}}}" \ 5 | {{#headers.has_headers}} 6 | {{#headers.header_list}} 7 | -H '{{{header_name}}}: {{{header_value}}}' \ 8 | {{/headers.header_list}} 9 | {{/headers.has_headers}} 10 | {{#headers.auth}} 11 | -u '{{{headers.auth.username}}}:{{{headers.auth.password}}}' {{#headers.auth.isDigest}}--digest{{/headers.auth.isDigest}}\ 12 | {{/headers.auth}} 13 | {{! ----- }} 14 | {{#body.has_url_encoded_body}} 15 | {{#body.url_encoded_body}} 16 | --data-urlencode "{{{name}}}={{{value}}}" \ 17 | {{/body.url_encoded_body}} 18 | {{/body.has_url_encoded_body}} 19 | {{! ----- }} 20 | {{#body.has_raw_body_with_tabs_or_new_lines}} 21 | -d $'{{{body.raw_body}}}' \ 22 | {{/body.has_raw_body_with_tabs_or_new_lines}} 23 | {{! ----- }} 24 | {{#body.has_raw_body_without_tabs_or_new_lines}} 25 | -d "{{{body.raw_body}}}" \ 26 | {{/body.has_raw_body_without_tabs_or_new_lines}} 27 | {{! ----- }} 28 | {{#body.has_long_body}} 29 | -d "{set your body string}" \ 30 | {{/body.has_long_body}} 31 | {{! ----- }} 32 | {{#body.has_multipart_body}} 33 | {{#body.multipart_body}} 34 | -F "{{{name}}}={{{value}}}" \ 35 | {{/body.multipart_body}} 36 | {{/body.has_multipart_body}} 37 | {{! ----- }} 38 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | {spawn} = require 'child_process' 2 | {ncp} = require 'ncp' 3 | mkdirp = require 'mkdirp' 4 | fs = require 'fs' 5 | 6 | file = 'cURLCodeGenerator.coffee' 7 | identifier = 'com.luckymarmot.PawExtensions.cURLCodeGenerator' 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 }/curl.mustache", fs.readFileSync("./curl.mustache") 31 | fs.writeFileSync "#{ build_dir }/mustache.js", fs.readFileSync("./node_modules/mustache/mustache.js") 32 | fs.writeFileSync "#{ build_dir }/Base64.js", fs.readFileSync("./node_modules/Base64/base64.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 | -------------------------------------------------------------------------------- /cURLCodeGenerator.coffee: -------------------------------------------------------------------------------- 1 | # in API v0.2.0 and below (Paw 2.2.2 and below), require had no return value 2 | ((root) -> root.Mustache = require("mustache.js") or root.Mustache)(this) 3 | ((root) -> root.Base64 = require("Base64.js") or root.Base64)(this) 4 | 5 | addslashes = (str) -> 6 | ("#{str}").replace(/[\\"]/g, '\\$&') 7 | 8 | addslashes_single_quotes = (str) -> 9 | ("#{str}").replace(/\\/g, '\\$&').replace(/'/g, "\\'") 10 | 11 | cURLCodeGenerator = -> 12 | self = this 13 | 14 | @headers = (request) -> 15 | headers = request.headers 16 | 17 | auth = null 18 | if headers['Authorization'] 19 | auth = @auth request, headers['Authorization'] 20 | if auth 21 | delete headers['Authorization'] 22 | 23 | return { 24 | "has_headers": Object.keys(headers).length > 0 25 | "header_list": ({ 26 | "header_name": addslashes_single_quotes header_name 27 | "header_value": addslashes_single_quotes header_value 28 | } for header_name, header_value of headers) 29 | "auth": auth 30 | } 31 | 32 | @auth = (request, authHeader) -> 33 | if self.options.useHeader 34 | return null 35 | match = authHeader.match(/([^\s]+)\s(.*)/) || [] 36 | scheme = match[1] || null 37 | params = match[2] || null 38 | 39 | 40 | if scheme == 'Basic' 41 | try 42 | decoded = Base64.atob(params) 43 | catch err 44 | return null 45 | userpass = decoded.match(/([^:]*):?(.*)/) 46 | return { 47 | "username": addslashes_single_quotes(userpass[1] || ''), 48 | "password": addslashes_single_quotes(userpass[2] || ''), 49 | } 50 | 51 | digestDS = request.getHeaderByName('Authorization', true) 52 | if digestDS and digestDS.length == 1 and digestDS.getComponentAtIndex(0).type == 'com.luckymarmot.PawExtensions.DigestAuthDynamicValue' 53 | digestDV = digestDS.getComponentAtIndex(0) 54 | DVuser = digestDV.username 55 | username = '' 56 | if typeof DVuser == 'object' 57 | username = DVuser.getEvaluatedString() 58 | else 59 | username = DVuser 60 | DVpass = digestDV.password 61 | password = '' 62 | if typeof DVpass == 'object' 63 | password = DVpass.getEvaluatedString() 64 | else 65 | password = DVpass 66 | 67 | return { 68 | "isDigest": true, 69 | "username": addslashes_single_quotes(username), 70 | "password": addslashes_single_quotes(password) 71 | } 72 | 73 | return null 74 | 75 | 76 | @body = (request) -> 77 | url_encoded_body = request.urlEncodedBody 78 | if url_encoded_body 79 | return { 80 | "has_url_encoded_body":true 81 | "url_encoded_body": ({ 82 | "name": addslashes name 83 | "value": addslashes value 84 | } for name, value of url_encoded_body) 85 | } 86 | 87 | multipart_body = request.multipartBody 88 | if multipart_body 89 | return { 90 | "has_multipart_body":true 91 | "multipart_body": ({ 92 | "name": addslashes name 93 | "value": addslashes value 94 | } for name, value of multipart_body) 95 | } 96 | 97 | json_body = request.jsonBody 98 | if json_body? 99 | return { 100 | "has_raw_body_with_tabs_or_new_lines": true 101 | "has_raw_body_without_tabs_or_new_lines": false 102 | "raw_body": addslashes_single_quotes(JSON.stringify(json_body, null, 2)) 103 | } 104 | 105 | raw_body = request.body 106 | if raw_body 107 | if raw_body.length < 5000 108 | has_tabs_or_new_lines = (null != /\r|\n|\t/.exec(raw_body)) 109 | return { 110 | "has_raw_body_with_tabs_or_new_lines":has_tabs_or_new_lines 111 | "has_raw_body_without_tabs_or_new_lines":!has_tabs_or_new_lines 112 | "raw_body": if has_tabs_or_new_lines then addslashes_single_quotes raw_body else addslashes raw_body 113 | } 114 | else 115 | return { 116 | "has_long_body":true 117 | } 118 | 119 | @strip_last_backslash = (string) -> 120 | # Remove the last backslash on the last non-empty line 121 | # We do that programatically as it's difficult to know the "last line" 122 | # in Mustache templates 123 | 124 | lines = string.split("\n") 125 | for i in [(lines.length - 1)..0] 126 | lines[i] = lines[i].replace(/\s*\\\s*$/, "") 127 | if not lines[i].match(/^\s*$/) 128 | break 129 | lines.join("\n") 130 | 131 | @generateRequest = (request) -> 132 | view = 133 | "request": request 134 | "request_is_head": request.method == "HEAD" 135 | "specify_method": request.method != "GET" && request.method != "HEAD" 136 | "headers": @headers request 137 | "body": @body request 138 | 139 | # Make multi-line description. 140 | if view.request.description 141 | view.request.cURLDescription = view.request.description.split('\n').map((line, index) -> 142 | return "# #{line}" 143 | ).join('\n') 144 | else 145 | view.request.cURLDescription = '' 146 | 147 | template = readFile "curl.mustache" 148 | rendered_code = Mustache.render template, view 149 | return @strip_last_backslash rendered_code 150 | 151 | @generate = (context, requests, options) -> 152 | self.options = (options || {}).inputs || {} 153 | 154 | curls = requests.map((request) -> 155 | return self.generateRequest(request) 156 | ) 157 | 158 | return curls.join('\n') 159 | 160 | return 161 | 162 | 163 | cURLCodeGenerator.identifier = 164 | "com.luckymarmot.PawExtensions.cURLCodeGenerator" 165 | cURLCodeGenerator.title = 166 | "cURL" 167 | cURLCodeGenerator.fileExtension = "sh" 168 | cURLCodeGenerator.languageHighlighter = "bash" 169 | cURLCodeGenerator.inputs = [ 170 | new InputField("useHeader", "do not use -u option", "Checkbox", {defaultValue: false}) 171 | ] 172 | 173 | registerCodeGenerator cURLCodeGenerator 174 | --------------------------------------------------------------------------------