servant/src/Servant/Utils/ApiQuasiQuoting.hs

203 lines
5.9 KiB
Haskell
Raw Normal View History

2014-10-28 13:22:11 +01:00
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
2014-11-12 11:34:33 +01:00
{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
2014-11-25 16:10:59 +01:00
-- | QuasiQuoting utilities for API types.
--
-- 'sitemap' allows you to write your type in a very natural way:
--
-- @
-- [sitemap|
-- PUT hello String -> ()
-- POST hello/p:Int String -> ()
-- GET hello/?name:String Int
-- |]
-- @
--
-- Will generate:
--
-- @
-- "hello" :> ReqBody String :> Put ()
-- :<|> "hello" :> Capture "p" Int :> ReqBody String :> Post ()
-- :<|> "hello" :> QueryParam "name" String :> Get Int
-- @
--
-- Note the '/' before a 'QueryParam'!
module Servant.Utils.ApiQuasiQuoting where
2014-10-28 13:22:11 +01:00
2014-11-12 11:34:33 +01:00
import Control.Monad (void)
import Control.Applicative hiding (many, (<|>), optional)
import Language.Haskell.TH.Quote
2014-10-28 13:22:11 +01:00
import Language.Haskell.TH
2014-11-12 11:34:33 +01:00
import Text.ParserCombinators.Parsec
2014-10-28 13:22:11 +01:00
import Servant.API.Capture
import Servant.API.Get
import Servant.API.Post
import Servant.API.Put
import Servant.API.Delete
2014-10-29 13:10:04 +01:00
import Servant.API.QueryParam
import Servant.API.ReqBody
2014-10-28 13:22:11 +01:00
import Servant.API.Sub
2014-10-30 11:37:58 +01:00
import Servant.API.Alternative
2014-10-28 13:22:11 +01:00
2014-11-25 16:10:59 +01:00
-- | Finally-tagless encoding for our DSL.
-- Keeping 'repr'' and 'repr' distinct when writing functions with an
-- @ExpSYM@ context ensures certain invariants (for instance, that there is
-- only one of 'get', 'post', 'put', and 'delete' in a value), but
-- sometimes requires a little more work.
2014-10-28 13:22:11 +01:00
class ExpSYM repr' repr | repr -> repr', repr' -> repr where
lit :: String -> repr' -> repr
capture :: String -> String -> repr -> repr
2014-10-29 13:10:04 +01:00
reqBody :: String -> repr -> repr
queryParam :: String -> String -> repr -> repr
conj :: repr' -> repr -> repr
get :: String -> repr
post :: String -> repr
put :: String -> repr
delete :: String -> repr
2014-11-12 11:34:33 +01:00
infixr 6 >:
(>:) :: Type -> Type -> Type
(>:) = conj
2014-10-28 13:22:11 +01:00
2014-10-28 17:32:22 +01:00
2014-10-28 13:22:11 +01:00
instance ExpSYM Type Type where
2014-10-29 14:54:13 +01:00
lit name r = LitT (StrTyLit name) >: r
capture name typ r = AppT (AppT (ConT ''Capture) (LitT (StrTyLit name)))
(ConT $ mkName typ) >: r
reqBody typ r = AppT (ConT ''ReqBody) (ConT $ mkName typ) >: r
queryParam name typ r = AppT (AppT (ConT ''QueryParam) (LitT (StrTyLit name)))
(ConT $ mkName typ) >: r
conj x = AppT (AppT (ConT ''(:>)) x)
2014-10-29 13:10:04 +01:00
get typ = AppT (ConT ''Get) (ConT $ mkName typ)
post typ = AppT (ConT ''Post) (ConT $ mkName typ)
put typ = AppT (ConT ''Put) (ConT $ mkName typ)
delete "()" = ConT ''Delete
delete _ = error "Delete does not return a request body"
2014-11-12 11:34:33 +01:00
parseMethod :: ExpSYM repr' repr => Parser (String -> repr)
parseMethod = try (string "GET" >> return get)
<|> try (string "POST" >> return post)
<|> try (string "PUT" >> return put)
<|> try (string "DELETE" >> return delete)
parseUrlSegment :: ExpSYM repr repr => Parser (repr -> repr)
parseUrlSegment = try parseCapture
<|> try parseQueryParam
<|> try parseLit
where
parseCapture = do
cname <- many (noneOf " ?/:")
char ':'
ctyp <- many (noneOf " ?/:")
return $ capture cname ctyp
parseQueryParam = do
char '?'
cname <- many (noneOf " ?/:")
char ':'
ctyp <- many (noneOf " ?/:")
return $ queryParam cname ctyp
parseLit = lit <$> many (noneOf " ?/:")
parseUrl :: ExpSYM repr repr => Parser (repr -> repr)
parseUrl = do
optional $ char '/'
url <- parseUrlSegment `sepBy1` char '/'
return $ foldr1 (.) url
data Typ = Val String
| ReqArgVal String String
parseTyp :: Parser Typ
parseTyp = do
2014-11-13 18:22:26 +01:00
f <- many (noneOf "-{\n\r")
2014-11-12 11:34:33 +01:00
spaces
2014-11-13 18:22:26 +01:00
s <- optionMaybe (try parseRet)
try $ optional inlineComment
try $ optional blockComment
2014-11-12 11:34:33 +01:00
case s of
Nothing -> return $ Val (stripTr f)
Just s' -> return $ ReqArgVal (stripTr f) (stripTr s')
where
parseRet :: Parser String
parseRet = do
string "->"
spaces
2014-11-13 18:22:26 +01:00
many (noneOf "-{\n\r")
2014-11-12 11:34:33 +01:00
stripTr = reverse . dropWhile (== ' ') . reverse
parseEntry :: ExpSYM repr repr => Parser repr
parseEntry = do
met <- parseMethod
spaces
url <- parseUrl
spaces
typ <- parseTyp
case typ of
Val s -> return $ url (met s)
ReqArgVal i o -> return $ url $ reqBody i (met o)
2014-11-13 18:22:26 +01:00
blockComment :: Parser ()
blockComment = do
string "{-"
manyTill anyChar (try $ string "-}")
return ()
inlineComment :: Parser ()
inlineComment = do
string "--"
manyTill anyChar (try $ lookAhead eol)
return ()
2014-11-12 11:34:33 +01:00
eol :: Parser String
eol = try (string "\n\r")
<|> try (string "\r\n")
<|> string "\n"
<|> string "\r"
<?> "end of line"
eols :: Parser ()
2014-11-13 18:22:26 +01:00
eols = skipMany $ void eol <|> blockComment <|> inlineComment
2014-11-12 11:34:33 +01:00
parseAll :: Parser Type
parseAll = do
eols
entries <- parseEntry `endBy` eols
return $ foldr1 union entries
where union :: Type -> Type -> Type
union a = AppT (AppT (ConT ''(:<|>)) a)
2014-11-25 16:10:59 +01:00
-- | The sitemap QuasiQuoter.
--
-- * @.../<var>:<type>/...@ becomes a capture
--
-- * @.../?<var>:<type>@ becomes a query parameter
--
-- * @<method> ... <typ>@ becomes a method returning @<typ>@
--
-- * @<method> ... <typ1> -> <typ2>@ becomes a method with request
-- body of @<typ1>@ and returning @<typ2>@
--
-- Comments are allowed, and have the standard Haskell format
--
-- * @--@ for inline
--
-- * @{- ... -}@ for block
--
sitemap :: QuasiQuoter
sitemap = QuasiQuoter { quoteExp = undefined
, quotePat = undefined
2014-11-12 11:34:33 +01:00
, quoteType = \x -> case parse parseAll "" x of
Left err -> error $ show err
Right st -> return st
, quoteDec = undefined
}
2014-10-28 13:22:11 +01:00