From 796fb54a6b36870e13fd49cf8ab6e901140e77cf Mon Sep 17 00:00:00 2001 From: gcanti Date: Fri, 16 Feb 2018 12:42:37 +0100 Subject: [PATCH] upgrade to `fp-ts@1.x.x` --- CHANGELOG.md | 5 + README.md | 151 ++++++++-------- examples/express.ts | 20 +-- examples/koa.ts | 18 +- examples/tsconfig.json | 17 ++ package.json | 9 +- src/{adapters/express.ts => ExpressConn.ts} | 36 ++-- src/{adapters/koa.ts => KoaConn.ts} | 36 ++-- src/MiddlewareState.ts | 44 ++--- src/MiddlewareTask.ts | 76 ++++---- src/index.ts | 185 +++++++++++--------- src/toExpressRequestHandler.ts | 8 + src/toKoaRequestHandler.ts | 7 + test/index.ts | 108 +++++------- 14 files changed, 360 insertions(+), 360 deletions(-) create mode 100644 examples/tsconfig.json rename src/{adapters/express.ts => ExpressConn.ts} (53%) rename src/{adapters/koa.ts => KoaConn.ts} (55%) create mode 100644 src/toExpressRequestHandler.ts create mode 100644 src/toKoaRequestHandler.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c643c03..565f6f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ **Note**: Gaps between patch versions are faulty/broken releases. **Note**: A feature tagged as Experimental is in a high state of flux, you're at risk of it changing without notice. +# 0.3.0 + +* **Breaking Change** + * upgrade to `fp-ts@1.x.x` (@gcanti) + # 0.2.0 * **Breaking Change** diff --git a/README.md b/README.md index 79405e9..e5d17fe 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,16 @@ State changes are tracked by the phanton type `S`. ```ts class Conn { readonly _S: S - constructor(readonly req: express.Request, readonly res: express.Response) {} + clearCookie: (name: string, options: CookieOptions) => void + endResponse: () => void + getBody: () => mixed + getHeader: (name: string) => mixed + getParams: () => mixed + getQuery: () => mixed + setBody: (body: mixed) => void + setCookie: (name: string, value: string, options: CookieOptions) => void + setHeader: (name: string, value: string) => void + setStatus: (status: Status) => void } ``` @@ -58,16 +67,18 @@ The default interpreter, `MiddlewareTask`, is based on [fp-ts](https://github.co ```ts import * as express from 'express' -import { status, closeHeaders, send } from 'hyper-ts/lib/MiddlewareTask' import { Status } from 'hyper-ts' +import { middleware } from 'hyper-ts/lib/MiddlewareTask' +import { toExpressRequestHandler } from 'hyper-ts/lib/toExpressRequestHandler' -const hello = status(Status.OK) - .ichain(() => closeHeaders) - .ichain(() => send('Hello hyper-ts!')) +const hello = middleware + .status(Status.OK) + .ichain(() => middleware.closeHeaders) + .ichain(() => middleware.send('Hello hyper-ts on express!')) -const app = express() -app.get('/', hello.toRequestHandler()) -app.listen(3000, () => console.log('App listening on port 3000!')) +express() + .get('/', toExpressRequestHandler(hello)) + .listen(3000, () => console.log('Express listening on port 3000. Use: GET /')) ``` ## Type safety @@ -75,14 +86,15 @@ app.listen(3000, () => console.log('App listening on port 3000!')) Invalid operations are prevented statically ```ts -import { status, closeHeaders, send, header } from 'hyper-ts/lib/MiddlewareTask' +import { middleware } from 'hyper-ts/lib/MiddlewareTask' import { Status } from 'hyper-ts' -const hello = status(Status.OK) - .ichain(() => closeHeaders) - .ichain(() => send('Hello hyper-ts!')) +middleware + .status(Status.OK) + .ichain(() => middleware.closeHeaders) + .ichain(() => middleware.send('Hello hyper-ts!')) // try to write a header after sending the body - .ichain(() => header(['field', 'value'])) // error: Type '"HeadersOpen"' is not assignable to type '"ResponseEnded"' + .ichain(() => middleware.headers({ field: 'value' })) // error: Type '"HeadersOpen"' is not assignable to type '"ResponseEnded"' ``` No more `"Can't set headers after they are sent."` errors. @@ -108,7 +120,7 @@ Here I'm using `t.string` but you can pass _any_ `io-ts` runtime type ```ts import { IntegerFromString } from 'io-ts-types/lib/number/IntegerFromString' -// validation succeeds only if `req.param.user_id` is an integer +// validation succeeds only if `req.param.user_id` can be parsed to an integer param('user_id', IntegerFromString) ``` @@ -153,22 +165,14 @@ ensure this requirement statically ```ts import * as express from 'express' -import { - status, - closeHeaders, - send, - MiddlewareTask, - param, - of, - Handler, - unsafeResponseStateTransition -} from 'hyper-ts/lib/MiddlewareTask' -import { Status, StatusOpen } from 'hyper-ts' +import { MiddlewareTask, param, Handler, unsafeResponseStateTransition, middleware } from 'hyper-ts/lib/MiddlewareTask' +import { Status, StatusOpen, Conn } from 'hyper-ts' import { Option, some, none } from 'fp-ts/lib/Option' import * as t from 'io-ts' -import * as task from 'fp-ts/lib/Task' +import { Task, task } from 'fp-ts/lib/Task' import { tuple } from 'fp-ts/lib/function' import { IntegerFromString } from 'io-ts-types/lib/number/IntegerFromString' +import { toExpressRequestHandler } from 'hyper-ts/lib/toExpressRequestHandler' // the new connection state type Authenticated = 'Authenticated' @@ -176,15 +180,15 @@ type Authenticated = 'Authenticated' interface Authentication extends MiddlewareTask>> {} -const withAuthentication = (strategy: (req: express.Request) => task.Task): Authentication => +const withAuthentication = (strategy: (c: Conn) => Task): Authentication => new MiddlewareTask(c => { - return strategy(c.req).map(authenticated => tuple(authenticated ? some(unsafeResponseStateTransition) : none, c)) + return strategy(c).map(authenticated => tuple(authenticated ? some(unsafeResponseStateTransition) : none, c)) }) // dummy authentication process -const tokenAuthentication = withAuthentication(req => task.of(t.string.is(req.get('token')))) +const tokenAuthentication = withAuthentication(c => task.of(t.string.is(c.getHeader('token')))) -// dummy ResponseStateTransition (like closeHeaders) +// dummy ResponseStateTransition (like middleware.closeHeaders) const authenticated: MiddlewareTask = unsafeResponseStateTransition // @@ -192,19 +196,22 @@ const authenticated: MiddlewareTask = unsafeRes // const badRequest = (message: string) => - status(Status.BadRequest) - .ichain(() => closeHeaders) - .ichain(() => send(message)) + middleware + .status(Status.BadRequest) + .ichain(() => middleware.closeHeaders) + .ichain(() => middleware.send(message)) const notFound = (message: string) => - status(Status.NotFound) - .ichain(() => closeHeaders) - .ichain(() => send(message)) + middleware + .status(Status.NotFound) + .ichain(() => middleware.closeHeaders) + .ichain(() => middleware.send(message)) const unauthorized = (message: string) => - status(Status.Unauthorized) - .ichain(() => closeHeaders) - .ichain(() => send(message)) + middleware + .status(Status.Unauthorized) + .ichain(() => middleware.closeHeaders) + .ichain(() => middleware.send(message)) // // user @@ -215,32 +222,34 @@ interface User { } // the result of this function requires a successful authentication upstream -const loadUser = (id: number) => authenticated.ichain(() => of(id === 1 ? some({ name: 'Giulio' }) : none)) +const loadUser = (id: number) => + authenticated.ichain(() => middleware.of(id === 1 ? some({ name: 'Giulio' }) : none)) const getUserId = param('user_id', IntegerFromString) const sendUser = (user: User) => - status(Status.OK) - .ichain(() => closeHeaders) - .ichain(() => send(`Hello ${user.name}!`)) + middleware + .status(Status.OK) + .ichain(() => middleware.closeHeaders) + .ichain(() => middleware.send(`Hello ${user.name}!`)) const user: Handler = getUserId.ichain(oid => oid.fold( () => badRequest('Invalid user id'), id => tokenAuthentication.ichain(oAuthenticated => - oAuthenticated.fold( + oAuthenticated.foldL( () => unauthorized('Unauthorized user'), authenticated => - authenticated.ichain(() => loadUser(id).ichain(ou => ou.fold(() => notFound('User not found'), sendUser))) + authenticated.ichain(() => loadUser(id).ichain(ou => ou.foldL(() => notFound('User not found'), sendUser))) ) ) ) ) -const app = express() -app.get('/:user_id', user.toRequestHandler()) -app.listen(3000, () => console.log('App listening on port 3000!')) +express() + .get('/:user_id', toExpressRequestHandler(user)) + .listen(3000, () => console.log('Express listening on port 3000')) ``` # Using the State monad for writing tests @@ -249,47 +258,43 @@ There's another interpreter for testing purposes: `MiddlewareState` ```ts import * as express from 'express' -import { MonadMiddleware, StatusOpen, ResponseEnded, Conn, param, Status } from 'hyper-ts' -import { monadMiddlewareTask } from 'hyper-ts/lib/MiddlewareTask' -import { monadMiddlewareState } from 'hyper-ts/lib/MiddlewareState' -import { HKT3, HKT3S, HKT3As } from 'fp-ts/lib/HKT' +import { MonadMiddleware, MonadMiddleware3, StatusOpen, ResponseEnded, Conn, param, Status } from 'hyper-ts' +import { middleware as middlewareTask } from 'hyper-ts/lib/MiddlewareTask' +import { middleware as middlewareState } from 'hyper-ts/lib/MiddlewareState' +import { HKT3, URIS3, Type3 } from 'fp-ts/lib/HKT' import * as t from 'io-ts' +import { toExpressRequestHandler } from 'hyper-ts/lib/toExpressRequestHandler' -function program(R: MonadMiddleware): HKT3As +function program(R: MonadMiddleware3): Type3 function program(R: MonadMiddleware): HKT3 function program(R: MonadMiddleware): HKT3 { - return R.ichain( - e => - R.ichain( - () => R.send(`Hello ${e.getOrElseValue('Anonymous')}!`), - R.ichain(() => R.closeHeaders, R.status(Status.OK)) - ), - param(R)('name', t.string) + return R.ichain(param(R)('name', t.string), e => + R.ichain(R.ichain(R.status(Status.OK), () => R.closeHeaders), () => R.send(`Hello ${e.getOrElse('Anonymous')}!`)) ) } // interpreted in Task -const helloTask = program(monadMiddlewareTask) +const programTask = program(middlewareTask) // interpreted in State -const helloState = program(monadMiddlewareState) +const programState = program(middlewareState) // fake Conn const c: Conn = { - req: { - params: {} - }, - res: { - status: () => null, - send: () => null - } + getParams: () => ({}), + setStatus: () => null, + setBody: () => null } as any -console.log(helloState.eval(c).run([])) +console.log(programState.eval(c).run([])) -const app = express() -app.get('/:name?', helloTask.toRequestHandler()) -app.listen(3000, () => console.log('App listening on port 3000!')) +// +// express app +// + +express() + .get('/:name?', toExpressRequestHandler(programTask)) + .listen(3000, () => console.log('Express listening on port 3000')) /* Output: diff --git a/examples/express.ts b/examples/express.ts index d0aee17..7d3178d 100644 --- a/examples/express.ts +++ b/examples/express.ts @@ -1,17 +1,13 @@ import * as express from 'express' -import { Status, StatusOpen, ResponseEnded } from '../src' -import { status, closeHeaders, send, MiddlewareTask } from '../src/MiddlewareTask' -import { ExpressConn } from '../src/adapters/express' +import { Status } from '../src' +import { middleware } from '../src/MiddlewareTask' +import { toExpressRequestHandler } from '../src/toExpressRequestHandler' -const hello = status(Status.OK) - .ichain(() => closeHeaders) - .ichain(() => send('Hello hyper-ts on express!')) - -export const toRequestHandler = (task: MiddlewareTask): express.RequestHandler => ( - req, - res -) => task.eval(new ExpressConn(req, res)).run() +const hello = middleware + .status(Status.OK) + .ichain(() => middleware.closeHeaders) + .ichain(() => middleware.send('Hello hyper-ts on express!')) express() - .get('/', toRequestHandler(hello)) + .get('/', toExpressRequestHandler(hello)) .listen(3000, () => console.log('Express listening on port 3000. Use: GET /')) diff --git a/examples/koa.ts b/examples/koa.ts index 41f4f93..3ffc66c 100644 --- a/examples/koa.ts +++ b/examples/koa.ts @@ -1,17 +1,15 @@ import * as Koa from 'koa' -import { Status, StatusOpen, ResponseEnded } from '..' -import { status, closeHeaders, send, MiddlewareTask } from '../src/MiddlewareTask' -import { KoaConn } from '../src/adapters/koa' +import { Status } from '..' +import { middleware } from '../src/MiddlewareTask' +import { toKoaRequestHandler } from '../src/toKoaRequestHandler' -const hello = status(Status.OK) - .ichain(() => closeHeaders) - .ichain(() => send('Hello hyper-ts on koa!')) +const hello = middleware + .status(Status.OK) + .ichain(() => middleware.closeHeaders) + .ichain(() => middleware.send('Hello hyper-ts on koa!')) const app = new Koa() -const toRequestHandler = (task: MiddlewareTask): Koa.Middleware => ctx => - task.eval(new KoaConn(ctx)).run() - -app.use(toRequestHandler(hello)).listen(3000, () => { +app.use(toKoaRequestHandler(hello)).listen(3000, () => { console.log('Koa listening on port 3000. Use: GET /') }) diff --git a/examples/tsconfig.json b/examples/tsconfig.json new file mode 100644 index 0000000..d3f46b1 --- /dev/null +++ b/examples/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "commonjs", + "strict": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "target": "es5", + "moduleResolution": "node", + "forceConsistentCasingInFileNames": true, + "lib": [ + "es6", + "dom" + ] + } +} diff --git a/package.json b/package.json index 8dd2421..8329254 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hyper-ts", - "version": "0.2.0", + "version": "0.3.0", "description": "hyper-ts description", "files": ["lib"], "main": "lib/index.js", @@ -9,7 +9,7 @@ "lint": "tslint src/**/*.ts test/**/*.ts", "typings-checker": "typings-checker --allow-expect-error --project typings-checker/tsconfig.json typings-checker/index.ts", - "mocha": "mocha -r ts-node/register test/*.ts", + "mocha": "TS_NODE_CACHE=false mocha -r ts-node/register test/*.ts", "prettier": "prettier --no-semi --single-quote --print-width 120 --parser typescript --list-different \"{src,test}/**/*.ts\"", "fix-prettier": @@ -29,14 +29,15 @@ }, "homepage": "https://github.com/gcanti/hyper-ts", "dependencies": { - "fp-ts": "^0.6.8", - "io-ts": "^0.9.5" + "fp-ts": "^1.0.1", + "io-ts": "^1.0.2" }, "devDependencies": { "@types/mocha": "2.2.38", "@types/node": "8.0.19", "@types/qs": "^6.5.1", "autocannon": "^0.16.5", + "io-ts-types": "^0.3.0", "mocha": "3.2.0", "prettier": "1.9.2", "qs": "^6.5.1", diff --git a/src/adapters/express.ts b/src/ExpressConn.ts similarity index 53% rename from src/adapters/express.ts rename to src/ExpressConn.ts index fc255c0..7dd26d5 100644 --- a/src/adapters/express.ts +++ b/src/ExpressConn.ts @@ -1,49 +1,39 @@ import * as express from 'express' -import { CookieOptions, Conn, Status } from '..' +import { CookieOptions, Conn, Status } from '.' import { mixed } from 'io-ts' export class ExpressConn implements Conn { - public readonly '-S': S - + // prettier-ignore + readonly '_S': S constructor(readonly req: express.Request, readonly res: express.Response) {} - - public clearCookie(name: string, options: CookieOptions) { + clearCookie(name: string, options: CookieOptions) { this.res.clearCookie(name, options) } - - public endResponse() { + endResponse() { return this.res.end() } - - public getBody() { + getBody() { return this.req.body } - - public getHeader(name: string) { + getHeader(name: string) { return this.req.header(name) } - - public getParams() { + getParams() { return this.req.params } - - public getQuery() { + getQuery() { return this.req.query } - - public setBody(body: mixed) { + setBody(body: mixed) { this.res.send(body) } - - public setCookie(name: string, value: string, options: CookieOptions) { + setCookie(name: string, value: string, options: CookieOptions) { this.res.cookie(name, value, options) } - - public setHeader(name: string, value: string) { + setHeader(name: string, value: string) { this.res.setHeader(name, value) } - - public setStatus(status: Status) { + setStatus(status: Status) { this.res.status(status) } } diff --git a/src/adapters/koa.ts b/src/KoaConn.ts similarity index 55% rename from src/adapters/koa.ts rename to src/KoaConn.ts index a269f9b..940c050 100644 --- a/src/adapters/koa.ts +++ b/src/KoaConn.ts @@ -1,49 +1,39 @@ -import { Conn, Status, CookieOptions } from '..' +import { Conn, Status, CookieOptions } from '.' import * as koa from 'koa' import { mixed } from 'io-ts' export class KoaConn implements Conn { - public readonly '-S': S - + // prettier-ignore + readonly '_S': S constructor(readonly context: koa.Context) {} - - public clearCookie(name: string, options: CookieOptions) { + clearCookie(name: string, options: CookieOptions) { this.context.cookies.set(name, undefined, options) } - - public endResponse() { + endResponse() { return this.context.response.res.end() } - - public getBody() { + getBody() { return this.context.request.body } - - public getHeader(name: string) { + getHeader(name: string) { return this.context.get(name) } - - public getParams() { + getParams() { return this.context.params } - - public getQuery() { + getQuery() { return this.context.query } - - public setBody(body: mixed) { + setBody(body: mixed) { this.context.body = body } - - public setCookie(name: string, value: string, options: CookieOptions) { + setCookie(name: string, value: string, options: CookieOptions) { this.context.cookies.set(name, value, options) } - - public setHeader(name: string, value: string) { + setHeader(name: string, value: string) { this.context.set(name, value) } - - public setStatus(status: Status) { + setStatus(status: Status) { this.context.status = status } } diff --git a/src/MiddlewareState.ts b/src/MiddlewareState.ts index fd40259..1f4d5b9 100644 --- a/src/MiddlewareState.ts +++ b/src/MiddlewareState.ts @@ -6,11 +6,11 @@ import { HeadersOpen, BodyOpen, ResponseEnded, - MonadMiddleware, + MonadMiddleware3, CookieOptions } from './index' import { State } from 'fp-ts/lib/State' -import * as state from 'fp-ts/lib/State' +import { state } from 'fp-ts/lib/State' const t = getMiddlewareT(state) @@ -88,7 +88,7 @@ export class MiddlewareState { return t.evalMiddleware(this.run, c) } map(this: MiddlewareState, f: (a: A) => B): MiddlewareState { - return new MiddlewareState(t.map(f, this.run)) + return new MiddlewareState(t.map(this.run, f)) } ap(this: MiddlewareState, fab: MiddlewareState B>): MiddlewareState { return new MiddlewareState(t.ap(fab.run, this.run)) @@ -97,35 +97,35 @@ export class MiddlewareState { return this.ichain(f) } ichain(f: (a: A) => MiddlewareState): MiddlewareState { - return new MiddlewareState(t.ichain(a => f(a).run, this.run)) + return new MiddlewareState(t.ichain(this.run, a => f(a).run)) } } -export const of = (a: A): MiddlewareState => { +const of = (a: A): MiddlewareState => { return new MiddlewareState(t.of(a)) } -export const map = (f: (a: A) => B, fa: MiddlewareState): MiddlewareState => { +const map = (fa: MiddlewareState, f: (a: A) => B): MiddlewareState => { return fa.map(f) } -export const ap = ( +const ap = ( fab: MiddlewareState B>, fa: MiddlewareState ): MiddlewareState => { return fa.ap(fab) } -export const chain = ( - f: (a: A) => MiddlewareState, - fa: MiddlewareState +const chain = ( + fa: MiddlewareState, + f: (a: A) => MiddlewareState ): MiddlewareState => { return fa.chain(f) } -export const ichain = ( - f: (a: A) => MiddlewareState, - fa: MiddlewareState +const ichain = ( + fa: MiddlewareState, + f: (a: A) => MiddlewareState ): MiddlewareState => { return fa.ichain(f) } @@ -134,7 +134,7 @@ export const lift = (fa: State): MiddlewareState => { return new MiddlewareState(t.lift(fa)) } -export const gets = (f: (c: Conn) => A): MiddlewareState => { +const gets = (f: (c: Conn) => A): MiddlewareState => { return new MiddlewareState(t.gets(f)) } @@ -152,29 +152,29 @@ const transition = (f: () => Event): ResponseStateTransition => }) ) -export const status = (status: Status): ResponseStateTransition => +const status = (status: Status): ResponseStateTransition => transition(() => new StatusEvent(status)) -export const headers = (headers: { [key: string]: string }): ResponseStateTransition => +const headers = (headers: { [key: string]: string }): ResponseStateTransition => transition(() => new HeadersEvent(headers)) -export const closeHeaders: ResponseStateTransition = transition(() => new CloseHeadersEvent()) +const closeHeaders: ResponseStateTransition = transition(() => new CloseHeadersEvent()) -export const send = (o: string): ResponseStateTransition => transition(() => new SendEvent(o)) +const send = (o: string): ResponseStateTransition => transition(() => new SendEvent(o)) -export const end: ResponseStateTransition = transition(() => new EndEvent()) +const end: ResponseStateTransition = transition(() => new EndEvent()) -export const cookie = ( +const cookie = ( name: string, value: string, options: CookieOptions ): ResponseStateTransition => transition(() => new CookieEvent(name, value, options)) -export const clearCookie = (name: string, options: CookieOptions): ResponseStateTransition => +const clearCookie = (name: string, options: CookieOptions): ResponseStateTransition => transition(() => new ClearCookieEvent(name, options)) /** @instance */ -export const monadMiddlewareState: MonadMiddleware = { +export const middleware: MonadMiddleware3 = { URI, map, of, diff --git a/src/MiddlewareTask.ts b/src/MiddlewareTask.ts index a0b8fa4..5b6ea50 100644 --- a/src/MiddlewareTask.ts +++ b/src/MiddlewareTask.ts @@ -7,7 +7,7 @@ import { BodyOpen, ResponseEnded, MediaType, - MonadMiddleware, + MonadMiddleware3, contentType as contentType_, json as json_, redirect as redirect_, @@ -19,8 +19,8 @@ import { CookieOptions } from './index' import { Task } from 'fp-ts/lib/Task' -import * as task from 'fp-ts/lib/Task' -import { Decoder, Validation } from 'io-ts' +import { task } from 'fp-ts/lib/Task' +import { Decoder, Validation, mixed } from 'io-ts' const t = getMiddlewareT(task) @@ -48,7 +48,7 @@ export class MiddlewareTask { return t.evalMiddleware(this.run, c) } map(this: MiddlewareTask, f: (a: A) => B): MiddlewareTask { - return new MiddlewareTask(t.map(f, this.run)) + return new MiddlewareTask(t.map(this.run, f)) } ap(this: MiddlewareTask, fab: MiddlewareTask B>): MiddlewareTask { return new MiddlewareTask(t.ap(fab.run, this.run)) @@ -57,35 +57,29 @@ export class MiddlewareTask { return this.ichain(f) } ichain(f: (a: A) => MiddlewareTask): MiddlewareTask { - return new MiddlewareTask(t.ichain(a => f(a).run, this.run)) + return new MiddlewareTask(t.ichain(this.run, a => f(a).run)) } } -export const of = (a: A): MiddlewareTask => { +const of = (a: A): MiddlewareTask => { return new MiddlewareTask(t.of(a)) } -export const map = (f: (a: A) => B, fa: MiddlewareTask): MiddlewareTask => { +const map = (fa: MiddlewareTask, f: (a: A) => B): MiddlewareTask => { return fa.map(f) } -export const ap = ( - fab: MiddlewareTask B>, - fa: MiddlewareTask -): MiddlewareTask => { +const ap = (fab: MiddlewareTask B>, fa: MiddlewareTask): MiddlewareTask => { return fa.ap(fab) } -export const chain = ( - f: (a: A) => MiddlewareTask, - fa: MiddlewareTask -): MiddlewareTask => { +const chain = (fa: MiddlewareTask, f: (a: A) => MiddlewareTask): MiddlewareTask => { return fa.chain(f) } -export const ichain = ( - f: (a: A) => MiddlewareTask, - fa: MiddlewareTask +const ichain = ( + fa: MiddlewareTask, + f: (a: A) => MiddlewareTask ): MiddlewareTask => { return fa.ichain(f) } @@ -94,7 +88,7 @@ export const lift = (fa: Task): MiddlewareTask => { return new MiddlewareTask(t.lift(fa)) } -export const gets = (f: (c: Conn) => A): MiddlewareTask => { +const gets = (f: (c: Conn) => A): MiddlewareTask => { return new MiddlewareTask(t.gets(f)) } @@ -113,10 +107,10 @@ const transition = (f: (c: Conn) => void): ResponseStateTransition => +const status = (status: Status): ResponseStateTransition => transition(c => c.setStatus(status)) -export const headers = (headers: { [key: string]: string }): ResponseStateTransition => +const headers = (headers: { [key: string]: string }): ResponseStateTransition => transition(c => { for (const field in headers) { c.setHeader(field, headers[field]) @@ -127,23 +121,23 @@ export const unsafeResponseStateTransition: ResponseStateTransition = task.of([undefined, c] as any) ) -export const closeHeaders: ResponseStateTransition = unsafeResponseStateTransition +const closeHeaders: ResponseStateTransition = unsafeResponseStateTransition -export const send = (o: string): ResponseStateTransition => transition(c => c.setBody(o)) +const send = (o: string): ResponseStateTransition => transition(c => c.setBody(o)) -export const end: ResponseStateTransition = transition(c => c.endResponse()) +const end: ResponseStateTransition = transition(c => c.endResponse()) -export const cookie = ( +const cookie = ( name: string, value: string, options: CookieOptions ): ResponseStateTransition => transition(c => c.setCookie(name, value, options)) -export const clearCookie = (name: string, options: CookieOptions): ResponseStateTransition => +const clearCookie = (name: string, options: CookieOptions): ResponseStateTransition => transition(c => c.clearCookie(name, options)) /** @instance */ -export const monadMiddlewareTask: MonadMiddleware = { +export const middleware: MonadMiddleware3 = { URI, map, of, @@ -162,33 +156,31 @@ export const monadMiddlewareTask: MonadMiddleware = { } export const contentType: (mediaType: MediaType) => ResponseStateTransition = contentType_( - monadMiddlewareTask + middleware ) -export const json: (o: string) => ResponseStateTransition = json_(monadMiddlewareTask) +export const json: (o: string) => ResponseStateTransition = json_(middleware) -export const redirect: (uri: string) => ResponseStateTransition = redirect_( - monadMiddlewareTask -) +export const redirect: (uri: string) => ResponseStateTransition = redirect_(middleware) export const param: ( name: string, - type: Decoder -) => MiddlewareTask> = param_(monadMiddlewareTask) + type: Decoder +) => MiddlewareTask> = param_(middleware) -export const params: (type: Decoder) => MiddlewareTask> = params_( - monadMiddlewareTask +export const params: (type: Decoder) => MiddlewareTask> = params_( + middleware ) -export const query: (type: Decoder) => MiddlewareTask> = query_( - monadMiddlewareTask +export const query: (type: Decoder) => MiddlewareTask> = query_( + middleware ) -export const body: (type: Decoder) => MiddlewareTask> = body_( - monadMiddlewareTask +export const body: (type: Decoder) => MiddlewareTask> = body_( + middleware ) export const header: ( name: string, - type: Decoder -) => MiddlewareTask> = header_(monadMiddlewareTask) + type: Decoder +) => MiddlewareTask> = header_(middleware) diff --git a/src/index.ts b/src/index.ts index f6a12fb..117ec81 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ -import { Monad } from 'fp-ts/lib/Monad' -import { HKT, HKTS, HKTAs, HKT3, HKT3S, HKT3As, HKT2S, HKT2As } from 'fp-ts/lib/HKT' +import { Monad, Monad1, Monad2 } from 'fp-ts/lib/Monad' +import { HKT, URIS, Type, HKT3, URIS3, Type3, URIS2, Type2 } from 'fp-ts/lib/HKT' import { tuple } from 'fp-ts/lib/function' -import { IxMonad } from 'fp-ts/lib/IxMonad' -import { Decoder, Validation, validate, Dictionary, mixed } from 'io-ts' +import { IxMonad, IxMonad3 } from 'fp-ts/lib/IxMonad' +import { Decoder, Validation, Dictionary, mixed } from 'io-ts' // Adapted from https://github.com/purescript-contrib/purescript-media-types export enum MediaType { @@ -62,17 +62,13 @@ export interface CookieOptions { * State changes are tracked by the phanton type `S` */ export interface Conn { - readonly '-S': S - + readonly _S: S clearCookie: (name: string, options: CookieOptions) => void - endResponse: () => void - getBody: () => mixed getHeader: (name: string) => mixed getParams: () => mixed getQuery: () => mixed - setBody: (body: mixed) => void setCookie: (name: string, value: string, options: CookieOptions) => void setHeader: (name: string, value: string) => void @@ -86,58 +82,58 @@ export interface Conn { */ export type Middleware = (c: Conn) => HKT]> -export type Middleware1 = (c: Conn) => HKTAs]> +export type Middleware1 = (c: Conn) => Type]> -export type Middleware2 = (c: Conn) => HKT2As]> +export type Middleware2 = (c: Conn) => Type2]> export interface MiddlewareT { - map: (f: (a: A) => B, fa: Middleware) => Middleware + map: (fa: Middleware, f: (a: A) => B) => Middleware of: (a: A) => Middleware ap: (fab: Middleware B>, fa: Middleware) => Middleware - chain: (f: (a: A) => Middleware, fa: Middleware) => Middleware - ichain: (f: (a: A) => Middleware, fa: Middleware) => Middleware + chain: (fa: Middleware, f: (a: A) => Middleware) => Middleware + ichain: (fa: Middleware, f: (a: A) => Middleware) => Middleware evalMiddleware: (ma: Middleware, c: Conn) => HKT lift: (fa: HKT) => Middleware gets: (f: (c: Conn) => A) => Middleware } -export interface MiddlewareT1 { - map: (f: (a: A) => B, fa: Middleware1) => Middleware1 +export interface MiddlewareT1 { + map: (fa: Middleware1, f: (a: A) => B) => Middleware1 of: (a: A) => Middleware1 ap: (fab: Middleware1 B>, fa: Middleware1) => Middleware1 - chain: (f: (a: A) => Middleware1, fa: Middleware1) => Middleware1 - ichain: (f: (a: A) => Middleware1, fa: Middleware1) => Middleware1 - evalMiddleware: (ma: Middleware1, c: Conn) => HKTAs - lift: (fa: HKTAs) => Middleware1 + chain: (fa: Middleware1, f: (a: A) => Middleware1) => Middleware1 + ichain: (fa: Middleware1, f: (a: A) => Middleware1) => Middleware1 + evalMiddleware: (ma: Middleware1, c: Conn) => Type + lift: (fa: Type) => Middleware1 gets: (f: (c: Conn) => A) => Middleware1 } -export interface MiddlewareT2 { - map: (f: (a: A) => B, fa: Middleware2) => Middleware2 +export interface MiddlewareT2 { + map: (fa: Middleware2, f: (a: A) => B) => Middleware2 of: (a: A) => Middleware2 ap: ( fab: Middleware2 B>, fa: Middleware2 ) => Middleware2 chain: ( - f: (a: A) => Middleware2, - fa: Middleware2 + fa: Middleware2, + f: (a: A) => Middleware2 ) => Middleware2 ichain: ( - f: (a: A) => Middleware2, - fa: Middleware2 + fa: Middleware2, + f: (a: A) => Middleware2 ) => Middleware2 - evalMiddleware: (ma: Middleware2, c: Conn) => HKT2As - lift: (fa: HKT2As) => Middleware2 + evalMiddleware: (ma: Middleware2, c: Conn) => Type2 + lift: (fa: Type2) => Middleware2 gets: (f: (c: Conn) => A) => Middleware2 } -export function getMiddlewareT(M: Monad): MiddlewareT2 -export function getMiddlewareT(M: Monad): MiddlewareT1 +export function getMiddlewareT(M: Monad2): MiddlewareT2 +export function getMiddlewareT(M: Monad1): MiddlewareT1 export function getMiddlewareT(M: Monad): MiddlewareT export function getMiddlewareT(M: Monad): MiddlewareT { - function map(f: (a: A) => B, fa: Middleware): Middleware { - return cf => M.map(([a, ct]) => tuple(f(a), ct), fa(cf)) + function map(fa: Middleware, f: (a: A) => B): Middleware { + return cf => M.map(fa(cf), ([a, ct]) => tuple(f(a), ct)) } function of(a: A): Middleware { @@ -148,27 +144,27 @@ export function getMiddlewareT(M: Monad): MiddlewareT { return c => { const ma = evalMiddleware(fa, c) const mab = evalMiddleware(fab, c) - return M.map(b => tuple(b, c), M.ap(mab, ma)) + return M.map(M.ap(mab, ma), b => tuple(b, c)) } } - function chain(f: (a: A) => Middleware, fa: Middleware): Middleware { - return ichain(f, fa) + function chain(fa: Middleware, f: (a: A) => Middleware): Middleware { + return ichain(fa, f) } function ichain( - f: (a: A) => Middleware, - fa: Middleware + fa: Middleware, + f: (a: A) => Middleware ): Middleware { - return ci => M.chain(([a, co]) => f(a)(co), fa(ci)) + return ci => M.chain(fa(ci), ([a, co]) => f(a)(co)) } function evalMiddleware(fa: Middleware, c: Conn): HKT { - return M.map(([a]) => a, fa(c)) + return M.map(fa(c), ([a]) => a) } function lift(fa: HKT): Middleware { - return c => M.map(a => tuple(a, c), fa) + return c => M.map(fa, a => tuple(a, c)) } function gets(f: (c: Conn) => A): Middleware { @@ -199,15 +195,23 @@ export type BodyOpen = 'BodyOpen' /** Type indicating that headers have already been sent, and that the body stream, and thus the response, is finished. */ export type ResponseEnded = 'ResponseEnded' -export interface Monad3 { +export interface InducedMonad { readonly URI: M - map(f: (a: A) => B, fa: HKT3): HKT3 - of(a: A): HKT3 - ap(fab: HKT3 B>, fa: HKT3): HKT3 - chain(f: (a: A) => HKT3, fa: HKT3): HKT3 + map: (fa: HKT3, f: (a: A) => B) => HKT3 + of: (a: A) => HKT3 + ap: (fab: HKT3 B>, fa: HKT3) => HKT3 + chain: (fa: HKT3, f: (a: A) => HKT3) => HKT3 } -export interface MonadMiddleware extends Monad3, IxMonad { +export interface InducedMonad3 { + readonly URI: M + map: (fa: Type3, f: (a: A) => B) => Type3 + of: (a: A) => Type3 + ap: (fab: Type3 B>, fa: Type3) => Type3 + chain: (fa: Type3, f: (a: A) => Type3) => Type3 +} + +export interface MonadMiddleware extends InducedMonad, IxMonad { status: (status: Status) => HKT3 headers: (headers: { [key: string]: string }) => HKT3 closeHeaders: HKT3 @@ -218,9 +222,20 @@ export interface MonadMiddleware extends Monad3, IxMonad { gets: (f: (c: Conn) => A) => HKT3 } -export function contentType( - R: MonadMiddleware -): (mediaType: MediaType) => HKT3As +export interface MonadMiddleware3 extends InducedMonad3, IxMonad3 { + status: (status: Status) => Type3 + headers: (headers: { [key: string]: string }) => Type3 + closeHeaders: Type3 + send: (o: string) => Type3 + end: Type3 + cookie: (name: string, value: string, options: CookieOptions) => Type3 + clearCookie: (name: string, options: CookieOptions) => Type3 + gets: (f: (c: Conn) => A) => Type3 +} + +export function contentType( + R: MonadMiddleware3 +): (mediaType: MediaType) => Type3 export function contentType(R: MonadMiddleware): (mediaType: MediaType) => HKT3 export function contentType( R: MonadMiddleware @@ -228,77 +243,77 @@ export function contentType( return mediaType => R.headers({ 'Content-Type': mediaType }) } -export function json(R: MonadMiddleware): (o: string) => HKT3As +export function json(R: MonadMiddleware3): (o: string) => Type3 export function json(R: MonadMiddleware): (o: string) => HKT3 export function json(R: MonadMiddleware): (o: string) => HKT3 { const contentType_ = contentType(R) - return o => R.ichain(() => R.send(o), R.ichain(() => R.closeHeaders, contentType_(MediaType.applicationJSON))) + return o => R.ichain(R.ichain(contentType_(MediaType.applicationJSON), () => R.closeHeaders), () => R.send(o)) } -export function redirect( - R: MonadMiddleware -): (uri: string) => HKT3As +export function redirect( + R: MonadMiddleware3 +): (uri: string) => Type3 export function redirect(R: MonadMiddleware): (uri: string) => HKT3 export function redirect(R: MonadMiddleware): (uri: string) => HKT3 { - return uri => R.ichain(() => R.headers({ Location: uri }), R.status(Status.Found)) + return uri => R.ichain(R.status(Status.Found), () => R.headers({ Location: uri })) } -export function param( - R: MonadMiddleware -): (name: string, type: Decoder) => HKT3As> +export function param( + R: MonadMiddleware3 +): (name: string, type: Decoder) => Type3> export function param( R: MonadMiddleware -): (name: string, type: Decoder) => HKT3> +): (name: string, type: Decoder) => HKT3> export function param( R: MonadMiddleware -): (name: string, type: Decoder) => HKT3> { - return (name, type) => R.gets(c => validate(c.getParams(), Dictionary).chain(params => validate(params[name], type))) +): (name: string, type: Decoder) => HKT3> { + return (name, type) => R.gets(c => Dictionary.decode(c.getParams()).chain(params => type.decode(params[name]))) } -export function params( - R: MonadMiddleware -): (type: Decoder) => HKT3As> +export function params( + R: MonadMiddleware3 +): (type: Decoder) => Type3> export function params( R: MonadMiddleware -): (type: Decoder) => HKT3> +): (type: Decoder) => HKT3> export function params( R: MonadMiddleware -): (type: Decoder) => HKT3> { - return type => R.gets(c => validate(c.getParams(), type)) +): (type: Decoder) => HKT3> { + return type => R.gets(c => type.decode(c.getParams())) } -export function query( - R: MonadMiddleware -): (type: Decoder) => HKT3As> +export function query( + R: MonadMiddleware3 +): (type: Decoder) => Type3> export function query( R: MonadMiddleware -): (type: Decoder) => HKT3> +): (type: Decoder) => HKT3> export function query( R: MonadMiddleware -): (type: Decoder) => HKT3> { - return type => R.gets(c => validate(c.getQuery(), type)) +): (type: Decoder) => HKT3> { + return type => R.gets(c => type.decode(c.getQuery())) } -export function body( - R: MonadMiddleware -): (type: Decoder) => HKT3As> +export function body( + R: MonadMiddleware3 +): (type: Decoder) => Type3> export function body( R: MonadMiddleware -): (type: Decoder) => HKT3> +): (type: Decoder) => HKT3> export function body( R: MonadMiddleware -): (type: Decoder) => HKT3> { - return type => R.gets(c => validate(c.getBody(), type)) +): (type: Decoder) => HKT3> { + return type => R.gets(c => type.decode(c.getBody())) } -export function header( - R: MonadMiddleware -): (name: string, type: Decoder) => HKT3As> +export function header( + R: MonadMiddleware3 +): (name: string, type: Decoder) => Type3> export function header( R: MonadMiddleware -): (name: string, type: Decoder) => HKT3> +): (name: string, type: Decoder) => HKT3> export function header( R: MonadMiddleware -): (name: string, type: Decoder) => HKT3> { - return (name, type) => R.gets(c => validate(c.getHeader(name), type)) +): (name: string, type: Decoder) => HKT3> { + return (name, type) => R.gets(c => type.decode(c.getHeader(name))) } diff --git a/src/toExpressRequestHandler.ts b/src/toExpressRequestHandler.ts new file mode 100644 index 0000000..b0aa4fa --- /dev/null +++ b/src/toExpressRequestHandler.ts @@ -0,0 +1,8 @@ +import * as express from 'express' +import { StatusOpen, ResponseEnded } from '.' +import { MiddlewareTask } from './MiddlewareTask' +import { ExpressConn } from './ExpressConn' + +export const toExpressRequestHandler = ( + task: MiddlewareTask +): express.RequestHandler => (req, res) => task.eval(new ExpressConn(req, res)).run() diff --git a/src/toKoaRequestHandler.ts b/src/toKoaRequestHandler.ts new file mode 100644 index 0000000..ad5ac77 --- /dev/null +++ b/src/toKoaRequestHandler.ts @@ -0,0 +1,7 @@ +import * as Koa from 'koa' +import { StatusOpen, ResponseEnded } from '.' +import { MiddlewareTask } from './MiddlewareTask' +import { KoaConn } from './KoaConn' + +export const toKoaRequestHandler = (task: MiddlewareTask): Koa.Middleware => ctx => + task.eval(new KoaConn(ctx)).run() diff --git a/test/index.ts b/test/index.ts index b2e954b..2f174f7 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1,19 +1,5 @@ import * as assert from 'assert' -import { - param, - status, - send, - json, - headers, - contentType, - redirect, - cookie, - clearCookie, - query, - params, - body, - header -} from '../src/MiddlewareTask' +import { middleware, param, json, contentType, redirect, query, params, body, header } from '../src/MiddlewareTask' import { right, left } from 'fp-ts/lib/Either' import { Conn, StatusOpen, HeadersOpen, BodyOpen, MediaType, Status, CookieOptions } from '../src/index' import * as t from 'io-ts' @@ -24,56 +10,46 @@ type MockedHeaders = { [key: string]: string } type MockedCookies = { [key: string]: [string | undefined, CookieOptions] } class MockConn implements Conn { - public readonly '-S': S - + // prettier-ignore + readonly '_S': S constructor(readonly req: MockRequest, readonly res: MockResponse) {} - - public clearCookie(name: string, options: CookieOptions) { + clearCookie(name: string, options: CookieOptions) { return this.res.clearCookie(name, options) } - - public endResponse() { + endResponse() { return this.res.responseEnded } - - public getBody() { + getBody() { return this.req.getBody() } - - public getHeader(name: string) { + getHeader(name: string) { return this.req.getHeader(name) } - - public getParams() { + getParams() { return this.req.getParams() } - - public getQuery() { + getQuery() { return this.req.getQuery() } - - public setBody(body: any) { + setBody(body: any) { this.res.setBody(body) } - - public setCookie(name: string, value: string, options: CookieOptions) { + setCookie(name: string, value: string, options: CookieOptions) { this.res.setCookie(name, value, options) } - - public setHeader(name: string, value: string) { + setHeader(name: string, value: string) { this.res.setHeader(name, value) } - - public setStatus(status: Status) { + setStatus(status: Status) { this.res.setStatus(status) } } class MockRequest { - public body: any - public headers: MockedHeaders - public params: any - public query: any + body: any + headers: MockedHeaders + params: any + query: any constructor(params?: any, query?: string, body?: any, headers?: MockedHeaders) { this.params = params @@ -82,51 +58,51 @@ class MockRequest { this.headers = headers || {} } - public getBody() { + getBody() { return this.body } - public getHeader(name: string) { + getHeader(name: string) { return this.headers[name] } - public getParams() { + getParams() { return this.params } - public getQuery() { + getQuery() { return this.query } } class MockResponse { - public body: any - public cookies: MockedCookies = {} - public headers: MockedHeaders = {} - public responseEnded: boolean = false - public status: Status | undefined + body: any + cookies: MockedCookies = {} + headers: MockedHeaders = {} + responseEnded: boolean = false + status: Status | undefined - public clearCookie(name: string, options: CookieOptions) { + clearCookie(name: string, options: CookieOptions) { delete this.cookies[name] } - public endResponse() { + endResponse() { this.responseEnded = true } - public setBody(body: any) { + setBody(body: any) { this.body = body } - public setCookie(name: string, value: string, options: CookieOptions) { + setCookie(name: string, value: string, options: CookieOptions) { this.cookies[name] = [value, options] } - public setHeader(name: string, value: string) { + setHeader(name: string, value: string) { this.headers[name] = value } - public setStatus(status: Status) { + setStatus(status: Status) { this.status = status } } @@ -147,10 +123,10 @@ function assertResponse( describe('MiddlewareTask', () => { describe('status', () => { it('should write the status code', () => { - const middleware = status(200) + const m = middleware.status(200) const res = new MockResponse() const conn = new MockConn(new MockRequest(), res) - return middleware + return m .eval(conn) .run() .then(() => { @@ -161,10 +137,10 @@ describe('MiddlewareTask', () => { describe('headers', () => { it('should write the headers', () => { - const middleware = headers({ name: 'value' }) + const m = middleware.headers({ name: 'value' }) const res = new MockResponse() const conn = new MockConn(new MockRequest(), res) - return middleware + return m .eval(conn) .run() .then(() => { @@ -175,10 +151,10 @@ describe('MiddlewareTask', () => { describe('send', () => { it('should send the content', () => { - const middleware = send('

Hello world!

') + const m = middleware.send('

Hello world!

') const res = new MockResponse() const conn = new MockConn(new MockRequest(), res) - return middleware + return m .eval(conn) .run() .then(() => { @@ -203,10 +179,10 @@ describe('MiddlewareTask', () => { describe('cookie', () => { it('should add the cookie', () => { - const middleware = cookie('name', 'value', {}) + const m = middleware.cookie('name', 'value', {}) const res = new MockResponse() const conn = new MockConn(new MockRequest(), res) - return middleware + return m .eval(conn) .run() .then(() => { @@ -217,10 +193,10 @@ describe('MiddlewareTask', () => { describe('clearCookie', () => { it('should clear the cookie', () => { - const middleware = cookie('name', 'value', {}).ichain(() => clearCookie('name', {})) + const m = middleware.cookie('name', 'value', {}).ichain(() => middleware.clearCookie('name', {})) const res = new MockResponse() const conn = new MockConn(new MockRequest(), res) - return middleware + return m .eval(conn) .run() .then(() => {