graphql-engine/server/src-test/Hasura/QuickCheck/Instances.hs

392 lines
13 KiB
Haskell
Raw Normal View History

{-# OPTIONS_GHC -fno-warn-orphans #-}
module Hasura.QuickCheck.Instances () where
-------------------------------------------------------------------------------
Avoid `Arrows` by interpreting monads TL;DR --- We go from this: ```haskell (| withRecordInconsistency ( (| modifyErrA ( do (info, dependencies) <- liftEitherA -< buildRelInfo relDef recordDependencies -< (metadataObject, schemaObject, dependencies) returnA -< info ) |) (addTableContext @b table . addRelationshipContext) ) |) metadataObject ``` to this: ```haskell withRecordInconsistencyM metadataObject $ do modifyErr (addTableContext @b table . addRelationshipContext) $ do (info, dependencies) <- liftEither $ buildRelInfo relDef recordDependenciesM metadataObject schemaObject dependencies return info ``` Background --- We use Haskell's `Arrows` language extension to gain some syntactic sugar when working with `Arrow`s. `Arrow`s are a programming abstraction comparable to `Monad`s. Unfortunately the syntactic sugar provided by this language extension is not very sweet. This PR shows how we can sometimes avoid using `Arrow`s altogether, without loss of functionality or correctness. It is a demo of a technique that can be used to cut down the amount of `Arrows`-based code in our codebase by about half. Approach --- Although _in general_ not every `Monad` is an `Arrow`, specific `Arrow` instantiations are exactly as powerful as their `Monad` equivalents. Otherwise they wouldn't be very equivalent, would they? Just like `liftEither` interprets the `Either e` monad into an arbitrary monad implementing `MonadError e`, we add `interpA` which interprets certain concrete monads such as `Writer w` into specific arrows, e.g. ones satisfying `ArrowWriter w`. This means that the part of the code that only uses such interpretable effects can be written _monadically_, and then used in _arrow_ constructions down the line. This approach cannot be used for arrow effects which do not have a monadic equivalent. In our codebase, the only instance of this is `ArrowCache m`, implemented by the `Rule m` arrow. So code written with `ArrowCache m` in the context cannot be rewritten monadically using this technique. See also --- - #1827 - #2210 PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3543 Co-authored-by: jkachmar <8461423+jkachmar@users.noreply.github.com> GitOrigin-RevId: eb79619c95f7a571bce99bc144ce42ee65d08505
2022-02-22 21:08:54 +03:00
import Data.Aeson.Types qualified as Aeson.Types
import Data.HashMap.Strict.Extended qualified as HashMap
import Data.HashMap.Strict.InsOrd qualified as InsOrd.HashMap
import Data.HashMap.Strict.Multi qualified as MMap
import Data.HashSet qualified as HashSet
import Data.Ratio ((%))
import Data.Set qualified as Set
import Data.Text qualified as T
import Data.Trie qualified as Trie
Avoid `Arrows` by interpreting monads TL;DR --- We go from this: ```haskell (| withRecordInconsistency ( (| modifyErrA ( do (info, dependencies) <- liftEitherA -< buildRelInfo relDef recordDependencies -< (metadataObject, schemaObject, dependencies) returnA -< info ) |) (addTableContext @b table . addRelationshipContext) ) |) metadataObject ``` to this: ```haskell withRecordInconsistencyM metadataObject $ do modifyErr (addTableContext @b table . addRelationshipContext) $ do (info, dependencies) <- liftEither $ buildRelInfo relDef recordDependenciesM metadataObject schemaObject dependencies return info ``` Background --- We use Haskell's `Arrows` language extension to gain some syntactic sugar when working with `Arrow`s. `Arrow`s are a programming abstraction comparable to `Monad`s. Unfortunately the syntactic sugar provided by this language extension is not very sweet. This PR shows how we can sometimes avoid using `Arrow`s altogether, without loss of functionality or correctness. It is a demo of a technique that can be used to cut down the amount of `Arrows`-based code in our codebase by about half. Approach --- Although _in general_ not every `Monad` is an `Arrow`, specific `Arrow` instantiations are exactly as powerful as their `Monad` equivalents. Otherwise they wouldn't be very equivalent, would they? Just like `liftEither` interprets the `Either e` monad into an arbitrary monad implementing `MonadError e`, we add `interpA` which interprets certain concrete monads such as `Writer w` into specific arrows, e.g. ones satisfying `ArrowWriter w`. This means that the part of the code that only uses such interpretable effects can be written _monadically_, and then used in _arrow_ constructions down the line. This approach cannot be used for arrow effects which do not have a monadic equivalent. In our codebase, the only instance of this is `ArrowCache m`, implemented by the `Rule m` arrow. So code written with `ArrowCache m` in the context cannot be rewritten monadically using this technique. See also --- - #1827 - #2210 PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3543 Co-authored-by: jkachmar <8461423+jkachmar@users.noreply.github.com> GitOrigin-RevId: eb79619c95f7a571bce99bc144ce42ee65d08505
2022-02-22 21:08:54 +03:00
import Hasura.Base.Error (QErr (..), QErrExtra (..))
import Hasura.Base.Error qualified as Error
import Hasura.GraphQL.Namespace (NamespacedField (..), namespacedField)
import Hasura.Prelude
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.Endpoint.Trie
Avoid `Arrows` by interpreting monads TL;DR --- We go from this: ```haskell (| withRecordInconsistency ( (| modifyErrA ( do (info, dependencies) <- liftEitherA -< buildRelInfo relDef recordDependencies -< (metadataObject, schemaObject, dependencies) returnA -< info ) |) (addTableContext @b table . addRelationshipContext) ) |) metadataObject ``` to this: ```haskell withRecordInconsistencyM metadataObject $ do modifyErr (addTableContext @b table . addRelationshipContext) $ do (info, dependencies) <- liftEither $ buildRelInfo relDef recordDependenciesM metadataObject schemaObject dependencies return info ``` Background --- We use Haskell's `Arrows` language extension to gain some syntactic sugar when working with `Arrow`s. `Arrow`s are a programming abstraction comparable to `Monad`s. Unfortunately the syntactic sugar provided by this language extension is not very sweet. This PR shows how we can sometimes avoid using `Arrow`s altogether, without loss of functionality or correctness. It is a demo of a technique that can be used to cut down the amount of `Arrows`-based code in our codebase by about half. Approach --- Although _in general_ not every `Monad` is an `Arrow`, specific `Arrow` instantiations are exactly as powerful as their `Monad` equivalents. Otherwise they wouldn't be very equivalent, would they? Just like `liftEither` interprets the `Either e` monad into an arbitrary monad implementing `MonadError e`, we add `interpA` which interprets certain concrete monads such as `Writer w` into specific arrows, e.g. ones satisfying `ArrowWriter w`. This means that the part of the code that only uses such interpretable effects can be written _monadically_, and then used in _arrow_ constructions down the line. This approach cannot be used for arrow effects which do not have a monadic equivalent. In our codebase, the only instance of this is `ArrowCache m`, implemented by the `Rule m` arrow. So code written with `ArrowCache m` in the context cannot be rewritten monadically using this technique. See also --- - #1827 - #2210 PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3543 Co-authored-by: jkachmar <8461423+jkachmar@users.noreply.github.com> GitOrigin-RevId: eb79619c95f7a571bce99bc144ce42ee65d08505
2022-02-22 21:08:54 +03:00
import Hasura.RQL.Types.Metadata.Object
( MetadataObjId (..),
MetadataObject (..),
)
import Hasura.RQL.Types.RemoteSchema
( RemoteSchemaInputValueDefinition (..),
RemoteSchemaIntrospection (..),
Avoid `Arrows` by interpreting monads TL;DR --- We go from this: ```haskell (| withRecordInconsistency ( (| modifyErrA ( do (info, dependencies) <- liftEitherA -< buildRelInfo relDef recordDependencies -< (metadataObject, schemaObject, dependencies) returnA -< info ) |) (addTableContext @b table . addRelationshipContext) ) |) metadataObject ``` to this: ```haskell withRecordInconsistencyM metadataObject $ do modifyErr (addTableContext @b table . addRelationshipContext) $ do (info, dependencies) <- liftEither $ buildRelInfo relDef recordDependenciesM metadataObject schemaObject dependencies return info ``` Background --- We use Haskell's `Arrows` language extension to gain some syntactic sugar when working with `Arrow`s. `Arrow`s are a programming abstraction comparable to `Monad`s. Unfortunately the syntactic sugar provided by this language extension is not very sweet. This PR shows how we can sometimes avoid using `Arrow`s altogether, without loss of functionality or correctness. It is a demo of a technique that can be used to cut down the amount of `Arrows`-based code in our codebase by about half. Approach --- Although _in general_ not every `Monad` is an `Arrow`, specific `Arrow` instantiations are exactly as powerful as their `Monad` equivalents. Otherwise they wouldn't be very equivalent, would they? Just like `liftEither` interprets the `Either e` monad into an arbitrary monad implementing `MonadError e`, we add `interpA` which interprets certain concrete monads such as `Writer w` into specific arrows, e.g. ones satisfying `ArrowWriter w`. This means that the part of the code that only uses such interpretable effects can be written _monadically_, and then used in _arrow_ constructions down the line. This approach cannot be used for arrow effects which do not have a monadic equivalent. In our codebase, the only instance of this is `ArrowCache m`, implemented by the `Rule m` arrow. So code written with `ArrowCache m` in the context cannot be rewritten monadically using this technique. See also --- - #1827 - #2210 PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3543 Co-authored-by: jkachmar <8461423+jkachmar@users.noreply.github.com> GitOrigin-RevId: eb79619c95f7a571bce99bc144ce42ee65d08505
2022-02-22 21:08:54 +03:00
RemoteSchemaName (..),
getTypeName,
)
import Hasura.RQL.Types.SchemaCache
import Hasura.RQL.Types.Table
import Hasura.Server.Utils qualified as Utils
import Hasura.Session (SessionVariable, mkSessionVariable)
import Language.GraphQL.Draft.Syntax qualified as GraphQL
Avoid `Arrows` by interpreting monads TL;DR --- We go from this: ```haskell (| withRecordInconsistency ( (| modifyErrA ( do (info, dependencies) <- liftEitherA -< buildRelInfo relDef recordDependencies -< (metadataObject, schemaObject, dependencies) returnA -< info ) |) (addTableContext @b table . addRelationshipContext) ) |) metadataObject ``` to this: ```haskell withRecordInconsistencyM metadataObject $ do modifyErr (addTableContext @b table . addRelationshipContext) $ do (info, dependencies) <- liftEither $ buildRelInfo relDef recordDependenciesM metadataObject schemaObject dependencies return info ``` Background --- We use Haskell's `Arrows` language extension to gain some syntactic sugar when working with `Arrow`s. `Arrow`s are a programming abstraction comparable to `Monad`s. Unfortunately the syntactic sugar provided by this language extension is not very sweet. This PR shows how we can sometimes avoid using `Arrow`s altogether, without loss of functionality or correctness. It is a demo of a technique that can be used to cut down the amount of `Arrows`-based code in our codebase by about half. Approach --- Although _in general_ not every `Monad` is an `Arrow`, specific `Arrow` instantiations are exactly as powerful as their `Monad` equivalents. Otherwise they wouldn't be very equivalent, would they? Just like `liftEither` interprets the `Either e` monad into an arbitrary monad implementing `MonadError e`, we add `interpA` which interprets certain concrete monads such as `Writer w` into specific arrows, e.g. ones satisfying `ArrowWriter w`. This means that the part of the code that only uses such interpretable effects can be written _monadically_, and then used in _arrow_ constructions down the line. This approach cannot be used for arrow effects which do not have a monadic equivalent. In our codebase, the only instance of this is `ArrowCache m`, implemented by the `Rule m` arrow. So code written with `ArrowCache m` in the context cannot be rewritten monadically using this technique. See also --- - #1827 - #2210 PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3543 Co-authored-by: jkachmar <8461423+jkachmar@users.noreply.github.com> GitOrigin-RevId: eb79619c95f7a571bce99bc144ce42ee65d08505
2022-02-22 21:08:54 +03:00
import Network.HTTP.Types qualified as HTTP.Types
import Test.QuickCheck.Extended
-------------------------------------------------------------------------------
-- Orphan instances for third-party libraries types
instance Arbitrary Text where
Update GraphQL Parser version to fix text encoding issue (fix #1965) ### A long tale about encoding GraphQL has an [introspection system](http://spec.graphql.org/June2018/#sec-Introspection), which allows its schema to be introspected. This is what we use to introspect [remote schemas](https://github.com/hasura/graphql-engine-mono/blob/41383e1f88c709c6cae4059a1b4fb8f2a58259e6/server/src-rsr/introspection.json). There is one place in the introspection where we might find GraphQL values: the default value of an argument. ```json { "fields": [ { "name": "echo", "args": [ { "name": "msg", "defaultValue": "\"Hello\\nWorld!\"" } ] } ] } ``` Note that GraphQL's introspection is transport agnostic: the default value isn't returned as a JSON value, but as a _string-encoded GraphQL Value_. In this case, the value is the GraphQL String `"Hello\nWorld!"`. Embedded into a string, it is encoded as: `"\"Hello\\nWorld!\""`. When we [parse that value](https://github.com/hasura/graphql-engine-mono/blob/41383e1f88c709c6cae4059a1b4fb8f2a58259e6/server/src-lib/Hasura/GraphQL/RemoteServer.hs#L351), we first extract that JSON string, to get its content, `"Hello\nWorld!"`, then use our [GraphQL Parser library](https://github.com/hasura/graphql-parser-hs/blob/21c1ddfb41791578b66633a2e51f9deb43761108/src/Language/GraphQL/Draft/Parser.hs#L200) to interpret this: we find the double quote, understand that the content is a String, unescape the backslashes, and end up with the desired string value: `['H', 'e', 'l', 'l', 'o', '\n', 'W', 'o', 'r', 'l', 'd', '!']`. This all works fine. However, there was a bug in the _printer_ part of our parser library: when printing back a String value, we would not re-escape characters properly. In practice, this meant that the GraphQL String `"Hello\nWorld"` would be encoded in JSON as `"\"Hello\nWorld!\""`. Note how the `\n` is not properly double-escaped. This led to a variety of problems, as described in #1965: - we would successfully parse a remote schema containing such characters in its default values, but then would print those erroneous JSON values in our introspection, which would _crash the console_ - we would inject those default values in queries sent to remote schemas, and print them wrong doing so, sending invalid values to remote schemas and getting errors in result It turns out that this bug had been lurking in the code for a long time: I combed through the history of [the parser library](https://github.com/hasura/graphql-parser-hs), and as far as I can tell, this bug has always been there. So why was it never caught? After all, we do have [round trip tests](https://github.com/hasura/graphql-parser-hs/blob/21c1ddfb41791578b66633a2e51f9deb43761108/test/Spec.hs#L52) that print + parse arbitrary values and check that we get the same value as a result. They do use any arbitrary unicode character in their generated strings. So... that should have covered it, right? Well... it turns out that [the tests were ignoring errors](https://github.com/hasura/graphql-parser-hs/blob/7678066c49b61acf0c102a0ffe48e86897e2e22d/test/Spec.hs#L45), and would always return "SUCCESS" in CI, even if they failed... Furthermore, the sample size was small enough that, most of the time, _they would not hit such characters_. Running the tests locally on a loop, I only got errors ~10% of the time... This was all fixed in hasura/graphql-parser-hs#44. This was probably one of Hasura's longest standing bugs? ^^' ### Description This PR bumps the version of graphql-parser-hs in the engine, and switches some of our own arbitrary tests to use unicode characters in text rather than alphanumeric values. It turns out those tests were much better at hitting "bad" values, and that they consistently failed when generating arbitrary unicode characters. https://github.com/hasura/graphql-engine-mono/pull/2031 GitOrigin-RevId: 54fa48270386a67336e5544351691619e0684559
2021-08-06 14:53:52 +03:00
arbitrary = T.pack <$> listOf arbitraryUnicodeChar
instance
(Arbitrary k, Eq k, Hashable k, Arbitrary v) =>
Arbitrary (HashMap k v)
where
arbitrary = HashMap.fromList <$> arbitrary
shrink = fmap HashMap.fromList . shrink . HashMap.toList
instance
(Arbitrary k, Eq k, Hashable k, Arbitrary v) =>
Arbitrary (InsOrdHashMap k v)
where
arbitrary = InsOrd.HashMap.fromList <$> arbitrary
shrink = fmap InsOrd.HashMap.fromList . shrink . InsOrd.HashMap.toList
Avoid `Arrows` by interpreting monads TL;DR --- We go from this: ```haskell (| withRecordInconsistency ( (| modifyErrA ( do (info, dependencies) <- liftEitherA -< buildRelInfo relDef recordDependencies -< (metadataObject, schemaObject, dependencies) returnA -< info ) |) (addTableContext @b table . addRelationshipContext) ) |) metadataObject ``` to this: ```haskell withRecordInconsistencyM metadataObject $ do modifyErr (addTableContext @b table . addRelationshipContext) $ do (info, dependencies) <- liftEither $ buildRelInfo relDef recordDependenciesM metadataObject schemaObject dependencies return info ``` Background --- We use Haskell's `Arrows` language extension to gain some syntactic sugar when working with `Arrow`s. `Arrow`s are a programming abstraction comparable to `Monad`s. Unfortunately the syntactic sugar provided by this language extension is not very sweet. This PR shows how we can sometimes avoid using `Arrow`s altogether, without loss of functionality or correctness. It is a demo of a technique that can be used to cut down the amount of `Arrows`-based code in our codebase by about half. Approach --- Although _in general_ not every `Monad` is an `Arrow`, specific `Arrow` instantiations are exactly as powerful as their `Monad` equivalents. Otherwise they wouldn't be very equivalent, would they? Just like `liftEither` interprets the `Either e` monad into an arbitrary monad implementing `MonadError e`, we add `interpA` which interprets certain concrete monads such as `Writer w` into specific arrows, e.g. ones satisfying `ArrowWriter w`. This means that the part of the code that only uses such interpretable effects can be written _monadically_, and then used in _arrow_ constructions down the line. This approach cannot be used for arrow effects which do not have a monadic equivalent. In our codebase, the only instance of this is `ArrowCache m`, implemented by the `Rule m` arrow. So code written with `ArrowCache m` in the context cannot be rewritten monadically using this technique. See also --- - #1827 - #2210 PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3543 Co-authored-by: jkachmar <8461423+jkachmar@users.noreply.github.com> GitOrigin-RevId: eb79619c95f7a571bce99bc144ce42ee65d08505
2022-02-22 21:08:54 +03:00
instance Arbitrary Aeson.Types.JSONPathElement where
arbitrary = Aeson.Types.Index <$> arbitrary
instance Arbitrary HTTP.Types.Status where
arbitrary = HTTP.Types.Status <$> arbitrary <*> pure mempty
-------------------------------------------------------------------------------
-- Orphan instances for types defined by us, but which are not coupled to
-- GraphQL Engine.
instance (Eq k, Hashable k, Arbitrary k, Eq v, Arbitrary v) => Arbitrary (Trie.Trie k v) where
arbitrary = Trie.Trie <$> scale (`div` 2) arbitrary <*> arbitrary
shrink (Trie.Trie m v) =
[Trie.Trie m v' | v' <- shrink v]
++ [Trie.Trie m' v | m' <- shrink m]
instance (Eq k, Hashable k, Arbitrary k, Ord v, Arbitrary v) => Arbitrary (MMap.MultiMap k v) where
arbitrary = MMap.fromMap . fmap (Set.fromList . take 5) <$> arbitrary
shrink m = map MMap.fromMap $ shrink $ MMap.toMap m
-------------------------------------------------------------------------------
-- Orphan instances for Language.GraphQL.Draft.Syntax types
--
-- TODO: We control `graphql-parser-hs`; we should upstream these orphan
-- instances as either a separate package (e.g. `graphql-parser-hs-quickcheck`)
-- or via flag (disabled by default) which enables QuickCheck as a dependency
-- and supplies (non-orphan) instances that way.
{-# ANN arbitraryGraphQLName ("HLint: ignore Use mkName" :: String) #-}
-- Factored out so that we can annotate it.
arbitraryGraphQLName :: Gen GraphQL.Name
arbitraryGraphQLName = GraphQL.unsafeMkName . T.pack <$> listOf1 (elements ['a' .. 'z'])
instance Arbitrary GraphQL.Name where
arbitrary = arbitraryGraphQLName
instance Arbitrary GraphQL.Description where
arbitrary = GraphQL.Description <$> arbitrary
instance Arbitrary GraphQL.EnumValue where
arbitrary = GraphQL.EnumValue <$> arbitrary
instance Arbitrary GraphQL.EnumValueDefinition where
arbitrary =
GraphQL.EnumValueDefinition
<$> arbitrary
<*> arbitrary
<*> pure []
instance Arbitrary GraphQL.Nullability where
arbitrary = GraphQL.Nullability <$> arbitrary
instance Arbitrary (GraphQL.Value Void) where
arbitrary =
oneof
[ pure GraphQL.VNull,
GraphQL.VInt <$> arbitrary,
GraphQL.VFloat <$> arbitraryScientific,
GraphQL.VString <$> arbitrary,
GraphQL.VBoolean <$> arbitrary,
GraphQL.VEnum <$> arbitrary,
-- reduce the internal size factor at every level, so that this
-- recursion is guaranteed to terminate
GraphQL.VList <$> scale (`div` 2) arbitrary,
GraphQL.VObject <$> scale (`div` 2) arbitrary
]
where
arbitraryScientific = do
-- fromRational can create invalid repeating values that loop forever
-- we avoid this by creating known good ratios
num :: Integer <- arbitrary
dem :: Integer <- elements [1 .. 32]
pure $ fromRational $ num % (10 ^ dem)
-- Generators for Language.GraphQL.Draft.Syntax types
genGType :: [GraphQL.Name] -> Gen GraphQL.GType
genGType typeNames =
frequency
-- bias towards avoiding deeply nested lists
[ (7, GraphQL.TypeNamed <$> arbitrary <*> elements typeNames),
(3, GraphQL.TypeList <$> arbitrary <*> genGType typeNames)
]
genInputValueDefinition :: [GraphQL.Name] -> Gen GraphQL.InputValueDefinition
genInputValueDefinition inputTypeNames =
GraphQL.InputValueDefinition
<$> arbitrary
<*> arbitrary
<*> genGType inputTypeNames
<*> arbitrary
<*> pure []
genScalarTypeDefinition :: GraphQL.Name -> Gen GraphQL.ScalarTypeDefinition
genScalarTypeDefinition name =
GraphQL.ScalarTypeDefinition
<$> arbitrary
<*> pure name
<*> pure []
genEnumTypeDefinition :: GraphQL.Name -> Gen GraphQL.EnumTypeDefinition
genEnumTypeDefinition name =
GraphQL.EnumTypeDefinition
<$> arbitrary
<*> pure name
<*> pure []
<*> listOf1 arbitrary
genUnionTypeDefinition ::
[GraphQL.Name] -> GraphQL.Name -> Gen GraphQL.UnionTypeDefinition
genUnionTypeDefinition objectTypeNames name =
GraphQL.UnionTypeDefinition
<$> arbitrary
<*> pure name
<*> pure []
<*> sublistOf1 objectTypeNames
genFieldDefinition ::
Gen [inputType] ->
[GraphQL.Name] ->
GraphQL.Name ->
Gen (GraphQL.FieldDefinition inputType)
genFieldDefinition inputTypes outputTypeNames name =
GraphQL.FieldDefinition
<$> arbitrary
<*> pure name
<*> inputTypes
<*> genGType outputTypeNames
<*> pure []
genObjectTypeDefinition ::
Gen [inputType] ->
[GraphQL.Name] ->
[GraphQL.Name] ->
GraphQL.Name ->
Gen (GraphQL.ObjectTypeDefinition inputType)
genObjectTypeDefinition inputTypes outputTypeNames interfaceTypeNames name =
GraphQL.ObjectTypeDefinition
<$> arbitrary
<*> pure name
<*> sublistOf interfaceTypeNames
<*> pure []
<*> fields
where
fields = distinct1 >>= traverse (genFieldDefinition inputTypes outputTypeNames)
genInterfaceTypeDefinition ::
Arbitrary possibleType =>
Gen [inputType] ->
[GraphQL.Name] ->
GraphQL.Name ->
Gen (GraphQL.InterfaceTypeDefinition [possibleType] inputType)
genInterfaceTypeDefinition inputTypes outputTypeNames name =
GraphQL.InterfaceTypeDefinition
<$> arbitrary
<*> pure name
<*> pure []
<*> fields
<*> listOf1 arbitrary
where
fields = distinct1 >>= traverse (genFieldDefinition inputTypes outputTypeNames)
genInputObjectTypeDefinition ::
Gen [inputType] ->
GraphQL.Name ->
Gen (GraphQL.InputObjectTypeDefinition inputType)
genInputObjectTypeDefinition values name =
GraphQL.InputObjectTypeDefinition
<$> arbitrary
<*> pure name
<*> pure []
<*> values
-------------------------------------------------------------------------------
-- Instances for GraphQL Engine types
instance Arbitrary a => Arbitrary (PathComponent a) where
arbitrary =
oneof
[ PathLiteral <$> arbitrary,
pure PathParam
]
instance Arbitrary SessionVariable where
arbitrary = do
name <- arbitrary
pure $ mkSessionVariable $ Utils.sessionVariablePrefix <> name
instance Arbitrary IntrospectionResult where
arbitrary = do
-- first, generate distinct names for each kind of object
scalarTypeNames <- distinct
objectTypeNames <- distinctExcluding1 scalarTypeNames
interfaceTypeNames <- distinctExcluding $ scalarTypeNames ++ objectTypeNames
unionTypeNames <- distinctExcluding $ scalarTypeNames ++ objectTypeNames ++ interfaceTypeNames
enumTypeNames <- distinctExcluding $ scalarTypeNames ++ objectTypeNames ++ interfaceTypeNames ++ unionTypeNames
let outputTypeNames = scalarTypeNames ++ objectTypeNames ++ interfaceTypeNames ++ unionTypeNames ++ enumTypeNames
inputObjectTypeNames <- distinctExcluding outputTypeNames
let inputTypeNames = scalarTypeNames ++ enumTypeNames ++ inputObjectTypeNames
let inputValues = case inputTypeNames of
[] -> pure []
_ -> listOf $ genRemoteSchemaInputValueDefinition inputTypeNames
-- then, create a matching definition for each name
scalarTypeDefinitions <-
for scalarTypeNames genScalarTypeDefinition
objectTypeDefinitions <-
for objectTypeNames $
genObjectTypeDefinition inputValues outputTypeNames interfaceTypeNames
interfaceTypeDefinitions <-
for interfaceTypeNames $
genInterfaceTypeDefinition inputValues outputTypeNames
unionTypeDefinitions <-
for unionTypeNames $
genUnionTypeDefinition objectTypeNames
enumTypeDefinitions <-
for enumTypeNames genEnumTypeDefinition
inputObjectTypeDefinitions <-
for inputObjectTypeNames $
genInputObjectTypeDefinition inputValues
-- finally, create an IntrospectionResult from the aggregated definitions
let irDoc =
RemoteSchemaIntrospection $
HashMap.fromListOn getTypeName $
concat
[ GraphQL.TypeDefinitionScalar <$> scalarTypeDefinitions,
GraphQL.TypeDefinitionObject <$> objectTypeDefinitions,
GraphQL.TypeDefinitionInterface <$> interfaceTypeDefinitions,
GraphQL.TypeDefinitionUnion <$> unionTypeDefinitions,
GraphQL.TypeDefinitionEnum <$> enumTypeDefinitions,
GraphQL.TypeDefinitionInputObject <$> inputObjectTypeDefinitions
]
irQueryRoot <- elements objectTypeNames
let maybeObjectTypeName = elements $ Nothing : (Just <$> objectTypeNames)
irMutationRoot <- maybeObjectTypeName
irSubscriptionRoot <- maybeObjectTypeName
pure $ IntrospectionResult {..}
instance Arbitrary a => Arbitrary (NamespacedField a) where
arbitrary = oneof [NotNamespaced <$> arbitrary, Namespaced <$> arbitrary]
shrink = namespacedField (fmap NotNamespaced . shrink) (fmap Namespaced . shrink)
Avoid `Arrows` by interpreting monads TL;DR --- We go from this: ```haskell (| withRecordInconsistency ( (| modifyErrA ( do (info, dependencies) <- liftEitherA -< buildRelInfo relDef recordDependencies -< (metadataObject, schemaObject, dependencies) returnA -< info ) |) (addTableContext @b table . addRelationshipContext) ) |) metadataObject ``` to this: ```haskell withRecordInconsistencyM metadataObject $ do modifyErr (addTableContext @b table . addRelationshipContext) $ do (info, dependencies) <- liftEither $ buildRelInfo relDef recordDependenciesM metadataObject schemaObject dependencies return info ``` Background --- We use Haskell's `Arrows` language extension to gain some syntactic sugar when working with `Arrow`s. `Arrow`s are a programming abstraction comparable to `Monad`s. Unfortunately the syntactic sugar provided by this language extension is not very sweet. This PR shows how we can sometimes avoid using `Arrow`s altogether, without loss of functionality or correctness. It is a demo of a technique that can be used to cut down the amount of `Arrows`-based code in our codebase by about half. Approach --- Although _in general_ not every `Monad` is an `Arrow`, specific `Arrow` instantiations are exactly as powerful as their `Monad` equivalents. Otherwise they wouldn't be very equivalent, would they? Just like `liftEither` interprets the `Either e` monad into an arbitrary monad implementing `MonadError e`, we add `interpA` which interprets certain concrete monads such as `Writer w` into specific arrows, e.g. ones satisfying `ArrowWriter w`. This means that the part of the code that only uses such interpretable effects can be written _monadically_, and then used in _arrow_ constructions down the line. This approach cannot be used for arrow effects which do not have a monadic equivalent. In our codebase, the only instance of this is `ArrowCache m`, implemented by the `Rule m` arrow. So code written with `ArrowCache m` in the context cannot be rewritten monadically using this technique. See also --- - #1827 - #2210 PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3543 Co-authored-by: jkachmar <8461423+jkachmar@users.noreply.github.com> GitOrigin-RevId: eb79619c95f7a571bce99bc144ce42ee65d08505
2022-02-22 21:08:54 +03:00
instance Arbitrary QErrExtra where
arbitrary =
oneof
[ ExtraExtensions <$> arbitrary,
ExtraInternal <$> arbitrary
]
instance Arbitrary MetadataObjId where
arbitrary =
oneof
-- This is not exhaustive, because it wasn't needed.
[ pure $ MOSource SNDefault,
MORemoteSchema . RemoteSchemaName <$> arbitrary
]
instance Arbitrary MetadataObject where
arbitrary = MetadataObject <$> arbitrary <*> arbitrary
instance Arbitrary QErr where
arbitrary = do
-- This is not exhaustive, because it wasn't needed.
--
-- I just picked a few random error codes.
let genCode =
elements
[ Error.AlreadyExists,
Error.Conflict,
Error.ConstraintError,
Error.ConstraintViolation,
Error.NotFound,
Error.Unexpected
]
QErr
<$> arbitrary
<*> arbitrary
<*> arbitrary
<*> genCode
<*> arbitrary
instance Arbitrary Comment where
arbitrary =
oneof
[ pure Automatic,
Explicit <$> arbitrary
]
shrink Automatic = []
shrink (Explicit t) = Explicit <$> shrink t
instance Arbitrary CustomRootField where
arbitrary = CustomRootField <$> arbitrary <*> arbitrary
instance Arbitrary TableCustomRootFields where
arbitrary =
( TableCustomRootFields
<$> arbitrary
<*> arbitrary
<*> arbitrary
<*> arbitrary
<*> arbitrary
<*> arbitrary
<*> arbitrary
<*> arbitrary
<*> arbitrary
<*> arbitrary
<*> arbitrary
)
`suchThat` allFieldNamesAreUnique
where
allFieldNamesAreUnique :: TableCustomRootFields -> Bool
allFieldNamesAreUnique tcrf =
let allNames = mapMaybe _crfName $ getAllCustomRootFields tcrf
uniqueNames = HashSet.fromList allNames
in length allNames == length uniqueNames
instance Arbitrary ColumnConfig where
arbitrary =
ColumnConfig
<$> arbitrary
<*> arbitrary
-- Generators for GraphQL Engine types
genRemoteSchemaInputValueDefinition ::
[GraphQL.Name] -> Gen RemoteSchemaInputValueDefinition
genRemoteSchemaInputValueDefinition inputTypeNames =
RemoteSchemaInputValueDefinition
<$> genInputValueDefinition inputTypeNames
<*> pure Nothing