Skip to content

Commit 98cbded

Browse files
Copilotxperiandri
andauthored
Enforce ListOf/Nullable wrapper kind safety and add tests
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Data.GraphQL/sessions/7e806543-4139-4145-940a-64322cdd35cd Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
1 parent f544923 commit 98cbded

11 files changed

Lines changed: 152 additions & 52 deletions

File tree

src/FSharp.Data.GraphQL.Server.Relay/Connections.fs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,13 @@ module Definitions =
159159
)
160160
Define.AsyncField (
161161
"startCursor",
162-
Nullable StringType,
162+
Nullable (StringType :> OutputDef<string>),
163163
"When paginating backwards, the cursor to continue.",
164164
fun _ pageInfo -> pageInfo.StartCursor
165165
)
166166
Define.AsyncField (
167167
"endCursor",
168-
Nullable StringType,
168+
Nullable (StringType :> OutputDef<string>),
169169
"When paginating forwards, the cursor to continue.",
170170
fun _ pageInfo -> pageInfo.EndCursor
171171
)
@@ -225,7 +225,7 @@ module Definitions =
225225
fields = [
226226
Define.AsyncField (
227227
"totalCount",
228-
Nullable IntType,
228+
Nullable (IntType :> OutputDef<int>),
229229
"""A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing \"5\" as the argument to `first`, then fetch the total count so it could display \"5 of 83\", for example. In cases where we employ infinite scrolling or don't have an exact count of entries, this field will return `null`.""",
230230
fun _ conn -> conn.TotalCount
231231
)
@@ -303,15 +303,19 @@ module Connection =
303303
/// </summary>
304304
/// <seealso cref="backwardArgs"/>
305305
/// <seealso cref="allArgs"/>
306-
let forwardArgs = [ Define.Input ("first", Nullable IntType); Define.Input ("after", Nullable StringType) ]
306+
let forwardArgs =
307+
[ Define.Input ("first", Nullable (IntType :> InputDef<int>))
308+
Define.Input ("after", Nullable (StringType :> InputDef<string>)) ]
307309

308310
/// <summary>
309311
/// Argument definitions for backward pagination ("last" and "before").
310312
/// Use these when defining GraphQL fields that support backward-only pagination.
311313
/// </summary>
312314
/// <seealso cref="forwardArgs"/>
313315
/// <seealso cref="allArgs"/>
314-
let backwardArgs = [ Define.Input ("last", Nullable IntType); Define.Input ("before", Nullable StringType) ]
316+
let backwardArgs =
317+
[ Define.Input ("last", Nullable (IntType :> InputDef<int>))
318+
Define.Input ("before", Nullable (StringType :> InputDef<string>)) ]
315319

316320
/// <summary>
317321
/// Complete set of argument definitions for bidirectional pagination.

src/FSharp.Data.GraphQL.Server/Schema.fs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,14 @@ type SchemaConfig =
152152
let args = [|
153153
Define.Input(
154154
"interval",
155-
Nullable IntType,
155+
Nullable (IntType :> InputDef<int>),
156156
defaultValue = streamOptions.Interval,
157157
description = "An optional argument used to buffer stream results. " +
158158
"When it's value is greater than zero, stream results will be buffered for milliseconds equal to the value, then sent to the client. " +
159159
"After that, starts buffering again until all results are streamed.")
160160
Define.Input(
161161
"preferredBatchSize",
162-
Nullable IntType,
162+
Nullable (IntType :> InputDef<int>),
163163
defaultValue = streamOptions.PreferredBatchSize,
164164
description = "An optional argument used to buffer stream results. " +
165165
"When it's value is greater than zero, stream results will be buffered until item count reaches this value, then sent to the client. " +

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ let rec __Type =
9090
fieldsFn =
9191
fun () -> [
9292
Define.Field ("kind", __TypeKind, (fun _ t -> t.Kind))
93-
Define.Field ("name", Nullable StringType, resolve = (fun _ t -> t.Name))
94-
Define.Field ("description", Nullable StringType, resolve = (fun _ t -> t.Description))
93+
Define.Field ("name", Nullable (StringType :> OutputDef<string>), resolve = (fun _ t -> t.Name))
94+
Define.Field ("description", Nullable (StringType :> OutputDef<string>), resolve = (fun _ t -> t.Description))
9595
Define.Field (
9696
"fields",
9797
Nullable (ListOf __Field),
@@ -173,9 +173,9 @@ and __InputValue =
173173
fieldsFn =
174174
fun () -> [
175175
Define.Field ("name", StringType, resolve = (fun _ f -> f.Name))
176-
Define.Field ("description", Nullable StringType, resolve = (fun _ f -> f.Description))
176+
Define.Field ("description", Nullable (StringType :> OutputDef<string>), resolve = (fun _ f -> f.Description))
177177
Define.Field ("type", __Type, resolve = (fun _ f -> f.Type))
178-
Define.Field ("defaultValue", Nullable StringType, (fun _ f -> f.DefaultValue))
178+
Define.Field ("defaultValue", Nullable (StringType :> OutputDef<string>), (fun _ f -> f.DefaultValue))
179179
]
180180
)
181181

@@ -189,11 +189,11 @@ and __Field =
189189
fieldsFn =
190190
fun () -> [
191191
Define.Field ("name", StringType, (fun _ f -> f.Name))
192-
Define.Field ("description", Nullable StringType, (fun _ f -> f.Description))
192+
Define.Field ("description", Nullable (StringType :> OutputDef<string>), (fun _ f -> f.Description))
193193
Define.Field ("args", ListOf __InputValue, (fun _ f -> f.Args))
194194
Define.Field ("type", __Type, (fun _ f -> f.Type))
195195
Define.Field ("isDeprecated", BooleanType, resolve = (fun _ f -> f.IsDeprecated))
196-
Define.Field ("deprecationReason", Nullable StringType, (fun _ f -> f.DeprecationReason))
196+
Define.Field ("deprecationReason", Nullable (StringType :> OutputDef<string>), (fun _ f -> f.DeprecationReason))
197197
]
198198
)
199199

@@ -208,9 +208,9 @@ and __EnumValue =
208208
fieldsFn =
209209
fun () -> [
210210
Define.Field ("name", StringType, resolve = (fun _ e -> e.Name))
211-
Define.Field ("description", Nullable StringType, resolve = (fun _ e -> e.Description))
211+
Define.Field ("description", Nullable (StringType :> OutputDef<string>), resolve = (fun _ e -> e.Description))
212212
Define.Field ("isDeprecated", BooleanType, resolve = (fun _ e -> Option.isSome e.DeprecationReason))
213-
Define.Field ("deprecationReason", Nullable StringType, resolve = (fun _ e -> e.DeprecationReason))
213+
Define.Field ("deprecationReason", Nullable (StringType :> OutputDef<string>), resolve = (fun _ e -> e.DeprecationReason))
214214
]
215215
)
216216

@@ -231,8 +231,8 @@ and __Directive =
231231
fieldsFn =
232232
fun () -> [
233233
Define.Field ("name", StringType, resolve = (fun _ directive -> directive.Name))
234-
Define.Field ("description", Nullable StringType, resolve = (fun _ directive -> directive.Description))
235-
Define.Field ("locations", ListOf __DirectiveLocation, resolve = (fun _ directive -> directive.Locations))
234+
Define.Field ("description", Nullable (StringType :> OutputDef<string>), resolve = (fun _ directive -> directive.Description))
235+
Define.Field ("locations", ListOf (__DirectiveLocation :> OutputDef<DirectiveLocation>), resolve = (fun _ directive -> directive.Locations))
236236
Define.Field ("args", ListOf __InputValue, resolve = (fun _ directive -> directive.Args))
237237
Define.Field (
238238
"onOperation",

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

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -392,17 +392,63 @@ module SchemaDefinitions =
392392
| false, _ -> getParseError destinationType s
393393
| InlineConstant value -> value.GetCoerceError destinationType
394394

395-
/// Wraps a GraphQL type definition, allowing defining field/argument
396-
/// to take option of provided value.
397-
let Nullable(innerDef : #TypeDef<'Val>) : NullableDef<'Val> = upcast { NullableDefinition.OfType = innerDef }
398-
399-
/// Wraps a GraphQL type definition, allowing defining field/argument
400-
/// to take voption of provided value.
401-
let StructNullable(innerDef : #TypeDef<'Val>) : StructNullableDef<'Val> = upcast { StructNullableDefinition.OfType = innerDef }
402-
403-
/// Wraps a GraphQL type definition, allowing defining field/argument
404-
/// to take collection of provided value.
405-
let ListOf(innerDef : #TypeDef<'Val>) : ListOfDef<'Val, 'Seq> = upcast { ListOfDefinition.OfType = innerDef }
395+
type TypeWrapperDispatch =
396+
static member Nullable<'Val>(innerDef : InputOutputDef<'Val>) : NullableDef<'Val> =
397+
let ofType : TypeDef<'Val> = upcast innerDef
398+
upcast { NullableDefinition.OfType = ofType }
399+
400+
static member Nullable<'Val>(innerDef : InputDef<'Val>) : InputDef<'Val option> =
401+
let ofType : TypeDef<'Val> = upcast innerDef
402+
upcast { NullableDefinition.OfType = ofType }
403+
404+
static member Nullable<'Val>(innerDef : OutputDef<'Val>) : OutputDef<'Val option> =
405+
let ofType : TypeDef<'Val> = upcast innerDef
406+
upcast { NullableDefinition.OfType = ofType }
407+
408+
static member StructNullable<'Val>(innerDef : InputOutputDef<'Val>) : StructNullableDef<'Val> =
409+
let ofType : TypeDef<'Val> = upcast innerDef
410+
upcast { StructNullableDefinition.OfType = ofType }
411+
412+
static member StructNullable<'Val>(innerDef : InputDef<'Val>) : InputDef<'Val voption> =
413+
let ofType : TypeDef<'Val> = upcast innerDef
414+
upcast { StructNullableDefinition.OfType = ofType }
415+
416+
static member StructNullable<'Val>(innerDef : OutputDef<'Val>) : OutputDef<'Val voption> =
417+
let ofType : TypeDef<'Val> = upcast innerDef
418+
upcast { StructNullableDefinition.OfType = ofType }
419+
420+
static member ListOf<'Val, 'Seq when 'Seq :> 'Val seq>(innerDef : InputOutputDef<'Val>) : ListOfDef<'Val, 'Seq> =
421+
let ofType : TypeDef<'Val> = upcast innerDef
422+
upcast { ListOfDefinition.OfType = ofType }
423+
424+
static member ListOf<'Val, 'Seq when 'Seq :> 'Val seq>(innerDef : InputDef<'Val>) : InputDef<'Seq> =
425+
let ofType : TypeDef<'Val> = upcast innerDef
426+
upcast { ListOfDefinition.OfType = ofType }
427+
428+
static member ListOf<'Val, 'Seq when 'Seq :> 'Val seq>(innerDef : OutputDef<'Val>) : OutputDef<'Seq> =
429+
let ofType : TypeDef<'Val> = upcast innerDef
430+
upcast { ListOfDefinition.OfType = ofType }
431+
432+
/// Wraps a GraphQL input or output type definition, allowing defining field/argument
433+
/// to take option of provided value while preserving input/output kind of wrapped type.
434+
let inline Nullable< ^Def, ^Wrapped when (^Def or TypeWrapperDispatch) : (static member Nullable : ^Def -> ^Wrapped) >
435+
(innerDef : ^Def)
436+
: ^Wrapped =
437+
((^Def or TypeWrapperDispatch) : (static member Nullable : ^Def -> ^Wrapped) innerDef)
438+
439+
/// Wraps a GraphQL input or output type definition, allowing defining field/argument
440+
/// to take voption of provided value while preserving input/output kind of wrapped type.
441+
let inline StructNullable< ^Def, ^Wrapped when (^Def or TypeWrapperDispatch) : (static member StructNullable : ^Def -> ^Wrapped) >
442+
(innerDef : ^Def)
443+
: ^Wrapped =
444+
((^Def or TypeWrapperDispatch) : (static member StructNullable : ^Def -> ^Wrapped) innerDef)
445+
446+
/// Wraps a GraphQL input or output type definition, allowing defining field/argument
447+
/// to take collection of provided value while preserving input/output kind of wrapped type.
448+
let inline ListOf< ^Def, ^Wrapped when (^Def or TypeWrapperDispatch) : (static member ListOf : ^Def -> ^Wrapped) >
449+
(innerDef : ^Def)
450+
: ^Wrapped =
451+
((^Def or TypeWrapperDispatch) : (static member ListOf : ^Def -> ^Wrapped) innerDef)
406452

407453
let internal variableOrElse other (_ : InputExecutionContextProvider) value (variables : IReadOnlyDictionary<string, obj>) =
408454
match value with
@@ -1415,13 +1461,14 @@ module SchemaDefinitions =
14151461
/// <param name="defaultValue">If defined, this value will be used when no matching input has been provided by the requester.</param>
14161462
/// <param name="description">Optional input description. Usefull for generating documentation.</param>
14171463
static member SkippableInput(name : string, typedef : #InputDef<'In>, ?description : string) : InputFieldDef =
1464+
let typedef : InputDef<'In> = upcast typedef
14181465
upcast { InputFieldDefinition.Name = name
14191466
Description = description |> Option.map (fun s -> s + " Skip this field if you want to avoid saving it")
14201467
IsSkippable = true
14211468
TypeDef =
1422-
match (box typedef) with
1423-
| :? NullableDef<'In> as n -> n
1424-
| _ -> Nullable typedef
1469+
match (box typedef) with
1470+
| :? NullableDef<'In> as n -> (n :> InputDef<'In option>)
1471+
| _ -> Nullable typedef
14251472
DefaultValue = None
14261473
ExecuteInput = Unchecked.defaultof<ExecuteInput> }
14271474

@@ -1539,4 +1586,3 @@ module SchemaDefinitions =
15391586
Description = description
15401587
FieldsFn = fun () -> fieldsFn() |> List.toArray
15411588
ResolveType = resolveType }
1542-

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ type internal CustomFieldsObjectDefinition<'Val> (source : ObjectDef<'Val>, fiel
2727
member _.Implements = source.Implements
2828
member _.IsTypeOf = source.IsTypeOf
2929
interface TypeDef with
30-
member this.MakeList () = upcast (ListOf this)
31-
member this.MakeNullable () = upcast (Nullable this)
30+
member this.MakeList () = upcast { ListOfDefinition.OfType = this }
31+
member this.MakeNullable () = upcast { NullableDefinition.OfType = this }
3232
member _.Type = (source :> TypeDef).Type
3333
interface NamedDef with
3434
member _.Name = (source :> NamedDef).Name

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

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,21 @@ and OutputDef<'Val> =
609609
inherit TypeDef<'Val>
610610
end
611611

612+
/// Representation of all type definitions, that can be used as both inputs and outputs.
613+
and InputOutputDef =
614+
interface
615+
inherit InputDef
616+
inherit OutputDef
617+
end
618+
619+
/// Representation of all type definitions, that can be used as both inputs and outputs.
620+
and InputOutputDef<'Val> =
621+
interface
622+
inherit InputOutputDef
623+
inherit InputDef<'Val>
624+
inherit OutputDef<'Val>
625+
end
626+
612627
/// Representation of leaf type definitions. Leaf types represents leafs
613628
/// of the GraphQL query tree. Each query path must end with a leaf.
614629
/// By default only scalars and enums are valid leaf types.
@@ -1067,8 +1082,7 @@ and ScalarDef =
10671082
abstract CoerceOutput : obj -> obj option
10681083
inherit TypeDef
10691084
inherit NamedDef
1070-
inherit InputDef
1071-
inherit OutputDef
1085+
inherit InputOutputDef
10721086
inherit LeafDef
10731087
end
10741088

@@ -1098,6 +1112,7 @@ and [<CustomEquality; NoComparison>] ScalarDefinition<'Primitive, 'Val> = {
10981112

10991113
interface InputDef
11001114
interface OutputDef
1115+
interface InputOutputDef
11011116

11021117
interface ScalarDef with
11031118
member x.Name = x.Name
@@ -1107,6 +1122,7 @@ and [<CustomEquality; NoComparison>] ScalarDefinition<'Primitive, 'Val> = {
11071122

11081123
interface InputDef<'Val>
11091124
interface OutputDef<'Val>
1125+
interface InputOutputDef<'Val>
11101126
interface LeafDef
11111127

11121128
interface NamedDef with
@@ -1182,8 +1198,7 @@ and EnumDef =
11821198
/// List of available enum cases.
11831199
abstract Options : EnumVal[]
11841200
inherit TypeDef
1185-
inherit InputDef
1186-
inherit OutputDef
1201+
inherit InputOutputDef
11871202
inherit LeafDef
11881203
inherit NamedDef
11891204
end
@@ -1197,8 +1212,7 @@ and EnumDef<'Val> =
11971212
abstract Options : EnumValue<'Val>[]
11981213
inherit EnumDef
11991214
inherit TypeDef<'Val>
1200-
inherit InputDef<'Val>
1201-
inherit OutputDef<'Val>
1215+
inherit InputOutputDef<'Val>
12021216
end
12031217

12041218
and internal EnumDefinition<'Val> = {
@@ -1212,6 +1226,7 @@ and internal EnumDefinition<'Val> = {
12121226

12131227
interface InputDef
12141228
interface OutputDef
1229+
interface InputOutputDef
12151230

12161231
interface TypeDef with
12171232
member _.Type = typeof<'Val>
@@ -1523,8 +1538,7 @@ and ListOfDef<'Val, 'Seq when 'Seq :> 'Val seq> =
15231538
/// GraphQL type definition of the container element type.
15241539
abstract OfType : TypeDef<'Val>
15251540
inherit TypeDef<'Seq>
1526-
inherit InputDef<'Seq>
1527-
inherit OutputDef<'Seq>
1541+
inherit InputOutputDef<'Seq>
15281542
inherit ListOfDef
15291543
end
15301544

@@ -1574,8 +1588,7 @@ and NullableDef<'Val> =
15741588
interface
15751589
/// GraphQL type definition of the nested type.
15761590
abstract OfType : TypeDef<'Val>
1577-
inherit InputDef<'Val option>
1578-
inherit OutputDef<'Val option>
1591+
inherit InputOutputDef<'Val option>
15791592
inherit NullableDef
15801593
end
15811594

@@ -1614,8 +1627,7 @@ and StructNullableDef<'Val> =
16141627
interface
16151628
/// GraphQL type definition of the nested type.
16161629
abstract OfType : TypeDef<'Val>
1617-
inherit InputDef<'Val voption>
1618-
inherit OutputDef<'Val voption>
1630+
inherit InputOutputDef<'Val voption>
16191631
inherit NullableDef
16201632
end
16211633

tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
<Compile Include="UnionInterfaceTests.fs" />
4242
<Compile Include="DirectivesTests.fs" />
4343
<Compile Include="TypeValidationTests.fs" />
44+
<Compile Include="TypeWrappersKindSafetyTests.fs" />
4445
<Compile Include="AstValidationTests.fs" />
4546
<Compile Include="ParserTests.fs" />
4647
<Compile Include="SchemaTests.fs" />

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ let ``Planning must retain correct types for lists``() =
139139
}
140140
}
141141
}"""
142-
let PersonList : ListOfDef<Person, Person list> = ListOf Person
142+
let PersonList : OutputDef<Person list> = ListOf Person
143143
let plan = schemaProcessor.CreateExecutionPlanOrFail(query)
144144
equals 1 plan.Fields.Length
145145
let listInfo = plan.Fields.Head
@@ -178,7 +178,7 @@ let ``Planning must work with interfaces``() =
178178
}"""
179179
let plan = schemaProcessor.CreateExecutionPlanOrFail(query)
180180
equals 1 plan.Fields.Length
181-
let INamedList : ListOfDef<obj, obj list> = ListOf INamed
181+
let INamedList : OutputDef<obj list> = ListOf INamed
182182
let listInfo = plan.Fields.Head
183183
listInfo.Identifier |> equals "names"
184184
listInfo.ReturnDef |> equals (upcast INamedList)
@@ -215,7 +215,7 @@ let ``Planning must work with unions``() =
215215
let plan = schemaProcessor.CreateExecutionPlanOrFail(query)
216216
equals 1 plan.Fields.Length
217217
let listInfo = plan.Fields.Head
218-
let UNamedList : ListOfDef<Named, Named list> = ListOf UNamed
218+
let UNamedList : OutputDef<Named list> = ListOf UNamed
219219
listInfo.Identifier |> equals "names"
220220
listInfo.ReturnDef |> equals (upcast UNamedList)
221221
let (ResolveCollection(info)) = listInfo.Kind
@@ -309,7 +309,7 @@ let ``Planning must handle inline fragment with non-matching type condition in u
309309
// Verify the execution plan structure
310310
equals 1 plan.Fields.Length
311311
let listInfo = plan.Fields.Head
312-
let UNamedList : ListOfDef<Named, Named list> = ListOf UNamed
312+
let UNamedList : OutputDef<Named list> = ListOf UNamed
313313
listInfo.Identifier |> equals "names"
314314
listInfo.ReturnDef |> equals (upcast UNamedList)
315315
let (ResolveCollection(info)) = listInfo.Kind

0 commit comments

Comments
 (0)