diff --git a/src/FSharp.Data.GraphQL.Server.Relay/Node.fs b/src/FSharp.Data.GraphQL.Server.Relay/Node.fs index 8d17bba97..743afef1e 100644 --- a/src/FSharp.Data.GraphQL.Server.Relay/Node.fs +++ b/src/FSharp.Data.GraphQL.Server.Relay/Node.fs @@ -78,6 +78,6 @@ module GlobalId = Define.Interface( name = "Node", description = "An object that can be uniquely identified by its id", - fields = [ Define.Field("id", IDType) ], + fields = [ Define.Field("id", IDType) ], resolveType = resolveTypeFun possibleTypes) diff --git a/src/FSharp.Data.GraphQL.Server/Linq.fs b/src/FSharp.Data.GraphQL.Server/Linq.fs index 1458caa60..be3b2cf3d 100644 --- a/src/FSharp.Data.GraphQL.Server/Linq.fs +++ b/src/FSharp.Data.GraphQL.Server/Linq.fs @@ -171,7 +171,8 @@ let private applyId: ArgApplication = fun expression callable -> let p0 = Expression.Parameter tSource let idProperty = memberExpr callable.Type "id" p0 // Func predicate = p0 => p0 == value - let predicate = Expression.Lambda(Expression.Equal(idProperty, Expression.Constant (extractValueIfOption callable)), p0) + let toStringMethodInfo = idProperty.Type.GetMethod("ToString", Array.empty) + let predicate = Expression.Lambda(Expression.Equal(Expression.Call(idProperty, toStringMethodInfo), Expression.Constant (extractValueIfOption callable)), p0) let where = methods.Where.MakeGenericMethod [| tSource |] upcast Expression.Call(null, where, expression, predicate) diff --git a/src/FSharp.Data.GraphQL.Server/Values.fs b/src/FSharp.Data.GraphQL.Server/Values.fs index d932da5ed..bd126b569 100644 --- a/src/FSharp.Data.GraphQL.Server/Values.fs +++ b/src/FSharp.Data.GraphQL.Server/Values.fs @@ -13,6 +13,7 @@ open System.Reflection open System.Text.Json open FsToolkit.ErrorHandling +open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Ast open FSharp.Data.GraphQL.Errors open FSharp.Data.GraphQL.Types diff --git a/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs b/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs index 6354dc055..9b30a9850 100644 --- a/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs @@ -21,14 +21,27 @@ module SchemaDefinitions = type InputValue with member inputValue.GetCoerceError(destinationType) = - let getMessage inputType value = $"Inline value '{value}' of type %s{inputType} cannot be converted to %s{destinationType}" + let getMessage inputType value = $"Inline value '{value}' of type %s{inputType} cannot be converted into %s{destinationType}" let message = match inputValue with | IntValue value -> getMessage "integer" value | FloatValue value -> getMessage "float" value | BooleanValue value -> getMessage "boolean" value | StringValue value -> getMessage "string" value - | NullValue -> $"Inline null value cannot be converted to {destinationType}" + | NullValue -> $"Inline value 'null' cannot be converted into {destinationType}" + | EnumValue value -> getMessage "enum" value + | value -> raise <| NotSupportedException $"{value} cannot be passed as scalar input" + Error [{ new IGQLError with member _.Message = message }] + + member inputValue.GetCoerceRangeError(destinationType, minValue, maxValue) = + let getMessage inputType value = $"Inline value '{value}' of type %s{inputType} cannot be converted into %s{destinationType} of range from {minValue} to {maxValue}" + let message = + match inputValue with + | IntValue value -> getMessage "integer" value + | FloatValue value -> getMessage "float" value + | BooleanValue value -> getMessage "boolean" value + | StringValue value -> getMessage "string" value + | NullValue -> $"Inline value 'null' cannot be converted into {destinationType}" | EnumValue value -> getMessage "enum" value | value -> raise <| NotSupportedException $"{value} cannot be passed as scalar input" Error [{ new IGQLError with member _.Message = message }] @@ -36,16 +49,18 @@ module SchemaDefinitions = type JsonElement with member e.GetDeserializeError(destinationType, minValue, maxValue ) = - Error [{ new IGQLError with member _.Message = $"Cannot deserialize JSON value '{e.GetRawText()}' into %s{destinationType} of range from {minValue} to {maxValue}" }] + let jsonValue = match e.ValueKind with JsonValueKind.String -> e.GetString() | _ -> e.GetRawText() + Error [{ new IGQLError with member _.Message = $"JSON value '{jsonValue}' of kind '{e.ValueKind}' cannot be deserialized into %s{destinationType} of range from {minValue} to {maxValue}" }] member e.GetDeserializeError(destinationType) = - Error [{ new IGQLError with member _.Message = $"Cannot deserialize JSON value '{e.GetRawText()}' into %s{destinationType}" }] + let jsonValue = match e.ValueKind with JsonValueKind.String -> e.GetString() | _ -> e.GetRawText() + Error [{ new IGQLError with member _.Message = $"JSON value '{jsonValue}' of kind '{e.ValueKind}' cannot be deserialized into %s{destinationType}" }] let getParseRangeError (destinationType, minValue, maxValue) value = - Error [{ new IGQLError with member _.Message = $"Cannot parse '%s{value}' into %s{destinationType} of range from {minValue} to {maxValue}" }] + Error [{ new IGQLError with member _.Message = $"Inline value '%s{value}' cannot be parsed into %s{destinationType} of range from {minValue} to {maxValue}" }] let getParseError destinationType value = - Error [{ new IGQLError with member _.Message = $"Cannot parse '%s{value}' into %s{destinationType}" }] + Error [{ new IGQLError with member _.Message = $"Inline value '%s{value}' cannot be parsed into %s{destinationType}" }] open System.Globalization @@ -173,12 +188,12 @@ module SchemaDefinitions = | _ -> Some(x.ToString()) /// Tries to convert any value to generic type parameter. - let coerceIdValue (x : obj) : 't option = + let coerceIdValue (x : obj) : string option = match x with | null -> None - | :? string as s -> Some (downcast Convert.ChangeType(s, typeof<'t>)) - | Option o -> Some(downcast Convert.ChangeType(o, typeof<'t>)) - | _ -> Some(downcast Convert.ChangeType(x, typeof<'t>)) + | :? string as s -> Some s + | Option o -> Some(string o) + | _ -> Some(string x) /// Tries to resolve AST query input to int. @@ -189,45 +204,40 @@ module SchemaDefinitions = match e.TryGetInt32() with | true, value -> Ok value | false, _ -> e.GetDeserializeError(destinationType, Int32.MinValue, Int32.MaxValue) - | Variable e -> e.GetDeserializeError destinationType + | Variable e when e.ValueKind = JsonValueKind.True -> Ok 1 + | Variable e when e.ValueKind = JsonValueKind.False -> Ok 0 + | Variable e -> e.GetDeserializeError (destinationType, Int32.MinValue, Int32.MaxValue) | InlineConstant (IntValue i) -> Ok (int i) - | InlineConstant (FloatValue f) -> Ok (int f) - | InlineConstant (StringValue s) -> - match Int32.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture) with - | true, i -> Ok i - | false, _ -> getParseRangeError(destinationType, Int32.MinValue, Int32.MaxValue) s | InlineConstant (BooleanValue b) -> Ok (if b then 1 else 0) - | InlineConstant value -> value.GetCoerceError destinationType + | InlineConstant value -> value.GetCoerceRangeError(destinationType, Int32.MinValue, Int32.MaxValue) /// Tries to resolve AST query input to int64. let coerceLongInput = let destinationType = "integer" function - | Variable e when e.ValueKind = JsonValueKind.Number -> Ok (e.GetInt64()) - | Variable e -> e.GetDeserializeError destinationType + | Variable e when e.ValueKind = JsonValueKind.Number -> + match e.TryGetInt64() with + | true, value -> Ok value + | false, _ -> e.GetDeserializeError(destinationType, Int64.MinValue, Int64.MaxValue) + | Variable e when e.ValueKind = JsonValueKind.True -> Ok 1L + | Variable e when e.ValueKind = JsonValueKind.False -> Ok 0L + | Variable e -> e.GetDeserializeError (destinationType, Int64.MinValue, Int64.MaxValue) | InlineConstant (IntValue i) -> Ok (int64 i) - | InlineConstant (FloatValue f) -> Ok(int64 f) - | InlineConstant (StringValue s) -> - match Int64.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture) with - | true, i -> Ok i - | false, _ -> getParseRangeError(destinationType, Int64.MinValue, Int64.MaxValue) s | InlineConstant (BooleanValue b) -> Ok(if b then 1L else 0L) - | InlineConstant value -> value.GetCoerceError destinationType + | InlineConstant value -> value.GetCoerceRangeError(destinationType, Int64.MinValue, Int64.MaxValue) /// Tries to resolve AST query input to double. let coerceFloatInput = let destinationType = "float" function | Variable e when e.ValueKind = JsonValueKind.Number -> Ok (e.GetDouble()) - | Variable e -> e.GetDeserializeError destinationType + | Variable e when e.ValueKind = JsonValueKind.True -> Ok 1. + | Variable e when e.ValueKind = JsonValueKind.False -> Ok 0. + | Variable e -> e.GetDeserializeError (destinationType, Double.MinValue, Double.MaxValue) | InlineConstant (IntValue i) -> Ok(double i) | InlineConstant (FloatValue f) -> Ok f - | InlineConstant (StringValue s) -> - match Double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture) with - | true, i -> Ok i - | false, _ -> getParseRangeError(destinationType, Double.MinValue, Double.MaxValue) s | InlineConstant (BooleanValue b) -> Ok(if b then 1. else 0.) - | InlineConstant value -> value.GetCoerceError destinationType + | InlineConstant value -> value.GetCoerceRangeError(destinationType, Double.MinValue, Double.MaxValue) /// Tries to resolve AST query input to string. let coerceStringInput = @@ -244,6 +254,7 @@ module SchemaDefinitions = | InlineConstant (FloatValue f) -> Ok(f.ToString(CultureInfo.InvariantCulture)) | InlineConstant (StringValue s) -> Ok s | InlineConstant (BooleanValue b) -> Ok (if b then "true" else "false") + | InlineConstant (EnumValue e) -> Ok e | InlineConstant value -> value.GetCoerceError destinationType let coerceEnumInput = @@ -257,25 +268,29 @@ module SchemaDefinitions = function | Variable e when e.ValueKind = JsonValueKind.True -> Ok true | Variable e when e.ValueKind = JsonValueKind.False -> Ok false + | Variable e when e.ValueKind = JsonValueKind.Number -> Ok (if e.GetDouble() = 0. then false else true) | Variable e -> e.GetDeserializeError destinationType | InlineConstant (IntValue i) -> Ok(if i = 0L then false else true) | InlineConstant (FloatValue f) -> Ok(if f = 0. then false else true) - | InlineConstant (StringValue s) -> - match Boolean.TryParse(s) with - | true, i -> Ok i - | false, _ -> getParseError destinationType s | InlineConstant (BooleanValue b) -> Ok b | InlineConstant value -> value.GetCoerceError destinationType /// Tries to resolve AST query input to provided generic type. - let coerceIdInput input : Result<'t, IGQLError list> = + let coerceIdInput input : Result = + let destinationType = "identifier" match input with - | Variable e when e.ValueKind = JsonValueKind.String -> Ok (downcast Convert.ChangeType(e.GetString() , typeof<'t>)) - | Variable e when e.ValueKind = JsonValueKind.Number -> Ok (downcast Convert.ChangeType(e.GetInt32() , typeof<'t>)) - | Variable e -> e.GetDeserializeError typeof<'t>.Name - | InlineConstant (IntValue i) -> Ok(downcast Convert.ChangeType(i, typeof<'t>)) - | InlineConstant (StringValue s) -> Ok(downcast Convert.ChangeType(s, typeof<'t>)) - | InlineConstant value -> value.GetCoerceError "id" + | Variable e when e.ValueKind = JsonValueKind.String -> Ok (e.GetString()) + | Variable e when e.ValueKind = JsonValueKind.Number -> + try + e.GetInt64() |> ignore + Ok (e.GetRawText()) + with :? FormatException -> + e.GetDeserializeError(destinationType, Int64.MinValue, Int64.MaxValue) + | Variable e -> e.GetDeserializeError destinationType + | InlineConstant (IntValue i) -> Ok(string i) + | InlineConstant (FloatValue i) -> (FloatValue i).GetCoerceRangeError(destinationType, Int64.MinValue, Int64.MaxValue) + | InlineConstant (StringValue s) -> Ok s + | InlineConstant value -> value.GetCoerceError destinationType /// Tries to resolve AST query input to URI. let coerceUriInput = @@ -305,7 +320,7 @@ module SchemaDefinitions = match DateTimeOffset.TryParse(s) with | true, date -> Ok date | false, _ -> getParseRangeError(destinationType, DateTimeOffset.MinValue, DateTimeOffset.MaxValue) s - | InlineConstant value -> value.GetCoerceError destinationType + | InlineConstant value -> value.GetCoerceRangeError(destinationType, DateTimeOffset.MinValue, DateTimeOffset.MaxValue) /// Tries to resolve AST query input to DateOnly. let coerceDateOnlyInput = @@ -321,7 +336,7 @@ module SchemaDefinitions = match DateOnly.TryParse(s) with | true, date -> Ok date | false, _ -> getParseRangeError(destinationType, DateOnly.MinValue, DateOnly.MaxValue) s - | InlineConstant value -> value.GetCoerceError destinationType + | InlineConstant value -> value.GetCoerceRangeError(destinationType, DateOnly.MinValue, DateOnly.MaxValue) /// Tries to resolve AST query input to Guid. let coerceGuidInput = @@ -402,7 +417,7 @@ module SchemaDefinitions = CoerceOutput = coerceStringValue } /// GraphQL type for custom identifier - let IDType<'Val> : ScalarDefinition<'Val> = + let IDType : ScalarDefinition = { Name = "ID" Description = Some @@ -515,10 +530,38 @@ module SchemaDefinitions = type Define = /// - /// Creates GraphQL type definition for user defined scalars. + /// Creates GraphQL type definition for user defined scalar. + /// + /// Type name. Must be unique in scope of the current schema. + /// Function used to resolve .NET object from GraphQL query AST or variable. + /// Function used to cross cast to .NET types. + /// Optional scalar description. Usefull for generating documentation. + static member Scalar(name : string, coerceInput : InputParameterValue -> Result<'T, string>, + coerceOutput : obj -> 'T option, ?description : string) : ScalarDefinition<'T> = + { Name = name + Description = description + CoerceInput = coerceInput >> Result.mapError (fun msg -> { new IGQLError with member _.Message = msg } |> List.singleton) + CoerceOutput = coerceOutput } + + /// + /// Creates GraphQL type definition for user defined scalar. /// /// Type name. Must be unique in scope of the current schema. - /// Function used to resolve .NET object from GraphQL query AST. + /// Function used to resolve .NET object from GraphQL query AST or variable. + /// Function used to cross cast to .NET types. + /// Optional scalar description. Usefull for generating documentation. + static member Scalar(name : string, coerceInput : InputParameterValue -> Result<'T, string list>, + coerceOutput : obj -> 'T option, ?description : string) : ScalarDefinition<'T> = + { Name = name + Description = description + CoerceInput = coerceInput >> Result.mapError (List.map (fun msg -> { new IGQLError with member _.Message = msg })) + CoerceOutput = coerceOutput } + + /// + /// Creates GraphQL type definition for user defined scalar. + /// + /// Type name. Must be unique in scope of the current schema. + /// Function used to resolve .NET object from GraphQL query AST or variable. /// Function used to cross cast to .NET types. /// Optional scalar description. Usefull for generating documentation. static member Scalar(name : string, coerceInput : InputParameterValue -> Result<'T, IGQLError>, @@ -529,10 +572,10 @@ module SchemaDefinitions = CoerceOutput = coerceOutput } /// - /// Creates GraphQL type definition for user defined scalars. + /// Creates GraphQL type definition for user defined scalar. /// /// Type name. Must be unique in scope of the current schema. - /// Function used to resolve .NET object from GraphQL query AST. + /// Function used to resolve .NET object from GraphQL query AST or variable. /// Function used to cross cast to .NET types. /// Optional scalar description. Usefull for generating documentation. static member Scalar(name : string, coerceInput : InputParameterValue -> Result<'T, IGQLError list>, @@ -542,6 +585,62 @@ module SchemaDefinitions = CoerceInput = coerceInput CoerceOutput = coerceOutput } + /// + /// Creates GraphQL type definition for user defined wrapped scalar. + /// + /// Type name. Must be unique in scope of the current schema. + /// Function used to resolve .NET object from GraphQL query AST or variable. + /// Function used to cross cast to .NET types. + /// Optional scalar description. Usefull for generating documentation. + static member WrappedScalar(name : string, coerceInput : InputParameterValue -> Result<'Wrapper, string>, + coerceOutput : obj -> 'Primitive option, ?description : string) : ScalarDefinition<'Primitive, 'Wrapper> = + { Name = name + Description = description + CoerceInput = coerceInput >> Result.mapError (fun msg -> { new IGQLError with member _.Message = msg } |> List.singleton) + CoerceOutput = coerceOutput } + + /// + /// Creates GraphQL type definition for user defined wrapped scalar. + /// + /// Type name. Must be unique in scope of the current schema. + /// Function used to resolve .NET object from GraphQL query AST or variable. + /// Function used to cross cast to .NET types. + /// Optional scalar description. Usefull for generating documentation. + static member WrappedScalar(name : string, coerceInput : InputParameterValue -> Result<'Wrapper, string list>, + coerceOutput : obj -> 'Primitive option, ?description : string) : ScalarDefinition<'Primitive, 'Wrapper> = + { Name = name + Description = description + CoerceInput = coerceInput >> Result.mapError (List.map (fun msg -> { new IGQLError with member _.Message = msg })) + CoerceOutput = coerceOutput } + + /// + /// Creates GraphQL type definition for user defined wrapped scalar. + /// + /// Type name. Must be unique in scope of the current schema. + /// Function used to resolve .NET object from GraphQL query AST or variable. + /// Function used to cross cast to .NET types. + /// Optional scalar description. Usefull for generating documentation. + static member WrappedScalar(name : string, coerceInput : InputParameterValue -> Result<'Wrapper, IGQLError>, + coerceOutput : obj -> 'Primitive option, ?description : string) : ScalarDefinition<'Primitive, 'Wrapper> = + { Name = name + Description = description + CoerceInput = coerceInput >> Result.mapError List.singleton + CoerceOutput = coerceOutput } + + /// + /// Creates GraphQL type definition for user defined wrapped scalar. + /// + /// Type name. Must be unique in scope of the current schema. + /// Function used to resolve .NET object from GraphQL query AST or variable. + /// Function used to cross cast to .NET types. + /// Optional scalar description. Usefull for generating documentation. + static member WrappedScalar(name : string, coerceInput : InputParameterValue -> Result<'Wrapper, IGQLError list>, + coerceOutput : obj -> 'Primitive option, ?description : string) : ScalarDefinition<'Primitive, 'Wrapper> = + { Name = name + Description = description + CoerceInput = coerceInput + CoerceOutput = coerceOutput } + /// /// Creates GraphQL type definition for user defined enums. /// diff --git a/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs b/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs index 64fe199ea..21373f7a4 100644 --- a/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs +++ b/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs @@ -1001,17 +1001,17 @@ and ScalarDef = inherit LeafDef end -/// Concrete representation of the scalar types. -and [] ScalarDefinition<'Val> = +/// Concrete representation of the scalar types wrapped into a value object. +and [] ScalarDefinition<'Primitive, 'Val> = { /// Name of the scalar type. Name : string /// Optional type description. Description : string option - /// A function used to retrieve a .NET object from provided GraphQL query. + /// A function used to retrieve a .NET object from provided GraphQL query or JsonElement variable. CoerceInput : InputParameterValue -> Result<'Val, IGQLError list> /// A function used to set a surrogate representation to be /// returned as a query result. - CoerceOutput : obj -> 'Val option } + CoerceOutput : obj -> 'Primitive option } interface TypeDef with member _.Type = typeof<'Val> @@ -1042,12 +1042,14 @@ and [] ScalarDefinition<'Val> = override x.Equals y = match y with - | :? ScalarDefinition<'Val> as s -> x.Name = s.Name + | :? ScalarDefinition<'Primitive, 'Val> as s -> x.Name = s.Name | _ -> false override x.GetHashCode() = x.Name.GetHashCode() override x.ToString() = x.Name + "!" +and ScalarDefinition<'Val> = ScalarDefinition<'Val, 'Val> + /// A GraphQL representation of single case of the enum type. /// Enum value return value is always represented as string. and EnumVal = diff --git a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj index 8581cc6f2..2bf279a36 100644 --- a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj +++ b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj @@ -24,6 +24,7 @@ + diff --git a/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs index 7c3fb39dd..25edd90a3 100644 --- a/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs @@ -24,7 +24,7 @@ let Contact = Define.Object("Contact", [ Define.Field("email", StringType, fun _ let Person = Define.Object("Person", - [ Define.Field("id", IDType, fun _ x -> x.ID) + [ Define.Field("id", IDType, fun _ x -> string x.ID) Define.AutoField("firstName", StringType) Define.Field("lastName", StringType, fun _ x -> x.LastName) Define.Field("fullName", StringType, fun _ x -> x.FirstName + " " + x.LastName) @@ -60,7 +60,7 @@ let resolveRoot ctx () = result let linqArgs = - [ Define.Input("id", Nullable IDType) + [ Define.Input("id", Nullable IDType) Define.Input("skip", Nullable IntType) Define.Input("take", Nullable IntType) Define.Input("orderBy", Nullable StringType) diff --git a/tests/FSharp.Data.GraphQL.Tests/TestAttributes.fs b/tests/FSharp.Data.GraphQL.Tests/TestAttributes.fs new file mode 100644 index 000000000..4d3cdc491 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TestAttributes.fs @@ -0,0 +1,22 @@ +namespace FSharp.Data.GraphQL.Tests + +open System +open System.Globalization +open Xunit.Sdk + +type UseInvariantCultureAttribute() = + inherit BeforeAfterTestAttribute() + + let mutable _originalUICulture: CultureInfo = null + let mutable _originalCulture: CultureInfo = null + + override _.Before (methodUnderTest) = + _originalUICulture <- CultureInfo.CurrentUICulture + _originalCulture <- CultureInfo.CurrentCulture + + CultureInfo.CurrentUICulture <- CultureInfo.InvariantCulture + CultureInfo.CurrentCulture <- CultureInfo.InvariantCulture + + override _.After (methodUnderTest) = + CultureInfo.CurrentUICulture <- _originalUICulture + CultureInfo.CurrentCulture <- _originalCulture diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/CoercionTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/CoercionTests.fs index b1cebe281..c1282e101 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/CoercionTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/CoercionTests.fs @@ -2,11 +2,13 @@ /// Copyright (c) 2015-Mar 2016 Kevin Thompson @kthompson // Copyright (c) 2016 Bazinga Technologies Inc +[] module FSharp.Data.GraphQL.Tests.CoercionTests #nowarn "25" open System +open System.Text.Json open Xunit open FSharp.Data.GraphQL.Ast open FSharp.Data.GraphQL.Types @@ -17,45 +19,96 @@ let private testCoercion graphQLType (expected: 't) actual = let result = (scalar.CoerceInput actual) |> Result.map (fun x -> downcast x) match result with | Ok x -> equals expected x - | Error _ -> raise (Exception(sprintf "Expected %A to be able to be coerced to %A" actual expected)) + | Error _ -> Assert.Fail $"Expected %A{actual} to be able to be coerced to %A{expected}" +let private testCoercionError graphQLType (expectedErrorMessage: string) actual = + let (Scalar scalar) = graphQLType + let result = (scalar.CoerceInput actual) |> Result.map (fun x -> downcast x) + match result with + | Ok _ -> Assert.Fail($"Expected %A{actual} to not be able to be coerced to %A{graphQLType.Type.Name}") + | Error errs -> equals expectedErrorMessage errs.Head.Message [] let ``Int coerces input`` () = + testCoercion IntType 123 (Variable (JsonDocument.Parse "123").RootElement) testCoercion IntType 123 (InlineConstant (IntValue 123L)) - testCoercion IntType 123 (InlineConstant (FloatValue 123.4)) - testCoercion IntType 123 (InlineConstant (StringValue "123")) + testCoercionError IntType "JSON value '123.4' of kind 'Number' cannot be deserialized into integer of range from -2147483648 to 2147483647" (Variable (JsonDocument.Parse "123.4").RootElement) + testCoercionError IntType "Inline value '123.4' of type float cannot be converted into integer of range from -2147483648 to 2147483647" (InlineConstant (FloatValue 123.4)) + testCoercion IntType 1 (Variable (JsonDocument.Parse "true").RootElement) testCoercion IntType 1 (InlineConstant (BooleanValue true)) + testCoercion IntType 0 (Variable (JsonDocument.Parse "false").RootElement) testCoercion IntType 0 (InlineConstant (BooleanValue false)) + testCoercionError IntType "JSON value 'enum' of kind 'String' cannot be deserialized into integer of range from -2147483648 to 2147483647" (Variable (JsonDocument.Parse "\"enum\"").RootElement) + testCoercionError IntType "Inline value 'enum' of type enum cannot be converted into integer of range from -2147483648 to 2147483647" (InlineConstant (EnumValue "enum")) + +[] +let ``Long coerces input`` () = + testCoercion LongType 123L (Variable (JsonDocument.Parse "123").RootElement) + testCoercion LongType 123L (InlineConstant (IntValue 123L)) + testCoercionError LongType "JSON value '123.4' of kind 'Number' cannot be deserialized into integer of range from -9223372036854775808 to 9223372036854775807" (Variable (JsonDocument.Parse "123.4").RootElement) + testCoercionError LongType "Inline value '123.4' of type float cannot be converted into integer of range from -9223372036854775808 to 9223372036854775807" (InlineConstant (FloatValue 123.4)) + testCoercion LongType 1L (Variable (JsonDocument.Parse "true").RootElement) + testCoercion LongType 1L (InlineConstant (BooleanValue true)) + testCoercion LongType 0L (Variable (JsonDocument.Parse "false").RootElement) + testCoercion LongType 0L (InlineConstant (BooleanValue false)) + testCoercionError LongType "JSON value 'enum' of kind 'String' cannot be deserialized into integer of range from -9223372036854775808 to 9223372036854775807" (Variable (JsonDocument.Parse "\"enum\"").RootElement) + testCoercionError LongType "Inline value 'enum' of type enum cannot be converted into integer of range from -9223372036854775808 to 9223372036854775807" (InlineConstant (EnumValue "enum")) [] let ``Float coerces input`` () = + testCoercion FloatType 123. (Variable (JsonDocument.Parse "123").RootElement) testCoercion FloatType 123. (InlineConstant (IntValue 123L)) + testCoercion FloatType 123.4 (Variable (JsonDocument.Parse "123.4").RootElement) testCoercion FloatType 123.4 (InlineConstant (FloatValue 123.4)) - testCoercion FloatType 123.4 (InlineConstant (StringValue "123.4")) + testCoercion FloatType 1. (Variable (JsonDocument.Parse "true").RootElement) testCoercion FloatType 1. (InlineConstant (BooleanValue true)) + testCoercion FloatType 0. (Variable (JsonDocument.Parse "false").RootElement) testCoercion FloatType 0. (InlineConstant (BooleanValue false)) - -[] -let ``Long coerces input`` () = - testCoercion LongType 123L (InlineConstant (IntValue 123L)) - testCoercion LongType 123L (InlineConstant (FloatValue 123.4)) - testCoercion LongType 123L (InlineConstant (StringValue "123")) - testCoercion LongType 1L (InlineConstant (BooleanValue true)) - testCoercion LongType 0L (InlineConstant (BooleanValue false)) + testCoercionError FloatType "JSON value 'enum' of kind 'String' cannot be deserialized into float of range from -1.7976931348623157E+308 to 1.7976931348623157E+308" (Variable (JsonDocument.Parse "\"enum\"").RootElement) + testCoercionError FloatType "Inline value 'enum' of type enum cannot be converted into float of range from -1.7976931348623157E+308 to 1.7976931348623157E+308" (InlineConstant (EnumValue "enum")) [] let ``Boolean coerces input`` () = - testCoercion BooleanType true (InlineConstant (IntValue 123L)) + testCoercion BooleanType false (Variable (JsonDocument.Parse "0").RootElement) testCoercion BooleanType false (InlineConstant (IntValue 0L)) + testCoercion BooleanType true (Variable (JsonDocument.Parse "123").RootElement) + testCoercion BooleanType true (InlineConstant (IntValue 123L)) + testCoercion BooleanType true (Variable (JsonDocument.Parse "123.4").RootElement) testCoercion BooleanType true (InlineConstant (FloatValue 123.4)) + testCoercion BooleanType true (Variable (JsonDocument.Parse "true").RootElement) testCoercion BooleanType true (InlineConstant (BooleanValue true)) + testCoercion BooleanType false (Variable (JsonDocument.Parse "false").RootElement) testCoercion BooleanType false (InlineConstant (BooleanValue false)) + testCoercionError BooleanType "JSON value 'enum' of kind 'String' cannot be deserialized into boolean" (Variable (JsonDocument.Parse "\"enum\"").RootElement) + testCoercionError BooleanType "Inline value 'enum' of type enum cannot be converted into boolean" (InlineConstant (EnumValue "enum")) [] let ``String coerces input`` () = + testCoercion StringType "123" (Variable (JsonDocument.Parse "123").RootElement) testCoercion StringType "123" (InlineConstant (IntValue 123L)) + testCoercion StringType "123.4" (Variable (JsonDocument.Parse "123.4").RootElement) testCoercion StringType "123.4" (InlineConstant (FloatValue 123.4)) + testCoercion StringType "abc123.4" (Variable (JsonDocument.Parse "\"abc123.4\"").RootElement) testCoercion StringType "acb123.4" (InlineConstant (StringValue "acb123.4")) + testCoercion StringType "true" (Variable (JsonDocument.Parse "true").RootElement) testCoercion StringType "true" (InlineConstant (BooleanValue true)) + testCoercion StringType "false" (Variable (JsonDocument.Parse "false").RootElement) testCoercion StringType "false" (InlineConstant (BooleanValue false)) + testCoercion StringType "enum" (Variable (JsonDocument.Parse "\"enum\"").RootElement) + testCoercion StringType "enum" (InlineConstant (EnumValue "enum")) + +[] +let ``ID coerces input`` () = + testCoercion IDType "123" (Variable (JsonDocument.Parse "123").RootElement) + testCoercion IDType "123" (InlineConstant (IntValue 123L)) + testCoercionError IDType "JSON value '123.4' of kind 'Number' cannot be deserialized into identifier of range from -9223372036854775808 to 9223372036854775807" (Variable (JsonDocument.Parse "123.4").RootElement) + testCoercionError IDType "Inline value '123.4' of type float cannot be converted into identifier of range from -9223372036854775808 to 9223372036854775807" (InlineConstant (FloatValue 123.4)) + testCoercion IDType "abc123.4" (Variable (JsonDocument.Parse "\"abc123.4\"").RootElement) + testCoercion IDType "acb123.4" (InlineConstant (StringValue "acb123.4")) + testCoercionError IDType "JSON value 'true' of kind 'True' cannot be deserialized into identifier" (Variable (JsonDocument.Parse "true").RootElement) + testCoercionError IDType "Inline value 'True' of type boolean cannot be converted into identifier" (InlineConstant (BooleanValue true)) + testCoercionError IDType "JSON value 'false' of kind 'False' cannot be deserialized into identifier" (Variable (JsonDocument.Parse "false").RootElement) + testCoercionError IDType "Inline value 'False' of type boolean cannot be converted into identifier" (InlineConstant (BooleanValue false)) + // We have no idea that it is an enum + testCoercion IDType "enum" (Variable (JsonDocument.Parse "\"enum\"").RootElement) + testCoercionError IDType "Inline value 'enum' of type enum cannot be converted into identifier" (InlineConstant (EnumValue "enum"))