git-subtree-dir: yesod-core git-subtree-mainline:cd5ee0fb12git-subtree-split:982d6185be
251 lines
7.5 KiB
Haskell
251 lines
7.5 KiB
Haskell
{-# LANGUAGE TemplateHaskell #-}
|
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
|
{-# LANGUAGE FlexibleInstances #-}
|
|
{-# LANGUAGE TypeSynonymInstances #-}
|
|
module Yesod.Message
|
|
( mkMessage
|
|
, RenderMessage (..)
|
|
, ToMessage (..)
|
|
) 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 (Deref (..), Ident (..), parseHash, derefToExp)
|
|
import Text.ParserCombinators.Parsec (parse, many, eof, many1, noneOf, (<|>))
|
|
import Control.Arrow ((***))
|
|
import Data.Monoid (mempty, mappend)
|
|
|
|
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)
|