module Main where import Control.Monad (when) import System.Environment (getArgs, getProgName) import System.Exit (exitFailure) import System.Console.GetOpt (getOpt, usageInfo, ArgOrder(..), OptDescr(..), ArgDescr(..)) import System.IO (hPutStrLn, stderr) import Data.List (intercalate) import Data.Char (isUpper, isSpace) import Data.Maybe (catMaybes, isJust) import Yesod.Routes.Dispatch (Piece(Static, Dynamic)) import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.IO as T import Data.Attoparsec.Text import Control.Applicative data Flag = Help | PathHelpers | Routes | NArgs Int | Mod String deriving (Show, Read, Eq) flags :: [OptDescr Flag] flags = [ Option ['r'] ["routes"] (NoArg Routes) "Generate routes.", Option ['p'] ["pathHelpers"] (NoArg PathHelpers) "Generate actionPath helper functions.", Option ['m'] ["module"] (ReqArg Mod "MODULE") "Implementation module to import.", Option ['n'] ["nArgs"] (ReqArg (NArgs . read) "NARGS") "Number of arguments the `route` function takes.", Option ['h'] ["help"] (NoArg Help) "Show this help text." ] usage :: [String] -> IO () usage errors = do mapM_ (hPutStrLn stderr) errors name <- getProgName hPutStrLn stderr $ usageInfo (name ++ " -r -p [-m MODULE] [-n NARGS] ") flags data Route = Route { method :: Text, pieces :: [Piece], multi :: Bool, target :: Text } deriving (Show) instance Show Piece where show Dynamic = "Dynamic" show (Static s) = "Static (pack " ++ show (T.unpack s) ++ ")" isDynamic :: Piece -> Bool isDynamic Dynamic = True isDynamic _ = False argList :: String -> Int -> [String] argList s n = snd $ until ((<1) . fst) (\(n,xs) -> (n-1, (s ++ show n):xs)) (n,[]) emitPathHelpers :: [Route] -> Int -> IO () emitPathHelpers rs nArgs = mapM_ emitPathHelper rs where emitPathHelper r = do let args = argList "arg" (length $ filter isDynamic (pieces r)) T.putStr (target r) putStr "Path " putStr (unwords args) putStr " = URI \"\" Nothing ('/' : intercalate \"/\" [" putStr $ intercalate ", " $ snd $ foldr (\p (n,xs) -> case p of Dynamic -> (n-1, ("escapeURIString (\\c -> not (isReserved c || not (isUnescapedInURI c)) ) $ unpack $ toPathPiece arg" ++ show n):xs) Static s -> (n, show s : xs) ) (length args, []) (pieces r) putStrLn "]) \"\" \"\"" -- The where clause forces the typechecker to infer that our arguments -- are of the same type as the arguments of the action we map to. putStrLn "\twhere" putStr "\ttypeRestrict _ " putStr (unwords $ argList "undef" nArgs) putStr " = " T.putStr (target r) putStr " " putStrLn $ unwords $ argList "undef" nArgs ++ args putStrLn "" emitRoutes :: [Route] -> Int -> IO () emitRoutes rs nArgs = do -- We want to be polymorphic in the parameter to route, so just let -- the inference engine do it all -- putStrLn "routes :: [Route a]" putStr "routes " putStr $ unwords args putStrLn " = [" putStrLn $ intercalate ",\n" $ map showRoute rs putStrLn "\t]" where args = argList "arg" nArgs showRoute r = "\t\tRoute {\n" ++ "\t\t\trhPieces = " ++ show (Static (method r) : pieces r) ++ ",\n" ++ "\t\t\trhHasMulti = " ++ show (multi r) ++ ",\n" ++ "\t\t\trhDispatch = (\\(" ++ piecesPattern (pieces r) ++ ") -> return (" ++ T.unpack (target r) ++ " " ++ unwords args ++ ")" ++ piecesAp (pieces r) ++ ")\n" ++ "\t\t}" piecesAp pieces = concat $ fst $ foldr (\p (ps,c) -> case p of Dynamic -> ((" `ap` (fromPathPiece val" ++ show c ++ ")"):ps, c+1) Static _ -> (ps, c) ) ([],0::Int) pieces piecesPattern pieces = intercalate ":" $ ("_":) $ fst $ foldr (\p (ps,c) -> case p of Dynamic -> (("val" ++ show c):ps, c+1) Static _ -> ("_":ps, c) ) (["_"],0::Int) pieces parser :: Parser [Route] parser = many1 $ do skipSpace m <- method skipSpace p <- pieces multi <- fmap isJust $ option Nothing (fmap Just (char '*')) skipSpace _ <- char '=' _ <- char '>' skipSpace t <- target skipWhile (\x -> isSpace x && not (isEndOfLine x)) endOfLine return $ Route m p multi t where target = takeWhile1 (not . isSpace) method = takeWhile1 isUpper pieces = fmap catMaybes $ many1 $ do _ <- char '/' option Nothing (fmap Just piece) piece = dynamic <|> static static = fmap Static (takeWhile1 (\x -> x /= '/' && x /= '*' && not (isSpace x))) dynamic = char ':' >> return Dynamic main :: IO () main = do (flags, args, errors) <- fmap (getOpt RequireOrder flags) getArgs case (args, errors) of _ | Help `elem` flags -> usage errors (_, _:_) -> usage errors >> exitFailure _ | length args /= 1 -> usage errors >> exitFailure _ | (Routes `notElem` flags) && (PathHelpers `notElem` flags) -> do hPutStrLn stderr "Must pass -r or -p" usage errors >> exitFailure _ -> main' (head args) flags where main' input flags = do Right routes <- fmap (parseOnly parser) $ T.readFile input when (Routes `elem` flags) $ do putStrLn "module Routes where" putStrLn "" mapM_ (\flag -> case flag of Mod m -> putStrLn $ "import " ++ m _ -> return () ) flags when (Routes `elem` flags) $ do putStrLn "import Control.Monad (ap)" putStrLn "import Data.Text (pack)" putStrLn "import Web.PathPieces (fromPathPiece)" putStrLn "import Yesod.Routes.Dispatch (Route(..), Piece(Static, Dynamic))" when (PathHelpers `elem` flags) $ do putStrLn "import Data.List (intercalate)" putStrLn "import Network.URI (URI(..), escapeURIString, isReserved, isUnescapedInURI)" putStrLn "import Data.Text (unpack)" putStrLn "import Web.PathPieces (toPathPiece)" putStrLn "" let nArgs = getNArgs flags when (PathHelpers `elem` flags) (emitPathHelpers routes nArgs) when (Routes `elem` flags) (emitRoutes routes nArgs) getNArgs = foldr (\flag n -> case (n,flag) of (0, NArgs n) -> n _ -> n ) 0