diff --git a/Packages.props b/Packages.props
index 673f3b7a7..f37d360ad 100644
--- a/Packages.props
+++ b/Packages.props
@@ -73,5 +73,6 @@
+
diff --git a/tests/FSharp.Data.GraphQL.Tests/ErrorHelpers.fs b/tests/FSharp.Data.GraphQL.Tests/ErrorHelpers.fs
index 138b32bdf..2feb909ff 100644
--- a/tests/FSharp.Data.GraphQL.Tests/ErrorHelpers.fs
+++ b/tests/FSharp.Data.GraphQL.Tests/ErrorHelpers.fs
@@ -26,7 +26,7 @@ let ensureRequestError (result : GQLExecutionResult) (onRequestError : GQLProble
| RequestError errors -> onRequestError errors
| response -> fail $"Expected RequestError GQLResponse but got {Environment.NewLine}{response}"
-let ensureValidationError (message : string) (path : FieldPath) (error) =
+let ensureValidationError (message : string) (path : FieldPath) (error : GQLProblemDetails) =
equals message error.Message
equals (Include path) error.Path
match error.Extensions with
@@ -34,7 +34,7 @@ let ensureValidationError (message : string) (path : FieldPath) (error) =
| Include extensions ->
equals Validation (unbox extensions[CustomErrorFields.Kind])
-let ensureExecutionError (message : string) (path : FieldPath) (error) =
+let ensureExecutionError (message : string) (path : FieldPath) (error : GQLProblemDetails) =
equals message error.Message
equals (Include path) error.Path
match error.Extensions with
@@ -42,7 +42,7 @@ let ensureExecutionError (message : string) (path : FieldPath) (error) =
| Include extensions ->
equals Execution (unbox extensions[CustomErrorFields.Kind])
-let ensureInputCoercionError (errorSource : ErrorSource) (message : string) (``type`` : string) (error) =
+let ensureInputCoercionError (errorSource : ErrorSource) (message : string) (``type`` : string) (error : GQLProblemDetails) =
equals message error.Message
match error.Extensions with
| Skip -> fail "Expected extensions to be present"
@@ -56,7 +56,7 @@ let ensureInputCoercionError (errorSource : ErrorSource) (message : string) (``t
equals name (unbox extensions[CustomErrorFields.ArgumentName])
equals ``type`` (unbox extensions[CustomErrorFields.ArgumentType])
-let ensureInputObjectFieldCoercionError (errorSource : ErrorSource) (message : string) (inputObjectPath : FieldPath) (objectType : string) (fieldType : string) (error) =
+let ensureInputObjectFieldCoercionError (errorSource : ErrorSource) (message : string) (inputObjectPath : FieldPath) (objectType : string) (fieldType : string) (error : GQLProblemDetails) =
equals message error.Message
match error.Extensions with
| Skip -> fail "Expected extensions to be present"
@@ -70,7 +70,7 @@ let ensureInputObjectFieldCoercionError (errorSource : ErrorSource) (message : s
equals objectType (unbox extensions[CustomErrorFields.ObjectType])
equals fieldType (unbox extensions[CustomErrorFields.FieldType])
-let ensureInputObjectValidationError (errorSource : ErrorSource) (message : string) (inputObjectPath : FieldPath) (objectType : string) (error) =
+let ensureInputObjectValidationError (errorSource : ErrorSource) (message : string) (inputObjectPath : FieldPath) (objectType : string) (error : GQLProblemDetails) =
equals message error.Message
match error.Extensions with
| Skip -> fail "Expected extensions to be present"
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 3c38c2014..8abaab60a 100644
--- a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj
+++ b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj
@@ -17,6 +17,7 @@
+
@@ -51,6 +52,8 @@
+
+
diff --git a/tests/FSharp.Data.GraphQL.Tests/Helpers.fs b/tests/FSharp.Data.GraphQL.Tests/Helpers.fs
index 6cc0efa22..a50199789 100644
--- a/tests/FSharp.Data.GraphQL.Tests/Helpers.fs
+++ b/tests/FSharp.Data.GraphQL.Tests/Helpers.fs
@@ -89,13 +89,13 @@ let asts query =
["defer"; "stream"]
|> Seq.map (query >> parse)
-let set (mre : ManualResetEvent) =
+let setEvent (mre : ManualResetEvent) =
mre.Set() |> ignore
-let reset (mre : ManualResetEvent) =
+let resetEvent (mre : ManualResetEvent) =
mre.Reset() |> ignore
-let wait (mre : ManualResetEvent) errorMsg =
+let waitEvent (mre : ManualResetEvent) errorMsg =
if TimeSpan.FromSeconds(float 30) |> mre.WaitOne |> not
then fail errorMsg
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 c1282e101..af901f1bc 100644
--- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/CoercionTests.fs
+++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/CoercionTests.fs
@@ -1,5 +1,5 @@
// The MIT License (MIT)
-/// Copyright (c) 2015-Mar 2016 Kevin Thompson @kthompson
+// Copyright (c) 2015-Mar 2016 Kevin Thompson @kthompson
// Copyright (c) 2016 Bazinga Technologies Inc
[]
diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/OptionalsNormalizationTests.ValidString.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/OptionalsNormalizationTests.ValidString.fs
new file mode 100644
index 000000000..1bede223a
--- /dev/null
+++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/OptionalsNormalizationTests.ValidString.fs
@@ -0,0 +1,165 @@
+// The MIT License (MIT)
+
+namespace FSharp.Data.GraphQL.Tests.OptionalsNormalizationTests
+
+open System
+open FSharp.Data.GraphQL
+
+[]
+type ValidString<'t> = internal ValidString of string
+with
+ static member internal CreateVOption<'t> (value: string option) : ValidString<'t> voption =
+ value |> ValueOption.ofOption |> ValueOption.map ValidString
+
+module ValidString =
+
+ let value (ValidString text) = text
+
+ let vOption strOption : string voption = strOption |> ValueOption.map value
+
+ let vOptionValue strOption : string = strOption |> ValueOption.map value |> ValueOption.toObj
+
+open Validus
+open Validus.Operators
+
+module String =
+
+ let notStartsWithWhiteSpace fieldName (s: string) =
+ if s.StartsWith ' ' then
+ Error
+ <| ValidationErrors.create fieldName [ $"'%s{fieldName}' field cannot start with whitespace" ]
+ else
+ Ok <| s
+
+ let notEndsWithWhiteSpace fieldName (s: string) =
+ if s.EndsWith ' ' then
+ Error
+ <| ValidationErrors.create fieldName [ $"'%s{fieldName}' field cannot end with whitespace" ]
+ else
+ Ok <| s
+
+ let notContainsWhiteSpace fieldName (s: string) =
+ if s.Contains ' ' then
+ Error
+ <| ValidationErrors.create fieldName [ $"'%s{fieldName}' field cannot contain whitespace" ]
+ else
+ Ok <| s
+
+ let notContainsBacktick fieldName (s: string) =
+ if s.Contains '`' then
+ Error
+ <| ValidationErrors.create fieldName [ $"'%s{fieldName}' field cannot contain backtick" ]
+ else
+ Ok <| s
+
+ let notContainsTilde fieldName (s: string) =
+ if s.Contains '~' then
+ Error
+ <| ValidationErrors.create fieldName [ $"'%s{fieldName}' field cannot contain tilde" ]
+ else
+ Ok <| s
+
+ let notContainsDash fieldName (s: string) =
+ if s.Contains '-' then
+ Error
+ <| ValidationErrors.create fieldName [ $"'%s{fieldName}' field cannot contain dash: '-'" ]
+ else
+ Ok <| s
+
+ let notContainsUmlauts fieldName (s: string) =
+ let umlauts = [ 'ä'; 'ö'; 'ü'; 'ß'; 'Ä'; 'Ö'; 'Ü' ] |> set
+
+ let contains = s |> Seq.exists (fun c -> umlauts |> Set.contains c)
+
+ if contains then
+ Error
+ <| ValidationErrors.create fieldName [
+ $"'%s{fieldName}' field cannot contain umlauts: ä, ö, ü, ß, Ä, Ö, Ü"
+ ]
+ else
+ Ok <| s
+
+ open Validus.Operators
+
+ let allowEmpty = ValueOption.ofObj >> ValueOption.filter (not << String.IsNullOrWhiteSpace)
+
+ let validateStringCharacters =
+ notStartsWithWhiteSpace
+ <+> notEndsWithWhiteSpace
+ <+> notContainsTilde
+ <+> notContainsUmlauts
+ <+> notContainsBacktick
+
+ module Uri =
+
+ let isValid fieldName uriString =
+ if Uri.IsWellFormedUriString(uriString, UriKind.Absolute) then
+ Ok uriString
+ else
+ Error
+ <| ValidationErrors.create fieldName [ $"'%s{fieldName}' field is not a valid URI" ]
+
+
+//module VOptionString =
+
+// let allow (validator : Validator) : Validator =
+// fun fieldName (value : string voption) ->
+// match value with
+// | ValueNone -> Ok ValueNone
+// | ValueSome str -> (validator *|* ValueSome) fieldName str
+
+// let toValidationResult _ value : ValidationResult =
+// let valueOption =
+// value
+// |> ValueOption.ofObj
+// |> ValueOption.filter (not << String.IsNullOrWhiteSpace)
+// match valueOption with
+// | ValueSome str -> ValueSome str |> Ok
+// | ValueNone -> ValueNone |> Ok
+
+module ValidationErrors =
+
+ let toIGQLErrors (errors: ValidationErrors) : IGQLError list =
+ errors
+ |> ValidationErrors.toList
+ |> List.map (fun e -> { new IGQLError with member _.Message = e })
+
+module Operators =
+
+ let vOption (v1: 'a -> 'a voption) (v2: Validator<'a, 'b>) : Validator<'a, 'b voption> =
+ fun x y ->
+ let value = v1 y
+ match value with
+ | ValueSome value -> (v2 *|* ValueSome) x y
+ | ValueNone -> Ok ValueNone
+
+ let (?=>) v1 v2 = vOption v1 v2
+ let (?=<) v2 v1 = vOption v1 v2
+
+module Scalars =
+
+ open System.Text.Json
+ open FSharp.Data.GraphQL.Ast
+ open FSharp.Data.GraphQL.Types
+ open FSharp.Data.GraphQL.Types.SchemaDefinitions.Errors
+
+ type Define with
+
+ static member ValidStringScalar<'t>(typeName, createValid : Validator, ?description: string) =
+ let createValid = createValid typeName
+ Define.WrappedScalar
+ (name = typeName,
+ coerceInput =
+ (function
+ | Variable e when e.ValueKind = JsonValueKind.String -> e.GetString() |> createValid |> Result.mapError ValidationErrors.toIGQLErrors
+ | InlineConstant (StringValue s) -> s |> createValid |> Result.mapError ValidationErrors.toIGQLErrors
+ | Variable e -> e.GetDeserializeError typeName
+ | InlineConstant value -> value.GetCoerceError typeName),
+ coerceOutput =
+ (function
+ | :? ('t voption) as x -> x |> string |> Some
+ | :? 't as x -> Some (string x)
+ | :? string as s -> s |> Some
+ | null -> None
+ | _ -> raise <| System.NotSupportedException ()),
+ ?description = description)
diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/OptionalsNormalizationTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/OptionalsNormalizationTests.fs
new file mode 100644
index 000000000..34717d2ad
--- /dev/null
+++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/OptionalsNormalizationTests.fs
@@ -0,0 +1,244 @@
+// The MIT License (MIT)
+
+[]
+module FSharp.Data.GraphQL.Tests.OptionalsNormalizationTests
+
+#nowarn "25"
+
+open Xunit
+open System
+open System.Collections.Immutable
+open System.Text.Json
+
+open FSharp.Data.GraphQL
+open FSharp.Data.GraphQL.Types
+open FSharp.Data.GraphQL.Parser
+open FSharp.Data.GraphQL.Samples.StarWarsApi
+
+module Phantom =
+
+ type ZipCode = interface end
+ type City = interface end
+ type State = interface end
+
+ module Address =
+
+ type Line1 = interface end
+ type Line2 = interface end
+
+open FSharp.Data.GraphQL.Tests.OptionalsNormalizationTests
+
+type AddressLine1 = ValidString
+type AddressLine2 = ValidString
+type City = ValidString
+type State = ValidString
+type ZipCode = ValidString
+
+type AddressRecord = {
+ Line1: AddressLine1 voption
+ Line2: AddressLine2 voption
+ City: City voption
+ State: State voption
+ ZipCode: ZipCode voption
+}
+
+type AddressClass(zipCode, city, state, line1, line2) =
+ member _.Line1 : AddressLine1 voption = line1
+ member _.Line2 : AddressLine2 voption = line2
+ member _.City : City voption = city
+ member _.State : State voption = state
+ member _.ZipCode : ZipCode voption = zipCode
+
+[]
+type AddressStruct (
+ zipCode : ZipCode voption,
+ city : City voption,
+ state : State voption,
+ line1 : AddressLine1 voption,
+ line2 : AddressLine2 voption
+) =
+ member _.Line1 = line1
+ member _.Line2 = line2
+ member _.City = city
+ member _.State = state
+ member _.ZipCode = zipCode
+
+
+open Validus
+open FSharp.Data.GraphQL.Tests.OptionalsNormalizationTests.String
+open FSharp.Data.GraphQL.Tests.OptionalsNormalizationTests.Operators
+
+[]
+module State =
+
+ open ValidString
+ open Validus.Operators
+
+ let create : Validator =
+ (Check.String.lessThanLen 100 <+> validateStringCharacters) *|* ValidString
+
+ let createOrWhitespace : Validator =
+ (allowEmpty ?=> (Check.String.lessThanLen 100 <+> validateStringCharacters)) *|* ValueOption.map ValidString
+
+module Address =
+
+ open ValidString
+ open Validus.Operators
+
+ let createLine1 : Validator =
+ (allowEmpty ?=> (Check.String.lessThanLen 1000 <+> validateStringCharacters)) *|* ValueOption.map ValidString
+
+ let createLine2 : Validator =
+ (allowEmpty ?=> (Check.String.lessThanLen 1000 <+> validateStringCharacters)) *|* ValueOption.map ValidString
+
+ let createZipCode : Validator =
+ (allowEmpty ?=> (Check.String.lessThanLen 100 <+> validateStringCharacters)) *|* ValueOption.map ValidString
+
+ let createCity : Validator =
+ (allowEmpty ?=> (Check.String.lessThanLen 100 <+> validateStringCharacters)) *|* ValueOption.map ValidString
+
+
+ open Scalars
+
+ let Line1Type = Define.ValidStringScalar("AddressLine1", createLine1, "Address line 1")
+ let Line2Type = Define.ValidStringScalar("AddressLine2", createLine2, "Address line 2")
+ let ZipCodeType = Define.ValidStringScalar("AddressZipCode", createZipCode, "Address zip code")
+ let CityType = Define.ValidStringScalar("City", createCity)
+ let StateType = Define.ValidStringScalar("State", State.createOrWhitespace)
+
+let InputAddressRecordType =
+ Define.InputObject(
+ name = "InputAddressRecord",
+ fields = [
+ Define.Input("line1", Nullable Address.Line1Type)
+ Define.Input("line2", Nullable Address.Line2Type)
+ Define.Input("zipCode", Nullable Address.ZipCodeType)
+ Define.Input("city", Nullable Address.CityType)
+ Define.Input("state", Nullable Address.StateType)
+ ]
+ )
+
+let InputAddressClassType =
+ Define.InputObject(
+ name = "InputAddressObject",
+ fields = [
+ Define.Input("line1", Nullable Address.Line1Type)
+ Define.Input("line2", Nullable Address.Line2Type)
+ Define.Input("zipCode", Nullable Address.ZipCodeType)
+ Define.Input("city", Nullable Address.CityType)
+ Define.Input("state", Nullable Address.StateType)
+ ]
+ )
+
+let InputAddressStructType =
+ Define.InputObject(
+ name = "InputAddressStruct",
+ fields = [
+ Define.Input("line1", Nullable Address.Line1Type)
+ Define.Input("line2", Nullable Address.Line2Type)
+ Define.Input("zipCode", Nullable Address.ZipCodeType)
+ Define.Input("city", Nullable Address.CityType)
+ Define.Input("state", Nullable Address.StateType)
+ ]
+ )
+
+open FSharp.Data.GraphQL.Execution
+open FSharp.Data.GraphQL.Validation
+open FSharp.Data.GraphQL.Validation.ValidationResult
+open ErrorHelpers
+
+let createSingleError message =
+ [{ new IGQLError with member _.Message = message }]
+
+type InputRecordNested = { HomeAddress : AddressRecord; WorkAddress : AddressRecord option; MailingAddress : AddressRecord voption }
+
+let InputRecordNestedType =
+ Define.InputObject (
+ "InputRecordNested",
+ [ Define.Input ("homeAddress", InputAddressRecordType)
+ Define.Input ("workAddress", Nullable InputAddressRecordType)
+ Define.Input ("mailingAddress", Nullable InputAddressRecordType) ],
+ fun inputRecord ->
+ match inputRecord.MailingAddress, inputRecord.WorkAddress with
+ | ValueNone, None -> ValidationError <| createSingleError "MailingAddress or WorkAddress must be provided"
+ | _ -> Success
+ @@
+ if inputRecord.MailingAddress.IsSome && inputRecord.HomeAddress = inputRecord.MailingAddress.Value then
+ ValidationError <| createSingleError "HomeAddress and MailingAddress must be different"
+ else
+ Success
+ )
+
+let schema =
+ let schema =
+ Schema (
+ query =
+ Define.Object (
+ "Query",
+ fun () ->
+ [ Define.Field (
+ "recordInputs",
+ StringType,
+ [ Define.Input ("record", InputAddressRecordType)
+ Define.Input ("recordOptional", Nullable InputAddressRecordType)
+ Define.Input ("recordNested", Nullable InputRecordNestedType) ],
+ stringifyInput
+ ) // TODO: add all args stringificaiton
+ Define.Field (
+ "objectInputs",
+ StringType,
+ [ Define.Input ("object", InputAddressClassType)
+ Define.Input ("objectOptional", Nullable InputAddressClassType) ],
+ stringifyInput
+ ) // TODO: add all args stringificaiton
+ Define.Field (
+ "structInputs",
+ StringType,
+ [ Define.Input ("struct", InputAddressStructType)
+ Define.Input ("structOptional", Nullable InputAddressStructType) ],
+ stringifyInput
+ ) ] // TODO: add all args stringificaiton
+ )
+ )
+
+ Executor schema
+
+
+[]
+let ``Execute handles validation of valid inline input records with all fields`` () =
+ let query =
+ """{
+ recordInputs(
+ record: { zipCode: "12345", city: "Miami" },
+ recordOptional: { zipCode: "12345", city: "Miami" },
+ recordNested: { homeAddress: { zipCode: "12345", city: "Miami" }, workAddress: { zipCode: "67890", city: "Miami" } }
+ )
+ objectInputs(
+ object: { zipCode: "12345", city: "Miami" },
+ objectOptional: { zipCode: "12345", city: "Miami" }
+ )
+ structInputs(
+ struct: { zipCode: "12345", city: "Miami" },
+ structOptional: { zipCode: "12345", city: "Miami" }
+ )
+ }"""
+ let result = sync <| schema.AsyncExecute(parse query)
+ ensureDirect result <| fun data errors -> empty errors
+
+[]
+let ``Execute handles validation of valid inline input records with mandatory-only fields`` () =
+ let query =
+ """{
+ recordInputs(
+ record: { zipCode: "12345", city: "Miami" },
+ recordNested: { homeAddress: { zipCode: "12345", city: "Miami" }, workAddress: { zipCode: "67890", city: "Miami" } }
+ )
+ objectInputs(
+ object: { zipCode: "12345", city: "Miami" },
+ )
+ structInputs(
+ struct: { zipCode: "12345", city: "Miami" },
+ )
+ }"""
+ let result = sync <| schema.AsyncExecute(parse query)
+ ensureDirect result <| fun data errors -> empty errors