diff --git a/src/FSharp.Data.GraphQL.Client/BaseTypes.fs b/src/FSharp.Data.GraphQL.Client/BaseTypes.fs index 5bc66444..4b93c3ff 100644 --- a/src/FSharp.Data.GraphQL.Client/BaseTypes.fs +++ b/src/FSharp.Data.GraphQL.Client/BaseTypes.fs @@ -15,23 +15,41 @@ open FSharp.Data.GraphQL.Client.ReflectionPatterns open FSharp.Data.GraphQL.Types.Introspection /// Contains information about a field on the query. -type SchemaFieldInfo = - { /// Gets the alias or the name of the field. - AliasOrName : string - /// Gets the introspection type information of the field. - SchemaTypeRef : IntrospectionTypeRef - /// Gets information about fields of this field, if it is an object type. - Fields : SchemaFieldInfo [] } +type SchemaFieldInfo = { + /// Gets the alias or the name of the field. + AliasOrName : string + /// Gets the introspection type information of the field. + SchemaTypeRef : IntrospectionTypeRef + /// Gets information about fields of this field, if it is an object type. + Fields : SchemaFieldInfo[] +} /// A type alias to represent a Type name. type TypeName = string -/// Contains data about a GQL operation error. -type OperationError = - { /// The description of the error that happened in the operation. - Message : string - /// The path to the field that produced the error while resolving its value. - Path : obj [] } +/// Contains source location information for a single GraphQL error location entry in the response. +/// See GraphQL specification sections and +/// . +type OperationErrorLocation = { + /// The source line of the GraphQL operation document where the error occurred. + Line : int + /// The source column of the GraphQL operation document where the error occurred. + Column : int +} + +/// Contains data about a GraphQL operation error as defined by the GraphQL response format. +/// See GraphQL specification sections and +/// . +type OperationError = { + /// The description of the error that happened in the operation. + Message : string + /// The source locations in the GraphQL operation document where the error occurred. + Locations : OperationErrorLocation[] + /// The path to the field that produced the error while resolving its value. + Path : obj[] + /// Extension data attached to the error. + Extensions : Map +} /// Contains helpers to build HTTP header sequences to be used in GraphQLProvider Run methods. module HttpHeaders = @@ -39,138 +57,155 @@ module HttpHeaders = /// The input headers string should be a string containing headers in the same way they are /// organized in a HTTP request (each header in a line, names and values separated by commas). let ofString (headers : string) : seq = - upcast (headers.Replace("\r\n", "\n").Split('\n') - |> Array.map (fun header -> - let separatorIndex = header.IndexOf(':') - if separatorIndex = -1 - then failwithf "Header \"%s\" has an invalid header format. Must provide a name and a value, both separated by a comma." header - else - let name = header.Substring(0, separatorIndex).Trim() - let value = header.Substring(separatorIndex + 1).Trim() - (name, value))) + upcast + (headers.Replace("\r\n", "\n").Split ('\n') + |> Array.map (fun header -> + let separatorIndex = header.IndexOf (':') + if separatorIndex = -1 then + failwithf "Header \"%s\" has an invalid header format. Must provide a name and a value, both separated by a comma." header + else + let name = header.Substring(0, separatorIndex).Trim () + let value = header.Substring(separatorIndex + 1).Trim () + (name, value))) /// Builds a sequence of HTTP headers as a sequence from a header file. /// The input file should be a file containing headers in the same way they are /// organized in a HTTP request (each header in a line, names and values separated by commas). - let ofFile (path : string) = - System.IO.File.ReadAllText path |> ofString + let ofFile (path : string) = System.IO.File.ReadAllText path |> ofString let internal load (location : StringLocation) : seq = let headersString = match location with | String headers -> headers | File path -> System.IO.File.ReadAllText path - if headersString = "" then upcast [||] - else headersString |> ofString + if headersString = "" then + upcast [||] + else + headersString |> ofString /// The base type for all GraphQLProvider provided enum types. type EnumBase (name : string, value : string) = /// Gets the name of the provided enum type. - member _.GetName() = name + member _.GetName () = name /// Gets the value of the provided enum type. - member _.GetValue() = value + member _.GetValue () = value - override x.ToString() = x.GetValue() + override x.ToString () = x.GetValue () - member x.Equals(other : EnumBase) = - x.GetName() = other.GetName() && x.GetValue() = other.GetValue() + member x.Equals (other : EnumBase) = + x.GetName () = other.GetName () + && x.GetValue () = other.GetValue () - override x.Equals(other : obj) = + override x.Equals (other : obj) = match other with - | :? EnumBase as other -> x.Equals(other) + | :? EnumBase as other -> x.Equals (other) | _ -> false - override x.GetHashCode() = x.GetName().GetHashCode() ^^^ x.GetValue().GetHashCode() + override x.GetHashCode () = x.GetName().GetHashCode () ^^^ x.GetValue().GetHashCode () interface IEquatable with - member x.Equals(other) = x.Equals(other) + member x.Equals (other) = x.Equals (other) /// Contains information about a GraphQLProvider record property. -type RecordProperty = - { /// Gets the name of the record property. - Name : string - /// Gets the value of the record property. - Value : obj } +type RecordProperty = { + /// Gets the name of the record property. + Name : string + /// Gets the value of the record property. + Value : obj +} /// The base type for all GraphQLProvider provided record types. type RecordBase (name : string, properties : RecordProperty seq) = do - if not (isNull properties) - then - let distinctCount = properties |> Seq.map (fun p -> p.Name) |> Seq.distinct |> Seq.length - if distinctCount <> Seq.length properties - then failwith "Duplicated property names were found. Record can not be created, because each property name must be distinct." + if not (isNull properties) then + let distinctCount = + properties + |> Seq.map (fun p -> p.Name) + |> Seq.distinct + |> Seq.length + if distinctCount <> Seq.length properties then + failwith "Duplicated property names were found. Record can not be created, because each property name must be distinct." let properties = - if not (isNull properties) - then properties |> Seq.sortBy _.Name |> List.ofSeq - else [] + if not (isNull properties) then + properties |> Seq.sortBy _.Name |> List.ofSeq + else + [] /// Gets the name of this provided record type. - member _.GetName() = name + member _.GetName () = name /// Gets a list of this provided record properties. - member _.GetProperties() = properties + member _.GetProperties () = properties /// Produces a dictionary containing all the properties of this provided record type. - member x.ToDictionary() = + member x.ToDictionary () = let rec mapDictionaryValue (v : obj) = match v with | null -> null | :? 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()) + | :? EnumBase as v -> v.GetValue () |> box + | :? RecordBase as v -> box (v.ToDictionary ()) | OptionValue v -> v |> Option.map mapDictionaryValue |> Option.toObj | EnumerableValue v -> v |> Array.map mapDictionaryValue |> box | _ -> v - x.GetProperties() + x.GetProperties () |> Seq.choose (fun p -> - if not (isNull p.Value) - then Some (p.Name, mapDictionaryValue p.Value) - else None) + if not (isNull p.Value) then + Some (p.Name, mapDictionaryValue p.Value) + else + None) |> dict - override x.ToString() = + override x.ToString () = let getPropValue (prop : RecordProperty) = sprintf "%A" prop.Value - let sb = StringBuilder() - sb.Append("{") |> ignore + let sb = StringBuilder () + sb.Append ("{") |> ignore let rec printProperties (properties : RecordProperty list) = match properties with | [] -> () - | [prop] -> sb.Append(sprintf "%s = %s;" prop.Name (getPropValue prop)) |> ignore - | prop :: tail -> sb.AppendLine(sprintf "%s = %s;" prop.Name (getPropValue prop)) |> ignore; printProperties tail - printProperties (x.GetProperties()) - sb.Append("}") |> ignore - sb.ToString() - - member x.Equals(other : RecordBase) = - x.GetName() = other.GetName() && x.GetProperties() = other.GetProperties() - - override x.Equals(other : obj) = + | [ prop ] -> + sb.Append (sprintf "%s = %s;" prop.Name (getPropValue prop)) + |> ignore + | prop :: tail -> + sb.AppendLine (sprintf "%s = %s;" prop.Name (getPropValue prop)) + |> ignore + printProperties tail + printProperties (x.GetProperties ()) + sb.Append ("}") |> ignore + sb.ToString () + + member x.Equals (other : RecordBase) = + x.GetName () = other.GetName () + && x.GetProperties () = other.GetProperties () + + override x.Equals (other : obj) = match other with - | :? RecordBase as other -> x.Equals(other) + | :? RecordBase as other -> x.Equals (other) | _ -> false - override x.GetHashCode() = - x.GetName().GetHashCode() ^^^ x.GetProperties().GetHashCode() + override x.GetHashCode () = + x.GetName().GetHashCode () + ^^^ x.GetProperties().GetHashCode () interface IEquatable with - member x.Equals(other) = x.Equals(other) + member x.Equals (other) = x.Equals (other) module internal TypeMapping = let scalar = - [| "Int", typeof - "Boolean", typeof - "Date", typeof - "Float", typeof - "ID", typeof - "String", typeof - "URI", typeof |] + [| + "Int", typeof + "Boolean", typeof + "Date", typeof + "Float", typeof + "ID", typeof + "String", typeof + "URI", typeof + |] |> Map.ofArray - let isBuiltInScalarTypeName (name : string) = - scalar |> Map.containsKey name + let isBuiltInScalarTypeName (name : string) = scalar |> Map.containsKey name let isScalarTypeName (schemaTypes : Map) (name : string) = match schemaTypes.TryFind name with @@ -178,27 +213,32 @@ module internal TypeMapping = | None -> isBuiltInScalarTypeName name let tryFindScalarType (schemaTypes : Map) (name : string) = - if isScalarTypeName schemaTypes name - then scalar |> Map.tryFind name - else None + if isScalarTypeName schemaTypes name then + scalar |> Map.tryFind name + else + None let getSchemaTypes (introspection : IntrospectionSchema) = - let schemaTypeNames = - [| "__TypeKind" - "__DirectiveLocation" - "__Type" - "__InputValue" - "__Field" - "__EnumValue" - "__Directive" - "__Schema" |] - let isIntrospectionType (name : string) = - schemaTypeNames |> Array.contains name + let schemaTypeNames = [| + "__TypeKind" + "__DirectiveLocation" + "__Type" + "__InputValue" + "__Field" + "__EnumValue" + "__Directive" + "__Schema" + |] + let isIntrospectionType (name : string) = schemaTypeNames |> Array.contains name introspection.Types |> Array.choose (fun t -> - if not (isIntrospectionType t.Name) && not (t.Kind = TypeKind.SCALAR && isBuiltInScalarTypeName t.Name) - then Some(t.Name, t) - else None) + if + not (isIntrospectionType t.Name) + && not (t.Kind = TypeKind.SCALAR && isBuiltInScalarTypeName t.Name) + then + Some (t.Name, t) + else + None) |> Map.ofArray let mapScalarType uploadInputTypeName tname = @@ -206,20 +246,25 @@ module internal TypeMapping = | Some uploadInputTypeName when uploadInputTypeName = tname -> typeof | _ -> // Unknown scalar types will be mapped to a string type. - if scalar.ContainsKey(tname) - then scalar.[tname] - else typeof + if scalar.ContainsKey (tname) then + scalar.[tname] + else + typeof - let makeOption (t : Type) = typedefof<_ option>.MakeGenericType(t) + let makeOption (t : Type) = typedefof<_ option>.MakeGenericType (t) - let makeArray (t : Type) = t.MakeArrayType() + let makeArray (t : Type) = t.MakeArrayType () let unwrapOption (t : Type) = - if t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<_ option> - then t.GetGenericArguments().[0] - else failwithf "Expected type to be an Option type, but it is %s." t.Name + if + t.IsGenericType + && t.GetGenericTypeDefinition () = typedefof<_ option> + then + t.GetGenericArguments().[0] + else + failwithf "Expected type to be an Option type, but it is %s." t.Name - let makeAsync (t : Type) = typedefof>.MakeGenericType(t) + let makeAsync (t : Type) = typedefof>.MakeGenericType (t) module internal JsonValueHelper = let getResponseFields (responseJson : JsonValue) = @@ -228,7 +273,10 @@ module internal JsonValueHelper = | _ -> failwithf "Expected root type to be a Record type, but type is %A." responseJson let getResponseDataFields (responseJson : JsonValue) = - match getResponseFields responseJson |> Array.tryFind (fun (name, _) -> name = "data") with + match + getResponseFields responseJson + |> Array.tryFind (fun (name, _) -> name = "data") + with | Some (_, data) -> match data with | JsonValue.Record fields -> Some fields @@ -237,10 +285,14 @@ module internal JsonValueHelper = | None -> None let getResponseErrors (responseJson : JsonValue) = - match getResponseFields responseJson |> Array.tryFind (fun (name, _) -> name = "errors") with + match + getResponseFields responseJson + |> Array.tryFind (fun (name, _) -> name = "errors") + with | Some (_, errors) -> match errors with - | JsonValue.Array [||] | JsonValue.Null -> None + | JsonValue.Array [||] + | JsonValue.Null -> None | JsonValue.Array items -> Some items | _ -> failwithf "Expected error field of root type to be an Array type, but type is %A." errors | None -> None @@ -249,11 +301,11 @@ module internal JsonValueHelper = getResponseFields responseJson |> Array.filter (fun (name, _) -> name <> "data" && name <> "errors") - let private removeTypeNameField (fields : (string * JsonValue) []) = - fields |> Array.filter (fun (name, _) -> name <> "__typename") + let private removeTypeNameField (fields : (string * JsonValue)[]) = + fields + |> Array.filter (fun (name, _) -> name <> "__typename") - let firstUpper (name : string, value) = - name.FirstCharUpper(), value + let firstUpper (name : string, value) = name.FirstCharUpper (), value let getTypeName (fields : (string * JsonValue) seq) = fields @@ -293,7 +345,10 @@ module internal JsonValueHelper = | TypeKind.NON_NULL -> match schemaField.SchemaTypeRef.OfType with | Some t when t.Kind = TypeKind.LIST -> t.OfType - | _ -> failwithf "Expected field to be a list type with an underlying item, but it is %A." schemaField.SchemaTypeRef.OfType + | _ -> + failwithf + "Expected field to be a list type with an underlying item, but it is %A." + schemaField.SchemaTypeRef.OfType | _ -> failwithf "Expected field to be a list type with an underlying item, but it is %A." schemaField.SchemaTypeRef match tref with | Some t -> t @@ -307,12 +362,16 @@ module internal JsonValueHelper = | Some itemType -> match itemType.Kind with | TypeKind.NON_NULL -> failwith "Schema definition is not supported: a non null type of a non null type was specified." - | TypeKind.OBJECT | TypeKind.INTERFACE | TypeKind.UNION -> makeArray typeof items + | TypeKind.OBJECT + | TypeKind.INTERFACE + | TypeKind.UNION -> makeArray typeof items | TypeKind.ENUM -> makeArray typeof items | TypeKind.SCALAR -> makeArray (getScalarType itemType) items | kind -> failwithf "Unsupported type kind \"%A\"." kind | None -> failwith "Item type is a non null type, but no underlying type exists on the schema definition of the type." - | TypeKind.OBJECT | TypeKind.INTERFACE | TypeKind.UNION -> makeOptionArray typeof items + | TypeKind.OBJECT + | TypeKind.INTERFACE + | TypeKind.UNION -> makeOptionArray typeof items | TypeKind.ENUM -> makeOptionArray typeof items | TypeKind.SCALAR -> makeOptionArray (getScalarType itemType) items | kind -> failwithf "Unsupported type kind \"%A\"." kind @@ -324,22 +383,31 @@ module internal JsonValueHelper = | None -> 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) with + match + schemaField.Fields + |> Array.tryFind (fun f -> f.AliasOrName = aliasOrName) + with | Some f -> f - | None -> failwithf "Expected to find field information for field with alias or name \"%s\" of type \"%s\" but it was not found." aliasOrName typeName + | None -> + failwithf + "Expected to find field information for field with alias or name \"%s\" of type \"%s\" but it was not found." + aliasOrName + typeName let value = helper true schemaField value { Name = aliasOrName; Value = value } let props = props |> removeTypeNameField |> Array.map (firstUpper >> mapRecordProperty) - RecordBase(typeName, props) |> makeSomeIfNeeded + RecordBase (typeName, props) |> makeSomeIfNeeded | JsonValue.Boolean b -> makeSomeIfNeeded b | JsonValue.Float f -> makeSomeIfNeeded f | JsonValue.Null -> match schemaField.SchemaTypeRef.Kind with | TypeKind.NON_NULL -> failwith "Expected a non null item from the schema definition, but a null item was found in the response." - | TypeKind.OBJECT | TypeKind.INTERFACE | TypeKind.UNION -> makeNoneIfNeeded typeof + | TypeKind.OBJECT + | TypeKind.INTERFACE + | TypeKind.UNION -> makeNoneIfNeeded typeof | TypeKind.ENUM -> makeNoneIfNeeded typeof | TypeKind.SCALAR -> getScalarType schemaField.SchemaTypeRef |> makeNoneIfNeeded | TypeKind.LIST -> null @@ -354,71 +422,129 @@ module internal JsonValueHelper = | TypeKind.NON_NULL -> failwith "Schema definition is not supported: a non null type of a non null type was specified." | TypeKind.SCALAR -> match itemType.Name with - | Some "URI" -> - System.Uri(s) |> box + | Some "URI" -> System.Uri (s) |> box | Some "Date" -> - match DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None) with + match DateTime.TryParse (s, CultureInfo.InvariantCulture, DateTimeStyles.None) with | (true, d) -> box d - | _ -> failwith "A string was received in the query response, and the schema recognizes it as a date and time string, but the conversion failed." - | Some _ -> - box s - | _ -> failwith "A string type was received in the query response item, but the matching schema field is not a string based type." - | TypeKind.ENUM when itemType.Name.IsSome -> EnumBase(itemType.Name.Value, s) |> box - | _ -> failwith "A string type was received in the query response item, but the matching schema field is not a string or an enum type." + | _ -> + failwith + "A string was received in the query response, and the schema recognizes it as a date and time string, but the conversion failed." + | Some _ -> box s + | _ -> + failwith + "A string type was received in the query response item, but the matching schema field is not a string based type." + | TypeKind.ENUM when itemType.Name.IsSome -> EnumBase (itemType.Name.Value, s) |> box + | _ -> + failwith + "A string type was received in the query response item, but the matching schema field is not a string or an enum type." | None -> failwith "Item type is a non null type, but no underlying type exists on the schema definition of the type." | TypeKind.SCALAR -> match schemaField.SchemaTypeRef.Name with - | Some "String" | Some "ID" -> - s |> makeSomeIfNeeded - | Some "URI" -> - s |> System.Uri |> makeSomeIfNeeded + | Some "String" + | Some "ID" -> s |> makeSomeIfNeeded + | Some "URI" -> s |> System.Uri |> makeSomeIfNeeded | Some "Date" -> - match DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None) with + match DateTime.TryParse (s, CultureInfo.InvariantCulture, DateTimeStyles.None) with | (true, d) -> makeSomeIfNeeded d - | _ -> failwith "A string was received in the query response, and the schema recognizes it as a date and time string, but the conversion failed." - | Some _ -> - s |> makeSomeIfNeeded + | _ -> + failwith + "A string was received in the query response, and the schema recognizes it as a date and time string, but the conversion failed." + | Some _ -> s |> makeSomeIfNeeded | _ -> failwith "A string type was received in the query response item, but the matching schema field is not a string based type." - | TypeKind.ENUM when schemaField.SchemaTypeRef.Name.IsSome -> EnumBase(schemaField.SchemaTypeRef.Name.Value, s) |> makeSomeIfNeeded - | _ -> failwith "A string type was received in the query response item, but the matching schema field is not a string based type or an enum type." + | TypeKind.ENUM when schemaField.SchemaTypeRef.Name.IsSome -> + EnumBase (schemaField.SchemaTypeRef.Name.Value, s) + |> makeSomeIfNeeded + | _ -> + failwith + "A string type was received in the query response item, but the matching schema field is not a string based type or an enum type." fieldName, (helper true schemaField fieldValue) - let getFieldValues (schemaTypeName : string) (schemaFields : SchemaFieldInfo []) (dataFields : (string * JsonValue) []) = + let getFieldValues (schemaTypeName : string) (schemaFields : SchemaFieldInfo[]) (dataFields : (string * JsonValue)[]) = let mapFieldValue (aliasOrName : string, value : JsonValue) = let schemaField = - match schemaFields |> Array.tryFind (fun f -> f.AliasOrName = aliasOrName) with + match + schemaFields + |> Array.tryFind (fun f -> f.AliasOrName = aliasOrName) + with | Some f -> f - | None -> failwithf "Expected to find field information for field with alias or name \"%s\" of type \"%s\" but it was not found." aliasOrName schemaTypeName + | None -> + failwithf + "Expected to find field information for field with alias or name \"%s\" of type \"%s\" but it was not found." + aliasOrName + schemaTypeName getFieldValue schemaField (aliasOrName, value) removeTypeNameField dataFields |> Array.map (firstUpper >> mapFieldValue) - let getErrors (errors : JsonValue []) = - let errorMapper = function + let getErrors (errors : JsonValue[]) = + let tryFindField fieldName (fields : (string * JsonValue)[]) = + fields + |> Array.tryFind (fun (name, _) -> name = fieldName) + |> Option.map snd + + let parsePath = + function + | Some (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 -> [||] + | _ -> failwith "Error parsing response errors. Path field must be an Array." + + let parseLocations = + function + | Some (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 } + | _ -> 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 -> [||] + | _ -> 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 + | _ -> failwith "Error parsing response errors. Extensions field must be a Record." + + let errorMapper = + function | JsonValue.Record fields -> - match fields |> Array.tryFind (fun (name, _) -> name = "message"), fields |> Array.tryFind (fun (name, _) -> name = "path") with - | Some (_, JsonValue.String message), Some (_, JsonValue.Array path) -> - let pathMapper = function - | JsonValue.String x -> box x - | JsonValue.Integer x -> box x - | _ -> failwith "Error parsing response errors. A item in the path is neither a String or a Number." - { Message = message; Path = Array.map pathMapper path } - | Some (_, JsonValue.String message), None-> - { Message = message; Path = [||]} + match tryFindField "message" fields with + | Some (JsonValue.String message) -> { + Message = message + Locations = tryFindField "locations" fields |> parseLocations + Path = tryFindField "path" fields |> parsePath + Extensions = tryFindField "extensions" fields |> parseExtensions + } | _ -> failwith "Error parsing response errors. Unsupported errors field format." - | other -> failwithf "Error parsing response errors. Expected error to be a Record type, but it is %s." (other.ToString()) + | other -> failwithf "Error parsing response errors. Expected error to be a Record type, but it is %s." (other.ToString ()) Array.map errorMapper errors /// The base type for all GraphQLProvider operation result provided types. -type OperationResultBase (rawResponse: HttpResponseMessage, responseJson : JsonValue, operationFields : SchemaFieldInfo [], operationTypeName : string) = +type OperationResultBase + (rawResponse : HttpResponseMessage, responseJson : JsonValue, operationFields : SchemaFieldInfo[], operationTypeName : string) = let rawData = let data = JsonValueHelper.getResponseDataFields responseJson match data with - | Some [||] | None -> None + | Some [||] + | None -> None | Some dataFields -> let fieldValues = JsonValueHelper.getFieldValues operationTypeName operationFields dataFields - let props = fieldValues |> Array.map (fun (name, value) -> { Name = name; Value = value }) - Some (RecordBase(operationTypeName, props)) + let props = + fieldValues + |> Array.map (fun (name, value) -> { Name = name; Value = value }) + Some (RecordBase (operationTypeName, props)) let errors = let errors = JsonValueHelper.getResponseErrors responseJson @@ -434,10 +560,12 @@ type OperationResultBase (rawResponse: HttpResponseMessage, responseJson : JsonV /// [] - [] + [] member _.RawData = rawData - /// Gets all the errors returned by the operation on the server. + /// Gets all GraphQL errors returned by the server. + /// See GraphQL specification sections and + /// . member _.Errors = errors /// Gets all the custom data returned by the operation on server as a map of names and values. @@ -445,15 +573,14 @@ type OperationResultBase (rawResponse: HttpResponseMessage, responseJson : JsonV member _.Headers = rawResponse.Headers - member x.Equals(other : OperationResultBase) = - x.ResponseJson = other.ResponseJson + member x.Equals (other : OperationResultBase) = x.ResponseJson = other.ResponseJson - override x.Equals(other : obj) = + override x.Equals (other : obj) = match other with - | :? OperationResultBase as other -> x.Equals(other) + | :? OperationResultBase as other -> x.Equals (other) | _ -> false - override x.GetHashCode() = x.ResponseJson.GetHashCode() + override x.GetHashCode () = x.ResponseJson.GetHashCode () /// The base type for al GraphQLProvider operation provided types. type OperationBase (query : string) = @@ -468,8 +595,8 @@ module VariableMapping = match value with | null -> null | :? string -> value - | :? EnumBase as v -> v.GetValue() |> box - | :? RecordBase as v -> v.ToDictionary() |> box + | :? EnumBase as v -> v.GetValue () |> box + | :? RecordBase as v -> v.ToDictionary () |> box | OptionValue v -> v |> Option.map mapVariableValue |> box | EnumerableValue v -> v |> Array.map mapVariableValue |> box | v -> v diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Schema.fs b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Schema.fs index fec874b4..5ba9de7d 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Schema.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Schema.fs @@ -2,214 +2,277 @@ namespace FSharp.Data.GraphQL.Samples.StarWarsApi open System open System.Text +open System.Collections.Generic open Microsoft.AspNetCore.Http open Microsoft.Extensions.DependencyInjection -type Root(ctx : HttpContext) = +type Root (ctx : HttpContext) = - member _.RequestAborted: System.Threading.CancellationToken = ctx.RequestAborted - member _.ServiceProvider: IServiceProvider = ctx.RequestServices - member root.GetRequiredService<'t>() = root.ServiceProvider.GetRequiredService<'t>() + member _.RequestAborted : System.Threading.CancellationToken = ctx.RequestAborted + member _.ServiceProvider : IServiceProvider = ctx.RequestServices + member root.GetRequiredService<'t> () = root.ServiceProvider.GetRequiredService<'t> () open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types -type InputField = - { String : string - Int : int - StringOption : string option - IntOption : int option - Uri : Uri - Guid : Guid - GuidOption : Guid option } - -type Input = - { Single : InputField option - List : InputField list option } - -type InputFile = - { File : FileData } - -type UploadedFile = - { Name : string - ContentType : string - ContentAsText : string } - -type UploadedContentFile = - { Name : string - ContentAsText : string } - -type UploadRequest = - { Single : FileData - Multiple : FileData list - NullableMultiple : FileData list option - NullableMultipleNullable : FileData option list option } - -type UploadResponse = - { Single : UploadedFile - Multiple : UploadedFile list - NullableMultiple : UploadedFile list option - NullableMultipleNullable : UploadedFile option list option } +type InputField = { + String : string + Int : int + StringOption : string option + IntOption : int option + Uri : Uri + Guid : Guid + GuidOption : Guid option +} + +type Input = { Single : InputField option; List : InputField list option } + +type InputFile = { File : FileData } + +type UploadedFile = { Name : string; ContentType : string; ContentAsText : string } + +type UploadedContentFile = { Name : string; ContentAsText : string } + +type UploadRequest = { + Single : FileData + Multiple : FileData list + NullableMultiple : FileData list option + NullableMultipleNullable : FileData option list option +} + +type UploadResponse = { + Single : UploadedFile + Multiple : UploadedFile list + NullableMultiple : UploadedFile list option + NullableMultipleNullable : UploadedFile option list option +} module Schema = let InputFieldType = - Define.InputObject( + Define.InputObject ( name = "InputField", - fields = - [ Define.Input("string", StringType, description = "A string value.") - Define.Input("int", IntType, description = "An integer value.") - Define.Input("stringOption", Nullable StringType, description = "A string option value.") - Define.Input("intOption", Nullable IntType, description = "An integer option value.") - Define.Input("uri", UriType, description = "An URI value.") - Define.Input("guid", GuidType, description = "A Guid value.") ]) + fields = [ + Define.Input ("string", StringType, description = "A string value.") + Define.Input ("int", IntType, description = "An integer value.") + Define.Input ("stringOption", Nullable StringType, description = "A string option value.") + Define.Input ("intOption", Nullable IntType, description = "An integer option value.") + Define.Input ("uri", UriType, description = "An URI value.") + Define.Input ("guid", GuidType, description = "A Guid value.") + ] + ) let InputType = - Define.InputObject( - name ="Input", + Define.InputObject ( + name = "Input", description = "Input object type.", - fields = - [ Define.Input("single", Nullable InputFieldType, description = "A single input field.") - Define.Input("list", Nullable (ListOf InputFieldType), description = "A list of input fields.") ]) + fields = [ + Define.Input ("single", Nullable InputFieldType, description = "A single input field.") + Define.Input ("list", Nullable (ListOf InputFieldType), description = "A list of input fields.") + ] + ) let OutputFieldType = - Define.Object( + Define.Object ( name = "OutputField", description = "The output for a field input.", - fields = - [ Define.Field("string", StringType, resolve = (fun _ x -> x.String), description = "A string value.") - Define.AutoField("int", IntType, description = "An integer value.") - Define.AutoField("stringOption", Nullable StringType, description = "A string option value.") - Define.AutoField("intOption", Nullable IntType, description = "An integer option value.") - Define.AutoField("uri", UriType, description = "An URI value.") - Define.AutoField("guid", GuidType, description = "A Guid value.") - Define.Field("guidId", IDType, description = "A Guid Id value.", resolve = fun _ o -> o.Guid |> string) - Define.Field("stringId", IDType, description = "A String Id value.", resolve = fun _ o -> o.String) - Define.Field("guidIdOption", Nullable IDType, description = "A Guid Id value.", resolve = fun _ o -> o.GuidOption |> Option.map string) - Define.Field("stringIdOption", Nullable IDType, description = "A String Id value.", resolve = fun _ o -> o.StringOption) - Define.Field("deprecated", StringType, resolve = (fun _ x -> x.String), description = "A string value through a deprecated field.", deprecationReason = "This field is deprecated.", args = []) ]) + fields = [ + Define.Field ("string", StringType, resolve = (fun _ x -> x.String), description = "A string value.") + Define.AutoField ("int", IntType, description = "An integer value.") + Define.AutoField ("stringOption", Nullable StringType, description = "A string option value.") + Define.AutoField ("intOption", Nullable IntType, description = "An integer option value.") + Define.AutoField ("uri", UriType, description = "An URI value.") + Define.AutoField ("guid", GuidType, description = "A Guid value.") + Define.Field ("guidId", IDType, description = "A Guid Id value.", resolve = (fun _ o -> o.Guid |> string)) + Define.Field ("stringId", IDType, description = "A String Id value.", resolve = (fun _ o -> o.String)) + Define.Field ( + "guidIdOption", + Nullable IDType, + description = "A Guid Id value.", + resolve = fun _ o -> o.GuidOption |> Option.map string + ) + Define.Field ("stringIdOption", Nullable IDType, description = "A String Id value.", resolve = (fun _ o -> o.StringOption)) + Define.Field ( + "deprecated", + StringType, + resolve = (fun _ x -> x.String), + description = "A string value through a deprecated field.", + deprecationReason = "This field is deprecated.", + args = [] + ) + ] + ) let UploadedFileType = - Define.Object( + Define.Object ( name = "UploadedFile", description = "Contains data of an uploaded file.", - fields = - [ Define.AutoField("name", StringType, description = "The name of the file.") - Define.AutoField("contentType", StringType, description = "The content type of the file.") - Define.AutoField("contentAsText", StringType, description = "The content of the file as text.") ]) + fields = [ + Define.AutoField ("name", StringType, description = "The name of the file.") + Define.AutoField ("contentType", StringType, description = "The content type of the file.") + Define.AutoField ("contentAsText", StringType, description = "The content of the file as text.") + ] + ) let UploadRequestType = - Define.InputObject( + Define.InputObject ( name = "UploadRequest", description = "Request for uploading files in several different forms.", - fields = - [ Define.Input("single", FileType, description = "A single file upload.") - Define.Input("multiple", ListOf FileType, description = "Multiple file uploads.") - Define.Input("nullableMultiple", Nullable (ListOf FileType), description = "Optional list of multiple file uploads.") - Define.Input("nullableMultipleNullable", Nullable (ListOf (Nullable FileType)), description = "Optional list of multiple optional file uploads.") ]) + fields = [ + Define.Input ("single", FileType, description = "A single file upload.") + Define.Input ("multiple", ListOf FileType, description = "Multiple file uploads.") + Define.Input ("nullableMultiple", Nullable (ListOf FileType), description = "Optional list of multiple file uploads.") + Define.Input ( + "nullableMultipleNullable", + Nullable (ListOf (Nullable FileType)), + description = "Optional list of multiple optional file uploads." + ) + ] + ) let UploadResponseType = - Define.Object( + Define.Object ( name = "UploadResponse", description = "Contains uploaded files of an upload files request.", - fields = - [ Define.AutoField("single", UploadedFileType, description = "A single file upload.") - Define.AutoField("multiple", ListOf UploadedFileType, description = "Multiple file uploads.") - Define.AutoField("nullableMultiple", Nullable (ListOf UploadedFileType), description = "Optional list of multiple file uploads.") - Define.AutoField("nullableMultipleNullable", Nullable (ListOf (Nullable UploadedFileType)), description = "Optional list of multiple optional file uploads.") ]) + fields = [ + Define.AutoField ("single", UploadedFileType, description = "A single file upload.") + Define.AutoField ("multiple", ListOf UploadedFileType, description = "Multiple file uploads.") + Define.AutoField ("nullableMultiple", Nullable (ListOf UploadedFileType), description = "Optional list of multiple file uploads.") + Define.AutoField ( + "nullableMultipleNullable", + Nullable (ListOf (Nullable UploadedFileType)), + description = "Optional list of multiple optional file uploads." + ) + ] + ) let OutputType = - Define.Object( + Define.Object ( name = "Output", description = "The output for an input.", - fields = - [ Define.AutoField("single", Nullable OutputFieldType, description = "A single output field.") - Define.AutoField("list", Nullable (ListOf OutputFieldType), description = "A list of output fields.") ]) + fields = [ + Define.AutoField ("single", Nullable OutputFieldType, description = "A single output field.") + Define.AutoField ("list", Nullable (ListOf OutputFieldType), description = "A list of output fields.") + ] + ) let QueryType = - Define.Object( + Define.Object ( name = "Query", description = "The query type.", - fields = - [ Define.Field( + fields = [ + Define.Field ( name = "echo", typedef = StructNullable OutputType, description = "Enters an input type and get it back.", - args = [ Define.Input("input", Nullable InputType, description = "The input to be echoed as an output.") ], - resolve = fun ctx _ -> ctx.TryArg("input")) ]) + args = [ Define.Input ("input", Nullable InputType, description = "The input to be echoed as an output.") ], + resolve = fun ctx _ -> ctx.TryArg ("input") + ) + Define.Field ( + name = "alwaysError", + typedef = Nullable StringType, + description = "Always produces an execution error for integration tests.", + args = [], + resolve = + fun _ _ -> + let extensions = + Dictionary ([ KeyValuePair ("code", box "OPERATION_ERROR_TEST"); KeyValuePair ("severity", box 7) ]) + raise (GQLMessageException ("Always fails for tests", extensions)) + ) + ] + ) - let InputFileObject = Define.InputObject( - name = "InputFile", - fields = - [ - Define.Input("file", FileType) - ]) + let InputFileObject = + Define.InputObject (name = "InputFile", fields = [ Define.Input ("file", FileType) ]) let MutationType = let contentAsText (stream : System.IO.Stream) = - use reader = new System.IO.StreamReader(stream, Encoding.UTF8) - reader.ReadToEnd() + use reader = new System.IO.StreamReader (stream, Encoding.UTF8) + reader.ReadToEnd () let getFileContent (ctx : ResolveFieldContext) argName = let inputFile = ctx.Arg argName let stream = inputFile.File.Stream - use reader = new System.IO.StreamReader(stream, Encoding.UTF8, true) - reader.ReadToEnd() - let mapUploadToOutput (file : FileData) = - { Name = file.FileName; ContentType = file.ContentType; ContentAsText = contentAsText file.Stream } - let mapUploadRequestToOutput (request : UploadRequest) = - { - Single = mapUploadToOutput request.Single - Multiple = request.Multiple |> List.map mapUploadToOutput - NullableMultiple = request.NullableMultiple |> Option.map (List.map mapUploadToOutput) - NullableMultipleNullable = request.NullableMultipleNullable |> Option.map (List.map (Option.map mapUploadToOutput)) - } - Define.Object( + use reader = new System.IO.StreamReader (stream, Encoding.UTF8, true) + reader.ReadToEnd () + let mapUploadToOutput (file : FileData) = { + Name = file.FileName + ContentType = file.ContentType + ContentAsText = contentAsText file.Stream + } + let mapUploadRequestToOutput (request : UploadRequest) = { + Single = mapUploadToOutput request.Single + Multiple = request.Multiple |> List.map mapUploadToOutput + NullableMultiple = + request.NullableMultiple + |> Option.map (List.map mapUploadToOutput) + NullableMultipleNullable = + request.NullableMultipleNullable + |> Option.map (List.map (Option.map mapUploadToOutput)) + } + Define.Object ( name = "Mutation", - fields = - [ - Define.Field( - name = "singleUpload", - typedef = UploadedFileType, - description = "Uploads a single file to the server and get it back.", - args = [ Define.Input("file", FileType, description = "The file to be uploaded.") ], - resolve = fun ctx _ -> mapUploadToOutput (ctx.Arg("file"))) - Define.Field( - name = "nullableSingleUpload", - typedef = StructNullable UploadedFileType, - description = "Uploads (maybe) a single file to the server and get it back (maybe).", - args = [ Define.Input("file", Nullable FileType, description = "The file to be uploaded.") ], - resolve = fun ctx _ -> ctx.TryArg("file") |> ValueOption.map mapUploadToOutput) - Define.Field( - name = "multipleUpload", - typedef = ListOf UploadedFileType, - description = "Uploads a list of files to the server and get them back.", - args = [ Define.Input("files", ListOf FileType, description = "The files to upload.") ], - resolve = fun ctx _ -> ctx.Arg("files") |> List.map mapUploadToOutput) - Define.Field( - name = "nullableMultipleUpload", - typedef = StructNullable (ListOf UploadedFileType), - description = "Uploads (maybe) a list of files to the server and get them back (maybe).", - args = [ Define.Input("files", Nullable (ListOf FileType), description = "The files to upload.") ], - resolve = fun ctx _ -> ctx.TryArg("files") |> ValueOption.map (List.map mapUploadToOutput)) - Define.Field( - name = "nullableMultipleNullableUpload", - typedef = StructNullable (ListOf (Nullable UploadedFileType)), - description = "Uploads (maybe) a list of files (maybe) to the server and get them back (maybe).", - args = [ Define.Input("files", Nullable (ListOf (Nullable FileType)), description = "The files to upload.") ], - resolve = fun ctx _ -> ctx.TryArg("files") |> ValueOption.map (List.map (Option.map mapUploadToOutput))) - Define.Field( - name = "uploadRequest", - typedef = UploadResponseType, - description = "Upload several files in different forms.", - args = [ Define.Input("request", UploadRequestType, description = "The request for uploading several files in different forms.") ], - resolve = fun ctx _ -> mapUploadRequestToOutput (ctx.Arg("request"))) - Define.Field ( - name = "uploadComplex", - typedef = StringType, - description = "", - args = [ Define.Input ("input", InputFileObject) ], - resolve = fun ctx _ -> getFileContent ctx "input") - ]) - - let schema : ISchema = upcast Schema(QueryType, MutationType) - - let executor = Executor(schema) + fields = [ + Define.Field ( + name = "singleUpload", + typedef = UploadedFileType, + description = "Uploads a single file to the server and get it back.", + args = [ Define.Input ("file", FileType, description = "The file to be uploaded.") ], + resolve = fun ctx _ -> mapUploadToOutput (ctx.Arg ("file")) + ) + Define.Field ( + name = "nullableSingleUpload", + typedef = StructNullable UploadedFileType, + description = "Uploads (maybe) a single file to the server and get it back (maybe).", + args = [ Define.Input ("file", Nullable FileType, description = "The file to be uploaded.") ], + resolve = fun ctx _ -> ctx.TryArg ("file") |> ValueOption.map mapUploadToOutput + ) + Define.Field ( + name = "multipleUpload", + typedef = ListOf UploadedFileType, + description = "Uploads a list of files to the server and get them back.", + args = [ Define.Input ("files", ListOf FileType, description = "The files to upload.") ], + resolve = fun ctx _ -> ctx.Arg ("files") |> List.map mapUploadToOutput + ) + Define.Field ( + name = "nullableMultipleUpload", + typedef = StructNullable (ListOf UploadedFileType), + description = "Uploads (maybe) a list of files to the server and get them back (maybe).", + args = [ Define.Input ("files", Nullable (ListOf FileType), description = "The files to upload.") ], + resolve = + fun ctx _ -> + ctx.TryArg ("files") + |> ValueOption.map (List.map mapUploadToOutput) + ) + Define.Field ( + name = "nullableMultipleNullableUpload", + typedef = StructNullable (ListOf (Nullable UploadedFileType)), + description = "Uploads (maybe) a list of files (maybe) to the server and get them back (maybe).", + args = [ + Define.Input ("files", Nullable (ListOf (Nullable FileType)), description = "The files to upload.") + ], + resolve = + fun ctx _ -> + ctx.TryArg ("files") + |> ValueOption.map (List.map (Option.map mapUploadToOutput)) + ) + Define.Field ( + name = "uploadRequest", + typedef = UploadResponseType, + description = "Upload several files in different forms.", + args = [ + Define.Input ("request", UploadRequestType, description = "The request for uploading several files in different forms.") + ], + resolve = fun ctx _ -> mapUploadRequestToOutput (ctx.Arg ("request")) + ) + Define.Field ( + name = "uploadComplex", + typedef = StringType, + description = "", + args = [ Define.Input ("input", InputFileObject) ], + resolve = fun ctx _ -> getFileContent ctx "input" + ) + ] + ) + + let schema : ISchema = upcast Schema (QueryType, MutationType) + + let executor = Executor (schema) diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/FSharp.Data.GraphQL.IntegrationTests.fsproj b/tests/FSharp.Data.GraphQL.IntegrationTests/FSharp.Data.GraphQL.IntegrationTests.fsproj index 44bc1d17..f133dee2 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/FSharp.Data.GraphQL.IntegrationTests.fsproj +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/FSharp.Data.GraphQL.IntegrationTests.fsproj @@ -22,6 +22,7 @@ + diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/OperationErrorTests.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/OperationErrorTests.fs new file mode 100644 index 00000000..f95b44df --- /dev/null +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/OperationErrorTests.fs @@ -0,0 +1,128 @@ +module FSharp.Data.GraphQL.IntegrationTests.OperationErrorTests + +open System.Net.Http +open Xunit +open Helpers +open FSharp.Data.GraphQL +open FSharp.Data.GraphQL.Client + +[] +let ServerUrl = "http://localhost:8085" + +type Provider = GraphQLProvider + +module ErrorOperation = + let operation = + Provider.Operation<"""query ErrorQuery { + alwaysError + }"""> () + + type Operation = Provider.Operations.ErrorQuery + +[] +let ``Should parse operation error fields from raw response`` () = + let result = + OperationResultBase ( + rawResponse = new HttpResponseMessage (), + responseJson = + JsonValue.Parse + """{ + "errors": [{ + "message": "unit-test error", + "path": ["alwaysError", 0], + "locations": [{ "line": 2, "column": 13 }], + "extensions": { "code": "UNIT_TEST", "retryable": false, "severity": 7 } + }] + }""", + operationFields = [||], + operationTypeName = "Query" + ) + + result.Errors.Length |> equals 1 + + let error : FSharp.Data.GraphQL.OperationError = result.Errors.[0] + error.Message |> equals "unit-test error" + error.Path |> equals [| box "alwaysError"; box 0 |] + error.Locations |> equals [| { Line = 2; Column = 13 } |] + error.Extensions.["code"] |> equals (box "UNIT_TEST") + error.Extensions.["retryable"] |> equals (box false) + error.Extensions.["severity"] |> equals (box 7) + +[] +let ``Should parse all combinations of optional operation error fields`` () = + let combinations = [ + for includePath in [ false; true ] do + for includeLocations in [ false; true ] do + for includeExtensions in [ false; true ] do + includePath, includeLocations, includeExtensions + ] + + for includePath, includeLocations, includeExtensions in combinations do + let optionalFields = [ + if includePath then + "\"path\":[\"alwaysError\",0]" + if includeLocations then + "\"locations\":[{\"line\":2,\"column\":13}]" + if includeExtensions then + "\"extensions\":{\"code\":\"UNIT_TEST\",\"retryable\":false,\"severity\":7}" + ] + + let errorObjectJson = + "\"message\":\"unit-test combination error\"" + :: optionalFields + |> String.concat "," + + let responseJson = $"""{{"errors":[{{{errorObjectJson}}}]}}""" + + let result = + OperationResultBase ( + rawResponse = new HttpResponseMessage (), + responseJson = JsonValue.Parse responseJson, + operationFields = [||], + operationTypeName = "Query" + ) + + result.Errors.Length |> equals 1 + + let error : FSharp.Data.GraphQL.OperationError = result.Errors.[0] + error.Message |> equals "unit-test combination error" + + if includePath then + error.Path |> equals [| box "alwaysError"; box 0 |] + else + error.Path |> equals [||] + + if includeLocations then + error.Locations |> equals [| { Line = 2; Column = 13 } |] + else + error.Locations |> equals [||] + + if includeExtensions then + error.Extensions.["code"] |> equals (box "UNIT_TEST") + error.Extensions.["retryable"] |> equals (box false) + error.Extensions.["severity"] |> equals (box 7) + else + error.Extensions |> equals Map.empty + +[] +let ``Should map server error extensions and locations into operation result`` () = + let result = ErrorOperation.operation.Run () + + result.Errors.Length |> equals 1 + + let error : FSharp.Data.GraphQL.OperationError = result.Errors.[0] + error.Message |> equals "Always fails for tests" + error.Path |> equals [| box "alwaysError" |] + + error.Locations |> equals [||] + + error.Extensions.ContainsKey "code" |> equals true + error.Extensions.["code"] + |> equals (box "OPERATION_ERROR_TEST") + error.Extensions.ContainsKey "severity" |> equals true + error.Extensions.["severity"] |> equals (box 7) + error.Extensions.ContainsKey "kind" |> equals true + match error.Extensions.["kind"] with + | :? string as kind -> kind |> equals "Execution" + | :? int as kind -> kind |> equals 3 + | kind -> failwithf "Unexpected kind extension value: %A" kind