Coder Social home page Coder Social logo

solga's Introduction

Solga: simple typesafe routing Build Status

Haddock | Hackage: solga / solga-swagger | Stackage: solga / solga-swagger

A library for easily specifying web APIs and implementing them in a type-safe way.

Implementing services

At the center of Solga is a typeclass called Router. You can serve any Router as a WAI application:

serve :: Router r => r -> Wai.Application

Routers are generally simple newtypes. For example, to serve a fixed JSON response, just use:

-- From Solga:
newtype JSON a = JSON {jsonResponse :: a}

instance ToJSON a => Router (JSON a)

This router will respond to every request with the given jsonResponse, ie. serve (JSON "It works!") produces an Application that always responds with "It works!".

Routers can also be composed. Let's say you only want to respond to GET requests under /does-it-work. We'll encode the path and the method in the type itself with DataKinds.

type MyAPI = Seg "does-it-work" (Method "GET" (JSON Text))

myAPI :: MyAPI
myAPI = Seg (Method (JSON "It works"))

There's some syntactic sugar we can apply here. First, let's use the :> operator to compose our routers. This is the same as type application.

-- From Solga:
-- type f :> g = f g

type MyAPI = Seg "does-it-work" :> Method "GET" :> JSON Text

Second, we can replace Seg with />:

-- From Solga:
-- type (/>) (seg :: Symbol) g = Seg seg :> g

type MyAPI = "does-it-work" /> Method "GET" :> JSON Text

And third, we can get rid of the constructor boilerplate using brief:

myAPI :: MyAPI
myAPI = brief "It works!"

What if we want to serve multiple different routes? It's easy - any product of Routers is automatically a Router, and Solga will try each field in order:

data MyAPI = MyAPI
  { doesItWork :: "does-it-work" /> Method "GET" :> JSON Text
  , whatAboutThis :: "what-about-this" /> Method "GET" :> JSON Text
  } deriving (Generic)
instance Router MyAPI
instance Abbreviated MyAPI

myAPI :: MyAPI
myAPI = MyAPI
  { doesItWork = brief "It works!"
  , whatAboutThis = brief "It also works!"
  }

We can nest these record routers as expected:

data UserAPI = UserAPI {..}
data WidgetAPI = WidgetAPI {..}

data MyAPI = MyAPI
  { userAPI :: "user" /> UserAPI 
  , widgetAPI :: "widget" /> WidgetAPI
  } deriving (Generic)

What if we want to capture a path segment? Let's see:

-- newtype Capture a next = Capture {captureNext :: a -> next}

data MyAPI = MyAPI
  { echo :: "echo" /> Method "GET" :> Capture Text :> JSON Text
  } deriving (Generic)
instance Router MyAPI
instance Abbreviated MyAPI

myAPI :: MyAPI
myAPI = MyAPI
  { echo = brief id -- short for: Seg $ Method $ Capture $ \captured -> JSON captured
  }

How about doing IO?

data MyAPI = MyAPI
  { rng :: "rng" /> Method "GET" :> WithIO :> JSON Int
  } deriving (Generic)
instance Router MyAPI
instance Abbreviated MyAPI

myAPI :: MyAPI
myAPI = MyAPI
  { rng = brief (getStdRandom random)
  }

Solga comes with a large set of useful Routers for parsing request bodies and producing responses. See the documentation for more details.

Creating Routers

To create a router yourself, just implement the Router typeclass:

-- | The right hand side of `Application`. `Request` is already known.
type Responder = (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived

class Router r where
  -- | Given a request, if the router supports the given request
  -- return a function that constructs a response with a concrete router.
  tryRoute :: Wai.Request -> Maybe (r -> Responder)

In Solga, all routing decisions are performed purely on the type of the Router - it's not possible to use its value to decide whether to accept a request or not. This is because this way an outer router can predict whether an inner router will match, even if the value of its implementation is non-deterministic.

For example, let's consider the router CustomAuthRouter :> "foo" /> WithIO :> JSON Text. We don't know exactly how "foo" /> WithIO :> JSON Text will be executed, as it contains WithIO. However, because of the restriction above, we can predict that it will only work for a path /foo. and so if we get a request with /bar, there's no need to do any authentication.

This is why the type of tryRoute is Wai.Request -> Maybe (r -> Responder). The router instance essentially says "I can't handle this request, try something else" or "I can handle this, please give me the implementation".

For example, here is the implementation of the JSON router:

instance Aeson.ToJSON a => Router (JSON a) where
  tryRoute _ = Just $ \json cont ->
    cont $ Wai.responseBuilder HTTP.status200 headers $ Aeson.encodeToBuilder $ Aeson.toJSON $ jsonResponse json
      where headers = [ ( HTTP.hContentType, "application/json" ) ]

tryRouteNext is a very useful function for implementing routers:

tryRouteNext :: Router r' => (r -> r') -> Wai.Request -> Maybe (r -> Responder)
tryRouteNextIO :: Router r' => (r -> IO r') -> Wai.Request -> Maybe (r -> Responder)

Essentially, if you can convert from your type r to another Router type r', you get the implementation for tryRoute for free. With this, it's easy to implement the Servant "fish operator":

data left :<|> right = (:<|>) { altLeft :: left, altRight :: right }
  deriving (Eq, Ord, Show)

infixr 1 :<|>

instance (Router left, Router right) => Router (left :<|> right) where
  tryRoute req = tryRouteNext altLeft req <|> tryRouteNext altRight req

Or Seg:

newtype Seg (seg :: Symbol) next = Seg { segNext :: next }
  deriving (Eq, Ord, Show)

instance (KnownSymbol seg, Router next) => Router (Seg seg next) where
  tryRoute req = case Wai.pathInfo req of
    s : segs | Text.unpack s == symbolVal (Proxy :: Proxy seg) ->
      tryRouteNext segNext req { Wai.pathInfo = segs }
    _ -> Nothing

Swagger and client generation

You can also generate Swagger specifications for your API for free. Just use the solga-swagger package, derive RouterSwagger the same way as Router, and use the genSwagger function to get your specification.

genSwagger :: RouterSwagger r => Proxy r -> Either (Text, Context) Swagger

It's possible for an API type to not be specific enough (not matching a Method), or to be inconsistent (matching two different request bodies). In this case, an error will be returned.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.