Skip to content
Closed
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
89 changes: 53 additions & 36 deletions src/FSharp.Data.GraphQL.Server/Values.fs
Original file line number Diff line number Diff line change
Expand Up @@ -38,31 +38,36 @@ let rec internal compileByType (errMsg: string) (inputDef: InputDef): ExecuteInp
| Scalar scalardef ->
variableOrElse (scalardef.CoerceInput >> Option.toObj)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jberzy can we switch this to Result<,> and pass an error to the response?

| InputObject objdef ->
let objtype = objdef.Type
let ctor = ReflectionHelper.matchConstructor objtype (objdef.Fields |> Array.map (fun x -> x.Name))
let mapper =
ctor.GetParameters()
|> Array.map(fun param ->
match objdef.Fields |> Array.tryFind(fun field -> field.Name = param.Name) with
| Some x -> x
| None ->
failwithf "Input object '%s' refers to type '%O', but constructor parameter '%s' doesn't match any of the defined input fields" objdef.Name objtype param.Name)
fun value variables ->
match value with
| ObjectValue props ->
let args =
mapper
|> Array.map (fun field ->
match Map.tryFind field.Name props with
| None -> null
| Some prop -> field.ExecuteInput prop variables)
let instance = ctor.Invoke(args)
instance
| Variable variableName ->
match Map.tryFind variableName variables with
| Some found -> found
| None -> null
| _ -> null
match objdef.CoerceInput with
| Some coerceInput ->
// Apply the user-provided coerce function, rather than attempt reflection
variableOrElse (coerceInput >> Option.toObj)
| None ->
let objtype = objdef.Type
let ctor = ReflectionHelper.matchConstructor objtype (objdef.Fields |> Array.map (fun x -> x.Name))
let mapper =
ctor.GetParameters()
|> Array.map(fun param ->
match objdef.Fields |> Array.tryFind(fun field -> field.Name = param.Name) with
| Some x -> x
| None ->
failwithf "Input object '%s' refers to type '%O', but constructor parameter '%s' doesn't match any of the defined input fields" objdef.Name objtype param.Name)
fun value variables ->
match value with
| ObjectValue props ->
let args =
mapper
|> Array.map (fun field ->
match Map.tryFind field.Name props with
| None -> null
| Some prop -> field.ExecuteInput prop variables)
let instance = ctor.Invoke(args)
instance
| Variable variableName ->
match Map.tryFind variableName variables with
| Some found -> found
| None -> null
| _ -> null
| List (Input innerdef) ->
let inner = compileByType errMsg innerdef
let cons, nil = ReflectionHelper.listOfType innerdef.Type
Expand Down Expand Up @@ -153,17 +158,29 @@ let rec private coerceVariableValue isNullable typedef (vardef: VarDef) (input:
| _ -> raise (GraphQLException <| errMsg + "Only Scalars, Nullables, Lists and InputObjects are valid type definitions.")

and private coerceVariableInputObject (objdef) (vardef: VarDef) (input: obj) errMsg =
//TODO: this should be eventually coerced to complex object
match input with
| :? Map<string, obj> as map ->
let mapped =
objdef.Fields
|> Array.map (fun field ->
let valueFound = Map.tryFind field.Name map |> Option.toObj
(field.Name, coerceVariableValue false field.TypeDef vardef valueFound (errMsg + (sprintf "in field '%s': " field.Name))))
|> Map.ofArray
upcast mapped
| _ -> input
match objdef.CoerceInput with
| Some coerceInput ->
// Apply the user-provided coerce function
match input with
| :? Value as v ->
coerceInput v |> Option.toObj
| _ -> failwithf "Expected an input Value but got %A" input
| None ->
match input with
| :? Map<string, obj> as map ->
let mapping =
objdef.Fields
|> Array.map (fun field ->
let valueFound = Map.tryFind field.Name map |> Option.toObj
let coercedValue = coerceVariableValue false field.TypeDef vardef valueFound (errMsg + (sprintf "in field '%s': " field.Name))
Comment on lines +173 to +175
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
|> Array.map (fun field ->
let valueFound = Map.tryFind field.Name map |> Option.toObj
let coercedValue = coerceVariableValue false field.TypeDef vardef valueFound (errMsg + (sprintf "in field '%s': " field.Name))
|> Seq.map (fun field ->
let valueFound = map |> Map.tryFind field.Name |> Option.toObj
let coercedValue = coerceVariableValue false field.TypeDef vardef valueFound (errMsg + (sprintf "in field '%s': " field.Name))

Maybe it does not worth to materialize it to an array


(field.Name, coercedValue))
|> Map.ofSeq

// TODO: Should we apply reflection tricks here?

upcast mapping
| _ -> input

let internal coerceVariable (vardef: VarDef) (inputs) =
let vname = vardef.Name
Expand Down
25 changes: 20 additions & 5 deletions src/FSharp.Data.GraphQL.Shared/TypeSystem.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1517,6 +1517,8 @@ and InputObjectDef =
abstract Description : string option
/// Collection of input object fields.
abstract Fields : InputFieldDef []
/// Optional function to customize default coercion behaviour
abstract CoerceInput : (Value -> obj option) option
inherit NamedDef
inherit InputDef
end
Expand All @@ -1530,13 +1532,22 @@ and InputObjectDefinition<'Val> =
Description : string option
/// Function used to define field inputs. It must be lazy
/// in order to support self-referencing types.
FieldsFn : unit -> InputFieldDef [] }
FieldsFn : unit -> InputFieldDef []
/// Optional function to customize default coercion behaviour
CoerceInput : (Value -> 'Val option) option }
Comment thread
xperiandri marked this conversation as resolved.
interface InputDef

interface InputObjectDef with
member x.Name = x.Name
member x.Description = x.Description
member x.Fields = x.FieldsFn()
member x.CoerceInput =
x.CoerceInput
|> Option.map
(fun coerceInputTyped ->
(fun v ->
coerceInputTyped v
|> Option.map box))

interface TypeDef<'Val>
interface InputDef<'Val>
Expand Down Expand Up @@ -2783,10 +2794,12 @@ module SchemaDefinitions =
/// Function which generates a list of input fields defined by the current input object. Usefull, when object defines recursive dependencies.
/// </param>
/// <param name="description">Optional input object description. Usefull for generating documentation.</param>
static member InputObject(name : string, fieldsFn : unit -> InputFieldDef list, ?description : string) : InputObjectDefinition<'Out> =
/// <param name="coerceInput">Optional customization of input coercion</param>
static member InputObject(name : string, fieldsFn : unit -> InputFieldDef list, ?description : string, ?coerceInput : _) : InputObjectDefinition<'Out> =
{ Name = name
FieldsFn = fun () -> fieldsFn() |> List.toArray
Description = description }
Description = description
CoerceInput = coerceInput }

/// <summary>
/// Creates a custom GraphQL input object type. Unlike GraphQL objects, input objects are valid input types,
Expand All @@ -2796,10 +2809,12 @@ module SchemaDefinitions =
/// <param name="name">Type name. Must be unique in scope of the current schema.</param>
/// <param name="fields">List of input fields defined by the current input object. </param>
/// <param name="description">Optional input object description. Usefull for generating documentation.</param>
static member InputObject(name : string, fields : InputFieldDef list, ?description : string) : InputObjectDefinition<'Out> =
/// <param name="coerceInput">Optional customization of input coercion</param>
static member InputObject(name : string, fields : InputFieldDef list, ?description : string, ?coerceInput : _) : InputObjectDefinition<'Out> =
{ Name = name
Description = description
FieldsFn = fun () -> fields |> List.toArray }
FieldsFn = fun () -> fields |> List.toArray
CoerceInput = coerceInput }

/// <summary>
/// Creates the top level subscription object that holds all of the possible subscriptions as fields.
Expand Down
68 changes: 67 additions & 1 deletion tests/FSharp.Data.GraphQL.Tests/VariablesTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type TestInput = {
c: string
d: string option
}

let TestInputObject =
Define.InputObject<TestInput>(
name = "TestInputObject",
Expand All @@ -39,6 +40,7 @@ type TestNestedInput = {
na: TestInput option
nb: string
}

let TestNestedInputObject =
Define.InputObject<TestNestedInput>(
name = "TestNestedInputObject",
Expand All @@ -51,7 +53,9 @@ let stringifyArg name (ctx: ResolveFieldContext) () =
let arg = ctx.TryArg name |> Option.toObj
toJson arg

let stringifyInput = stringifyArg "input"
let stringifyInput (ctx: ResolveFieldContext) () =
let inputArg : TestInput option = ctx.Arg "input"
toJson inputArg

type EnumTestType = Foo | Bar

Expand All @@ -64,6 +68,52 @@ let EnumTestType =
, "Test enum"
)

let TestInputObjectWithCustomCoercion =
Define.InputObject<TestInput>(
name = "TestInputObject",
fields = [
Define.Input("a", Nullable String)
Define.Input("b", Nullable(ListOf (Nullable String)))
Define.Input("c", String)
Define.Input("d", Nullable TestComplexScalar)
],
coerceInput =
(fun value ->
// This can be greatly simplified by utility functions that the user can design
match value with
| ObjectValue props ->
try
let a = props |> Map.tryFind "a" |> Option.bind String.CoerceInput

let b =
props
|> Map.tryFind "b"
|> Option.bind (
function
| ListValue xs ->
xs
|> Seq.map (
function
| StringValue s -> Some s
| NullValue -> None
)
|> Some
| _ -> None
)

let c = props |> Map.find "c" |> String.CoerceInput |> Option.toObj

let d = props |> Map.tryFind "d" |> Option.bind TestComplexScalar.CoerceInput

Some {
a = a
b = b
c = c
d = d
}
with _ -> None
| _ -> None))

let TestType =
Define.Object<unit>(
name = "TestType",
Expand All @@ -78,6 +128,7 @@ let TestType =
Define.Field("nnList", String, "", [ Define.Input("input", ListOf (Nullable String)) ], stringifyInput)
Define.Field("listNN", String, "", [ Define.Input("input", Nullable (ListOf String)) ], stringifyInput)
Define.Field("nnListNN", String, "", [ Define.Input("input", ListOf String) ], stringifyInput)
Define.Field("fieldWithObjectInputWithCustomCoercion", String, "", [ Define.Input("input", Nullable TestInputObjectWithCustomCoercion) ], stringifyInput)
])

let schema = Schema(TestType)
Expand Down Expand Up @@ -543,3 +594,18 @@ let ``Execute handles enum input as variable`` () =
data.["data"] |> equals (upcast expected)
| _ -> fail "Expected Direct GQResponse"


[<Fact>]
let ``Execute handles objects with custom coercion`` () =
let ast = parse """{ fieldWithObjectInputWithCustomCoercion(input: {a: "foo", b: ["bar"], c: "baz"}) }"""
let actual = sync <| Executor(schema).AsyncExecute(ast)
let expected =
NameValueLookup.ofList [
"fieldWithObjectInputWithCustomCoercion", upcast """{"a":"foo","b":["bar"],"c":"baz","d":null}"""
]

match actual with
| Direct(data, errors) ->
empty errors
data.["data"] |> equals (upcast expected)
| _ -> fail "Expected Direct GQResponse"