2020-06-06 21:22:11 +02:00
|
|
|
{-# LANGUAGE ExplicitForAll #-}
|
2020-05-21 10:20:59 +02:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
|
|
|
|
-- | Types and functions used for input and result coercion.
|
|
|
|
module Language.GraphQL.Execute.Coerce
|
|
|
|
( VariableValue(..)
|
2020-06-06 21:22:11 +02:00
|
|
|
, coerceInputLiteral
|
|
|
|
, matchFieldValues
|
2020-05-21 10:20:59 +02:00
|
|
|
) where
|
|
|
|
|
|
|
|
import qualified Data.Aeson as Aeson
|
2020-05-22 10:11:48 +02:00
|
|
|
import Data.HashMap.Strict (HashMap)
|
2020-05-21 10:20:59 +02:00
|
|
|
import qualified Data.HashMap.Strict as HashMap
|
2020-05-22 10:11:48 +02:00
|
|
|
import qualified Data.Text.Lazy as Text.Lazy
|
|
|
|
import qualified Data.Text.Lazy.Builder as Text.Builder
|
|
|
|
import qualified Data.Text.Lazy.Builder.Int as Text.Builder
|
2020-05-21 10:20:59 +02:00
|
|
|
import Data.Scientific (toBoundedInteger, toRealFloat)
|
2020-06-07 06:16:45 +02:00
|
|
|
import Language.GraphQL.AST (Name)
|
2020-05-24 13:51:00 +02:00
|
|
|
import qualified Language.GraphQL.Type.In as In
|
2020-05-21 10:20:59 +02:00
|
|
|
import Language.GraphQL.Type.Definition
|
|
|
|
|
|
|
|
-- | Since variables are passed separately from the query, in an independent
|
|
|
|
-- format, they should be first coerced to the internal representation used by
|
|
|
|
-- this implementation.
|
|
|
|
class VariableValue a where
|
|
|
|
-- | Only a basic, format-specific, coercion must be done here. Type
|
|
|
|
-- correctness or nullability shouldn't be validated here, they will be
|
|
|
|
-- validated later. The type information is provided only as a hint.
|
|
|
|
--
|
|
|
|
-- For example @GraphQL@ prohibits the coercion from a 't:Float' to an
|
|
|
|
-- 't:Int', but @JSON@ doesn't have integers, so whole numbers should be
|
|
|
|
-- coerced to 't:Int` when receiving variables as a JSON object. The same
|
|
|
|
-- holds for 't:Enum'. There are formats that support enumerations, @JSON@
|
|
|
|
-- doesn't, so the type information is given and 'coerceVariableValue' can
|
|
|
|
-- check that an 't:Enum' is expected and treat the given value
|
|
|
|
-- appropriately. Even checking whether this value is a proper member of the
|
|
|
|
-- corresponding 't:Enum' type isn't required here, since this can be
|
|
|
|
-- checked independently.
|
|
|
|
--
|
|
|
|
-- Another example is an @ID@. @GraphQL@ explicitly allows to coerce
|
|
|
|
-- integers and strings to @ID@s, so if an @ID@ is received as an integer,
|
|
|
|
-- it can be left as is and will be coerced later.
|
|
|
|
--
|
|
|
|
-- If a value cannot be coerced without losing information, 'Nothing' should
|
|
|
|
-- be returned, the coercion will fail then and the query won't be executed.
|
|
|
|
coerceVariableValue
|
2020-05-25 07:41:21 +02:00
|
|
|
:: In.Type -- ^ Expected type (variable type given in the query).
|
2020-05-21 10:20:59 +02:00
|
|
|
-> a -- ^ Variable value being coerced.
|
2020-05-27 23:18:35 +02:00
|
|
|
-> Maybe Value -- ^ Coerced value on success, 'Nothing' otherwise.
|
2020-05-21 10:20:59 +02:00
|
|
|
|
|
|
|
instance VariableValue Aeson.Value where
|
2020-05-27 23:18:35 +02:00
|
|
|
coerceVariableValue _ Aeson.Null = Just Null
|
2020-05-25 07:41:21 +02:00
|
|
|
coerceVariableValue (In.ScalarBaseType scalarType) value
|
2020-05-27 23:18:35 +02:00
|
|
|
| (Aeson.String stringValue) <- value = Just $ String stringValue
|
|
|
|
| (Aeson.Bool booleanValue) <- value = Just $ Boolean booleanValue
|
2020-05-21 10:20:59 +02:00
|
|
|
| (Aeson.Number numberValue) <- value
|
|
|
|
, (ScalarType "Float" _) <- scalarType =
|
2020-05-27 23:18:35 +02:00
|
|
|
Just $ Float $ toRealFloat numberValue
|
2020-05-21 10:20:59 +02:00
|
|
|
| (Aeson.Number numberValue) <- value = -- ID or Int
|
2020-05-27 23:18:35 +02:00
|
|
|
Int <$> toBoundedInteger numberValue
|
2020-05-25 07:41:21 +02:00
|
|
|
coerceVariableValue (In.EnumBaseType _) (Aeson.String stringValue) =
|
2020-05-27 23:18:35 +02:00
|
|
|
Just $ Enum stringValue
|
2020-05-25 07:41:21 +02:00
|
|
|
coerceVariableValue (In.InputObjectBaseType objectType) value
|
2020-05-21 10:20:59 +02:00
|
|
|
| (Aeson.Object objectValue) <- value = do
|
2020-05-25 07:41:21 +02:00
|
|
|
let (In.InputObjectType _ _ inputFields) = objectType
|
2020-05-21 10:20:59 +02:00
|
|
|
(newObjectValue, resultMap) <- foldWithKey objectValue inputFields
|
|
|
|
if HashMap.null newObjectValue
|
2020-05-27 23:18:35 +02:00
|
|
|
then Just $ Object resultMap
|
2020-05-21 10:20:59 +02:00
|
|
|
else Nothing
|
|
|
|
where
|
2020-06-06 21:22:11 +02:00
|
|
|
foldWithKey objectValue = HashMap.foldrWithKey matchFieldValues'
|
2020-05-21 10:20:59 +02:00
|
|
|
$ Just (objectValue, HashMap.empty)
|
2020-06-06 21:22:11 +02:00
|
|
|
matchFieldValues' _ _ Nothing = Nothing
|
|
|
|
matchFieldValues' fieldName inputField (Just (objectValue, resultMap)) =
|
2020-05-25 07:41:21 +02:00
|
|
|
let (In.InputField _ fieldType _) = inputField
|
2020-05-21 10:20:59 +02:00
|
|
|
insert = flip (HashMap.insert fieldName) resultMap
|
|
|
|
newObjectValue = HashMap.delete fieldName objectValue
|
|
|
|
in case HashMap.lookup fieldName objectValue of
|
|
|
|
Just variableValue -> do
|
|
|
|
coerced <- coerceVariableValue fieldType variableValue
|
|
|
|
pure (newObjectValue, insert coerced)
|
|
|
|
Nothing -> Just (objectValue, resultMap)
|
2020-05-25 07:41:21 +02:00
|
|
|
coerceVariableValue (In.ListBaseType listType) value
|
2020-05-27 23:18:35 +02:00
|
|
|
| (Aeson.Array arrayValue) <- value = List
|
2020-05-21 10:20:59 +02:00
|
|
|
<$> foldr foldVector (Just []) arrayValue
|
|
|
|
| otherwise = coerceVariableValue listType value
|
|
|
|
where
|
|
|
|
foldVector _ Nothing = Nothing
|
|
|
|
foldVector variableValue (Just list) = do
|
|
|
|
coerced <- coerceVariableValue listType variableValue
|
|
|
|
pure $ coerced : list
|
|
|
|
coerceVariableValue _ _ = Nothing
|
2020-05-22 10:11:48 +02:00
|
|
|
|
2020-06-06 21:22:11 +02:00
|
|
|
-- | Looks up a value by name in the given map, coerces it and inserts into the
|
|
|
|
-- result map. If the coercion fails, returns 'Nothing'. If the value isn't
|
|
|
|
-- given, but a default value is known, inserts the default value into the
|
|
|
|
-- result map. Otherwise it fails with 'Nothing' if the Input Type is a
|
|
|
|
-- Non-Nullable type, or returns the unchanged, original map.
|
|
|
|
matchFieldValues :: forall a
|
|
|
|
. (In.Type -> a -> Maybe Value)
|
|
|
|
-> HashMap Name a
|
|
|
|
-> Name
|
|
|
|
-> In.Type
|
|
|
|
-> Maybe Value
|
|
|
|
-> Maybe (HashMap Name Value)
|
|
|
|
-> Maybe (HashMap Name Value)
|
|
|
|
matchFieldValues coerce values' fieldName type' defaultValue resultMap =
|
|
|
|
case HashMap.lookup fieldName values' of
|
|
|
|
Just variableValue -> coerceRuntimeValue $ coerce type' variableValue
|
|
|
|
Nothing
|
|
|
|
| Just value <- defaultValue ->
|
|
|
|
HashMap.insert fieldName value <$> resultMap
|
|
|
|
| Nothing <- defaultValue
|
|
|
|
, In.isNonNullType type' -> Nothing
|
|
|
|
| otherwise -> resultMap
|
|
|
|
where
|
|
|
|
coerceRuntimeValue (Just Null)
|
|
|
|
| In.isNonNullType type' = Nothing
|
|
|
|
coerceRuntimeValue coercedValue =
|
|
|
|
HashMap.insert fieldName <$> coercedValue <*> resultMap
|
|
|
|
|
2020-05-22 10:11:48 +02:00
|
|
|
-- | Coerces operation arguments according to the input coercion rules for the
|
2020-06-06 21:22:11 +02:00
|
|
|
-- corresponding types.
|
|
|
|
coerceInputLiteral :: In.Type -> Value -> Maybe Value
|
|
|
|
coerceInputLiteral (In.ScalarBaseType type') value
|
|
|
|
| (String stringValue) <- value
|
|
|
|
, (ScalarType "String" _) <- type' = Just $ String stringValue
|
|
|
|
| (Boolean booleanValue) <- value
|
|
|
|
, (ScalarType "Boolean" _) <- type' = Just $ Boolean booleanValue
|
|
|
|
| (Int intValue) <- value
|
|
|
|
, (ScalarType "Int" _) <- type' = Just $ Int intValue
|
|
|
|
| (Float floatValue) <- value
|
|
|
|
, (ScalarType "Float" _) <- type' = Just $ Float floatValue
|
|
|
|
| (Int intValue) <- value
|
|
|
|
, (ScalarType "Float" _) <- type' =
|
|
|
|
Just $ Float $ fromIntegral intValue
|
|
|
|
| (String stringValue) <- value
|
|
|
|
, (ScalarType "ID" _) <- type' = Just $ String stringValue
|
|
|
|
| (Int intValue) <- value
|
|
|
|
, (ScalarType "ID" _) <- type' = Just $ decimal intValue
|
2020-05-22 10:11:48 +02:00
|
|
|
where
|
2020-05-27 23:18:35 +02:00
|
|
|
decimal = String
|
2020-05-22 10:11:48 +02:00
|
|
|
. Text.Lazy.toStrict
|
|
|
|
. Text.Builder.toLazyText
|
|
|
|
. Text.Builder.decimal
|
2020-06-06 21:22:11 +02:00
|
|
|
coerceInputLiteral (In.EnumBaseType type') (Enum enumValue)
|
|
|
|
| member enumValue type' = Just $ Enum enumValue
|
|
|
|
where
|
2020-06-07 06:16:45 +02:00
|
|
|
member value (EnumType _ _ members) = HashMap.member value members
|
2020-06-06 21:22:11 +02:00
|
|
|
coerceInputLiteral (In.InputObjectBaseType type') (Object values) =
|
|
|
|
let (In.InputObjectType _ _ inputFields) = type'
|
|
|
|
in Object
|
|
|
|
<$> HashMap.foldrWithKey (matchFieldValues' values) (Just HashMap.empty) inputFields
|
|
|
|
where
|
|
|
|
matchFieldValues' values' fieldName (In.InputField _ inputFieldType defaultValue) =
|
|
|
|
matchFieldValues coerceInputLiteral values' fieldName inputFieldType defaultValue
|
|
|
|
coerceInputLiteral _ _ = Nothing
|