Skip to content

Commit 1fa480e

Browse files
committed
Implemented the ability to recognize if an input field is null or not present at all
1 parent 3a4fc85 commit 1fa480e

File tree

9 files changed

+596
-40
lines changed

9 files changed

+596
-40
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ let TypeMetaFieldDef =
3131
args = [
3232
{ Name = "name"
3333
Description = None
34+
IsSkippable = false
3435
TypeDef = StringType
3536
DefaultValue = None
3637
ExecuteInput = variableOrElse(InlineConstant >> coerceStringInput >> Result.map box) }

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,18 +132,24 @@ module internal ReflectionHelper =
132132

133133
let [<Literal>] OptionTypeName = "Microsoft.FSharp.Core.FSharpOption`1"
134134
let [<Literal>] ValueOptionTypeName = "Microsoft.FSharp.Core.FSharpValueOption`1"
135+
let [<Literal>] SkippableTypeName = "System.Text.Json.Serialization.Skippable`1"
135136
let [<Literal>] ListTypeName = "Microsoft.FSharp.Collections.FSharpList`1"
136137
let [<Literal>] ArrayTypeName = "System.Array`1"
137138
let [<Literal>] IEnumerableTypeName = "System.Collections.IEnumerable"
138139
let [<Literal>] IEnumerableGenericTypeName = "System.Collections.Generic.IEnumerable`1"
139140

141+
let rec isTypeOptional (t: Type) =
142+
t.FullName.StartsWith OptionTypeName
143+
|| t.FullName.StartsWith ValueOptionTypeName
144+
|| (t.FullName.StartsWith SkippableTypeName && isTypeOptional (t.GetGenericArguments().[0]))
145+
140146
let isParameterOptional (p: ParameterInfo) =
141-
p.IsOptional
142-
|| p.ParameterType.FullName.StartsWith OptionTypeName
143-
|| p.ParameterType.FullName.StartsWith ValueOptionTypeName
147+
p.IsOptional || isTypeOptional p.ParameterType
144148

145149
let isPrameterMandatory = not << isParameterOptional
146150

151+
let isParameterSkippable (p: ParameterInfo) = p.ParameterType.FullName.StartsWith SkippableTypeName
152+
147153
let unwrapOptions (ty : Type) =
148154
if ty.FullName.StartsWith OptionTypeName || ty.FullName.StartsWith ValueOptionTypeName then
149155
ty.GetGenericArguments().[0]
@@ -172,12 +178,15 @@ module internal ReflectionHelper =
172178
false
173179

174180
let actualFrom =
175-
if from.FullName.StartsWith OptionTypeName || from.FullName.StartsWith ValueOptionTypeName then
181+
if from.FullName.StartsWith OptionTypeName ||
182+
from.FullName.StartsWith ValueOptionTypeName
183+
then
176184
from.GetGenericArguments()[0]
177185
else from
178186
let actualTo =
179187
if ``to``.FullName.StartsWith OptionTypeName ||
180-
``to``.FullName.StartsWith ValueOptionTypeName
188+
``to``.FullName.StartsWith ValueOptionTypeName ||
189+
``to``.FullName.StartsWith SkippableTypeName
181190
then
182191
``to``.GetGenericArguments()[0]
183192
else ``to``

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

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ let rec internal compileByType
111111

112112
let parametersMap =
113113
let typeMismatchParameters = HashSet ()
114+
let skippableMismatchParameters = HashSet ()
114115
let nullableMismatchParameters = HashSet ()
115116
let missingParameters = HashSet ()
116117

@@ -123,22 +124,32 @@ let rec internal compileByType
123124
|> Array.tryFind (fun field -> field.Name = param.Name)
124125
with
125126
| Some field ->
127+
let isParameterSkippable = ReflectionHelper.isParameterSkippable param
126128
match field.TypeDef with
129+
| Nullable _ when field.IsSkippable <> isParameterSkippable ->
130+
skippableMismatchParameters.Add param.Name |> ignore
127131
| Nullable _ when
128-
ReflectionHelper.isPrameterMandatory param
132+
not (isParameterSkippable)
133+
&& ReflectionHelper.isPrameterMandatory param
129134
&& field.DefaultValue.IsNone
130135
->
131136
nullableMismatchParameters.Add param.Name |> ignore
132137
| inputDef ->
133-
let inputType, paramType = inputDef.Type, param.ParameterType
138+
let inputType, paramType =
139+
if isParameterSkippable then
140+
inputDef.Type, param.ParameterType.GenericTypeArguments.[0]
141+
else
142+
inputDef.Type, param.ParameterType
134143
if ReflectionHelper.isAssignableWithUnwrap inputType paramType then
135-
allParameters.Add (struct (ValueSome field, param))
136-
|> ignore
144+
allParameters.Add (struct (ValueSome field, param)) |> ignore
137145
else
138146
// TODO: Consider improving by specifying type mismatches
139147
typeMismatchParameters.Add param.Name |> ignore
140148
| None ->
141-
if ReflectionHelper.isParameterOptional param then
149+
if
150+
ReflectionHelper.isParameterSkippable param
151+
|| ReflectionHelper.isParameterOptional param
152+
then
142153
allParameters.Add <| struct (ValueNone, param) |> ignore
143154
else
144155
missingParameters.Add param.Name |> ignore
@@ -157,6 +168,11 @@ let rec internal compileByType
157168
let ``params`` = String.Join ("', '", nullableMismatchParameters)
158169
$"Input object %s{objDef.Name} refers to type '%O{objtype}', but constructor parameters for optional GraphQL fields '%s{``params``}' are not optional"
159170
InvalidInputTypeException (message, nullableMismatchParameters.ToImmutableHashSet ())
171+
if skippableMismatchParameters.Any () then
172+
let message =
173+
let ``params`` = String.Join ("', '", skippableMismatchParameters)
174+
$"Input object %s{objDef.Name} refers to type '%O{objtype}', but skippable '%s{``params``}' GraphQL fields and constructor parameters do not match"
175+
InvalidInputTypeException (message, skippableMismatchParameters.ToImmutableHashSet ())
160176
if typeMismatchParameters.Any () then
161177
let message =
162178
let ``params`` = String.Join ("', '", typeMismatchParameters)
@@ -204,15 +220,26 @@ let rec internal compileByType
204220
parametersMap
205221
|> Seq.map (fun struct (field, param) ->
206222
match field with
207-
| ValueSome field ->
208-
match Map.tryFind field.Name props with
209-
| None ->
210-
Ok
211-
<| wrapOptionalNone param.ParameterType field.TypeDef.Type
212-
| Some prop ->
213-
field.ExecuteInput prop variables
214-
|> Result.map (normalizeOptional param.ParameterType)
215-
|> attachErrorExtensionsIfScalar inputSource inputObjectPath originalInputDef field
223+
| ValueSome field -> result {
224+
match Map.tryFind field.Name props with
225+
| None when field.IsSkippable -> return Activator.CreateInstance param.ParameterType
226+
| None -> return wrapOptionalNone param.ParameterType field.TypeDef.Type
227+
| Some prop ->
228+
let! value =
229+
field.ExecuteInput prop variables
230+
|> attachErrorExtensionsIfScalar inputSource inputObjectPath originalInputDef field
231+
if field.IsSkippable then
232+
let innerType = param.ParameterType.GenericTypeArguments[0]
233+
if not (ReflectionHelper.isTypeOptional innerType) &&
234+
(value = null || (innerType.IsValueType && value = Activator.CreateInstance innerType))
235+
then
236+
return Activator.CreateInstance param.ParameterType
237+
else
238+
let ``include``, _ = ReflectionHelper.ofSkippable param.ParameterType
239+
return normalizeOptional innerType value |> ``include``
240+
else
241+
return normalizeOptional param.ParameterType value
242+
}
216243
| ValueNone -> Ok <| wrapOptionalNone param.ParameterType typeof<obj>)
217244
|> Seq.toList
218245

@@ -236,12 +263,25 @@ let rec internal compileByType
236263
parametersMap
237264
|> Seq.map (fun struct (field, param) -> result {
238265
match field with
266+
| ValueSome field when field.IsSkippable && not (objectFields.ContainsKey field.Name) ->
267+
return (Activator.CreateInstance param.ParameterType)
239268
| ValueSome field ->
240269
let! value =
241270
field.ExecuteInput (VariableName field.Name) objectFields
242271
// TODO: Take into account variable name
243272
|> attachErrorExtensionsIfScalar inputSource inputObjectPath originalInputDef field
244-
return normalizeOptional param.ParameterType value
273+
if field.IsSkippable then
274+
let innerType = param.ParameterType.GenericTypeArguments[0]
275+
if not (ReflectionHelper.isTypeOptional innerType) &&
276+
(value = null || (innerType.IsValueType && value = Activator.CreateInstance innerType))
277+
then
278+
return Activator.CreateInstance param.ParameterType
279+
else
280+
let normalizedValue = normalizeOptional innerType value
281+
let ``include``, _ = ReflectionHelper.ofSkippable param.ParameterType
282+
return ``include`` normalizedValue
283+
else
284+
return normalizeOptional param.ParameterType value
245285
| ValueNone -> return wrapOptionalNone param.ParameterType typeof<obj>
246286
})
247287
|> Seq.toList
@@ -506,6 +546,7 @@ and private coerceVariableInputObject inputObjectPath (originalObjDef, objDef) (
506546
KeyValuePair (field.Name, value)
507547
match input.TryGetProperty field.Name with
508548
| true, value -> coerce value |> ValueSome
549+
| false, _ when field.IsSkippable -> ValueNone
509550
| false, _ ->
510551
match field.DefaultValue with
511552
| Some value -> KeyValuePair (field.Name, Ok value)

src/FSharp.Data.GraphQL.Shared/Helpers/Reflection.fs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
namespace FSharp.Data.GraphQL
55

66
open System
7-
open System.Reflection
87
open System.Collections.Generic
98
open System.Collections.Immutable
9+
open System.Reflection
10+
open System.Text.Json.Serialization
1011

1112
/// General helper functions and types.
1213
module Helpers =
@@ -133,3 +134,47 @@ module internal ReflectionHelper =
133134
else input
134135
else input
135136
(some, none, value)
137+
138+
/// <summary>
139+
/// Returns pair of function constructors for `include(value)` and `skip`
140+
/// used to create option of type <paramref name="t"/> given at runtime.
141+
/// </summary>
142+
/// <param name="t">Type used for result option constructors as type param</param>
143+
let ofSkippable (skippableType : Type) =
144+
let skippableType = skippableType.GetTypeInfo ()
145+
let skip =
146+
let x = skippableType.GetDeclaredProperty "Skip"
147+
x.GetValue(null)
148+
let ``include`` =
149+
let createInclude = skippableType.GetDeclaredMethod "NewInclude"
150+
fun value ->
151+
let valueType =
152+
match value with
153+
| null -> null
154+
| _ -> value.GetType().GetTypeInfo()
155+
if valueType = skippableType
156+
then value
157+
else createInclude.Invoke(null, [| value |])
158+
(``include``, skip)
159+
160+
/// <summary>
161+
/// Returns pair of function constructors for `include(value)` and `skip`
162+
/// used to create option of type <paramref name="t"/> given at runtime.
163+
/// </summary>
164+
/// <param name="t">Type used for result option constructors as type param</param>
165+
let skippableOfType t =
166+
let skippableType = typedefof<_ Skippable>.GetTypeInfo().MakeGenericType([|t|]).GetTypeInfo()
167+
let skip =
168+
let x = skippableType.GetDeclaredProperty "Skip"
169+
x.GetValue(null)
170+
let ``include`` =
171+
let createInclude = skippableType.GetDeclaredMethod "NewInclude"
172+
fun value ->
173+
let valueType =
174+
match value with
175+
| null -> null
176+
| _ -> value.GetType().GetTypeInfo()
177+
if valueType = skippableType
178+
then value
179+
else createInclude.Invoke(null, [| value |])
180+
(``include``, skip)

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ module SchemaDefinitions =
479479
Args =
480480
[| { InputFieldDefinition.Name = "if"
481481
Description = Some "Included when true."
482+
IsSkippable = false
482483
TypeDef = BooleanType
483484
DefaultValue = None
484485
ExecuteInput = variableOrElse (InlineConstant >> coerceBoolInput >> Result.map box) } |] }
@@ -492,6 +493,7 @@ module SchemaDefinitions =
492493
Args =
493494
[| { InputFieldDefinition.Name = "if"
494495
Description = Some "Skipped when true."
496+
IsSkippable = false
495497
TypeDef = BooleanType
496498
DefaultValue = None
497499
ExecuteInput = variableOrElse (InlineConstant >> coerceBoolInput >> Result.map box) } |] }
@@ -1313,10 +1315,32 @@ module SchemaDefinitions =
13131315
static member Input(name : string, typedef : #InputDef<'In>, ?defaultValue : 'In, ?description : string) : InputFieldDef =
13141316
upcast { InputFieldDefinition.Name = name
13151317
Description = description
1318+
IsSkippable = false
13161319
TypeDef = typedef
13171320
DefaultValue = defaultValue
13181321
ExecuteInput = Unchecked.defaultof<ExecuteInput> }
13191322

1323+
/// <summary>
1324+
/// Creates an input field. Input fields are used like ordinary fileds in case of <see cref="InputObject"/>s,
1325+
/// and can be used to define arguments to objects and interfaces fields.
1326+
/// </summary>
1327+
/// <param name="name">
1328+
/// Field name. Must be unique in scope of the defining input object or withing field's argument list.
1329+
/// </param>
1330+
/// <param name="typedef">GraphQL type definition of the current input type</param>
1331+
/// <param name="defaultValue">If defined, this value will be used when no matching input has been provided by the requester.</param>
1332+
/// <param name="description">Optional input description. Usefull for generating documentation.</param>
1333+
static member SkippableInput(name : string, typedef : #InputDef<'In>, ?description : string) : InputFieldDef =
1334+
upcast { InputFieldDefinition.Name = name
1335+
Description = description |> Option.map (fun s -> s + " Skip this field if you want to avoid saving it")
1336+
IsSkippable = true
1337+
TypeDef =
1338+
match (box typedef) with
1339+
| :? NullableDef<'In> as n -> n
1340+
| _ -> Nullable typedef
1341+
DefaultValue = None
1342+
ExecuteInput = Unchecked.defaultof<ExecuteInput> }
1343+
13201344
/// <summary>
13211345
/// Creates a custom GraphQL interface type. It's needs to be implemented by object types and should not be used alone.
13221346
/// </summary>

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,6 +1637,8 @@ and InputFieldDef =
16371637
abstract Name : string
16381638
/// Optional input field / argument description.
16391639
abstract Description : string option
1640+
/// Not applied to input object if field is missing but does not allow null.
1641+
abstract IsSkippable : bool
16401642
/// GraphQL type definition of the input type.
16411643
abstract TypeDef : InputDef
16421644
/// Optional default input value - used when no input was provided.
@@ -1654,6 +1656,8 @@ and [<CustomEquality; NoComparison>] InputFieldDefinition<'In> = {
16541656
Name : string
16551657
/// Optional input field / argument description.
16561658
Description : string option
1659+
/// Not applied to input object if field is missing but does not allow null.
1660+
IsSkippable : bool
16571661
/// GraphQL type definition of the input type.
16581662
TypeDef : InputDef<'In>
16591663
/// Optional default input value - used when no input was provided.
@@ -1666,6 +1670,7 @@ and [<CustomEquality; NoComparison>] InputFieldDefinition<'In> = {
16661670
interface InputFieldDef with
16671671
member x.Name = x.Name
16681672
member x.Description = x.Description
1673+
member x.IsSkippable = x.IsSkippable
16691674
member x.TypeDef = upcast x.TypeDef
16701675
member x.DefaultValue = x.DefaultValue |> Option.map (fun x -> upcast x)
16711676

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
<Compile Include="Variables and Inputs\CoercionTests.fs" />
5454
<Compile Include="Variables and Inputs\OptionalsNormalizationTests.ValidString.fs" />
5555
<Compile Include="Variables and Inputs\OptionalsNormalizationTests.fs" />
56+
<Compile Include="Variables and Inputs\SkippablesNormalizationTests.fs" />
5657
<Compile Include="Variables and Inputs\InputRecordTests.fs" />
5758
<Compile Include="Variables and Inputs\InputObjectValidatorTests.fs" />
5859
<Compile Include="Variables and Inputs\InputScalarAndAutoFieldScalarTests.fs" />

0 commit comments

Comments
 (0)