flaska: Add jsonhandler and basic httperror class
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded

flaska: Add promise-based listen and close methods
This commit is contained in:
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 if [ $? -eq 0 ] ; then
echo "Release already exists, nothing to do."; echo "Release already exists, nothing to do.";
else else
./test/7zas a -mx9 "${CURR_VER}_sc-core.7z" package.json index.mjs cli.mjs core bin
echo "Creating release on gitea" echo "Creating release on gitea"
RELEASE_RESULT=$(curl \ RELEASE_RESULT=$(curl \
-X POST \ -X POST \
@ -59,13 +58,6 @@ on_success:
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases \ 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}\"}") -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 '//registry.npmjs.org/:_authToken=${npmtoken}' > ~/.npmrc
echo "Publishing new version to npm" echo "Publishing new version to npm"
npm publish 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 { export class FlaskaRouter {
constructor() { constructor() {
this.root = new Branch() 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') { if (typeof(port) !== 'number') {
throw new Error('Flaska.listen() called with non-number in port') throw new Error('Flaska.listen() called with non-number in port')
} }
this.compile() this.compile()
this.server = this.http.createServer(this.requestStart.bind(this)) 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", "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.", "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", "main": "flaska.mjs",
"scripts": { "scripts": {
"test": "eltro" "test": "eltro",
"test:watch": "npm-watch test"
},
"watch": {
"test": {
"patterns": [
"test/*",
"flaska.mjs"
],
"extensions": "mjs",
"quiet": true,
"inherit": true
}
}, },
"type": "module", "type": "module",
"repository": { "repository": {
@ -27,7 +39,8 @@
}, },
"homepage": "https://github.com/nfp-projects/bottle-node#readme", "homepage": "https://github.com/nfp-projects/bottle-node#readme",
"devDependencies": { "devDependencies": {
"eltro": "^1.1.0" "eltro": "^1.3.1",
"formidable": "^1.2.2"
}, },
"files": [ "files": [
"flaska.mjs", "flaska.mjs",

View file

@ -15,7 +15,6 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const opts = defaults(defaults(options, { const opts = defaults(defaults(options, {
agent: false,
method: method, method: method,
timeout: 500, timeout: 500,
protocol: urlObj.protocol, protocol: urlObj.protocol,
@ -24,6 +23,7 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) {
host: urlObj.hostname, host: urlObj.hostname,
port: Number(urlObj.port), port: Number(urlObj.port),
path: urlObj.pathname + urlObj.search, path: urlObj.pathname + urlObj.search,
headers: {},
})) }))
const req = http.request(opts) const req = http.request(opts)
@ -33,7 +33,6 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) {
req.on('error', reject) req.on('error', reject)
req.on('timeout', function() { req.on('timeout', function() {
console.log(req.destroy())
reject(new Error(`Request ${method} ${path} timed out`)) reject(new Error(`Request ${method} ${path} timed out`))
}) })
req.on('response', res => { req.on('response', res => {
@ -45,6 +44,7 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) {
}) })
res.on('end', function () { res.on('end', function () {
if (!output) return resolve(null)
try { try {
output = JSON.parse(output) output = JSON.parse(output)
} catch (e) { } catch (e) {
@ -63,6 +63,63 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) {
}) })
} }
Client.prototype.get = function(path = '/') { Client.prototype.get = function(url = '/') {
return this.customRequest('GET', path, null) 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 { Flaska } from '../flaska.mjs'
import { spy, createCtx, fakeHttp } from './helper.mjs' import { createCtx, fakeHttp } from './helper.mjs'
const faker = fakeHttp() const faker = fakeHttp()
@ -441,12 +441,39 @@ t.describe('#listen()', function() {
t.test('call http correctly', function() { t.test('call http correctly', function() {
const assertPort = 325897235 const assertPort = 325897235
const assertIp = 'asdga'
const assertCb = function() { } const assertCb = function() { }
let checkPort = null let checkPort = null
let checkIp = null
let checkListenCb = null let checkListenCb = null
let testFaker = fakeHttp(null, function(port, cb) { let testFaker = fakeHttp(null, function(port, ip, cb) {
checkPort = port 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 checkListenCb = cb
}) })
let flaska = new Flaska({}, testFaker) let flaska = new Flaska({}, testFaker)
@ -457,6 +484,7 @@ t.describe('#listen()', function() {
} }
flaska.listen(assertPort, assertCb) flaska.listen(assertPort, assertCb)
assert.strictEqual(checkPort, assertPort) assert.strictEqual(checkPort, assertPort)
assert.strictEqual(checkIp, '::')
assert.strictEqual(checkListenCb, assertCb) assert.strictEqual(checkListenCb, assertCb)
}) })
@ -484,3 +512,131 @@ t.describe('#listen()', function() {
assert.strictEqual(checkInternalThis, flaska) 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 { Flaska } from '../flaska.mjs'
import { fakeHttp, createReq, spy, createRes } from './helper.mjs' import { fakeHttp, createReq, createRes } from './helper.mjs'
const faker = fakeHttp() 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 { Flaska, FlaskaRouter } from '../flaska.mjs'
import { fakeHttp, createCtx, spy } from './helper.mjs' import { fakeHttp, createCtx } from './helper.mjs'
const fakerHttp = fakeHttp() const fakerHttp = fakeHttp()
const fakeStream = { pipeline: spy() } const fakeStream = { pipeline: spy() }

View file

@ -1,40 +1,19 @@
import { spy } from 'eltro'
const indexMap = [ const indexMap = [
'firstCall', 'firstCall',
'secondCall', 'secondCall',
'thirdCall', '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) { export function fakeHttp(inj1, inj2) {
let intermediate = { let intermediate = {
createServer: function(cb) { createServer: function(cb) {
if (inj1) inj1(cb) if (inj1) inj1(cb)
intermediate.fakeRequest = cb intermediate.fakeRequest = cb
return { return {
listen: function(port, cb) { listen: function(port, ip, cb) {
if (inj2) inj2(port, cb) if (inj2) inj2(port, ip, cb)
else if (cb) cb() else if (cb) cb()
} }
} }

View file

@ -6,24 +6,36 @@ const port = 51024
const flaska = new Flaska({}) const flaska = new Flaska({})
const client = new Client(port) const client = new Client(port)
let reqBody = null
flaska.get('/', function(ctx) { flaska.get('/', function(ctx) {
ctx.body = { status: true } ctx.body = { status: true }
}) })
flaska.post('/test', function(ctx) {
t.before(function(cb) { ctx.body = { success: true }
flaska.listen(port, cb) // console.log(ctx)
}) })
t.describe('', function() { t.before(function() {
t.test('/ should return status true', function() { return flaska.listenAsync(port)
return client.get().then(function(body) { })
t.describe('/', function() {
t.test('should return status true', function() {
return client.get('/').then(function(body) {
assert.deepEqual(body, { status: true }) assert.deepEqual(body, { status: true })
}) })
}) })
}) })
t.after(function(cb) { t.describe('/test', function() {
setTimeout(function() { t.test('should return success and store body', async function() {
flaska.server.close(cb) reqBody = null
}, 1000) 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 { Eltro as t, assert} from 'eltro'
import { QueryHandler } from '../flaska.mjs' import { QueryHandler, JsonHandler } from '../flaska.mjs'
t.describe('#QueryHandler()', function() { t.describe('#QueryHandler()', function() {
let queryHandler = QueryHandler() let queryHandler = QueryHandler()
@ -22,3 +22,27 @@ t.describe('#QueryHandler()', function() {
assert.strictEqual(ctx.query.get('ITEM2'), 'hello world') 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