Consider __typename when evaluating fragments

Fixes #30.
This commit is contained in:
Eugen Wissner 2019-12-01 20:43:19 +01:00
parent def52ddc20
commit fc9ad9c4a1
4 changed files with 153 additions and 119 deletions

View File

@ -6,6 +6,9 @@ All notable changes to this project will be documented in this file.
- Parsing multiple string arguments, such as - Parsing multiple string arguments, such as
`login(username: "username", password: "password")` would fail on the comma `login(username: "username", password: "password")` would fail on the comma
due to strings not having a space consumer. due to strings not having a space consumer.
- Fragment spread is evaluated based on the `__typename` resolver. If the
resolver is missing, it is assumed that the type condition is satisfied (all
fragments are included).
## [0.6.0.0] - 2019-11-27 ## [0.6.0.0] - 2019-11-27
### Changed ### Changed

View File

@ -23,7 +23,7 @@ import qualified Language.GraphQL.Schema as Schema
-- | Associates a fragment name with a list of 'Core.Field's. -- | Associates a fragment name with a list of 'Core.Field's.
data Replacement = Replacement data Replacement = Replacement
{ fragments :: HashMap Core.Name (Seq Core.Selection) { fragments :: HashMap Core.Name Core.Fragment
, fragmentDefinitions :: HashMap Full.Name Full.FragmentDefinition , fragmentDefinitions :: HashMap Full.Name Full.FragmentDefinition
} }
@ -65,15 +65,8 @@ selection ::
Full.Selection -> Full.Selection ->
TransformT (Either (Seq Core.Selection) Core.Selection) TransformT (Either (Seq Core.Selection) Core.Selection)
selection (Full.SelectionField fld) = Right . Core.SelectionField <$> field fld selection (Full.SelectionField fld) = Right . Core.SelectionField <$> field fld
selection (Full.SelectionFragmentSpread (Full.FragmentSpread name _)) = do selection (Full.SelectionFragmentSpread fragment) =
fragments' <- gets fragments Right . Core.SelectionFragment <$> fragmentSpread fragment
Left <$> maybe lookupDefinition liftJust (HashMap.lookup name fragments')
where
lookupDefinition :: TransformT (Seq Core.Selection)
lookupDefinition = do
fragmentDefinitions' <- gets fragmentDefinitions
found <- lift . lift $ HashMap.lookup name fragmentDefinitions'
fragmentDefinition found
selection (Full.SelectionInlineFragment fragment) selection (Full.SelectionInlineFragment fragment)
| (Full.InlineFragment (Just typeCondition) _ selectionSet) <- fragment | (Full.InlineFragment (Just typeCondition) _ selectionSet) <- fragment
= Right = Right
@ -94,12 +87,23 @@ collectFragments = do
_ <- fragmentDefinition nextValue _ <- fragmentDefinition nextValue
collectFragments collectFragments
fragmentSpread :: Full.FragmentSpread -> TransformT Core.Fragment
fragmentSpread (Full.FragmentSpread name _) = do
fragments' <- gets fragments
maybe lookupDefinition liftJust (HashMap.lookup name fragments')
where
lookupDefinition = do
fragmentDefinitions' <- gets fragmentDefinitions
found <- lift . lift $ HashMap.lookup name fragmentDefinitions'
fragmentDefinition found
fragmentDefinition :: fragmentDefinition ::
Full.FragmentDefinition -> Full.FragmentDefinition ->
TransformT (Seq Core.Selection) TransformT Core.Fragment
fragmentDefinition (Full.FragmentDefinition name _tc _dirs selections) = do fragmentDefinition (Full.FragmentDefinition name typeCondition _dirs selections) = do
modify deleteFragmentDefinition modify deleteFragmentDefinition
newValue <- appendSelection selections fragmentSelection <- appendSelection selections
let newValue = Core.Fragment typeCondition fragmentSelection
modify $ insertFragment newValue modify $ insertFragment newValue
liftJust newValue liftJust newValue
where where

View File

@ -131,8 +131,8 @@ resolve resolvers = fmap (Aeson.toJSON . fold) . traverse tryResolvers
tryResolvers (SelectionField fld@(Field _ name _ _)) tryResolvers (SelectionField fld@(Field _ name _ _))
= maybe (errmsg fld) (tryResolver fld) $ find (compareResolvers name) resolvers = maybe (errmsg fld) (tryResolver fld) $ find (compareResolvers name) resolvers
tryResolvers (SelectionFragment (Fragment typeCondition selections')) = do tryResolvers (SelectionFragment (Fragment typeCondition selections')) = do
that <- maybe (return "") resolveTypeName (find (compareResolvers "__typename") resolvers) that <- traverse resolveTypeName (find (compareResolvers "__typename") resolvers)
if Aeson.String typeCondition == that if maybe True (Aeson.String typeCondition ==) that
then fmap fold . traverse tryResolvers $ selections' then fmap fold . traverse tryResolvers $ selections'
else return mempty else return mempty
compareResolvers name (Resolver name' _) = name == name' compareResolvers name (Resolver name' _) = name == name'

View File

@ -48,117 +48,144 @@ hasErrors (Object object') = HashMap.member "errors" object'
hasErrors _ = True hasErrors _ = True
spec :: Spec spec :: Spec
spec = describe "Inline fragment executor" $ do spec = do
it "chooses the first selection if the type matches" $ do describe "Inline fragment executor" $ do
actual <- graphql (garment "Hat" :| []) inlineQuery it "chooses the first selection if the type matches" $ do
let expected = object actual <- graphql (garment "Hat" :| []) inlineQuery
[ "data" .= object let expected = object
[ "garment" .= object [ "data" .= object
[ "garment" .= object
[ "circumference" .= (60 :: Int)
]
]
]
in actual `shouldBe` expected
it "chooses the last selection if the type matches" $ do
actual <- graphql (garment "Shirt" :| []) inlineQuery
let expected = object
[ "data" .= object
[ "garment" .= object
[ "size" .= ("L" :: Text)
]
]
]
in actual `shouldBe` expected
it "embeds inline fragments without type" $ do
let query = [r|{
garment {
circumference
... {
size
}
}
}|]
resolvers = Schema.object "garment" $ return [circumference, size]
actual <- graphql (resolvers :| []) query
let expected = object
[ "data" .= object
[ "garment" .= object
[ "circumference" .= (60 :: Int)
, "size" .= ("L" :: Text)
]
]
]
in actual `shouldBe` expected
it "evaluates fragments on Query" $ do
let query = [r|{
... {
size
}
}|]
actual <- graphql (size :| []) query
actual `shouldNotSatisfy` hasErrors
describe "Fragment spread executor" $ do
it "evaluates fragment spreads" $ do
let query = [r|
{
...circumferenceFragment
}
fragment circumferenceFragment on Hat {
circumference
}
|]
actual <- graphql (circumference :| []) query
let expected = object
[ "data" .= object
[ "circumference" .= (60 :: Int) [ "circumference" .= (60 :: Int)
] ]
] ]
] in actual `shouldBe` expected
in actual `shouldBe` expected
it "chooses the last selection if the type matches" $ do it "evaluates nested fragments" $ do
actual <- graphql (garment "Shirt" :| []) inlineQuery let query = [r|
let expected = object {
[ "data" .= object garment {
[ "garment" .= object ...circumferenceFragment
[ "size" .= ("L" :: Text) }
}
fragment circumferenceFragment on Hat {
...hatFragment
}
fragment hatFragment on Hat {
circumference
}
|]
actual <- graphql (garment "Hat" :| []) query
let expected = object
[ "data" .= object
[ "garment" .= object
[ "circumference" .= (60 :: Int)
]
] ]
] ]
] in actual `shouldBe` expected
in actual `shouldBe` expected
it "embeds inline fragments without type" $ do it "rejects recursive fragments" $ do
let query = [r|{ let query = [r|
garment { {
circumference ...circumferenceFragment
... { }
size
}
}
}|]
resolvers = Schema.object "garment" $ return [circumference, size]
actual <- graphql (resolvers :| []) query fragment circumferenceFragment on Hat {
let expected = object ...circumferenceFragment
[ "data" .= object }
[ "garment" .= object |]
[ "circumference" .= (60 :: Int)
, "size" .= ("L" :: Text) actual <- graphql (circumference :| []) query
actual `shouldSatisfy` hasErrors
it "considers type condition" $ do
let query = [r|
{
garment {
...circumferenceFragment
...sizeFragment
}
}
fragment circumferenceFragment on Hat {
circumference
}
fragment sizeFragment on Shirt {
size
}
|]
expected = object
[ "data" .= object
[ "garment" .= object
[ "circumference" .= (60 :: Int)
]
] ]
] ]
] actual <- graphql (garment "Hat" :| []) query
in actual `shouldBe` expected actual `shouldBe` expected
it "evaluates fragments on Query" $ do
let query = [r|{
... {
size
}
}|]
actual <- graphql (size :| []) query
actual `shouldNotSatisfy` hasErrors
it "evaluates nested fragments" $ do
let query = [r|
{
...circumferenceFragment
}
fragment circumferenceFragment on Hat {
circumference
}
fragment hatFragment on Hat {
...circumferenceFragment
}
|]
actual <- graphql (circumference :| []) query
let expected = object
[ "data" .= object
[ "circumference" .= (60 :: Int)
]
]
in actual `shouldBe` expected
it "evaluates fragments defined in any order" $ do
let query = [r|
{
...circumferenceFragment
}
fragment circumferenceFragment on Hat {
...hatFragment
}
fragment hatFragment on Hat {
circumference
}
|]
actual <- graphql (circumference :| []) query
let expected = object
[ "data" .= object
[ "circumference" .= (60 :: Int)
]
]
in actual `shouldBe` expected
it "rejects recursive" $ do
let query = [r|
{
...circumferenceFragment
}
fragment circumferenceFragment on Hat {
...circumferenceFragment
}
|]
actual <- graphql (circumference :| []) query
actual `shouldSatisfy` hasErrors