{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveLift #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE NoImplicitPrelude #-} module Data.Morpheus.Types.Internal.AST.Selection ( Selection (..), SelectionContent (..), SelectionSet, UnionTag (..), UnionSelection, Fragment (..), Fragments, Operation (..), Variable (..), VariableDefinitions, DefaultValue, getOperationName, getOperationDataType, ) where import Control.Applicative (pure) import Data.Foldable (all, foldr) import Data.Functor ((<$>), fmap) import Data.Maybe (Maybe (..), fromMaybe, isJust, maybe) import Data.Morpheus.Error.NameCollision ( NameCollision (..), ) import Data.Morpheus.Error.Operation ( mutationIsNotDefined, subscriptionIsNotDefined, ) import Data.Morpheus.Internal.Utils ( Failure (..), KeyOf (..), Merge (..), elems, ) import Data.Morpheus.Rendering.RenderGQL ( RenderGQL (..), Rendering, renderArguments, renderObject, space, ) import Data.Morpheus.Types.Internal.AST.Base ( FieldName, Message, Msg (..), OperationType (..), Position, Ref (..), TypeName (..), ValidationError (..), ValidationErrors, intercalateName, msg, msgValidation, readName, ) import Data.Morpheus.Types.Internal.AST.Fields ( Arguments, Directives, ) import Data.Morpheus.Types.Internal.AST.MergeSet ( MergeSet, ) import Data.Morpheus.Types.Internal.AST.OrdMap ( OrdMap, ) import Data.Morpheus.Types.Internal.AST.Stage ( RAW, Stage, VALID, ) import Data.Morpheus.Types.Internal.AST.TypeCategory ( OBJECT, ) import Data.Morpheus.Types.Internal.AST.TypeSystem ( Schema (..), TypeDefinition (..), ) import Data.Morpheus.Types.Internal.AST.Value ( ResolvedValue, Variable (..), VariableDefinitions, ) import Data.Semigroup ((<>)) import Language.Haskell.TH.Syntax (Lift (..)) import Prelude ( ($), (.), Eq (..), Show (..), otherwise, ) data Fragment (stage :: Stage) = Fragment { fragmentName :: FieldName, fragmentType :: TypeName, fragmentPosition :: Position, fragmentSelection :: SelectionSet stage, fragmentDirectives :: Directives stage } deriving (Show, Eq, Lift) -- ERRORs instance NameCollision (Fragment s) where nameCollision Fragment {fragmentName, fragmentPosition} = ValidationError ("There can be only one fragment named " <> msg fragmentName <> ".") [fragmentPosition] instance KeyOf FieldName (Fragment s) where keyOf = fragmentName type Fragments (s :: Stage) = OrdMap FieldName (Fragment s) data SelectionContent (s :: Stage) where SelectionField :: SelectionContent s SelectionSet :: SelectionSet s -> SelectionContent s UnionSelection :: UnionSelection VALID -> SelectionContent VALID renderSelectionSet :: SelectionSet VALID -> Rendering renderSelectionSet = renderObject . elems instance RenderGQL (SelectionContent VALID) where render SelectionField = "" render (SelectionSet selSet) = renderSelectionSet selSet render (UnionSelection unionSets) = renderObject (elems unionSets) instance Merge (SelectionSet s) => Merge (SelectionContent s) where merge path (SelectionSet s1) (SelectionSet s2) = SelectionSet <$> merge path s1 s2 merge path (UnionSelection u1) (UnionSelection u2) = UnionSelection <$> merge path u1 u2 merge path oldC currentC | oldC == currentC = pure oldC | otherwise = failure [ ValidationError { validationMessage = msg (intercalateName "." $ fmap refName path), validationLocations = fmap refPosition path } ] deriving instance Show (SelectionContent a) deriving instance Eq (SelectionContent a) deriving instance Lift (SelectionContent a) data UnionTag = UnionTag { unionTagName :: TypeName, unionTagSelection :: SelectionSet VALID } deriving (Show, Eq, Lift) instance KeyOf TypeName UnionTag where keyOf = unionTagName instance RenderGQL UnionTag where render UnionTag {unionTagName, unionTagSelection} = "... on " <> render unionTagName <> space <> renderSelectionSet unionTagSelection mergeConflict :: [Ref] -> ValidationError -> ValidationErrors mergeConflict [] err = [err] mergeConflict refs@(rootField : xs) err = [ ValidationError { validationMessage = renderSubfields <> validationMessage err, validationLocations = fmap refPosition refs <> validationLocations err } ] where fieldConflicts ref = msg (refName ref) <> " conflict because " renderSubfield ref txt = txt <> "subfields " <> fieldConflicts ref renderStart = "Fields " <> fieldConflicts rootField renderSubfields = foldr renderSubfield renderStart xs instance Merge UnionTag where merge path (UnionTag oldTag oldSel) (UnionTag _ currentSel) = UnionTag oldTag <$> merge path oldSel currentSel type UnionSelection (s :: Stage) = MergeSet s UnionTag type SelectionSet (s :: Stage) = MergeSet s (Selection s) data Selection (s :: Stage) where Selection :: { selectionName :: FieldName, selectionAlias :: Maybe FieldName, selectionPosition :: Position, selectionArguments :: Arguments s, selectionContent :: SelectionContent s, selectionDirectives :: Directives s } -> Selection s InlineFragment :: Fragment RAW -> Selection RAW Spread :: Directives RAW -> Ref -> Selection RAW instance RenderGQL (Selection VALID) where render Selection { .. } = render (fromMaybe selectionName selectionAlias) <> renderArguments (elems selectionArguments) <> render selectionContent instance KeyOf FieldName (Selection s) where keyOf Selection { selectionName, selectionAlias } = fromMaybe selectionName selectionAlias keyOf _ = "" useDifferentAliases :: Message useDifferentAliases = "Use different aliases on the " <> "fields to fetch both if this was intentional." instance Merge (SelectionSet a) => Merge (Selection a) where merge path old@Selection {selectionPosition = pos1} current@Selection {selectionPosition = pos2} = do selectionName <- mergeName let currentPath = path <> [Ref selectionName pos1] selectionArguments <- mergeArguments currentPath selectionContent <- merge currentPath (selectionContent old) (selectionContent current) pure $ Selection { selectionAlias = mergeAlias, selectionPosition = pos1, selectionDirectives = selectionDirectives old <> selectionDirectives current, .. } where -- passes if: -- user1: user -- } -- fails if: -- user1: product -- } mergeName | selectionName old == selectionName current = pure $ selectionName current | otherwise = failure $ mergeConflict path $ ValidationError { validationMessage = "" <> msg (selectionName old) <> " and " <> msg (selectionName current) <> " are different fields. " <> useDifferentAliases, validationLocations = [pos1, pos2] } --------------------- -- alias name is relevant only if they collide by allies like: -- { user1: user -- user1: user -- } mergeAlias | all (isJust . selectionAlias) [old, current] = selectionAlias old | otherwise = Nothing --- arguments must be equal mergeArguments currentPath | selectionArguments old == selectionArguments current = pure $ selectionArguments current | otherwise = failure $ mergeConflict currentPath $ ValidationError { validationMessage = "they have differing arguments. " <> useDifferentAliases, validationLocations = [pos1, pos2] } merge path _ _ = failure $ mergeConflict path ("INTERNAL: can't merge. " <> msgValidation useDifferentAliases :: ValidationError) deriving instance Show (Selection a) deriving instance Lift (Selection a) deriving instance Eq (Selection a) type DefaultValue = Maybe ResolvedValue data Operation (s :: Stage) = Operation { operationName :: Maybe FieldName, operationType :: OperationType, operationArguments :: VariableDefinitions s, operationSelection :: SelectionSet s, operationPosition :: Position, operationDirectives :: Directives s } deriving (Show, Lift) instance RenderGQL (Operation VALID) where render Operation { operationName, operationType, operationSelection } = render operationType <> space <> render operationName <> space <> renderSelectionSet operationSelection getOperationName :: Maybe FieldName -> TypeName getOperationName = maybe "AnonymousOperation" (TypeName . readName) getOperationDataType :: Failure ValidationError m => Operation s -> Schema VALID -> m (TypeDefinition OBJECT VALID) getOperationDataType Operation {operationType = Query} lib = pure (query lib) getOperationDataType Operation {operationType = Mutation, operationPosition} lib = maybe (failure $ mutationIsNotDefined operationPosition) pure (mutation lib) getOperationDataType Operation {operationType = Subscription, operationPosition} lib = maybe (failure $ subscriptionIsNotDefined operationPosition) pure (subscription lib)