marionette-1.0.0: src/Test/Marionette/Protocol.hs
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE RoleAnnotations #-}
{-# LANGUAGE UndecidableInstances #-}
module Test.Marionette.Protocol where
import Control.Monad (MonadPlus (mzero))
import Control.Monad.Catch (Exception (..))
import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON), Value (..), withArray)
import Data.Aeson qualified as Aeson
import Data.Aeson.Types qualified as Aeson
import Data.Binary (Binary (..), getWord8)
import Data.Binary qualified as Binary
import Data.Binary.Parser (decimal, getLazyByteString)
import Data.Binary.Put (putLazyByteString)
import Data.ByteString.Lazy (LazyByteString)
import Data.ByteString.Lazy qualified as LazyByteString
import Data.Char (chr)
import Data.Either (fromRight)
import Data.Foldable qualified as Foldable
import Data.String (IsString (fromString))
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Text
import GHC.Generics (Generic)
import Prelude hiding (log)
data Message a = Message {messageId :: Int, messageContent :: a}
deriving stock (Show)
newtype MarionetteMessage = MarionetteMessage LazyByteString
instance Binary MarionetteMessage where
put :: MarionetteMessage -> Binary.Put
put (MarionetteMessage lbs) =
putLazyByteString $
(fromString . show . LazyByteString.length $ lbs) <> ":" <> lbs
get :: Binary.Get MarionetteMessage
get = do
len <- decimal
':' <- chr . fromIntegral <$> getWord8
MarionetteMessage <$> getLazyByteString len
instance Show MarionetteMessage where
show :: MarionetteMessage -> String
show = Text.unpack . Text.decodeUtf8 . LazyByteString.toStrict . Binary.encode
data Command = Command
{ command :: Text
, parameters :: Value
}
deriving stock (Show)
instance IsString Command where
fromString str =
Command
{ command = Text.pack str
, parameters = Aeson.object []
}
instance ToJSON (Message Command) where
toJSON :: Message Command -> Value
toJSON Message{messageId, messageContent = Command{..}} =
Array
[ Number 0
, toJSON messageId
, toJSON command
, parameters
]
instance FromJSON (Message Command) where
parseJSON :: Value -> Aeson.Parser (Message Command)
parseJSON = withArray "Command" \a -> do
case Foldable.toList a of
[Number 0, messageIdVal, String command, parameters] -> do
messageId <- parseJSON messageIdVal
pure $ Message messageId $ Command{..}
_ -> mzero
type Result = Either Error Value
instance ToJSON (Message Result) where
toJSON :: Message Result -> Value
toJSON Message{..} =
Array
[ Number 1
, toJSON messageId
, errVal
, resVal
]
where
errVal = either toJSON (const Null) messageContent
resVal = fromRight Null messageContent
instance FromJSON (Message Result) where
parseJSON :: Value -> Aeson.Parser (Message Result)
parseJSON = withArray "Result" \a -> do
case Foldable.toList a of
[Number 1, messageIdVal, err, res] -> do
messageId <- parseJSON messageIdVal
Message messageId
<$> if err == Null
then pure . Right $ res
else Left <$> parseJSON err
_ -> mzero
-- | An error reported by a Marionette or WebDriver command, as returned by
-- the server.
--
-- See <https://w3c.github.io/webdriver/#errors>
data Error = Error
{ error :: Text
-- ^ An error code identifying the kind of failure, e.g. @no such element@.
, message :: Text
-- ^ A human-readable description of the failure.
, stacktrace :: Text
-- ^ A stack trace taken on the server side when the error occurred.
}
deriving stock (Generic)
deriving anyclass (Exception, FromJSON, ToJSON)
instance Show Error where
show = Text.unpack . message
data Greeting = Greeting
{ applicationType :: Text
, marionetteProtocol :: Int
}
deriving stock (Generic, Show)
deriving anyclass (FromJSON)