A web microframework for Haskell based on monadic parsing
Go to file
2022-07-05 09:05:00 +00:00
examples Update chess example 2022-05-03 17:20:08 +00:00
src Make huge changes to interface 2022-07-05 09:05:00 +00:00
test Make huge changes to interface 2022-07-05 09:05:00 +00:00
.gitignore Make first Hackage release 2022-05-30 08:51:05 +00:00
ChangeLog.md first commit 2022-03-30 00:07:37 +00:00
Conduit.postman_collection.json Transfer all example from old repo 2022-03-30 01:36:29 +00:00
how-to-run-realworld-test.txt Add servant + okapi calculator example 2022-03-30 10:45:29 +00:00
LICENSE first commit 2022-03-30 00:07:37 +00:00
okapi_1_50.png Add files via upload 2022-03-30 21:44:17 -05:00
okapi.cabal Add tests 2022-07-03 06:46:12 +00:00
okapi.png Add files via upload 2022-03-30 21:40:18 -05:00
openapi.yml Transfer all example from old repo 2022-03-30 01:36:29 +00:00
package.yaml Add tests 2022-07-03 06:46:12 +00:00
README.md Replace abort with error; remove type applications 2022-04-17 09:51:05 +00:00
run-api-tests.sh Transfer all example from old repo 2022-03-30 01:36:29 +00:00
Setup.hs first commit 2022-03-30 00:07:37 +00:00
stack.yaml Make first Hackage release 2022-05-30 08:51:05 +00:00
stack.yaml.lock Working on sse implementation 2022-04-19 03:02:47 +00:00

Okapi

A microframework based on monadic parsing. Official documentation here.

Okapi mascot

Introduction

Okapi is a microframework for building web servers in Haskell based on monadic parsing. In contrast to other web frameworks in the Haskell ecosystem, Okapi is primarily concerned with being easy to understand and use, instead of extreme type safety. Here's an example of a simple web server:

{-# LANGUAGE OverloadedStrings #-}

import Data.Text
import Okapi

main :: IO ()
main = runOkapi id 3000 greet

greet = do
  seg "greet"
  name <- segParam
  respondPlainText [] $ "Hello " <> name <> "! I'm Okapi."

Running this code will start a server on localhost:3000. If you go to http://localhost:3000/greeting/Bob the server will respond with Hello Bob! I'm Okapi. in plain text format.

Okapi is a monadic parser for HTTP requests. This means it can be used with all Applicative, Alternative, and Monad typeclass methods, plus other Haskell idioms like parser combinators.

Here's a more complicated example that implements a calculator API. Type annotations are added to make the code easier to follow:

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}

module Main where

import Control.Applicative ((<|>))
import Data.Aeson (ToJSON)
import Data.Text
import GHC.Generics (Generic)
import Okapi


main :: IO ()
main = runOkapi id 3000 calc

type Okapi a = OkapiT IO a

calc :: Okapi Response
calc = do
  get
  seg "calc"
  addOp <|> subOp <|> mulOp <|> divOp

addOp :: Okapi Response
addOp = do
  seg "add"
  (x, y) <- getArgs
  respondJSON [] $ x + y

subOp :: Okapi Response
subOp = do
  seg "sub" <|> seg "minus"
  (x, y) <- getArgs
  respondJSON [] $ x - y

mulOp :: Okapi Response
mulOp = do
  seg "mul"
  (x, y) <- getArgs
  respondJSON [] $ x * y

data DivResult = DivResult
  { answer :: Int,
    remainder :: Int
  }
  deriving (Eq, Show, Generic, ToJSON)

divOp :: Okapi Response
divOp = do
  seg "div"
  (x, y) <- getArgs
  if y == 0
    then error403 [] "Forbidden"
    else respondJSON [] $ DivResult {answer = x `div` y, remainder = x `mod` y}

getArgs :: Okapi (Int, Int)
getArgs = getArgsFromPath <|> getArgsFromQueryParams
  where
    getArgsFromPath :: Okapi (Int, Int)
    getArgsFromPath = do
      x <- segParamAs @Int
      y <- segParamAs @Int
      pure (x, y)

    getArgsFromQueryParams :: Okapi (Int, Int)
    getArgsFromQueryParams = do
      x <- queryParamAs @Int "x"
      y <- queryParamAs @Int "y"
      pure (x, y)

Okapi is very flexible. You could also define the above without do notation or type annotations:

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}

module Main where

import Control.Applicative ((<|>))
import Control.Monad.Combinators (choice)
import Data.Aeson (ToJSON)
import Data.Text
import GHC.Generics (Generic)
import Okapi

main :: IO ()
main = runOkapi id 3000 calcNoDo

calcNoDo = get >> seg "calc" >> choice [addOp, subOp, mulOp, divOp]

addOp = seg "add" >> (getArgs >>= (\(x, y) -> respondJSON [] $ x + y))

subOp = (seg "sub" <|> seg "minus") >> (getArgs >>= (\(x, y) -> respondJSON [] $ x - y))

mulOp = seg "mul" >> (getArgs >>= (\(x, y) -> respondJSON [] $ x * y))

data DivResult = DivResult
  { answer :: Int,
    remainder :: Int
  }
  deriving (Eq, Show, Generic, ToJSON)

divOp = seg "div" >> (getArgs >>= (\(x, y) -> if y == 0 then error403 [] "Forbidden" else respondJSON [] $ DivResult (x `div` y) (x `mod` y)))

getArgs = getArgsFromPath <|> getArgsFromQueryParams
  where
    getArgsFromPath = segParamAs @Int >>= (\x -> segParamAs @Int >>= (\y -> pure (x, y)))
    getArgsFromQueryParams = queryParamAs @Int "x" >>= (\x -> queryParamAs @Int "y" >>= (\y -> pure (x, y)))

Don't like monads at all? You can use Okapi as an applicative parser too:

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}

module Main where

import Control.Applicative (liftA2, (<|>))
import Control.Applicative.Combinators (choice)
import Data.Aeson (ToJSON)
import Data.Text
import GHC.Generics (Generic)
import Okapi

main :: IO ()
main = runOkapi id 3000 calcAp

calcAp = get *> seg "calc" *> choice [addOp, subOp, mulOp, divOp]

addOp = seg "add" *> respondJSONAp [] (uncurry (+) <$> getArgs)

subOp = (seg "sub" <|> seg "minus") *> respondJSONAp [] (uncurry (-) <$> getArgs)

mulOp = seg "mul" *> respondJSONAp [] (uncurry (*) <$> getArgs)

data DivResult = DivResult
  { answer :: Int,
    remainder :: Int
  }
  deriving (Eq, Show, Generic, ToJSON)

divOp = seg "div" *> (getArgs >>= (\(x, y) -> if y == 0 then error403 [] "Forbidden" else respondJSON [] $ DivResult (x `div` y) (x `mod` y)))

getArgs = getArgsFromPath <|> getArgsFromQueryParams
  where
    getArgsFromPath = liftA2 (,) (segParamAs @Int) (segParamAs @Int)
    getArgsFromQueryParams = liftA2 (,) (queryParamAs @Int "x") (queryParamAs @Int "y")

As you can see, Okapi's parsing functions are very modular and can be easily composed with one another to create parsing primitives tailored specifically to your needs. With Okapi, and the rest of the amazing Haskell ecosystem, you can create anything from simple website servers to complex APIs for web apps. All you need to get started is basic knowledge about the structure of HTTP requests and an idea of how monadic parsing works.

Contributing

Help is needed! There are issues and project board. Look there or open an issue or PR. There are no barriers. Any kind of help is welcome.