From 1d24f472d5a90c5e4721ccfab705d717590c5e07 Mon Sep 17 00:00:00 2001 From: Peter Kese Date: Sat, 6 Apr 2024 19:28:56 +0200 Subject: [PATCH] Add support for generic IDictionary query inputs - relates to #472 --- ...rp.Data.GraphQL.Samples.StarWarsApi.fsproj | 2 +- samples/star-wars-api/Schema.fs | 51 +++++++++++++++- samples/star-wars-api/appsettings.json | 4 +- .../ReflectionHelper.fs | 60 +++++++++++++++++++ src/FSharp.Data.GraphQL.Server/Values.fs | 28 +++++++-- 5 files changed, 136 insertions(+), 9 deletions(-) diff --git a/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj b/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj index 09bfd9f9f..0e07a357e 100644 --- a/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj +++ b/samples/star-wars-api/FSharp.Data.GraphQL.Samples.StarWarsApi.fsproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 false diff --git a/samples/star-wars-api/Schema.fs b/samples/star-wars-api/Schema.fs index f8f2ef29b..9822e82d4 100644 --- a/samples/star-wars-api/Schema.fs +++ b/samples/star-wars-api/Schema.fs @@ -153,12 +153,30 @@ module Schema = | Human _ -> upcast HumanType | Droid _ -> upcast DroidType) ) +(* + and CharacterInfo = + Define.Interface ( + name = "CharacterInfo", + description = "A character common info.", + fieldsFn = + fun () -> + [ Define.Field ("id", StringType, "The id of the character.") + Define.Field ("name", Nullable StringType, "The name of the character.") + Define.Field ("appearsIn", ListOf EpisodeType, "Which movies they appear in.") + Define.Field ( + "friends", + ConnectionOf CharacterType, + "The friends of the human, or an empty list if they have none." ) + ] + ) +*) and HumanType : ObjectDef = Define.Object ( name = "Human", description = "A humanoid creature in the Star Wars universe.", isTypeOf = (fun o -> o :? Human), + //interfaces = [ CharacterInfo ], fieldsFn = fun () -> [ Define.Field ("id", StringType, "The id of the human.", (fun _ (h : Human) -> h.Id)) @@ -206,7 +224,9 @@ module Schema = con ) Define.Field ("appearsIn", ListOf EpisodeType, "Which movies they appear in.", (fun _ (h : Human) -> h.AppearsIn)) - Define.Field ("homePlanet", Nullable StringType, "The home planet of the human, or null if unknown.", (fun _ h -> h.HomePlanet)) ] + Define.Field ("homePlanet", Nullable StringType, "The home planet of the human, or null if unknown.", (fun _ h -> h.HomePlanet)) + Define.Field ("planet", Nullable PlanetType, "The home planet of the human, or null if unknown.", (fun _ h -> h.HomePlanet |> Option.bind getPlanet )) + ] ) and DroidType = @@ -250,6 +270,30 @@ module Schema = fieldsFn = fun () -> [ Define.Field ("requestId", StringType, "The ID of the client.", (fun _ (r : Root) -> r.RequestId)) ] ) + //Define.Input ("range", Nullable (Define.InputObject>("Range", [ + //Define.Input ("range", Nullable (Define.InputObject("Range", [ + type Range = { Min : int; Max : int } + let randoms = + let inputs = [ + Define.Input ("count", IntType) + //Define.Input ("range", Nullable (Define.InputObject("Range", [ + //Define.Input ("range", Nullable (Define.InputObject>("Range", [ + Define.Input ("range", Nullable (Define.InputObject("Range", [ + //Define.Input ("range", Nullable (Define.InputObject>("Range", [ + Define.Input ("min", IntType) + Define.Input ("max", IntType) + ]))) + ] + let renderRandoms (ctx:ResolveFieldContext) (r:Root) = + printfn "Random called with count:%A and range:%A" (ctx.Arg "count") (ctx.Arg "range") + //let range = ctx.Arg "range" |> unbox + //printfn "members: %A" (range.GetDynamicMemberNames()) + //range.GetDynamicMemberNames() |> Seq.iter (printfn "Dynamic member: %A") + [ for i in 1..ctx.Arg("count") -> System.Random.Shared.Next() ] + + Define.Field ("random", ListOf IntType, "Render random numbers", inputs, renderRandoms) + + let Query = let inputs = [ Define.Input ("id", StringType) ] Define.Object ( @@ -258,7 +302,10 @@ module Schema = [ Define.Field ("hero", Nullable HumanType, "Gets human hero", inputs, fun ctx _ -> getHuman (ctx.Arg ("id"))) Define.Field ("droid", Nullable DroidType, "Gets droid", inputs, (fun ctx _ -> getDroid (ctx.Arg ("id")))) Define.Field ("planet", Nullable PlanetType, "Gets planet", inputs, fun ctx _ -> getPlanet (ctx.Arg ("id"))) - Define.Field ("characters", ListOf CharacterType, "Gets characters", (fun _ _ -> characters)) ] + Define.Field ("characters", ListOf CharacterType, "Gets characters", (fun _ _ -> characters)) + randoms + ] + ) let Subscription = diff --git a/samples/star-wars-api/appsettings.json b/samples/star-wars-api/appsettings.json index 5fff67bac..cf4b309a6 100644 --- a/samples/star-wars-api/appsettings.json +++ b/samples/star-wars-api/appsettings.json @@ -1,8 +1,8 @@ { "Logging": { - "IncludeScopes": false, "LogLevel": { - "Default": "Warning" + "Default": "Trace", + "Microsoft.Hosting": "Trace" } } } diff --git a/src/FSharp.Data.GraphQL.Server/ReflectionHelper.fs b/src/FSharp.Data.GraphQL.Server/ReflectionHelper.fs index f304e02ac..fa2f39c8c 100644 --- a/src/FSharp.Data.GraphQL.Server/ReflectionHelper.fs +++ b/src/FSharp.Data.GraphQL.Server/ReflectionHelper.fs @@ -140,8 +140,68 @@ module internal ReflectionHelper = let isPrameterMandatory = not << isParameterOptional + +(* + let makeConstructor (fields: string[]) = + { new ConstructorInfo() as this with + let typ = typeof + let ctor = + typ.GetConstructors(BindingFlags.NonPublic|||BindingFlags.Public|||BindingFlags.Instance) + |> Seq.filter (fun ctor -> ctor.GetParameters().Length = 0) + |> Seq.head + override _.GetParameters() = [||] + override _.Attributes = ctor.Attributes + override _.MethodHandle = ctor.MethodHandle + override _.GetMethodImplementationFlags() = ctor.GetMethodImplementationFlags() + member _.Invoke(args: obj []) = Activator.CreateInstance(typeof, args) + member _.Name = "DynamicConstructorInfo" + member _.DeclaringType = typeof + override _.Invoke(bindingFlags, binder, args, cultureInfo) = obj() + } +*) + + type DynamicConstructorInfo(typ:System.Type, fields: string[]) = + inherit ConstructorInfo() + let ctor = + typ.GetConstructors(BindingFlags.NonPublic|||BindingFlags.Public|||BindingFlags.Instance) + |> Seq.filter (fun ctor -> ctor.GetParameters().Length = 0) + |> Seq.head + override _.GetParameters() = + printfn "DynamicConstructorInfo.GetParameters() -> %A" (fields |> String.concat ",") + fields + |> Array.mapi (fun i fName -> + { new ParameterInfo() with + member _.Name = fName + member _.Position = i + member _.ParameterType = typeof + member _.Attributes = ParameterAttributes.Optional + } + ) + override _.Attributes = ctor.Attributes + override _.MethodHandle = ctor.MethodHandle + override _.GetMethodImplementationFlags() = ctor.GetMethodImplementationFlags() + override _.GetCustomAttributes(_inherit) = typ.GetCustomAttributes(_inherit) + override _.GetCustomAttributes(x, _inherit) = typ.GetCustomAttributes(x, _inherit) + override _.IsDefined(x, _inherit) = typ.IsDefined(x, _inherit) + override _.Name = "DynamicConstructorInfo" + override _.DeclaringType = typ.DeclaringType + override _.ReflectedType = typ.ReflectedType + member this.Invoke(args: obj []) = + printfn "DynamicConstructorInfo.Invoke(%A) nArgs=%d nFields=%d" args args.Length fields.Length + let o = Activator.CreateInstance(typ) + let dict = o :?> IDictionary + (fields, args) + ||> Seq.iter2 (fun k v -> dict.Add(k, v)) + o + override this.Invoke(bindingFlags, binder, args, cultureInfo) = this.Invoke(args) + override this.Invoke(o, bindingFlags, binder, args, cultureInfo) = this.Invoke(bindingFlags, binder, args, cultureInfo) + let matchConstructor (t: Type) (fields: string []) = if FSharpType.IsRecord(t, true) then FSharpValue.PreComputeRecordConstructorInfo(t, true) + //else if t = typeof then + else if typeof>.IsAssignableFrom(t) then + eprintfn "matched DynamicConstructorInfo for type %A" t + upcast DynamicConstructorInfo(t, fields) else let constructors = t.GetConstructors(BindingFlags.NonPublic|||BindingFlags.Public|||BindingFlags.Instance) let inputFieldNames = diff --git a/src/FSharp.Data.GraphQL.Server/Values.fs b/src/FSharp.Data.GraphQL.Server/Values.fs index cddd8c0c7..8d140574e 100644 --- a/src/FSharp.Data.GraphQL.Server/Values.fs +++ b/src/FSharp.Data.GraphQL.Server/Values.fs @@ -89,10 +89,30 @@ let rec internal compileByType (inputObjectPath: FieldPath) (inputSource : Input | InputObject objDef -> let objtype = objDef.Type - let ctor = ReflectionHelper.matchConstructor objtype (objDef.Fields |> Array.map (fun x -> x.Name)) + let (constructor : obj[] -> obj), (parameterInfos : Reflection.ParameterInfo[]) = + if typeof>.IsAssignableFrom(objtype) then + let parameterInfos = [| + for f in objDef.Fields -> + { new Reflection.ParameterInfo() with + member _.Name = f.Name + member _.ParameterType = f.TypeDef.Type + member _.Attributes = Reflection.ParameterAttributes.Optional + } + |] + let constructor (args:obj[]) = + let o = Activator.CreateInstance(objtype) + let dict = o :?> IDictionary + for fld,arg in Seq.zip objDef.Fields args do + dict.Add(fld.Name, arg) + box o + constructor, parameterInfos + else + let ctor = ReflectionHelper.matchConstructor objtype (objDef.Fields |> Array.map (fun x -> x.Name)) + ctor.Invoke, ctor.GetParameters() + let struct (mapper, nullableMismatchParameters, missingParameters) = - ctor.GetParameters () + parameterInfos |> Array.fold ( fun struct(all: ResizeArray<_>, areNullable: HashSet<_>, missing: HashSet<_>) @@ -170,7 +190,7 @@ let rec internal compileByType (inputObjectPath: FieldPath) (inputSource : Input let! args = argResults |> splitSeqErrorsList - let instance = ctor.Invoke args + let instance = constructor args do! objDef.Validator instance |> ValidationResult.mapErrors (fun err -> err |> mapInputObjectError inputSource inputObjectPath originalInputDef) return instance @@ -197,7 +217,7 @@ let rec internal compileByType (inputObjectPath: FieldPath) (inputSource : Input let! args = argResults |> splitSeqErrorsList - let instance = ctor.Invoke args + let instance = constructor args do! objDef.Validator instance |> ValidationResult.mapErrors (fun err -> err |> mapInputObjectError inputSource inputObjectPath originalInputDef) return instance