Skip to content

Commit 298b58b

Browse files
vanhoeijxperiandri
andauthored
Fix recursive definition of InputObject fields (#349)
* Implemented use of `lazy` for `InputObject` fields * Renamed `FieldsFn` to `Fields` * Fixed `Execute handles nested input objects and nullability using inline structs and proprely coerces complex scalar types` test Co-authored-by: XperiAndri <[email protected]>
1 parent 83521d9 commit 298b58b

File tree

3 files changed

+53
-13
lines changed

3 files changed

+53
-13
lines changed

samples/star-wars-api/HttpHandlers.fs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace FSharp.Data.GraphQL.Samples.StarWarsApi
1+
namespace FSharp.Data.GraphQL.Samples.StarWarsApi
22

33
open System.IO
44
open System.Text
@@ -13,6 +13,7 @@ open FSharp.Data.GraphQL.Types
1313
type HttpHandler = HttpFunc -> HttpContext -> HttpFuncResult
1414

1515
module HttpHandlers =
16+
1617
let private converters : JsonConverter[] = [| OptionConverter () |]
1718
let private jsonSettings = jsonSerializerSettings converters
1819

src/FSharp.Data.GraphQL.Shared/TypeSystem.fs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,15 +1528,15 @@ and InputObjectDefinition<'Val> =
15281528
Name : string
15291529
/// Optional input object description.
15301530
Description : string option
1531-
/// Function used to define field inputs. It must be lazy
1532-
/// in order to support self-referencing types.
1533-
FieldsFn : unit -> InputFieldDef [] }
1531+
/// Lazy resolver for the input object fields. It must be lazy in
1532+
/// order to allow self-recursive type references.
1533+
Fields : Lazy<InputFieldDef[]> }
15341534
interface InputDef
15351535

15361536
interface InputObjectDef with
15371537
member x.Name = x.Name
15381538
member x.Description = x.Description
1539-
member x.Fields = x.FieldsFn()
1539+
member x.Fields = x.Fields.Force()
15401540

15411541
interface TypeDef<'Val>
15421542
interface InputDef<'Val>
@@ -2775,31 +2775,31 @@ module SchemaDefinitions =
27752775

27762776
/// <summary>
27772777
/// Creates a custom GraphQL input object type. Unlike GraphQL objects, input objects are valid input types,
2778-
/// that can be included in GraphQL query strings. Input object maps to a .NET type, which can be strandard
2778+
/// that can be included in GraphQL query strings. Input object maps to a .NET type, which can be standard
27792779
/// .NET class or struct, or a F# record.
27802780
/// </summary>
27812781
/// <param name="name">Type name. Must be unique in scope of the current schema.</param>
27822782
/// <param name="fieldsFn">
2783-
/// Function which generates a list of input fields defined by the current input object. Usefull, when object defines recursive dependencies.
2783+
/// Function which generates a list of input fields defined by the current input object. Useful, when object defines recursive dependencies.
27842784
/// </param>
2785-
/// <param name="description">Optional input object description. Usefull for generating documentation.</param>
2785+
/// <param name="description">Optional input object description. Useful for generating documentation.</param>
27862786
static member InputObject(name : string, fieldsFn : unit -> InputFieldDef list, ?description : string) : InputObjectDefinition<'Out> =
27872787
{ Name = name
2788-
FieldsFn = fun () -> fieldsFn() |> List.toArray
2788+
Fields = lazy (fieldsFn () |> List.toArray)
27892789
Description = description }
27902790

27912791
/// <summary>
27922792
/// Creates a custom GraphQL input object type. Unlike GraphQL objects, input objects are valid input types,
2793-
/// that can be included in GraphQL query strings. Input object maps to a .NET type, which can be strandard
2793+
/// that can be included in GraphQL query strings. Input object maps to a .NET type, which can be standard
27942794
/// .NET class or struct, or a F# record.
27952795
/// </summary>
27962796
/// <param name="name">Type name. Must be unique in scope of the current schema.</param>
27972797
/// <param name="fields">List of input fields defined by the current input object. </param>
2798-
/// <param name="description">Optional input object description. Usefull for generating documentation.</param>
2798+
/// <param name="description">Optional input object description. Useful for generating documentation.</param>
27992799
static member InputObject(name : string, fields : InputFieldDef list, ?description : string) : InputObjectDefinition<'Out> =
28002800
{ Name = name
28012801
Description = description
2802-
FieldsFn = fun () -> fields |> List.toArray }
2802+
Fields = lazy (fields |> List.toArray) }
28032803

28042804
/// <summary>
28052805
/// Creates the top level subscription object that holds all of the possible subscriptions as fields.

tests/FSharp.Data.GraphQL.Tests/VariablesTests.fs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type TestNestedInput = {
4545
na: TestInput option
4646
nb: string
4747
}
48+
4849
let TestNestedInputObject =
4950
Define.InputObject<TestNestedInput>(
5051
name = "TestNestedInputObject",
@@ -53,6 +54,21 @@ let TestNestedInputObject =
5354
Define.Input("nb", String)
5455
])
5556

57+
type TestRecusiveInput = {
58+
ra: TestRecusiveInput option
59+
rb: string
60+
}
61+
62+
#nowarn "40"
63+
let rec TestRecursiveInputObject =
64+
Define.InputObject<TestRecusiveInput>(
65+
name = "TestRecusiveInput",
66+
fieldsFn =
67+
fun () -> [
68+
Define.Input("ra", Nullable TestRecursiveInputObject)
69+
Define.Input("rb", String)]
70+
)
71+
5672
let stringifyArg name (ctx: ResolveFieldContext) () =
5773
let arg = ctx.TryArg name |> Option.toObj
5874
toJson arg
@@ -79,6 +95,7 @@ let TestType =
7995
Define.Field("fieldWithNonNullableStringInput", String, "", [ Define.Input("input", String) ], stringifyInput)
8096
Define.Field("fieldWithDefaultArgumentValue", String, "", [ Define.Input("input", Nullable String, Some "hello world") ], stringifyInput)
8197
Define.Field("fieldWithNestedInputObject", String, "", [ Define.Input("input", TestNestedInputObject, { na = None; nb = "hello world"}) ], stringifyInput)
98+
Define.Field("fieldWithRecursiveInputObject", String, "", [ Define.Input("input", TestRecursiveInputObject, { ra = None; rb = "hello world"}) ], stringifyInput)
8299
Define.Field("fieldWithEnumInput", String, "", [ Define.Input("input", EnumTestType) ], stringifyInput)
83100
Define.Field("fieldWithNullableEnumInput", String, "", [ Define.Input("input", Nullable EnumTestType) ], stringifyInput)
84101
Define.Field("list", String, "", [ Define.Input("input", Nullable(ListOf (Nullable String))) ], stringifyInput)
@@ -123,7 +140,7 @@ let ``Execute handles objects and nullability using inline structs and doesn't u
123140
| _ -> fail "Expected Direct GQResponse"
124141

125142
[<Fact>]
126-
let ``Execute handles objects and nullability using inline structs and proprely coerces complex scalar types`` () =
143+
let ``Execute handles objects and nullability using inline structs and properly coerces complex scalar types`` () =
127144
let ast = parse """{ fieldWithObjectInput(input: {c: "foo", d: "SerializedValue"}) }"""
128145
let actual = sync <| Executor(schema).AsyncExecute(ast)
129146
let expected = NameValueLookup.ofList [ "fieldWithObjectInput", upcast """{"a":null,"b":null,"c":"foo","d":"DeserializedValue","e":null}"""]
@@ -328,6 +345,28 @@ let ``Execute handles non-nullable scalars and passes along null for non-nullabl
328345
data.["data"] |> equals (upcast expected)
329346
| _ -> fail "Expected Direct GQResponse"
330347

348+
[<Fact>]
349+
let ``Execute handles nested input objects and nullability using inline structs and properly coerces complex scalar types`` () =
350+
let ast = parse """{ fieldWithNestedInputObject(input: {na:{c:"c"},nb:"b"})}"""
351+
let actual = sync <| Executor(schema).AsyncExecute(ast)
352+
let expected = NameValueLookup.ofList [ "fieldWithNestedInputObject", upcast """{"na":{"a":null,"b":null,"c":"c","d":null,"e":null},"nb":"b"}""" ]
353+
match actual with
354+
| Direct(data, errors) ->
355+
empty errors
356+
data.["data"] |> equals (upcast expected)
357+
| _ -> fail "Expected Direct GQResponse"
358+
359+
[<Fact>]
360+
let ``Execute handles recursive input objects and nullability using inline structs and properly coerces complex scalar types`` () =
361+
let ast = parse """{ fieldWithRecursiveInputObject(input: {ra:{rb:"bb"},rb:"b"})}"""
362+
let actual = sync <| Executor(schema).AsyncExecute(ast)
363+
let expected = NameValueLookup.ofList [ "fieldWithRecursiveInputObject", upcast """{"ra":{"ra":null,"rb":"bb"},"rb":"b"}""" ]
364+
match actual with
365+
| Direct(data, errors) ->
366+
empty errors
367+
data.["data"] |> equals (upcast expected)
368+
| _ -> fail "Expected Direct GQResponse"
369+
331370
[<Fact>]
332371
let ``Execute handles list inputs and nullability and allows lists to be null`` () =
333372
let ast = parse """query q($input: [String]) {

0 commit comments

Comments
 (0)