yesod-core-1.7.0.0: src/Yesod/Routes/Parse.hs
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# OPTIONS_GHC -fno-warn-missing-fields #-} -- QuasiQuoter
module Yesod.Routes.Parse
( parseRoutes
, parseRoutesFile
, parseRoutesNoCheck
, parseRoutesFileNoCheck
, parseType
, parseTypeM
, parseTypeTree
, TypeTree (..)
, dropBracket
, dropBracketM
, nameToType
, isTvar
) where
import Language.Haskell.TH.Syntax
import Data.Char (isUpper, isLower, isSpace)
import Language.Haskell.TH.Quote
import qualified System.IO as SIO
import Yesod.Routes.TH
import Yesod.Routes.Overlap (findOverlapNames)
import Data.List (foldl', isPrefixOf)
import Data.Maybe (mapMaybe)
import qualified Data.Set as Set
-- | A quasi-quoter to parse a string into a list of 'Resource's. Checks for
-- overlapping routes, failing if present; use 'parseRoutesNoCheck' to skip the
-- checking. See documentation site for details on syntax.
parseRoutes :: QuasiQuoter
parseRoutes = QuasiQuoter { quoteExp = x }
where
x s = do
res <- resourcesFromString s
case findOverlapNames res of
[] -> lift res
z -> fail $ unlines $ "Overlapping routes: " : map show z
-- | Same as 'parseRoutes', but uses an external file instead of quasiquotation.
--
-- The recommended file extension is @.yesodroutes@.
parseRoutesFile :: FilePath -> Q Exp
parseRoutesFile = parseRoutesFileWith parseRoutes
-- | Same as 'parseRoutesNoCheck', but uses an external file instead of quasiquotation.
--
-- The recommended file extension is @.yesodroutes@.
parseRoutesFileNoCheck :: FilePath -> Q Exp
parseRoutesFileNoCheck = parseRoutesFileWith parseRoutesNoCheck
parseRoutesFileWith :: QuasiQuoter -> FilePath -> Q Exp
parseRoutesFileWith qq fp = do
qAddDependentFile fp
s <- qRunIO $ readUtf8File fp
quoteExp qq s
readUtf8File :: FilePath -> IO String
readUtf8File fp = do
h <- SIO.openFile fp SIO.ReadMode
SIO.hSetEncoding h SIO.utf8_bom
SIO.hGetContents h
-- | Same as 'parseRoutes', but performs no overlap checking.
parseRoutesNoCheck :: QuasiQuoter
parseRoutesNoCheck = QuasiQuoter
{ quoteExp = \s -> resourcesFromString s >>= lift
}
-- | Converts a multi-line string to a set of resources. See documentation for
-- the format of this string. Runs in any 'MonadFail' (e.g. the 'Q' monad of a
-- splice) and reports malformed input through 'fail', so diagnostics are
-- attributed to the splice rather than surfacing as a raw 'error' call stack.
resourcesFromString :: MonadFail m => String -> m [ResourceTree String]
resourcesFromString =
fmap fst . parse 0 . filter (not . all (== ' ')) . foldr lineContinuations [] . lines . filter (/= '\r')
where
parse _ [] = pure ([], [])
parse indent (thisLine:otherLines)
| length spaces < indent = pure ([], thisLine : otherLines)
| otherwise = do
(this, otherLines') <- classify
(others, remainder) <- parse indent otherLines'
pure (this others, remainder)
where
parseAttr ('!':x) = Just x
parseAttr _ = Nothing
stripColonLast =
go id
where
go _ [] = Nothing
go front [x]
| null x = Nothing
| last x == ':' = Just $ front [init x]
| otherwise = Nothing
go front (x:xs) = go (front . (x:)) xs
spaces = takeWhile (== ' ') thisLine
classify = do
toks <- splitSpaces thisLine
case takeWhile (not . isPrefixOf "--") toks of
(pattern:rest0)
| Just (constr:rest) <- stripColonLast rest0
, Just attrs <- mapM parseAttr rest -> do
(children, otherLines'') <- parse (length spaces + 1) otherLines
let children' = addAttrs attrs children
(pieces, mmulti, check) <- piecesFromStringCheck pattern
case mmulti of
Nothing -> pure ()
Just _ -> fail $ "Parent routes cannot have a multipiece: " ++ pattern
pure ((ResourceParent constr check (Set.fromList attrs) pieces children' :), otherLines'')
(pattern:constr:rest) -> do
(pieces, mmulti, check) <- piecesFromStringCheck pattern
let (attrs, rest') = takeAttrs rest
disp <- dispatchFromString rest' mmulti
pure ((ResourceLeaf (Resource constr pieces disp attrs check):), otherLines)
[] -> pure (id, otherLines)
_ -> fail $ "Invalid resource line: " ++ thisLine
-- | Splits a string by spaces, as long as the spaces are not enclosed by curly brackets (not recursive).
splitSpaces :: MonadFail m => String -> m [String]
splitSpaces "" = pure []
splitSpaces str = do
(rest, piece) <- parse $ dropWhile isSpace str
rest' <- splitSpaces rest
pure (piece : rest')
where
parse ('{':s) = do (r, p) <- parseBracket s; pure (r, '{':p)
parse (c:s) | isSpace c = pure (s, [])
parse (c:s) = do (r, p) <- parse s; pure (r, c:p)
parse "" = pure ("", "")
parseBracket ('{':_) = fail $ "Invalid resource line (nested curly bracket): " ++ str
parseBracket ('}':s) = do (r, p) <- parse s; pure (r, '}':p)
parseBracket (c:s) = do (r, p) <- parseBracket s; pure (r, c:p)
parseBracket "" = fail $ "Invalid resource line (unclosed curly bracket): " ++ str
piecesFromStringCheck :: MonadFail m => String -> m ([Piece String], Maybe String, Bool)
piecesFromStringCheck s0 = do
let (s1, check1) = stripBang s0
(pieces', mmulti') <- piecesFromString $ drop1Slash s1
let pieces = map snd pieces'
mmulti = fmap snd mmulti'
check = check1 && all fst pieces' && maybe True fst mmulti'
pure (pieces, mmulti, check)
where
stripBang ('!':rest) = (rest, False)
stripBang x = (x, True)
addAttrs :: [String] -> [ResourceTree String] -> [ResourceTree String]
addAttrs attrs =
map goTree
where
goTree (ResourceLeaf res) = ResourceLeaf (goRes res)
goTree (ResourceParent v w x y z) = ResourceParent v w x y (map goTree z)
goRes res =
res { resourceAttrs = noDupes ++ resourceAttrs res }
where
usedKeys = Set.fromList $ map fst $ mapMaybe toPair $ resourceAttrs res
used attr =
case toPair attr of
Nothing -> False
Just (key, _) -> key `Set.member` usedKeys
noDupes = filter (not . used) attrs
toPair s =
case break (== '=') s of
(x, '=':y) -> Just (x, y)
_ -> Nothing
-- | Take attributes out of the list and put them in the first slot in the
-- result tuple.
takeAttrs :: [String] -> ([String], [String])
takeAttrs =
go id id
where
go x y [] = (x [], y [])
go x y (('!':attr):rest) = go (x . (attr:)) y rest
go x y (z:rest) = go x (y . (z:)) rest
dispatchFromString :: MonadFail m => [String] -> Maybe String -> m (Dispatch String)
dispatchFromString rest mmulti
| null rest = pure $ Methods mmulti []
| all (all isUpper) rest = pure $ Methods mmulti rest
dispatchFromString [subTyp, subFun] Nothing =
pure $ Subsite subTyp subFun
dispatchFromString [_, _] Just{} =
fail "Subsites cannot have a multipiece"
dispatchFromString rest _ = fail $ "Invalid list of methods: " ++ show rest
drop1Slash :: String -> String
drop1Slash ('/':x) = x
drop1Slash x = x
piecesFromString :: MonadFail m => String -> m ([(CheckOverlap, Piece String)], Maybe (CheckOverlap, String))
piecesFromString "" = pure ([], Nothing)
piecesFromString x = do
rest <- piecesFromString $ drop 1 z
this <- pieceFromString y
case (this, rest) of
(Left typ, ([], Nothing)) -> pure ([], Just typ)
(Left _, _) -> fail "Multipiece must be last piece"
(Right piece, (pieces, mtyp)) -> pure (piece:pieces, mtyp)
where
(y, z) = break (== '/') x
parseType :: String -> Type
parseType orig =
maybe (error $ "Invalid type: " ++ show orig) ttToType $ parseTypeTree orig
-- | 'parseType' in 'MonadFail': a malformed type surfaces via 'fail' instead of
-- a raw 'error'. Used at the 'Language.Haskell.TH.Q' splice sites so a bad type
-- in a route definition becomes an attributed compile error. The pure
-- 'parseType' is retained for callers (e.g. tests) supplying known-good input.
--
-- @since 1.7.0.0
parseTypeM :: MonadFail m => String -> m Type
parseTypeM orig =
maybe (fail $ "Invalid type: " ++ show orig) (pure . ttToType) $ parseTypeTree orig
parseTypeTree :: String -> Maybe TypeTree
parseTypeTree orig =
toTypeTree pieces
where
pieces = filter (not . null) $ splitOn (\c -> c == '-' || c == ' ') $ addDashes orig
addDashes [] = []
addDashes (x:xs) =
front $ addDashes xs
where
front rest
| x `elem` "()[]" = '-' : x : '-' : rest
| otherwise = x : rest
splitOn c s =
case y' of
_:y -> x : splitOn c y
[] -> [x]
where
(x, y') = break c s
data TypeTree = TTTerm String
| TTApp TypeTree TypeTree
| TTList TypeTree
deriving (Show, Eq)
toTypeTree :: [String] -> Maybe TypeTree
toTypeTree orig = do
(x, []) <- gos orig
return x
where
go [] = Nothing
go ("(":xs) = do
(x, rest) <- gos xs
case rest of
")":rest' -> Just (x, rest')
_ -> Nothing
go ("[":xs) = do
(x, rest) <- gos xs
case rest of
"]":rest' -> Just (TTList x, rest')
_ -> Nothing
go (x:xs) = Just (TTTerm x, xs)
gos xs1 = do
(t, xs2) <- go xs1
(ts, xs3) <- gos' id xs2
Just (foldl' TTApp t ts, xs3)
gos' front [] = Just (front [], [])
gos' front (x:xs)
| x `elem` words ") ]" = Just (front [], x:xs)
| otherwise = do
(t, xs') <- go $ x:xs
gos' (front . (t:)) xs'
ttToType :: TypeTree -> Type
ttToType (TTTerm s) = fst $ nameToType s
ttToType (TTApp x y) = ttToType x `AppT` ttToType y
ttToType (TTList t) = ListT `AppT` ttToType t
nameToType :: String -> (Type, Name)
nameToType t = (, nm) $ if isTvar t
then VarT nm
else ConT nm
where nm = mkName t
isTvar :: String -> Bool
isTvar (h:_) = isLower h
isTvar _ = False
-- | 'MonadFail' so that an unclosed bracket in a 'Dynamic' piece (via
-- 'dropBracketM') surfaces as an attributed splice error rather than a raw
-- 'error' (the pure 'dropBracket' would otherwise bottom out deep in codegen).
pieceFromString :: MonadFail m => String -> m (Either (CheckOverlap, String) (CheckOverlap, Piece String))
pieceFromString ('#':'!':x) = Right . (False,) . Dynamic <$> dropBracketM x
pieceFromString ('!':'#':x) = Right . (False,) . Dynamic <$> dropBracketM x -- https://github.com/yesodweb/yesod/issues/652
pieceFromString ('#':x) = Right . (True,) . Dynamic <$> dropBracketM x
pieceFromString ('*':'!':x) = pure $ Left (False, x)
pieceFromString ('+':'!':x) = pure $ Left (False, x)
pieceFromString ('!':'*':x) = pure $ Left (False, x)
pieceFromString ('!':'+':x) = pure $ Left (False, x)
pieceFromString ('*':x) = pure $ Left (True, x)
pieceFromString ('+':x) = pure $ Left (True, x)
pieceFromString ('!':x) = pure $ Right (False, Static x)
pieceFromString x = pure $ Right (True, Static x)
dropBracket :: String -> String
dropBracket str@('{':x) = case break (== '}') x of
(s, "}") -> s
_ -> error $ "Unclosed bracket ('{'): " ++ str
dropBracket x = x
-- | 'dropBracket' in 'MonadFail': an unclosed @{@ surfaces via 'fail' instead
-- of a raw 'error', so it becomes an attributed compile error at a splice site.
--
-- @since 1.7.0.0
dropBracketM :: MonadFail m => String -> m String
dropBracketM str@('{':x) = case break (== '}') x of
(s, "}") -> pure s
_ -> fail $ "Unclosed bracket ('{'): " ++ str
dropBracketM x = pure x
-- | If this line ends with a backslash, concatenate it together with the next line.
--
-- @since 1.6.8
lineContinuations :: String -> [String] -> [String]
lineContinuations this [] = [this]
lineContinuations this below@(next:rest) = case unsnoc this of
Just (this', '\\') -> (this' ++ next):rest
_ -> this:below
where unsnoc s = if null s then Nothing else Just (init s, last s)