Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,4 +329,6 @@
* Fixed the GraphQL client provider building `Operation<...>` again for every file that uses the same operation, which added another operation type of the same name to `Operations` each time
* Changed the checks for options, value options, skippable values and F# lists in input coercion, execution and the `ObjectListFilter` middleware to compare types instead of the culture-sensitive prefixes of their full names: a check takes about a nanosecond instead of hundreds
* Fixed `Helpers.unwrap` and `Helpers.objectOptionCast` throwing `NullReferenceException` for an array of options, which they took for an option
* Added support of more collection types for GraphQL input lists: an argument or an input object field gets the collection type of its list definition, and an input object constructor parameter of another collection type gets the items copied into it. Supported are arrays, F# lists and sets, `ResizeArray`, `HashSet`, the immutable collections, interfaces such as `IReadOnlyList<T>`, `IList<T>` and `ISet<T>`, and any type with a constructor that takes an `IEnumerable<T>` or with a collection initializer
* Fixed a list argument given through a variable getting the F# list the variable was coerced to instead of the collection type of its definition, such as an array
* Removed the internal `Observable.withCompletionMarker`
125 changes: 104 additions & 21 deletions src/FSharp.Data.GraphQL.Server/ReflectionHelper.fs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ open System.Reflection
open FSharp.Reflection
open System.Collections.Generic
open System.Linq
open System.Runtime.CompilerServices
open FSharp.Data.GraphQL.Types
open FSharp.Quotations

Expand Down Expand Up @@ -137,7 +138,6 @@ module internal Gen =
module internal ReflectionHelper =

let private genericEnumerableTypeDefinition = typedefof<IEnumerable<_>>
let private enumerableType = typeof<System.Collections.IEnumerable>

let rec isTypeOptional (t: Type) =
ReflectionHelper.isOptionType t
Expand All @@ -156,29 +156,112 @@ module internal ReflectionHelper =
ty.GetGenericArguments().[0]
else ty

/// <summary>
/// The type of the items of a collection type: the element type of an array, or the type argument
/// of the <see cref="T:System.Collections.Generic.IEnumerable`1"/> it is or implements.
/// A string is a sequence of characters, but not a collection a GraphQL list builds.
/// </summary>
let tryGetItemType (collectionType : Type) : Type voption =
if collectionType.IsArray then
ValueSome (collectionType.GetElementType ())
elif Type.(=) (collectionType, typeof<string>) then
ValueNone
elif ReflectionHelper.isConstructedFrom genericEnumerableTypeDefinition collectionType then
ValueSome collectionType.GenericTypeArguments[0]
else
collectionType.GetInterfaces ()
|> Array.vtryFind (ReflectionHelper.isConstructedFrom genericEnumerableTypeDefinition)
|> ValueOption.map (fun enumerableType -> enumerableType.GenericTypeArguments[0])

/// Builds a collection through the <c>CreateRange</c> method of the builder type that the
/// <see cref="T:System.Runtime.CompilerServices.CollectionBuilderAttribute"/> of the collection type names,
/// as the immutable collections have. The method the attribute names takes a <see cref="T:System.ReadOnlySpan`1"/>,
/// which reflection cannot pass.
let private tryCollectionBuilderFactory (collectionType : Type) (itemType : Type) (items : obj list -> obj) =
match collectionType.GetCustomAttribute<CollectionBuilderAttribute> () with
| null -> ValueNone
| attribute ->
attribute.BuilderType.GetMethods (BindingFlags.Public ||| BindingFlags.Static)
|> Array.vtryFind (fun method ->
String.Equals (method.Name, "CreateRange", StringComparison.Ordinal)
&& method.IsGenericMethodDefinition
&& method.GetGenericArguments().Length = 1
&& (let parameters = method.GetParameters ()
parameters.Length = 1
&& ReflectionHelper.isConstructedFrom genericEnumerableTypeDefinition parameters[0].ParameterType))
Comment on lines +187 to +191
|> ValueOption.map (fun method -> method.MakeGenericMethod itemType)
|> ValueOption.filter (fun method -> collectionType.IsAssignableFrom method.ReturnType)
|> ValueOption.map (fun method -> fun values -> method.Invoke (null, [| items values |]))

/// Builds a collection through its public constructor that takes the items as an <see cref="T:System.Collections.Generic.IEnumerable`1"/>.
let private tryEnumerableConstructorFactory (collectionType : Type) (itemType : Type) (items : obj list -> obj) =
match collectionType.GetConstructor [| genericEnumerableTypeDefinition.MakeGenericType itemType |] with
| null -> ValueNone
| constructor -> ValueSome (fun values -> constructor.Invoke [| items values |])

/// Builds a collection the way a collection initializer does: through its public parameterless constructor and
/// an <c>Add</c> method for every item. An <c>Add</c> that returns the collection type, as an immutable collection's does,
/// leaves the collection unchanged, so only one that returns nothing or a <see cref="T:System.Boolean"/> counts.
let private tryCollectionInitializerFactory (collectionType : Type) (itemType : Type) =
match collectionType.GetConstructor Type.EmptyTypes, collectionType.GetMethod ("Add", [| itemType |]) with
| null, _
| _, null -> ValueNone
| _, add when not (Type.(=) (add.ReturnType, typeof<Void>) || Type.(=) (add.ReturnType, typeof<bool>)) -> ValueNone
| constructor, add ->
ValueSome (fun (values : obj list) ->
let collection = constructor.Invoke [||]
for value in values do
add.Invoke (collection, [| value |]) |> ignore
collection)

/// <summary>
/// Creates the function that builds a collection of the type from the coerced items of a GraphQL list.
/// </summary>
/// <remarks>
/// It supports, in this order: arrays; the types an F# list is assignable to, such as
/// <see cref="T:System.Collections.Generic.IReadOnlyList`1"/>, which get an F# list; interfaces that
/// <see cref="T:System.Collections.Generic.List`1"/> or <see cref="T:System.Collections.Generic.HashSet`1"/> implement,
/// such as <see cref="T:System.Collections.Generic.IList`1"/> or <see cref="T:System.Collections.Generic.ISet`1"/>,
/// which get one of them; types with a collection builder, such as the immutable collections; types with a constructor
/// that takes an <see cref="T:System.Collections.Generic.IEnumerable`1"/>, such as F# sets; and types with
/// a collection initializer.
/// </remarks>
let tryCreateCollectionFactory (collectionType : Type) : (obj list -> obj) voption =
match tryGetItemType collectionType with
| ValueNone -> ValueNone
| ValueSome itemType ->
let toArray (values : obj list) = ReflectionHelper.arrayOfList itemType values
if collectionType.IsArray then
ValueSome toArray
elif collectionType.IsAssignableFrom (typedefof<_ list>.MakeGenericType itemType) then
let cons, nil = ReflectionHelper.listOfType itemType
ValueSome (fun values -> List.foldBack cons values nil)
else
let concreteType =
if collectionType.IsInterface || collectionType.IsAbstract then
[ typedefof<List<_>>; typedefof<HashSet<_>> ]
|> List.map _.MakeGenericType(itemType)
|> List.tryFind collectionType.IsAssignableFrom
|> Option.defaultValue null
else
collectionType
match concreteType with
| null -> ValueNone
| concreteType ->
tryCollectionBuilderFactory concreteType itemType toArray
|> ValueOption.orElseWith (fun () -> tryEnumerableConstructorFactory concreteType itemType toArray)
|> ValueOption.orElseWith (fun () -> tryCollectionInitializerFactory concreteType itemType)

let rec isAssignableWithUnwrap (from: Type) (``to``: Type) =

// A GraphQL list builds a collection of its own type, which input coercion then copies into
// a constructor parameter of another collection type that it can build
let checkCollections (from: Type) (``to``: Type) =
if
// TODO: Implement support of other types of collections using collection initializers
// An array is not among them: input coercion builds an array only for a GraphQL list
// whose type is an array, which is assignable without unwrapping
ReflectionHelper.isListType ``to``
&& (from.IsGenericType
&& from.GenericTypeArguments[0].IsAssignableTo(``to``.GenericTypeArguments[0])
&& from.GetInterfaces()
|> Array.exists (
fun i -> ReflectionHelper.isConstructedFrom genericEnumerableTypeDefinition i
|| Type.(=) (i, enumerableType)
)
)

then
let fromType = from.GetGenericArguments()[0]
let toType = ``to``.GetGenericArguments()[0]
fromType.IsAssignableTo toType
else
false
match tryGetItemType from, tryGetItemType ``to`` with
| ValueSome fromItemType, ValueSome toItemType ->
fromItemType.IsAssignableTo toItemType
&& (tryCreateCollectionFactory ``to``).IsSome
| _ -> false

let actualFrom = unwrapOptions from
let actualTo =
Expand Down
82 changes: 58 additions & 24 deletions src/FSharp.Data.GraphQL.Server/Values.fs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,38 @@ let normalizeOptional (outputType : Type) value =
else
value

/// Creates the function that builds a collection of the type of a GraphQL list from its coerced items.
/// A type that no collection can be built for gets an F# list, as every type but an array did before.
let private createListCollection (listType : Type) (itemType : Type) : obj list -> obj =
match ReflectionHelper.tryCreateCollectionFactory listType with
| ValueSome create -> create
| ValueNone ->
let cons, nil = ReflectionHelper.listOfType itemType
fun items -> List.foldBack cons items nil

/// Creates the function that copies the collection a GraphQL list field builds into the collection type
/// of the constructor parameter that the field is bound to, when the parameter has another collection type.
let private createCollectionConverter (fieldType : Type) (parameterType : Type) : obj -> obj =
let targetType =
let valueType =
if ReflectionHelper.isSkippableType parameterType then
parameterType.GenericTypeArguments[0]
else
parameterType
ReflectionHelper.unwrapOptions valueType
if targetType.IsAssignableFrom (ReflectionHelper.unwrapOptions fieldType) then
id
else
match ReflectionHelper.tryCreateCollectionFactory targetType with
| ValueNone -> id
| ValueSome create ->
fun value ->
match value with
| null -> null
| value when targetType.IsInstanceOfType value -> value
| :? System.Collections.IEnumerable as items -> create (items |> Seq.cast<obj> |> Seq.toList)
| value -> value

/// Tries to convert type defined in AST into one of the type defs known in schema.
let inline tryConvertAst schema ast =
let rec convert isNullable (schema : ISchema) (ast : InputType) : TypeDef voption =
Expand Down Expand Up @@ -139,7 +171,7 @@ let rec internal compileByType
else
inputDef.Type, param.ParameterType
if ReflectionHelper.isAssignableWithUnwrap inputType paramType then
allParameters.Add (struct (ValueSome field, param))
allParameters.Add (struct (ValueSome field, param, createCollectionConverter inputType paramType))
else
let expectedType = inputDef.Type.ToString ()
let actualType = paramType.ToString ()
Expand All @@ -150,7 +182,7 @@ let rec internal compileByType
ReflectionHelper.isParameterSkippable param
|| ReflectionHelper.isParameterOptional param
then
allParameters.Add <| struct (ValueNone, param)
allParameters.Add <| struct (ValueNone, param, id)
else
missingParameters.Add param.Name |> ignore
allParameters)
Expand Down Expand Up @@ -228,7 +260,7 @@ let rec internal compileByType
| ObjectValue props -> result {
let argResults =
parametersMap
|> Seq.map (fun struct (field, param) ->
|> Seq.map (fun struct (field, param, convertCollection) ->
match field with
| ValueSome field -> result {
match Map.tryFind field.Name props with
Expand All @@ -249,9 +281,9 @@ let rec internal compileByType
return Activator.CreateInstance param.ParameterType
else
let ``include``, _ = ReflectionHelper.ofSkippable param.ParameterType
return normalizeOptional innerType value |> ``include``
return normalizeOptional innerType (convertCollection value) |> ``include``
else
return normalizeOptional param.ParameterType value
return normalizeOptional param.ParameterType (convertCollection value)
}
| ValueNone -> Ok <| wrapOptionalNone param.ParameterType typeof<obj>)
|> Seq.toList
Expand All @@ -274,7 +306,7 @@ let rec internal compileByType

let argResults =
parametersMap
|> Seq.map (fun struct (field, param) -> result {
|> Seq.map (fun struct (field, param, convertCollection) -> result {
match field with
| ValueSome field when field.IsSkippable && not (objectFields.ContainsKey field.Name) ->
return (Activator.CreateInstance param.ParameterType)
Expand All @@ -290,11 +322,11 @@ let rec internal compileByType
then
return Activator.CreateInstance param.ParameterType
else
let normalizedValue = normalizeOptional innerType value
let normalizedValue = normalizeOptional innerType (convertCollection value)
let ``include``, _ = ReflectionHelper.ofSkippable param.ParameterType
return ``include`` normalizedValue
else
return normalizeOptional param.ParameterType value
return normalizeOptional param.ParameterType (convertCollection value)
| ValueNone -> return wrapOptionalNone param.ParameterType typeof<obj>
})
|> Seq.toList
Expand Down Expand Up @@ -337,10 +369,9 @@ let rec internal compileByType
inputObjDef.ExecuteInput <- inner
| _ -> ()

let isArray = inputDef.Type.IsArray
let createCollection = createListCollection inputDef.Type innerDef.Type
// TODO: Improve creation of inner
let inner index = compileByType ((box index) :: inputObjectPath) inputSource (innerDef, innerDef) getInputContext
let cons, nil = ReflectionHelper.listOfType innerDef.Type

fun getInputContext value variables ->
match value with
Expand All @@ -355,22 +386,29 @@ let rec internal compileByType
|> Seq.map (normalizeOptional innerDef.Type)
|> Seq.toList

if isArray then
return ReflectionHelper.arrayOfList innerDef.Type mappedValues
else
return List.foldBack cons mappedValues nil
return createCollection mappedValues
}
| VariableName variableName -> Ok variables[variableName]
| VariableName variableName ->
// The variable is coerced by its own type, so its list gets the collection type of this one
match variables[variableName] with
| null -> Ok null
| value when inputDef.Type.IsInstanceOfType value -> Ok value
| :? System.Collections.IEnumerable as items ->
items
|> Seq.cast<obj>
|> Seq.map (normalizeOptional innerDef.Type)
|> Seq.toList
|> createCollection
|> Ok
| value -> Ok value
| _ -> result {
// try to construct a list from single element
let! single = inner 0 getInputContext value variables

if single = null then
return null
else if isArray then
return ReflectionHelper.arrayOfList innerDef.Type [ single ]
else
return cons single nil
return createCollection [ single ]
}

| Nullable (Input innerDef) ->
Expand Down Expand Up @@ -517,7 +555,7 @@ let rec internal coerceVariableValue (ctx : CoerceVariableContext, inputContext
}
coerceVariableValue(ctx', inputContext)
| List (Input innerDef) ->
let cons, nil = ReflectionHelper.listOfType innerDef.Type
let createCollection = createListCollection ctx.TypeDef.Type innerDef.Type

match ctx.Input with
| _ when ctx.Input.ValueKind = JsonValueKind.Null && ctx.IsNullable -> Ok null
Expand Down Expand Up @@ -574,11 +612,7 @@ let rec internal coerceVariableValue (ctx : CoerceVariableContext, inputContext
return [ single ]
}

let isArray = ctx.TypeDef.Type.IsArray
if isArray then
return ReflectionHelper.arrayOfList innerDef.Type items
else
return List.foldBack cons items nil
return createCollection items
}
| InputObject objDef ->
coerceVariableInputObject ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
<Compile Include="TaskSeqFieldTests.fs" />
<Compile Include="AssemblyInfo.fs" />
<Compile Include="Helpers and Extensions\ReflectionHelperTests.fs" />
<Compile Include="Variables and Inputs\InputCollectionTests.fs" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading
Loading