graphql-engine/server/tests-dc-api/Command.hs
David Overton 5c7489cc23 Flesh out capabilities types
>

## Description ✍️
->

Fleshes out the Capabilities types used by Data Connector agents.

### Related Issues ✍
->

https://hasurahq.atlassian.net/browse/GDW-85

### Solution and Design ✍
>

Capabilities are divided into sections for queries, mutation, subscriptions, filtering and relationships:
```haskell
data Capabilities = Capabilities
  { cQueries :: Maybe QueryCapabilities,
    cMutations :: Maybe MutationCapabilities,
    cSubscriptions :: Maybe SubscriptionCapabilities,
    cFiltering :: Maybe FilteringCapabilities,
    cRelationships :: Maybe RelationshipCapabilities
  }
```

Each section is optional. If the section is absent it means the the related capabilities are not supported by the agent.
The types for each section can contain additional details about exactly what is supported. E.g.
```haskell
data QueryCapabilities = QueryCapabilities
  { qcSupportsPrimaryKeys :: Bool
  }
```

These are currently mostly empty, but will be filled in later.

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4671
GitOrigin-RevId: 3d48570266bfce5e177a543a0ed6f63a7b450f0b
2022-06-16 08:31:07 +00:00

210 lines
6.5 KiB
Haskell

{-# LANGUAGE TemplateHaskell #-}
module Command
( Command (..),
TestOptions (..),
AgentCapabilities (..),
parseCommandLine,
)
where
import Control.Arrow (left)
import Control.Lens (contains, modifying, use, (^.), _2)
import Control.Lens.TH (makeLenses)
import Control.Monad (when)
import Control.Monad.State (State, runState)
import Data.Aeson (eitherDecodeStrict')
import Data.HashSet (HashSet)
import Data.HashSet qualified as HashSet
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Text
import Data.Version (showVersion)
import Hasura.Backends.DataConnector.API qualified as API
import Options.Applicative
import Paths_graphql_engine qualified as PackageInfo
import Servant.Client (BaseUrl, parseBaseUrl)
import Prelude
data Command
= Test TestOptions
| ExportOpenAPISpec
data TestOptions = TestOptions
{ _toAgentBaseUrl :: BaseUrl,
_toAgentConfig :: API.Config,
_toAgentCapabilities :: AgentCapabilities,
_toParallelDegree :: Maybe Int,
_toMatch :: Maybe String,
_toSkip :: Maybe String
}
data AgentCapabilities
= AutoDetect
| Explicit API.Capabilities
data CapabilitiesState = CapabilitiesState
{ _csRemainingCapabilities :: HashSet Text,
_csCapabilitiesEnquired :: HashSet Text
}
$(makeLenses ''CapabilitiesState)
parseCommandLine :: IO Command
parseCommandLine =
execParser $
info
(helper <*> version <*> commandParser)
( fullDesc
<> header "Hasura Data Connector Agent Test Utility"
)
version :: Parser (a -> a)
version =
infoOption
displayText
( long "version"
<> short 'v'
<> help "Prints the version of the application and quits"
<> hidden
)
where
displayText = "Version " <> showVersion PackageInfo.version
commandParser :: Parser Command
commandParser =
subparser
(testCommand <> exportOpenApiSpecCommand)
where
testCommand =
command
"test"
( info
(helper <*> testCommandParser)
(progDesc "Executes a suite of tests against an agent to ensure its correct function")
)
exportOpenApiSpecCommand =
command
"export-openapi-spec"
( info
(helper <*> pure ExportOpenAPISpec)
(progDesc "Exports the OpenAPI specification of the Data Connector API that agents must implement")
)
testOptionsParser :: Parser TestOptions
testOptionsParser =
TestOptions
<$> option
baseUrl
( long "agent-base-url"
<> short 'u'
<> metavar "URL"
<> help "The base URL of the Data Connector agent to be tested"
)
<*> option
configValue
( long "agent-config"
<> short 's'
<> metavar "JSON"
<> help "The configuration JSON to be sent to the agent via the X-Hasura-DataConnector-Config header"
)
<*> agentCapabilitiesParser
<*> optional
( option
positiveNonZeroInt
( long "jobs"
<> short 'j'
<> metavar "INT"
<> help "Run at most N parallelizable tests simultaneously (default: number of available processors)"
)
)
<*> optional
( strOption
( long "match"
<> short 'm'
<> metavar "PATTERN"
<> help "Only run tests that match given PATTERN"
)
)
<*> optional
( option
auto
( long "skip"
<> short 's'
<> metavar "PATTERN"
<> help "Skip tests that match given PATTERN"
)
)
testCommandParser :: Parser Command
testCommandParser = Test <$> testOptionsParser
baseUrl :: ReadM BaseUrl
baseUrl = eitherReader $ left show . parseBaseUrl
positiveNonZeroInt :: ReadM Int
positiveNonZeroInt =
auto >>= \int ->
if int <= 0 then readerError "Must be a positive, non-zero integer" else pure int
configValue :: ReadM API.Config
configValue = eitherReader $ (fmap API.Config . eitherDecodeStrict' . Text.encodeUtf8 . Text.pack)
agentCapabilitiesParser :: Parser AgentCapabilities
agentCapabilitiesParser =
option
agentCapabilities
( long "capabilities"
<> short 'c'
<> metavar "CAPABILITIES"
<> value AutoDetect
<> help (Text.unpack helpText)
)
where
helpText =
"The capabilities that the agent has, to determine what tests to run. By default, they will be autodetected. The valid capabilities are: " <> allCapabilitiesText
allCapabilitiesText =
"[autodetect | none | " <> Text.intercalate "," (HashSet.toList allPossibleCapabilities) <> "]"
agentCapabilities :: ReadM AgentCapabilities
agentCapabilities =
str >>= \text -> do
let capabilities = HashSet.fromList $ Text.strip <$> Text.split (== ',') text
if HashSet.member "autodetect" capabilities
then
if HashSet.size capabilities == 1
then pure AutoDetect
else readerError "You can either autodetect capabilities or specify them manually, not both"
else
if HashSet.member "none" capabilities
then
if HashSet.size capabilities == 1
then pure . Explicit . fst $ readCapabilities mempty
else readerError "You cannot specify other capabilities when specifying none"
else Explicit <$> readExplicitCapabilities capabilities
where
readExplicitCapabilities :: HashSet Text -> ReadM API.Capabilities
readExplicitCapabilities providedCapabilities =
let (capabilities, CapabilitiesState {..}) = readCapabilities providedCapabilities
in if _csRemainingCapabilities /= mempty
then readerError . Text.unpack $ "Unknown capabilities: " <> Text.intercalate "," (HashSet.toList _csRemainingCapabilities)
else pure capabilities
readCapabilities :: HashSet Text -> (API.Capabilities, CapabilitiesState)
readCapabilities providedCapabilities =
flip runState (CapabilitiesState providedCapabilities mempty) $ do
supportsRelationships <- readCapability "relationships"
pure $ API.emptyCapabilities {API.cRelationships = if supportsRelationships then Just API.RelationshipCapabilities {} else Nothing}
readCapability :: Text -> State CapabilitiesState Bool
readCapability capability = do
modifying csCapabilitiesEnquired $ HashSet.insert capability
hasCapability <- use $ csRemainingCapabilities . contains capability
when hasCapability $
modifying csRemainingCapabilities $ HashSet.delete capability
pure hasCapability
allPossibleCapabilities :: HashSet Text
allPossibleCapabilities =
readCapabilities mempty ^. _2 . csCapabilitiesEnquired