Skip to content

Build fully isomorphic apps and APIs in PureScript by providing value level spec and renderers.

License

Notifications You must be signed in to change notification settings

purescript-isomers/purescript-isomers

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

79 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

purescript-isomers

WIP!

Use the same building blocks to create fully isomorphic apps (SPA with SSR or traditional server side apps) and APIs in PureScript. Everything is derived from value level spec and renderers.

Objectives

Once again - the whole spec is just a value. Values are much easier to transform and compose.

Design

The main idea behind this framework is quite simple. Let's prototype it quickly here.

As a base layer this lib provides a pair of composable codec types. For now we can think about them as encoding and decoding functions. Let say that we use just Tuple to keep them together:

type RequestCodec a =  (a -> HTTPRequest) /\ (HTTPRequest -> Maybe a)

type ResponseCodec a = (a -> HTTPResponse) /\ (HTTPResponse -> Maybe a)

These codecs (I'm using Duplex term in the codebase following Nate's convention) allow us to send and receive data through HTTP channel.

In the lib the representation and parsing/printing is a bit more complicated than that because we have different encodings on the client than on the server of HTTP messages (they are even parametrized) and we work in an effectful monad. Let say that this is not really important now.

Single endpoint

Given the above types we can define a simple, single endpoint API just by providing a pair:

type Api i o = RequestCodec i /\ ResponseCodec o

So client would be just:

client :: forall i o m. Monad m => Api i o -> i -> m (Maybe o)
client ((reqEnc /\ _) /\ (_ /\ resDec)) i = do
  httpRes <- httpFetch (reqEnc i)
  pure $ resDec httpRes

To build a server we need a function which actually computes the o given the i:

server :: forall i o n. Monad n => Api i o -> (i -> n o) -> HTTPRequest -> n HTTPResponse
server ((_ /\ reqDec) /\ (resEnc /\ _)) handler httpReq = do
  reqDec httpReq >>= case _ of
    Just i -> resEnc <$> handler i
    Nothing -> -- handle bad request

Please ignore the details like moands which we work in or error handling because they are not important now.

Multiple endpoints

A single endpoint APIs are a really rare thing. Usually we want to be able to provide multiple functions which serve different types of values for different types of inputs carried by requests. Server should be able to pick a request, decode it and pass the data to the appropriate handler and encode the result using appropriate encoding function. Similar thinking should be applied to the client function which should accept a request in the form of an application level value turn it into HTTPRequest and wait for a response which should be decoded by appropriate codec which is dedicated for this particular response type. On both ends of the wire we want to use aligning functions in this case of course and our codecs keep them together for us.

To fulfill this "dispatch" requirement we use "compatible" Variants and Records types. So let me introduce RealWorldApi type :-P which can be used by both the client and the server to describe multiple endpoints safely:

type RealWorldApi req res = RequestCodec (Variant req) /\ { | res }

What we have above is a codec which encodes / decodes requests into a Variant on the first position of our tuple. The { | res } type represents a record of response codecs which we use to turn results into a HTTPResponses. In this record we have codecs which should be used to encode / decode responses for particular requests. We are able to pick appropriate response codec or handler on the server using the label included in the Variant from the request. These labels don't carry any HTTP semantic meaning by themselves - they are only a dispatch layer. On the application layer you work with data directly and use these labels / paths only to pick endpoints which you want to use or to create URLs.

So the simple client can be sketched as:

client ::
  forall i m o req req_ res res_.
  Monad m =>
  Row.Cons endpoint i req_ req =>
  Row.Cons endpoint (ResponseCodec o) res_ res =>
  RealWorldApi req res ->
  SProxy endpoint ->
  i ->
  m (Maybe o)
client ((reqEnc /\ _) /\ resCodecs) endpoint i = do
  let
    httpReq = reqEnc (Variant.inj endpoint i)
    resDec = snd (Record.get endpoint resCodecs)
  httpRes <- httpFetch httpReq
  pure $ resDec httpRes

On the server we additionally have to pick the hander from provided handlers record which resides under the appropriate label (simple hmap is enough there).

Stay tuned

So that was the general idea. Based on it I can also define and combine rendering functions with particular data layer endpoints and extend an API and build a fully isomorphic SSR solution and SPA web routing. Of course SSR / SPA "routes" are addition to the API which can be still seen as just the above pair when we ignore rendering record.

I'm trying to cover this and many more things in this attempt: "heterogeneous DSL" for defining your specs which is easy to compose, nested routes handilng but also flattening, easy to use combinators for codeces, HTTP semantics preserving response and request wrappers... please stay tuned!

Credits

The core pieces of Isomers.Request.Duplex were copied from routing-duplex library by @natefaubion. I wasn't able to extend its recursive AST and this change was too invasive to be included in the original lib.

About

Build fully isomorphic apps and APIs in PureScript by providing value level spec and renderers.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published