Skip to content

Commit cf54384

Browse files
committed
Implemented support of Guid and value object scalars as InputValue
* Enhanced `InputValue.OfObject` to handle `Guid` (as `StringValue "D"`), `IReadOnlyDictionary`/`IDictionary` (as `ObjectValue`), and improved F# union handling. * Added `GuidId` DU, wrapped scalar, and new `Guid`/`ValueObject` fields to test types, extended tests for filtering with `Guid` and custom value object scalars.
1 parent 0c37497 commit cf54384

2 files changed

Lines changed: 159 additions & 21 deletions

File tree

src/FSharp.Data.GraphQL.Shared/Ast.fs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ namespace FSharp.Data.GraphQL.Ast
44

55
open System
66
open System.Text.Json
7+
open Microsoft.FSharp.Reflection
78
//NOTE: For references, see https://facebook.github.io/graphql/
89

910
/// 2.2 Query Document
@@ -124,26 +125,35 @@ and InputValue =
124125
| :? single as value -> FloatValue (double value)
125126
| :? bool as value -> BooleanValue value
126127
| :? string as value -> StringValue value
128+
| :? Guid as value -> StringValue (value.ToString "D")
127129
| :? uint64 as value -> IntValue (int64 value)
128130
| :? uint32 as value -> IntValue (int64 value)
129131
| :? uint16 as value -> IntValue (int64 value)
132+
| :? System.Collections.Generic.IReadOnlyDictionary<string, obj> as dict ->
133+
let map =
134+
dict
135+
|> Seq.map (fun kv -> kv.Key, InputValue.OfObject kv.Value)
136+
|> Map.ofSeq
137+
ObjectValue map
138+
| :? System.Collections.Generic.IDictionary<string, obj> as dict ->
139+
let map =
140+
dict
141+
|> Seq.map (fun kv -> kv.Key, InputValue.OfObject kv.Value)
142+
|> Map.ofSeq
143+
ObjectValue map
130144
| value ->
131145
let ``type`` = value.GetType()
132146
if ``type``.IsArray then
133147
let array = value :?> System.Array
134148
let list = [ for i in 0 .. array.Length - 1 -> InputValue.OfObject (array.GetValue i) ]
135149
ListValue list
150+
elif FSharpType.IsUnion (``type``, true) then
151+
let _, unionFields = FSharpValue.GetUnionFields (value, ``type``, true)
152+
match unionFields with
153+
| [| singleField |] -> InputValue.OfObject singleField
154+
| _ -> failwith "Cannot convert object to 'InputValue'"
136155
else
137-
let genericType = ``type``.GetGenericTypeDefinition()
138-
if typeof<System.Collections.Generic.IReadOnlyDictionary<string, obj>>.IsAssignableFrom genericType then
139-
let dict = value :?> System.Collections.Generic.IReadOnlyDictionary<string, obj>
140-
let map =
141-
dict
142-
|> Seq.map (fun kv -> kv.Key.ToString(), InputValue.OfObject kv.Value)
143-
|> Map.ofSeq
144-
ObjectValue map
145-
else
146-
failwith "Cannot convert object to 'InputValue'"
156+
failwith "Cannot convert object to 'InputValue'"
147157

148158
static member OfJsonElement (element : JsonElement) =
149159
match element.ValueKind with

tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs

Lines changed: 139 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,65 @@ open FSharp
1010
open FSharp.Data.GraphQL
1111
open FSharp.Data.GraphQL.Types
1212
open FSharp.Data.GraphQL.Server.Middleware
13-
open FSharp.Data.GraphQL.Shared
1413
open FSharp.Data.GraphQL.Parser
15-
open FSharp.Data.GraphQL.Execution
1614
open FSharp.Data.GraphQL.Ast
1715

1816
#nowarn "40"
1917

2018
type Root = { clientId : int }
2119

22-
and Subject =
20+
type GuidId = ValueObjectId of Guid
21+
22+
let private parseGuidId (value : string) =
23+
match Guid.TryParse value with
24+
| true, guid -> Ok (ValueObjectId guid)
25+
| false, _ ->
26+
Error [
27+
{ new IGQLError with
28+
member _.Message = $"Cannot coerce '{value}' to GuidID"
29+
}
30+
]
31+
32+
let private guidIdToString (ValueObjectId guidId) = guidId.ToString "D"
33+
34+
let ValueObjectType =
35+
Define.WrappedScalar (
36+
name = "ValueObject",
37+
coerceInput =
38+
(function
39+
| InputParameterValue.Variable value when value.ValueKind = JsonValueKind.String -> parseGuidId (value.GetString ())
40+
| InputParameterValue.InlineConstant (StringValue value) -> parseGuidId value
41+
| _ ->
42+
Error [
43+
{ new IGQLError with
44+
member _.Message = "ValueObject must be provided as string"
45+
}
46+
]),
47+
coerceOutput =
48+
(function
49+
| :? GuidId as guid -> guidIdToString guid |> Some
50+
| _ -> None)
51+
)
52+
53+
type Subject =
2354
| A of A
2455
| B of B
2556

26-
and A = { Id : int; Value : string; Subjects : int list }
57+
and A = {
58+
Id : int
59+
Value : string
60+
GuidValue : Guid
61+
ValueObject : GuidId
62+
Subjects : int list
63+
}
2764

28-
and B = { Id : int; Value : string; Subjects : int list }
65+
and B = {
66+
Id : int
67+
Value : string
68+
GuidValue : Guid
69+
ValueObject : GuidId
70+
Subjects : int list
71+
}
2972

3073
type Complex = {
3174
Id : int
@@ -43,12 +86,12 @@ type Property =
4386
| Community of Community
4487

4588
let getExecutor (expectedFilter : ObjectListFilter voption) =
46-
let a1 : A = { Id = 1; Value = "A1"; Subjects = [ 2; 6 ] }
47-
let a2 : A = { Id = 2; Value = "A2"; Subjects = [ 1; 3; 5 ] }
48-
let a3 : A = { Id = 3; Value = "A3"; Subjects = [ 1; 2; 4 ] }
49-
let b1 = { Id = 4; Value = "1000"; Subjects = [ 1; 5 ] }
50-
let b2 = { Id = 5; Value = "2000"; Subjects = [ 3; 4; 6 ] }
51-
let b3 = { Id = 6; Value = "3000"; Subjects = [ 1; 3; 5 ] }
89+
let a1 : A = { Id = 1; Value = "A1"; GuidValue = Guid.Parse "11111111-1111-1111-1111-111111111111"; ValueObject = ValueObjectId (Guid.Parse "11111111-1111-1111-1111-111111111111"); Subjects = [ 2; 6 ] }
90+
let a2 : A = { Id = 2; Value = "A2"; GuidValue = Guid.Parse "22222222-2222-2222-2222-222222222222"; ValueObject = ValueObjectId (Guid.Parse "22222222-2222-2222-2222-222222222222"); Subjects = [ 1; 3; 5 ] }
91+
let a3 : A = { Id = 3; Value = "A3"; GuidValue = Guid.Parse "33333333-3333-3333-3333-333333333333"; ValueObject = ValueObjectId (Guid.Parse "33333333-3333-3333-3333-333333333333"); Subjects = [ 1; 2; 4 ] }
92+
let b1 = { Id = 4; Value = "1000"; GuidValue = Guid.Parse "44444444-4444-4444-4444-444444444444"; ValueObject = ValueObjectId (Guid.Parse "44444444-4444-4444-4444-444444444444"); Subjects = [ 1; 5 ] }
93+
let b2 = { Id = 5; Value = "2000"; GuidValue = Guid.Parse "55555555-5555-5555-5555-555555555555"; ValueObject = ValueObjectId (Guid.Parse "55555555-5555-5555-5555-555555555555"); Subjects = [ 3; 4; 6 ] }
94+
let b3 = { Id = 6; Value = "3000"; GuidValue = Guid.Parse "66666666-6666-6666-6666-666666666666"; ValueObject = ValueObjectId (Guid.Parse "66666666-6666-6666-6666-666666666666"); Subjects = [ 1; 3; 5 ] }
5295
let al = [ a1; a2; a3 ]
5396
let bl = [ b1; b2; b3 ]
5497
let p1 = Complex{ Id = 1; Name = "Complex 1"; Discriminator = "Complex"; Communities = [ 5 ]; Buildings = [ 3 ] }
@@ -90,6 +133,8 @@ let getExecutor (expectedFilter : ObjectListFilter voption) =
90133
fun () -> [
91134
Define.Field ("id", IntType, resolve = (fun _ a -> a.Id))
92135
Define.Field ("value", StringType, resolve = (fun _ a -> a.Value))
136+
Define.Field ("guidValue", GuidType, resolve = (fun _ a -> a.GuidValue))
137+
Define.Field ("valueObject", ValueObjectType, resolve = (fun _ a -> a.ValueObject))
93138
Define
94139
.Field(
95140
"subjects",
@@ -111,6 +156,8 @@ let getExecutor (expectedFilter : ObjectListFilter voption) =
111156
fun () -> [
112157
Define.Field ("id", IntType, resolve = (fun _ b -> b.Id))
113158
Define.Field ("value", StringType, resolve = (fun _ b -> b.Value))
159+
Define.Field ("guidValue", GuidType, resolve = (fun _ b -> b.GuidValue))
160+
Define.Field ("valueObject", ValueObjectType, resolve = (fun _ b -> b.ValueObject))
114161
Define
115162
.Field(
116163
"subjects",
@@ -1093,6 +1140,87 @@ let ``Object list filter: Must parse filter that references variables`` () =
10931140
data |> equals (upcast expected)
10941141
result.Metadata.TryFind<ObjectListFilters> ("filters") |> wantValueSome |> seqEquals [ expectedFilter ]
10951142

1143+
[<Fact>]
1144+
let ``Object list filter: Must parse inline filter variable backed by Guid scalar`` () =
1145+
let query =
1146+
parse
1147+
"""query testQuery($filter: Guid!) {
1148+
A (id : 1) {
1149+
id
1150+
value
1151+
subjects (filter : { guidValue : $filter }) { ...Value }
1152+
}
1153+
}
1154+
1155+
fragment Value on Subject {
1156+
...on A {
1157+
id
1158+
value
1159+
}
1160+
...on B {
1161+
id
1162+
value
1163+
}
1164+
}"""
1165+
let expected =
1166+
NameValueLookup.ofList [
1167+
"A",
1168+
upcast
1169+
NameValueLookup.ofList [
1170+
"id", upcast 1
1171+
"value", upcast "A1"
1172+
"subjects",
1173+
upcast
1174+
[
1175+
NameValueLookup.ofList [ "id", upcast 2; "value", upcast "A2" ]
1176+
NameValueLookup.ofList [ "id", upcast 6; "value", upcast "3000" ]
1177+
]
1178+
]
1179+
]
1180+
1181+
let guidText = "22222222-2222-2222-2222-222222222222"
1182+
let filterValue = $"\"{guidText}\"" |> JsonDocument.Parse |> _.RootElement
1183+
let variables = ImmutableDictionary<string, JsonElement>.Empty.Add ("filter", filterValue)
1184+
let filter = Equals ({ FieldName = "guidvalue"; Value = guidText }, null)
1185+
let expectedFilter : KeyValuePair<obj list, _> = kvp ([ "A"; "subjects" ]) filter
1186+
let result = executeAndVerifyFilter (query, variables, filter)
1187+
1188+
ensureDirect result <| fun data errors ->
1189+
empty errors
1190+
data |> equals (upcast expected)
1191+
result.Metadata.TryFind<ObjectListFilters> ("filters") |> wantValueSome |> seqEquals [ expectedFilter ]
1192+
1193+
[<Fact>]
1194+
let ``Object list filter: Must parse inline filter variable backed by wrapped value object`` () =
1195+
let query =
1196+
parse
1197+
"""query testQuery($valueObject: ValueObject!) {
1198+
A (id : 1) {
1199+
id
1200+
value
1201+
subjects (filter : { valueObject : $valueObject }) { ...Value }
1202+
}
1203+
}
1204+
1205+
fragment Value on Subject {
1206+
...on A {
1207+
id
1208+
value
1209+
}
1210+
...on B {
1211+
id
1212+
value
1213+
}
1214+
}"""
1215+
1216+
let valueObjectText = "22222222-2222-2222-2222-222222222222"
1217+
let valueObjectVariable = $"\"{valueObjectText}\"" |> JsonDocument.Parse |> _.RootElement
1218+
let variables = ImmutableDictionary<string, JsonElement>.Empty.Add ("valueObject", valueObjectVariable)
1219+
let result = executeWithVariables (query, variables)
1220+
1221+
ensureDirect result <| fun _ errors ->
1222+
empty errors
1223+
10961224
[<Fact>]
10971225
let ``Object list filter: Must return empty filter when all discriminated union types are specified`` () =
10981226
let query =

0 commit comments

Comments
 (0)