packages feed

hackage-server-0.4: Distribution/Server/Util/NameIndex.hs

{-# LANGUAGE DeriveDataTypeable, TemplateHaskell,
             FlexibleInstances, FlexibleContexts, MultiParamTypeClasses #-}
-- TypeOperators, TypeSynonymInstances, TypeFamilies

module Distribution.Server.Util.NameIndex where

import Data.Map (Map)
import Data.Typeable (Typeable)
import qualified Data.Map as Map
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Char (toLower)
import Data.List (unfoldr, foldl')
import Data.Maybe (maybeToList)
import Control.DeepSeq
import Data.SafeCopy

import Distribution.Server.Framework.MemSize

-- | Case-insensitive name search. This is meant to be an enhanced set of
-- names, not a full text search. It's also meant to be a sort of a short-term
-- solution for name suggestion searches; e.g., package searches should also
-- consider the tagline of a package.
data NameIndex = NameIndex {
    -- | This is the mapping from case-insensitive search term -> name.
    nameIndex :: Map String (Set String),
    -- | This is the set of names.
    storedNamesIndex :: Set String,
    -- | This is the specification of the type of generator, mainly here because
    -- functions can't be serialized. Just str means to break on any char in
    -- str (breakGenerator); Nothing is defaultGenerator.
    nameGenType :: Maybe [Char],
    -- | This is the generator of search terms from names.
    nameSearchGenerator :: String -> [String]
} deriving (Typeable)

emptyNameIndex :: Maybe [Char] -> NameIndex
emptyNameIndex gen = NameIndex Map.empty Set.empty gen $ case gen of
    Nothing -> defaultGenerator
    Just st -> breakGenerator st

defaultGenerator :: String -> [String]
defaultGenerator name = [name]

breakGenerator :: [Char] -> String -> [String]
breakGenerator breakStr name = name:unfoldr unfoldName name
  where unfoldName str = case break (`elem` breakStr) str of
            ([], _) -> Nothing
            (_, []) -> Nothing
            (_, _:str') -> Just (str', str')

constructIndex :: [String] -> Maybe [Char] -> NameIndex
constructIndex strs gen = foldl' (flip addName) (emptyNameIndex gen) strs

addName :: String -> NameIndex -> NameIndex
addName caseName (NameIndex index stored gen' gen) =
    let name = map toLower caseName
        nameSet = Set.singleton caseName
        forName = Map.fromList $ map (\term -> (term, nameSet)) (gen name)
    in NameIndex (Map.unionWith Set.union index forName)
                 (Set.insert caseName stored) gen' gen

deleteName :: String -> NameIndex -> NameIndex
deleteName caseName (NameIndex index stored gen' gen) =
    let name = map toLower caseName
        nameSet = Set.singleton caseName
        forName = Map.fromList $ map (\term -> (term, nameSet)) (gen name)
    in NameIndex (Map.differenceWith (\a b -> keepSet $ Set.difference a b) index forName)
                 (Set.delete caseName stored) gen' gen
  where keepSet s = if Set.null s then Nothing else Just s

lookupName :: String -> NameIndex -> Set String
lookupName caseName (NameIndex index _ _ _) =
    Map.findWithDefault Set.empty (map toLower caseName) index

lookupPrefix :: String -> NameIndex -> Set String
lookupPrefix caseName (NameIndex index _ _ _) =
    let name = map toLower caseName
        (_, mentry, startTree) = Map.splitLookup name index
        -- the idea is, select all names in the range [name, mapLast succ name)
        -- an alternate idea would just be to takeWhile (`isPrefixOf` name)
        (totalTree, _, _) = Map.splitLookup (mapLast succ name) startTree
        nameSets = maybeToList mentry ++ Map.elems totalTree
    in Set.unions nameSets

takeSetPrefix :: String -> Set String -> Set String
takeSetPrefix name strs =
    let (_, present, startSet) = Set.splitMember name strs
        (totalSet, _, _) = Set.splitMember (mapLast succ name) startSet
    in (if present then Set.insert name else id) totalSet

-- | Map only the last element of a list
mapLast :: (a -> a) -> [a] -> [a]
mapLast f (x:[]) = f x:[]
mapLast f (x:xs) = x:mapLast f xs
mapLast _ [] = []

-- store arguments which can be sent to constructIndex :: [String] -> Maybe [Char] -> NameIndex
instance SafeCopy NameIndex where
    putCopy index = contain $ safePut (nameGenType index) >> safePut (storedNamesIndex index)
    getCopy = contain $ do
        gen <- safeGet
        index <- safeGet
        return $ constructIndex (Set.toList index) gen

instance NFData NameIndex where
    rnf (NameIndex a b _ _) = rnf a `seq` rnf b

instance MemSize NameIndex where
    memSize (NameIndex a b c d) = memSize4 a b c d