Skip to content

Commit

Permalink
style: Add basic internal type checks
Browse files Browse the repository at this point in the history
  • Loading branch information
Jan Krems committed Jun 1, 2019
1 parent 25fee1d commit 538e3cd
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 14 deletions.
4 changes: 4 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/node_modules
/tmp
/examples/decorators
/examples/hello-world
32 changes: 26 additions & 6 deletions lib/express.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,37 @@

'use strict';

const quinn = require('./quinn');
const { respond, runApplication } = require('./quinn');

const runApplication = quinn.runApplication;
const respond = quinn.respond;
/**
* @typedef {import('http').IncomingMessage} IncomingMessage
* @typedef {import('http').ServerResponse} ServerResponse
* @typedef {() => any} QuinnHandler
*/

/**
* @param {QuinnHandler} handler
*/
function createApp(handler) {
return function(req, res, next) {
/**
*
* @param {IncomingMessage} req
* @param {ServerResponse} res
* @param {(error?: Error) => void} next
*/
function expressHandler(req, res, next) {
/**
* @param {Error} err
*/
function forwardError(err) {
setImmediate(() => {
next(err);
});
}

/**
* @param {unknown} result
*/
function callNext(result) {
if (result === undefined) setImmediate(next);
return result;
Expand All @@ -53,11 +71,13 @@ function createApp(handler) {
return runApplication(handler, req, res)
.then(callNext)
.then(null, forwardError);
};
}

return expressHandler;
}

module.exports = createApp;
createApp['default'] = createApp;
createApp.default = createApp;
createApp.createApp = createApp;
createApp.respond = respond;
createApp.runApplication = runApplication;
32 changes: 29 additions & 3 deletions lib/quinn.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,27 @@

const respond = require('./respond');

/**
* @typedef {import('http').IncomingMessage} IncomingMessage
* @typedef {import('http').ServerResponse} ServerResponse
* @typedef {() => any} QuinnHandler
*/

const NOT_FOUND = Buffer.from('Not Found\n', 'utf8');
const INTERNAL_ERROR = Buffer.from('Internal Server Error\n', 'utf8');

/**
* @param {ServerResponse} res
*/
function sendNotFound(res) {
res.statusCode = 404;
res.end(NOT_FOUND);
}

/**
* @param {ServerResponse} res
* @param {Error} err
*/
function sendFatalError(res, err) {
try {
res.statusCode = 500;
Expand All @@ -52,6 +65,11 @@ function sendFatalError(res, err) {
return Promise.reject(err);
}

/**
* @param {QuinnHandler} handler
* @param {IncomingMessage} req
* @param {ServerResponse} res
*/
function runApplication(handler, req, res) {
return Promise.resolve(req)
.then(handler)
Expand All @@ -64,8 +82,15 @@ function runApplication(handler, req, res) {
});
}

/**
* @param {QuinnHandler} handler
*/
function createApp(handler) {
return function(req, res) {
/**
* @param {IncomingMessage} req
* @param {ServerResponse} res
*/
function requestListener(req, res) {
return runApplication(handler, req, res)
.then(result => {
if (result === undefined) return sendNotFound(res);
Expand All @@ -74,11 +99,12 @@ function createApp(handler) {
.then(null, err => {
return sendFatalError(res, err);
});
};
}
return requestListener;
}

module.exports = createApp;
createApp['default'] = createApp;
createApp.default = createApp;
createApp.createApp = createApp;
createApp.respond = respond;
createApp.runApplication = runApplication;
69 changes: 65 additions & 4 deletions lib/respond.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,31 @@
const { PassThrough } = require('stream');
const { httpify } = require('caseless');

/**
* @typedef {import('http').IncomingMessage} IncomingMessage
* @typedef {import('http').ServerResponse} ServerResponse
* @typedef {() => any} QuinnHandler
* @typedef {any} QuinnBody
* @typedef {(() => QuinnBody) | ((req: IncomingMessage, res: ServerResponse) => QuinnBody) | null} QuinnBodyFactory
*/

/**
* @param {any} value
*/
function isStream(value) {
return !!value && typeof value.pipe === 'function';
}

/**
* @param {unknown} value
*/
function isLazy(value) {
return typeof value === 'function';
}

/**
* @type {(value: any) => boolean}
*/
const isData =
typeof Uint8Array === 'function'
? function isData(value) {
Expand All @@ -52,6 +69,9 @@ const isData =
return typeof value === 'string';
};

/**
* @param {any} value
*/
function isBody(value) {
return value === null || isData(value) || isStream(value) || isLazy(value);
}
Expand All @@ -61,37 +81,58 @@ function getDefaultBody() {
}

class VirtualResponse extends PassThrough {
/**
*
* @param {{ statusCode?: number, headers?: object, body?: any }} options
*/
constructor({ statusCode = 200, headers = {}, body }) {
super();

this.statusCode = statusCode;
/** @type {QuinnBodyFactory} */
this.bodyFactory = null;
/** @type {Error | null} */
this.cachedError = null;

httpify(this, headers);

if (isBody(body)) {
this.body(body);
} else {
this.bodyFactory = getDefaultBody.bind(null, this);
}
}

/**
* @param {Error} e
*/
error(e) {
// throw error! but maybe make it possible for this to be delayed until
// after the stream is flowing.
this.emit('error', e);
}

/**
* @param {number} code
*/
status(code) {
this.statusCode = code;
return this;
}

/**
* @param {string} name
* @param {string | string[]} value
*/
header(name, value) {
// @ts-ignore Typescript doesn't understand `httpify(this)`
this.setHeader(name, value);
return this;
}

/**
* @param {QuinnBody} body
*/
body(body) {
if (typeof body === 'function') {
this.bodyFactory = body;
Expand All @@ -103,12 +144,13 @@ class VirtualResponse extends PassThrough {
if (body === null) body = Buffer.alloc(0);

if (body instanceof Buffer) {
// @ts-ignore
this.body = body;
this.header('Content-Length', body.length);
this.header('Content-Length', `${body.length}`);
this.end(body);
} else if (isStream(body)) {
if (typeof body.on === 'function') {
body.on('error', e => {
body.on('error', (/** @type {Error} */ e) => {
this.error(e);
});
}
Expand All @@ -119,6 +161,10 @@ class VirtualResponse extends PassThrough {
return this;
}

/**
* @param {IncomingMessage} req
* @param {ServerResponse} res
*/
forwardTo(req, res) {
return new Promise((resolve, reject) => {
this.on('error', reject);
Expand All @@ -142,14 +188,24 @@ class VirtualResponse extends PassThrough {
});
}

/**
* @template {NodeJS.WritableStream} T
* @param {T} res
* @param {{ end?: boolean }} [options]
* @returns {T}
*/
pipe(res, options) {
// @ts-ignore
res.statusCode = this.statusCode;

// @ts-ignore Typescript doesn't like type narrowing like this
if (typeof res.setHeader === 'function') {
// @ts-ignore Typescript can't understand `httpify(this)` above
const headers = this.headers;
const headerNames = Object.keys(headers);
for (let i = 0; i < headerNames.length; ++i) {
const name = headerNames[i];
// @ts-ignore Typescript doesn't let us cast `res` to ServerResponse
res.setHeader(name, headers[name]);
}
}
Expand All @@ -165,12 +221,17 @@ function respond(props = {}) {
if (props instanceof VirtualResponse) return props;

if (isBody(props)) {
return new VirtualResponse({ body: props });
return new VirtualResponse({ body: /** @type {QuinnBody} */ (props) });
}

return new VirtualResponse(props);
}

/**
* @param {any} obj
* @param {(key: string, value: any) => any} [visitor]
* @param {string | number} [indent]
*/
function json(obj, visitor, indent) {
return respond({
headers: { 'Content-Type': 'application/json; charset=utf-8' },
Expand All @@ -179,6 +240,6 @@ function json(obj, visitor, indent) {
}

module.exports = respond;
module.exports['default'] = respond;
module.exports.default = respond;
module.exports.respond = respond;
module.exports.json = json;
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"url": "https://github.com/groupon/quinn/issues"
},
"scripts": {
"pretest": "eslint lib test",
"pretest": "eslint . && tsc",
"test": "mocha",
"posttest": "nlm verify"
},
Expand All @@ -28,6 +28,9 @@
"caseless": "^0.10.0"
},
"devDependencies": {
"@types/caseless": "^0.12.2",
"@types/mocha": "^5.2.7",
"@types/node": "^12.0.4",
"assertive": "^2.1.0",
"eslint": "^5.1.0",
"eslint-config-groupon": "^7.2.0",
Expand All @@ -41,6 +44,7 @@
"nlm": "^3.6.1",
"prettier": "^1.6.1",
"response": "^0.18.0",
"typescript": "^3.5.1",
"wegweiser": "^3.2.1"
},
"author": {
Expand Down

0 comments on commit 538e3cd

Please sign in to comment.