Skip to content

Commit

Permalink
feat(initial): Initial implementation with full test coverage.
Browse files Browse the repository at this point in the history
  • Loading branch information
mikeal committed Aug 30, 2017
0 parents commit 9254603
Show file tree
Hide file tree
Showing 6 changed files with 413 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.nyc_output
coverage
package-lock.json
node_modules
163 changes: 163 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/* globals fetch, Headers */
/* istanbul ignore next */
if (!process.browser) {
global.fetch = require('node-fetch')
global.Headers = global.fetch.Headers
}

const caseless = require('caseless')
const toTypedArray = require('typedarray-to-buffer')

const makeHeaders = obj => {
let h = new Headers()
for (let key in obj) {
h.append(key, obj[key])
}
return h
}

const makeBody = value => {
// TODO: Streams support.
if (typeof value === 'string') {
value = Buffer.from(value)
}
/* Can't test Blob types in Node.js */
/* istanbul ignore else */
if (Buffer.isBuffer(value)) {
value = toTypedArray(value)
}
return value
}

const resolvable = () => {
let _resolve
let _reject
let p = new Promise((resolve, reject) => {
_resolve = resolve
_reject = reject
})
p.resolve = (...args) => _resolve(...args)
p.reject = (...args) => _reject(...args)
return p
}

class R2 {
constructor (...args) {
this.opts = {method: 'GET'}
this.response = resolvable()
this._headers = {}
this._caseless = caseless(this._headers)

let failSet = () => { throw new Error('Cannot set read-only property.') }
Object.defineProperty(this, 'json', {
get: () => this.response.then(resp => resp.json()),
set: failSet
})
Object.defineProperty(this, 'text', {
get: () => this.response.then(resp => resp.text()),
set: failSet
})
Object.defineProperty(this, 'arrayBuffer', {
get: () => this.response.then(resp => resp.arrayBuffer()),
set: failSet
})
Object.defineProperty(this, 'blob', {
get: () => this.response.then(resp => resp.blob()),
set: failSet
})
Object.defineProperty(this, 'fromData', {
/* This isn't implemented in the shim yet */
get: /* istanbul ignore next */
() => this.response.then(resp => resp.formData()),
set: failSet
})

this._args(...args)

setTimeout(() => {
this._request()
}, 0)
}
_args (...args) {
let opts = this.opts
if (typeof args[0] === 'string') {
opts.url = args.shift()
}
if (typeof args[0] === 'object') {
opts = Object.assign(opts, args.shift())
}
if (opts.headers) this.setHeaders(opts.headers)
this.opts = opts
}
put (...args) {
this.opts.method = 'PUT'
this._args(...args)
return this
}
get (...args) {
this.opts.method = 'GET'
this._args(...args)
return this
}
post (...args) {
this.opts.method = 'POST'
this._args(...args)
return this
}
head (...args) {
this.opts.method = 'HEAD'
this._args(...args)
return this
}
patch (...args) {
this.opts.method = 'PATCH'
this._args(...args)
return this
}
delete (...args) {
this.opts.method = 'DELETE'
this._args(...args)
return this
}
_request () {
let url = this.opts.url
delete this.opts.url

if (this.opts.json) {
this.opts.body = JSON.stringify(this.opts.json)
this.setHeader('content-type', 'application/json')
delete this.opts.json
}

if (this.opts.body) {
this.opts.body = makeBody(this.opts.body)
}

// TODO: formData API.

this.opts.headers = makeHeaders(this._headers)

fetch(url, this.opts)
.then(resp => this.response.resolve(resp))
.catch(err => this.response.reject(err))
}
setHeaders (obj) {
for (let key in obj) {
this._caseless.set(key, obj[key])
}
return this
}
setHeader (key, value) {
let o = {}
o[key] = value
return this.setHeaders(o)
}
}

module.exports = (...args) => new R2(...args)
module.exports.put = (...args) => new R2().put(...args)
module.exports.get = (...args) => new R2().get(...args)
module.exports.post = (...args) => new R2().post(...args)
module.exports.head = (...args) => new R2().head(...args)
module.exports.patch = (...args) => new R2().patch(...args)
module.exports.delete = (...args) => new R2().delete(...args)
40 changes: 40 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "r2",
"version": "0.1.0",
"description": "",
"main": "index.js",
"scripts": {
"commit": "git-cz",
"test": "tap test/test-*.js --100",
"posttest": "standard",
"coverage": "tap test/test-*.js --coverage-report=lcov",
"postcoverage": "codecov",
"precommit": "npm test",
"prepush": "npm test",
"commitmsg": "validate-commit-msg"
},
"keywords": [],
"author": "Mikeal Rogers <mikeal.rogers@gmail.com> (http://www.mikealrogers.com)",
"license": "Apache-2.0",
"dependencies": {
"node-fetch": "^2.0.0-alpha.8",
"typedarray-to-buffer": "^3.1.2"
},
"devDependencies": {
"blob-to-buffer": "^1.2.6",
"body": "^5.1.0",
"codecov": "^2.3.0",
"commitizen": "^2.9.6",
"cz-conventional-changelog": "^2.0.0",
"fake-indexeddb": "^2.0.3",
"husky": "^0.14.3",
"lucass": "^4.1.0",
"semantic-release": "^7.0.2",
"standard": "^10.0.3",
"tap": "^10.7.2",
"validate-commit-msg": "^2.14.0"
},
"browser": {
"node-fetch": false
}
}
125 changes: 125 additions & 0 deletions test/test-basics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
const r2 = require('../')
const test = require('tap').test
const http = require('http')
const promisify = require('util').promisify
const body = promisify(require('body'))

let createServer = handler => {
let server = http.createServer(handler)
server._close = server.close
server.close = promisify(cb => server._close(cb))
server._listen = server.listen
server.listen = promisify((port, cb) => server._listen(port, cb))
return server
}

let url = 'http://localhost:1123/test'

test('basic get', async t => {
t.plan(2)
let server = createServer((req, res) => {
t.same(req.url, '/test')
res.end('ok')
})
await server.listen(1123)
t.same(await r2(url).text, 'ok')
await server.close()
})

test('basic put', async t => {
t.plan(3)
let server = createServer(async (req, res) => {
t.same(req.url, '/test')
t.same(await body(req), 'test')
res.end('ok')
})
await server.listen(1123)
t.same(await r2.put(url, {body: 'test'}).text, 'ok')
await server.close()
})

test('json put and get', async t => {
t.plan(4)
let server = createServer(async (req, res) => {
t.same(req.url, '/test')
t.same(req.headers['content-type'], 'application/json')
t.same(await body(req), '{"t":"test"}')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ok: true}))
})
await server.listen(1123)
t.same(await r2.put(url, {json: {t: 'test'}}).json, {ok: true})
await server.close()
})

test('headers', async t => {
t.plan(3)
let server = createServer(async (req, res) => {
t.same(req.url, '/test')
t.same(req.headers['x-test'], 'blah')
res.end('test')
})
await server.listen(1123)
t.same(await r2.get(url, {headers: {'x-test': 'blah'}}).text, 'test')
await server.close()
})

test('post', async t => {
t.plan(3)
let server = createServer(async (req, res) => {
t.same(req.url, '/test')
t.same(req.method, 'POST')
res.end('test')
})
await server.listen(1123)
t.same(await r2.post(url).text, 'test')
await server.close()
})

test('head', async t => {
t.plan(3)
let server = createServer(async (req, res) => {
t.same(req.url, '/test')
t.same(req.method, 'HEAD')
res.end('test')
})
await server.listen(1123)
t.same(await r2.head(url).text, '')
await server.close()
})

test('delete', async t => {
t.plan(3)
let server = createServer(async (req, res) => {
t.same(req.url, '/test')
t.same(req.method, 'DELETE')
res.end('test')
})
await server.listen(1123)
t.same(await r2.delete(url).text, 'test')
await server.close()
})

test('patch', async t => {
t.plan(3)
let server = createServer(async (req, res) => {
t.same(req.url, '/test')
t.same(req.method, 'PATCH')
res.end('test')
})
await server.listen(1123)
t.same(await r2.patch(url).text, 'test')
await server.close()
})

test('put buffer', async t => {
t.plan(3)
let server = createServer(async (req, res) => {
t.same(req.url, '/test')
t.same(await body(req), 'test')
res.end('ok')
})
await server.listen(1123)
t.same(await r2.put(url, {body: Buffer.from('test')}).text, 'ok')
await server.close()
})
43 changes: 43 additions & 0 deletions test/test-errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const r2 = require('../')
const http = require('http')
const test = require('tap').test
const promisify = require('util').promisify

let createServer = handler => {
let server = http.createServer(handler)
server._close = server.close
server.close = promisify(cb => server._close(cb))
server._listen = server.listen
server.listen = promisify((port, cb) => server._listen(port, cb))
return server
}

test('cannot connect', async t => {
t.plan(2)
let msg = 'request to http://localhost:1234/ failed, reason: connect ECONNREFUSED 127.0.0.1:1234'
try {
await r2('http://localhost:1234').response
} catch (e) {
t.type(e, 'Error')
t.same(e.message, msg)
}
})

test('set read-only property', async t => {
t.plan(2)
let msg = 'Cannot set read-only property.'
let server = createServer((req, res) => {
res.end()
})
await server.listen(1123)
let r
try {
r = r2('http://localhost:1123/test')
r.json = 'asdf'
} catch (e) {
t.type(e, 'Error')
t.same(e.message, msg)
}
await r.text
await server.close()
})
Loading

0 comments on commit 9254603

Please sign in to comment.