{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE OverloadedStrings #-} module ApiModule where import qualified Data.Attoparsec.Text as Atto import qualified Data.Text as T import Data.Text (Text) import Data.Char (isSpace) import Data.List (sort, (\\)) import Control.Applicative (some, Alternative(..)) import Control.Monad (void) import Options (Config(..)) newtype Module = Module { getModuleName :: Text } data ApiModule = ApiModule { moduleName :: Module , imports :: [Module] } renderApiModule :: Config -> ApiModule -> Text renderApiModule config ApiModule { moduleName, imports } = ( T.unlines $ "{-# OPTIONS_GHC -fno-warn-partial-type-signatures " <> ghcOptions config <> " #-}" : "{-# LANGUAGE PartialTypeSignatures #-}" : "{-# LANGUAGE ExplicitNamespaces #-}" : "{-# LANGUAGE TypeOperators #-}" : "" : "module " <> getModuleName moduleName <> " (type Route, handler) where" : "" : "import Servant ((:<|>)((:<|>)))" : "import qualified GHC.Stack as Stack" : "import qualified Servant" : "" : fmap renderImport imports ) <> "\n" <> renderApiType imports <> "\n\n" <> renderServerFunction imports where renderImport :: Module -> Text renderImport modu = "import qualified " <> getModuleName modu renderApiType :: [Module] -> Text renderApiType modules = "type Route\n = " <> T.intercalate "\n :<|> " (fmap (\modul -> getModuleName modul <> ".Route") modules) renderServerFunction :: [Module] -> Text renderServerFunction modules = "handler :: Stack.HasCallStack => Servant.ServerT Route _\n" <> "handler\n = " <> handler where handler = T.intercalate "\n :<|> " (fmap (\modul -> getModuleName modul <> ".handler") modules) -- | used to calculate the difference between discovered handler modules -- and imported modules at the call sight of @makeApi@ splice difference :: Ord a => [a] -> [a] -> [a] difference listA listB = let sortedA = sort listA sortedB = sort listB in sortedA \\ sortedB decodeApiModule :: Text -> Either String ApiModule decodeApiModule input = Atto.parseOnly parserApiModule input parserApiModule :: Atto.Parser ApiModule parserApiModule = ApiModule <$> parserModule <*> parserImports skipBlockComment :: -- | Start of block comment Text -> -- | End of block comment Text -> Atto.Parser () skipBlockComment start end = p *> void (Atto.manyTill Atto.anyChar n) where p = Atto.string start n = Atto.string end skipLineComment :: Text -> Atto.Parser () skipLineComment prefix = Atto.string prefix *> (Atto.skipWhile (/= '\n')) {-# INLINEABLE skipLineComment #-} comment :: Atto.Parser () comment = skipBlockComment "{-" "-}" <|> skipLineComment "--" spaceConsumer :: Atto.Parser () spaceConsumer = Atto.skipMany singleSpaceConsumer where singleSpaceConsumer = (Atto.skipMany1 $ Atto.skip isSpace) <|> comment parserModule :: Atto.Parser Module parserModule = do spaceConsumer void $ Atto.string "module " moduleName <- Atto.takeWhile1 (not . isSpace) Atto.skipWhile (/= '\n') spaceConsumer pure $ Module moduleName parserImports :: Atto.Parser [Module] parserImports = some parserImport where parserImport = do void $ Atto.string "import " moduleName <- Atto.takeWhile1 (not . isSpace) spaceConsumer pure $ Module moduleName failModule :: Text -> [Text] -> Text failModule modName errMsgs = let typeErrors = fmap (\err -> "TypeLits.Text \"" <> err <> "\"") errMsgs in T.unlines [ "{-# LANGUAGE ExplicitNamespaces #-}" , "{-# LANGUAGE TypeOperators #-}" , "{-# LANGUAGE DataKinds #-}" , "" , "module " <> modName <> " where" , "" , "import qualified GHC.TypeLits as TypeLits" , "" , "handler :: TypeLits.TypeError (" <> T.intercalate " TypeLits.:$$: " typeErrors <> ")" , "handler = undefined" ]