Permalink
Browse files

FIRST!

  • Loading branch information...
0 parents commit 9ee58841e9ef5fb52be020ac13df51b97c000e04 @kriskowal committed Oct 28, 2010
Showing with 426 additions and 0 deletions.
  1. +19 −0 LICENSE
  2. +43 −0 README
  3. +29 −0 package.json
  4. +256 −0 q-http.js
  5. +36 −0 test/basic.js
  6. +43 −0 test/keep-alive.js
@@ -0,0 +1,19 @@
+
+Copyright 2009, 2010 Kristopher Michael Kowal. All rights reserved.
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
@@ -0,0 +1,43 @@
+
+Provides a Q promise API for HTTP requests and responses. This should
+resemble JSGI and its hypothetical inverse, but I haven't pored
+through the specification to ensure this.
+
+The API
+
+ Server(respond(request):Response*)
+ listen(port:Number):Undefined*
+ stop():Undefined*
+
+ Client(port:Number, host:String)
+ request(request:Request):Response*
+
+ request(request:Request):Response*
+
+ Request:Object
+
+ method || "GET"
+ host:String
+ port:Number
+ headers:Object
+ body*
+ forEach(write(String*)):Undefined*
+ connection:NodeSocketConnection
+
+ Response:Object
+
+ status:Number
+ headers:Object
+ body*
+ forEach(write(String*)):Undefined*
+ onClose:Function?
+
+Conventions
+
+ `*` indicates that a value may be a promise
+ `?` indicates optional
+ `||` indicates a default
+
+Copyright 2009, 2010 Kristopher Michael Kowal
+MIT License (enclosed)
+
@@ -0,0 +1,29 @@
+{
+ "name": "q-http",
+ "description": "Q promise based HTTP client and server interface",
+ "version": "0.0.0",
+ "homepage": "http://github.com/kriskowal/q-http/",
+ "author": "Kris Kowal <kris@cixar.com> (http://github.com/kriskowal/)",
+ "bugs": {
+ "mail": "kris@cixar.com",
+ "web": "http://github.com/kriskowal/q-http/issues"
+ },
+ "licenses": [
+ {
+ "type": "MIT",
+ "url": "http://github.com/kriskowal/q-http/raw/master/LICENSE"
+ }
+ ],
+ "main": "q-http.js",
+ "dependencies": {
+ "q-util": "0.0.0",
+ "q-io": "0.0.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "http://github.com/kriskowal/q-http.git"
+ },
+ "engines": {
+ "node": ">=0.2.0"
+ }
+}
@@ -0,0 +1,256 @@
+
+/**
+ * A promise-based Q-JSGI server and client API.
+ * @module
+ */
+
+/*whatsupdoc*/
+
+var HTTP = require("http"); // node
+var URL = require("url"); // node
+var Q = require("q-util");
+var IO = require("q-io");
+
+/**
+ * @param {respond(request Request)} respond a JSGI responder function that
+ * receives a Request object as its argument. The JSGI responder promises to
+ * return an object of the form `{status, headers, body}`. The status and
+ * headers must be fully resolved, but the body may be a promise for an object
+ * with a `forEach(write(chunk String))` method, albeit an array of strings.
+ * The `forEach` method may promise to resolve when all chunks have been
+ * written.
+ * @returns a Node Server object.
+ */
+exports.Server = function (respond) {
+ var self = Object.create(exports.Server.prototype);
+
+ var server = HTTP.createServer(function (_request, _response) {
+ var request = exports.Request(_request);
+
+ var closed = Q.defer();
+ _request.connection.on("close", function (error, value) {
+ if (error)
+ closed.reject(error);
+ else
+ closed.resolve(value);
+ });
+
+ Q.when(respond(request), function (response) {
+ _response.writeHead(response.status, response.headers);
+
+ if (response.onClose)
+ Q.when(closed, response.onClose);
+
+ return Q.when(response.body, function (body) {
+ if (
+ Array.isArray(body) &&
+ body.length === 1 &&
+ typeof body[0] === "string"
+ ) {
+ _response.end(body[0]);
+ } else if (body) {
+ var end = Q.forEach(body, function (chunk) {
+ _response.write(chunk, "binary");
+ });
+ return Q.when(end, function () {
+ _response.end();
+ });
+ } else {
+ _response.end();
+ }
+ });
+ });
+ });
+
+ var stopped = Q.defer();
+ server.on("close", function (err) {
+ if (err) {
+ stopped.reject(err);
+ } else {
+ stopped.resolve();
+ }
+ });
+
+ /***
+ * Stops the server.
+ * @returns {Promise * Undefined} a promise that will
+ * resolve when the server is stopped.
+ */
+ self.stop = function () {
+ server.close();
+ listening = undefined;
+ return stopped.promise;
+ };
+
+ var listening = Q.defer();
+ server.on("listening", function (err) {
+ if (err) {
+ listening.reject(err);
+ } else {
+ listening.resolve();
+ }
+ });
+ /***
+ * Starts the server, listening on the given port
+ * @param {Number} port
+ * @returns {Promise * Undefined} a promise that will
+ * resolve when the server is ready to receive
+ * connections
+ */
+ self.listen = function (port) {
+ if (!listening)
+ throw new Error("A server cannot be restarted or " +
+ "started on a new port");
+ server.listen(port >>> 0);
+ return listening.promise;
+ };
+
+ return self;
+};
+
+/**
+ * A wrapper for a Node HTTP Request, as received by
+ * the Q HTTP Server, suitable for use by the Q HTTP Client.
+ */
+exports.Request = function (_request) {
+ var request = Object.create(exports.Request.prototype);
+ /*** {String} HTTP method, e.g., `"GET"` */
+ request.method = _request.method;
+ /*** {String} path, starting with `"/"` */
+ request.path = _request.url;
+ /*** A Q IO asynchronous text reader */
+ request.body = IO.Reader(_request);
+ /*** {Object} HTTP headers */
+ request.headers = _request.headers;
+ /*** The underlying Node TCP connection */
+ request.connection = _request.connection;
+ return request;
+};
+
+/**
+ * Creates an HTTP client for issuing requests to
+ * the given host on the given port.
+ * @param {Number} port
+ * @param {String} host
+ */
+exports.Client = function (port, host) {
+ var self = Object.create(exports.Client.prototype);
+
+ var _client = HTTP.createClient(port, host);
+
+ /***
+ * Issues an HTTP request. The request may be
+ * any object that has `method`, `path`, `headers`
+ * and `body` properties.
+ *
+ * * `method` `String` is optional, defaults to `"GET"`.
+ * * `path` `String` is optional, defaults to `"/"`.
+ * * `headers` `Object` is optional, defaults to `{}`.
+ * * `body` is optional, defaults to `[]`. Body must
+ * be an object with a `forEach` method that accepts a
+ * `write` callback. `forEach` may return a promise,
+ * and may send promises to `write`. `body` may be a
+ * promise.
+ *
+ * The Q HTTP `Server` responder receives a `Request`
+ * object that is suitable for `request`, and `request`
+ * returns a `Response` suitable for returning to the
+ * `Server`.
+ *
+ * @param {{method, path, headers, body}}
+ * @returns {Promise * Response}
+ */
+ self.request = function (request) {
+ // host, port, method, path, headers, body
+ var deferred = Q.defer();
+ var _request = _client.request(
+ request.method || 'GET',
+ request.path || '/',
+ request.headers || {}
+ );
+ _request.on('response', function (_response) {
+ var response = exports.Response(_response);
+ deferred.resolve(response);
+ });
+ _request.on("error", function (error) {
+ deferred.reject(error);
+ });
+ Q.when(request.body, function (body) {
+ var end;
+ if (body) {
+ end = Q.forEach(body, function (chunk) {
+ _request.write(chunk, request.charset);
+ });
+ }
+ Q.when(end, function () {
+ _request.end();
+ });
+ });
+ return deferred.promise;
+ };
+
+ return self;
+};
+
+/**
+ * Issues an HTTP request.
+ *
+ * @param {Request {host, port, method, path, headers,
+ * body}} request (may be a promise)
+ * @returns {Promise * Response} promise for a response
+ */
+exports.request = function (request) {
+ return Q.when(request, function (request) {
+ var client = exports.Client(request.port || 80, request.host);
+ return client.request(request);
+ });
+};
+
+/**
+ * Issues a GET request to the given URL and returns
+ * a promise for a `String` containing the entirety
+ * of the response.
+ *
+ * @param {String} url
+ * @returns {Promise * String} or a rejection if the
+ * status code is not exactly 200. The reason for the
+ * rejection is the full response object.
+ */
+exports.read = function (url) {
+ url = URL.parse(url);
+ return Q.when(exports.request({
+ "host": url.hostname,
+ "port": url.port,
+ "method": "GET",
+ "path": (url.pathname || "") + (url.search || ""),
+ "headers": {}
+ }), function (response) {
+ if (response.status !== 200)
+ return Q.reject(response);
+ return Q.when(response.body, function (body) {
+ return body.read();
+ });
+ });
+};
+
+
+/**
+ * A wrapper for the Node HTTP Response as provided
+ * by the Q HTTP Client API, suitable for use by the
+ * Q HTTP Server API.
+ */
+exports.Response = function (_response) {
+ var response = Object.create(exports.Response.prototype);
+ /*** {Number} HTTP status code */
+ response.status = _response.statusCode;
+ /*** HTTP version */
+ response.version = _response.httpVersion;
+ /*** {Object} HTTP headers */
+ response.headers = _response.headers;
+ /***
+ * A Q IO asynchronous text reader.
+ */
+ response.body = IO.Reader(_response);
+ return response;
+};
+
@@ -0,0 +1,36 @@
+
+var SYS = require("sys");
+var Q = require("q");
+var HTTP = require("q-http");
+
+var request = {
+ "host": "localhost",
+ "port": 8080,
+ "headers": {
+ "host": "localhost"
+ }
+};
+
+var response = {
+ "status": 200,
+ "headers": {
+ "content-type": "text/plain"
+ },
+ "body": [
+ "Hello, World!"
+ ]
+};
+
+var server = HTTP.Server(function () {
+ return response;
+});
+
+Q.when(server.listen(8080), function () {
+ return Q.when(HTTP.request(request), function (response) {
+ return Q.when(response.body, function (body) {
+ var done = body.forEach(SYS.puts);
+ Q.when(done, server.stop);
+ });
+ });
+}, Q.error);
+
Oops, something went wrong.

0 comments on commit 9ee5884

Please sign in to comment.