flaska: Add jsonhandler and basic httperror class
continuous-integration/appveyor/branch AppVeyor build succeeded Details

flaska: Add promise-based listen and close methods
master v0.9.9
Jonatan Nilsson 2022-03-16 10:41:25 +00:00
parent 5a28a0cf78
commit 9c43b429c4
14 changed files with 358 additions and 61 deletions

1
.npmrc Normal file
View File

@ -0,0 +1 @@
package-lock=false

View File

@ -51,7 +51,6 @@ on_success:
if [ $? -eq 0 ] ; then
echo "Release already exists, nothing to do.";
else
./test/7zas a -mx9 "${CURR_VER}_sc-core.7z" package.json index.mjs cli.mjs core bin
echo "Creating release on gitea"
RELEASE_RESULT=$(curl \
-X POST \
@ -59,13 +58,6 @@ on_success:
-H "Content-Type: application/json" \
https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases \
-d "{\"tag_name\":\"v${CURR_VER}\",\"name\":\"v${CURR_VER}\",\"body\":\"Automatic release from Appveyor from ${APPVEYOR_REPO_COMMIT} :\n\n${APPVEYOR_REPO_COMMIT_MESSAGE}\"}")
RELEASE_ID=$(echo $RELEASE_RESULT | jq -r .id)
echo "Adding ${CURR_VER}_sc-core.7z to release ${RELEASE_ID}"
curl \
-X POST \
-H "Authorization: token $deploytoken" \
-F "attachment=@${CURR_VER}_sc-core.7z" \
https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases/$RELEASE_ID/assets
echo '//registry.npmjs.org/:_authToken=${npmtoken}' > ~/.npmrc
echo "Publishing new version to npm"
npm publish

View File

@ -57,6 +57,33 @@ export function QueryHandler() {
}
}
export function JsonHandler() {
return async function(ctx) {
const buffers = [];
for await (const chunk of ctx.req) {
buffers.push(chunk);
}
const data = Buffer.concat(buffers).toString();
ctx.req.body = JSON.parse(data)
}
}
export class HttpError extends Error {
constructor(statusCode, message, body = null) {
super(message);
Error.captureStackTrace(this, HttpError);
let proto = Object.getPrototypeOf(this);
proto.name = 'HttpError';
this.status = statusCode
}
}
export class FlaskaRouter {
constructor() {
this.root = new Branch()
@ -547,12 +574,48 @@ export class Flaska {
}
}
listen(port, cb) {
listen(port, orgIp, orgcb) {
let ip = orgIp
let cb = orgcb
if (!cb && typeof(orgIp) === 'function') {
ip = '::'
cb = orgIp
}
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)
this.server.listen(port, ip, cb)
}
listenAsync(port, ip = '::') {
if (typeof(port) !== 'number') {
return Promise.reject(new Error('Flaska.listen() called with non-number in port'))
}
this.compile()
this.server = this.http.createServer(this.requestStart.bind(this))
return new Promise((res, rej) => {
this.server.listen(port, ip, function(err) {
if (err) return rej(err)
return res()
})
})
}
closeAsync() {
if (!this.server) return Promise.resolve()
return new Promise((res, rej) => {
this.server.close(function(err) {
if (err) { return rej(err) }
// Waiting 0.1 second for it to close down
setTimeout(function() { res() }, 100)
})
})
}
}

View File

@ -1,10 +1,22 @@
{
"name": "flaska",
"version": "0.9.8",
"version": "0.9.9",
"description": "Flaska is a micro web-framework for node. It is designed to be fast, simple and lightweight, and is distributed as a single file module with no dependencies.",
"main": "flaska.mjs",
"scripts": {
"test": "eltro"
"test": "eltro",
"test:watch": "npm-watch test"
},
"watch": {
"test": {
"patterns": [
"test/*",
"flaska.mjs"
],
"extensions": "mjs",
"quiet": true,
"inherit": true
}
},
"type": "module",
"repository": {
@ -27,7 +39,8 @@
},
"homepage": "https://github.com/nfp-projects/bottle-node#readme",
"devDependencies": {
"eltro": "^1.1.0"
"eltro": "^1.3.1",
"formidable": "^1.2.2"
},
"files": [
"flaska.mjs",

View File

@ -15,7 +15,6 @@ 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,
@ -24,6 +23,7 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) {
host: urlObj.hostname,
port: Number(urlObj.port),
path: urlObj.pathname + urlObj.search,
headers: {},
}))
const req = http.request(opts)
@ -33,7 +33,6 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) {
req.on('error', reject)
req.on('timeout', function() {
console.log(req.destroy())
reject(new Error(`Request ${method} ${path} timed out`))
})
req.on('response', res => {
@ -45,6 +44,7 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) {
})
res.on('end', function () {
if (!output) return resolve(null)
try {
output = JSON.parse(output)
} catch (e) {
@ -63,6 +63,63 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) {
})
}
Client.prototype.get = function(path = '/') {
return this.customRequest('GET', path, null)
}
Client.prototype.get = function(url = '/') {
return this.customRequest('GET', url, null)
}
Client.prototype.post = function(url = '/', body = {}) {
let parsed = JSON.stringify(body)
return this.customRequest('POST', url, parsed, {
headers: {
'Content-Type': 'application/json',
'Content-Length': parsed.length,
},
})
}
Client.prototype.del = function(url = '/', body = {}) {
return this.customRequest('DELETE', url, JSON.stringify(body))
}
const random = (length = 8) => {
// Declare all characters
let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
// Pick characers randomly
let str = '';
for (let i = 0; i < length; i++) {
str += chars.charAt(Math.floor(Math.random() * chars.length));
}
return str;
}
Client.prototype.upload = function(url, file, method = 'POST', body = {}) {
return fs.readFile(file).then(data => {
const crlf = '\r\n'
const filename = path.basename(file)
const boundary = `---------${random(32)}`
const multipartBody = Buffer.concat([
Buffer.from(
`${crlf}--${boundary}${crlf}` +
`Content-Disposition: form-data; name="file"; filename="${filename}"` + crlf + crlf
),
data,
Buffer.concat(Object.keys(body).map(function(key) {
return Buffer.from(''
+ `${crlf}--${boundary}${crlf}`
+ `Content-Disposition: form-data; name="${key}"` + crlf + crlf
+ JSON.stringify(body[key])
)
})),
Buffer.from(`${crlf}--${boundary}--`),
])
return this.customRequest(method, url, multipartBody, {
timeout: 5000,
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Content-Length': multipartBody.length,
},
})
})
}

View File

@ -1,6 +1,6 @@
import { Eltro as t, assert} from 'eltro'
import { Eltro as t, assert, stub, spy } from 'eltro'
import { Flaska } from '../flaska.mjs'
import { spy, createCtx, fakeHttp } from './helper.mjs'
import { createCtx, fakeHttp } from './helper.mjs'
const faker = fakeHttp()
@ -441,12 +441,39 @@ t.describe('#listen()', function() {
t.test('call http correctly', function() {
const assertPort = 325897235
const assertIp = 'asdga'
const assertCb = function() { }
let checkPort = null
let checkIp = null
let checkListenCb = null
let testFaker = fakeHttp(null, function(port, cb) {
let testFaker = fakeHttp(null, function(port, ip, cb) {
checkPort = port
checkIp = ip
checkListenCb = cb
})
let flaska = new Flaska({}, testFaker)
assert.ok(flaska.requestStart)
flaska.requestStart = function() {
checkInternalThis = this
checkIsTrue = true
}
flaska.listen(assertPort, assertIp, assertCb)
assert.strictEqual(checkPort, assertPort)
assert.strictEqual(checkIp, assertIp)
assert.strictEqual(checkListenCb, assertCb)
})
t.test('call http correctly if only port is specified', function() {
const assertPort = 325897235
const assertCb = function() { }
let checkPort = null
let checkIp = null
let checkListenCb = null
let testFaker = fakeHttp(null, function(port, ip, cb) {
checkPort = port
checkIp = ip
checkListenCb = cb
})
let flaska = new Flaska({}, testFaker)
@ -457,6 +484,7 @@ t.describe('#listen()', function() {
}
flaska.listen(assertPort, assertCb)
assert.strictEqual(checkPort, assertPort)
assert.strictEqual(checkIp, '::')
assert.strictEqual(checkListenCb, assertCb)
})
@ -484,3 +512,131 @@ t.describe('#listen()', function() {
assert.strictEqual(checkInternalThis, flaska)
})
})
t.describe('#listenAsync()', function() {
t.test('it should throw if missing port', async function() {
let flaska = new Flaska({}, faker)
let tests = [
undefined,
'asdf',
'123',
[],
{},
function() {},
]
let errors = await Promise.all(tests.map(x => assert.isRejected(flaska.listenAsync(x))))
assert.strictEqual(errors.length, tests.length)
for (let i = 0; i < errors.length; i++) {
assert.match(errors[i].message, /[Pp]ort/)
}
})
t.test('it should automatically call compile', async function() {
let assertCalled = false
let flaska = new Flaska({}, faker)
flaska.compile = function() { assertCalled = true }
await flaska.listenAsync(404)
assert.strictEqual(assertCalled, true)
})
t.test('call http correctly', async function() {
const assertPort = 325897235
const assertIp = 'asdf'
let checkPort = null
let checkIp = null
let testFaker = fakeHttp(null, function(port, ip, cb) {
checkPort = port
checkIp = ip
cb()
})
let flaska = new Flaska({}, testFaker)
assert.ok(flaska.requestStart)
flaska.requestStart = function() {
checkInternalThis = this
checkIsTrue = true
}
await flaska.listenAsync(assertPort, assertIp)
assert.strictEqual(checkPort, assertPort)
assert.strictEqual(checkIp, assertIp)
})
t.test('call http correctly if only port specified', async function() {
const assertPort = 325897235
let checkPort = null
let checkIp = null
let testFaker = fakeHttp(null, function(port, ip, cb) {
checkPort = port
checkIp = ip
cb()
})
let flaska = new Flaska({}, testFaker)
assert.ok(flaska.requestStart)
flaska.requestStart = function() {
checkInternalThis = this
checkIsTrue = true
}
await flaska.listenAsync(assertPort)
assert.strictEqual(checkPort, assertPort)
assert.strictEqual(checkIp, '::')
})
t.test('register requestStart if no async', async function() {
let checkIsTrue = false
let checkInternalThis = null
let checkHandler = null
let testFaker = fakeHttp(function(cb) {
checkHandler = cb
})
let flaska = new Flaska({}, testFaker)
assert.ok(flaska.requestStart)
flaska.requestStart = function() {
checkInternalThis = this
checkIsTrue = true
}
await flaska.listenAsync(404)
assert.strictEqual(typeof(checkHandler), 'function')
assert.notStrictEqual(checkHandler, flaska.requestStart)
assert.notStrictEqual(checkIsTrue, true)
assert.notStrictEqual(checkInternalThis, flaska)
checkHandler()
assert.strictEqual(checkIsTrue, true)
assert.strictEqual(checkInternalThis, flaska)
})
})
t.describe('#closeAsync()', function() {
t.test('it return if server is null', function() {
let flaska = new Flaska()
flaska.server = null
return flaska.closeAsync()
})
t.test('it should call close on server correctly', async function() {
const assertError = new Error('Pirate Fight')
let flaska = new Flaska()
flaska.server = {
close: stub()
}
flaska.server.close.returnWith(function(cb) {
cb(assertError)
})
let err = await assert.isRejected(flaska.closeAsync())
assert.strictEqual(err, assertError)
})
t.test('should otherwise work', async function() {
let flaska = new Flaska()
flaska.server = {
close: stub()
}
flaska.server.close.returnWith(function(cb) {
cb(null, { a: 1 })
})
let res = await flaska.closeAsync()
assert.notOk(res)
})
})

View File

View File

View File

@ -1,6 +1,6 @@
import { Eltro as t, assert} from 'eltro'
import { Eltro as t, spy, assert} from 'eltro'
import { Flaska } from '../flaska.mjs'
import { fakeHttp, createReq, spy, createRes } from './helper.mjs'
import { fakeHttp, createReq, createRes } from './helper.mjs'
const faker = fakeHttp()

View File

@ -1,6 +1,6 @@
import { Eltro as t, assert} from 'eltro'
import { Eltro as t, spy, assert} from 'eltro'
import { Flaska, FlaskaRouter } from '../flaska.mjs'
import { fakeHttp, createCtx, spy } from './helper.mjs'
import { fakeHttp, createCtx } from './helper.mjs'
const fakerHttp = fakeHttp()
const fakeStream = { pipeline: spy() }

View File

@ -1,40 +1,19 @@
import { spy } from 'eltro'
const indexMap = [
'firstCall',
'secondCall',
'thirdCall',
]
export function spy() {
let calls = []
let called = 0
let func = function(...args) {
func.called = true
calls.push(args)
if (called < indexMap.length) {
func[indexMap[called]] = args
}
called++
func.callCount = called
}
func.called = false
func.callCount = called
func.onCall = function(i) {
return calls[i]
}
for (let i = 0; i < indexMap.length; i++) {
func[indexMap] = null
}
return func
}
export function fakeHttp(inj1, inj2) {
let intermediate = {
createServer: function(cb) {
if (inj1) inj1(cb)
intermediate.fakeRequest = cb
return {
listen: function(port, cb) {
if (inj2) inj2(port, cb)
listen: function(port, ip, cb) {
if (inj2) inj2(port, ip, cb)
else if (cb) cb()
}
}

View File

@ -6,24 +6,36 @@ const port = 51024
const flaska = new Flaska({})
const client = new Client(port)
let reqBody = null
flaska.get('/', function(ctx) {
ctx.body = { status: true }
})
t.before(function(cb) {
flaska.listen(port, cb)
flaska.post('/test', function(ctx) {
ctx.body = { success: true }
// console.log(ctx)
})
t.describe('', function() {
t.test('/ should return status true', function() {
return client.get().then(function(body) {
t.before(function() {
return flaska.listenAsync(port)
})
t.describe('/', function() {
t.test('should return status true', function() {
return client.get('/').then(function(body) {
assert.deepEqual(body, { status: true })
})
})
})
t.after(function(cb) {
setTimeout(function() {
flaska.server.close(cb)
}, 1000)
t.describe('/test', function() {
t.test('should return success and store body', async function() {
reqBody = null
let body = await client.post('/test')
assert.deepEqual(body, { success: true })
})
})
t.after(function() {
return flaska.closeAsync()
})

View File

@ -1,5 +1,5 @@
import { Eltro as t, assert} from 'eltro'
import { QueryHandler } from '../flaska.mjs'
import { QueryHandler, JsonHandler } from '../flaska.mjs'
t.describe('#QueryHandler()', function() {
let queryHandler = QueryHandler()
@ -22,3 +22,27 @@ t.describe('#QueryHandler()', function() {
assert.strictEqual(ctx.query.get('ITEM2'), 'hello world')
})
})
t.describe('#JsonHandler()', function() {
let jsonHandler = JsonHandler()
t.test('should return a handler', function() {
assert.strictEqual(typeof(jsonHandler), 'function')
})
t.test('should support separating query from request url', async function() {
const assertBody = { a: 1, temp: 'test', hello: 'world'}
let parsed = JSON.stringify(assertBody)
const ctx = {
req: [
Promise.resolve(Buffer.from(parsed.slice(0, parsed.length / 2))),
Promise.resolve(Buffer.from(parsed.slice(parsed.length / 2))),
]
}
await jsonHandler(ctx)
assert.notStrictEqual(ctx.req.body, assertBody)
assert.deepStrictEqual(ctx.req.body, assertBody)
})
})

BIN
test/test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB