packages feed

ribosome-host-0.9.9.9: lib/Ribosome/Host/Class/Msgpack/Decode.hs

{-# options_haddock prune #-}

-- |Decoding values from MessagePack format
module Ribosome.Host.Class.Msgpack.Decode where

import qualified Data.Map.Strict as Map (empty, fromList, toList)
import Data.MessagePack (Object (..))
import Exon (exon)
import GHC.Float (double2Float, float2Double)
import GHC.Generics (
  C1,
  Constructor,
  D1,
  K1 (..),
  M1 (..),
  Rep,
  S1,
  Selector,
  conIsRecord,
  selName,
  to,
  (:*:) (..),
  (:+:) (..),
  )
import Path (Abs, Dir, File, Path, Rel, parseAbsDir, parseAbsFile, parseRelDir, parseRelFile)
import Prelude hiding (to)
import Time (MicroSeconds, MilliSeconds, NanoSeconds, Seconds (Seconds))

import qualified Ribosome.Host.Class.Msgpack.Util as Util (illegalType, invalid, lookupObjectMap, missingRecordKey)

-- |Class of values that can be decoded from MessagePack 'Object's.
class MsgpackDecode a where
  -- |Attempt to decode an 'Object', returning an error message in a 'Left' if the data is incompatible.
  --
  -- The default implementation uses generic derivation.
  fromMsgpack :: Object -> Either Text a
  default fromMsgpack :: (Generic a, GMsgpackDecode (Rep a)) => Object -> Either Text a
  fromMsgpack = fmap to . gMsgpackDecode

  -- |Utility method called by the generic machinery when a record key is missing.
  missingKey :: String -> Object -> Either Text a
  missingKey = Util.missingRecordKey

-- |Pattern synonym for decoding an 'Object'.
pattern Msgpack :: ∀ a . MsgpackDecode a => a -> Object
pattern Msgpack a <- (fromMsgpack -> Right a)

class GMsgpackDecode f where
  gMsgpackDecode :: Object -> Either Text (f a)

  gMissingKey :: String -> Object -> Either Text (f a)
  gMissingKey = Util.missingRecordKey

class MsgpackDecodeProd f where
  msgpackDecodeRecord :: Map Object Object -> Either Text (f a)
  msgpackDecodeProd :: [Object] -> Either Text ([Object], f a)

instance (GMsgpackDecode f) => GMsgpackDecode (D1 c f) where
  gMsgpackDecode =
    fmap M1 . gMsgpackDecode @f

instance (Constructor c, MsgpackDecodeProd f) => GMsgpackDecode (C1 c f) where
  gMsgpackDecode =
    fmap M1 . decode
    where
      isRec = conIsRecord (undefined :: t c f p)
      decode o@(ObjectMap om) =
        if isRec then msgpackDecodeRecord om else Util.invalid "illegal ObjectMap for product" o
      decode o | isRec =
        Util.invalid "illegal non-ObjectMap for record" o
      decode o =
        msgpackDecodeProd (prod o) >>= check
        where
          check ([], a) = Right a
          check _ = Util.invalid "too many values for product" o
          prod (ObjectArray oa) = oa
          prod ob = [ob]

instance (MsgpackDecodeProd f, MsgpackDecodeProd g) => MsgpackDecodeProd (f :*: g) where
  msgpackDecodeRecord o = do
    left <- msgpackDecodeRecord o
    right <- msgpackDecodeRecord o
    pure $ left :*: right
  msgpackDecodeProd o = do
    (rest, left) <- msgpackDecodeProd o
    (rest1, right) <- msgpackDecodeProd rest
    pure (rest1, left :*: right)

instance (GMsgpackDecode f, GMsgpackDecode g) => GMsgpackDecode (f :+: g) where
  gMsgpackDecode o = fromRight (L1 <$> gMsgpackDecode @f o) (Right . R1 <$> gMsgpackDecode @g o)

-- TODO use Proxy instead of undefined
instance (Selector s, GMsgpackDecode f) => MsgpackDecodeProd (S1 s f) where
  msgpackDecodeRecord o =
    M1 <$> maybe (gMissingKey key (ObjectMap o)) gMsgpackDecode lookup
    where
      lookup =
        Util.lookupObjectMap key o <|> lookupUnderscore
      lookupUnderscore =
        if hasUnderscore
        then Util.lookupObjectMap (dropWhile ('_' ==) key) o
        else Nothing
      hasUnderscore =
        take 1 key == "_"
      key =
        selName (undefined :: t s f p)
  msgpackDecodeProd (cur:rest) = do
    a <- gMsgpackDecode cur
    pure (rest, M1 a)
  msgpackDecodeProd [] = Util.invalid "too few values for product" ObjectNil

instance MsgpackDecode a => GMsgpackDecode (K1 i a) where
  gMsgpackDecode = fmap K1 . fromMsgpack

  gMissingKey key =
    fmap K1 . missingKey key

instance (Ord k, MsgpackDecode k, MsgpackDecode v) => MsgpackDecode (Map k v) where
  fromMsgpack (ObjectMap om) = do
    m <- traverse decodePair $ Map.toList om
    Right $ Map.fromList m
    where
      decodePair (k, v) = do
        k1 <- fromMsgpack k
        v1 <- fromMsgpack v
        pure (k1, v1)
  fromMsgpack o = Util.illegalType "Map" o
  missingKey _ _ = Right Map.empty

integralFromString ::
  Read a =>
  ByteString ->
  Either Text a
integralFromString =
  readEither . decodeUtf8

msgpackIntegral ::
  Integral a =>
  Read a =>
  Object ->
  Either Text a
msgpackIntegral (ObjectInt i) = Right $ fromIntegral i
msgpackIntegral (ObjectUInt i) = Right $ fromIntegral i
msgpackIntegral (ObjectString s) = integralFromString s
msgpackIntegral (ObjectBinary s) = integralFromString s
msgpackIntegral o = Util.illegalType "Integral" o

msgpackText :: ConvertUtf8 t ByteString => Text -> (t -> Either Text a) -> Object -> Either Text a
msgpackText typeName decode =
  run
  where
    run (ObjectString os) = decode $ decodeUtf8 os
    run (ObjectBinary os) = decode $ decodeUtf8 os
    run o = Util.illegalType typeName o

instance MsgpackDecode Integer where
  fromMsgpack = msgpackIntegral

instance MsgpackDecode Int where
  fromMsgpack = msgpackIntegral

instance MsgpackDecode Int64 where
  fromMsgpack = msgpackIntegral

instance MsgpackDecode Float where
  fromMsgpack (ObjectFloat a) = Right a
  fromMsgpack (ObjectDouble a) = Right (double2Float a)
  fromMsgpack (ObjectInt a) = Right (fromIntegral a)
  fromMsgpack (ObjectUInt a) = Right (fromIntegral a)
  fromMsgpack o = Util.illegalType "Float" o

instance MsgpackDecode Double where
  fromMsgpack (ObjectFloat a) = Right (float2Double a)
  fromMsgpack (ObjectDouble a) = Right a
  fromMsgpack (ObjectInt a) = Right (fromIntegral a)
  fromMsgpack (ObjectUInt a) = Right (fromIntegral a)
  fromMsgpack o = Util.illegalType "Double" o

instance {-# OVERLAPPING #-} MsgpackDecode String where
  fromMsgpack = msgpackText "String" Right

instance {-# OVERLAPPABLE #-} MsgpackDecode a => MsgpackDecode [a] where
  fromMsgpack (ObjectArray oa) = traverse fromMsgpack oa
  fromMsgpack o = Util.illegalType "List" o
  missingKey _ _ = Right []

instance MsgpackDecode Text where
  fromMsgpack =
    msgpackText "Text" Right

instance MsgpackDecode ByteString where
  fromMsgpack (ObjectString os) = Right os
  fromMsgpack (ObjectBinary os) = Right os
  fromMsgpack o = Util.illegalType "ByteString" o

instance MsgpackDecode Char where
  fromMsgpack o =
    msgpackText "Char" check o
    where
      check :: [Char] -> Either Text Char
      check [c] = Right c
      check _ = Util.invalid "multiple characters when decoding Char" o

instance MsgpackDecode a => MsgpackDecode (Maybe a) where
  fromMsgpack ObjectNil = Right Nothing
  fromMsgpack o = Just <$> fromMsgpack o
  missingKey _ _ = Right Nothing

instance (MsgpackDecode a, MsgpackDecode b) => MsgpackDecode (Either a b) where
  fromMsgpack o =
    fromRight (Left <$> fromMsgpack o) (Right . Right <$> fromMsgpack o)

instance MsgpackDecode Bool where
  fromMsgpack (ObjectBool a) = Right a
  fromMsgpack (ObjectInt 0) = Right False
  fromMsgpack (ObjectInt 1) = Right True
  fromMsgpack o = Util.illegalType "Bool" o

instance MsgpackDecode () where
  fromMsgpack _ = Right ()

instance MsgpackDecode Object where
  fromMsgpack = Right

decodeTuple :: Int -> ([Object] -> Either (Maybe Text) a) -> Object -> Either Text a
decodeTuple i f = \case
  o@(ObjectArray oa) ->
    case f oa of
      Right a -> pure a
      Left Nothing -> Util.invalid [exon|invalid array length for #{show i}-tuple|] o
      Left (Just err) -> Left err
  o ->
    Util.illegalType [exon|#{show i}-tuple|] o

instance (MsgpackDecode a, MsgpackDecode b) => MsgpackDecode (a, b) where
  fromMsgpack =
    decodeTuple 2 \case
      [a, b] ->
        first Just ((,) <$> fromMsgpack a <*> fromMsgpack b)
      _ ->
        Left Nothing

instance (MsgpackDecode a, MsgpackDecode b, MsgpackDecode c) => MsgpackDecode (a, b, c) where
  fromMsgpack =
    decodeTuple 3 \case
      [a, b, c] ->
        first Just ((,,) <$> fromMsgpack a <*> fromMsgpack b <*> fromMsgpack c)
      _ ->
        Left Nothing

instance (MsgpackDecode a, MsgpackDecode b, MsgpackDecode c, MsgpackDecode d) => MsgpackDecode (a, b, c, d) where
  fromMsgpack =
    decodeTuple 4 \case
      [a, b, c, d] ->
        first Just ((,,,) <$> fromMsgpack a <*> fromMsgpack b <*> fromMsgpack c <*> fromMsgpack d)
      _ ->
        Left Nothing

class DecodePath b t where
  decodePath :: FilePath -> Either SomeException (Path b t)

instance DecodePath Abs File where
  decodePath =
    parseAbsFile

instance DecodePath Abs Dir where
  decodePath =
    parseAbsDir

instance DecodePath Rel File where
  decodePath =
    parseRelFile

instance DecodePath Rel Dir where
  decodePath =
    parseRelDir

decodePathE ::
  ∀ b t .
  DecodePath b t =>
  Text ->
  Either Text (Path b t)
decodePathE =
  first show . decodePath . toString

instance DecodePath b t => MsgpackDecode (Path b t) where
  fromMsgpack =
    msgpackText "Path" decodePathE

timeUnit ::
  Fractional a =>
  Text ->
  Object ->
  Either Text a
timeUnit name = \case
  Msgpack d -> Right (realToFrac @Double d)
  Msgpack i -> Right (fromIntegral @Int64 i)
  o -> Util.illegalType name o

instance MsgpackDecode NanoSeconds where
  fromMsgpack =
    timeUnit "NanoSeconds"

instance MsgpackDecode MicroSeconds where
  fromMsgpack =
    timeUnit "MicroSeconds"

instance MsgpackDecode MilliSeconds where
  fromMsgpack =
    timeUnit "MilliSeconds"

instance MsgpackDecode Seconds where
  fromMsgpack =
    fmap Seconds . fromMsgpack

msgpackFromString :: IsString a => Text -> Object -> Either Text a
msgpackFromString name o =
  case fromMsgpack o of
    Right a ->
      Right (fromString a)
    Left _ ->
      Util.illegalType name o