DenisFrezzato / hyper-ts

Type safe middleware architecture for HTTP servers
https://denisfrezzato.github.io/hyper-ts/
MIT License
391 stars 18 forks source link

refactoring (drop tagless final) #15

Closed gcanti closed 5 years ago

gcanti commented 5 years ago

@mlegenhausen here's a POC. Middleware is a wrapper of (c: Conn<I>) => TaskEither<L, [A, Conn<O>]> with a bunch of helpers methods.

Some examples

Hello world (express)

import * as express from 'express'
import { Status, status } from '../src'
import { fromMiddleware } from '../src/toExpressRequestHandler'

const hello = status<never>(Status.OK)
  .closeHeaders()
  .send('Hello hyper-ts on express!')

express()
  .get('/', fromMiddleware(hello))
  .listen(3000, () => console.log('Express listening on port 3000. Use: GET /'))

Parsing url params and error handling

import * as express from 'express'
import { none, Option, some } from 'fp-ts/lib/Option'
import * as t from 'io-ts'
import { Middleware, of, param, Status, status, StatusOpen } from '../src'
import { fromMiddleware } from '../src/toExpressRequestHandler'

interface UserIdBrand {
  readonly UserId: unique symbol
}

const UserId = t.brand(t.string, (s): s is t.Branded<string, UserIdBrand> => s.length > 1, 'UserId')

type UserId = t.TypeOf<typeof UserId>

interface User {
  name: string
}

const badRequest = (message: string) =>
  status<never>(Status.BadRequest)
    .closeHeaders()
    .send(message)

const notFound = (message: string) =>
  status<never>(Status.NotFound)
    .closeHeaders()
    .send(message)

const getUserId = param('user_id', UserId)

// fake database query
const loadUser = <L>(userId: UserId): Middleware<StatusOpen, StatusOpen, L, Option<User>> =>
  userId === 'ab' ? of(some({ name: 'User name...' })) : of(none)

const sendUser = <L>(user: User) =>
  status<L>(Status.OK)
    .closeHeaders()
    .send(JSON.stringify(user))

const user = getUserId
  .ichain(loadUser)
  .ichain(o => o.foldL(() => notFound('user not found'), user => sendUser(user)))
  .orElse(() => badRequest('invalid arguments'))

express()
  .get('/:user_id', fromMiddleware(user))
  .listen(3000, () => console.log('Express listening on port 3000. Use: GET /:user_id'))

/cc @giogonzo

mlegenhausen commented 5 years ago

@gcanti looks great! Especially the "hello world" example look more appealing :wink:

How about adding a ifold function? The ichain fold calls are very common in hyper-ts.

const user = getUserId.ifold(
  () => badRequest('invalid arguments'),
  userId => loadUser(userId).ifold(() => notFound(`user id ${userId} not found`), sendUser)
);
gcanti commented 5 years ago

@mlegenhausen yeah, the list of methods / functions is not complete, I wanted an early feedback before polishing the whole thing

EDIT: btw what's the signature of the ifold you are proposing?

mlegenhausen commented 5 years ago

This would be the equivalent of TaskEither::foldTaskEither

export class Middleware<I, O, L, A> {
  ifold<Z, M, B>(onLeft: (l: L) => Middleware<O, Z, M, B>, onRight: (a: A) => Middleware<O, Z, M, B>): Middleware<I, Z, M, B>
}
mlegenhausen commented 5 years ago

Maybe we should have the full set of TaskEither fold functions? Or do you want to drop MiddlwareTask entirely?

gcanti commented 5 years ago

This would be the equivalent of TaskEither::foldTaskEither

Ok, so that's what I named foldMiddleware. Note that onLeft has a different signature though

foldMiddleware<Z, M, B>(
  onLeft: (l: L) => Middleware<I, Z, M, B>, // <= `I` instead of `O`
  onRight: (a: A) => Middleware<O, Z, M, B>
): Middleware<I, Z, M, B>

Maybe we should have the full set of TaskEither fold functions?

The problem is that we can't transform a Middleware into a Task without a Conn

mlegenhausen commented 5 years ago

The problem is that we can't transform a Middleware into a Task without a Conn

Would be ok for me. I never used Task at all. I dislike the throw nature of exceptions cause it makes evaluating them much harder.

Nice to see a routing example!

mlegenhausen commented 5 years ago

@gcanti are there any future plans to drop the dependency on express and koa entirely? I already implemented my own file upload middleware with hyper-ts but had to work around the fact that the request object is not accessable directly.

gcanti commented 5 years ago

are there any future plans to drop the dependency on express and koa entirely?

@mlegenhausen what do you mean? There's no dependency on express or koa in the core (index.ts), they are abstracted by Conn

mlegenhausen commented 5 years ago

@gcanti you currently can't use hyper-ts as standalone framework. To use it you need a "high level" http server like express or koa. So there is an indirect dependency on these frameworks. It would be nice to use hyper-ts with the http module alone. This would of course result in reimplementing features like a body parser in hyper-ts which is curently handled by express and koa.

Example

import * as http from 'http'
import { Status, status } from '../src'
import { fromMiddleware } from '../src/toHttpRequestHandler'

const hello = status<never>(Status.OK)
  .closeHeaders()
  .send('Hello hyper-ts on express!')

http.createServer(fromMiddleware(hello)).listen(3000);

This would result in a more generell Conn type like defined in the purescript version hyper. Where Conn has only getter for the request and response objects. This access would in turn allow implementations of (in my case) file upload middlewares, that need direct access to the request object.

gcanti commented 5 years ago

It would be nice to use hyper-ts with the http module alone

@mlegenhausen it should be possible to define a specialized Connection<S> for http. Actually both ExpressConnection and KoaConnection could live in their own package, now they are inside this package just for convenience but we could move them outside.

allow implementations of (in my case) file upload middlewares, that need direct access to the request object

We could expose request in Connection