Skip to content

Commit

Permalink
Implemented the ability to recognize if an input field is null or not…
Browse files Browse the repository at this point in the history
… present at all
  • Loading branch information
xperiandri committed Dec 8, 2024
1 parent 3a4fc85 commit 63540d9
Show file tree
Hide file tree
Showing 9 changed files with 596 additions and 40 deletions.
1 change: 1 addition & 0 deletions src/FSharp.Data.GraphQL.Server/Planning.fs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ let TypeMetaFieldDef =
args = [
{ Name = "name"
Description = None
IsSkippable = false
TypeDef = StringType
DefaultValue = None
ExecuteInput = variableOrElse(InlineConstant >> coerceStringInput >> Result.map box) }
Expand Down
19 changes: 14 additions & 5 deletions src/FSharp.Data.GraphQL.Server/ReflectionHelper.fs
Original file line number Diff line number Diff line change
Expand Up @@ -132,18 +132,24 @@ module internal ReflectionHelper =

let [<Literal>] OptionTypeName = "Microsoft.FSharp.Core.FSharpOption`1"
let [<Literal>] ValueOptionTypeName = "Microsoft.FSharp.Core.FSharpValueOption`1"
let [<Literal>] SkippableTypeName = "System.Text.Json.Serialization.Skippable`1"
let [<Literal>] ListTypeName = "Microsoft.FSharp.Collections.FSharpList`1"
let [<Literal>] ArrayTypeName = "System.Array`1"
let [<Literal>] IEnumerableTypeName = "System.Collections.IEnumerable"
let [<Literal>] IEnumerableGenericTypeName = "System.Collections.Generic.IEnumerable`1"

let rec isTypeOptional (t: Type) =
t.FullName.StartsWith OptionTypeName
|| t.FullName.StartsWith ValueOptionTypeName
|| (t.FullName.StartsWith SkippableTypeName && isTypeOptional (t.GetGenericArguments().[0]))

let isParameterOptional (p: ParameterInfo) =
p.IsOptional
|| p.ParameterType.FullName.StartsWith OptionTypeName
|| p.ParameterType.FullName.StartsWith ValueOptionTypeName
p.IsOptional || isTypeOptional p.ParameterType

let isPrameterMandatory = not << isParameterOptional

let isParameterSkippable (p: ParameterInfo) = p.ParameterType.FullName.StartsWith SkippableTypeName

let unwrapOptions (ty : Type) =
if ty.FullName.StartsWith OptionTypeName || ty.FullName.StartsWith ValueOptionTypeName then
ty.GetGenericArguments().[0]
Expand Down Expand Up @@ -172,12 +178,15 @@ module internal ReflectionHelper =
false

let actualFrom =
if from.FullName.StartsWith OptionTypeName || from.FullName.StartsWith ValueOptionTypeName then
if from.FullName.StartsWith OptionTypeName ||
from.FullName.StartsWith ValueOptionTypeName
then
from.GetGenericArguments()[0]
else from
let actualTo =
if ``to``.FullName.StartsWith OptionTypeName ||
``to``.FullName.StartsWith ValueOptionTypeName
``to``.FullName.StartsWith ValueOptionTypeName ||
``to``.FullName.StartsWith SkippableTypeName
then
``to``.GetGenericArguments()[0]
else ``to``
Expand Down
71 changes: 56 additions & 15 deletions src/FSharp.Data.GraphQL.Server/Values.fs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ let rec internal compileByType

let parametersMap =
let typeMismatchParameters = HashSet ()
let skippableMismatchParameters = HashSet ()
let nullableMismatchParameters = HashSet ()
let missingParameters = HashSet ()

Expand All @@ -123,22 +124,32 @@ let rec internal compileByType
|> Array.tryFind (fun field -> field.Name = param.Name)
with
| Some field ->
let isParameterSkippable = ReflectionHelper.isParameterSkippable param
match field.TypeDef with
| Nullable _ when field.IsSkippable <> isParameterSkippable ->
skippableMismatchParameters.Add param.Name |> ignore
| Nullable _ when
ReflectionHelper.isPrameterMandatory param
not (isParameterSkippable)
&& ReflectionHelper.isPrameterMandatory param
&& field.DefaultValue.IsNone
->
nullableMismatchParameters.Add param.Name |> ignore
| inputDef ->
let inputType, paramType = inputDef.Type, param.ParameterType
let inputType, paramType =
if isParameterSkippable then
inputDef.Type, param.ParameterType.GenericTypeArguments.[0]
else
inputDef.Type, param.ParameterType
if ReflectionHelper.isAssignableWithUnwrap inputType paramType then
allParameters.Add (struct (ValueSome field, param))
|> ignore
allParameters.Add (struct (ValueSome field, param)) |> ignore
else
// TODO: Consider improving by specifying type mismatches
typeMismatchParameters.Add param.Name |> ignore
| None ->
if ReflectionHelper.isParameterOptional param then
if
ReflectionHelper.isParameterSkippable param
|| ReflectionHelper.isParameterOptional param
then
allParameters.Add <| struct (ValueNone, param) |> ignore
else
missingParameters.Add param.Name |> ignore
Expand All @@ -157,6 +168,11 @@ let rec internal compileByType
let ``params`` = String.Join ("', '", nullableMismatchParameters)
$"Input object %s{objDef.Name} refers to type '%O{objtype}', but constructor parameters for optional GraphQL fields '%s{``params``}' are not optional"
InvalidInputTypeException (message, nullableMismatchParameters.ToImmutableHashSet ())
if skippableMismatchParameters.Any () then
let message =
let ``params`` = String.Join ("', '", skippableMismatchParameters)
$"Input object %s{objDef.Name} refers to type '%O{objtype}', but skippable '%s{``params``}' GraphQL fields and constructor parameters do not match"
InvalidInputTypeException (message, skippableMismatchParameters.ToImmutableHashSet ())
if typeMismatchParameters.Any () then
let message =
let ``params`` = String.Join ("', '", typeMismatchParameters)
Expand Down Expand Up @@ -204,15 +220,26 @@ let rec internal compileByType
parametersMap
|> Seq.map (fun struct (field, param) ->
match field with
| ValueSome field ->
match Map.tryFind field.Name props with
| None ->
Ok
<| wrapOptionalNone param.ParameterType field.TypeDef.Type
| Some prop ->
field.ExecuteInput prop variables
|> Result.map (normalizeOptional param.ParameterType)
|> attachErrorExtensionsIfScalar inputSource inputObjectPath originalInputDef field
| ValueSome field -> result {
match Map.tryFind field.Name props with
| None when field.IsSkippable -> return Activator.CreateInstance param.ParameterType
| None -> return wrapOptionalNone param.ParameterType field.TypeDef.Type
| Some prop ->
let! value =
field.ExecuteInput prop variables
|> attachErrorExtensionsIfScalar inputSource inputObjectPath originalInputDef field
if field.IsSkippable then
let innerType = param.ParameterType.GenericTypeArguments[0]
if not (ReflectionHelper.isTypeOptional innerType) &&
(value = null || (innerType.IsValueType && value = Activator.CreateInstance innerType))
then
return Activator.CreateInstance param.ParameterType
else
let ``include``, _ = ReflectionHelper.ofSkippable param.ParameterType
return normalizeOptional innerType value |> ``include``
else
return normalizeOptional param.ParameterType value
}
| ValueNone -> Ok <| wrapOptionalNone param.ParameterType typeof<obj>)
|> Seq.toList

Expand All @@ -236,12 +263,25 @@ let rec internal compileByType
parametersMap
|> Seq.map (fun struct (field, param) -> result {
match field with
| ValueSome field when field.IsSkippable && not (objectFields.ContainsKey field.Name) ->
return (Activator.CreateInstance param.ParameterType)
| ValueSome field ->
let! value =
field.ExecuteInput (VariableName field.Name) objectFields
// TODO: Take into account variable name
|> attachErrorExtensionsIfScalar inputSource inputObjectPath originalInputDef field
return normalizeOptional param.ParameterType value
if field.IsSkippable then
let innerType = param.ParameterType.GenericTypeArguments[0]
if not (ReflectionHelper.isTypeOptional innerType) &&
(value = null || (innerType.IsValueType && value = Activator.CreateInstance innerType))
then
return Activator.CreateInstance param.ParameterType
else
let normalizedValue = normalizeOptional innerType value
let ``include``, _ = ReflectionHelper.ofSkippable param.ParameterType
return ``include`` normalizedValue
else
return normalizeOptional param.ParameterType value
| ValueNone -> return wrapOptionalNone param.ParameterType typeof<obj>
})
|> Seq.toList
Expand Down Expand Up @@ -506,6 +546,7 @@ and private coerceVariableInputObject inputObjectPath (originalObjDef, objDef) (
KeyValuePair (field.Name, value)
match input.TryGetProperty field.Name with
| true, value -> coerce value |> ValueSome
| false, _ when field.IsSkippable -> ValueNone
| false, _ ->
match field.DefaultValue with
| Some value -> KeyValuePair (field.Name, Ok value)
Expand Down
47 changes: 46 additions & 1 deletion src/FSharp.Data.GraphQL.Shared/Helpers/Reflection.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
namespace FSharp.Data.GraphQL

open System
open System.Reflection
open System.Collections.Generic
open System.Collections.Immutable
open System.Reflection
open System.Text.Json.Serialization

/// General helper functions and types.
module Helpers =
Expand Down Expand Up @@ -133,3 +134,47 @@ module internal ReflectionHelper =
else input
else input
(some, none, value)

/// <summary>
/// Returns pair of function constructors for `include(value)` and `skip`
/// used to create option of type <paramref name="t"/> given at runtime.
/// </summary>
/// <param name="t">Type used for result option constructors as type param</param>
let ofSkippable (skippableType : Type) =
let skippableType = skippableType.GetTypeInfo ()
let skip =
let x = skippableType.GetDeclaredProperty "Skip"
x.GetValue(null)
let ``include`` =
let createInclude = skippableType.GetDeclaredMethod "NewInclude"
fun value ->
let valueType =
match value with
| null -> null
| _ -> value.GetType().GetTypeInfo()
if valueType = skippableType
then value
else createInclude.Invoke(null, [| value |])
(``include``, skip)

/// <summary>
/// Returns pair of function constructors for `include(value)` and `skip`
/// used to create option of type <paramref name="t"/> given at runtime.
/// </summary>
/// <param name="t">Type used for result option constructors as type param</param>
let skippableOfType t =
let skippableType = typedefof<_ Skippable>.GetTypeInfo().MakeGenericType([|t|]).GetTypeInfo()
let skip =
let x = skippableType.GetDeclaredProperty "Skip"
x.GetValue(null)
let ``include`` =
let createInclude = skippableType.GetDeclaredMethod "NewInclude"
fun value ->
let valueType =
match value with
| null -> null
| _ -> value.GetType().GetTypeInfo()
if valueType = skippableType
then value
else createInclude.Invoke(null, [| value |])
(``include``, skip)
24 changes: 24 additions & 0 deletions src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ module SchemaDefinitions =
Args =
[| { InputFieldDefinition.Name = "if"
Description = Some "Included when true."
IsSkippable = false
TypeDef = BooleanType
DefaultValue = None
ExecuteInput = variableOrElse (InlineConstant >> coerceBoolInput >> Result.map box) } |] }
Expand All @@ -492,6 +493,7 @@ module SchemaDefinitions =
Args =
[| { InputFieldDefinition.Name = "if"
Description = Some "Skipped when true."
IsSkippable = false
TypeDef = BooleanType
DefaultValue = None
ExecuteInput = variableOrElse (InlineConstant >> coerceBoolInput >> Result.map box) } |] }
Expand Down Expand Up @@ -1313,10 +1315,32 @@ module SchemaDefinitions =
static member Input(name : string, typedef : #InputDef<'In>, ?defaultValue : 'In, ?description : string) : InputFieldDef =
upcast { InputFieldDefinition.Name = name
Description = description
IsSkippable = false
TypeDef = typedef
DefaultValue = defaultValue
ExecuteInput = Unchecked.defaultof<ExecuteInput> }

/// <summary>
/// Creates an input field. Input fields are used like ordinary fileds in case of <see cref="InputObject"/>s,
/// and can be used to define arguments to objects and interfaces fields.
/// </summary>
/// <param name="name">
/// Field name. Must be unique in scope of the defining input object or withing field's argument list.
/// </param>
/// <param name="typedef">GraphQL type definition of the current input type</param>
/// <param name="defaultValue">If defined, this value will be used when no matching input has been provided by the requester.</param>
/// <param name="description">Optional input description. Usefull for generating documentation.</param>
static member SkippableInput(name : string, typedef : #InputDef<'In>, ?description : string) : InputFieldDef =
upcast { InputFieldDefinition.Name = name
Description = description |> Option.map (fun s -> s + " Skip this field if you want to avoid saving it")
IsSkippable = true
TypeDef =
match (box typedef) with
| :? NullableDef<'In> as n -> n
| _ -> Nullable typedef
DefaultValue = None
ExecuteInput = Unchecked.defaultof<ExecuteInput> }

/// <summary>
/// Creates a custom GraphQL interface type. It's needs to be implemented by object types and should not be used alone.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/FSharp.Data.GraphQL.Shared/TypeSystem.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1637,6 +1637,8 @@ and InputFieldDef =
abstract Name : string
/// Optional input field / argument description.
abstract Description : string option
/// Not applied to input object if field is missing but does not allow null.
abstract IsSkippable : bool
/// GraphQL type definition of the input type.
abstract TypeDef : InputDef
/// Optional default input value - used when no input was provided.
Expand All @@ -1654,6 +1656,8 @@ and [<CustomEquality; NoComparison>] InputFieldDefinition<'In> = {
Name : string
/// Optional input field / argument description.
Description : string option
/// Not applied to input object if field is missing but does not allow null.
IsSkippable : bool
/// GraphQL type definition of the input type.
TypeDef : InputDef<'In>
/// Optional default input value - used when no input was provided.
Expand All @@ -1666,6 +1670,7 @@ and [<CustomEquality; NoComparison>] InputFieldDefinition<'In> = {
interface InputFieldDef with
member x.Name = x.Name
member x.Description = x.Description
member x.IsSkippable = x.IsSkippable
member x.TypeDef = upcast x.TypeDef
member x.DefaultValue = x.DefaultValue |> Option.map (fun x -> upcast x)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
<Compile Include="Variables and Inputs\CoercionTests.fs" />
<Compile Include="Variables and Inputs\OptionalsNormalizationTests.ValidString.fs" />
<Compile Include="Variables and Inputs\OptionalsNormalizationTests.fs" />
<Compile Include="Variables and Inputs\SkippablesNormalizationTests.fs" />
<Compile Include="Variables and Inputs\InputRecordTests.fs" />
<Compile Include="Variables and Inputs\InputObjectValidatorTests.fs" />
<Compile Include="Variables and Inputs\InputScalarAndAutoFieldScalarTests.fs" />
Expand Down
Loading

0 comments on commit 63540d9

Please sign in to comment.