servant-purescript-0.1.0.0: src/Servant/PureScript/CodeGen.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
module Servant.PureScript.CodeGen where
import Servant.PureScript.Internal
import Text.PrettyPrint.Mainland
import Control.Lens hiding (List)
import Data.Aeson
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe (mapMaybe, maybeToList)
import Data.Monoid
import Data.Proxy
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Typeable
import GHC.Generics hiding (to)
import Language.PureScript.Bridge
import Language.PureScript.Bridge.Printer
import Language.PureScript.Bridge.PSTypes (psString)
import Language.PureScript.Bridge.TypeInfo
import Network.HTTP.Types.URI (urlEncode)
import Servant.API
import Servant.Foreign
genModule :: Settings -> [Req PSType] -> Doc
genModule opts reqs = let
allParams = concatMap reqToParams reqs
rParams = getReaderParams opts allParams
apiImports = reqsToImportLines reqs
imports = mergeImportLines (_standardImports opts) apiImports
importLines = map (strictText . importLineToText) . Map.elems $ imports
in
"-- File auto generated by servant-purescript! --"
</> "module" <+> strictText (_apiModuleName opts) <+> "where" <> line
</> "import Prelude" <> line
</> docIntercalate line importLines <> line
</> genParamSettings rParams <> line
</> (docIntercalate line . map (genFunction rParams)) reqs
getReaderParams :: Settings -> [PSParam] -> [PSParam]
getReaderParams opts allParams = let
isReaderParam = (`Set.member` _readerParams opts) . _pName
rParamsDirty = filter isReaderParam allParams
rParamsMap = Map.fromListWith useOld . map toPair $ rParamsDirty
rParams = map fromPair . Map.toList $ rParamsMap
-- Helpers
toPair (Param n t) = (n, t)
fromPair (n, t) = Param n t
useOld = flip const
in
rParams
genParamSettings :: [PSParam]-> Doc
genParamSettings rParams = let
genEntry arg = arg ^. pName ^. to psVar <+> "::" <+> arg ^. pType ^. typeName ^. to strictText
genEntries = docIntercalate (line <> ", ") . map genEntry
in
"newtype SPParams_ = SPParams_" <+/> align (
lbrace
<+> genEntries rParams
</> rbrace
)
genFunction :: [PSParam] -> Req PSType -> Doc
genFunction allRParams req = let
rParamsSet = Set.fromList allRParams
fnName = req ^. reqFuncName ^. camelCaseL
allParamsList = baseURLParam : reqToParams req
allParams = Set.fromList allParamsList
fnParams = filter (not . flip Set.member rParamsSet) allParamsList -- Use list not set, as we don't want to change order of parameters
rParams = Set.toList $ rParamsSet `Set.intersection` allParams
pTypes = map _pType fnParams
pNames = map _pName fnParams
signature = genSignature fnName pTypes (req ^. reqReturnType)
body = genFnHead fnName pNames <+> genFnBody rParams req
in signature </> body
genGetReaderParams :: [PSParam] -> Doc
genGetReaderParams = stack . map (genGetReaderParam . psVar . _pName)
where
genGetReaderParam pName = "let" <+> pName <+> "= spParams_." <> pName
genSignature :: Text -> [PSType] -> Maybe PSType -> Doc
genSignature fnName params mRet = fName <+> align (constraint <+/> parameterString)
where
fName = strictText fnName
constraint = ":: forall eff m." <+/> "(MonadReader (SPSettings_ SPParams_) m, MonadError AjaxError m, MonadAff ( ajax :: AJAX | eff) m)" <+/> "=>"
retName = maybe "Unit" (strictText . typeInfoToText True) mRet
retString = "m" <+> retName
typeNames = map (strictText . typeInfoToText True) params
parameterString = docIntercalate (softline <> "-> ") (typeNames <> [retString])
genFnHead :: Text -> [Text] -> Doc
genFnHead fnName params = fName <+> align (docIntercalate softline docParams <+> "=")
where
docParams = map psVar params
fName = strictText fnName
genFnBody :: [PSParam] -> Req PSType -> Doc
genFnBody rParams req = "do"
</> indent 2 (
"SPSettings_ spOpts_ <- ask"
</> "let spParams_ = case spOpts_.params of SPParams_ ps_ -> ps_"
</> genGetReaderParams rParams
</> hang 6 ("let httpMethod =" <+> dquotes (req ^. reqMethod ^. to T.decodeUtf8 ^. to strictText))
</> hang 6 ("let reqUrl =" <+> genBuildURL (req ^. reqUrl))
</> "let reqHeaders =" </> indent 6 (req ^. reqHeaders ^. to genBuildHeaders)
</> "affResp <- liftAff $ affjax " <> hang 2 ( "defaultRequest" </>
"{ method =" <+> "httpMethod"
</> ", url =" <+> "reqUrl"
</> ", headers =" <+> "defaultRequest.headers <> reqHeaders"
</> case req ^. reqBody of
Nothing -> "}"
Just _ -> ", content =" <+> "toNullable <<< Just <<< printJson <<< gAesonEncodeJson $ reqBody" </> "}"
)
</> "getResult gAesonDecodeJson affResp" <> line
)
genBuildURL :: Url PSType -> Doc
genBuildURL url = psVar baseURLId <+> "<>"
<+> genBuildPath (url ^. path ) <+> genBuildQuery (url ^. queryStr)
----------
genBuildPath :: Path PSType -> Doc
genBuildPath = docIntercalate (softline <> "<> \"/\" <> ") . map (genBuildSegment . unSegment)
genBuildSegment :: SegmentType PSType -> Doc
genBuildSegment (Static (PathSegment seg)) = dquotes $ strictText (textURLEncode False seg)
genBuildSegment (Cap arg) = parens $ "encodeURIComponent" <+> arg ^. argName ^. to unPathSegment ^. to psVar
----------
genBuildQuery :: [QueryArg PSType] -> Doc
genBuildQuery [] = ""
genBuildQuery args = softline <> "<> \"?\" <> " <> (docIntercalate (softline <> "<> \"&\" <> ") . map genBuildQueryArg $ args)
genBuildQueryArg :: QueryArg PSType -> Doc
genBuildQueryArg arg = case arg ^. queryArgType of
Normal -> genQueryEncoding "encodeQuery"
Flag -> genQueryEncoding "encodeQuery"
List -> genQueryEncoding "encodeListQuery"
where
argText = arg ^. queryArgName ^. argName ^. to unPathSegment
encodedArgName = strictText . textURLEncode True $ argText
genQueryEncoding fn = fn <+> dquotes encodedArgName <+> psVar argText
-----------
genBuildHeaders :: [HeaderArg PSType] -> Doc
genBuildHeaders = list . map genBuildHeader
genBuildHeader :: HeaderArg PSType -> Doc
genBuildHeader (HeaderArg arg) = let
argText = arg ^. argName ^. to unPathSegment
encodedArgName = strictText . textURLEncode True $ argText
in
align $ "{ field : " <> dquotes encodedArgName
<+/> comma <+> "value :"
<+> "(encodeURIComponent <<< gDefaultToURLPiece)" <+> psVar argText
</> "}"
genBuildHeader (ReplaceHeaderArg _ _) = error "ReplaceHeaderArg - not yet implemented!"
reqsToImportLines :: [Req PSType] -> ImportLines
reqsToImportLines = typesToImportLines Map.empty . concatMap reqToPSTypes
reqToPSTypes :: Req PSType -> [PSType]
reqToPSTypes req = map _pType (reqToParams req) ++ maybeToList (req ^. reqReturnType)
-- | Extract all function parameters from a given Req.
reqToParams :: Req PSType -> [Param PSType]
reqToParams req = Param baseURLId psString
: fmap headerArgToParam (req ^. reqHeaders)
++ maybeToList (reqBodyToParam (req ^. reqBody))
++ urlToParams (req ^. reqUrl)
urlToParams :: Url f -> [Param f]
urlToParams url = mapMaybe (segmentToParam . unSegment) (url ^. path) ++ map queryArgToParam (url ^. queryStr)
segmentToParam :: SegmentType f -> Maybe (Param f)
segmentToParam (Static _) = Nothing
segmentToParam (Cap arg) = Just Param {
_pType = arg ^. argType
, _pName = arg ^. argName ^. to unPathSegment
}
queryArgToParam :: QueryArg f -> Param f
queryArgToParam arg = Param {
_pType = arg ^. queryArgName ^. argType
, _pName = arg ^. queryArgName ^. argName ^. to unPathSegment
}
headerArgToParam :: HeaderArg f -> Param f
headerArgToParam (HeaderArg arg) = Param {
_pName = arg ^. argName ^. to unPathSegment
, _pType = arg ^. argType
}
headerArgToParam _ = error "We do not support ReplaceHeaderArg - as I have no idea what this is all about."
reqBodyToParam :: Maybe f -> Maybe (Param f)
reqBodyToParam = fmap (Param "reqBody")
docIntercalate :: Doc -> [Doc] -> Doc
docIntercalate i = mconcat . punctuate i
textURLEncode :: Bool -> Text -> Text
textURLEncode spaceIsPlus = T.decodeUtf8 . urlEncode spaceIsPlus . T.encodeUtf8
-- | Little helper for generating valid variable names
psVar :: Text -> Doc
psVar = strictText . toPSVarName