/yesod-core/Yesod/Message.hs
https://github.com/repos-haskell/yesod · Haskell · 259 lines · 229 code · 25 blank · 5 comment · 15 complexity · d24939fce7a1a9ed2810677ba25c67f0 MD5 · raw file
- {-# LANGUAGE TemplateHaskell #-}
- {-# LANGUAGE MultiParamTypeClasses #-}
- {-# LANGUAGE FlexibleInstances #-}
- {-# LANGUAGE TypeSynonymInstances #-}
- {-# LANGUAGE ExistentialQuantification #-}
- module Yesod.Message
- ( mkMessage
- , RenderMessage (..)
- , ToMessage (..)
- , SomeMessage (..)
- ) where
- import Language.Haskell.TH.Syntax
- import Data.Text (Text, pack, unpack)
- import System.Directory
- import Data.Maybe (catMaybes)
- import Data.List (isSuffixOf, sortBy, foldl')
- import qualified Data.ByteString as S
- import Data.Text.Encoding (decodeUtf8)
- import Data.Char (isSpace, toLower, toUpper)
- import Data.Ord (comparing)
- import Text.Shakespeare.Base (Deref (..), Ident (..), parseHash, derefToExp)
- import Text.ParserCombinators.Parsec (parse, many, eof, many1, noneOf, (<|>))
- import Control.Arrow ((***))
- import Data.Monoid (mempty, mappend)
- import qualified Data.Text as T
- import Data.String (IsString (fromString))
- class ToMessage a where
- toMessage :: a -> Text
- instance ToMessage Text where
- toMessage = id
- instance ToMessage String where
- toMessage = Data.Text.pack
- class RenderMessage master message where
- renderMessage :: master
- -> [Text] -- ^ languages
- -> message
- -> Text
- instance RenderMessage master Text where
- renderMessage _ _ = id
- type Lang = Text
- mkMessage :: String
- -> FilePath
- -> Lang
- -> Q [Dec]
- mkMessage dt folder lang = do
- files <- qRunIO $ getDirectoryContents folder
- contents <- qRunIO $ fmap catMaybes $ mapM (loadLang folder) files
- sdef <-
- case lookup lang contents of
- Nothing -> error $ "Did not find main language file: " ++ unpack lang
- Just def -> toSDefs def
- mapM_ (checkDef sdef) $ map snd contents
- let dt' = ConT $ mkName dt
- let mname = mkName $ dt ++ "Message"
- c1 <- fmap concat $ mapM (toClauses dt) contents
- c2 <- mapM (sToClause dt) sdef
- c3 <- defClause
- return
- [ DataD [] mname [] (map (toCon dt) sdef) []
- , InstanceD
- []
- (ConT ''RenderMessage `AppT` dt' `AppT` ConT mname)
- [ FunD (mkName "renderMessage") $ c1 ++ c2 ++ [c3]
- ]
- ]
- toClauses :: String -> (Lang, [Def]) -> Q [Clause]
- toClauses dt (lang, defs) =
- mapM go defs
- where
- go def = do
- a <- newName "lang"
- (pat, bod) <- mkBody dt (constr def) (map fst $ vars def) (content def)
- guard <- fmap NormalG [|$(return $ VarE a) == pack $(lift $ unpack lang)|]
- return $ Clause
- [WildP, ConP (mkName ":") [VarP a, WildP], pat]
- (GuardedB [(guard, bod)])
- []
- mkBody :: String -- ^ datatype
- -> String -- ^ constructor
- -> [String] -- ^ variable names
- -> [Content]
- -> Q (Pat, Exp)
- mkBody dt cs vs ct = do
- vp <- mapM go vs
- let pat = RecP (mkName $ "Msg" ++ cs) (map (varName dt *** VarP) vp)
- let ct' = map (fixVars vp) ct
- pack' <- [|Data.Text.pack|]
- tomsg <- [|toMessage|]
- let ct'' = map (toH pack' tomsg) ct'
- mapp <- [|mappend|]
- let app a b = InfixE (Just a) mapp (Just b)
- e <-
- case ct'' of
- [] -> [|mempty|]
- [x] -> return x
- (x:xs) -> return $ foldl' app x xs
- return (pat, e)
- where
- toH pack' _ (Raw s) = pack' `AppE` SigE (LitE (StringL s)) (ConT ''String)
- toH _ tomsg (Var d) = tomsg `AppE` derefToExp [] d
- go x = do
- let y = mkName $ '_' : x
- return (x, y)
- fixVars vp (Var d) = Var $ fixDeref vp d
- fixVars _ (Raw s) = Raw s
- fixDeref vp (DerefIdent (Ident i)) = DerefIdent $ Ident $ fixIdent vp i
- fixDeref vp (DerefBranch a b) = DerefBranch (fixDeref vp a) (fixDeref vp b)
- fixDeref _ d = d
- fixIdent vp i =
- case lookup i vp of
- Nothing -> i
- Just y -> nameBase y
- sToClause :: String -> SDef -> Q Clause
- sToClause dt sdef = do
- (pat, bod) <- mkBody dt (sconstr sdef) (map fst $ svars sdef) (scontent sdef)
- return $ Clause
- [WildP, ConP (mkName "[]") [], pat]
- (NormalB bod)
- []
- defClause :: Q Clause
- defClause = do
- a <- newName "sub"
- c <- newName "langs"
- d <- newName "msg"
- rm <- [|renderMessage|]
- return $ Clause
- [VarP a, ConP (mkName ":") [WildP, VarP c], VarP d]
- (NormalB $ rm `AppE` VarE a `AppE` VarE c `AppE` VarE d)
- []
- toCon :: String -> SDef -> Con
- toCon dt (SDef c vs _) =
- RecC (mkName $ "Msg" ++ c) $ map go vs
- where
- go (n, t) = (varName dt n, NotStrict, ConT $ mkName t)
- varName :: String -> String -> Name
- varName a y =
- mkName $ concat [lower a, "Message", upper y]
- where
- lower (x:xs) = toLower x : xs
- lower [] = []
- upper (x:xs) = toUpper x : xs
- upper [] = []
- checkDef :: [SDef] -> [Def] -> Q ()
- checkDef x y =
- go (sortBy (comparing sconstr) x) (sortBy (comparing constr) y)
- where
- go _ [] = return ()
- go [] (b:_) = error $ "Extra message constructor: " ++ constr b
- go (a:as) (b:bs)
- | sconstr a < constr b = go as (b:bs)
- | sconstr a > constr b = error $ "Extra message constructor: " ++ constr b
- | otherwise = do
- go' (svars a) (vars b)
- go as bs
- go' ((an, at):as) ((bn, mbt):bs)
- | an /= bn = error "Mismatched variable names"
- | otherwise =
- case mbt of
- Nothing -> go' as bs
- Just bt
- | at == bt -> go' as bs
- | otherwise -> error "Mismatched variable types"
- go' [] [] = return ()
- go' _ _ = error "Mistmached variable count"
- toSDefs :: [Def] -> Q [SDef]
- toSDefs = mapM toSDef
- toSDef :: Def -> Q SDef
- toSDef d = do
- vars' <- mapM go $ vars d
- return $ SDef (constr d) vars' (content d)
- where
- go (a, Just b) = return (a, b)
- go (a, Nothing) = error $ "Main language missing type for " ++ show (constr d, a)
- data SDef = SDef
- { sconstr :: String
- , svars :: [(String, String)]
- , scontent :: [Content]
- }
- data Def = Def
- { constr :: String
- , vars :: [(String, Maybe String)]
- , content :: [Content]
- }
- loadLang :: FilePath -> FilePath -> IO (Maybe (Lang, [Def]))
- loadLang folder file = do
- let file' = folder ++ '/' : file
- e <- doesFileExist file'
- if e && ".msg" `isSuffixOf` file
- then do
- let lang = pack $ reverse $ drop 4 $ reverse file
- bs <- S.readFile file'
- let s = unpack $ decodeUtf8 bs
- defs <- fmap catMaybes $ mapM parseDef $ lines s
- return $ Just (lang, defs)
- else return Nothing
- parseDef :: String -> IO (Maybe Def)
- parseDef "" = return Nothing
- parseDef ('#':_) = return Nothing
- parseDef s =
- case end of
- ':':end' -> do
- content' <- fmap compress $ parseContent $ dropWhile isSpace end'
- case words begin of
- [] -> error $ "Missing constructor: " ++ s
- (w:ws) -> return $ Just Def
- { constr = w
- , vars = map parseVar ws
- , content = content'
- }
- _ -> error $ "Missing colon: " ++ s
- where
- (begin, end) = break (== ':') s
- data Content = Var Deref | Raw String
- compress :: [Content] -> [Content]
- compress [] = []
- compress (Raw a:Raw b:rest) = compress $ Raw (a ++ b) : rest
- compress (x:y) = x : compress y
- parseContent :: String -> IO [Content]
- parseContent s =
- either (error . show) return $ parse go s s
- where
- go = do
- x <- many go'
- eof
- return x
- go' = (Raw `fmap` many1 (noneOf "#")) <|> (fmap (either Raw Var) parseHash)
- parseVar :: String -> (String, Maybe String)
- parseVar s =
- case break (== '@') s of
- (x, '@':y) -> (x, Just y)
- _ -> (s, Nothing)
- data SomeMessage master = forall msg. RenderMessage master msg => SomeMessage msg
- instance IsString (SomeMessage master) where
- fromString = SomeMessage . T.pack