{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveTraversable, TypeFamilies, OverloadedStrings #-} -- | -- Module: Data.Greskell.PMap -- Description: Property map, a map with Text keys and cardinality options -- Maintainer: Toshio Ito -- -- This module defines 'PMap', a map with 'Text' keys and cardinality -- options. -- -- @since 1.0.0.0 module Data.Greskell.PMap ( -- * PMap PMap, -- ** Single lookup lookup, lookupM, lookupAs, lookupAs', lookupAsM, -- ** List lookup lookupList, lookupListAs, lookupListAs', -- ** Others pMapInsert, pMapDelete, pMapLookup, pMapToList, pMapFromList, -- * Cardinality Single, Multi, -- * PMapKey PMapKey(..), -- * Errors PMapLookupException(..), pMapDecribeError, pMapToThrow, pMapToFail ) where import Prelude hiding (lookup) import Control.Exception (Exception) import Control.Monad.Catch (MonadThrow(..), MonadCatch(..)) import Control.Monad.Fail (MonadFail) import Data.Aeson.Types (Parser) import qualified Data.Foldable as F import Data.Functor.Identity (Identity) import Data.Greskell.AsIterator (AsIterator(..)) import Data.Greskell.GMap (GMapEntry) import Data.Greskell.GraphSON (GValue, GraphSONTyped(..), FromGraphSON(..), parseEither) import qualified Data.HashMap.Strict as HM import Data.List.NonEmpty (NonEmpty((:|))) import Data.Maybe (listToMaybe) import Data.Monoid (Monoid(..)) import Data.Semigroup (Semigroup((<>))) import qualified Data.Semigroup as S import Data.Traversable (Traversable(traverse)) import Data.Text (Text, unpack) import Data.Greskell.NonEmptyLike (NonEmptyLike) import qualified Data.Greskell.NonEmptyLike as NEL -- | A property map, which has text keys and @v@ values. @c@ specifies -- the cardinality of each item, and should be an instance of -- 'NonEmptyLike'. -- -- You can look up values from 'PMap' using key types of 'PMapKey' -- class. -- -- @since 1.0.0.0 newtype PMap c v = PMap (HM.HashMap Text (c v)) deriving (Show,Eq,Functor,Foldable,Traversable) instance GraphSONTyped (PMap c v) where gsonTypeFor _ = "g:Map" instance FromGraphSON (c v) => FromGraphSON (PMap c v) where parseGraphSON gv = fmap PMap $ parseGraphSON gv -- | Make a union of the two 'PMap's. If the two 'PMap's share some -- keys, those values are merged by 'NEL.append' method from -- 'NonEmptyLike'. instance NonEmptyLike c => Semigroup (PMap c v) where (PMap a) <> (PMap b) = PMap (HM.unionWith NEL.append a b) instance NonEmptyLike c => Monoid (PMap c v) where mempty = PMap $ HM.empty mappend = (<>) instance AsIterator (PMap c v) where type IteratorItem (PMap c v) = GMapEntry Text (c v) -- | Insert a key-value pair to 'PMap'. If it already has some items -- for that key, 'NEL.append' method of the 'NonEmptyLike' type @c@ -- determines its behavior. pMapInsert :: NonEmptyLike c => Text -> v -> PMap c v -> PMap c v pMapInsert k v (PMap hm) = PMap $ HM.insertWith NEL.append k (NEL.singleton v) hm -- | Delete a key and all values associated with it. pMapDelete :: Text -> PMap c v -> PMap c v pMapDelete k (PMap hm) = PMap $ HM.delete k hm -- | Lookup all items for the key (low-level function). If there is no -- item for the key, it returns an empty list. pMapLookup :: NonEmptyLike c => Text -> PMap c v -> [v] pMapLookup k (PMap hm) = maybe [] (F.toList . NEL.toNonEmpty) $ HM.lookup k hm -- | List up all entries. pMapToList :: F.Foldable c => PMap c v -> [(Text, v)] pMapToList (PMap hm) = expandValues =<< HM.toList hm where expandValues (k, cv) = map ((,) k) $ F.toList cv -- | Make a 'PMap' from list of entries. pMapFromList :: NonEmptyLike c => [(Text, v)] -> PMap c v pMapFromList = F.foldr f mempty where f (k, v) pm = pMapInsert k v pm -- | Lookup the first value for the key from 'PMap'. lookup :: (PMapKey k, NonEmptyLike c) => k -> PMap c v -> Maybe v lookup k pm = listToMaybe $ lookupList k pm -- | 'MonadThrow' version of 'lookup'. If there is no value for the -- key, it throws 'PMapNoSuchKey'. lookupM :: (PMapKey k, NonEmptyLike c, MonadThrow m) => k -> PMap c v -> m v lookupM k pm = maybe (throwM $ PMapNoSuchKey $ keyText k) return $ lookup k pm -- | Lookup the value and parse it into @a@. lookupAs :: (PMapKey k, NonEmptyLike c, PMapValue k ~ a, FromGraphSON a) => k -> PMap c GValue -> Either PMapLookupException a lookupAs k pm = case lookup k pm of Nothing -> Left $ PMapNoSuchKey kt Just gv -> either (Left . PMapParseError kt) Right $ parseEither gv where kt = keyText k -- | Similar to 'lookupAs', but this function converts a @null@ result -- into 'Nothing'. -- -- A @null@ result is either (1) the key @k@ is not found in the map, -- or (2) the key is found, but the value is @null@. lookupAs' :: (PMapKey k, NonEmptyLike c, PMapValue k ~ (Maybe a), FromGraphSON a) => k -> PMap c GValue -> Either PMapLookupException (Maybe a) lookupAs' k pm = either fromError Right $ lookupAs k pm where fromError (PMapNoSuchKey _) = Right Nothing fromError e = Left e -- | 'MonadThrow' version of 'lookupAs'. lookupAsM :: (PMapKey k, NonEmptyLike c, PMapValue k ~ a, FromGraphSON a, MonadThrow m) => k -> PMap c GValue -> m a lookupAsM k pm = pMapToThrow $ lookupAs k pm -- | Lookup all items for the key. If there is no item for the key, it -- returns an empty list. lookupList :: (PMapKey k, NonEmptyLike c) => k -> PMap c v -> [v] lookupList k pm = pMapLookup (keyText k) pm -- | Look up the values and parse them into @a@. lookupListAs :: (PMapKey k, NonEmptyLike c, PMapValue k ~ a, FromGraphSON a) => k -> PMap c GValue -> Either PMapLookupException (NonEmpty a) lookupListAs k pm = case lookupList k pm of [] -> Left $ PMapNoSuchKey kt (x : rest) -> either (Left . PMapParseError kt) Right $ traverse parseEither (x :| rest) where kt = keyText k -- | Similar to 'lookupListAs', but this function accepts @null@ -- results. -- -- If the key @k@ is not found in the map, it returns an empty -- list. If the key @k@ is found and @null@s are included in the -- values, they are obtained as 'Nothing'. lookupListAs' :: (PMapKey k, NonEmptyLike c, PMapValue k ~ (Maybe a), FromGraphSON a) => k -> PMap c GValue -> Either PMapLookupException [Maybe a] lookupListAs' k pm = either fromError (Right . F.toList) $ lookupListAs k pm where fromError (PMapNoSuchKey _) = Right [] fromError e = Left e -- | The single cardinality for 'PMap'. 'pMapInsert' method replaces -- the old value. '<>' on 'PMap' prefers the items from the left -- 'PMap'. 'pMapFromList' prefers the first item for each key. -- -- @since 1.0.0.0 type Single = S.First -- | The \"one or more\" cardinality for 'PMap'. 'pMapInsert' method -- prepends the new value at the head. '<>' on 'PMap' appends the -- right items to the tail of the left items. 'pMapFromList' preserves -- the order of the items for each key. -- -- @since 1.0.0.0 newtype Multi a = Multi (NonEmpty a) deriving (Show,Eq,Ord,Functor,Semigroup,Foldable,Traversable,NonEmptyLike,FromGraphSON) -- | A typed key for 'PMap'. -- -- @since 1.0.0.0 class PMapKey k where -- | Type of the value associated with the key. type PMapValue k :: * -- | 'Text' representation of the key. keyText :: k -> Text -- | For simple 'Text', the 'GValue' is returned without being parsed. instance PMapKey Text where type PMapValue Text = GValue keyText = id -- | An 'Exception' raised when looking up values from 'PMap'. -- -- @since 1.0.0.0 data PMapLookupException = PMapNoSuchKey Text -- ^ The 'PMap' doesn't have the given key. | PMapParseError Text String -- ^ Failed to parse the value into the type that the 'PMapKey' -- indicates. The 'Text' is the key, and the 'String' is the error -- message. deriving (Show,Eq,Ord) instance Exception PMapLookupException -- | Make a human-readable description on 'PMapLookupException'. pMapDecribeError :: PMapLookupException -> String pMapDecribeError (PMapNoSuchKey k) = "Property '" ++ unpack k ++ "' does not exist." pMapDecribeError (PMapParseError k pe) = "Parse error of property '" ++ unpack k ++ "': " ++ pe -- | Convert the lookup result into a 'MonadThrow'. It throws -- 'PMapLookupException'. pMapToThrow :: MonadThrow m => Either PMapLookupException a -> m a pMapToThrow (Left e) = throwM e pMapToThrow (Right a) = return a -- | Convert the lookup result into a 'MonadFail'. It fails with the -- description returned by 'pMapDecribeError'. pMapToFail :: MonadFail m => Either PMapLookupException a -> m a pMapToFail (Left e) = fail $ pMapDecribeError e pMapToFail (Right a) = return a