{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeFamilies #-} module Octane.Type.Dictionary (Dictionary(..)) where import Data.Function ((&)) import qualified Control.DeepSeq as DeepSeq import qualified Data.Aeson as Aeson import qualified Data.Binary as Binary import qualified Data.Map.Strict as Map import qualified GHC.Exts as Exts import qualified GHC.Generics as Generics import qualified Octane.Type.Text as Text -- $setup -- >>> import GHC.Exts -- >>> import Octane.Type.Word8 -- | A mapping between text and arbitrary values. newtype Dictionary a = Dictionary { unpack :: (Map.Map Text.Text a) } deriving (Eq, Generics.Generic) -- | Elements are stored with the key first, then the value. The dictionary -- ends when a key is @"None"@. -- -- >>> Binary.decode "\x02\x00\x00\x00k\x00\x01\x05\x00\x00\x00None\x00" :: Dictionary Word8 -- fromList [("k",0x01)] -- -- >>> Binary.encode ([("k", 1)] :: Dictionary Word8) -- "\STX\NUL\NUL\NULk\NUL\SOH\ENQ\NUL\NUL\NULNone\NUL" instance (Binary.Binary a) => Binary.Binary (Dictionary a) where get = do element <- getElement if Map.null element then element & Dictionary & pure else do Dictionary elements <- Binary.get elements & Map.union element & Dictionary & pure put dictionary = do dictionary & unpack & Map.assocs & mapM_ putElement noneKey & Binary.put -- | Allows creating 'Dictionary' values with 'Exts.fromList'. Also allows -- 'Dictionary' literals with the @OverloadedLists@ extension. -- -- >>> [("one", 1)] :: Dictionary Int -- fromList [("one",1)] instance Exts.IsList (Dictionary a) where type Item (Dictionary a) = (Text.Text, a) fromList items = Dictionary (Map.fromList items) toList dictionary = Map.toList (unpack dictionary) instance (DeepSeq.NFData a) => DeepSeq.NFData (Dictionary a) where -- | Shown as @fromList [("key","value")]@. -- -- >>> show ([("one", 1)] :: Dictionary Int) -- "fromList [(\"one\",1)]" instance (Show a) => Show (Dictionary a) where show dictionary = show (unpack dictionary) -- | Encoded directly as a JSON object. -- -- >>> Aeson.encode ([("one", 1)] :: Dictionary Int) -- "{\"one\":1}" instance (Aeson.ToJSON a) => Aeson.ToJSON (Dictionary a) where toJSON dictionary = dictionary & unpack & Map.mapKeys Text.unpack & Aeson.toJSON getElement :: (Binary.Binary a) => Binary.Get (Map.Map Text.Text a) getElement = do key <- Binary.get if key == noneKey then pure Map.empty else do value <- Binary.get value & Map.singleton key & pure putElement :: (Binary.Binary a) => (Text.Text, a) -> Binary.Put putElement (key, value) = do Binary.put key Binary.put value noneKey :: Text.Text noneKey = "None"