flaska: Add jsonhandler and basic httperror class
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
flaska: Add promise-based listen and close methods
This commit is contained in:
parent
5a28a0cf78
commit
9c43b429c4
14 changed files with 358 additions and 61 deletions
1
.npmrc
Normal file
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
package-lock=false
|
|
@ -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
|
||||
|
|
67
flaska.mjs
67
flaska.mjs
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
19
package.json
19
package.json
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
0
test/flaska.body.test.mjs
Normal file
0
test/flaska.body.test.mjs
Normal file
0
test/flaska.formidable.test.mjs
Normal file
0
test/flaska.formidable.test.mjs
Normal 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()
|
||||
|
||||
|
|
|
@ -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() }
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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
BIN
test/test.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 278 KiB |
Loading…
Reference in a new issue