diff --git a/src/FSharp.Data.GraphQL.Client/BaseTypes.fs b/src/FSharp.Data.GraphQL.Client/BaseTypes.fs index ce527ebb..6867cd04 100644 --- a/src/FSharp.Data.GraphQL.Client/BaseTypes.fs +++ b/src/FSharp.Data.GraphQL.Client/BaseTypes.fs @@ -147,15 +147,15 @@ type RecordBase (name : string, properties : RecordProperty seq) = | :? string -> v // We need this because strings are enumerables, and we don't want to enumerate them recursively as an object | :? EnumBase as v -> v.GetValue () |> box | :? RecordBase as v -> box (v.ToDictionary ()) - | OptionValue v -> v |> Option.map mapDictionaryValue |> Option.toObj + | OptionValue v -> v |> ValueOption.map mapDictionaryValue |> ValueOption.toObj | EnumerableValue v -> v |> Array.map mapDictionaryValue |> box | _ -> v x.GetProperties () - |> Seq.choose (fun p -> + |> Seq.vchoose (fun p -> if not (isNull p.Value) then - Some (p.Name, mapDictionaryValue p.Value) + ValueSome (p.Name, mapDictionaryValue p.Value) else - None) + ValueNone) |> dict override x.ToString () = @@ -309,8 +309,8 @@ module internal JsonValueHelper = let getTypeName (fields : (string * JsonValue) seq) = fields - |> Seq.tryFind (fun (name, _) -> name = "__typename") - |> Option.map (fun (_, value) -> + |> Seq.vtryFind (fun (name, _) -> name = "__typename") + |> ValueOption.map (fun (_, value) -> match value with | JsonValue.String x -> x | _ -> failwithf "Expected \"__typename\" field to be a string field, but it was %A." value) @@ -379,16 +379,16 @@ module internal JsonValueHelper = | JsonValue.Record props -> let typeName = match getTypeName props with - | Some typeName -> typeName - | None -> failwith "Expected type to have a \"__typename\" field, but it was not found." + | ValueSome typeName -> typeName + | ValueNone -> failwith "Expected type to have a \"__typename\" field, but it was not found." let mapRecordProperty (aliasOrName : string, value : JsonValue) = let schemaField = match schemaField.Fields - |> Array.tryFind (fun f -> f.AliasOrName = aliasOrName) + |> Array.vtryFind (fun f -> f.AliasOrName = aliasOrName) with - | Some f -> f - | None -> + | ValueSome f -> f + | ValueNone -> failwithf "Expected to find field information for field with alias or name \"%s\" of type \"%s\" but it was not found." aliasOrName @@ -479,49 +479,49 @@ module internal JsonValueHelper = let getErrors (errors : JsonValue[]) = let tryFindField fieldName (fields : (string * JsonValue)[]) = fields - |> Array.tryFind (fun (name, _) -> name = fieldName) - |> Option.map snd + |> Array.vtryFind (fun (name, _) -> name = fieldName) + |> ValueOption.map snd - let parsePath = - function - | Some (JsonValue.Array path) -> + let parsePath jsonValueOpt = + match jsonValueOpt with + | ValueSome (JsonValue.Array path) -> let pathMapper = function | JsonValue.String x -> box x | JsonValue.Integer x -> box x | _ -> failwith "Error parsing response errors. An item in the path is neither a String nor an Integer." path |> Array.map pathMapper - | Some JsonValue.Null - | None -> [||] + | ValueSome JsonValue.Null + | ValueNone -> [||] | _ -> failwith "Error parsing response errors. Path field must be an Array." - let parseLocations = - function - | Some (JsonValue.Array locations) -> + let parseLocations jsonValueOpt = + match jsonValueOpt with + | ValueSome (JsonValue.Array locations) -> let parseLocation = function | JsonValue.Record locationFields -> match tryFindField "line" locationFields, tryFindField "column" locationFields with - | Some (JsonValue.Integer line), Some (JsonValue.Integer column) -> { Line = line; Column = column } + | ValueSome (JsonValue.Integer line), ValueSome (JsonValue.Integer column) -> { Line = line; Column = column } | _ -> failwith "Error parsing response errors. A location item must contain Integer fields named \"line\" and \"column\"." | _ -> failwith "Error parsing response errors. A location item is not a Record." locations |> Array.map parseLocation - | Some JsonValue.Null - | None -> [||] + | ValueSome JsonValue.Null + | ValueNone -> [||] | _ -> failwith "Error parsing response errors. Locations field must be an Array." - let parseExtensions = - function - | Some (JsonValue.Record fields) -> Serialization.deserializeMap fields - | Some JsonValue.Null - | None -> Map.empty + let parseExtensions jsonValueOpt = + match jsonValueOpt with + | ValueSome (JsonValue.Record fields) -> Serialization.deserializeMap fields + | ValueSome JsonValue.Null + | ValueNone -> Map.empty | _ -> failwith "Error parsing response errors. Extensions field must be a Record." let errorMapper = function | JsonValue.Record fields -> match tryFindField "message" fields with - | Some (JsonValue.String message) -> { + | ValueSome (JsonValue.String message) -> { Message = message Locations = tryFindField "locations" fields |> parseLocations Path = tryFindField "path" fields |> parsePath @@ -597,6 +597,6 @@ module VariableMapping = | :? string -> value | :? EnumBase as v -> v.GetValue () |> box | :? RecordBase as v -> v.ToDictionary () |> box - | OptionValue v -> v |> Option.map mapVariableValue |> box + | OptionValue v -> v |> ValueOption.map mapVariableValue |> ValueOption.toObj | EnumerableValue v -> v |> Array.map mapVariableValue |> box | v -> v diff --git a/src/FSharp.Data.GraphQL.Client/GraphQLClient.fs b/src/FSharp.Data.GraphQL.Client/GraphQLClient.fs index 6a757fc2..bc5291f8 100644 --- a/src/FSharp.Data.GraphQL.Client/GraphQLClient.fs +++ b/src/FSharp.Data.GraphQL.Client/GraphQLClient.fs @@ -125,24 +125,21 @@ module GraphQLClient = let rec tryMapFileVariable (name : string, value : obj) = match value with | null - | :? string -> None - | :? Upload as x -> Some [| name, x |] - | OptionValue x -> x |> Option.bind (fun x -> tryMapFileVariable (name, x)) + | :? string -> [||] + | :? Upload as x -> [| struct (name, x) |] + | OptionValue x -> x |> ValueOption.map (fun x -> tryMapFileVariable (name, x)) |> ValueOption.defaultValue [||] | :? IDictionary as x -> x - |> Seq.collect (fun kvp -> - tryMapFileVariable (name + "." + (kvp.Key.FirstCharLower ()), kvp.Value) - |> Option.defaultValue [||]) - |> Array.ofSeq - |> Some + |> Seq.collect (fun kvp -> tryMapFileVariable (name + "." + (kvp.Key.FirstCharLower ()), kvp.Value)) + |> Seq.toArray | EnumerableValue x -> x - |> Array.mapi (fun ix x -> tryMapFileVariable ($"%s{name}.%i{ix}", x)) - |> Array.collect (Option.defaultValue [||]) - |> Some - | _ -> None + |> Seq.mapi (fun ix x -> tryMapFileVariable ($"%s{name}.%i{ix}", x)) + |> Seq.collect id + |> Seq.toArray + | _ -> [||] request.Variables - |> Array.collect (tryMapFileVariable >> (Option.defaultValue [||])) + |> Array.collect tryMapFileVariable let operationContent = let variables = @@ -171,7 +168,7 @@ module GraphQLClient = let mapContent = let files = files - |> Array.mapi (fun ix (name, _) -> ix.ToString (), JsonValue.Array [| JsonValue.String ("variables." + name) |]) + |> Array.mapi (fun ix struct (name, _) -> ix.ToString (), JsonValue.Array [| JsonValue.String ("variables." + name) |]) |> JsonValue.Record let content = new StringContent (files.ToString (JsonSaveOptions.DisableFormatting)) content.Headers.Add ("Content-Disposition", "form-data; name=\"map\"") @@ -179,7 +176,7 @@ module GraphQLClient = content.Add (mapContent) let fileContents = files - |> Seq.mapi (fun _ (_, value) -> + |> Seq.mapi (fun _ struct (_, value) -> let content = new StreamContent (value.Stream) content.Headers.Add ("Content-Disposition", $"form-data; name=\"%s{value.Name}\"; filename=\"%s{value.FileName}\"") content.Headers.Add ("Content-Type", value.ContentType) diff --git a/src/FSharp.Data.GraphQL.Client/ReflectionPatterns.fs b/src/FSharp.Data.GraphQL.Client/ReflectionPatterns.fs index 55a7eadc..c86478d8 100644 --- a/src/FSharp.Data.GraphQL.Client/ReflectionPatterns.fs +++ b/src/FSharp.Data.GraphQL.Client/ReflectionPatterns.fs @@ -143,8 +143,8 @@ module ReflectionPatterns = let xtype = x.GetType() let tryGetValue optionType = match FSharpValue.GetUnionFields(x, optionType) with - | (_, [|value|]) -> ValueSome (OptionValue Some value) - | _ -> ValueSome (OptionValue None) + | (_, [|value|]) -> ValueSome (OptionValue ValueSome value) + | _ -> ValueSome (OptionValue ValueNone) if isOption xtype then tryGetValue xtype elif isValueOption xtype @@ -162,10 +162,10 @@ module ReflectionPatterns = let isOption = isOption t match value, isOption with | null, true -> makeNone t - | OptionValue (Some null), true -> box (makeSome (Convert.ChangeType(null, t))) - | OptionValue (Some value), true -> box (makeSome value) - | OptionValue (Some value), false -> Convert.ChangeType(value, t) - | OptionValue None, false -> Convert.ChangeType(null, t) - | OptionValue None, true -> box (makeNone t) + | OptionValue (ValueSome null), true -> box (makeSome (Convert.ChangeType(null, t))) + | OptionValue (ValueSome value), true -> box (makeSome value) + | OptionValue (ValueSome value), false -> Convert.ChangeType(value, t) + | OptionValue ValueNone, false -> Convert.ChangeType(null, t) + | OptionValue ValueNone, true -> box (makeNone t) | value, true -> makeSome value | value, false -> Convert.ChangeType(value, t) diff --git a/src/FSharp.Data.GraphQL.Client/Serialization.fs b/src/FSharp.Data.GraphQL.Client/Serialization.fs index 4939c5ee..34761704 100644 --- a/src/FSharp.Data.GraphQL.Client/Serialization.fs +++ b/src/FSharp.Data.GraphQL.Client/Serialization.fs @@ -169,7 +169,7 @@ module Serialization = Tracer.runAndMeasureExecutionTime $"Converted object type %O{t} to JsonValue" (fun _ -> match x with | null -> JsonValue.Null - | OptionValue None -> JsonValue.Null + | OptionValue ValueNone -> JsonValue.Null | :? int as x -> JsonValue.Integer (int x) | :? float as x -> JsonValue.Float x | :? string as x -> JsonValue.String x @@ -189,7 +189,7 @@ module Serialization = items |> Array.map toJsonValue |> JsonValue.Array - | OptionValue (Some x) -> toJsonValue x + | OptionValue (ValueSome x) -> toJsonValue x | EnumValue x -> JsonValue.String x | _ -> let props = t.GetProperties(BindingFlags.Public ||| BindingFlags.Instance) diff --git a/src/FSharp.Data.GraphQL.Server/Execution.fs b/src/FSharp.Data.GraphQL.Server/Execution.fs index af71afc4..5a200885 100644 --- a/src/FSharp.Data.GraphQL.Server/Execution.fs +++ b/src/FSharp.Data.GraphQL.Server/Execution.fs @@ -13,7 +13,6 @@ open FSharp.Data.GraphQL.Ast open FSharp.Data.GraphQL.Errors open FSharp.Data.GraphQL.Extensions open FSharp.Data.GraphQL.Helpers -open FSharp.Data.GraphQL.Shared open FSharp.Data.GraphQL.Types open FSharp.Data.GraphQL.Types.Patterns open FSharp.Data.GraphQL @@ -40,22 +39,24 @@ let internal argumentValue inputContext variables (argDef: InputFieldDef) (argum let private getArgumentValues (argDefs: InputFieldDef []) (args: Argument list) (inputContext : InputExecutionContextProvider) (variables: ImmutableDictionary) : Result, IGQLError list> = argDefs - |> Array.fold (fun acc argdef -> - match List.tryFind (fun (a: Argument) -> a.Name = argdef.Name) args with - | Some argument -> validation { + |> Array.fold + (fun acc argdef -> + match List.vtryFind (fun (a : Argument) -> a.Name = argdef.Name) args with + | ValueSome argument -> validation { let! acc = acc and! arg = argumentValue inputContext variables argdef argument match arg with | null -> return acc | v -> return Map.add argdef.Name v acc - } - | None -> validation { + } + | ValueNone -> validation { let! acc = acc return collectDefaultArgValue acc argdef } ) (Ok Map.empty) -let private getOperation = function +let private getOperation definition = + match definition with | OperationDefinition odef -> ValueSome odef | _ -> ValueNone @@ -104,23 +105,21 @@ let private createFieldContext objdef inputContext argDefs ctx (info: ExecutionI Path = normalizeErrorPath path } } -let private resolveField (execute: ExecuteField) (ctx: ResolveFieldContext) (parentValue: obj) = - if ctx.ExecutionInfo.IsNullable - then - execute ctx parentValue - |> AsyncVal.map(optionCast) +let private resolveField (execute : ExecuteField) (ctx : ResolveFieldContext) (parentValue : obj) = + if ctx.ExecutionInfo.IsNullable then + execute ctx parentValue |> AsyncVal.map (objectOptionCast) else execute ctx parentValue - |> AsyncVal.map(fun v -> if isNull v then None else Some v) + |> AsyncVal.map (fun v -> if isNull v then ValueNone else ValueSome v) -type ResolverResult<'T> = Result<'T * IObservable option * GQLProblemDetails list, GQLProblemDetails list> +type ResolverResult<'T> = Result<'T * IObservable voption * GQLProblemDetails list, GQLProblemDetails list> [] module ResolverResult = - let data data = Ok (data, None, []) - let defered data deferred = Ok (data, Some deferred, []) + let data data = Ok (data, ValueNone, []) + let defered data deferred = Ok (data, ValueSome deferred, []) let mapValue (f : 'T -> 'U) (r : ResolverResult<'T>) : ResolverResult<'U> = Result.map(fun (data, deferred, errs) -> (f data, deferred, errs)) r @@ -145,14 +144,23 @@ let private raiseErrors errs = AsyncVal.wrap <| Error errs /// to a list of GQLProblemDetails. let private resolverError path ctx e = ctx.Schema.ParseError path e |> List.map (GQLProblemDetails.OfFieldExecutionError (normalizeErrorPath path)) // Helper functions for generating more specific GQLProblemDetails. -let private nullResolverError name path ctx = resolverError path ctx (GQLMessageException <| sprintf "Non-Null field %s resolved as a null!" name) -let private coercionError value tyName path ctx = resolverError path ctx (GQLMessageException <| sprintf "Value '%O' could not be coerced to scalar %s" value tyName) -let private interfaceImplError ifaceName tyName path ctx = resolverError path ctx (GQLMessageException <| sprintf "GraphQL Interface '%s' is not implemented by the type '%s'" ifaceName tyName) -let private unionImplError unionName tyName path ctx = resolverError path ctx (GQLMessageException (sprintf "GraphQL Union '%s' is not implemented by the type '%s'" unionName tyName)) -let private deferredNullableError name tyName path ctx = resolverError path ctx (GQLMessageException (sprintf "Deferred field %s of type '%s' must be nullable" name tyName)) -let private streamListError name tyName path ctx = resolverError path ctx (GQLMessageException (sprintf "Streamed field %s of type '%s' must be list" name tyName)) - -let private resolved name v : AsyncVal>> = KeyValuePair(name, box v) |> ResolverResult.data |> AsyncVal.wrap +let private nullResolverError name path ctx = + resolverError path ctx (GQLMessageException $"Non-Null field %s{name} resolved as a null!") +let private coercionError value tyName path ctx = + resolverError path ctx (GQLMessageException $"Value '{value}' could not be coerced to scalar %s{tyName}") +let private interfaceImplError ifaceName tyName path ctx = + resolverError path ctx (GQLMessageException $"GraphQL Interface '%s{ifaceName}' is not implemented by the type '%s{tyName}'") +let private unionImplError unionName tyName path ctx = + resolverError path ctx (GQLMessageException $"GraphQL Union '%s{unionName}' is not implemented by the type '%s{tyName}'") +let private deferredNullableError name tyName path ctx = + resolverError path ctx (GQLMessageException $"Deferred field %s{name} of type '%s{tyName}' must be nullable") +let private streamListError name tyName path ctx = + resolverError path ctx (GQLMessageException $"Streamed field %s{name} of type '%s{tyName}' must be list") + +let private resolved name v : AsyncVal>> = + KeyValuePair(name, box v) + |> ResolverResult.data + |> AsyncVal.wrap let deferResults path (res : ResolverResult) : IObservable = let formattedPath = normalizeErrorPath path @@ -163,30 +171,30 @@ let deferResults path (res : ResolverResult) : IObservable DeferredResult (data, formattedPath) | _ -> DeferredErrors (data |> ValueOption.ofObj, errs, formattedPath) |> Observable.singleton - Option.foldBack Observable.concat deferred deferredData + ValueOption.foldBack Observable.concat deferred deferredData | Error errs -> Observable.singleton <| DeferredErrors (ValueNone, errs, formattedPath) /// Collect together an array of results using the appropriate execution strategy. let collectFields (strategy : ExecutionStrategy) (rs : AsyncVal>> []) : AsyncVal []>> = asyncVal { - let! collected = - match strategy with - | Parallel -> AsyncVal.collectParallel rs - | Sequential -> AsyncVal.collectSequential rs - - let data = Array.zeroCreate (collected.Length) - - let merge r acc = - match (r, acc) with - | Ok(field, d, e), Ok(i, deferred, errs) -> - Array.set data i field - Ok(i - 1, Option.mergeWith Observable.merge deferred d, e @ errs) - | Error e, Ok (_, _, errs) -> Error (e @ errs) - | Ok (_, _, e), Error errs -> Error (e @ errs) - | Error e, Error errs -> Error (e @ errs) - return - Array.foldBack merge collected (Ok (data.Length - 1, None, [])) - |> ResolverResult.mapValue(fun _ -> data) - } + let! collected = + match strategy with + | Parallel -> AsyncVal.collectParallel rs + | Sequential -> AsyncVal.collectSequential rs + + let data = Array.zeroCreate (collected.Length) + + let merge r acc = + match (r, acc) with + | Ok (field, d, e), Ok (i, deferred, errs) -> + Array.set data i field + Ok (i - 1, ValueOption.mergeWith Observable.merge deferred d, e @ errs) + | Error e, Ok (_, _, errs) -> Error (e @ errs) + | Ok (_, _, e), Error errs -> Error (e @ errs) + | Error e, Error errs -> Error (e @ errs) + return + Array.foldBack merge collected (Ok (data.Length - 1, ValueNone, [])) + |> ResolverResult.mapValue (fun _ -> data) +} let rec private direct (returnDef : OutputDef) (inputContext : InputExecutionContextProvider) (ctx : ResolveFieldContext) (path : FieldPath) (parent : obj) (value : obj) : AsyncVal>> = let name = ctx.ExecutionInfo.Identifier @@ -205,10 +213,12 @@ let rec private direct (returnDef : OutputDef) (inputContext : InputExecutionCon | None -> raiseErrors <| coercionError value scalarDef.Name path ctx | Enum enumDef -> - let enumCase = enumDef.Options |> Array.tryPick(fun case -> if case.Value.Equals(value) then Some case.Name else None) + let enumCase = + enumDef.Options + |> Array.vtryPick (fun case -> if case.Value.Equals (value) then ValueSome case.Name else ValueNone) match enumCase with - | Some v' -> resolved name (v' :> obj) - | None -> raiseErrors <| coercionError value enumDef.Name path ctx + | ValueSome v' -> resolved name (v' :> obj) + | ValueNone -> raiseErrors <| coercionError value enumDef.Name path ctx | List (Output innerDef) -> let innerCtx = @@ -216,7 +226,7 @@ let rec private direct (returnDef : OutputDef) (inputContext : InputExecutionCon | ResolveCollection innerPlan -> { ctx with ExecutionInfo = { innerPlan with ReturnDef = innerDef } } | kind -> failwithf "Unexpected value of ctx.ExecutionPlan.Kind: %A" kind let resolveItem index item = - executeResolvers inputContext innerCtx (box index :: path) value (toOption item |> AsyncVal.wrap) + executeResolvers inputContext innerCtx (box index :: path) value (toValueOption item |> AsyncVal.wrap) let resolveItems (items : obj[]) = items |> Array.mapi resolveItem @@ -247,9 +257,15 @@ let rec private direct (returnDef : OutputDef) (inputContext : InputExecutionCon | _ -> raise <| GQLMessageException (ErrorMessages.expectedEnumerableValue ctx.ExecutionInfo.Identifier (value.GetType())) | Nullable (Output innerDef) -> - let innerCtx = { ctx with ExecutionInfo = { ctx.ExecutionInfo with IsNullable = true; ReturnDef = innerDef } } - executeResolvers inputContext innerCtx path parent (toOption value |> AsyncVal.wrap) - |> AsyncVal.map(Result.valueOr (fun errs -> (KeyValuePair(name, null), None, errs)) >> Ok) + let innerCtx = { + ctx with + ExecutionInfo = { ctx.ExecutionInfo with IsNullable = true; ReturnDef = innerDef } + } + executeResolvers inputContext innerCtx path parent (toValueOption value |> AsyncVal.wrap) + |> AsyncVal.map ( + Result.valueOr (fun errs -> (KeyValuePair (name, null), ValueNone, errs)) + >> Ok + ) | Interface iDef -> let possibleTypesFn = ctx.Schema.GetPossibleTypes @@ -259,9 +275,9 @@ let rec private direct (returnDef : OutputDef) (inputContext : InputExecutionCon match ctx.ExecutionInfo.Kind with | ResolveAbstraction typeMap -> typeMap | kind -> failwithf $"Unexpected value of ctx.ExecutionPlan.Kind: %A{kind}" - match Map.tryFind resolvedDef.Name typeMap with - | Some fields -> executeObjectFields fields name resolvedDef inputContext ctx path value - | None -> KeyValuePair(name, obj()) |> ResolverResult.data |> AsyncVal.wrap + match Map.vtryFind resolvedDef.Name typeMap with + | ValueSome fields -> executeObjectFields fields name resolvedDef inputContext ctx path value + | ValueNone -> KeyValuePair(name, obj()) |> ResolverResult.data |> AsyncVal.wrap | Union uDef -> let possibleTypesFn = ctx.Schema.GetPossibleTypes @@ -271,16 +287,16 @@ let rec private direct (returnDef : OutputDef) (inputContext : InputExecutionCon match ctx.ExecutionInfo.Kind with | ResolveAbstraction typeMap -> typeMap | kind -> failwithf $"Unexpected value of ctx.ExecutionPlan.Kind: %A{kind}" - match Map.tryFind resolvedDef.Name typeMap with - | Some fields -> executeObjectFields fields name resolvedDef inputContext ctx path (uDef.ResolveValue value) - | None -> KeyValuePair(name, obj()) |> ResolverResult.data |> AsyncVal.wrap + match Map.vtryFind resolvedDef.Name typeMap with + | ValueSome fields -> executeObjectFields fields name resolvedDef inputContext ctx path (uDef.ResolveValue value) + | ValueNone -> KeyValuePair(name, obj()) |> ResolverResult.data |> AsyncVal.wrap | _ -> failwithf "Unexpected value of returnDef: %O" returnDef and deferred (inputContext : InputExecutionContextProvider) (ctx : ResolveFieldContext) (path : FieldPath) (parent : obj) (value : obj) = let info = ctx.ExecutionInfo let deferred = - executeResolvers inputContext ctx path parent (toOption value |> AsyncVal.wrap) + executeResolvers inputContext ctx path parent (toValueOption value |> AsyncVal.wrap) |> Observable.ofAsyncVal |> Observable.bind(ResolverResult.mapValue(_.Value) >> deferResults path) ResolverResult.defered (KeyValuePair (info.Identifier, null)) deferred |> AsyncVal.wrap @@ -314,9 +330,9 @@ and private streamed (options : BufferedStreamOptions) (innerDef : OutputDef) (i match r with | Ok (item, d, e) -> Array.set data i item.Value - (i - 1, box index :: indices, Option.mergeWith Observable.merge deferred d, e @ errs) + (i - 1, box index :: indices, ValueOption.mergeWith Observable.merge deferred d, e @ errs) | Error e -> (i - 1, box index :: indices, deferred, e @ errs) - let (_, indices, deferred, errs) = List.foldBack merge chunk (chunk.Length - 1, [], None, []) + let (_, indices, deferred, errs) = List.foldBack merge chunk (chunk.Length - 1, [], ValueNone, []) deferResults (box indices :: path) (Ok (box data, deferred, errs)) let collectBuffered (events : StreamEvent list) : IObservable = @@ -343,7 +359,7 @@ and private streamed (options : BufferedStreamOptions) (innerDef : OutputDef) (i |> Observable.bind collectBuffered let resolveItem index item = asyncVal { - let! result = executeResolvers inputContext innerCtx (box index :: path) parent (toOption item |> AsyncVal.wrap) + let! result = executeResolvers inputContext innerCtx (box index :: path) parent (toValueOption item |> AsyncVal.wrap) return (index, result) } @@ -388,7 +404,7 @@ and private live (inputContext : InputExecutionContextProvider) (ctx : ResolveFi /// So the updatedValue here is actually the fresh parent. let resolveUpdate updatedValue = - executeResolvers inputContext ctx path parent (updatedValue |> Some |> AsyncVal.wrap) + executeResolvers inputContext ctx path parent (updatedValue |> ValueSome |> AsyncVal.wrap) |> AsyncVal.map(ResolverResult.mapValue(fun d -> d.Value) >> deferResults path) |> Observable.ofAsyncVal |> Observable.mergeInner @@ -400,12 +416,12 @@ and private live (inputContext : InputExecutionContextProvider) (ctx : ResolveFi | Some filterFn -> provider.Add (filterFn parent) typeName name |> Observable.bind resolveUpdate | None -> failwithf "No live provider for %s:%s" typeName name - executeResolvers inputContext ctx path parent (value |> Some |> AsyncVal.wrap) + executeResolvers inputContext ctx path parent (value |> ValueSome |> AsyncVal.wrap) // TODO: Add tests for `Observable.merge deferred updates` correct order - |> AsyncVal.map(Result.map(fun (data, deferred, errs) -> (data, Some <| Option.foldBack Observable.merge deferred updates, errs))) + |> AsyncVal.map(Result.map(fun (data, deferred, errs) -> (data, ValueSome <| ValueOption.foldBack Observable.merge deferred updates, errs))) /// Actually execute the resolvers. -and private executeResolvers (inputContext : InputExecutionContextProvider) (ctx : ResolveFieldContext) (path : FieldPath) (parent : obj) (value : AsyncVal) : AsyncVal>> = +and private executeResolvers (inputContext : InputExecutionContextProvider) (ctx : ResolveFieldContext) (path : FieldPath) (parent : obj) (value : AsyncVal) : AsyncVal>> = let info = ctx.ExecutionInfo let name = info.Identifier let returnDef = info.ReturnDef @@ -429,17 +445,17 @@ and private executeResolvers (inputContext : InputExecutionContextProvider) (ctx |> Seq.toList | false, _ -> [] match resolved with - | Error errs when ctx.ExecutionInfo.IsNullable -> return Ok (KeyValuePair(name, null), None, errs @ additionalErrs) - | Ok None when ctx.ExecutionInfo.IsNullable -> return Ok (KeyValuePair(name, null), None, additionalErrs) + | Error errs when ctx.ExecutionInfo.IsNullable -> return Ok (KeyValuePair(name, null), ValueNone, errs @ additionalErrs) + | Ok ValueNone when ctx.ExecutionInfo.IsNullable -> return Ok (KeyValuePair(name, null), ValueNone, additionalErrs) | Error errs -> return Error (errs @ additionalErrs) - | Ok None -> return Error ((nullResolverError name path ctx) @ additionalErrs) - | Ok (Some v) -> + | Ok ValueNone -> return Error ((nullResolverError name path ctx) @ additionalErrs) + | Ok (ValueSome v) -> let! onSuccessResult = try onSuccess ctx path parent v with e -> resolverError path ctx e |> Error |> AsyncVal.wrap match onSuccessResult with | Ok (res, deferred, errs) -> return Ok (res, deferred, errs @ additionalErrs) - | Error errs when ctx.ExecutionInfo.IsNullable -> return Ok (KeyValuePair(name, null), None, errs @ additionalErrs) + | Error errs when ctx.ExecutionInfo.IsNullable -> return Ok (KeyValuePair(name, null), ValueNone, errs @ additionalErrs) | Error errs -> return Error (errs @ additionalErrs) } @@ -462,24 +478,34 @@ and private executeResolvers (inputContext : InputExecutionContextProvider) (ctx |> resolveWith ctx -and executeObjectFields (fields : ExecutionInfo list) (objName : string) (objDef : ObjectDef) (inputContext: InputExecutionContextProvider) (ctx : ResolveFieldContext) (path : FieldPath) (value : obj) : AsyncVal>> = asyncVal { - let executeField field = - let argDefs = ctx.Context.FieldExecuteMap.GetArgs(objDef.Name, field.Definition.Name) - let resolver = ctx.Context.FieldExecuteMap.GetExecute(objDef.Name, field.Definition.Name) - let fieldPath = (box field.Identifier :: path) - match createFieldContext objDef inputContext argDefs ctx field fieldPath with - | Ok fieldCtx -> executeResolvers inputContext fieldCtx fieldPath value (resolveField resolver fieldCtx value) - | Error errs -> asyncVal { return Error (errs |> List.map GQLProblemDetails.OfError) } - - let! res = - fields - |> Seq.map executeField - |> Seq.toArray - |> collectFields Parallel - match res with - | Error errs -> return Error errs - | Ok(kvps, def, errs) -> return Ok (KeyValuePair(objName, box <| NameValueLookup(kvps)), def, errs) -} +and executeObjectFields + (fields : ExecutionInfo list) + (objName : string) + (objDef : ObjectDef) + (inputContext: InputExecutionContextProvider) + (ctx : ResolveFieldContext) + (path : FieldPath) + (value : obj) + : AsyncVal>> + = + asyncVal { + let executeField field = + let argDefs = ctx.Context.FieldExecuteMap.GetArgs(objDef.Name, field.Definition.Name) + let resolver = ctx.Context.FieldExecuteMap.GetExecute(objDef.Name, field.Definition.Name) + let fieldPath = (box field.Identifier :: path) + match createFieldContext objDef inputContext argDefs ctx field fieldPath with + | Ok fieldCtx -> executeResolvers inputContext fieldCtx fieldPath value (resolveField resolver fieldCtx value) + | Error errs -> asyncVal { return Error (errs |> List.map GQLProblemDetails.OfError) } + + let! res = + fields + |> Seq.map executeField + |> Seq.toArray + |> collectFields Parallel + match res with + | Error errs -> return Error errs + | Ok(kvps, def, errs) -> return Ok (KeyValuePair(objName, box <| NameValueLookup(kvps)), def, errs) + } let internal compileSubscriptionField (subfield: SubscriptionFieldDef) = match subfield.Resolve with @@ -535,7 +561,7 @@ let private executeQueryOrMutation (resultSet: (string * ExecutionInfo) []) (ctx | Ok (Error errs) | Error errs -> Error errs match result with - | Error errs when info.IsNullable -> return Ok (KeyValuePair(name, null), None, errs) + | Error errs when info.IsNullable -> return Ok (KeyValuePair(name, null), ValueNone, errs) | Error errs -> return Error errs | Ok r -> return Ok r } @@ -561,8 +587,8 @@ let private executeQueryOrMutation (resultSet: (string * ExecutionInfo) []) (ctx |> Seq.map (fun (KeyValue (i, struct (args, _))) -> executeRootOperation resultSet[i] args) |> Seq.toArray match! operations |> collectFields ctx.ExecutionPlan.Strategy with - | Ok (data, Some deferred, errs) -> return GQLExecutionResult.Deferred(documentId, NameValueLookup(data), errs, deferred, ctx.Metadata) - | Ok (data, None, errs) -> return GQLExecutionResult.Direct(documentId, NameValueLookup(data), errs, ctx.Metadata) + | Ok (data, ValueSome deferred, errs) -> return GQLExecutionResult.Deferred(documentId, NameValueLookup(data), errs, deferred, ctx.Metadata) + | Ok (data, ValueNone, errs) -> return GQLExecutionResult.Direct(documentId, NameValueLookup(data), errs, ctx.Metadata) // Only a non-null root field failing during execution reaches this branch: an execution result whose // data is null, as the spec requires, unlike the request error returned above for a coercion failure | Error errs -> return GQLExecutionResult.Direct(documentId, null, errs, ctx.Metadata) @@ -585,10 +611,10 @@ let private executeSubscription (resultSet: (string * ExecutionInfo) []) (inputC Variables = ctx.Variables Path = fieldPath |> List.rev } let onValue v = asyncVal { - match! executeResolvers inputContext fieldCtx fieldPath value (toOption v |> AsyncVal.wrap) with - | Ok (data, None, []) -> return SubscriptionResult (NameValueLookup.ofList [nameOrAlias, data.Value]) - | Ok (data, None, errs) -> return SubscriptionErrors (ValueSome (NameValueLookup.ofList [nameOrAlias, data.Value]), errs) - | Ok (_, Some _, _) -> return failwith "Deferred/Streamed/Live are not supported for subscriptions!" + match! executeResolvers inputContext fieldCtx fieldPath value (toValueOption v |> AsyncVal.wrap) with + | Ok (data, ValueNone, []) -> return SubscriptionResult (NameValueLookup.ofList [nameOrAlias, data.Value]) + | Ok (data, ValueNone, errs) -> return SubscriptionErrors (ValueSome (NameValueLookup.ofList [nameOrAlias, data.Value]), errs) + | Ok (_, ValueSome _, _) -> return failwith "Deferred/Streamed/Live are not supported for subscriptions!" | Error errs -> return SubscriptionErrors (ValueNone, errs) } return diff --git a/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj b/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj index 3bd336b2..3dc6ee7b 100644 --- a/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj +++ b/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj @@ -38,6 +38,7 @@ + diff --git a/src/FSharp.Data.GraphQL.Shared/Helpers/CollectionExtensions.fs b/src/FSharp.Data.GraphQL.Shared/Helpers/CollectionExtensions.fs new file mode 100644 index 00000000..87a0fd50 --- /dev/null +++ b/src/FSharp.Data.GraphQL.Shared/Helpers/CollectionExtensions.fs @@ -0,0 +1,209 @@ +namespace rec FSharp.Data.GraphQL + +open System.Collections.Generic + +[] +module KeyValuePairExtensions = + + let inline kvp key value = KeyValuePair (key, value) + let inline kvpObj key (value : obj) = KeyValuePair (key, value) + +module internal Seq = + + let vtryHead (source : 'T seq) = + use enumerator = source.GetEnumerator () + if not (enumerator.MoveNext ()) then + ValueNone + else + ValueSome enumerator.Current + + let vtryLast (source : 'T seq) = + use enumerator = source.GetEnumerator () + if not (enumerator.MoveNext ()) then + ValueNone + else + let mutable last = enumerator.Current + while enumerator.MoveNext () do + last <- enumerator.Current + ValueSome last + + let vchoose mapping seq = + seq + |> Seq.map mapping + |> Seq.where ValueOption.isSome + |> Seq.map ValueOption.get + + let vtryFind predicate (source : 'T seq) = source |> Seq.where predicate |> Seq.vtryHead + + let vtryItem index (source : 'T seq) = + if index < 0 then + ValueNone + else + use enumerator = source.GetEnumerator () + let mutable currentIndex = 0 + let mutable result = ValueNone + let mutable found = false + + while not found && enumerator.MoveNext () do + if currentIndex = index then + result <- ValueSome enumerator.Current + found <- true + else + currentIndex <- currentIndex + 1 + + result + +module internal List = + + let vchoose mapping list = list |> Seq.vchoose mapping |> Seq.toList + + /// + /// Merges elements of two lists, returning a new list without duplicates. + /// + /// Function used to determine if any two given elements are considered equal. + /// First list with elements to merge. + /// Second list with elements to merge. + let mergeBy f listx listy = + let uniqx = + listx + |> List.filter (fun x -> not <| List.exists (fun y -> f x = f y) listy) + uniqx @ listy + + /// + /// Attempts to find the first element in a list that satisfies the given predicate. + /// + /// Function to test each element. + /// The input list. + /// ValueSome of the first matching element, or ValueNone if no match is found. + let vtryFind predicate list = list |> Seq.where predicate |> Seq.vtryHead + + let vtryItem index list = + let rec loop currentIndex list = + match currentIndex, list with + | _, [] -> ValueNone + | 0, head :: _ -> ValueSome head + | currentIndex, _ :: tail when currentIndex > 0 -> loop (currentIndex - 1) tail + | _ -> ValueNone + + loop index list + + /// + /// Applies a function to each element of a list and returns the first result where the function returns ValueSome. + /// + /// Function to apply to each element. + /// The input list. + /// ValueSome of the first successful mapping result, or ValueNone if no match is found. + let rec vtryPick mapping (source : 'T list) = + match source with + | [] -> ValueNone + | head :: tail -> + match mapping head with + | ValueSome result -> ValueSome result + | ValueNone -> vtryPick mapping tail + +module internal Array = + + let vchoose mapping array = array |> Seq.vchoose mapping |> Seq.toArray + + /// + /// Returns a new array with unique elements. Uniqueness is determined by + /// output of the function. + /// + /// Function, which output is used to determine uniqueness of input elements. + /// Array of elements. + let distinctBy keyf (array : 'T array) = + let temp = Array.zeroCreate array.Length + let mutable i = 0 + let hashSet = HashSet<_>(HashIdentity.Structural<_>) + for v in array do + if hashSet.Add (keyf v) then + temp.[i] <- v + i <- i + 1 + Array.sub temp 0 i + + let vtryItem index (array : 'T array) = + if index < 0 || index >= array.Length then + ValueNone + else + ValueSome array[index] + + /// + /// Attempts to find the first element in an array that satisfies the given predicate. + /// + /// Function to test each element. + /// The input array. + /// ValueSome of the first matching element, or ValueNone if no match is found. + let vtryFind predicate (array : 'T array) = + let mutable i = 0 + let mutable result = ValueNone + while i < array.Length && result.IsNone do + if predicate array[i] then + result <- ValueSome array[i] + i <- i + 1 + result + + /// + /// Applies a function to each element of an array and returns the first result where the function returns ValueSome. + /// + /// Function to apply to each element. + /// The input array. + /// ValueSome of the first successful mapping result, or ValueNone if no match is found. + let vtryPick (chooser : 'T -> 'U voption) (source : 'T array) = + let mutable i = 0 + let mutable result = ValueNone + while i < source.Length && result.IsNone do + result <- chooser source[i] + i <- i + 1 + result + +module internal Map = + + let vtryFind key (map : Map<_, _>) = + match map.TryGetValue key with + | true, value -> ValueSome value + | false, _ -> ValueNone + + /// + /// Merges the entries of two maps by their key, returning new map in result. + /// + /// + /// Function, which takes key shared by entries in both maps, first entry's value, + /// second entry's value to produce a result value used in newly generated map. + /// + /// First map with elements to merge. + /// Second map with elements to merge. + let merge mergeFn mapx mapy = + mapy + |> Map.fold + (fun acc ky vy -> + match Map.tryFind ky acc with + | Some vx -> Map.add ky (mergeFn ky vx vy) acc + | None -> Map.add ky vy acc) + mapx + +module Dictionary = + + let addWith (f : 'V -> 'V -> 'V) (key : 'K) (value : 'V) (dict : Dictionary<'K, 'V>) : unit = + match dict.TryGetValue (key) with + | true, v -> dict.[key] <- f value v + | false, _ -> dict.Add (key, value) + +module internal DictionaryExtensions = + + type IDictionary<'TKey, 'TValue> with + + member x.TryFind (key : 'TKey) = + match x.TryGetValue (key) with + | true, value -> ValueSome value + | _ -> ValueNone + +module internal Set = + + /// + /// Maps over each of the elements, applying function + /// over each one of them to generate new Set. Sets generated this way are + /// then flattened into single output set. + /// + /// Function used to generate Set from each of the input's elements. + /// Input set. + let collect f set = set |> Set.fold (fun acc e -> acc + f e) Set.empty diff --git a/src/FSharp.Data.GraphQL.Shared/Helpers/Extensions.fs b/src/FSharp.Data.GraphQL.Shared/Helpers/Extensions.fs index b1a26099..efd1e6dd 100644 --- a/src/FSharp.Data.GraphQL.Shared/Helpers/Extensions.fs +++ b/src/FSharp.Data.GraphQL.Shared/Helpers/Extensions.fs @@ -4,184 +4,29 @@ module internal FSharp.Data.GraphQL.Extensions open System.Reflection -open System.Collections.Generic open System.Text.Json.Serialization -type IDictionary<'TKey, 'TValue> with - - member x.TryFind(key : 'TKey) = - match x.TryGetValue(key) with - | (true, value) -> Some value - | _ -> None - type TypeInfo with /// If no property is found with the specified name, it will try changing the case of the first letter - member x.GetDeclaredProperty(propertyName: string, ignoreCase: bool) = - match x.GetDeclaredProperty(propertyName), ignoreCase with + member x.GetDeclaredProperty (propertyName : string, ignoreCase : bool) = + match x.GetDeclaredProperty (propertyName), ignoreCase with | null, true -> let first = - let first = propertyName.Substring(0,1) - match first.ToUpper() with + let first = propertyName.Substring (0, 1) + match first.ToUpper () with | upper when upper <> first -> upper - | _ -> first.ToLower() - x.GetDeclaredProperty(first + propertyName.Substring(1)) + | _ -> first.ToLower () + x.GetDeclaredProperty (first + propertyName.Substring (1)) | prop, _ -> prop /// If no method is found with the specified name, it will try changing the case of the first letter - member x.GetDeclaredMethod(propertyName: string, ignoreCase: bool) = - match x.GetDeclaredMethod(propertyName), ignoreCase with + member x.GetDeclaredMethod (propertyName : string, ignoreCase : bool) = + match x.GetDeclaredMethod (propertyName), ignoreCase with | null, true -> let first = - let first = propertyName.Substring(0,1) - match first.ToUpper() with + let first = propertyName.Substring (0, 1) + match first.ToUpper () with | upper when upper <> first -> upper - | _ -> first.ToLower() - x.GetDeclaredMethod(first + propertyName.Substring(1)) + | _ -> first.ToLower () + x.GetDeclaredMethod (first + propertyName.Substring (1)) | prop, _ -> prop - -module Option = - - let mergeWith (f: 'T -> 'T -> 'T) (o1 : 'T option) (o2 : 'T option) : 'T option = - match (o1, o2) with - | Some a, Some b -> Some (f a b) - | Some a, _ -> Some a - | _, Some b -> Some b - | _, _ -> None - - let unwrap (defaultValue : 'U) (onSome : 'T -> 'U) (o : 'T option) : 'U = - match o with - | Some t -> onSome t - | None -> defaultValue - -module Skippable = - - let ofList list = - match list with - | [] -> Skip - | list -> Include list - -module Dictionary = - - let addWith (f : 'V -> 'V -> 'V) (key : 'K) (value : 'V) (dict : Dictionary<'K, 'V>) : unit = - match dict.TryGetValue(key) with - | true, v -> dict.[key] <- f value v - | false, _ -> dict.Add(key, value) - -module Array = - - /// - /// Returns a new array with unique elements. Uniqueness is determined by - /// output of the function. - /// - /// Function, which output is used to determine uniqueness of input elements. - /// Array of elements. - let distinctBy keyf (array:'T[]) = - let temp = Array.zeroCreate array.Length - let mutable i = 0 - let hashSet = HashSet<_>(HashIdentity.Structural<_>) - for v in array do - if hashSet.Add(keyf v) then - temp.[i] <- v - i <- i + 1 - Array.sub temp 0 i - - /// - /// Attempts to find the first element in an array that satisfies the given predicate. - /// - /// Function to test each element. - /// The input array. - /// ValueSome of the first matching element, or ValueNone if no match is found. - let vtryFind predicate (source : 'T array) = - let mutable result = ValueNone - let mutable i = 0 - while i < source.Length && result.IsNone do - if predicate source[i] then - result <- ValueSome source[i] - i <- i + 1 - result - - /// - /// Applies a function to each element of an array and returns the first result where the function returns ValueSome. - /// - /// Function to apply to each element. - /// The input array. - /// ValueSome of the first successful mapping result, or ValueNone if no match is found. - let vtryPick mapping (source : 'T array) = - let mutable result = ValueNone - let mutable i = 0 - while i < source.Length && result.IsNone do - result <- mapping source[i] - i <- i + 1 - result - -module List = - - /// - /// Merges elements of two lists, returning a new list without duplicates. - /// - /// Function used to determine if any two given elements are considered equal. - /// First list with elements to merge. - /// Second list with elements to merge. - let mergeBy f listx listy = - let uniqx = - listx - |> List.filter (fun x -> not <| List.exists(fun y -> f(x) = f(y)) listy) - uniqx @ listy - - /// - /// Attempts to find the first element in a list that satisfies the given predicate. - /// - /// Function to test each element. - /// The input list. - /// ValueSome of the first matching element, or ValueNone if no match is found. - let rec vtryFind predicate (source : 'T list) = - match source with - | [] -> ValueNone - | head :: tail -> - if predicate head then - ValueSome head - else - vtryFind predicate tail - - /// - /// Applies a function to each element of a list and returns the first result where the function returns ValueSome. - /// - /// Function to apply to each element. - /// The input list. - /// ValueSome of the first successful mapping result, or ValueNone if no match is found. - let rec vtryPick mapping (source : 'T list) = - match source with - | [] -> ValueNone - | head :: tail -> - match mapping head with - | ValueSome result -> ValueSome result - | ValueNone -> vtryPick mapping tail - -module Set = - - /// - /// Maps over each of the elements, applying function - /// over each one of them to generate new Set. Sets generated this way are - /// then flattened into single output set. - /// - /// Function used to generate Set from each of the input's elements. - /// Input set. - let collect f set = set |> Set.fold (fun acc e -> acc + f e) Set.empty - -module Map = - - /// - /// Merges the entries of two maps by their key, returning new map in result. - /// - /// - /// Function, which takes key shared by entries in both maps, first entry's value, - /// second entry's value to produce a result value used in newly generated map. - /// - /// First map with elements to merge. - /// Second map with elements to merge. - let merge mergeFn mapx mapy = - mapy - |> Map.fold (fun acc ky vy -> - match Map.tryFind ky acc with - | Some vx -> Map.add ky (mergeFn ky vx vy) acc - | None -> Map.add ky vy acc) mapx diff --git a/src/FSharp.Data.GraphQL.Shared/Helpers/ObjAndStructConversions.fs b/src/FSharp.Data.GraphQL.Shared/Helpers/ObjAndStructConversions.fs index cb4f3532..4349ffc5 100644 --- a/src/FSharp.Data.GraphQL.Shared/Helpers/ObjAndStructConversions.fs +++ b/src/FSharp.Data.GraphQL.Shared/Helpers/ObjAndStructConversions.fs @@ -1,112 +1,55 @@ namespace rec FSharp.Data.GraphQL -open System.Collections.Generic +open System.Text.Json.Serialization open FsToolkit.ErrorHandling -module internal ValueOption = - - let mapOption mapping option = Option.toValueOption option |> ValueOption.map mapping - module internal Option = - let mapValueOption mapping voption = voption |> ValueOption.map mapping |> ValueOption.toOption - -[] -module KeyValuePair = - - let inline kvp key value = KeyValuePair (key, value) - let inline kvpObj key (value : obj) = KeyValuePair (key, value) - -[] -module internal ValueTuple = - - let fstv struct (a, _) = a - let sndv struct (_, b) = b - -[] -module Seq = + let mergeWith (f : 'T -> 'T -> 'T) (o1 : 'T option) (o2 : 'T option) : 'T option = + match (o1, o2) with + | Some a, Some b -> Some (f a b) + | Some a, _ -> Some a + | _, Some b -> Some b + | _, _ -> None - let vtryHead (source : 'T seq) = - use enumerator = source.GetEnumerator () - if not (enumerator.MoveNext ()) then - ValueNone - else - ValueSome enumerator.Current + let unwrap (defaultValue : 'U) (onSome : 'T -> 'U) (o : 'T option) : 'U = + match o with + | Some t -> onSome t + | None -> defaultValue - let vtryLast (source : 'T seq) = - use enumerator = source.GetEnumerator () - if not (enumerator.MoveNext ()) then - ValueNone - else - let mutable last = enumerator.Current - while enumerator.MoveNext () do - last <- enumerator.Current - ValueSome last + let mapValueOption mapping voption = + voption + |> ValueOption.map mapping + |> ValueOption.toOption - let vchoose mapping seq = - seq - |> Seq.map mapping - |> Seq.where ValueOption.isSome - |> Seq.map ValueOption.get - - let vtryFind predicate (source : 'T seq) = source |> Seq.where predicate |> Seq.vtryHead - - let vtryItem index (source : 'T seq) = - if index < 0 then - ValueNone - else - use enumerator = source.GetEnumerator () - let mutable currentIndex = 0 - let mutable result = ValueNone - let mutable found = false - - while not found && enumerator.MoveNext () do - if currentIndex = index then - result <- ValueSome enumerator.Current - found <- true - else - currentIndex <- currentIndex + 1 - - result - -module internal List = - - let vchoose mapping list = list |> Seq.vchoose mapping |> Seq.toList - - let vtryFind predicate list = list |> Seq.where predicate |> Seq.vtryHead - - let vtryItem index list = - let rec loop currentIndex list = - match currentIndex, list with - | _, [] -> ValueNone - | 0, head :: _ -> ValueSome head - | currentIndex, _ :: tail when currentIndex > 0 -> loop (currentIndex - 1) tail - | _ -> ValueNone +module internal ValueOption = - loop index list + let mergeWith (f : 'T -> 'T -> 'T) (o1 : 'T voption) (o2 : 'T voption) : 'T voption = + match (o1, o2) with + | ValueSome a, ValueSome b -> ValueSome (f a b) + | ValueSome a, _ -> ValueSome a + | _, ValueSome b -> ValueSome b + | _, _ -> ValueNone -module internal Array = + let unwrap (defaultValue : 'U) (onSome : 'T -> 'U) (o : 'T voption) : 'U = + match o with + | ValueSome t -> onSome t + | ValueNone -> defaultValue - let vchoose mapping array = array |> Seq.vchoose mapping |> Seq.toArray + let mapOption mapping option = + option + |> Option.toValueOption + |> ValueOption.map mapping - let vtryItem index (array : 'T array) = - if index < 0 || index >= array.Length then - ValueNone - else - ValueSome array[index] +module Skippable = - let vtryFind predicate (array : 'T array) = - let mutable i = 0 - let mutable result = ValueNone - while i < array.Length && result.IsNone do - if predicate array[i] then - result <- ValueSome array[i] - i <- i + 1 - result + let ofList list = + match list with + | [] -> Skip + | list -> Include list -module internal Map = +[] +module internal ValueTuple = - let vtryFind key (map : Map<_, _>) = - match map.TryGetValue key with - | true, value -> ValueSome value - | false, _ -> ValueNone + let fstv struct (a, _) = a + let sndv struct (_, b) = b diff --git a/src/FSharp.Data.GraphQL.Shared/Helpers/Reflection.fs b/src/FSharp.Data.GraphQL.Shared/Helpers/Reflection.fs index 5c43063c..b60cd93f 100644 --- a/src/FSharp.Data.GraphQL.Shared/Helpers/Reflection.fs +++ b/src/FSharp.Data.GraphQL.Shared/Helpers/Reflection.fs @@ -171,9 +171,13 @@ module Helpers = let rec internal moduleType = ReflectionHelper.getModuleType <@ moduleType @> - let private objectOptionCast (value: obj) = - if isNull value then ValueNone - else + /// + /// Casts a to a . + /// + let objectOptionCast (value: obj) = + match value with + | null -> ValueNone + | _ -> let t = value.GetType() if t.FullName.StartsWith ReflectionHelper.OptionTypeName then let p = t.GetProperty("Value") @@ -185,11 +189,6 @@ module Helpers = ValueSome (p.GetValue(value, [||])) else ValueNone - /// - /// Casts a to a . - /// - let optionCast (value: obj) = objectOptionCast value |> ValueOption.toOption - /// /// Matches a containing a boxed or . /// Returns the wrapped value for and , and does not match for , , or non-option values. @@ -206,6 +205,24 @@ module Helpers = | ObjectOption v | v -> Some v + /// + /// Lifts a to an , unless it is already an . + /// + let toValueOption x = + match x with + | null -> ValueNone + | value -> + let t = value.GetType() + match t.FullName with + | null -> ValueSome value + | _ when t.IsGenericType -> + let genericTypeDefinition = t.GetGenericTypeDefinition() + match genericTypeDefinition.FullName with + | ReflectionHelper.OptionTypeName + | ReflectionHelper.ValueOptionTypeName -> objectOptionCast value + | _ -> ValueSome value + | _ -> ValueSome value + /// /// Unwraps a from an or , /// unless it is not wrapped. diff --git a/src/FSharp.Data.GraphQL.Shared/Validation.fs b/src/FSharp.Data.GraphQL.Shared/Validation.fs index db85a03e..4134e21f 100644 --- a/src/FSharp.Data.GraphQL.Shared/Validation.fs +++ b/src/FSharp.Data.GraphQL.Shared/Validation.fs @@ -514,8 +514,8 @@ module Ast = | InlineFragment frag -> List.append (getFieldNames frag.SelectionSet) acc | FragmentSpread spread -> fragmentDefinitions - |> List.tryFind (fun x -> x.Name.IsSome && x.Name.Value = spread.Name) - |> Option.unwrap acc (fun frag -> getFieldNames frag.SelectionSet)) + |> List.vtryFind (fun x -> x.Name.IsSome && x.Name.Value = spread.Name) + |> ValueOption.unwrap acc (fun frag -> getFieldNames frag.SelectionSet)) ctx.Document.Definitions |> ValidationResult.collect (function | OperationDefinition def when def.OperationType = Subscription -> diff --git a/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs b/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs index 40c61ed2..deef59dc 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs @@ -358,6 +358,18 @@ let ``Execution handles basic tasks: list of scalars`` () = empty errors data |> equals (upcast NameValueLookup.ofList ["strings", box [ box "foo"; upcast "bar"; upcast "baz" ]]) +[] +let ``Execution handles basic tasks: list of struct nullable scalars`` () = + let schema = + Schema(Define.Object<{| Items : string voption list |}>( + "Type", [ + Define.Field("items", ListOf (StructNullable StringType), fun _ (value : {| Items : string voption list |}) -> value.Items) + ])) + let result = sync <| Executor(schema).AsyncExecute("query Example { items }", getMockInputContext, {| Items = [ ValueSome "foo"; ValueNone; ValueSome "bar" ] |}) + ensureDirect result <| fun data errors -> + empty errors + data |> equals (upcast NameValueLookup.ofList ["items", box [ box "foo"; null; box "bar" ]]) + type TwiceTest = { A : string; B : int } []