DenisFrezzato / hyper-ts

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

Middleware should have a fold operation #33

Open kylegoetz opened 3 years ago

kylegoetz commented 3 years ago

I think the Middleware monad, like Option/Either/etc. should have a

fold: <E,A,B>(left:(e:E)=>B, right:(a:A)=>B) => (a:Middleware<I,O,E,A>) => Middleware<I,O,never,B>.

A motivating use case is when you might or might not have a header defined (say, x-forwarded-for in prod vs dev). If you make an API call with some data in a body and this header set/not set, ideally you'd want to do something like

sequenceT(H.middleware)(
  H.decodeBody(BodyDecoder.decode), // fails if body is malformed
  pipe(H.decodeHeader('x-forwarded-for', NonEmptyString.decode), H.ichain(H.fold(O.none, O.some)))
) // H.Middleware<StatusOpen,StatusOpen,t.Errors,[MyBodyType, Option<string>]>

Basically this use case needs decoding to be an successful None rather than a failing Left

kylegoetz commented 3 years ago

As a note, right now in lieu of this I'm doing

H.decodeHeader('my-header-name', flow(O.fromNullable, E.right)) // Middleware<StatusOpen,StatusOpen,never,Option<unknown>>

but figured since we have map, mapLeft, bimap, ichain, etc. that fold might also be desirable.

nibtime commented 3 years ago

I just started to play around with hyper-ts as an implementation mechanism for declarative API specs (inspired by https://github.com/rawrmaan/restyped) and created a router that consumes hyper-ts middleware.

At some point, I was puzzled, because I wanted to resolve both success and error (represented as Either in protocol spec) and serialize the response to meet the API spec. I wanted to communicate that it has been handled and dealt with further down the chain and I didn't know how to get them both and remove the error. I wanted to get Middleware<I, O, never, Either<Error, Success> and then I found out through this issue, that what I need is a fold operation for middleware, so I tried to write one:

export function fold<I, O, Z, E, A, R>(
  onLeft: (ci: H.Connection<I>) => (e: E) => [R, H.Connection<Z>],
  onRight: (ci: H.Connection<I>) =>(ao: [A, H.Connection<O>]) => [R, H.Connection<Z>],
): (ma: Middleware<I, O, E, A>) => Middleware<I, Z, never, R> {
  return ma => ci => {
    const folder = TE.fold(flow(onLeft(ci), T.of), flow(onRight(ci), T.of))
    return TE.fromTask(folder(ma(ci)))
  }
}

I hope this helps you. I am just a beginner with fp-ts though, so I am not quite sure if this is the best way to implement it, I just followed the type signatures.

kylegoetz commented 3 years ago

Time permitting I'll probably do a PR of this code. My MO lately has been to propose things as an issue/feature request and if people like the idea, implement and do a PR. I just don't want to do the work and people be like "nah this is dumb"

If one other person needs it, then I think a PR is a good idea. If you don't want to do it, @nibtime, then I'll do it fairly soon.