pretty much fully finished version 1

This commit is contained in:
Jonatan Nilsson 2021-10-09 00:12:56 +00:00
parent 26c9b4a27e
commit c8290126fa
13 changed files with 2045 additions and 212 deletions

13
LICENSE Normal file
View file

@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

View file

@ -1,7 +1,7 @@
import assert from 'assert'
import Benchmark from 'benchmarkjs-pretty'
import { koaRouter1, koaRouter2 } from './router_koa.js'
import { flaskaRouter1, flaskaRouter2, flaskaClassRouter1, flaskaClassRouter2 } from './router_flaska.js'
import { flaskaRouter1, flaskaRouter2 } from './router_flaska.js'
import { expressRouter1, expressRouter2 } from './router_express.js'
import * as consts from './const.js'
@ -10,6 +10,216 @@ let testData = null
consts.overrideDummy(function() { testData = assertOk })
function TestPromiseCreators() {
let resolveTrue = Promise.resolve(true)
let asyncFunc = async function() {
let check = await resolveTrue
if (check !== true) {
throw new Error('false')
}
}
return new Benchmark.default('test different promise creation methods')
.add('new Promise()', function() {
return new Promise((res, rej) => {
try {
resolveTrue.then(function(check) {
res(check)
})
} catch (err) {
rej(err)
}
})
})
.add('new Promise() static', function() {
return new Promise((res, rej) => {
res(true)
})
})
.add('new Promise() static with try catch', function() {
return new Promise((res, rej) => {
try {
res(true)
} catch (err) {
rej(err)
}
})
})
.add('new Promise() static with one then()', function() {
return new Promise((res, rej) => {
res(true)
}).then(function(check) {
return check
})
})
.add('Promise.resolve()', function() {
return Promise.resolve().then(function() {
return resolveTrue
}).then(function(check) {
return check
})
})
.add('resolved promise one then()', function() {
return resolveTrue.then(function(check) {
return check
})
})
.add('resolved promise but four then()', function() {
return resolveTrue.then(function(check) {
return check
})
.then(function(item) { return item })
.then(function(item) { return item })
.then(function(item) { return item })
})
.add('resolved promise but twenty then()', function() {
return resolveTrue.then(function(check) {
if (check !== true) {
throw new Error('false')
}
return true
})
.then(function(item) { return item })
.then(function(item) { return item })
.then(function(item) { return item })
.then(function(item) { return item })
.then(function(item) { return item })
.then(function(item) { return item })
.then(function(item) { return item })
.then(function(item) { return item })
.then(function(item) { return item })
.then(function(item) { return item })
.then(function(item) { return item })
.then(function(item) { return item })
.then(function(item) { return item })
.then(function(item) { return item })
.then(function(item) { return item })
.then(function(item) { return item })
.then(function(item) { return item })
.then(function(item) { return item })
.then(function(item) { return item })
})
.add('async function()', async function() {
let check = await resolveTrue
if (check !== true) {
throw new Error('false')
}
})
.run()
.then(function() {}, function(e) {
console.error('error:', e)
process.exit(1)
})
}
function TestObjectAssign() {
const base = {
state: {},
log: {
info: function() { },
warn: function() { },
error: function() { },
},
router1: flaskaRouter1,
router2: flaskaRouter2,
}
const req = {
test: 1,
test2: 2,
koa1: koaRouter1,
koa2: koaRouter2,
test: function() {},
}
const res = {
express1: expressRouter1,
express2: expressRouter2,
test: function() {},
}
let propMaker = {}
let propMakerAlt = {}
Object.keys(base).forEach(function(key) {
propMaker[key] = {
enumerable: true,
value: base[key],
writable: true,
configurable: true,
}
propMakerAlt[key] = {
enumerable: true,
value: base[key],
writable: true,
configurable: true,
}
})
propMakerAlt.req = {
enumerable: true,
get: function() {
return req
},
configurable: true,
}
propMakerAlt.res = {
enumerable: true,
get: function() {
return res
},
configurable: true,
}
function register1(ctx) {
ctx.log = base.log
}
function register2(ctx) {
ctx.router1 = flaskaRouter1
}
function register3(ctx) {
ctx.router2 = flaskaRouter2
}
return new Benchmark.default('test different method to initialize objects)')
.add('Object.assign()', function() {
let ctx = {
req: req,
res: res,
}
Object.assign(ctx, base)
ctx.log.info()
})
.add('register functions with direct assignment', function() {
let ctx = {
state: {},
req: req,
res: res,
}
register1(ctx)
register2(ctx)
register3(ctx)
ctx.log.info()
})
/*.add('Object.create() all props', function() {
let ctx = Object.create({}, propMakerAlt)
ctx.log.info()
})
.add('Object.defineProperties()', function() {
let ctx = {
req: req,
res: res,
}
Object.defineProperties(ctx, propMaker)
ctx.log.info()
})
.add('Object.defineProperties() all props', function() {
let ctx = { }
Object.defineProperties(ctx, propMakerAlt)
ctx.log.info()
})*/
.run()
.then(function() {}, function(e) {
console.error('error:', e)
process.exit(1)
})
}
function TestSmallStaticRoute() {
return new Benchmark.default('Small router static route benchmark: /api/staff (16 routes registered)')
.add('expressjs', function() {
@ -28,10 +238,6 @@ function TestSmallStaticRoute() {
testData = flaskaRouter1.match('/api/staff')
assert.ok(testData.handler)
})
.add('bottle-router-alt', function() {
testData = flaskaClassRouter1.match('/api/staff')
assert.ok(testData.handler)
})
.run()
.then(function() {}, function(e) {
console.error('error:', e)
@ -57,10 +263,6 @@ function TestSmallParamRoute() {
testData = flaskaRouter1.match('/api/staff/justatest')
assert.ok(testData.handler)
})
.add('bottle-router-alt', function() {
testData = flaskaClassRouter1.match('/api/staff/justatest')
assert.ok(testData.handler)
})
.run()
.then(function() {}, function(e) {
console.error('error:', e)
@ -86,10 +288,6 @@ function TestLargeStaticRoute() {
testData = flaskaRouter2.match('/api/staff')
assert.ok(testData.handler)
})
.add('bottle-router-alt', function() {
testData = flaskaClassRouter2.match('/api/staff')
assert.ok(testData.handler)
})
.run()
.then(function() {}, function(e) {
console.error('error:', e)
@ -115,10 +313,6 @@ function TestLargeParamRoute() {
testData = flaskaRouter2.match('/api/staff/justatest')
assert.ok(testData.handler)
})
.add('bottle-router-alt', function() {
testData = flaskaClassRouter2.match('/api/staff/justatest')
assert.ok(testData.handler)
})
.run()
.then(function() {}, function(e) {
console.error('error:', e)
@ -144,10 +338,6 @@ function TestLargeParamLargeUrlRoute() {
testData = flaskaRouter2.match('/api/products/justatest/sub_products/foobar')
assert.ok(testData.handler)
})
.add('bottle-router-alt', function() {
testData = flaskaClassRouter2.match('/api/products/justatest/sub_products/foobar')
assert.ok(testData.handler)
})
.run()
.then(function() {}, function(e) {
console.error('error:', e)
@ -156,6 +346,8 @@ function TestLargeParamLargeUrlRoute() {
}
TestSmallStaticRoute()
// TestObjectAssign()
// TestPromiseCreators()
.then(function() {
return TestSmallParamRoute()
})

View file

@ -1,25 +1,18 @@
import { FlaskaRouter, FlaskaRouterClass } from '../flaska.mjs'
import { FlaskaRouter } from '../flaska.mjs'
import * as consts from './const.js'
const router1 = new FlaskaRouter()
const router2 = new FlaskaRouter()
const classRouter1 = new FlaskaRouterClass()
const classRouter2 = new FlaskaRouterClass()
for (let key in consts.allRoutes) {
router1.addRoute(consts.allRoutes[key], consts.dummy)
classRouter1.addRoute(consts.allRoutes[key], consts.dummy)
}
for (let key in consts.allManyRoutes) {
router2.addRoute(consts.allManyRoutes[key], consts.dummy)
classRouter2.addRoute(consts.allManyRoutes[key], consts.dummy)
}
export {
router1 as flaskaRouter1,
router2 as flaskaRouter2,
classRouter1 as flaskaClassRouter1,
classRouter2 as flaskaClassRouter2,
}

View file

@ -1,3 +1,6 @@
import http from 'http'
import stream from 'stream'
/**
* Router
*/
@ -12,9 +15,41 @@ class Branch {
}
}
export const ErrorCodes = {
ERR_CONNECTION_ABORTED: 'ERR_CON_ABORTED'
}
// Taken from https://github.com/nfp-projects/koa-lite/blob/master/lib/statuses.js
const statuses = {
100: 'Continue', 101: 'Switching Protocols', 102: 'Processing', 103: 'Early Hints',
200: 'OK', 201: 'Created', 202: 'Accepted', 203: 'Non-Authoritative Information', 204: 'No Content', 205: 'Reset Content', 206: 'Partial Content', 207: 'Multi-Status', 208: 'Already Reported', 226: 'IM Used',
300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Found', 303: 'See Other', 304: 'Not Modified', 305: 'Use Proxy', 306: '(Unused)', 307: 'Temporary Redirect', 308: 'Permanent Redirect',
400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', 407: 'Proxy Authentication Required', 408: 'Request Timeout', 409: 'Conflict', 410: 'Gone', 411: 'Length Required', 412: 'Precondition Failed', 413: 'Payload Too Large', 414: 'URI Too Long', 415: 'Unsupported Media Type', 416: 'Range Not Satisfiable', 417: 'Expectation Failed', 418: 'I\'m a teapot', 421: 'Misdirected Request', 422: 'Unprocessable Entity', 423: 'Locked', 424: 'Failed Dependency', 425: 'Too Early', 426: 'Upgrade Required', 428: 'Precondition Required', 429: 'Too Many Requests', 431: 'Request Header Fields Too Large', 451: 'Unavailable For Legal Reasons',
500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Timeout', 505: 'HTTP Version Not Supported', 506: 'Variant Also Negotiates', 507: 'Insufficient Storage', 508: 'Loop Detected', 509: 'Bandwidth Limit Exceeded', 510: 'Not Extended', 511: 'Network Authentication Required',
redirect: {
300: true,
301: true,
302: true,
303: true,
305: true,
307: true,
308: true
},
empty: {
204: true,
205: true,
304: true
}
}
const __paramMapName = '__param'
const __fullParamMapName = '__fullparam'
function assertIsHandler(handler, name) {
if (typeof(handler) !== 'function') {
throw new Error(`${name} was called with a handler that was not a function`)
}
}
export class FlaskaRouter {
constructor() {
this.root = new Branch()
@ -33,9 +68,7 @@ export class FlaskaRouter {
if (middlewares && typeof(middlewares) === 'function') {
middlewares = [middlewares]
}
if (typeof(handler) !== 'function') {
throw new Error(`route "${route}" was missing a handler`)
}
assertIsHandler(handler, 'addRoute()')
let start = 1
let end = 1
@ -150,14 +183,34 @@ export class FlaskaRouter {
else if (output = branch.children.get(__paramMapName)) {
branch = output
params[branch.paramName] = name
}
else if (output = branch.children.get(__fullParamMapName)) {
params[output.fullparamName] = url.slice(start)
return {
handler: output.handler,
middlewares: output.middlewares,
params: params,
}
} else {
if (output = this.root.children.get(__fullParamMapName)) {
params = {
[output.fullparamName]: url.slice(1)
}
return {
handler: output.handler,
middlewares: output.middlewares,
params: params,
}
}
return null
}
i++
end = start = i
char = url[i]
}
if (i >= url.length) {
// Check branch.handler. This can happen if route /::path is added
// and request is '/' it will attempt to match root which will fail
if (i >= url.length && branch.handler) {
return {
handler: branch.handler,
middlewares: branch.middlewares,
@ -170,167 +223,313 @@ export class FlaskaRouter {
end++
}
}
if (output = this.root.children.get(__fullParamMapName)) {
params = {
[output.fullparamName]: url.slice(1)
}
return {
handler: output.handler,
middlewares: output.middlewares,
params: params,
}
}
return null
}
}
export function FlaskaRouter() {
this.root = new Branch()
}
/**
* Flaska
*/
export class Flaska {
constructor(opts, orgHttp = http, orgStream = stream) {
this._before = []
this._beforeCompiled = null
this._beforeAsync = []
this._beforeAsyncCompiled = null
this._after = []
this._afterCompiled = null
this._afterAsync = []
this._afterAsyncCompiled = null
this._on404 = function(ctx) {
ctx.status = 404
ctx.body = {
status: 404,
message: statuses[404],
}
}
this._backuperror = this._onerror = function(err, ctx) {
ctx.log.error(err)
ctx.status = 500
ctx.body = {
status: 500,
message: statuses[500],
}
}
this._onreqerror = function(err, ctx) {
ctx.log.error(err)
ctx.res.statusCode = ctx.statusCode = 400
ctx.res.end()
ctx.finished = true
}
this._onreserror = function(err, ctx) {
ctx.log.error(err)
}
FlaskaRouter.prototype.addRoute = function(route, orgMiddlewares, orgHandler) {
if (route[0] !== '/')
throw new Error(`route "${route}" must start with forward slash`)
let options = opts || {}
let middlewares = orgMiddlewares
let handler = orgHandler
if (!orgHandler) {
handler = orgMiddlewares
middlewares = []
}
if (middlewares && typeof(middlewares) === 'function') {
middlewares = [middlewares]
}
if (typeof(handler) !== 'function') {
throw new Error(`route "${route}" was missing a handler`)
this.log = options.log || console
this.http = orgHttp
this.stream = orgStream
this.server = null
this.routers = {
'GET': new FlaskaRouter(),
'POST': new FlaskaRouter(),
'PUT': new FlaskaRouter(),
'DELETE': new FlaskaRouter(),
'OPTIONS': new FlaskaRouter(),
'PATCH': new FlaskaRouter(),
}
this.get = this.routers.GET.addRoute.bind(this.routers.GET)
this.post = this.routers.POST.addRoute.bind(this.routers.POST)
this.put = this.routers.PUT.addRoute.bind(this.routers.PUT)
this.delete = this.routers.DELETE.addRoute.bind(this.routers.DELETE)
this.options = this.routers.OPTIONS.addRoute.bind(this.routers.OPTIONS)
this.patch = this.routers.PATCH.addRoute.bind(this.routers.PATCH)
}
let start = 1
let end = 1
let name = ''
let param = ''
let isParam = false
let isFullParam = false
let branch = this.root
if (route.indexOf(':') < 0) {
let name = route
if (name.length > 1 && name[name.length - 1] === '/') {
name = name.slice(0, -1)
_assertIsHandler(handler, name) {
if (typeof(handler) !== 'function') {
throw new Error(`${name} was called with a handler that was not a function`)
}
let child = new Branch()
branch.children.set(name, child)
child.handler = handler
child.middlewares = middlewares
}
for (let i = 1; i <= route.length; i++) {
if ((i === route.length || route[i] === '/') && end > start) {
if (branch.fullparamName) {
throw new Error(`route "${route}" conflicts with a sub-branch that has a full param child`)
devMode() {
this._backuperror = this._onerror = function(err, ctx) {
ctx.log.error(err)
ctx.status = 500
ctx.body = {
status: 500,
message: `${statuses[500]}: ${err.message}`,
stack: err.stack || '',
}
let child
name = route.substring(start, end)
if (isFullParam) {
param = name
name = __fullParamMapName
} else if (isParam) {
param = name
name = __paramMapName
}
if (branch.children.has(name)) {
child = branch.children.get(name)
}
else if (isParam && !isFullParam && branch.children.has(__fullParamMapName)) {
throw new Error(`route "${route}" conflicts with a sub-branch that has a full param child`)
}
else if (isFullParam && branch.children.has(__paramMapName)) {
throw new Error(`route "${route}" conflicts with a sub-branch that has a partial param child`)
}
else {
child = new Branch()
branch.children.set(name, child)
}
branch = child
end = i
start = i
if (isParam) {
if (branch.paramName && branch.paramName !== param) {
throw new Error(`route "${route}" conflicts with pre-existing param name of ${branch.paramName} instead of ${param}`)
}
if (isFullParam) {
branch.fullparamName = param
} else {
branch.paramName = param
}
isParam = false
}
} else if (route[i] === '/' && end === start) {
throw new Error(`route "${route}" has missing path name inbetween slashes`)
}
if (i === route.length) {
branch.handler = handler
branch.middlewares = middlewares
continue
}
if (route[i] === ':') {
if (isParam) {
isFullParam = true
}
isParam = true
end = start = i + 1
}
else if (route[i] === '/') {
end = start = i + 1
}
else {
end++
}
}
}
FlaskaRouter.prototype.match = function(orgUrl) {
let url = orgUrl
if (url.length > 1 && url[url.length - 1] === '/') {
url = url.slice(0, -1)
on404(handler) {
assertIsHandler(handler, 'on404()')
this._on404 = handler
}
let branch = this.root
let start = 1
let end = 1
let output
let name
let char
let params = {}
if (output = branch.children.get(url)) {
return {
handler: output.handler,
middlewares: output.middlewares,
params: params,
onerror(handler) {
assertIsHandler(handler, 'onerror()')
this._onerror = handler
}
onreqerror(handler) {
assertIsHandler(handler, 'onreqerror()')
this._onreqerror = handler
}
onreserror(handler) {
assertIsHandler(handler, 'onreserror()')
this._onreserror = handler
}
onerror(handler) {
assertIsHandler(handler, 'onerror()')
this._onerror = handler
}
before(handler) {
assertIsHandler(handler, 'before()')
this._before.push(handler)
}
beforeAsync(handler) {
assertIsHandler(handler, 'beforeAsync()')
this._beforeAsync.push(handler)
}
after(handler) {
assertIsHandler(handler, 'after()')
this._after.push(handler)
}
afterAsync(handler) {
assertIsHandler(handler, 'afterAsync()')
this._afterAsync.push(handler)
}
requestStart(req, res) {
let url = req.url
let search = ''
let hasSearch = url.indexOf('?')
if (hasSearch > 0) {
search = url.slice(hasSearch)
url = url.slice(0, hasSearch)
}
let ctx = {
req: req,
res: res,
method: req.method,
url: url,
search: search,
state: {},
status: 200,
body: null,
type: null,
length: null,
}
req.on('error', (err) => {
this._onreqerror(err, ctx)
})
res.on('error', (err) => {
this._onreserror(err, ctx)
})
req.on('aborted', function() {
ctx.aborted = true
})
req.on('close', () => {
this.requestEnded()
})
try {
this._beforeCompiled(ctx)
if (this._beforeAsyncCompiled) {
return this._beforeAsyncCompiled(ctx)
.then(() => {
this.requestStartInternal(ctx)
}).catch(err => {
this.requestEnd(err, ctx)
})
}
this.requestStartInternal(ctx)
}
catch (err) {
this.requestEnd(err, ctx)
}
}
for (let i = 1; i <= url.length; i++) {
char = url[i]
if ((i === url.length || char === '/') && end > start) {
name = url.slice(start, end)
if (output = branch.children.get(name)) {
branch = output
}
else if (output = branch.children.get(__paramMapName)) {
branch = output
params[branch.paramName] = name
} else {
return null
}
i++
end = start = i
char = url[i]
}
if (i >= url.length) {
return {
handler: branch.handler,
middlewares: branch.middlewares,
params: params,
requestStartInternal(ctx) {
let route = this.routers[ctx.method].match(ctx.url)
ctx.params = route.params
if (route.middlewares.length) {
let middle = this.handleMiddleware(ctx, route.middlewares, 0)
if (middle && middle.then) {
return middle.then(() => {
return route.handler(ctx)
})
.then(() => {
this.requestEnd(null, ctx)
}, err => {
this.requestEnd(err, ctx)
})
}
}
if (char === '/') {
end = start = i + 1
let handler = route.handler(ctx)
if (handler && handler.then) {
return handler.then(() => {
this.requestEnd(null, ctx)
}, err => {
this.requestEnd(err, ctx)
})
}
this.requestEnd(null, ctx)
}
handleMiddleware(ctx, middles, index) {
for (let i = index; i < middles.length; i++) {
let res = middles[i](ctx)
if (res && res.then) {
return res.then(() => {
return this.handleMiddleware(ctx, middles, i + 1)
})
}
}
}
requestEnd(err, ctx) {
if (err) {
this._onerror(err, ctx)
return ctx.res.end()
}
if (ctx.req.complete) {
return
}
ctx.res.statusCode = ctx.status
if (statuses.empty[ctx.status]) {
return ctx.res.end()
}
let body = ctx.body
let length = 0
if (body && typeof(body.pipe) === 'function') {
ctx.res.setHeader('Content-Type', ctx.type || 'application/octet-stream')
return this.stream.pipeline(body, ctx.res, function() {})
}
if (typeof(body) === 'object') {
body = JSON.stringify(body)
length = Buffer.byteLength(body)
ctx.res.setHeader('Content-Type', 'application/json; charset=utf-8')
} else {
end++
body = body.toString()
length = Buffer.byteLength(body)
ctx.res.setHeader('Content-Type', ctx.type || 'text/plain; charset=utf-8')
}
ctx.res.setHeader('Content-Length', length)
ctx.res.end(body)
}
requestEnded(ctx) {
this._afterCompiled(ctx)
if (this._afterAsyncCompiled) {
return this._afterAsyncCompiled(ctx).then()
}
}
return null
}
export function Flaska() {
compile() {
let types = ['before', 'after']
for (let i = 0; i < types.length; i++) {
let type = types[i]
let args = ''
let body = ''
for (let i = 0; i < this['_' + type].length; i++) {
args += `a${i}, `
body += `a${i}(ctx);`
}
args += 'ctx'
let func = new Function(args, body)
this[`_${type}Compiled`] = func.bind(this, ...this['_' + type])
if (this[`_${type}Async`].length) {
args = ''
body = 'return Promise.all(['
for (let i = 0; i < this[`_${type}Async`].length; i++) {
args += `a${i}, `
body += `a${i}(ctx),`
}
args += 'ctx'
body += '])'
func = new Function(args, body)
this[`_${type}AsyncCompiled`] = func.bind(this, ...this[`_${type}Async`])
}
}
}
listen(port, cb) {
if (typeof(port) !== 'number') {
throw new Error('Flaska.listen() called with non-number in port')
}
this.compile()
this.server = this.http.createServer(this.requestStart.bind(this))
this.server.listen(port, cb)
}
}

17
test.mjs Normal file
View file

@ -0,0 +1,17 @@
import { Flaska } from './flaska.mjs'
const port = 51024
const flaska = new Flaska({}, )
flaska.get('/', function(ctx) {
return new Promise(function(res, rej) {
ctx.body = { status: true }
setTimeout(res, 1000)
}).then(function() {
process.stdout.write(".")
})
})
flaska.listen(port, function() {
console.log('listening on port', port)
})

View file

@ -1,39 +1,6 @@
import http from 'http'
import { URL } from 'url'
// taken from isobject npm library
function isObject(val) {
return val != null && typeof val === 'object' && Array.isArray(val) === false
}
function defaults(options, def) {
let out = { }
if (options) {
Object.keys(options || {}).forEach(key => {
out[key] = options[key]
if (Array.isArray(out[key])) {
out[key] = out[key].map(item => {
if (isObject(item)) return defaults(item)
return item
})
} else if (out[key] && typeof out[key] === 'object') {
out[key] = defaults(options[key], def && def[key])
}
})
}
if (def) {
Object.keys(def).forEach(function(key) {
if (typeof out[key] === 'undefined') {
out[key] = def[key]
}
})
}
return out
}
import { defaults } from './helper.mjs'
export default function Client(port, opts) {
this.options = defaults(opts, {})
@ -48,6 +15,7 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) {
return new Promise((resolve, reject) => {
const opts = defaults(defaults(options, {
agent: false,
method: method,
timeout: 500,
protocol: urlObj.protocol,
@ -64,7 +32,10 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) {
}
req.on('error', reject)
req.on('timeout', function() { reject(new Error(`Request ${method} ${path} timed out`)) })
req.on('timeout', function() {
console.log(req.destroy())
reject(new Error(`Request ${method} ${path} timed out`))
})
req.on('response', res => {
res.setEncoding('utf8')
let output = ''
@ -79,7 +50,7 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) {
} catch (e) {
return reject(new Error(`${e.message} while decoding: ${output}`))
}
if (output.status) {
if (output.status && typeof(output.status) === 'number') {
let err = new Error(`Request failed [${output.status}]: ${output.message}`)
err.body = output
return reject(err)
@ -88,6 +59,7 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) {
})
})
req.end()
return req
})
}

487
test/flaska.api.test.mjs Normal file
View file

@ -0,0 +1,487 @@
import { Eltro as t, assert} from 'eltro'
import { Flaska } from '../flaska.mjs'
import { spy, createCtx, fakeHttp } from './helper.mjs'
const faker = fakeHttp()
t.test('should be able to override the http', function() {
let flaska = new Flaska({}, faker)
assert.strictEqual(flaska.http, faker)
})
t.test('it should have all the common verbs', function() {
let flaska = new Flaska({}, faker)
assert.ok(flaska.get)
assert.strictEqual(typeof(flaska.get), 'function')
assert.ok(flaska.post)
assert.strictEqual(typeof(flaska.post), 'function')
assert.ok(flaska.put)
assert.strictEqual(typeof(flaska.put), 'function')
assert.ok(flaska.delete)
assert.strictEqual(typeof(flaska.delete), 'function')
assert.ok(flaska.options)
assert.strictEqual(typeof(flaska.options), 'function')
assert.ok(flaska.patch)
assert.strictEqual(typeof(flaska.patch), 'function')
})
t.describe('#log', function() {
t.test('default have a logger valid', function() {
let flaska = new Flaska({}, faker)
assert.strictEqual(typeof(flaska.log.error), 'function')
assert.strictEqual(typeof(flaska.log.info), 'function')
assert.strictEqual(typeof(flaska.log.warn), 'function')
})
t.test('allow overwriting in options', function() {
const assertFunction = function() { return 1 }
let flaska = new Flaska({ log: {
error: assertFunction,
info: assertFunction,
warn: assertFunction,
debug: assertFunction,
} }, faker)
assert.strictEqual(flaska.log.error, assertFunction)
assert.strictEqual(flaska.log.info, assertFunction)
assert.strictEqual(flaska.log.warn, assertFunction)
assert.strictEqual(flaska.log.debug, assertFunction)
})
})
let specialHandlers = ['on404', 'onerror', 'onreqerror', 'onreserror']
specialHandlers.forEach(function(type) {
t.describe(`#${type}()`, function() {
t.test('exist', function() {
let flaska = new Flaska({}, faker)
assert.strictEqual(typeof(flaska[type]), 'function')
})
t.test('validate handler', function() {
let flaska = new Flaska({}, faker)
assert.throws(function() { flaska[type]() }, /[Ff]unction/)
assert.throws(function() { flaska[type]('asdf') }, /[Ff]unction/)
assert.throws(function() { flaska[type]('123') }, /[Ff]unction/)
assert.throws(function() { flaska[type]([]) }, /[Ff]unction/)
assert.throws(function() { flaska[type]({}) }, /[Ff]unction/)
assert.throws(function() { flaska[type](1234) }, /[Ff]unction/)
})
if (type !== 'on404') {
t.test('should call ctx.log.error correctly', function() {
const assertError = new Error('Samuraism')
let flaska = new Flaska({}, faker)
let ctx = createCtx()
flaska['_' + type](assertError, ctx)
assert.strictEqual(ctx.log.error.callCount, 1)
assert.strictEqual(ctx.log.error.firstCall[0], assertError)
})
}
if (type === 'onreqerror') {
t.test('default sends 400 immediately', function() {
let flaska = new Flaska({}, faker)
let ctx = createCtx()
flaska['_' + type](new Error(), ctx)
assert.strictEqual(ctx.res.statusCode, 400)
assert.strictEqual(ctx.finished, true)
assert.strictEqual(ctx.res.end.callCount, 1)
assert.strictEqual(ctx.res.end.firstCall.length, 0)
})
}
t.test('register it into flaska', function() {
const assertFunction = function() { return true }
let flaska = new Flaska({}, faker)
flaska[type](assertFunction)
assert.strictEqual(flaska['_' + type], assertFunction)
})
})
})
t.describe('_on404', function() {
t.test('a valid function', function() {
let flaska = new Flaska({}, faker)
assert.strictEqual(typeof(flaska._on404), 'function')
})
t.test('default valid handling of context', function() {
let ctx = createCtx()
let flaska = new Flaska({}, faker)
flaska._on404(ctx)
assert.strictEqual(ctx.status, 404)
assert.deepStrictEqual(ctx.body, {
status: 404,
message: 'Not Found',
})
})
})
t.describe('_onerror', function() {
t.test('a valid function', function() {
let flaska = new Flaska({}, faker)
assert.strictEqual(typeof(flaska._onerror), 'function')
})
t.test('default valid handling of context', function() {
const assertError = new Error('should not be seen')
let ctx = createCtx()
let flaska = new Flaska({}, faker)
flaska._onerror(assertError, ctx)
assert.strictEqual(ctx.log.error.callCount, 1)
assert.strictEqual(ctx.log.error.firstCall[0], assertError)
assert.strictEqual(ctx.status, 500)
assert.deepStrictEqual(ctx.body, {
status: 500,
message: 'Internal Server Error',
})
})
})
t.describe('#devMode()', function() {
t.test('turns on debug mode', function() {
const assertErrorMessage = 'Fascination'
const assertError = new Error(assertErrorMessage)
let ctx = createCtx()
let flaska = new Flaska({}, faker)
flaska._onerror(assertError, ctx)
assert.strictEqual(ctx.log.error.callCount, 1)
assert.strictEqual(ctx.log.error.firstCall[0], assertError)
assert.strictEqual(ctx.status, 500)
assert.deepStrictEqual(ctx.body, {
status: 500,
message: 'Internal Server Error',
})
ctx = createCtx()
flaska._backuperror(assertError, ctx)
assert.strictEqual(ctx.log.error.callCount, 1)
assert.strictEqual(ctx.log.error.firstCall[0], assertError)
assert.strictEqual(ctx.status, 500)
assert.deepStrictEqual(ctx.body, {
status: 500,
message: 'Internal Server Error',
})
flaska.devMode()
ctx = createCtx()
flaska._onerror(assertError, ctx)
assert.strictEqual(ctx.log.error.callCount, 1)
assert.strictEqual(ctx.log.error.firstCall[0], assertError)
assert.strictEqual(ctx.status, 500)
assert.strictEqual(ctx.body.status, 500)
assert.match(ctx.body.message, /Internal Server Error/)
assert.match(ctx.body.message, new RegExp(assertErrorMessage))
assert.ok(ctx.body.stack)
ctx = createCtx()
flaska._backuperror(assertError, ctx)
assert.strictEqual(ctx.log.error.callCount, 1)
assert.strictEqual(ctx.log.error.firstCall[0], assertError)
assert.strictEqual(ctx.status, 500)
assert.strictEqual(ctx.body.status, 500)
assert.match(ctx.body.message, /Internal Server Error/)
assert.match(ctx.body.message, new RegExp(assertErrorMessage))
assert.ok(ctx.body.stack)
})
})
t.describe('#before()', function() {
t.test('should throw if not a function', function() {
let flaska = new Flaska({}, faker)
assert.throws(function() { flaska.before() }, /[Ff]unction/)
assert.throws(function() { flaska.before('asdf') }, /[Ff]unction/)
assert.throws(function() { flaska.before('123') }, /[Ff]unction/)
assert.throws(function() { flaska.before([]) }, /[Ff]unction/)
assert.throws(function() { flaska.before({}) }, /[Ff]unction/)
assert.throws(function() { flaska.before(1234) }, /[Ff]unction/)
})
t.test('add handler to preflight list', function() {
const assertFunction = function() {}
let flaska = new Flaska({}, faker)
assert.ok(flaska._before)
flaska.before(assertFunction)
assert.strictEqual(flaska._before.length, 1)
assert.strictEqual(flaska._before[0], assertFunction)
})
})
t.describe('#beforeAsync()', function() {
t.test('should throw if not a function', function() {
let flaska = new Flaska({}, faker)
assert.throws(function() { flaska.beforeAsync() }, /[Ff]unction/)
assert.throws(function() { flaska.beforeAsync('asdf') }, /[Ff]unction/)
assert.throws(function() { flaska.beforeAsync('123') }, /[Ff]unction/)
assert.throws(function() { flaska.beforeAsync([]) }, /[Ff]unction/)
assert.throws(function() { flaska.beforeAsync({}) }, /[Ff]unction/)
assert.throws(function() { flaska.beforeAsync(1234) }, /[Ff]unction/)
})
t.test('add handler to preflight list', function() {
const assertFunction = function() {}
let flaska = new Flaska({}, faker)
assert.ok(flaska._beforeAsync)
flaska.beforeAsync(assertFunction)
assert.strictEqual(flaska._beforeAsync.length, 1)
assert.strictEqual(flaska._beforeAsync[0], assertFunction)
})
})
t.describe('#after()', function() {
t.test('should throw if not a function', function() {
let flaska = new Flaska({}, faker)
assert.throws(function() { flaska.after() }, /[Ff]unction/)
assert.throws(function() { flaska.after('asdf') }, /[Ff]unction/)
assert.throws(function() { flaska.after('123') }, /[Ff]unction/)
assert.throws(function() { flaska.after([]) }, /[Ff]unction/)
assert.throws(function() { flaska.after({}) }, /[Ff]unction/)
assert.throws(function() { flaska.after(1234) }, /[Ff]unction/)
})
t.test('add handler to preflight list', function() {
const assertFunction = function() {}
let flaska = new Flaska({}, faker)
assert.ok(flaska._after)
flaska.after(assertFunction)
assert.strictEqual(flaska._after.length, 1)
assert.strictEqual(flaska._after[0], assertFunction)
})
})
t.describe('#afterAsync()', function() {
t.test('should throw if not a function', function() {
let flaska = new Flaska({}, faker)
assert.throws(function() { flaska.afterAsync() }, /[Ff]unction/)
assert.throws(function() { flaska.afterAsync('asdf') }, /[Ff]unction/)
assert.throws(function() { flaska.afterAsync('123') }, /[Ff]unction/)
assert.throws(function() { flaska.afterAsync([]) }, /[Ff]unction/)
assert.throws(function() { flaska.afterAsync({}) }, /[Ff]unction/)
assert.throws(function() { flaska.afterAsync(1234) }, /[Ff]unction/)
})
t.test('add handler to preflight list', function() {
const assertFunction = function() {}
let flaska = new Flaska({}, faker)
assert.ok(flaska._afterAsync)
flaska.afterAsync(assertFunction)
assert.strictEqual(flaska._afterAsync.length, 1)
assert.strictEqual(flaska._afterAsync[0], assertFunction)
})
})
t.describe('#compile()', function() {
t.test('join all before together in one function', function() {
let flaska = new Flaska({}, faker)
flaska.before(function(ctx) { ctx.a = 1 })
flaska.before(function(ctx) { ctx.b = 2 })
flaska.before(function(ctx) { ctx.c = 3 })
flaska.before(function(ctx) { ctx.d = 4 })
assert.notOk(flaska._beforeCompiled)
flaska.compile()
assert.ok(flaska._beforeCompiled)
assert.notOk(flaska._beforeAsyncCompiled)
assert.strictEqual(typeof(flaska._beforeCompiled), 'function')
let ctx = createCtx()
flaska._beforeCompiled(ctx)
assert.strictEqual(ctx.a, 1)
assert.strictEqual(ctx.b, 2)
assert.strictEqual(ctx.c, 3)
assert.strictEqual(ctx.d, 4)
})
t.test('join all beforeAsync together in one function', function() {
let flaska = new Flaska({}, faker)
flaska.beforeAsync(function(ctx) { return Promise.resolve().then(function() { ctx.a = 1 }) })
flaska.beforeAsync(function(ctx) { ctx.b = 2 })
flaska.beforeAsync(function(ctx) { return new Promise(function(res) { ctx.c = 3; res() }) })
flaska.beforeAsync(function(ctx) { ctx.d = 4 })
assert.notOk(flaska._beforeAsyncCompiled)
flaska.compile()
assert.ok(flaska._beforeAsyncCompiled)
assert.strictEqual(typeof(flaska._beforeAsyncCompiled), 'function')
let ctx = createCtx()
return flaska._beforeAsyncCompiled(ctx).then(function() {
assert.strictEqual(ctx.a, 1)
assert.strictEqual(ctx.b, 2)
assert.strictEqual(ctx.c, 3)
assert.strictEqual(ctx.d, 4)
})
})
t.test('join all after together in one function', function() {
let flaska = new Flaska({}, faker)
flaska.after(function(ctx) { ctx.a = 1 })
flaska.after(function(ctx) { ctx.b = 2 })
flaska.after(function(ctx) { ctx.c = 3 })
flaska.after(function(ctx) { ctx.d = 4 })
assert.notOk(flaska._afterCompiled)
flaska.compile()
assert.ok(flaska._afterCompiled)
assert.notOk(flaska._afterAsyncCompiled)
assert.strictEqual(typeof(flaska._afterCompiled), 'function')
let ctx = createCtx()
flaska._afterCompiled(ctx)
assert.strictEqual(ctx.a, 1)
assert.strictEqual(ctx.b, 2)
assert.strictEqual(ctx.c, 3)
assert.strictEqual(ctx.d, 4)
})
t.test('join all afterAsync together in one function', function() {
let flaska = new Flaska({}, faker)
flaska.afterAsync(function(ctx) { return Promise.resolve().then(function() { ctx.a = 1 }) })
flaska.afterAsync(function(ctx) { ctx.b = 2 })
flaska.afterAsync(function(ctx) { return new Promise(function(res) { ctx.c = 3; res() }) })
flaska.afterAsync(function(ctx) { ctx.d = 4 })
assert.notOk(flaska._afterAsyncCompiled)
flaska.compile()
assert.ok(flaska._afterAsyncCompiled)
assert.strictEqual(typeof(flaska._afterAsyncCompiled), 'function')
let ctx = createCtx()
return flaska._afterAsyncCompiled(ctx).then(function() {
assert.strictEqual(ctx.a, 1)
assert.strictEqual(ctx.b, 2)
assert.strictEqual(ctx.c, 3)
assert.strictEqual(ctx.d, 4)
})
})
})
t.describe('#handleMiddleware()', function() {
t.test('should work with empty array', function() {
let flaska = new Flaska({}, faker)
flaska.handleMiddleware({}, [], 0)
})
t.test('should work with correct index', function() {
let checkIsTrue = false
let flaska = new Flaska({}, faker)
flaska.handleMiddleware({}, [
function() { throw new Error('should not be thrown') },
function() { throw new Error('should not be thrown') },
function() { throw new Error('should not be thrown') },
function() { checkIsTrue = true },
], 3)
assert.strictEqual(checkIsTrue, true)
})
t.test('should work with static functions', function() {
const assertCtx = createCtx({ a: 1 })
let checkCounter = 0
let flaska = new Flaska({}, faker)
flaska.handleMiddleware(assertCtx, [
function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ },
function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ },
function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ },
function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ },
function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ },
], 0)
assert.strictEqual(checkCounter, 5)
})
t.test('should work with random promises inbetween', function() {
const assertCtx = createCtx({ a: 1 })
let checkCounter = 0
let flaska = new Flaska({}, faker)
let result = flaska.handleMiddleware(assertCtx, [
function(ctx) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 0); checkCounter++ },
function(ctx) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 1); checkCounter++ },
function(ctx) { return new Promise(function(res) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 2); checkCounter++; res() }) },
function(ctx) { return Promise.resolve().then(function() { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 3); checkCounter++ }) },
function(ctx) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 4); checkCounter++ },
function(ctx) { return Promise.resolve().then(function() { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 5); checkCounter++ }) },
function(ctx) { return Promise.resolve().then(function() { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 6); checkCounter++ }) },
function(ctx) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 7); checkCounter++ },
function(ctx) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 8); checkCounter++ },
], 0)
assert.ok(result)
assert.strictEqual(typeof(result.then), 'function')
return result.then(function() {
assert.strictEqual(checkCounter, 9)
})
})
t.test('should work with rejected promises inbetween', async function() {
const assertError = { a: 1 }
let checkCounter = 0
let flaska = new Flaska