Skip to content

Commit

Permalink
Merge https://github.com/disfated/restler into 2.x.x-disfated
Browse files Browse the repository at this point in the history
Conflicts:
	package.json
  • Loading branch information
ayoung committed Jan 17, 2012
2 parents 18fa9a9 + 00c4011 commit 2704ec0
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 74 deletions.
6 changes: 3 additions & 3 deletions README.md
Expand Up @@ -24,8 +24,8 @@ Features
* Transparently handle SSL (just specify https in the URL)
* Deals with basic auth for you, just provide username and password options
* Simple service wrapper that allows you to easily put together REST API libraries
* Transparently handle content-encoded responses (gzip, deflate)
* Transparently handle different content charsets via `iconv`
* Transparently handle content-encoded responses (gzip, deflate) (requires node 0.6+)
* Transparently handle different content charsets via [iconv](https://github.com/bnoordhuis/node-iconv) (if available)


API
Expand All @@ -47,7 +47,7 @@ Basic method to make a request of any type. The function returns a RestRequest o

#### members

* `abort()` Cancels request. `abort` event is emitted. `aborted` property is set to `true`. only `complete` and `error` event should.
* `abort([error])` Cancels request. `abort` event is emitted. `request.aborted` is set to `true`. If non-falsy `error` is passed, then `error` will be additionaly emitted (with `error` passed as a param and `error.type` is set to `"abort"`). Otherwise only `complete` event will raise.
* `aborted` Determines if request was aborted.


Expand Down
94 changes: 67 additions & 27 deletions lib/restler.js
@@ -1,20 +1,29 @@
var sys = require('util'),
http = require('http'),
https = require('https'),
url = require('url'),
qs = require('querystring'),
multipart = require('./multipartform'),
zlib = require('zlib'),
Iconv = require('iconv').Iconv;

var sys = require('util');
var http = require('http');
var https = require('https');
var url = require('url');
var qs = require('querystring');
var multipart = require('./multipartform');
var zlib = null;
var Iconv = null;

try {
zlib = require('zlib');
} catch (err) {}

try {
Iconv = require('iconv').Iconv;
} catch (err) {}

function mixin(target, source) {
source = source || {};
Object.keys(source).forEach(function(key) {
target[key] = source[key];
});

return target;
}

function Request(uri, options) {
this.url = url.parse(uri);
this.options = options;
Expand All @@ -23,6 +32,10 @@ function Request(uri, options) {
'User-Agent': 'Restler for node.js',
'Host': this.url.host
};

if (zlib) {
this.headers['Accept-Encoding'] = 'gzip, deflate';
}

mixin(this.headers, options.headers || {});

Expand Down Expand Up @@ -97,7 +110,7 @@ mixin(Request.prototype, {
_responseHandler: function(response) {
var self = this;

if (this._isRedirect(response) && this.options.followRedirects == true) {
if (this._isRedirect(response) && this.options.followRedirects) {
try {
var location = url.resolve(this.url, response.headers['location']);
Request.call(this, location, this.options); // reusing request object to handle recursive redirects
Expand Down Expand Up @@ -145,18 +158,22 @@ mixin(Request.prototype, {
}
},
_iconv: function(body, response) {
var charset = response.headers['content-type'];
if (!charset) return body;
charset = charset.match(/\bcharset=(.+)(?:;|$)/i);
if (!charset) return body;
charset = charset[1].trim().toUpperCase();
if (charset == 'UTF-8') return body;
try {
var iconv = new Iconv(charset, 'UTF-8//TRANSLIT//IGNORE');
return iconv.convert(body);
} catch (err) {
return body;
if (Iconv) {
var charset = response.headers['content-type'];
if (charset) {
charset = /\bcharset=(.+)(?:;|$)/i.exec(charset);
if (charset) {
charset = charset[1].trim().toUpperCase();
if (charset != 'UTF-8') {
try {
var iconv = new Iconv(charset, 'UTF-8//TRANSLIT//IGNORE');
return iconv.convert(body);
} catch (err) {}
}
}
}
}
return body;
},
_encode: function(body, response, callback) {
var self = this;
Expand Down Expand Up @@ -190,7 +207,9 @@ mixin(Request.prototype, {
this.request.on('response', function(response) {
self._responseHandler(response);
}).on('error', function(err) {
self._fireError(err, null);
if (!self.aborted) {
self._fireError(err, null);
}
});
},
run: function() {
Expand All @@ -209,10 +228,31 @@ mixin(Request.prototype, {

return this;
},
abort: function() {
this.aborted = true;
this.request.abort();
this.emit('abort');
abort: function(err) {
var self = this;

if (err) {
if (typeof err == 'string') {
err = new Error(err);
} else if (!(err instanceof Error)) {
err = new Error('AbortError');
}
err.type = 'abort';
} else {
err = null;
}

self.request.on('close', function() {
if (err) {
self._fireError(err, null);
} else {
self.emit('complete', null, null);
}
});

self.aborted = true;
self.request.abort();
self.emit('abort', err);
return this;
}
});
Expand Down
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -8,11 +8,11 @@
"main" : "./lib/restler",
"engines": { "node": ">= 0.6.x" },
"dependencies": {
"iconv": ">=1.0.0"
},
"devDependencies": {
"nodeunit": ">=0.5.0",
"xml2js" : ">=0.1.0",
"yaml" : ">=0.2.0"
"yaml" : ">=0.2.0",
"iconv": ">=1.0.0"
}
}
1 change: 1 addition & 0 deletions test/all.js
@@ -1,4 +1,5 @@

require('./restler'); // debug
var nodeunit = require('nodeunit');
var reporter = nodeunit.reporters['default'];
process.chdir(__dirname);
Expand Down
133 changes: 91 additions & 42 deletions test/restler.js
Expand Up @@ -2,10 +2,19 @@
var rest = require('../lib/restler');
var http = require('http');
var sys = require('util');
var zlib = require('zlib');
var path = require('path');
var fs = require('fs');
var crypto = require('crypto');
var zlib = null;
var Iconv = null;

try {
zlib = require('zlib');
} catch (err) {}

try {
Iconv = require('iconv').Iconv;
} catch (err) {}

var p = sys.inspect;

Expand Down Expand Up @@ -289,6 +298,12 @@ function dataResponse(request, response) {
response.writeHead(200, { 'content-type': 'application/yaml' });
response.end('{Чебурашка');
break;
case '/abort':
setTimeout(function() {
response.writeHead(200);
response.end('not aborted');
}, 100);
break;
default:
response.writeHead(404);
response.end();
Expand Down Expand Up @@ -322,31 +337,43 @@ module.exports['Deserialization'] = {
},

'Should gunzip': function(test) {
rest.get(host + '/gzip').on('complete', function(data) {
test.re(data, /^(compressed data){10}$/, 'returned: ' + p(data));
if (zlib) {
rest.get(host + '/gzip').on('complete', function(data) {
test.re(data, /^(compressed data){10}$/, 'returned: ' + p(data));
test.done();
});
} else {
test.done();
})
}
},

'Should inflate': function(test) {
rest.get(host + '/deflate').on('complete', function(data) {
test.re(data, /^(compressed data){10}$/, 'returned: ' + p(data));
if (zlib) {
rest.get(host + '/deflate').on('complete', function(data) {
test.re(data, /^(compressed data){10}$/, 'returned: ' + p(data));
test.done();
})
} else {
test.done();
})
}
},

'Should decode and parse': function(test) {
rest.get(host + '/truth').on('complete', function(data) {
try {
with (data) {
var result = what + (is + the + answer + to + life + the + universe + and + everything).length;
if (zlib) {
rest.get(host + '/truth').on('complete', function(data) {
try {
with (data) {
var result = what + (is + the + answer + to + life + the + universe + and + everything).length;
}
test.equal(result, 42, 'returned: ' + p(data));
} catch (err) {
test.ok(false, 'returned: ' + p(data));
}
test.equal(result, 42, 'returned: ' + p(data));
} catch (err) {
test.ok(false, 'returned: ' + p(data));
}
test.done();
})
} else {
test.done();
})
}
},

'Should decode as buffer': function(test) {
Expand Down Expand Up @@ -410,22 +437,39 @@ module.exports['Deserialization'] = {
});
},

'Should correctly abort request': function(test) {
test.expect(5);
rest.get(host + '/json').on('complete', function(data) {
test.ok(data instanceof Error, 'should be error, got: ' + p(data));
test.equal(this.aborted, true, 'should aborted, got: ' + p(this.aborted));
'Should correctly soft-abort request': function(test) {
test.expect(4);
rest.get(host + '/abort').on('complete', function(data) {
test.equal(data, null, 'data should be null');
test.equal(this.aborted, true, 'should be aborted');
test.done();
}).on('error', function(data) {
}).on('error', function(err) {
test.ok(false, 'should not emit error event');
}).on('abort', function(err) {
test.equal(err, null, 'err should be null');
test.equal(this.aborted, true, 'should be aborted');
}).on('success', function() {
test.ok(false, 'should not emit success event');
}).on('fail', function() {
test.ok(false, 'should not emit fail event');
}).abort();
},

'Should correctly hard-abort request': function(test) {
test.expect(4);
rest.get(host + '/abort').on('complete', function(data) {
test.ok(data instanceof Error, 'should be error, got: ' + p(data));
test.equal(this.aborted, true, 'should aborted, got: ' + p(this.aborted));
}).on('abort', function() {
test.equal(this.aborted, true, 'should aborted, got: ' + p(this.aborted));
}).on('success', function() {
test.ok(false, 'should not have got here');
}).on('fail', function() {
test.ok(false, 'should not have got here');
}).abort();
test.equal(this.aborted, true, 'should be aborted');
test.done();
}).on('error', function(err) {
test.ok(err instanceof Error, 'should be error, got: ' + p(err));
}).on('abort', function(err) {
test.equal(this.aborted, true, 'should be aborted');
}).on('success', function() {
test.ok(false, 'should not emit success event');
}).on('fail', function() {
test.ok(false, 'should not emit fail event');
}).abort(true);
},

'Should correctly handle malformed JSON': function(test) {
Expand Down Expand Up @@ -487,9 +531,13 @@ function charsetsResponse(request, response) {
var charset = request.url.substr(1);
response.writeHead(200, {
'content-type': 'text/plain; charset=' + charset,
'content-encoding': 'gzip'
'content-encoding': zlib ? 'gzip' : ''
});
fs.createReadStream(path.join(__dirname, charsetsDir, charset)).pipe(zlib.createGzip()).pipe(response);
var stream = fs.createReadStream(path.join(__dirname, charsetsDir, charset));
if (zlib) {
stream = stream.pipe(zlib.createGzip());
}
stream.pipe(response);
}

module.exports['Charsets'] = {
Expand All @@ -511,15 +559,17 @@ var charsetCases = {
'gbk' : '01329db97a6a202ecffaf95d4f77a18d'
};

for (var charset in charsetCases) {
(function(charset, hash) {
module.exports['Charsets']['Should correctly convert charset ' + charset] = function(test) {
rest.get(host + '/' + charset).on('complete', function(data) {
test.equal(md5(Buffer(data, 'utf8')), hash, 'hashes should match');
test.done();
});
};
})(charset, charsetCases[charset]);
if (Iconv) {
for (var charset in charsetCases) {
(function(charset, hash) {
module.exports['Charsets']['Should correctly convert charset ' + charset] = function(test) {
rest.get(host + '/' + charset).on('complete', function(data) {
test.equal(md5(Buffer(data, 'utf8')), hash, 'hashes should match');
test.done();
});
};
})(charset, charsetCases[charset]);
}
}


Expand Down Expand Up @@ -600,4 +650,3 @@ module.exports['Content-Length'] = {
}

};

0 comments on commit 2704ec0

Please sign in to comment.