diff --git a/Yesod/Dispatch.hs b/Yesod/Dispatch.hs index c62b65a7..de219d05 100644 --- a/Yesod/Dispatch.hs +++ b/Yesod/Dispatch.hs @@ -15,7 +15,6 @@ module Yesod.Dispatch -- ** Path pieces , SinglePiece (..) , MultiPiece (..) - , Strings , Texts -- * Convert to WAI , toWaiApp @@ -29,9 +28,8 @@ import Yesod.Internal.Core import Yesod.Handler import Yesod.Internal.Dispatch -import Web.Routes.Quasi (SinglePiece (..), MultiPiece (..), Strings) -import Web.Routes.Quasi.Parse (Resource (..), parseRoutes, parseRoutesFile) -import Web.Routes.Quasi.TH (THResource, Pieces (..), createRoutes, createRender) +import Web.PathPieces (SinglePiece (..), MultiPiece (..)) +import Yesod.Internal.RouteParsing (THResource, Pieces (..), createRoutes, createRender, Resource (..), parseRoutes, parseRoutesFile) import Language.Haskell.TH.Syntax import qualified Network.Wai as W diff --git a/Yesod/Internal/Dispatch.hs b/Yesod/Internal/Dispatch.hs index 7f269cb0..5be1fc0e 100644 --- a/Yesod/Internal/Dispatch.hs +++ b/Yesod/Internal/Dispatch.hs @@ -7,9 +7,8 @@ module Yesod.Internal.Dispatch import Prelude hiding (exp) import Language.Haskell.TH.Syntax -import Web.Routes.Quasi -import Web.Routes.Quasi.Parse -import Web.Routes.Quasi.TH +import Web.PathPieces +import Yesod.Internal.RouteParsing import Control.Monad (foldM) import Yesod.Handler (badMethod) import Yesod.Content (chooseRep) diff --git a/Yesod/Internal/RouteParsing.hs b/Yesod/Internal/RouteParsing.hs new file mode 100644 index 00000000..82489351 --- /dev/null +++ b/Yesod/Internal/RouteParsing.hs @@ -0,0 +1,349 @@ +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE DeriveDataTypeable #-} +module Yesod.Internal.RouteParsing + ( createRoutes + , createRender + , createParse + , createDispatch + , Pieces (..) + , THResource + , parseRoutes + , parseRoutesFile + , parseRoutesNoCheck + , parseRoutesFileNoCheck + , Resource (..) + , Piece (..) + ) where + +import Web.PathPieces +import Language.Haskell.TH.Syntax +import Data.Maybe +import Data.Either +import Data.List +import Data.Char (toLower) +import qualified Data.Text +import Language.Haskell.TH.Syntax +import Language.Haskell.TH.Quote +import Data.Data +import Data.Maybe +import qualified System.IO as SIO + +data Pieces = + SubSite + { ssType :: Type + , ssParse :: Exp + , ssRender :: Exp + , ssDispatch :: Exp + , ssToMasterArg :: Exp + , ssPieces :: [Piece] + } + | Simple [Piece] [String] -- ^ methods + deriving Show +type THResource = (String, Pieces) + +createRoutes :: [THResource] -> Q [Con] +createRoutes res = + return $ map go res + where + go (n, SubSite{ssType = s, ssPieces = pieces}) = + NormalC (mkName n) $ mapMaybe go' pieces ++ [(NotStrict, s)] + go (n, Simple pieces _) = NormalC (mkName n) $ mapMaybe go' pieces + go' (SinglePiece x) = Just (NotStrict, ConT $ mkName x) + go' (MultiPiece x) = Just (NotStrict, ConT $ mkName x) + go' (StaticPiece _) = Nothing + +-- | Generates the set of clauses necesary to parse the given 'Resource's. See 'quasiParse'. +createParse :: [THResource] -> Q [Clause] +createParse res = do + final' <- final + clauses <- mapM go res + return $ if areResourcesComplete res + then clauses + else clauses ++ [final'] + where + cons x y = ConP (mkName ":") [x, y] + go (constr, SubSite{ssParse = p, ssPieces = ps}) = do + ri <- [|Right|] + be <- [|ape|] + (pat', parse) <- mkPat' be ps $ ri `AppE` ConE (mkName constr) + + x <- newName "x" + let pat = init pat' ++ [VarP x] + + --let pat = foldr (\a b -> cons [LitP (StringL a), b]) (VarP x) pieces + let eitherSub = p `AppE` VarE x + let bod = be `AppE` parse `AppE` eitherSub + --let bod = fmape' `AppE` ConE (mkName constr) `AppE` eitherSub + return $ Clause [foldr1 cons pat] (NormalB bod) [] + go (n, Simple ps _) = do + ri <- [|Right|] + be <- [|ape|] + (pat, parse) <- mkPat' be ps $ ri `AppE` ConE (mkName n) + return $ Clause [foldr1 cons pat] (NormalB parse) [] + final = do + no <- [|Left "Invalid URL"|] + return $ Clause [WildP] (NormalB no) [] + mkPat' :: Exp -> [Piece] -> Exp -> Q ([Pat], Exp) + mkPat' be [MultiPiece s] parse = do + v <- newName $ "var" ++ s + fmp <- [|fromMultiPiece|] + let parse' = InfixE (Just parse) be $ Just $ fmp `AppE` VarE v + return ([VarP v], parse') + mkPat' _ (MultiPiece _:_) _parse = error "MultiPiece must be last" + mkPat' be (StaticPiece s:rest) parse = do + (x, parse') <- mkPat' be rest parse + let sp = LitP $ StringL s + return (sp : x, parse') + mkPat' be (SinglePiece s:rest) parse = do + fsp <- [|fromSinglePiece|] + v <- newName $ "var" ++ s + let parse' = InfixE (Just parse) be $ Just $ fsp `AppE` VarE v + (x, parse'') <- mkPat' be rest parse' + return (VarP v : x, parse'') + mkPat' _ [] parse = return ([ListP []], parse) + +-- | 'ap' for 'Either' +ape :: Either String (a -> b) -> Either String a -> Either String b +ape (Left e) _ = Left e +ape (Right _) (Left e) = Left e +ape (Right f) (Right a) = Right $ f a + +-- | Generates the set of clauses necesary to render the given 'Resource's. See +-- 'quasiRender'. +createRender :: [THResource] -> Q [Clause] +createRender = mapM go + where + go (n, Simple ps _) = do + let ps' = zip [1..] ps + let pat = ConP (mkName n) $ mapMaybe go' ps' + bod <- mkBod ps' + return $ Clause [pat] (NormalB $ TupE [bod, ListE []]) [] + go (n, SubSite{ssRender = r, ssPieces = pieces}) = do + cons' <- [|\a (b, c) -> (a ++ b, c)|] + let cons a b = cons' `AppE` a `AppE` b + x <- newName "x" + let r' = r `AppE` VarE x + let pieces' = zip [1..] pieces + let pat = ConP (mkName n) $ mapMaybe go' pieces' ++ [VarP x] + bod <- mkBod pieces' + return $ Clause [pat] (NormalB $ cons bod r') [] + go' (_, StaticPiece _) = Nothing + go' (i, _) = Just $ VarP $ mkName $ "var" ++ show (i :: Int) + mkBod :: (Show t) => [(t, Piece)] -> Q Exp + mkBod [] = lift ([] :: [String]) + mkBod ((_, StaticPiece x):xs) = do + x' <- lift x + pack <- [|Data.Text.pack|] + xs' <- mkBod xs + return $ ConE (mkName ":") `AppE` (pack `AppE` x') `AppE` xs' + mkBod ((i, SinglePiece _):xs) = do + let x' = VarE $ mkName $ "var" ++ show i + tsp <- [|toSinglePiece|] + let x'' = tsp `AppE` x' + xs' <- mkBod xs + return $ ConE (mkName ":") `AppE` x'' `AppE` xs' + mkBod ((i, MultiPiece _):_) = do + let x' = VarE $ mkName $ "var" ++ show i + tmp <- [|toMultiPiece|] + return $ tmp `AppE` x' + +-- | Whether the set of resources cover all possible URLs. +areResourcesComplete :: [THResource] -> Bool +areResourcesComplete res = + let (slurps, noSlurps) = partitionEithers $ mapMaybe go res + in case slurps of + [] -> False + _ -> let minSlurp = minimum slurps + in helper minSlurp $ reverse $ sort noSlurps + where + go :: THResource -> Maybe (Either Int Int) + go (_, Simple ps _) = + case reverse ps of + [] -> Just $ Right 0 + (MultiPiece _:rest) -> go' Left rest + x -> go' Right x + go (n, SubSite{ssPieces = ps}) = + go (n, Simple (ps ++ [MultiPiece ""]) []) + go' b x = if all isSingle x then Just (b $ length x) else Nothing + helper 0 _ = True + helper _ [] = False + helper m (i:is) + | i >= m = helper m is + | i + 1 == m = helper i is + | otherwise = False + isSingle (SinglePiece _) = True + isSingle _ = False + +notStatic :: Piece -> Bool +notStatic StaticPiece{} = False +notStatic _ = True + +createDispatch :: Exp -- ^ modify a master handler + -> Exp -- ^ convert a subsite handler to a master handler + -> [THResource] + -> Q [Clause] +createDispatch modMaster toMaster = mapM go + where + go :: (String, Pieces) -> Q Clause + go (n, Simple ps methods) = do + meth <- newName "method" + xs <- mapM newName $ replicate (length $ filter notStatic ps) "x" + let pat = [ ConP (mkName n) $ map VarP xs + , if null methods then WildP else VarP meth + ] + bod <- go' n meth xs methods + return $ Clause pat (NormalB bod) [] + go (n, SubSite{ssDispatch = d, ssToMasterArg = tma, ssPieces = ps}) = do + meth <- newName "method" + x <- newName "x" + xs <- mapM newName $ replicate (length $ filter notStatic ps) "x" + let pat = [ConP (mkName n) $ map VarP xs ++ [VarP x], VarP meth] + let bod = d `AppE` VarE x `AppE` VarE meth + fmap' <- [|fmap|] + let routeToMaster = foldl AppE (ConE (mkName n)) $ map VarE xs + tma' = foldl AppE tma $ map VarE xs + let toMaster' = toMaster `AppE` routeToMaster `AppE` tma' `AppE` VarE x + let bod' = InfixE (Just toMaster') fmap' (Just bod) + let bod'' = InfixE (Just modMaster) fmap' (Just bod') + return $ Clause pat (NormalB bod'') [] + go' n _ xs [] = do + jus <- [|Just|] + let bod = foldl AppE (VarE $ mkName $ "handle" ++ n) $ map VarE xs + return $ jus `AppE` (modMaster `AppE` bod) + go' n meth xs methods = do + noth <- [|Nothing|] + j <- [|Just|] + let noMatch = Match WildP (NormalB noth) [] + return $ CaseE (VarE meth) $ map (go'' n xs j) methods ++ [noMatch] + go'' n xs j method = + let pat = LitP $ StringL method + func = map toLower method ++ n + bod = foldl AppE (VarE $ mkName func) $ map VarE xs + in Match pat (NormalB $ j `AppE` (modMaster `AppE` bod)) [] + +-- | 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 + , quotePat = y + } + where + x s = do + let res = resourcesFromString s + case findOverlaps res of + [] -> lift res + z -> error $ "Overlapping routes: " ++ unlines (map show z) + y = dataToPatQ (const Nothing) . resourcesFromString + +parseRoutesFile :: FilePath -> Q Exp +parseRoutesFile fp = do + s <- qRunIO $ readUtf8File fp + quoteExp parseRoutes s + +parseRoutesFileNoCheck :: FilePath -> Q Exp +parseRoutesFileNoCheck fp = do + s <- qRunIO $ readUtf8File fp + quoteExp parseRoutesNoCheck 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 = x + , quotePat = y + } + where + x = lift . resourcesFromString + y = dataToPatQ (const Nothing) . resourcesFromString + +instance Lift Resource where + lift (Resource s ps h) = do + r <- [|Resource|] + s' <- lift s + ps' <- lift ps + h' <- lift h + return $ r `AppE` s' `AppE` ps' `AppE` h' + +-- | A single resource pattern. +-- +-- First argument is the name of the constructor, second is the URL pattern to +-- match, third is how to dispatch. +data Resource = Resource String [Piece] [String] + deriving (Read, Show, Eq, Data, Typeable) + +-- | A single piece of a URL, delimited by slashes. +-- +-- In the case of StaticPiece, the argument is the value of the piece; for the +-- other constructors, it is the name of the parameter represented by this +-- piece. That value is not used here, but may be useful elsewhere. +data Piece = StaticPiece String + | SinglePiece String + | MultiPiece String + deriving (Read, Show, Eq, Data, Typeable) + +instance Lift Piece where + lift (StaticPiece s) = do + c <- [|StaticPiece|] + s' <- lift s + return $ c `AppE` s' + lift (SinglePiece s) = do + c <- [|SinglePiece|] + s' <- lift s + return $ c `AppE` s' + lift (MultiPiece s) = do + c <- [|MultiPiece|] + s' <- lift s + return $ c `AppE` s' + +-- | 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 -> [Resource] +resourcesFromString = + mapMaybe go . lines + where + go s = + case takeWhile (/= "--") $ words s of + (pattern:constr:rest) -> + let pieces = piecesFromString $ drop1Slash pattern + in Just $ Resource constr pieces rest + [] -> Nothing + _ -> error $ "Invalid resource line: " ++ s + +drop1Slash :: String -> String +drop1Slash ('/':x) = x +drop1Slash x = x + +piecesFromString :: String -> [Piece] +piecesFromString "" = [] +piecesFromString x = + let (y, z) = break (== '/') x + in pieceFromString y : piecesFromString (drop1Slash z) + +pieceFromString :: String -> Piece +pieceFromString ('#':x) = SinglePiece x +pieceFromString ('*':x) = MultiPiece x +pieceFromString x = StaticPiece x + +findOverlaps :: [Resource] -> [(Resource, Resource)] +findOverlaps = gos . map justPieces + where + justPieces r@(Resource _ ps _) = (ps, r) + gos [] = [] + gos (x:xs) = mapMaybe (go x) xs ++ gos xs + go (StaticPiece x:xs, xr) (StaticPiece y:ys, yr) + | x == y = go (xs, xr) (ys, yr) + | otherwise = Nothing + go (MultiPiece _:_, xr) (_, yr) = Just (xr, yr) + go (_, xr) (MultiPiece _:_, yr) = Just (xr, yr) + go ([], xr) ([], yr) = Just (xr, yr) + go ([], _) (_, _) = Nothing + go (_, _) ([], _) = Nothing + go (_:xs, xr) (_:ys, yr) = go (xs, xr) (ys, yr) diff --git a/yesod-core.cabal b/yesod-core.cabal index b9d3a276..35794f2f 100644 --- a/yesod-core.cabal +++ b/yesod-core.cabal @@ -33,7 +33,7 @@ library , bytestring >= 0.9.1.4 && < 0.10 , text >= 0.5 && < 0.12 , template-haskell - , web-routes-quasi >= 0.7.0.1 && < 0.8 + , path-pieces >= 0.0 && < 0.1 , hamlet >= 0.9 && < 0.10 , blaze-builder >= 0.2.1 && < 0.4 , transformers >= 0.2 && < 0.3 @@ -63,6 +63,7 @@ library Yesod.Internal.Session Yesod.Internal.Request Yesod.Internal.Dispatch + Yesod.Internal.RouteParsing Paths_yesod_core ghc-options: -Wall if flag(test)