yesod/yesod-routes/Yesod/Routes/Parse.hs
Michael Snoyman e23c78f2ce Better overlap rules #779
We now have the concept that either an entire route is overlap checked
or not. This is essentially what we had before, except there was code
littered everywhere on the mistaken assumption that just one component
could be overlap checked. This also allows us to mark parent routes or
multipiece components as non-overlapped checked.

In addition, if you put a bang at the beginning of the pattern, the
entire route is not overlap checked. The previous syntax is kept for
backwards compatibility.
2014-07-23 12:40:24 +03:00

214 lines
7.0 KiB
Haskell

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# OPTIONS_GHC -fno-warn-missing-fields #-} -- QuasiQuoter
module Yesod.Routes.Parse
( parseRoutes
, parseRoutesFile
, parseRoutesNoCheck
, parseRoutesFileNoCheck
, parseType
, parseTypeTree
, TypeTree (..)
) where
import Language.Haskell.TH.Syntax
import Data.Char (isUpper)
import Language.Haskell.TH.Quote
import qualified System.IO as SIO
import Yesod.Routes.TH
import Yesod.Routes.Overlap (findOverlapNames)
import Data.List (foldl')
-- | 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
let res = resourcesFromString s
case findOverlapNames res of
[] -> lift res
z -> error $ unlines $ "Overlapping routes: " : map show z
parseRoutesFile :: FilePath -> Q Exp
parseRoutesFile = parseRoutesFileWith parseRoutes
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 = lift . resourcesFromString
}
-- | Convert a multi-line string to a set of resources. See documentation for
-- the format of this string. This is a partial function which calls 'error' on
-- invalid input.
resourcesFromString :: String -> [ResourceTree String]
resourcesFromString =
fst . parse 0 . filter (not . all (== ' ')) . lines
where
parse _ [] = ([], [])
parse indent (thisLine:otherLines)
| length spaces < indent = ([], thisLine : otherLines)
| otherwise = (this others, remainder)
where
spaces = takeWhile (== ' ') thisLine
(others, remainder) = parse indent otherLines'
(this, otherLines') =
case takeWhile (/= "--") $ words thisLine of
[pattern, constr] | last constr == ':' ->
let (children, otherLines'') = parse (length spaces + 1) otherLines
(pieces, Nothing, check) = piecesFromStringCheck pattern
in ((ResourceParent (init constr) check pieces children :), otherLines'')
(pattern:constr:rest) ->
let (pieces, mmulti, check) = piecesFromStringCheck pattern
(attrs, rest') = takeAttrs rest
disp = dispatchFromString rest' mmulti
in ((ResourceLeaf (Resource constr pieces disp attrs check):), otherLines)
[] -> (id, otherLines)
_ -> error $ "Invalid resource line: " ++ thisLine
piecesFromStringCheck :: String -> ([Piece String], Maybe String, Bool)
piecesFromStringCheck s0 =
(pieces, mmulti, check)
where
(s1, check1) = stripBang s0
(pieces', mmulti') = piecesFromString $ drop1Slash s1
pieces = map snd pieces'
mmulti = fmap snd mmulti'
check = check1 && all fst pieces' && maybe True fst mmulti'
stripBang ('!':rest) = (rest, False)
stripBang x = (x, True)
-- | 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 :: [String] -> Maybe String -> Dispatch String
dispatchFromString rest mmulti
| null rest = Methods mmulti []
| all (all isUpper) rest = Methods mmulti rest
dispatchFromString [subTyp, subFun] Nothing =
Subsite subTyp subFun
dispatchFromString [_, _] Just{} =
error "Subsites cannot have a multipiece"
dispatchFromString rest _ = error $ "Invalid list of methods: " ++ show rest
drop1Slash :: String -> String
drop1Slash ('/':x) = x
drop1Slash x = x
piecesFromString :: String -> ([(CheckOverlap, Piece String)], Maybe (CheckOverlap, String))
piecesFromString "" = ([], Nothing)
piecesFromString x =
case (this, rest) of
(Left typ, ([], Nothing)) -> ([], Just typ)
(Left _, _) -> error "Multipiece must be last piece"
(Right piece, (pieces, mtyp)) -> (piece:pieces, mtyp)
where
(y, z) = break (== '/') x
this = pieceFromString y
rest = piecesFromString $ drop 1 z
parseType :: String -> Type
parseType orig =
maybe (error $ "Invalid type: " ++ show orig) ttToType $ parseTypeTree orig
parseTypeTree :: String -> Maybe TypeTree
parseTypeTree orig =
toTypeTree pieces
where
pieces = filter (not . null) $ splitOn '-' $ 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) = ConT $ mkName s
ttToType (TTApp x y) = ttToType x `AppT` ttToType y
ttToType (TTList t) = ListT `AppT` ttToType t
pieceFromString :: String -> Either (CheckOverlap, String) (CheckOverlap, Piece String)
pieceFromString ('#':'!':x) = Right $ (False, Dynamic x)
pieceFromString ('!':'#':x) = Right $ (False, Dynamic x) -- https://github.com/yesodweb/yesod/issues/652
pieceFromString ('#':x) = Right $ (True, Dynamic x)
pieceFromString ('*':'!':x) = Left (False, x)
pieceFromString ('+':'!':x) = Left (False, x)
pieceFromString ('!':'*':x) = Left (False, x)
pieceFromString ('!':'+':x) = Left (False, x)
pieceFromString ('*':x) = Left (True, x)
pieceFromString ('+':x) = Left (True, x)
pieceFromString ('!':x) = Right $ (False, Static x)
pieceFromString x = Right $ (True, Static x)