Skip to content

Commit

Permalink
Merge pull request #458 from fsprojects/abstraction-impl-skip-fix
Browse files Browse the repository at this point in the history
  • Loading branch information
xperiandri authored Feb 15, 2024
2 parents 68b4b4e + 1556194 commit 95d2de2
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 12 deletions.
19 changes: 12 additions & 7 deletions src/FSharp.Data.GraphQL.Server/Execution.fs
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,12 @@ let private resolveField (execute: ExecuteField) (ctx: ResolveFieldContext) (par

type ResolverResult<'T> = Result<'T * IObservable<GQLDeferredResponseContent> option * GQLProblemDetails list, GQLProblemDetails list>

[<RequireQualifiedAccess>]
module ResolverResult =

let data data = Ok (data, None, [])
let defered data deferred = Ok (data, Some deferred, [])

let mapValue (f : 'T -> 'U) (r : ResolverResult<'T>) : ResolverResult<'U> =
Result.map(fun (data, deferred, errs) -> (f data, deferred, errs)) r

Expand All @@ -280,7 +285,7 @@ let private unionImplError unionName tyName path ctx = resolverError path ctx (G
let private deferredNullableError name tyName path ctx = resolverError path ctx (GQLMessageException (sprintf "Deferred field %s of type '%s' must be nullable" name tyName))
let private streamListError name tyName path ctx = resolverError path ctx (GQLMessageException (sprintf "Streamed field %s of type '%s' must be list" name tyName))

let private resolved name v : AsyncVal<ResolverResult<KeyValuePair<string, obj>>> = AsyncVal.wrap <| Ok(KeyValuePair(name, box v), None, [])
let private resolved name v : AsyncVal<ResolverResult<KeyValuePair<string, obj>>> = KeyValuePair(name, box v) |> ResolverResult.data |> AsyncVal.wrap

let deferResults path (res : ResolverResult<obj>) : IObservable<GQLDeferredResponseContent> =
let formattedPath = path |> List.rev
Expand Down Expand Up @@ -370,7 +375,7 @@ let rec private direct (returnDef : OutputDef) (ctx : ResolveFieldContext) (path
| kind -> failwithf "Unexpected value of ctx.ExecutionPlan.Kind: %A" kind
match Map.tryFind resolvedDef.Name typeMap with
| Some fields -> executeObjectFields fields name resolvedDef ctx path value
| None -> raiseErrors <| interfaceImplError iDef.Name resolvedDef.Name path ctx
| None -> KeyValuePair(name, obj()) |> ResolverResult.data |> AsyncVal.wrap

| Union uDef ->
let possibleTypesFn = ctx.Schema.GetPossibleTypes
Expand All @@ -382,7 +387,7 @@ let rec private direct (returnDef : OutputDef) (ctx : ResolveFieldContext) (path
| kind -> failwithf "Unexpected value of ctx.ExecutionPlan.Kind: %A" kind
match Map.tryFind resolvedDef.Name typeMap with
| Some fields -> executeObjectFields fields name resolvedDef ctx path (uDef.ResolveValue value)
| None -> raiseErrors <| unionImplError uDef.Name resolvedDef.Name path ctx
| None -> KeyValuePair(name, obj()) |> ResolverResult.data |> AsyncVal.wrap

| _ -> failwithf "Unexpected value of returnDef: %O" returnDef

Expand All @@ -393,7 +398,7 @@ and deferred (ctx : ResolveFieldContext) (path : FieldPath) (parent : obj) (valu
executeResolvers ctx path parent (toOption value |> AsyncVal.wrap)
|> Observable.ofAsyncVal
|> Observable.bind(ResolverResult.mapValue(fun d -> d.Value) >> deferResults path)
AsyncVal.wrap <| Ok(KeyValuePair(info.Identifier, null), Some deferred, [])
ResolverResult.defered (KeyValuePair (info.Identifier, null)) deferred |> AsyncVal.wrap

and private streamed (options : BufferedStreamOptions) (innerDef : OutputDef) (ctx : ResolveFieldContext) (path : FieldPath) (parent : obj) (value : obj) =
let info = ctx.ExecutionInfo
Expand Down Expand Up @@ -444,7 +449,7 @@ and private streamed (options : BufferedStreamOptions) (innerDef : OutputDef) (c
|> Array.mapi resolveItem
|> Observable.ofAsyncValSeq
|> buffer
AsyncVal.wrap <| Ok(KeyValuePair(info.Identifier, box [||]), Some stream, [])
ResolverResult.defered (KeyValuePair (info.Identifier, box [])) stream |> AsyncVal.wrap
| _ -> raise <| GQLMessageException (ErrorMessages.expectedEnumerableValue ctx.ExecutionInfo.Identifier (value.GetType()))

and private live (ctx : ResolveFieldContext) (path : FieldPath) (parent : obj) (value : obj) =
Expand Down Expand Up @@ -510,12 +515,12 @@ and private executeResolvers (ctx : ResolveFieldContext) (path : FieldPath) (par
match info.Kind, returnDef with
| ResolveDeferred innerInfo, _ when innerInfo.IsNullable -> // We can only defer nullable fields
deferred
|> resolveWith { ctx with ExecutionInfo = { innerInfo with IsNullable = false } }
|> resolveWith { ctx with ExecutionInfo = innerInfo }
| ResolveDeferred innerInfo, _ ->
raiseErrors <| deferredNullableError (innerInfo.Identifier) (innerInfo.ReturnDef.ToString()) path ctx
| ResolveStreamed (innerInfo, mode), HasList innerDef -> // We can only stream lists
streamed mode innerDef
|> resolveWith { ctx with ExecutionInfo = innerInfo; }
|> resolveWith { ctx with ExecutionInfo = innerInfo }
| ResolveStreamed (innerInfo, _), _ ->
raiseErrors <| streamListError innerInfo.Identifier (returnDef.ToString()) path ctx
| ResolveLive innerInfo, _ ->
Expand Down
4 changes: 2 additions & 2 deletions src/FSharp.Data.GraphQL.Server/IO.fs
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ type GQLExecutionResult =
static member Invalid(documentId, errors, meta) =
GQLExecutionResult.RequestError(documentId, errors, meta)
static member ErrorAsync(documentId, msg : string, meta) =
asyncVal.Return (GQLExecutionResult.Error (documentId, msg, meta))
AsyncVal.wrap (GQLExecutionResult.Error (documentId, msg, meta))
static member ErrorAsync(documentId, error : IGQLError, meta) =
asyncVal.Return (GQLExecutionResult.Error (documentId, error, meta))
AsyncVal.wrap (GQLExecutionResult.Error (documentId, error, meta))

// TODO: Rename to PascalCase
and GQLResponseContent =
Expand Down
106 changes: 105 additions & 1 deletion tests/FSharp.Data.GraphQL.Tests/AbstractionTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ let schemaWithInterface =
[ Define.Field (
"pets",
ListOf PetType,
fun _ _ -> [ { Name = "Odie"; Woofs = true } :> IPet; upcast { Name = "Garfield"; Meows = false } ]
fun _ _ -> [ { Name = "Odie"; Woofs = true } :> IPet; { Name = "Garfield"; Meows = false } ]
) ]
),
config = { SchemaConfig.Default with Types = [ CatType; DogType ] }
Expand Down Expand Up @@ -111,6 +111,48 @@ let ``Execute handles execution of abstract types: isTypeOf is used to resolve r
empty errors
data |> equals (upcast expected)

[<Fact>]
let ``Execute handles execution of abstract types: not specified Interface types must be empty objects`` () =
let query =
"""{
pets {
... on Dog {
name
woofs
}
}
}"""

let result = sync <| schemaWithInterface.Value.AsyncExecute (parse query)

let expected = NameValueLookup.ofList [ "name", "Odie" :> obj; "woofs", upcast true ]

ensureDirect result <| fun data errors ->
empty errors
let [| dog; emptyObj |] = data["pets"] :?> obj array
dog |> equals (upcast expected)
emptyObj.GetType() |> equals typeof<obj>

let query =
"""{
pets {
... on Cat {
name
meows
}
}
}"""

let result = sync <| schemaWithInterface.Value.AsyncExecute (parse query)

let expected = NameValueLookup.ofList [ "name", "Garfield" :> obj; "meows", upcast false ]

ensureDirect result <| fun data errors ->
empty errors
let [| emptyObj; cat|] = data["pets"] :?> obj array
cat |> equals (upcast expected)
emptyObj.GetType() |> equals typeof<obj>

[<Fact>]
let ``Execute handles execution of abstract types: absent field resolution produces errors for Interface`` () =
let query =
Expand Down Expand Up @@ -155,6 +197,26 @@ let ``Execute handles execution of abstract types: absent type resolution produc
catError |> ensureValidationError "Field 'unknownField2' is not defined in schema type 'Cat'." [ "pets"; "unknownField2" ]
dogError |> ensureValidationError "Inline fragment has type condition 'UnknownDog', but that type does not exist in the schema." [ "pets" ]

let query =
"""{
pets {
name
... on Dog {
woofs
unknownField1
}
... on UnknownCat {
meows
unknownField2
}
}
}"""

let result = sync <| schemaWithInterface.Value.AsyncExecute (parse query)
ensureRequestError result <| fun [ catError; dogError ] ->
catError |> ensureValidationError "Field 'unknownField1' is not defined in schema type 'Dog'." [ "pets"; "unknownField1" ]
dogError |> ensureValidationError "Inline fragment has type condition 'UnknownCat', but that type does not exist in the schema." [ "pets" ]


let schemaWithUnion =
lazy
Expand Down Expand Up @@ -219,6 +281,48 @@ let ``Execute handles execution of abstract types: isTypeOf is used to resolve r
empty errors
data |> equals (upcast expected)

[<Fact>]
let ``Execute handles execution of abstract types: not specified Union types must be empty objects`` () =
let query =
"""{
pets {
... on Dog {
name
woofs
}
}
}"""

let result = sync <| schemaWithUnion.Value.AsyncExecute (parse query)

let expected = NameValueLookup.ofList [ "name", "Odie" :> obj; "woofs", upcast true ]

ensureDirect result <| fun data errors ->
empty errors
let [| dog; emptyObj |] = data["pets"] :?> obj array
dog |> equals (upcast expected)
emptyObj.GetType() |> equals typeof<obj>

let query =
"""{
pets {
... on Cat {
name
meows
}
}
}"""

let result = sync <| schemaWithUnion.Value.AsyncExecute (parse query)

let expected = NameValueLookup.ofList [ "name", "Garfield" :> obj; "meows", upcast false ]

ensureDirect result <| fun data errors ->
empty errors
let [| emptyObj; cat|] = data["pets"] :?> obj array
cat |> equals (upcast expected)
emptyObj.GetType() |> equals typeof<obj>

[<Fact>]
let ``Execute handles execution of abstract types: absent field resolution produces errors for Union`` () =
let query =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ let ``AsyncVal computation defines zero value`` () =

[<Fact>]
let ``AsyncVal can be returned from Async computation`` () =
let a = async { return! asyncVal.Return 1 }
let a = async { return! AsyncVal.wrap 1 }
let res = a |> sync
res |> equals 1

[<Fact>]
let ``AsyncVal can be bound inside Async computation`` () =
let a = async {
let! v = asyncVal.Return 1
let! v = AsyncVal.wrap 1
return v }
let res = a |> sync
res |> equals 1
Expand Down

0 comments on commit 95d2de2

Please sign in to comment.