@@ -93,6 +93,8 @@ let inline tryConvertAst schema ast =
9393
9494 convert true schema ast
9595
96+ let compileByTypeCache = Collections.Concurrent.ConcurrentDictionary< InputDef, ExecuteInput>()
97+
9698let rec internal compileByType
9799 ( inputObjectPath : FieldPath )
98100 ( inputSource : InputSource )
@@ -103,6 +105,120 @@ let rec internal compileByType
103105
104106 | Scalar scalarDef -> variableOrElse ( InlineConstant >> scalarDef.CoerceInput)
105107 | InputCustom customDef -> fun inputContext value variables -> customDef.CoerceInput inputContext ( InlineConstant value) variables
108+
109+ //| Nullable (InputObject objDef)
110+ //| InputObject objDef when typeof<IDictionary<string,obj>>.IsAssignableFrom(objDef.Type) ->
111+ | InputObject objDef when typeof< Dynamic.ExpandoObject>. IsAssignableFrom( objDef.Type) ->
112+
113+ let executeInputFn = compileByTypeCache.GetOrAdd( objDef, Func<_,_>( fun _ ->
114+ let nestedFields =
115+ seq {
116+ for fld in objDef.Fields do
117+ match fld.TypeDef with
118+ | InputObject innerObjDef
119+ | Nullable ( InputObject innerObjDef)
120+ | List ( InputObject innerObjDef) ->
121+ // If the field contains a nested input object with a proper InputObject type definition
122+ // then we'll use the built in `executeInput` functionality to populate it.
123+ // **Caveat**: some of these objects may be recursively defined, so we need to delay the invocation of compileByType,
124+ // otherwise we get stack overflow at GraphQL schema compilation time.
125+ let delayedExecuteInputStub getInputContext value variables =
126+ let innerExecuteInput =
127+ compileByTypeCache.GetOrAdd( fld.TypeDef, Func<_,_>( fun _ ->
128+ compileByType ( box fld.Name :: inputObjectPath) inputSource ( fld.TypeDef, fld.TypeDef) getInputContext
129+ ))
130+ innerObjDef.ExecuteInput <- innerExecuteInput // replace ExecuteInput with true function on first call
131+ innerExecuteInput getInputContext value variables // call it
132+ innerObjDef.ExecuteInput <- delayedExecuteInputStub
133+ yield KeyValuePair( fld.Name, fld)
134+ | _ -> ()
135+ }
136+ |> Collections.Frozen.FrozenDictionary.ToFrozenDictionary
137+
138+ // Helper for populating fields from untyped input values
139+ let rec getRawInputValue ( variables : Variables ) ( value : InputValue ) =
140+ match value with
141+ | NullValue -> ValueNone
142+ | StringValue s -> ValueSome ( box s)
143+ | IntValue i -> ValueSome ( box i)
144+ | FloatValue f -> ValueSome ( box f)
145+ | BooleanValue b -> ValueSome ( box b)
146+ | EnumValue e -> ValueSome ( box e)
147+ | ListValue lst -> ValueSome ( box ( lst |> Seq.map ( fun el ->
148+ match getRawInputValue variables el with
149+ | ValueNone -> null
150+ | ValueSome v -> v
151+ )))
152+ | ObjectValue o ->
153+ let dict = Dictionary< string, obj>()
154+ for KeyValue ( key, prop) in o do
155+ match getRawInputValue variables prop with
156+ | ValueNone -> ()
157+ | ValueSome v -> dict[ key] <- v
158+ ValueSome ( box dict)
159+ | VariableName varName -> ValueSome ( variables[ varName])
160+
161+ // Populate dictionary from parsed ObjectValue (Map<string, InputValue>)
162+ let populateFromTypedInputObj ( props : Map < string , InputValue >) ( variables : Variables ) = result {
163+ let o = Activator.CreateInstance( objDef.Type)
164+ let dict = o :?> IDictionary< string, obj>
165+ for KeyValue ( key, prop) in props do
166+ match nestedFields.TryGetValue key with
167+ | true , fieldDef ->
168+ let! coerced = fieldDef.ExecuteInput getInputContext prop variables
169+ dict[ key] <- normalizeOptional fieldDef.TypeDef.Type coerced
170+ | false , _ ->
171+ match prop |> getRawInputValue variables with
172+ | ValueNone -> ()
173+ | ValueSome v -> dict[ key] <- v
174+ return o
175+ }
176+
177+ // Populate dictionary from variable value (untyped object)
178+ let populateFromUntypedInputObj ( props : IReadOnlyDictionary < string , obj >) ( variables : Variables ) = result {
179+ let o = Activator.CreateInstance( objDef.Type)
180+ let dict = o :?> IDictionary< string, obj>
181+ for KeyValue ( key, value) in props do
182+ match nestedFields.TryGetValue key with
183+ | true , fieldDef ->
184+ let inputValue = InputValue.OfObject value
185+ let! coerced =
186+ fieldDef.ExecuteInput getInputContext inputValue variables
187+ dict[ key] <- normalizeOptional fieldDef.TypeDef.Type coerced
188+ | false , _ -> dict[ key] <- value
189+ return o
190+ }
191+
192+ // return function: executeInput
193+ fun ctxGetter value variables ->
194+ match value with
195+ | ObjectValue props -> populateFromTypedInputObj props variables
196+ | VariableName variableName ->
197+ match variables.TryGetValue variableName with
198+ | true , found ->
199+ match found with
200+ | :? IReadOnlyDictionary< string, obj> as objectFields ->
201+ populateFromUntypedInputObj objectFields variables
202+ | null -> Ok null
203+ | _ ->
204+ Debugger.Break ()
205+ Error [
206+ { new IGQLError with
207+ member _. Message = $" A variable '${variableName}' is not an object"
208+ }
209+ ]
210+ | false , _ -> Ok null
211+ | NullValue -> populateFromTypedInputObj Map.empty variables
212+ | IntValue _ | FloatValue _ | BooleanValue _ | StringValue _ | EnumValue _ | ListValue _ ->
213+ Error [
214+ { new IGQLError with
215+ member _. Message = $" Input object expected, but got scalar value"
216+ }
217+ ]
218+ ))
219+ objDef.ExecuteInput <- executeInputFn
220+ executeInputFn
221+
106222 | InputObject objDef ->
107223 let objType = objDef.Type
108224 let ctor = ReflectionHelper.matchConstructor objType ( objDef.Fields |> Array.map _. Name)
0 commit comments