packages feed

servant-purescript-0.3.0.0: src/Servant/PureScript/Subscriber.hs

{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeOperators         #-}

-- TODO: This module duplicates quite a lot of code from CodeGen.hs.
module Servant.PureScript.Subscriber where

import           Control.Lens                       hiding (List)
import           Data.Map                           (Map)
import           Data.Maybe                         (mapMaybe, maybeToList)
import qualified Data.Set                           as Set
import           Data.Text                          (Text)
import qualified Data.Text                          as T
import qualified Data.Text.Encoding                 as T
import           Language.PureScript.Bridge
import           Language.PureScript.Bridge.PSTypes (psString, psUnit)
import           Network.HTTP.Types.URI             (urlEncode)
import           Servant.Foreign
import           Servant.PureScript.CodeGen         hiding (genBuildHeader,
                                                     genBuildHeaders,
                                                     genBuildPath,
                                                     genBuildQuery,
                                                     genBuildQueryArg,
                                                     genBuildSegment, genFnBody,
                                                     genFunction, genModule,
                                                     genSignature)
import           Servant.PureScript.Internal
import           Text.PrettyPrint.Mainland

subscriberImportLines :: Map Text ImportLine
subscriberImportLines = importsFromList
  [
    ImportLine "Servant.Subscriber.Subscriptions" (Set.fromList [ "Subscriptions"
                                                                , "makeSubscriptions"
                                                                ])
  , ImportLine "Servant.Subscriber.Util" (Set.fromList [ "toUserType"
                                                       , "subGenNormalQuery"
                                                       , "subGenListQuery"
                                                       , "subGenFlagQuery"
                                                       , "TypedToUser"
                                                       ])
  , ImportLine "Servant.Subscriber" (Set.fromList ["ToUserType"])
  , ImportLine "Servant.Subscriber.Request" (Set.fromList ["HttpRequest(..)"])
  , ImportLine "Servant.Subscriber.Types" (Set.fromList ["Path(..)"])
  , ImportLine "Data.Tuple" (Set.fromList ["Tuple(..)"])
  ]

genModule :: Settings -> [Req PSType] -> Doc
genModule opts allReqs = let
    isSubscribable :: Req PSType -> Bool
    isSubscribable req = T.empty `elem`  req ^.reqFuncName . to unFunctionName
    reqs       = filter isSubscribable allReqs
    allParams  = concatMap reqToParams allReqs
    rParams    = getReaderParams opts allParams
    apiImports = reqsToImportLines reqs
    webAPIImports = importsFromList [
        ImportLine (opts ^. apiModuleName) (Set.fromList ["SPParams_(..)"])
      ]
    imports    = _standardImports opts
                  `mergeImportLines` apiImports
                  `mergeImportLines` subscriberImportLines
                  `mergeImportLines` webAPIImports
    moduleName = _apiModuleName opts <> ".Subscriber"
  in
    genModuleHeader moduleName imports
    </> (docIntercalate line . map (genFunction rParams)) reqs

genFunction :: [PSParam] -> Req PSType -> Doc
genFunction allRParams req = let
    rParamsSet = Set.fromList allRParams
    fnName = req ^. reqFuncName ^. camelCaseL
    responseType = case req ^. reqReturnType of
                     Nothing -> psUnit
                     Just t -> t
    allParamsList = makeTypedToUserParam responseType : 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 (Just psSubscriptions)
    body = genFnHead fnName pNames <+> genFnBody rParams req
  in signature </> body


genSignature :: Text -> [PSType] -> Maybe PSType -> Doc
genSignature = genSignatureBuilder $ "forall m a." <+/> "MonadReader (SPSettings_ SPParams_) m" <+/> "=>"

genFnBody :: [PSParam] -> Req PSType -> Doc
genFnBody rParams req = "do"
    </> indent 2 (
          "spOpts_' <- ask"
      </> "let spOpts_ = case spOpts_' of SPSettings_ o -> o"
      </> "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 reqPath ="     <+> genBuildPath (req ^. reqUrl . path))
      </> "let reqHeaders =" </> indent 6 (req ^. reqHeaders ^. to genBuildHeaders)
      </> "let reqQuery =" </> indent 6 (req ^. reqUrl ^. queryStr . to genBuildQuery)
      </> "let spReq = " <> hang 2 ("HttpRequest" </>
                                   "{ httpMethod:" <+> "httpMethod"
                               </> ", httpPath:" <+> "reqPath"
                               </> ", httpHeaders:" <+> "reqHeaders"
                               </> ", httpQuery:" <+> "reqQuery"
                               </> ", httpBody:" <+> case req ^. reqBody of
                                       Nothing -> "\"\""
                                       Just _ -> "printJson <<< encodeJson $ reqBody"
                               </> "}")
      </> "pure $ makeSubscriptions spReq (toUserType " <> strictText subscriberToUserId <> ")"
    )

----------
genBuildPath :: Path PSType -> Doc
genBuildPath p = "Path ["
  <> (docIntercalate (softline <> ", ") . map (genBuildSegment . unSegment)) p
  <> "]"

genBuildSegment :: SegmentType PSType -> Doc
genBuildSegment (Static (PathSegment seg)) = dquotes $ strictText seg
genBuildSegment (Cap arg) = "gDefaultToURLPiece" <+> arg ^. argName ^. to unPathSegment ^. to psVar

----------
genBuildQuery :: [QueryArg PSType] -> Doc
genBuildQuery []   = "[]"
genBuildQuery args = docIntercalate (softline <> "<> ") . map genBuildQueryArg $ args

genBuildQueryArg :: QueryArg PSType -> Doc
genBuildQueryArg arg = case arg ^. queryArgType of
    Normal -> genQueryEncoding "subGenNormalQuery"
    Flag   -> genQueryEncoding "subGenFlagQuery"
    List   -> genQueryEncoding "subGenListQuery"
  where
    argText = arg ^. queryArgName ^. argName ^. to unPathSegment
    argDoc = strictText argText
    genQueryEncoding fn = fn <+> dquotes argDoc <+> psVar argText

-----------

genBuildHeaders :: [HeaderArg PSType] -> Doc
genBuildHeaders = list . map genBuildHeader

genBuildHeader :: HeaderArg PSType -> Doc
genBuildHeader (HeaderArg arg) = let
    argText = arg ^. argName ^. to unPathSegment
    argDoc = strictText argText
  in
    align $ "Tuple" <+> dquotes argDoc <+> "(gDefaultToURLPiece" <+> psVar argText <> ")"
genBuildHeader (ReplaceHeaderArg _ _) = error "ReplaceHeaderArg - not yet implemented!"