Permalink
Browse files

Working commit.

  • Loading branch information...
1 parent 7224857 commit 8ba0c42a32cc4010df19a1c0684d06ba740c1dd6 @hgarcia committed Dec 31, 2012
Showing with 284 additions and 102 deletions.
  1. +79 −1 README.md
  2. +1 −0 index.js
  3. +88 −46 lib/curl-transport.js
  4. +4 −3 package.json
  5. +112 −52 tests/curl-transport.tests.js
View
80 README.md
@@ -1,4 +1,82 @@
curling
=======
-A node wrapper for curl with a very simple api.
+A node wrapper for curl with a very simple api.
+
+## API
+
+Exports only two methods `connect` and `run`
+
+### run(command, cb)
+
+You shouldn't have to use `run` but is in there just as a convenience or if you need to do something crazy that is not possible to do via the connection object.
+
+It pretty much allow you to send any command with any option to curl. It used internally by connect and the connection object.
+
+Ex:
+
+ var curl = require('curling');
+ curl.run("--GET http://www.cnn.com", function (err, result) {
+ console.log(result.payload); // should output the html for the cnn page to console.
+ console.log(result.stats); // should output some of the statistics on downloading the page
+ });
+
+### connect(options)
+
+This method takes an `options` object with general options for the connection like the username and password in case you are using basic auth to connect to an end point.
+
+ var options = {username: "hernan", password: "secret"};
+
+At the moment those are the only options that connect knows how to deal with.
+
+It returns a connection object.
+
+### Connection object API
+
+It has five methods, each corresponding to an HTTP verb. They all have the same signature: `method(url, options, cb)`. The method are:
+
+ head();
+ get();
+ post();
+ put();
+ del(); //DELETE
+
+The callback takes two parameters `cb(err, result)` where the result is a `curl-result` object.
+
+
+### curl-result
+
+It has two properties, `payload` and `stats`. The payload contains the data returned in the stdout by curl while the stats is an object that parse as the content of stderr.
+
+Stats is of the form:
+
+ {
+ totalSize: 0,
+ received: 0,
+ xferd: 0,
+ averageDownloadSpeed: 0,
+ averageUploadSpeed: 0,
+ totalTime: 0,
+ timeSpent: 0,
+ timeLeft: 0,
+ currentSpeed: '0 Kb'
+ }
+
+The time properties are converted to milliseconds, the rest of the properties are of type `Number` in the same units as returned by curl except for the `currentSpeed` that is a string with the unit at the end (again as returned by curl).
+
+## Passing options.
+
+There are two ways to pass options and data to a request.
+You can use the `options` for the `connect` method and this options will be used in each and every request.
+You can also use the `options` object in each of the verb methods.
+
+The `options` object is actually a hash where the keys should be the name of the flag in a curl command, for example to set an Accept header and pass some data you could pass an `options` as the following.
+
+ var options = {
+ header: "Accept: text/html",
+ data: ["name=hernan", "last=garcia"]
+ };
+
+The keys in an `options` object can be one of the following types, String, Array or null.
+
+Strings are useful when you only need to set a single value, arrays are used to pass multiple values, like data, header and so. Null is a specialcase and is used for empty flags, like `--false`.
View
1 index.js
@@ -1 +1,2 @@
exports.connect = require('./lib/curl-transport').connect;
+exports.run = require('./lib/curl-transport').run;
View
134 lib/curl-transport.js
@@ -1,77 +1,119 @@
"use strict";
var exec = require('child_process').exec;
-var qs = require('querystring');
-exports.connect = function (connOptions) {
- function run(command, cb) {
- exec(command, function (error, stdout, stderr) {
- try {
- if (connOptions.json) {
- cb(null, JSON.parse(stdout));
- } else {
- cb(null, stdout);
- }
- } catch (e) {
- cb(e, null);
- }
- })
- .on('error', function (err) {
- cb(err, null);
- });
+function parseStats(stderr) {
+ var lines = [];
+ var items = [];
+ var stats = {};
+ var propMap = [
+ {index: 1, name: 'totalSize', val: 0},
+ {index: 3, name: 'received', val: 0},
+ {index: 5, name: 'xferd', val: 0},
+ {index: 6, name: 'averageDownloadSpeed', val: 0},
+ {index: 7, name: 'averageUploadSpeed', val: 0},
+ {index: 8, name: 'totalTime', val: "--:--:--"},
+ {index: 9, name: 'timeSpent', val: "--:--:--"},
+ {index: 10, name: 'timeLeft', val: "--:--:--"},
+ {index: 11, name: 'currentSpeed', val: 0}
+ ];
+
+ if (stderr) {
+ try {
+ lines = stderr.split("\r");
+ items = lines[2].replace("\n", "").split(" ").filter(function (item) { return item; });
+ } catch (e) {}
}
- function getCredentials(connOptions) {
- if (connOptions.username && connOptions.password) {
- return " --user " + connOptions.username + ":" + connOptions.password;
+ function getValue(prop) {
+ if (items.length > prop.index) {
+ return items[prop.index];
}
- return "";
+ return prop.val;
}
- function getData(options) {
- if (options.data) {
- if (options.data.toLowerCase) {
- return " --data \"" + options.data + "\"";
- } else {
- return " --data '" + JSON.stringify(options.data) + "'";
- }
+ propMap.forEach(function (prop) {
+ stats[prop.name] = getValue(prop);
+ });
+
+ return stats;
+}
+
+
+exports.run = function (command, cb) {
+ exec("curl " + command, function (error, stdout, stderr) {
+ cb(null, {payload: stdout, stats: parseStats(stderr)});
+ })
+ .on('error', function (err) {
+ cb(err, null);
+ });
+};
+
+exports.connect = function (connOptions) {
+
+ var ctx = this;
+
+ function getEmptyOption(option) {
+ if (option.length === 1) {
+ return " -" + option;
}
- return "";
+ return " --" + option;
}
- function getHeaders(options) {
- if (options.headers) {
- var headers = "";
- Object.keys(options.headers).forEach(function (key) {
- headers += " -H " + key + ":\"" + options.headers[key] + "\"";
+ function getStringOption(option, value) {
+ return getEmptyOption(option) + " \"" + value + "\"";
+ }
+
+ function processOptions(options) {
+ var tmp = "";
+ if (options) {
+ Object.keys(options).forEach(function (option) {
+ var values = options[option];
+ if (!values) {
+ tmp += getEmptyOption(option)
+ } else if (values.toLowerCase) {
+ tmp += getStringOption(option, values);
+ } else if (Array.isArray(values)) {
+ values.forEach(function (value) {
+ tmp += getStringOption(option, value);
+ });
+ }
});
- return headers;
}
- return "";
+ return tmp;
}
- function getUrlForCommand(url, connOptions) {
- return getCredentials(connOptions) + " " + url;
+ function getOptions(options) {
+ return processOptions(connOptions) + processOptions(options);
+ }
+
+ function getCommand(url, options) {
+ return url + getOptions(options);
}
return {
+ head: function (url, options, cb) {
+ var command = "--HEAD " + getCommand(url, options);
+ ctx.run(command, cb);
+ },
+
get: function (url, options, cb) {
- var command = "curl --GET" + getUrlForCommand(url, connOptions) + getHeaders(options) + getData(options);
- run(command, cb);
+ var command = "--GET " + getCommand(url, options);
+ ctx.run(command, cb);
},
post: function (url, options, cb) {
- var command = "curl" + getUrlForCommand(url, connOptions) + getHeaders(options) + getData(options);
- run(command, cb);
+ var command = getCommand(url, options);
+ ctx.run(command, cb);
},
put: function (url, options, cb) {
- var command = "curl --request PUT" + getUrlForCommand(url, connOptions) + getHeaders(options) + getData(options);
- run(command, cb);
+ var command = "--request PUT " + getCommand(url, options);
+ ctx.run(command, cb);
},
del: function (url, options, cb) {
- var command = "curl --include --request DELETE" + getUrlForCommand(url, connOptions) + getHeaders(options);
- run(command, cb, true);
+ var command = "--include --request DELETE" + getCommand(url, options);
+ ctx.run(command, cb, true);
}
};
};
View
7 package.json
@@ -4,15 +4,16 @@
"description": "A simple wrapper around curl with a easy to use interface.",
"main": "index.js",
"scripts": {
- "test": "mocha tests/*.tests.js"
- },
+ "test": "mocha tests/*.tests.js -t 15000 -R spec"
+ },
"repository": {
"type": "git",
"url": "git://github.com/hgarcia/curling.git"
},
"devDependencies": {
"mocha": "1.7.4",
- "should": "1.2.1"
+ "should": "1.2.1",
+ "sinon": "1.5.2"
},
"keywords": [
"curl",
View
164 tests/curl-transport.tests.js
@@ -1,65 +1,125 @@
var should = require("should");
var curl = require('../index');
+var sinon = require('sinon');
-function catchCreateException(options) {
- return function () {
- try {
- bitbucket.createClient(options);
- } catch (e) {
- return;
- }
- throw new Error('No error throw by class with options: ' + JSON.stringify(options));
- };
-}
-
-function catchCreateRepoException(options, cb, done) {
- return function (done) {
- try {
- bitbucket.createClient(goodOptions)
- .getRepository(options, cb);
- } catch (e) {
- return done();
- }
- done(Error('No error throw by class with options: ' + JSON.stringify(options)));
- };
-}
-
-describe('BitBucket', function() {
- describe('.createClient()', function () {
- it('should throw when no options passed', catchCreateException());
- it('should throw when empty options passed', catchCreateException({}));
- it('should throw when options with no username or password', catchCreateException({username: '', password: ''}));
- it('should create a client', function () {
- var client = bitbucket.createClient(goodOptions);
- client.username.should.eql(goodOptions.username);
- client.password.should.eql(goodOptions.password);
+describe('curl', function() {
+ describe('.run(command, cb)', function () {
+ it('should run a command', function (done) {
+ curl.run("--GET http://www.google.ca", function (err, result) {
+ result.payload.should.be.ok;
+ done(err);
+ });
});
});
- describe(".repositories()", function () {
- it('should return a list of repositories for the user', function (done) {
- var client = bitbucket.createClient(goodOptions);
- client.repositories(function (err, repositories) {
- Array.isArray(repositories).should.be.ok;
- done();
- });
+ describe('.connect(null)', function () {
+ it('should return a connection object', function () {
+ var connection = curl.connect(null);
+ connection.should.have.property('head');
+ connection.should.have.property('get');
+ connection.should.have.property('post');
+ connection.should.have.property('put');
+ connection.should.have.property('del');
+ });
+ });
+
+ describe('.connect(options)', function () {
+ it('should return a connection object', function () {
+ var connection = curl.connect({user: 'hernan:secret'});
+ connection.should.have.property('head');
+ connection.should.have.property('get');
+ connection.should.have.property('post');
+ connection.should.have.property('put');
+ connection.should.have.property('del');
});
});
- describe(".getRepository()", function () {
- it('should throw if no options passed', catchCreateRepoException(null, null));
- it('should throw if empty options passed', catchCreateRepoException({}, null));
- it('should throw if options with no username and password passed', catchCreateRepoException({slug: '', owner: ''}, null));
- it('should throw if no cb passed', catchCreateRepoException({slug: 'app', owner: 'inline'}, null));
- it('should return a Repository object', function (done) {
- var client = bitbucket.createClient(goodOptions);
- client.getRepository(repoData, function (err, repository) {
- repository.should.have.property('provider');
- repository.should.have.property('resourceURI');
- repository.resourceURI.should.eql('/1.0/repositories/inline/app');
- done();
+
+ describe('.connect({header: [], user: ""})', function () {
+ var connection;
+ beforeEach(function () {
+ connection = curl.connect({header: ["Accept: text/html"], user: 'hernan:secret'});
+ sinon.stub(curl, 'run', function (command, cb) { cb(null, true); });
+ });
+
+ afterEach(function () {
+ connection = null;
+ curl.run.restore();
+ });
+
+ describe('.head(url, null, cb)', function () {
+ it('should combine options in command', function (done) {
+ connection.head('http://www.dynamicprogrammer.com', null, function (err, result) {
+ curl.run.calledOnce.should.be.ok;
+ curl.run.args[0][0].should.eql("--HEAD http://www.dynamicprogrammer.com --header \"Accept: text/html\" --user \"hernan:secret\"");
+ done(err);
+ });
});
});
});
-});
+
+ describe('.connect(options)', function () {
+ var connection;
+ beforeEach(function () {
+ connection = curl.connect({user: 'hernan:secret'});
+ sinon.stub(curl, 'run', function (command, cb) { cb(null, true); });
+ });
+
+ afterEach(function () {
+ connection = null;
+ curl.run.restore();
+ });
+
+ describe('.head(url, null, cb)', function () {
+ it('should build a proper command', function (done) {
+ connection.head('http://www.dynamicprogrammer.com', null, function (err, result) {
+ curl.run.calledOnce.should.be.ok;
+ curl.run.args[0][0].should.eql("--HEAD http://www.dynamicprogrammer.com --user \"hernan:secret\"");
+ done(err);
+ });
+ });
+ });
+
+ describe('.head(url, {header: string}, cb)', function () {
+ it('should build a proper command with some args', function (done) {
+ connection.head('http://www.dynamicprogrammer.com', {header: "Accept: text/html"}, function (err, result) {
+ curl.run.calledOnce.should.be.ok;
+ curl.run.args[0][0].should.eql("--HEAD http://www.dynamicprogrammer.com --user \"hernan:secret\" --header \"Accept: text/html\"");
+ done(err);
+ });
+ });
+ });
+
+
+ describe('.head(url, {header: string, fail: null}, cb)', function () {
+ it('should build a proper command with some args', function (done) {
+ connection.head('http://www.dynamicprogrammer.com', {header: "Accept: text/html", fail: null}, function (err, result) {
+ curl.run.calledOnce.should.be.ok;
+ curl.run.args[0][0].should.eql("--HEAD http://www.dynamicprogrammer.com --user \"hernan:secret\" --header \"Accept: text/html\" --fail");
+ done(err);
+ });
+ });
+ });
+
+ describe('.head(url, {header: []}, cb)', function () {
+ it('should build a proper command with some args', function (done) {
+ connection.head('http://www.dynamicprogrammer.com', {header: ["Accept: text/html", "X-Custom: \"Hernan\""]}, function (err, result) {
+ curl.run.calledOnce.should.be.ok;
+ curl.run.args[0][0].should.eql("--HEAD http://www.dynamicprogrammer.com --user \"hernan:secret\" --header \"Accept: text/html\" --header \"X-Custom: \"Hernan\"\"");
+ done(err);
+ });
+ });
+ });
+
+ describe('.head(url, {H: []}, cb)', function () {
+ it('should build a proper command with some args', function (done) {
+ connection.head('http://www.dynamicprogrammer.com', {H: ["Accept: text/html", "X-Custom: \"Hernan\""]}, function (err, result) {
+ curl.run.calledOnce.should.be.ok;
+ curl.run.args[0][0].should.eql("--HEAD http://www.dynamicprogrammer.com --user \"hernan:secret\" -H \"Accept: text/html\" -H \"X-Custom: \"Hernan\"\"");
+ done(err);
+ });
+ });
+ });
+ });
+})

0 comments on commit 8ba0c42

Please sign in to comment.