packages feed

yesod-core-1.7.0.0: src/Yesod/Routes/TH/RouteAttrs.hs

{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}

module Yesod.Routes.TH.RouteAttrs
    ( mkRouteAttrsInstance
    , mkRouteAttrsInstanceFor
    , mkRouteAttrsNestedInstances
    ) where

import Yesod.Routes.TH.Types
import Yesod.Routes.TH.Internal (nestedInstanceExists, resolveRouteCon, instanceD, conPCompat, findNestedRoute)
import Yesod.Routes.Class
import Language.Haskell.TH.Syntax
import Data.Foldable (toList)
import qualified Data.Set  as Set
import qualified Data.Text as Text
import Control.Monad

mkRouteAttrsInstance :: Cxt -> Type -> [ResourceTree a] -> Q Dec
mkRouteAttrsInstance cxt typ ress = do
    clauses <- mapM (goTree Nothing id) ress
    return $ instanceD cxt (ConT ''RouteAttrs `AppT` typ)
        [ FunD 'routeAttrs $ concat clauses ++
                [Clause [ WildP ] (NormalB $ VarE 'mempty) []]
        ]

-- | Like 'mkRouteAttrsInstance', but uses a 'String' name of a nested
-- subroute to generate nested instances instead of generating the full
-- instance for all routes.
--
-- @since 1.7.0.0
mkRouteAttrsInstanceFor :: Cxt -> Type -> String -> [ResourceTree a] -> Q [Dec]
mkRouteAttrsInstanceFor cxt typ target ress = do
    -- Fail loudly (matching Dispatch/RenderRoute/ParseRoute) when the focus
    -- target is missing, rather than silently emitting an always-'mempty'
    -- 'RouteAttrsNested' instance.
    case findNestedRoute target ress of
        Nothing ->
            fail $ "Target '" ++ target ++ "' was not found in resources."
        Just _ ->
            pure ()
    clauses <- mapM (goTree (Just target) id) ress
    return [instanceD cxt (ConT ''RouteAttrsNested `AppT` typ)
        [ FunD 'routeAttrsNested $ concat clauses ++
                [Clause [ WildP ] (NormalB $ VarE 'mempty) []]
        ]
        ]

-- | Generate a 'RouteAttrsNested' instance for every nested parent route in
-- the tree (recursively), skipping any whose instance already exists (e.g. one
-- generated in another module when routes are split). This is the
-- 'RouteAttrs' counterpart to the @RenderRouteNested@ \/ @ParseRouteNested@ \/
-- @YesodDispatchNested@ instances that a plain 'mkYesod' already emits for its
-- child route fragments, so that @routeAttrsNested ChildR@ resolves for a
-- single-module site just like the other nested-delegation methods do.
--
-- @since 1.7.0.0
mkRouteAttrsNestedInstances :: Cxt -> TyArgs -> [ResourceTree a] -> Q [Dec]
mkRouteAttrsNestedInstances cxt tyargs ress =
    concat <$> mapM perParent (parentNames ress)
  where
    parentNames trees = concat
        [ name : parentNames children
        | ResourceParent name _ _ _ children <- trees
        ]
    perParent name = do
        exists <- nestedInstanceExists ''RouteAttrsNested =<< resolveRouteCon name
        if exists
            then pure []
            else mkRouteAttrsInstanceFor cxt (applyTyArgs (ConT (mkName name)) tyargs) name ress

goTree :: Maybe String -> (Pat -> Pat) -> ResourceTree a -> Q [Clause]
goTree mtarget front (ResourceLeaf res) =
    case mtarget of
        Nothing ->
            return $ toList $ goRes front res
        Just _ ->
            return []
goTree (Just target) front (ResourceParent name _check _attrs _pieces trees)
    | target /= name = concat <$> mapM (goTree (Just target) front) trees
goTree mtarget front (ResourceParent name _check _attrs pieces trees) = do
    doesNestedInstanceExist <- nestedInstanceExists ''RouteAttrsNested =<< resolveRouteCon name

    if doesNestedInstanceExist
        then do
            x <- newName "x"
            pure [Clause [front (conPCompat (mkName name) (ignored (VarP x)))] (NormalB $ VarE 'routeAttrsNested `AppE` VarE x) []]
        else do
            -- We only reach this clause when @mtarget@ is 'Nothing' (full
            -- instance) or @Just name@ (we found the focus target); clause 2
            -- above peeled off the non-matching @Just other@ case. Either way
            -- the remaining subtree is generated unfocused (@Nothing@). The
            -- focus target itself is the route boundary, so its own
            -- constructor is not prepended (@front@); ordinary parents in the
            -- full instance do prepend theirs (@front'@).
            let front'' =
                    if Just name == mtarget
                        then front
                        else front'
            concat <$> mapM (goTree Nothing front'') trees
  where
    ignored = (replicate toIgnore WildP ++) . return
    toIgnore = length $ filter isDynamic pieces
    isDynamic Dynamic{} = True
    isDynamic Static{} = False
    front' = front . conPCompat (mkName name)
                   . ignored

goRes :: (Pat -> Pat) -> Resource a -> Maybe Clause
goRes front Resource {..} = do
    guard (not (null resourceAttrs))
    return $ Clause
        [front $ RecP (mkName resourceName) []]
        (NormalB $ VarE 'Set.fromList `AppE` ListE (map toText resourceAttrs))
        []
  where
    toText s = VarE 'Text.pack `AppE` LitE (StringL s)