packages feed

packstream-bolt-0.1.0.0: src/Data/PackStream/Ps.hs

{-# LANGUAGE IncoherentInstances  #-}
{-# LANGUAGE OverloadedLists      #-}

-- | Core PackStream AST: the 'Ps' type and the 'PackStream' type class for
-- converting between Haskell types and PackStream values.
--
-- Specification: <https://neo4j.com/docs/bolt/current/packstream/>
module Data.PackStream.Ps
    ( -- * Core types
      Tag
    , Ps(..)
    , structFromDict
    , structureSingleton
      -- * PackStream class
    , PackStream(..)
      -- * Operators
    , (.:), (.=)
      -- * Accessors
    , getPs, putPs
      -- * Type matching
    , typeMismatch
    , withNull, withBoolean, withInteger, withFloat
    , withBytes, withString, withList, withDictionary
    ) where

import           Compat.Prelude
import           Prelude                       hiding (putStr)

import qualified Data.ByteString               as S
import qualified Data.ByteString.Lazy          as L
import qualified Data.ByteString.Short         as SBS
import qualified Data.HashMap.Strict           as HashMap
import           Data.Kind                     (Constraint, Type)
import           Data.List.NonEmpty            (NonEmpty)
import qualified Data.List.NonEmpty            as NEL
import qualified Data.Text                     as T
import qualified Data.Text.Lazy                as LT
import           Data.Typeable
import qualified Data.Vector                   as V
import qualified Data.HashMap.Lazy             as H

import           Data.PackStream.Get.Internal
import           Data.PackStream.Integer
import           Data.PackStream.Put
import           Data.PackStream.Result
import           Data.PackStream.Tags ()

import           Compat.Binary
import           TextShow (showt)


-- | Structure tag byte identifying a PackStream structure type.
type Tag :: Type
type Tag = Word8

-- | A PackStream value. This is the intermediate AST used for serialization.
type Ps :: Type
data Ps
  = PsNull
    -- ^ missing or empty value
  | PsBoolean !Bool
    -- ^ true or false
  | PsInteger !PSInteger
    -- ^ signed 64-bit integer
  | PsFloat !Double
    -- ^ 64-bit floating point number
  | PsBytes !S.ByteString
    -- ^ byte array
  | PsString !T.Text
    -- ^ unicode text, UTF-8
  | PsList !(V.Vector Ps)
    -- ^ ordered collection of values
  | PsDictionary !(H.HashMap T.Text Ps)
    -- ^ collection of key-value entries (no order guaranteed)
  | PsStructure !Tag !(V.Vector Ps)
    -- ^ composite value with a type signature
    --
    -- fields being a Vector is a bit wasteful, but the spec demands it
    -- in practice there is always just 1 field, which is a dictionary
    -- Control messages all use dictionaries: https://neo4j.com/docs/bolt/current/bolt/message/#messages
    -- Datatypes all use dictionaries: https://neo4j.com/docs/bolt/current/bolt/structure-semantics/
  deriving stock (Show, Eq)

-- | Build a structure with a single dictionary field.
structFromDict :: Tag -> H.HashMap T.Text Ps -> Ps
structFromDict tag map = PsStructure tag $ V.singleton $ PsDictionary map

-- | Build a structure with exactly one field.
structureSingleton :: Tag -> Ps -> Ps
structureSingleton tag ps = PsStructure tag $ V.singleton ps


-- | Look up a key in a 'PsDictionary' and decode the value.
(.:) :: PackStream a => Ps -> T.Text -> Result a
(PsDictionary m) .: key = case H.lookup key m of
  Nothing -> Error $ "missing key \"" <> key <> "\""
  Just v -> fromPs v
m .: _ = Error $ ("expected PsDictionary got " :: T.Text) <> (showt . typeOf $ m)

-- | Build a key-value pair for constructing a 'PsDictionary'.
(.=) :: PackStream a => T.Text -> a -> (Ps, Ps)
k .= a = (PsString k, toPs a)

instance NFData Ps where
  rnf obj = case obj of
    PsList a -> rnf a
    PsDictionary   m -> rnf m
    _             -> ()

-- | Decode any PackStream value from the binary stream.
getPs :: Get Ps
getPs = do
  tag <- getWord8

  tryNull tag (const PsNull) $
    tryBoolean    tag PsBoolean $
    tryPSInteger  tag PsInteger $
    tryFloat      tag PsFloat $
    tryString     tag PsString $
    tryBytes      tag PsBytes $
    tryList getPs tag PsList $
    tryDictionary getString getPs tag PsDictionary $
    tryStructure getPs tag (uncurry PsStructure) $
    fail ("getPs: internal error " ++ show tag) -- should never happen

-- | Encode any PackStream value to the binary stream.
putPs :: Ps -> Put
putPs = \case
  PsNull         -> putNull
  PsBoolean    b -> putBoolean b
  PsInteger    n -> put n
  PsFloat      d -> putFloat d
  PsString     t -> putString t
  PsBytes      b -> putBytes b
  PsList       a -> putList putPs a
  PsDictionary m -> putDictionary putString putPs m
  PsStructure t fs -> do
    let nfields = V.length fs
    putWord8 (0xB0 .|. fromIntegral nfields)
    putWord8 t
    V.mapM_ putPs fs

-- | This 'Persist' instance encodes\/decodes to\/from PackStream format
instance Persist Ps where
  get = getPs
  put = putPs

-- | Class for converting between PackStream 'Ps's and native Haskell types.
type PackStream :: Type -> Constraint
class PackStream a where
  toPs   :: a -> Ps

  -- | Encodes directly to 'Put' monad bypassing the intermediate 'Ps' AST
  --
  -- @since 1.1.0.0
  toBinary :: a -> Put
  toBinary = putPs . toPs

  fromPs :: Ps -> Result a

-- core instances

-- | The trivial identity 'PackStream' instance
instance PackStream Ps where
  toPs = id
  toBinary = putPs
  fromPs = pure

-- | Encodes as 'PsNull'
instance PackStream () where
  toPs _ = PsNull
  toBinary _ = putNull
  fromPs = withNull "()" (pure ())

instance PackStream Bool where
  toPs = PsBoolean
  toBinary = putBoolean
  fromPs = withBoolean "Boolean" pure

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

instance PackStream PSInteger where
  toPs = PsInteger
  toBinary = put
  fromPs = withInteger "PSInteger" pure

fromPsInteger :: FromPSInteger i => T.Text -> Ps -> Result i
fromPsInteger expected = withInteger expected go
  where
    go j = case fromPSInteger j of
      Just j' -> pure j'
      Nothing -> fail $ T.unpack $ "PackStream integer " <> showt j <> " cannot be decoded into " <> expected

instance PackStream Word32 where
  toPs = PsInteger . toPSInteger
  toBinary = put . toPSInteger
  fromPs = fromPsInteger "Word32"

instance PackStream Word16 where
  toPs = PsInteger . toPSInteger
  toBinary = put . toPSInteger
  fromPs = fromPsInteger "Word16"

instance PackStream Word8 where
  toPs = PsInteger . toPSInteger
  toBinary = put . toPSInteger
  fromPs = fromPsInteger "Word8"

instance PackStream Int64 where
  toPs = PsInteger . toPSInteger
  toBinary = put . toPSInteger
  fromPs = fromPsInteger "Int64"

instance PackStream Int32 where
  toPs = PsInteger . toPSInteger
  toBinary = put . toPSInteger
  fromPs = fromPsInteger "Int32"

instance PackStream Int16 where
  toPs = PsInteger . toPSInteger
  toBinary = put . toPSInteger
  fromPs = fromPsInteger "Int16"

instance PackStream Int8 where
  toPs = PsInteger . toPSInteger
  toBinary = put . toPSInteger
  fromPs = fromPsInteger "Int8"

instance PackStream Int where
  toPs = PsInteger . toPSInteger
  toBinary = put . toPSInteger
  fromPs = fromPsInteger "Int"

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

-- | This instance decodes 64bit and 32bit floats from PackStream streams into a 'Double'
instance PackStream Double where
  toPs = PsFloat
  toBinary = putFloat
  fromPs = withFloat "Double" pure

instance PackStream S.ByteString where
  toPs = PsBytes
  toBinary = putBytes
  fromPs = withBytes "ByteString" pure

-- Because of overlapping instance, this must be above [a]
instance PackStream String where
  toPs = toPs . T.pack
  toBinary = putString . T.pack
  fromPs obj = T.unpack <$> fromPs obj

instance PackStream a => PackStream (V.Vector a) where
  toPs = PsList . V.map toPs
  toBinary = putList toBinary
  fromPs = withList "Vector" (traverse fromPs)



-- | 'Maybe's are encoded as nullable types, i.e. 'Nothing' is encoded as @nil@.
--
-- __NOTE__: Encoding nested 'Maybe's or 'Maybe's enclosing types which encode to @nil@ (such as '()') will break round-tripping
instance PackStream a => PackStream (Maybe a) where
  toPs = \case
    Just a  -> toPs a
    Nothing -> PsNull
  toBinary = \case
    Just a  -> toBinary a
    Nothing -> putNull

  fromPs = \case
    PsNull -> pure Nothing
    obj       -> Just <$> fromPs obj

-- UTF8 string like

instance PackStream L.ByteString where
  toPs = PsBytes . L.toStrict
  toBinary = putBytes . L.toStrict
  fromPs obj = L.fromStrict <$> fromPs obj

instance PackStream SBS.ShortByteString where
  toPs = PsBytes . SBS.fromShort
  toBinary = putBytes . SBS.fromShort
  fromPs obj = SBS.toShort <$> fromPs obj

instance PackStream T.Text where
  toPs = PsString
  toBinary = putString
  fromPs = withString "Text" pure

instance PackStream LT.Text where
  toPs = toPs . LT.toStrict
  toBinary = putString . LT.toStrict
  fromPs obj = LT.fromStrict <$> fromPs obj

-- array like

instance PackStream a => PackStream [a] where
  toPs = toPs . V.fromList
  toBinary = putList toBinary . V.fromList
  fromPs obj = V.toList <$> fromPs obj

instance PackStream a => PackStream (NonEmpty a) where
  toPs = toPs . NEL.toList
  toBinary = toBinary . NEL.toList
  fromPs o = do
    lst <- fromPs o
    case NEL.nonEmpty lst of
      Just as -> Success as
      Nothing -> Error "empty list"

-- dictionary like

instance (PackStream v) => PackStream (HashMap.HashMap T.Text v) where
  toPs = PsDictionary . H.map toPs
  toBinary = putDictionary putString toBinary . H.fromList . HashMap.toList
  fromPs = withDictionary "HashMap" (traverse fromPs)



-- tuples

instance (PackStream a1, PackStream a2) => PackStream (a1, a2) where
  toPs (a1, a2) = PsList [toPs a1, toPs a2]
  toBinary (a1, a2) = putList' 2 $ do { toBinary a1; toBinary a2 }
  fromPs (PsList [a1, a2]) = (,) <$> fromPs a1 <*> fromPs a2
  fromPs obj                    = typeMismatch "2-tuple" obj

instance (PackStream a1, PackStream a2, PackStream a3) => PackStream (a1, a2, a3) where
  toPs (a1, a2, a3) = PsList [toPs a1, toPs a2, toPs a3]
  toBinary (a1, a2, a3) = putList' 3 $ do { toBinary a1; toBinary a2; toBinary a3 }
  fromPs (PsList [a1, a2, a3]) = (,,) <$> fromPs a1 <*> fromPs a2 <*> fromPs a3
  fromPs obj = typeMismatch "3-tuple" obj

instance (PackStream a1, PackStream a2, PackStream a3, PackStream a4) => PackStream (a1, a2, a3, a4) where
  toPs (a1, a2, a3, a4) = PsList [toPs a1, toPs a2, toPs a3, toPs a4]
  toBinary (a1, a2, a3, a4) = putList' 4 $ do { toBinary a1; toBinary a2; toBinary a3; toBinary a4 }
  fromPs (PsList [a1, a2, a3, a4]) = (,,,) <$> fromPs a1 <*> fromPs a2 <*> fromPs a3 <*> fromPs a4
  fromPs obj = typeMismatch "4-tuple" obj

instance (PackStream a1, PackStream a2, PackStream a3, PackStream a4, PackStream a5) => PackStream (a1, a2, a3, a4, a5) where
  toPs (a1, a2, a3, a4, a5) = PsList [toPs a1, toPs a2, toPs a3, toPs a4, toPs a5]
  toBinary (a1, a2, a3, a4, a5) = putList' 5 $ do { toBinary a1; toBinary a2; toBinary a3; toBinary a4; toBinary a5 }
  fromPs (PsList [a1, a2, a3, a4, a5]) = (,,,,) <$> fromPs a1 <*> fromPs a2 <*> fromPs a3 <*> fromPs a4 <*> fromPs a5
  fromPs obj = typeMismatch "5-tuple" obj

instance (PackStream a1, PackStream a2, PackStream a3, PackStream a4, PackStream a5, PackStream a6) => PackStream (a1, a2, a3, a4, a5, a6) where
  toPs (a1, a2, a3, a4, a5, a6) = PsList [toPs a1, toPs a2, toPs a3, toPs a4, toPs a5, toPs a6]
  toBinary (a1, a2, a3, a4, a5, a6) = putList' 6 $ do { toBinary a1; toBinary a2; toBinary a3; toBinary a4; toBinary a5; toBinary a6 }
  fromPs (PsList [a1, a2, a3, a4, a5, a6]) = (,,,,,) <$> fromPs a1 <*> fromPs a2 <*> fromPs a3 <*> fromPs a4 <*> fromPs a5 <*> fromPs a6
  fromPs obj = typeMismatch "6-tuple" obj

instance (PackStream a1, PackStream a2, PackStream a3, PackStream a4, PackStream a5, PackStream a6, PackStream a7) => PackStream (a1, a2, a3, a4, a5, a6, a7) where
  toPs (a1, a2, a3, a4, a5, a6, a7) = PsList [toPs a1, toPs a2, toPs a3, toPs a4, toPs a5, toPs a6, toPs a7]
  toBinary (a1, a2, a3, a4, a5, a6, a7) = putList' 7 $ do { toBinary a1; toBinary a2; toBinary a3; toBinary a4; toBinary a5; toBinary a6; toBinary a7 }
  fromPs (PsList [a1, a2, a3, a4, a5, a6, a7]) = (,,,,,,) <$> fromPs a1 <*> fromPs a2 <*> fromPs a3 <*> fromPs a4 <*> fromPs a5 <*> fromPs a6 <*> fromPs a7
  fromPs obj = typeMismatch "7-tuple" obj

instance (PackStream a1, PackStream a2, PackStream a3, PackStream a4, PackStream a5, PackStream a6, PackStream a7, PackStream a8) => PackStream (a1, a2, a3, a4, a5, a6, a7, a8) where
  toPs (a1, a2, a3, a4, a5, a6, a7, a8) = PsList [toPs a1, toPs a2, toPs a3, toPs a4, toPs a5, toPs a6, toPs a7, toPs a8]
  toBinary (a1, a2, a3, a4, a5, a6, a7, a8) = putList' 8 $ do { toBinary a1; toBinary a2; toBinary a3; toBinary a4; toBinary a5; toBinary a6; toBinary a7; toBinary a8 }
  fromPs (PsList [a1, a2, a3, a4, a5, a6, a7, a8]) = (,,,,,,,) <$> fromPs a1 <*> fromPs a2 <*> fromPs a3 <*> fromPs a4 <*> fromPs a5 <*> fromPs a6 <*> fromPs a7 <*> fromPs a8
  fromPs obj = typeMismatch "8-tuple" obj

instance (PackStream a1, PackStream a2, PackStream a3, PackStream a4, PackStream a5, PackStream a6, PackStream a7, PackStream a8, PackStream a9) => PackStream (a1, a2, a3, a4, a5, a6, a7, a8, a9) where
  toPs (a1, a2, a3, a4, a5, a6, a7, a8, a9) = PsList [toPs a1, toPs a2, toPs a3, toPs a4, toPs a5, toPs a6, toPs a7, toPs a8, toPs a9]
  toBinary (a1, a2, a3, a4, a5, a6, a7, a8, a9) = putList' 8 $ do { toBinary a1; toBinary a2; toBinary a3; toBinary a4; toBinary a5; toBinary a6; toBinary a7; toBinary a8; toBinary a9 }
  fromPs (PsList [a1, a2, a3, a4, a5, a6, a7, a8, a9]) = (,,,,,,,,) <$> fromPs a1 <*> fromPs a2 <*> fromPs a3 <*> fromPs a4 <*> fromPs a5 <*> fromPs a6 <*> fromPs a7 <*> fromPs a8 <*> fromPs a9
  fromPs obj = typeMismatch "9-tuple" obj

-- | Report a type mismatch error for the expected type and actual 'Ps' value.
typeMismatch :: T.Text -> Ps -> Result a
typeMismatch expected obj = fail $ T.unpack ("PackStream " <> got <> " type cannot be decoded into " <> expected)
  where
    got = case obj of
      PsNull           -> "nil"
      PsList v         -> "array[" <> showt (V.length v) <> "]"
      PsDictionary v   -> "map[" <> showt (H.size v) <> "]"
      PsString _       -> "str"
      PsBoolean _      -> "bool"
      PsInteger _      -> "int"
      PsFloat _        -> "float"
      PsBytes _        -> "bin"
      PsStructure ty _ -> "structure[" <> showt ty <> "]"

-- | Match a 'PsNull', or report a type mismatch.
withNull :: T.Text -> Result a -> Ps -> Result a
withNull _ f PsNull  = f
withNull expected _ got = typeMismatch expected got

-- | Match a 'PsBoolean', or report a type mismatch.
withBoolean :: T.Text -> (Bool -> Result a) -> Ps -> Result a
withBoolean _ f (PsBoolean b) = f b
withBoolean expected _ got = typeMismatch expected got

-- | Match a 'PsInteger', or report a type mismatch.
withInteger :: T.Text -> (PSInteger -> Result a) -> Ps -> Result a
withInteger _ f (PsInteger i) = f i
withInteger expected _ got = typeMismatch expected got

-- | Match a 'PsFloat', or report a type mismatch.
withFloat :: T.Text -> (Double -> Result a) -> Ps -> Result a
withFloat _ f (PsFloat x) = f x
withFloat expected _ got   = typeMismatch expected got

-- | Match a 'PsBytes', or report a type mismatch.
withBytes :: T.Text -> (S.ByteString -> Result a) -> Ps -> Result a
withBytes _ f (PsBytes i) = f i
withBytes expected _ got = typeMismatch expected got

-- | Match a 'PsString', or report a type mismatch.
withString :: T.Text -> (T.Text -> Result a) -> Ps -> Result a
withString _ f (PsString i) = f i
withString expected _ got = typeMismatch expected got

-- | Match a 'PsList', or report a type mismatch.
withList :: T.Text -> (V.Vector Ps -> Result a) -> Ps -> Result a
withList _ f (PsList xs) = f xs
withList expected _ got   = typeMismatch expected got

-- | Match a 'PsDictionary', or report a type mismatch.
withDictionary :: T.Text -> (H.HashMap T.Text Ps -> Result a) -> Ps -> Result a
withDictionary _ f (PsDictionary xs) = f xs
withDictionary expected _ got = typeMismatch expected got