Skip to content

Commit

Permalink
Large refactor of cross platform bodies
Browse files Browse the repository at this point in the history
  • Loading branch information
blakeembrey committed Apr 7, 2018
1 parent 7252dc7 commit 40754f5
Show file tree
Hide file tree
Showing 28 changed files with 500 additions and 310 deletions.
33 changes: 19 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ npm install servie --save
* [`servie-errorhandler`](https://github.com/serviejs/servie-errorhandler) Standard error handler for transport layers
* [`servie-finalhandler`](https://github.com/serviejs/servie-finalhandler) Standard final handler for transport layers

### `ServieBase`
### `Servie`

> Base HTTP class for common request and response logic.
```ts
import { ServieBase } from 'servie'
import { Servie } from 'servie'
```

#### Options
Expand Down Expand Up @@ -70,7 +70,7 @@ import { ServieBase } from 'servie'

### `Request`

> HTTP class for encapsulating a `Request`, extends `ServieBase`.
> HTTP class for encapsulating a `Request`, extends `Servie`.
```ts
import { Request } from 'servie'
Expand All @@ -85,7 +85,7 @@ const request = new Request({
})
```

> Extends `ServieBase` options.
> Extends `Servie` options.
* `url` The HTTP request url (`string`)
* `method?` The HTTP request method (`string`, default: `GET`)
Expand All @@ -111,7 +111,7 @@ const request = new Request({

### `Response`

> HTTP class for encapsulating a `Response`, extends `ServieBase`.
> HTTP class for encapsulating a `Response`, extends `Servie`.
```ts
import { Response } from 'servie'
Expand All @@ -123,7 +123,7 @@ import { Response } from 'servie'
const response = new Response({})
```

> Extends `ServieBase` options.
> Extends `Servie` options.
* `status?` The HTTP response status code (`number`)
* `statusText?` The HTTP response status message (`string`)
Expand All @@ -135,12 +135,14 @@ const response = new Response({})

### `Headers`

> Used by `ServieBase` for `Request` and `Response` objects.
> Used by `Servie` for `Request`, `Response` and `Body` objects.
#### Options

Take a single parameter with the headers in raw array format.

**Tip:** Use `createHeaders(value?: any)` to create a `Headers` instance from raw data (e.g. `HeadersObject | string[] | null`).

#### Properties

* `rawHeaders` The raw HTTP headers list (`string[]`)
Expand All @@ -163,7 +165,6 @@ Take a single parameter with the headers in raw array format.
#### Static Methods

* `is(obj: any): boolean` Checks if an object is `Headers`
* `from(obj: HeadersObject | string[]): Headers` Return a `Headers` instance from supported inputs

### `Body`

Expand All @@ -172,12 +173,15 @@ Take a single parameter with the headers in raw array format.
#### Options

```ts
const response = new Body({})
const body = new Body({})
```

* `rawBody?` Supported body type (`any`)
* `headers?` Headers related to the body (e.g. `Content-Type`, `Content-Length`) (`Headers`)
* `buffered?` Boolean indicating if the raw body is entirely in memory (`boolean`)
* `rawBody` Supported body type (`any`)
* `headers?` Headers related to the body, e.g. `Content-Type` (`Headers`)

**Tip:** Use `createBody(value?: any)` to create a `Body` instance from raw data (e.g. `Readable | ReadableStream | Buffer | ArrayBuffer | object | string | null`).

`Body` is the most complex part of Servie due to support for node.js and browsers. TypeScript also [lacks a good story](https://github.com/Microsoft/TypeScript/issues/7753) for universal modules with code paths offering different features (e.g. `Buffer` in node.js, `ReadableStream` in browsers) so there's some logic duplication to support requiring via `servie/dist/body/node` or via `servie/dist/body/browser`. If you are a module author supporting only browsers or node.js, feel free to use the `NodeBody` or `BrowserBody` exports to provide a better DX.

#### Properties

Expand All @@ -189,14 +193,15 @@ const response = new Body({})
#### Methods

* `text(): Promise<string>` Returns body as a UTF-8 string
* `json(): Promise<any>` Returns body parsed as JSON
* `arrayBuffer(): Promise<ArrayBuffer>` Returns the body as an `ArrayBuffer` instance
* `buffer(): Promise<Buffer>` Returns the body as a `Buffer` instance (node.js)
* `stream(): Readable` Returns a readable node.js stream (node.js)
* `arrayBuffer(): Promise<ArrayBuffer>` Returns the body as an `ArrayBuffer` instance (browsers)
* `readableStream(): ReadableStream` Returns a readable WHATWG stream (browsers)

#### Static Methods

* `is(obj: any): boolean` Checks if an object is `Body`
* `from(obj: Body | Stream | ArrayBuffer | string | object): Body` Return a `Body` instance from supported inputs

### `HttpError`

Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"logo.svg"
],
"browser": {
"./dist/body/index": "./dist/body/browser"
"./dist/body/node": "./dist/body/browser"
},
"scripts": {
"lint": "tslint \"src/**/*.ts\" --project tsconfig.json",
Expand Down Expand Up @@ -67,7 +67,7 @@
"ts-jest": "^22.4.2",
"tslint": "^5.9.1",
"tslint-config-standard": "^7.0.0",
"typescript": "^2.7.2"
"typescript": "^2.8.1"
},
"dependencies": {
"byte-length": "^1.0.2",
Expand Down
18 changes: 9 additions & 9 deletions src/base.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ServieBase } from './base'
import { Headers } from './headers'
import { Body } from './body'
import { Servie } from './base'
import { Headers, createHeaders } from './headers'
import { Body, createBody } from './body'

describe('servie base', () => {
it('should contain base properties', () => {
const base = new ServieBase()
const base = new Servie()

expect(base.headers).toBeInstanceOf(Headers)
expect(base.trailers).toBeInstanceOf(Headers)
Expand All @@ -14,18 +14,18 @@ describe('servie base', () => {
})

it('should use instances of headers and body', () => {
const body = new Body()
const body = createBody()
const headers = new Headers()
const base = new ServieBase({ headers, body })
const base = new Servie({ headers, body })

expect(base.headers).toBe(headers)
expect(base.body).toBe(body)
})

it('should combine body headers and base headers', () => {
const body = Body.from({ json: true })
const headers = Headers.from(['X-Powered-By', 'Servie'])
const base = new ServieBase({ headers, body })
const body = createBody({ json: true })
const headers = createHeaders(['X-Powered-By', 'Servie'])
const base = new Servie({ headers, body })

expect(Array.from(base.getHeaders().entries())).toEqual([
['X-Powered-By', 'Servie'],
Expand Down
23 changes: 11 additions & 12 deletions src/base.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,31 @@
import { EventEmitter } from 'events'
import { Body, createBody } from './body'
import { Headers, createHeaders } from './headers'

import { Body } from './body'
import { Headers } from './headers'

export interface ServieBaseOptions {
export interface ServieOptions {
events?: EventEmitter
headers?: Headers
trailers?: Headers
body?: Body
}

export class ServieBase implements ServieBaseOptions {
export class Servie implements ServieOptions {

events: EventEmitter
readonly events: EventEmitter
protected _body!: Body
protected _headers!: Headers
protected _trailers!: Headers
protected _bytesTransferred = 0

constructor ({ trailers, headers, events, body }: ServieBaseOptions = {}) {
this.events = events || new EventEmitter()
this.headers = headers || new Headers()
this.trailers = trailers || new Headers()
this.body = body || new Body()
constructor (options: ServieOptions = {}) {
this.events = options.events || new EventEmitter()
this.headers = options.headers || createHeaders()
this.trailers = options.trailers || createHeaders()
this.body = options.body || createBody()
}

getHeaders () {
const headers = Headers.from(this.headers)
const headers = this.headers.clone()
for (const [key, value] of this.body.headers.entries()) {
if (!headers.has(key)) headers.append(key, value)
}
Expand Down
82 changes: 21 additions & 61 deletions src/body/base.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
import { byteLength } from 'byte-length'

import { Headers } from '../headers'

export interface BodyBaseOptions <T> {
rawBody?: T
export interface BodyOptions <T> {
rawBody: T
headers?: Headers
buffered?: boolean
}

export type RawBodyBase = string
export type BodyBaseFrom = object | string | null | undefined

export class BodyBase<T = any> implements BodyBaseOptions<T> {
export abstract class Body <T = any> implements BodyOptions<T> {

readonly rawBody?: T
readonly buffered!: boolean
readonly rawBody!: T
readonly bodyUsed!: boolean
readonly hasBody!: boolean
readonly headers!: Headers

constructor (options: BodyBaseOptions<T> = {}) {
readonly buffered: boolean = true

constructor (options: BodyOptions<T>) {
Object.defineProperty(this, 'rawBody', {
configurable: true,
value: options.rawBody
Expand All @@ -33,69 +28,28 @@ export class BodyBase<T = any> implements BodyBaseOptions<T> {
// These properties do not change after initialisation.
Object.defineProperty(this, 'hasBody', { value: options.rawBody !== undefined })
Object.defineProperty(this, 'headers', { value: options.headers || new Headers() })
Object.defineProperty(this, 'buffered', { value: !!options.buffered })
}

static is (obj: any): obj is BodyBase {
static is (obj: any): obj is Body<any> {
return typeof obj === 'object' &&
typeof obj.useRawBody === 'function' &&
typeof obj.bodyUsed === 'boolean'
}

static from (obj: BodyBaseFrom): BodyBase<any> {
return new this(this.configure(obj))
}

static configure (obj: BodyBaseFrom): BodyBaseOptions<any> {
if (obj === undefined) return {}

if (BodyBase.is(obj)) {
return {
rawBody: obj.useRawBody(x => x),
buffered: obj.buffered,
headers: obj.headers
}
}

if (typeof obj === 'string') {
const headers = Headers.from({
'Content-Type': 'text/plain',
'Content-Length': byteLength(obj)
})

return { rawBody: obj, buffered: true, headers }
}

const str = JSON.stringify(obj)

const headers = Headers.from({
'Content-Type': 'application/json',
'Content-Length': byteLength(str)
})

return { rawBody: str, buffered: true, headers }
}

useRawBody <U> (fn: (rawBody: T | undefined) => U): U {
useRawBody () {
if (this.bodyUsed) throw new TypeError('Body already used')

const result = fn(this.rawBody)
const rawBody = this.rawBody
Object.defineProperty(this, 'rawBody', { value: undefined })
Object.defineProperty(this, 'bodyUsed', { value: true })
return result
return rawBody
}

async text (): Promise<string> {
return this.useRawBody(async rawBody => {
if (rawBody === undefined) return ''
if (typeof rawBody === 'string') return rawBody
clone (): this {
const rawBody = this.useRawBody()
const headers = this.headers

throw new TypeError('`Body#text()` not implemented')
})
}

async json () {
return JSON.parse(await this.text())
return new (this as any).constructor({ rawBody, headers })
}

toJSON (): object {
Expand All @@ -107,4 +61,10 @@ export class BodyBase<T = any> implements BodyBaseOptions<T> {
}
}

abstract text (): Promise<string>

abstract json (): Promise<any>

abstract arrayBuffer (): Promise<ArrayBuffer>

}

0 comments on commit 40754f5

Please sign in to comment.