packages feed

hackage-server-0.4: Distribution/Server/Framework/Resource.hs

{-# LANGUAGE GeneralizedNewtypeDeriving, StandaloneDeriving,
             FlexibleContexts, FlexibleInstances, NamedFieldPuns #-}

module Distribution.Server.Framework.Resource (
    -- | Paths
    DynamicPath,
    BranchComponent(..),
    BranchPath,
    trunkAt,

    -- | Resources
    Resource(..),
    ResourceFormat(..),
    BranchFormat(..),
    BranchEnd(..),
    Content,
    resourceAt,
    extendResource,
    extendResourcePath,
    serveResource,
    resourceMethods,

    -- | URI generation
    renderURI,
    renderResource,
    renderResource',

    -- | ServerTree
    ServerTree(..),
    serverTreeEmpty,
    addServerNode,
    renderServerTree,
    drawServerTree,

    -- | Error page serving
    serveErrorResponse,
    ServerErrorResponse,
  ) where

import Happstack.Server
import Distribution.Server.Util.Happstack (remainingPathString, uriEscape)
import Distribution.Server.Util.ContentType (parseContentAccept)
import Distribution.Server.Framework.Error

import Data.List (isSuffixOf)
import Data.Monoid
import Data.Map (Map)
import qualified Data.Map as Map
import Control.Monad
import Data.Maybe
import Data.Function (on)
import Data.List (intercalate, unionBy, findIndices, find)
import qualified Text.ParserCombinators.Parsec as Parse

import System.FilePath.Posix ((</>), (<.>))
import qualified Data.Tree as Tree (Tree(..), drawTree)
import qualified Data.ByteString.Char8 as BS -- Used for accept header only

type Content = String

type DynamicPath = [(String, String)]

type ServerResponse = DynamicPath -> ServerPartE Response
type ServerErrorResponse = ErrorResponse -> ServerPartE Response

-- | A resource is an object that handles requests at a given URI. Best practice
-- is to construct it by calling resourceAt and then setting the method fields
-- using record update syntax. You can also extend an existing resource with
-- extendResource, which can be mappended to the original to combine their
-- functionality, or with extendResourcePath.
data Resource = Resource {
    -- | The location in a form which can be added to a ServerTree.
    resourceLocation :: BranchPath,
    -- | Handlers for GET requests for different content-types
    resourceGet      :: [(Content, ServerResponse)],
    -- | Handlers for PUT requests
    resourcePut      :: [(Content, ServerResponse)],
    -- | Handlers for POST requests
    resourcePost     :: [(Content, ServerResponse)],
    -- | Handlers for DELETE requests
    resourceDelete   :: [(Content, ServerResponse)],
    -- | The format conventions held by the resource.
    resourceFormat   :: ResourceFormat,
    -- | The trailing slash conventions held by the resource.
    resourcePathEnd  :: BranchEnd,
    -- | Human readable description of the resource
    resourceDesc     :: [(Method, String)]
  }

-- favors first
instance Monoid Resource where
    mempty = Resource [] [] [] [] [] noFormat NoSlash []
    mappend (Resource bpath rget rput rpost rdelete rformat rend desc)
            (Resource bpath' rget' rput' rpost' rdelete' rformat' rend' desc') =
        Resource (simpleCombine bpath bpath') (ccombine rget rget') (ccombine rput rput')
                   (ccombine rpost rpost') (ccombine rdelete rdelete')
                   (simpleCombine rformat rformat') (simpleCombine rend rend') (desc ++ desc')
      where ccombine = unionBy ((==) `on` fst)
            simpleCombine xs ys = if null bpath then ys else xs

-- | A path element of a URI.
--
-- * StaticBranch dirName - \/dirName
-- * DynamicBranch dynamicName - \/anyName (mapping created dynamicName -> anyName)
-- * TrailingBranch - doesn't consume any path; it's here to prevent e.g. conflict between \/.:format and \/...
--
-- trunkAt yields a simple list of BranchComponents.
-- resourceAt yields the same, and some complex metadata for processing formats and the like.
data BranchComponent = StaticBranch String | DynamicBranch String | TrailingBranch deriving (Show, Eq, Ord)
type BranchPath = [BranchComponent]

-- | This type dictates the preprocessing we must do on extensions (foo.json) when serving Resources
-- For BranchFormat, we need to do:
-- 1. for NoFormat - don't do any preprocessing. The second field is ignored here.
-- 1. for StaticFormat  - look for a specific format, and guard against that
-- 2. for DynamicFormat - strip off any extension (starting from the right end, so no periods allowed)
-- Under either of the above cases, the component might need to be preprocessed as well.
-- 1. for Nothing - this means a standalone format, like \/.json (as in \/distro\/arch\/.json)
--                  either accept \/distro\/arch\/ or read \/distro\/arch\/.format and pass it along
-- 2. for Just (StaticBranch sdir) - strip off the pre-format part and make sure it equals sdir
-- 3. for Just (DynamicBranch sdir) - strip off the pre-format part and pass it with the DynamicPath as sdir
-- DynamicFormat also has the property that it is optional, and defaulting is allowed.
data ResourceFormat = ResourceFormat BranchFormat (Maybe BranchComponent) deriving (Show, Eq, Ord)

noFormat :: ResourceFormat
noFormat = ResourceFormat NoFormat Nothing

data BranchFormat = NoFormat | DynamicFormat | StaticFormat String deriving (Show, Eq, Ord)
data BranchEnd  = Slash | NoSlash | Trailing deriving (Show, Eq, Ord)

-- | Creates an empty resource from a string specifying its location and format conventions.
--
-- (Explain path literal syntax.)
resourceAt :: String -> Resource
resourceAt arg = mempty
  { resourceLocation = reverse loc
  , resourceFormat  = format
  , resourcePathEnd = slash
  }
  where
    branch = either trunkError id $ Parse.parse parseFormatTrunkAt "Distribution.Server.Resource.parseFormatTrunkAt" arg
    trunkError pe = error $ "Distribution.Server.Resource.resourceAt: Could not parse trunk literal " ++ show arg ++ ". Parsec error: " ++ show pe
    (loc, slash, format) = trunkToResource branch

-- | Creates a new resource at the same location, but without any of the request
-- handlers of the original. When mappend'd to the original, its methods and content-types
-- will be combined. This can be useful for extending an existing resource with new representations and new
-- functionality.
extendResource :: Resource -> Resource
extendResource resource = resource {
    resourceDesc   = []
  , resourceGet    = []
  , resourcePut    = []
  , resourcePost   = []
  , resourceDelete = []
  }

-- | Creates a new resource that is at a subdirectory of an existing resource. This function takes care of formats
-- as best as it can.
--
-- extendResourcePath "\/bar\/.:format" (resourceAt "\/data\/:foo.:format") == resourceAt "\/data\/:foo\/bar\/:.format"
--
-- Extending static formats with this method is not recommended. (extending "\/:tarball.tar.gz"
-- with "\/data" will give "\/:tarball\/data", with the format stripped, and extending
-- "\/help\/.json" with "\/tree" will give "\/help\/.json\/tree")
extendResourcePath :: String -> Resource -> Resource
extendResourcePath arg resource =
  let endLoc = case resourceFormat resource of
        ResourceFormat (StaticFormat _) Nothing -> case loc of
            (DynamicBranch "format":rest) -> rest
            _ -> funcError "Static ending format must have dynamic 'format' branch"
        ResourceFormat (StaticFormat _) (Just (StaticBranch sdir)) -> case loc of
            (DynamicBranch sdir':rest) | sdir == sdir' -> rest
            _ -> funcError "Static branch and format must match stated location"
        ResourceFormat (StaticFormat _) (Just (DynamicBranch sdir)) -> case loc of
            (DynamicBranch sdir':_) | sdir == sdir' -> loc
            _ -> funcError "Dynamic branch with static format must match stated location"
        ResourceFormat DynamicFormat Nothing -> loc
        ResourceFormat DynamicFormat (Just (StaticBranch sdir)) -> case loc of
            (DynamicBranch sdir':rest) | sdir == sdir' -> StaticBranch sdir:rest
            _ -> funcError "Dynamic format with static branch must match stated location"
        ResourceFormat DynamicFormat (Just (DynamicBranch sdir)) -> case loc of
            (DynamicBranch sdir':_) | sdir == sdir' -> loc
            _ -> funcError "Dynamic branch and format must match stated location"
        -- For a URI like /resource/.format: since it is encoded as NoFormat in trunkToResource,
        -- this branch will incorrectly be taken. this isn't too big a handicap though
        ResourceFormat NoFormat Nothing -> case loc of
            (TrailingBranch:rest) -> rest
            _ -> loc
        _ -> funcError $ "invalid resource format in argument 2"
  in
    extendResource resource { resourceLocation = reverse loc' ++ endLoc, resourceFormat = format', resourcePathEnd = slash' }
  where
    branch = either trunkError id $ Parse.parse parseFormatTrunkAt "Distribution.Server.Resource.parseFormatTrunkAt" arg
    trunkError pe = funcError $ "Could not parse trunk literal " ++ show arg ++ ". Parsec error: " ++ show pe
    funcError reason = error $ "Distribution.Server.Resource.extendResourcePath :" ++ reason
    loc = resourceLocation resource
    (loc', slash', format') = trunkToResource branch

-- Allows the formation of a URI from a URI specification (BranchPath).
-- URIs may obey additional constraints and have special rules (e.g., formats).
-- To accomodate these, insteaduse renderResource to get a URI.
--
-- ".." is a special argument that fills in a TrailingBranch. Make sure it's
-- properly escaped (see Happstack.Server.SURI)
--
-- renderURI (trunkAt "/home/:user/..")
--    [("user", "mgruen"), ("..", "docs/todo.txt")]
--    == "/home/mgruen/docs/todo.txt"
renderURI :: BranchPath -> DynamicPath -> String
renderURI bpath dpath = renderGenURI bpath (flip lookup dpath)

-- Render a URI generally using a function of one's choosing (usually flip lookup dpath)
-- Stops when a requested field is not found, yielding an incomplete URI. I think
-- this is better than having a function that doesn't return *some* result.
renderGenURI :: BranchPath -> (String -> Maybe String) -> String
renderGenURI bpath pathFunc = "/" </> go (reverse bpath)
  where go (StaticBranch  sdir:rest) = uriEscape sdir </> go rest
        go (DynamicBranch sdir:rest)
          | (ddir, sformat@('.':_)) <- break (=='.') sdir
          = case pathFunc ddir of
            Nothing  -> ""
            Just str -> uriEscape str <.> uriEscape sformat </> go rest
        go (DynamicBranch sdir:rest) = case pathFunc sdir of
            Nothing  -> ""
            Just str -> uriEscape str </> go rest
        go (TrailingBranch:_) = fromMaybe "" $ pathFunc ".."
        go [] = ""

-- Doesn't use a DynamicPath - rather, munches path components from a list
-- it stops if there aren't enough, and returns the extras if there's more than enough.
--
-- Trailing branches currently are assumed to be complete escaped URI paths.
renderListURI :: BranchPath -> [String] -> (String, [String])
renderListURI bpath list = let (res, extra) = go (reverse bpath) list in ("/" </> res, extra)
    where go (StaticBranch  sdir:rest) xs  = let (res, extra) = go rest xs in (uriEscape sdir </> res, extra)
          go (DynamicBranch _:rest) (x:xs) = let (res, extra) = go rest xs in (uriEscape x </> res, extra)
          go (TrailingBranch:_) xs = case xs of [] -> ("", []); (x:rest) -> (x, rest)
          go _ rest = ("", rest)

-- Given a Resource, construct a URI generator. If the Resource uses a dynamic format, it can
-- be passed in the DynamicPath as "format". Trailing slash conventions are obeyed.
--
-- See documentation for the Resource to see the conventions in interpreting the DynamicPath.
-- As in renderURI, ".." is interpreted as the trailing branch. It should be the case that
--
-- > fix $ \r -> (resourceAt uri) { resourceGet = [("txt", ok . toResponse . renderResource' r)] }
--
-- will print the URI that was used to request the page.
--
-- renderURI (resourceAt "/home/:user/docs/:doc.:format")
--     [("user", "mgruen"), ("doc", "todo"), ("format", "txt")]
--     == "/home/mgruen/docs/todo.txt"
renderResource' :: Resource -> DynamicPath -> String
renderResource' resource dpath = renderResourceFormat resource (lookup "format" dpath)
               $ renderGenURI (normalizeResourceLocation resource) (flip lookup dpath)

-- A more convenient form of renderResource'. Used when you know the path structure.
-- The first argument unused in the path, if any, will be used as a dynamic format,
-- if any.
--
-- renderResource (resourceAt "/home/:user/docs/:doc.:format")
--     ["mgruen", "todo", "txt"]
--     == "/home/mgruen/docs/todo.txt"
renderResource :: Resource -> [String] -> String
renderResource resource list = case renderListURI (normalizeResourceLocation resource) list of
    (str, format:_) -> renderResourceFormat resource (Just format) str
    (str, []) -> renderResourceFormat resource Nothing str

-- in some cases, DynamicBranches are used to accomodate formats for StaticBranches.
-- this returns them to their pre-format state so renderGenURI can handle them
normalizeResourceLocation :: Resource -> BranchPath
normalizeResourceLocation resource = case (resourceFormat resource, resourceLocation resource) of
    (ResourceFormat _ (Just (StaticBranch sdir)), DynamicBranch sdir':xs) | sdir == sdir' -> StaticBranch sdir:xs
    (_, loc) -> loc

renderResourceFormat :: Resource -> Maybe String -> String -> String
renderResourceFormat resource dformat str = case (resourcePathEnd resource, resourceFormat resource) of
    (NoSlash, ResourceFormat NoFormat _) -> str
    (Slash, ResourceFormat NoFormat _) -> case str of "/" -> "/"; _ -> str ++ "/"
    (NoSlash, ResourceFormat (StaticFormat format) branch) -> case branch of
        Just {} -> str ++ "." ++ format
        Nothing -> str ++ "/." ++ format
    (Slash, ResourceFormat DynamicFormat Nothing) -> case dformat of
        Just format@(_:_) -> str ++ "/." ++ format
        _ -> str ++ "/"
    (NoSlash, ResourceFormat DynamicFormat _) -> case dformat of
        Just format@(_:_) -> str ++ "." ++ format
        _ -> str
    -- This case might be taken by TrailingBranch
    _ -> str

-----------------------
-- Converts the output of parseTrunkFormatAt to something consumable by a Resource
-- It forbids many things that parse correctly, most notably using formats in the middle of a path.
-- It's possible in theory to allow such things, but complicated.
-- Directories are really format-less things.
--
-- trunkToResource is a top-level call to weed out cases that don't make sense recursively in trunkToResource'
trunkToResource  :: [(BranchComponent, BranchFormat)] -> ([BranchComponent], BranchEnd, ResourceFormat)
trunkToResource [] = ([], Slash, noFormat) -- ""
trunkToResource [(StaticBranch "", format)] = ([], Slash, ResourceFormat format Nothing) -- "/" or "/.format"
trunkToResource anythingElse = trunkToResource' anythingElse

trunkToResource' :: [(BranchComponent, BranchFormat)] -> ([BranchComponent], BranchEnd, ResourceFormat)
trunkToResource' [] = ([], NoSlash, noFormat)
-- /...
trunkToResource' ((TrailingBranch, _):xs) | null xs = ([TrailingBranch], Trailing, noFormat)
                                          | otherwise = error "Trailing path only allowed at very end"
-- /foo/, /foo/.format, or /foo/.:format
trunkToResource' [(branch, NoFormat), (StaticBranch "", format)] = pathFormatSep format
  where pathFormatSep (StaticFormat form) = ([branch, StaticBranch ("." ++ form)], NoSlash, noFormat) -- /foo/.json, format is not optional here!
        pathFormatSep DynamicFormat = ([branch], Slash, ResourceFormat DynamicFormat Nothing) -- /foo/.format
        pathFormatSep NoFormat = ([branch], Slash, noFormat) -- /foo/
-- /foo.format/[...] (rewrite into next case)
trunkToResource' ((StaticBranch sdir, StaticFormat format):xs) = trunkToResource' ((StaticBranch (sdir ++ "." ++ format), NoFormat):xs)
trunkToResource' ((DynamicBranch ddir, StaticFormat format):xs) = trunkToResource' ((DynamicBranch (ddir ++ "." ++ format), NoFormat):xs)
-- /foo/[...]
trunkToResource' ((branch, NoFormat):xs) = case trunkToResource' xs of (xs', slash, res) -> (branch:xs', slash, res)
-- /foo.format
trunkToResource' [(branch, format)] = pathFormat branch format
  where pathFormat (StaticBranch sdir) (StaticFormat form) = ([StaticBranch (sdir ++ "." ++ form)], NoSlash, noFormat) -- foo.json
        pathFormat (StaticBranch sdir) DynamicFormat = ([DynamicBranch sdir], NoSlash, ResourceFormat DynamicFormat (Just branch)) -- foo.:json
        pathFormat (DynamicBranch {}) (StaticFormat {}) = ([branch], NoSlash, ResourceFormat format (Just branch)) -- :foo.json
        pathFormat (DynamicBranch {}) DynamicFormat = ([branch], NoSlash, ResourceFormat DynamicFormat (Just branch)) -- :foo.:json
        pathFormat _ NoFormat = ([branch], NoSlash, noFormat) -- foo or :foo
        pathFormat _ _ = error "Trailing path can't have a format"
-- /foo.format/[...]
trunkToResource' _ = error "Format only allowed at end of path"

trunkAt :: String -> BranchPath
trunkAt arg = either trunkError reverse $ Parse.parse parseTrunkAt "Distribution.Server.Resource.parseTrunkAt" arg
    where trunkError pe = error $ "Distribution.Server.Resource.trunkAt: Could not parse trunk literal " ++ show arg ++ ". Parsec error: " ++ show pe

parseTrunkAt :: Parse.Parser [BranchComponent]
parseTrunkAt = do
    components <- Parse.many (Parse.try parseComponent)
    Parse.optional (Parse.char '/')
    Parse.eof
    return components
  where
    parseComponent = do
        void $ Parse.char '/'
        fmap DynamicBranch (Parse.char ':' >> Parse.many1 (Parse.noneOf "/"))
          Parse.<|> fmap StaticBranch (Parse.many1 (Parse.noneOf "/"))

parseFormatTrunkAt :: Parse.Parser [(BranchComponent, BranchFormat)]
parseFormatTrunkAt = do
    components <- Parse.many (Parse.try parseComponent)
    rest <- Parse.option [] (Parse.char '/' >> return [(StaticBranch "", NoFormat)])
    Parse.eof
    return (components ++ rest)
  where
    parseComponent :: Parse.Parser (BranchComponent, BranchFormat)
    parseComponent = do
        void $ Parse.char '/'
        Parse.choice $ map Parse.try
          [ Parse.char '.' >> Parse.many1 (Parse.char '.') >>
            ((Parse.lookAhead (Parse.char '/') >> return ()) Parse.<|> Parse.eof) >> return (TrailingBranch, NoFormat)
          , Parse.char ':' >> parseMaybeFormat DynamicBranch
          , do Parse.lookAhead (void $ Parse.satisfy (/=':'))
               Parse.choice $ map Parse.try
                 [ parseMaybeFormat StaticBranch
                 , fmap ((,) (StaticBranch "")) parseFormat
                 , fmap (flip (,) NoFormat . StaticBranch) untilNext
                 ]
          ]
    parseMaybeFormat :: (String -> BranchComponent) -> Parse.Parser (BranchComponent, BranchFormat)
    parseMaybeFormat control = do
        sdir <- Parse.many1 (Parse.noneOf "/.")
        format <- Parse.option NoFormat parseFormat
        return (control sdir, format)
    parseFormat :: Parse.Parser BranchFormat
    parseFormat = Parse.char '.' >> Parse.choice
        [ Parse.char ':' >> untilNext >> return DynamicFormat
        , fmap StaticFormat untilNext
        ]
    untilNext :: Parse.Parser String
    untilNext = Parse.many1 (Parse.noneOf "/")

-- serveResource does all the path format and HTTP method preprocessing for a Resource
--
-- For a small curl-based testing mini-suite of [Resource]:
-- [res "/foo" ["json"], res "/foo/:bar.:format" ["html", "json"], res "/baz/test/.:format" ["html", "text", "json"], res "/package/:package/:tarball.tar.gz" ["tarball"], res "/a/:a/:b/" ["html", "json"], res "/mon/..." [""], res "/wiki/path.:format" [], res "/hi.:format" ["yaml", "blah"]]
--     where res field formats = (resourceAt field) { resourceGet = map (\format -> (format, \_ -> return . toResponse . (++"\n") . ((show format++" - ")++) . show)) formats }
serveResource :: [(Content, ServerErrorResponse)] -> Resource -> ServerResponse
serveResource errRes (Resource _ rget rput rpost rdelete rformat rend _) = \dpath -> msum $
    map (\func -> func dpath) $ methodPart ++ [optionPart]
  where
    optionPart = makeOptions $ concat [ met | ((_:_), met) <- zip methods methodsList]
    methodPart = [ serveResources met res | (res@(_:_), met) <- zip methods methodsList]
    methods = [rget, rput, rpost, rdelete]
    methodsList = [[GET, HEAD], [PUT], [POST], [DELETE]]
    makeOptions :: [Method] -> ServerResponse
    makeOptions methodList = \_ -> method OPTIONS >> nullDir >> do
        setHeaderM "Allow" (intercalate ", " . map show $ methodList)
        return $ toResponse ()
    -- some of the dpath lookup calls can be replaced by pattern matching the head/replacing
    -- at the moment, duplicate entries tend to be inserted in dpath, because old ones are not replaced
    -- Procedure:
    -- > Guard against method
    -- > Extract format/whatnot
    -- > Potentially redirect to canonical slash form
    -- > Go from format/content-type to ServerResponse to serve-}
    serveResources :: [Method] -> [(Content, ServerResponse)] -> ServerResponse
    serveResources met res dpath = case rend of
        Trailing -> method met >> (remainingPathString >>= \str ->
                                   serveContent res (("..", str):dpath))
        _ -> serveFormat res met dpath
    serveFormat :: [(Content, ServerResponse)] -> [Method] -> ServerResponse
    serveFormat res met = case rformat of
        ResourceFormat NoFormat Nothing -> \dpath -> method met >> nullDir >> serveContent res dpath
        ResourceFormat (StaticFormat format) Nothing -> \dpath -> path $ \format' -> method met >> nullDir >> do
            -- this branch shouldn't happen - /foo/.json would instead be stored as two static dirs
            guard (format' == ('.':format))
            serveContent res dpath
        ResourceFormat (StaticFormat format) (Just (StaticBranch sdir)) -> \dpath -> method met >> nullDir >>
            -- likewise, foo.json should be stored as a single static dir
            if lookup sdir dpath == Just (sdir ++ "." ++ format) then mzero
                                                                 else serveContent res dpath
        ResourceFormat (StaticFormat format) (Just (DynamicBranch sdir)) -> \dpath -> method met >> nullDir >>
            case matchExt format =<< lookup sdir dpath of
                Just pname -> serveContent res ((sdir, pname):dpath)
                Nothing -> mzero
        ResourceFormat DynamicFormat Nothing -> \dpath ->
            msum [ method met >> nullDir >> serveContent res dpath
                 , path $ \pname -> case pname of
                       ('.':format) -> method met >> nullDir >> serveContent res (("format", format):dpath)
                       _ -> mzero
                 ]
        ResourceFormat DynamicFormat (Just (StaticBranch sdir)) -> \dpath -> method met >> nullDir >>
            case fmap extractExt (lookup sdir dpath) of
                Just (pname, format) | pname == sdir -> serveContent res (("format", format):dpath)
                _ -> mzero
        ResourceFormat DynamicFormat (Just (DynamicBranch sdir)) -> \dpath -> method met >> nullDir >>
            -- this is somewhat complicated. consider /pkg-0.1 and /pkg-0.1.html. If the format is optional, where to split?
            -- the solution is to manually check the available formats to see if something matches
            -- if this situation comes up in practice, try to require a trailing slash, e.g. /pkg-0.1/.html
            case fmap (\sd -> (,) sd $ extractExt sd) (lookup sdir dpath) of
                Just (full, (pname, format)) -> do
                    let splitOption = serveContent res (("format", format):(sdir, pname):dpath)
                        fullOption  = serveContent res ((sdir, full):dpath)
                    case guard (not $ null format) >> lookup format res of
                        Nothing -> fullOption
                        Just {} -> splitOption
                _ -> mzero
        -- some invalid combination
        _ -> \dpath -> method met >> nullDir >> serveContent res dpath
    serveContent :: [(Content, ServerResponse)] -> ServerResponse
    serveContent res dpath = do
        -- there should be no remaining path segments at this point, now check page redirection
        met <- fmap rqMethod askRq
        -- we don't check if the page exists before redirecting!
        -- just send them to the canonical place the document may or may not be
        (if met == HEAD || met == GET
            then redirCanonicalSlash dpath
            else id) $ do
          -- "Find " ++ show (lookup "format" dpath) ++ " in " ++ show (map fst res)
          case lookup "format" dpath of
            Just format@(_:_) -> case lookup format res of
                               -- return a specific format if it is found
                Just answer -> handleErrors (Just format) $ answer dpath
                Nothing -> mzero -- return 404 if the specific format is not found
                  -- return default response when format is empty or non-existent
            _ -> do (format,answer) <- negotiateContent (head res) res
                    handleErrors (Just format) $ answer dpath

    handleErrors format =
      handleErrorResponse (serveErrorResponse errRes format)

    redirCanonicalSlash :: DynamicPath -> ServerPartE Response -> ServerPartE Response
    redirCanonicalSlash dpath trueRes = case rformat of
        ResourceFormat format Nothing | format /= NoFormat -> case lookup "format" dpath of
            Just {} -> requireNoSlash `mplus` trueRes
            Nothing -> requireSlash `mplus` trueRes
        _ -> case rend of
            Slash    -> requireSlash `mplus` trueRes
            NoSlash  -> requireNoSlash `mplus` trueRes
            Trailing -> mplus (nullDir >> requireSlash) trueRes

    requireSlash = do
        theUri <- fmap rqUri askRq
        guard $ last theUri /= '/'
        movedPermanently (theUri ++ "/") (toResponse ())
    requireNoSlash = do
        theUri <- fmap rqUri askRq
        guard $ last theUri == '/'
        movedPermanently (reverse . dropWhile (=='/') . reverse $ theUri) (toResponse ())
    -- matchExt and extractExt could also use manual string-chomping recursion if wanted
    matchExt format pname = let fsize = length format
                                (pname', format') = splitAt (length pname - fsize - 1) pname
                            in if '.':format == format' then Just pname'
                                                        else Nothing
    extractExt pname = case findIndices (=='.') pname of
        [] -> (pname, "")
        xs -> case splitAt (last xs) pname of (pname', _:format) -> (pname', format)
                                              _ -> (pname, "") -- this shouldn't happen

resourceMethods :: Resource -> [Method]
resourceMethods (Resource _ rget rput rpost rdelete _ _ _) = [ met | ((_:_), met) <- zip methods methodsList]
  where methods = [rget, rput, rpost, rdelete]
        methodsList = [GET, PUT, POST, DELETE]

serveErrorResponse :: [(Content, ServerErrorResponse)] -> Maybe Content -> ServerErrorResponse
serveErrorResponse errRes mformat err = do
    -- So our strategy is to give priority to a content type requested by
    -- the client, then secondly any format implied by the requested url
    -- e.g. requesting a .html file, or html being default for the resource
    -- and finally just fall through to using text/plain
    (_, errHandler) <- negotiateContent ("", defHandler) errRes
    errHandler err
  where
    defHandler = fromMaybe throwError $ do
      format <- mformat
      lookup format errRes

negotiateContent :: ServerMonad m => (Content, a) -> [(Content, a)] -> m (Content, a)
negotiateContent def available = do
    maccept <- getHeaderM "Accept"
    case maccept of
      Nothing -> return def
      Just accept ->
        return $ fromMaybe def $ listToMaybe $ catMaybes
                   [ simpleContentTypeMapping ct
                       >>= \f -> find (\x -> fst x == f) available
                   | let acceptable = parseContentAccept (BS.unpack accept)
                   , ct <- acceptable ]
  where
    -- This is rather a non-extensible hack
    simpleContentTypeMapping ContentType {ctType, ctSubtype, ctParameters = []} =
      case (ctType, ctSubtype) of
        ("text",        "html")  -> Just "html"
        ("text",        "plain") -> Just "txt"
        ("application", "json")  -> Just "json"
        _                        -> Nothing
    simpleContentTypeMapping _    = Nothing

----------------------------------------------------------------------------

data ServerTree a = ServerTree {
    nodeResponse :: Maybe a,
    nodeForest :: Map BranchComponent (ServerTree a)
} deriving (Show)

instance Functor ServerTree where
    fmap func (ServerTree value forest) = ServerTree (fmap func value) (Map.map (fmap func) forest)

drawServerTree :: ServerTree a -> Maybe (a -> String) -> String
drawServerTree tree func = Tree.drawTree (transformTree tree Nothing)
  where transformTree (ServerTree res for) mlink = Tree.Node (drawLink mlink res) (map transformForest $ Map.toList for)
        drawLink mlink res = maybe "" ((++": ") . show) mlink ++ maybe "(nothing)" (fromMaybe (const "node") func) res
        transformForest (link, tree') = transformTree tree' (Just link)

serverTreeEmpty :: ServerTree a
serverTreeEmpty = ServerTree Nothing Map.empty

-- essentially a ReaderT DynamicPath ServerPart Response
-- this always renders parent URIs, but usually we guard against remaining path segments, so it's fine
renderServerTree :: DynamicPath -> ServerTree ServerResponse -> ServerPartE Response
renderServerTree dpath (ServerTree func forest) =
    msum $ maybeToList (fmap (\fun -> fun dpath) func)
        ++ map (uncurry renderBranch) (Map.toList forest)
  where
    renderBranch :: BranchComponent -> ServerTree ServerResponse -> ServerPartE Response
    renderBranch (StaticBranch  sdir) tree = dir sdir $ renderServerTree dpath tree
    renderBranch (DynamicBranch sdir) tree
      | (ddir, sformat@('.':_)) <- break (=='.') sdir
                   = path $ \pname -> do guard (sformat `isSuffixOf` pname)
                                         let pname' = take (length pname - length sformat) pname
                                         renderServerTree ((ddir, pname'):dpath) tree
      | otherwise  = path $ \pname -> renderServerTree ((sdir, pname):dpath) tree
    renderBranch TrailingBranch tree = renderServerTree dpath tree

reinsert :: Monoid a => BranchComponent -> ServerTree a -> Map BranchComponent (ServerTree a) -> Map BranchComponent (ServerTree a)
-- combine will only be called if branchMap already contains the key
reinsert key newTree branchMap = Map.insertWith combine key newTree branchMap

combine :: Monoid a => ServerTree a -> ServerTree a -> ServerTree a
combine (ServerTree newResponse newForest) (ServerTree oldResponse oldForest) =
    -- replace old resource with new resource, combine old and new responses
    ServerTree (mappend newResponse oldResponse) (Map.foldWithKey reinsert oldForest newForest)

addServerNode :: Monoid a => BranchPath -> a -> ServerTree a -> ServerTree a
addServerNode trunk response tree = treeFold trunk (ServerTree (Just response) Map.empty) tree

--this function takes a list whose head is the resource and traverses leftwards in the URI
--this is due to the original design of specifying URI branches: if the resources are
--themselves encoded in the branch, then subresources should share the same parent
--resource, sharing list tails.
--
--This version is greatly simplified compared to what was previously here.
treeFold :: Monoid a => BranchPath -> ServerTree a -> ServerTree a -> ServerTree a
treeFold [] newChild topLevel = combine newChild topLevel
treeFold (sdir:otherTree) newChild topLevel = treeFold otherTree (ServerTree Nothing $ Map.singleton sdir newChild) topLevel