packages feed

webdriver-precore-0.2.0.0: src/WebDriverPreCore/BiDi/Command.hs

module WebDriverPreCore.BiDi.Command
  ( Command (..),
    CommandMethod (..),
    KnownCommand (..),
    OffSpecCommand (..),
    mkCommand,
    emptyCommand,
    mkOffSpecCommand,
    extendLoosenCommand,
    extendCommand,
    extendCoerceCommand,
    loosenCommand,
    coerceCommand,
    knownCommandToText,
    toCommandText,
  )
where

import AesonUtils (jsonToText, objectOrThrow)
import Control.Applicative (Alternative (..))
import Data.Aeson
  ( Object,
    ToJSON,
    Value (..),
  )
import Data.Aeson.KeyMap qualified as KM
import Data.Aeson.Types (FromJSON (parseJSON), Parser, ToJSON (..))
import Data.Text as T (Text, intercalate, unpack)
import Utils (enumerate)

-- |  A BiDi Command to be sent over the WebSocket connection.
--
-- All functions in "WebDriverPreCore.BiDi.API" return values of this type.
--
-- The command contains the method name and parameters to be serialized as JSON and a phantom response type @r@, which is always an instance of 'FromJSON'.
data Command r = MkCommand
  { -- | An identifier for the command to be invoked
    method :: CommandMethod,
    -- | The JSON payload to be sent
    params :: Object
  }
  deriving (Show, Eq)

-- | The method name of a BiDi command.
--
-- This library provides known commands as 'KnownCommand' values, but users can also create off-spec commands using 'OffSpecCommand' as a fallback, when the driver supports commands not yet implemented in this library.
data CommandMethod
  = KnownCommand KnownCommand
  | OffSpecCommand OffSpecCommand
  deriving (Show, Eq)

instance ToJSON CommandMethod where
  toJSON :: CommandMethod -> Value
  toJSON = String . toCommandText

-- | This type enumerates the BiDi commands known to this library.
data KnownCommand
  = BrowserClose
  | BrowserCreateUserContext
  | BrowserGetClientWindows
  | BrowserGetUserContexts
  | BrowserRemoveUserContext
  | BrowserSetClientWindowState
  | BrowserSetDownloadBehavior
  | BrowsingContextActivate
  | BrowsingContextCaptureScreenshot
  | BrowsingContextClose
  | BrowsingContextCreate
  | BrowsingContextGetTree
  | BrowsingContextHandleUserPrompt
  | BrowsingContextLocateNodes
  | BrowsingContextNavigate
  | BrowsingContextPrint
  | BrowsingContextReload
  | BrowsingContextSetViewport
  | BrowsingContextTraverseHistory
  | EmulationSetForcedColorsModeThemeOverride
  | EmulationSetGeolocationOverride
  | EmulationSetLocaleOverride
  | EmulationSetNetworkConditions
  | EmulationSetScreenOrientationOverride
  | EmulationSetScreenSettingsOverride
  | EmulationSetScriptingEnabled
  | EmulationSetTimezoneOverride
  | EmulationSetTouchOverride
  | EmulationSetUserAgentOverride
  | InputPerformActions
  | InputReleaseActions
  | InputSetFiles
  | NetworkAddDataCollector
  | NetworkAddIntercept
  | NetworkContinueRequest
  | NetworkContinueResponse
  | NetworkContinueWithAuth
  | NetworkDisownData
  | NetworkFailRequest
  | NetworkGetData
  | NetworkProvideResponse
  | NetworkRemoveDataCollector
  | NetworkRemoveIntercept
  | NetworkSetCacheBehavior
  | NetworkSetExtraHeaders
  | ScriptAddPreloadScript
  | ScriptCallFunction
  | ScriptDisown
  | ScriptEvaluate
  | ScriptGetRealms
  | ScriptRemovePreloadScript
  | SessionEnd
  | SessionNew
  | SessionStatus
  | SessionSubscribe
  | SessionUnsubscribe
  | StorageDeleteCookies
  | StorageGetCookies
  | StorageSetCookie
  | WebExtensionInstall
  | WebExtensionUninstall
  deriving (Show, Eq, Enum, Bounded)

-- | The method name of a BiDi command not yet implemented in this library
newtype OffSpecCommand = MkOffSpecCommand {command :: Text}
  deriving (Show, Eq)
  deriving newtype (FromJSON, ToJSON)

-- constructors

-- | Creates a 'Command' with the given 'KnownCommand' method and parameters.
--
-- Patially applied in "WebDriverPreCore.BiDi.API" to generate specific named command functions which accept only an input type, relevant to that command.
-- The input type must be an instance of 'ToJSON' and encode to a JSON 'Object' type.
--
-- E.g. the 'WebDriverPreCore.BiDi.API.browsingContextNavigate', which takes a 'Navigate' input and returns a 'Command' with a 'NavigateResult' output, is defined by the partial application of 'mkCommand':
--
-- @
-- browsingContextNavigate :: Navigate -> Command NavigateResult
-- browsingContextNavigate = mkCommand BrowsingContextNavigate
-- @
mkCommand ::
  forall c r.
  (ToJSON c) =>
  -- | The identitifier for the command to be invoked
  KnownCommand ->
  -- | The parameters to be sent with the command
  c ->
  -- | A 'Command' value with the given method and parameters
  Command r
mkCommand method params = MkCommand {method = KnownCommand method, params = objectOrThrow ("mkCommand - " <> toCommandText (KnownCommand method)) params}

-- | Creates a 'Command' with the given 'KnownCommand' method and no parameters.
-- This is the same as calling 'mkCommand' with an empty object as parameters.
--
-- E.g. the 'WebDriverPreCore.BiDi.API.browserGetClientWindows', which takes no input and returns a 'Command' with a list of 'WindowInfo' output, is defined by the partial application of 'emptyCommand':
--
-- @
-- browserGetClientWindows :: Command [WindowInfo]
-- browserGetClientWindows = emptyCommand BrowserGetClientWindows
-- @
emptyCommand ::
  forall r.
  -- | The identitifier for the command to be invoked
  KnownCommand ->
  -- | A 'Command' value with the given method and no parameters
  Command r
emptyCommand method = MkCommand {method = KnownCommand method, params = KM.empty}

-- fallback modifiers

-- | Creates a 'Command' with the given method name text, JSON Object parameters, and an expected JSON Object response.
mkOffSpecCommand :: Text -> Object -> Command Object
mkOffSpecCommand method = MkCommand (OffSpecCommand $ MkOffSpecCommand method)

-- | Extends the parameters of a 'Command' with additional fields from a JSON 'Object', changing the expected response type to a generic JSON 'Value'.
extendLoosenCommand :: forall r. Object -> Command r -> Command Value
extendLoosenCommand = extendCoerceCommand

-- | Extends the parameters of a 'Command' with additional fields from a JSON 'Object', preserving the expected response type.
extendCommand :: forall r. Object -> Command r -> Command r
extendCommand = extendCoerceCommand

-- | Extends the parameters of a 'Command' with additional fields from a JSON 'Object', changing the expected response type to a different type.
extendCoerceCommand :: forall r r2. Object -> Command r -> Command r2
extendCoerceCommand extended MkCommand {method, params} =
  MkCommand
    { method,
      params = params <> extended
    }

-- | Changes the expected response type of a 'Command' to a generic JSON 'Value'.
loosenCommand :: forall r. Command r -> Command Value
loosenCommand = coerceCommand

-- | Changes the expected response type of a 'Command' to a different type.
coerceCommand :: forall r r'. Command r -> Command r'
coerceCommand MkCommand {method, params} = MkCommand {method, params}

instance ToJSON KnownCommand where
  toJSON :: KnownCommand -> Value
  toJSON = String . knownCommandToText

instance FromJSON KnownCommand where
  parseJSON :: Value -> Parser KnownCommand
  parseJSON =
    \case
      String c -> case c of
        "browser.close" -> p BrowserClose
        "browser.createUserContext" -> p BrowserCreateUserContext
        "browser.getClientWindows" -> p BrowserGetClientWindows
        "browser.getUserContexts" -> p BrowserGetUserContexts
        "browser.removeUserContext" -> p BrowserRemoveUserContext
        "browser.setClientWindowState" -> p BrowserSetClientWindowState
        "browser.setDownloadBehavior" -> p BrowserSetDownloadBehavior
        "browsingContext.activate" -> p BrowsingContextActivate
        "browsingContext.captureScreenshot" -> p BrowsingContextCaptureScreenshot
        "browsingContext.close" -> p BrowsingContextClose
        "browsingContext.create" -> p BrowsingContextCreate
        "browsingContext.getTree" -> p BrowsingContextGetTree
        "browsingContext.handleUserPrompt" -> p BrowsingContextHandleUserPrompt
        "browsingContext.locateNodes" -> p BrowsingContextLocateNodes
        "browsingContext.navigate" -> p BrowsingContextNavigate
        "browsingContext.print" -> p BrowsingContextPrint
        "browsingContext.reload" -> p BrowsingContextReload
        "browsingContext.setViewport" -> p BrowsingContextSetViewport
        "browsingContext.traverseHistory" -> p BrowsingContextTraverseHistory
        "emulation.setForcedColorsModeThemeOverride" -> p EmulationSetForcedColorsModeThemeOverride
        "emulation.setGeolocationOverride" -> p EmulationSetGeolocationOverride
        "emulation.setLocaleOverride" -> p EmulationSetLocaleOverride
        "emulation.setNetworkConditions" -> p EmulationSetNetworkConditions
        "emulation.setScreenOrientationOverride" -> p EmulationSetScreenOrientationOverride
        "emulation.setScreenSettingsOverride" -> p EmulationSetScreenSettingsOverride
        "emulation.setScriptingEnabled" -> p EmulationSetScriptingEnabled
        "emulation.setTimezoneOverride" -> p EmulationSetTimezoneOverride
        "emulation.setTouchOverride" -> p EmulationSetTouchOverride
        "emulation.setUserAgentOverride" -> p EmulationSetUserAgentOverride
        "input.performActions" -> p InputPerformActions
        "input.releaseActions" -> p InputReleaseActions
        "input.setFiles" -> p InputSetFiles
        "network.addDataCollector" -> p NetworkAddDataCollector
        "network.addIntercept" -> p NetworkAddIntercept
        "network.continueRequest" -> p NetworkContinueRequest
        "network.continueResponse" -> p NetworkContinueResponse
        "network.continueWithAuth" -> p NetworkContinueWithAuth
        "network.disownData" -> p NetworkDisownData
        "network.failRequest" -> p NetworkFailRequest
        "network.getData" -> p NetworkGetData
        "network.provideResponse" -> p NetworkProvideResponse
        "network.removeDataCollector" -> p NetworkRemoveDataCollector
        "network.removeIntercept" -> p NetworkRemoveIntercept
        "network.setCacheBehavior" -> p NetworkSetCacheBehavior
        "network.setExtraHeaders" -> p NetworkSetExtraHeaders
        "script.addPreloadScript" -> p ScriptAddPreloadScript
        "script.callFunction" -> p ScriptCallFunction
        "script.disown" -> p ScriptDisown
        "script.evaluate" -> p ScriptEvaluate
        "script.getRealms" -> p ScriptGetRealms
        "script.removePreloadScript" -> p ScriptRemovePreloadScript
        "session.end" -> p SessionEnd
        "session.new" -> p SessionNew
        "session.status" -> p SessionStatus
        "session.subscribe" -> p SessionSubscribe
        "session.unsubscribe" -> p SessionUnsubscribe
        "storage.deleteCookies" -> p StorageDeleteCookies
        "storage.getCookies" -> p StorageGetCookies
        "storage.setCookie" -> p StorageSetCookie
        "webExtension.install" -> p WebExtensionInstall
        "webExtension.uninstall" -> p WebExtensionUninstall
        u -> failConversion u
      c -> failConversion $ jsonToText c
    where
      p = pure
      failConversion c =
        fail . unpack $
          "Unrecognised CommandMethodType: "
            <> c
            <> " - "
            <> "Expected one of: "
            <> " "
            <> (T.intercalate ", " $ knownCommandToText <$> enumerate @KnownCommand)

-- | Converts a 'KnownCommand' to its corresponding method name text.
knownCommandToText :: KnownCommand -> Text
knownCommandToText = \case
  BrowserClose -> "browser.close"
  BrowserCreateUserContext -> "browser.createUserContext"
  BrowserGetClientWindows -> "browser.getClientWindows"
  BrowserGetUserContexts -> "browser.getUserContexts"
  BrowserRemoveUserContext -> "browser.removeUserContext"
  BrowserSetClientWindowState -> "browser.setClientWindowState"
  BrowserSetDownloadBehavior -> "browser.setDownloadBehavior"
  BrowsingContextActivate -> "browsingContext.activate"
  BrowsingContextCaptureScreenshot -> "browsingContext.captureScreenshot"
  BrowsingContextClose -> "browsingContext.close"
  BrowsingContextCreate -> "browsingContext.create"
  BrowsingContextGetTree -> "browsingContext.getTree"
  BrowsingContextHandleUserPrompt -> "browsingContext.handleUserPrompt"
  BrowsingContextLocateNodes -> "browsingContext.locateNodes"
  BrowsingContextNavigate -> "browsingContext.navigate"
  BrowsingContextPrint -> "browsingContext.print"
  BrowsingContextReload -> "browsingContext.reload"
  BrowsingContextSetViewport -> "browsingContext.setViewport"
  BrowsingContextTraverseHistory -> "browsingContext.traverseHistory"
  EmulationSetForcedColorsModeThemeOverride -> "emulation.setForcedColorsModeThemeOverride"
  EmulationSetGeolocationOverride -> "emulation.setGeolocationOverride"
  EmulationSetLocaleOverride -> "emulation.setLocaleOverride"
  EmulationSetNetworkConditions -> "emulation.setNetworkConditions"
  EmulationSetScreenOrientationOverride -> "emulation.setScreenOrientationOverride"
  EmulationSetScreenSettingsOverride -> "emulation.setScreenSettingsOverride"
  EmulationSetScriptingEnabled -> "emulation.setScriptingEnabled"
  EmulationSetTimezoneOverride -> "emulation.setTimezoneOverride"
  EmulationSetTouchOverride -> "emulation.setTouchOverride"
  EmulationSetUserAgentOverride -> "emulation.setUserAgentOverride"
  InputPerformActions -> "input.performActions"
  InputReleaseActions -> "input.releaseActions"
  InputSetFiles -> "input.setFiles"
  NetworkAddDataCollector -> "network.addDataCollector"
  NetworkAddIntercept -> "network.addIntercept"
  NetworkContinueRequest -> "network.continueRequest"
  NetworkContinueResponse -> "network.continueResponse"
  NetworkContinueWithAuth -> "network.continueWithAuth"
  NetworkDisownData -> "network.disownData"
  NetworkFailRequest -> "network.failRequest"
  NetworkGetData -> "network.getData"
  NetworkProvideResponse -> "network.provideResponse"
  NetworkRemoveDataCollector -> "network.removeDataCollector"
  NetworkRemoveIntercept -> "network.removeIntercept"
  NetworkSetCacheBehavior -> "network.setCacheBehavior"
  NetworkSetExtraHeaders -> "network.setExtraHeaders"
  ScriptAddPreloadScript -> "script.addPreloadScript"
  ScriptCallFunction -> "script.callFunction"
  ScriptDisown -> "script.disown"
  ScriptEvaluate -> "script.evaluate"
  ScriptGetRealms -> "script.getRealms"
  ScriptRemovePreloadScript -> "script.removePreloadScript"
  SessionEnd -> "session.end"
  SessionNew -> "session.new"
  SessionStatus -> "session.status"
  SessionSubscribe -> "session.subscribe"
  SessionUnsubscribe -> "session.unsubscribe"
  StorageDeleteCookies -> "storage.deleteCookies"
  StorageGetCookies -> "storage.getCookies"
  StorageSetCookie -> "storage.setCookie"
  WebExtensionInstall -> "webExtension.install"
  WebExtensionUninstall -> "webExtension.uninstall"

-- | Converts a 'CommandMethod' to its corresponding method name text.
toCommandText :: CommandMethod -> Text
toCommandText = \case
  KnownCommand k -> knownCommandToText k
  OffSpecCommand (MkOffSpecCommand t) -> t

instance FromJSON CommandMethod where
  parseJSON :: Value -> Parser CommandMethod
  parseJSON val =
    (KnownCommand <$> parseJSON @KnownCommand val)
      <|> (OffSpecCommand <$> parseJSON @OffSpecCommand val)
      <|> failConversion
    where
      failConversion =
        fail . unpack $
          "Unrecognised CommandMethodType: "
            <> jsonToText val
            <> " - "
            <> "Expected a string, the standard command types of whcih are: "
            <> " "
            <> (T.intercalate ", " $ knownCommandToText <$> enumerate @KnownCommand)