Skip to content

Commit 15886a1

Browse files
authored
Merge pull request #50 from alfonsogarciacaro/client-provider
WIP: Client Type Provider
2 parents 0585953 + 82db21d commit 15886a1

File tree

6 files changed

+407
-119
lines changed

6 files changed

+407
-119
lines changed

FSharp.Data.GraphQL.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{DFA5AAFF
7373
EndProject
7474
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Data.GraphQL.Client", "src\FSharp.Data.GraphQL.Client\FSharp.Data.GraphQL.Client.fsproj", "{7C9CC625-FB4E-4215-9F7C-494AA6043821}"
7575
EndProject
76+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "client-provider", "client-provider", "{3D948D55-3CD2-496D-A04C-3B4E7BB69140}"
77+
ProjectSection(SolutionItems) = preProject
78+
samples\client-provider\query.fsx = samples\client-provider\query.fsx
79+
EndProjectSection
80+
EndProject
7681
Global
7782
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7883
Debug|Any CPU = Debug|Any CPU
@@ -134,5 +139,6 @@ Global
134139
{9B25360F-2CE4-43D2-AFF0-5EAA693E98F7} = {B0C25450-74BF-40C2-9E02-09AADBAE2C2F}
135140
{E7139F5F-22CA-4392-8C1C-481A39EEB554} = {9B25360F-2CE4-43D2-AFF0-5EAA693E98F7}
136141
{DFA5AAFF-31B8-4203-822C-8ACC4D7A7D74} = {9B25360F-2CE4-43D2-AFF0-5EAA693E98F7}
142+
{3D948D55-3CD2-496D-A04C-3B4E7BB69140} = {B0C25450-74BF-40C2-9E02-09AADBAE2C2F}
137143
EndGlobalSection
138144
EndGlobal

samples/client-provider/query.fsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#r "../../src/FSharp.Data.GraphQL.Client/bin/Debug/FSharp.Data.GraphQL.Client.dll"
2+
3+
open FSharp.Data.GraphQL
4+
open System.Collections.Generic
5+
6+
let [<Literal>] serverUrl = "http://localhost:8083"
7+
8+
// The name and arguments of the query will be automatically set by the type provider
9+
let [<Literal>] queryFields = "{ id, name, appearsIn, friends { name } }"
10+
let [<Literal>] queryFieldsWithFragments = "{ ...data, friends { name } } fragment data on Human { id, name, appearsIn }"
11+
12+
type MyClient = GraphQLProvider<serverUrl>
13+
14+
let hero =
15+
MyClient.Queries.Hero<queryFieldsWithFragments>("1000")
16+
|> Async.RunSynchronously
17+
18+
let droid =
19+
MyClient.Queries.Droid<queryFields>("2000")
20+
|> Async.RunSynchronously
21+
22+
// Result is an option type
23+
match hero with
24+
| None -> ()
25+
| Some hero ->
26+
printfn "My hero is %A" hero.name
27+
printfn "Appears in %O: %b" MyClient.Types.Episode.Empire
28+
(hero.appearsIn |> Array.exists ((=) MyClient.Types.Episode.Empire))
29+
printfn "My hero's friends are:"
30+
hero.friends
31+
|> Array.choose (fun x -> x.name)
32+
|> Array.iter (printfn "- %s")
33+
34+
let freeQuery = "{ hero(id: \"1000\"){ id, name, appearsIn, friends { name } } }"
35+
36+
let hero2 =
37+
MyClient.Query(freeQuery)
38+
|> Async.Catch
39+
|> Async.RunSynchronously
40+
|> function
41+
| Choice1Of2 data -> (data :?> IDictionary<string,obj>).["hero"] :?> MyClient.Types.Human |> Some
42+
| Choice2Of2 err -> printfn "ERROR: %s" err.Message; None
43+
44+
printfn "%A" hero2.Value.name
45+
46+
let [<Literal>] queryFields2 = "{ id, name"
47+
// This code won't compile as the query is not properly formed
48+
//MyClient.QueryHero<queryFields2>("1000")
49+

samples/graphiql-client/server.fsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,17 @@ open FSharp.Data.GraphQL
8484
open FSharp.Data.GraphQL.Types
8585
open FSharp.Data.GraphQL.Execution
8686

87-
let EpisodeType = Define.Enum(
87+
let EpisodeType =
88+
Define.Enum(
8889
name = "Episode",
8990
description = "One of the films in the Star Wars Trilogy",
9091
options = [
91-
Define.EnumValue("NEWHOPE", Episode.NewHope, "Released in 1977.")
92-
Define.EnumValue("EMPIRE", Episode.Empire, "Released in 1980.")
93-
Define.EnumValue("JEDI", Episode.Jedi, "Released in 1983.") ])
92+
Define.EnumValue("NewHope", Episode.NewHope, "Released in 1977.")
93+
Define.EnumValue("Empire", Episode.Empire, "Released in 1980.")
94+
Define.EnumValue("Jedi", Episode.Jedi, "Released in 1983.") ])
9495

95-
let rec CharacterType = Define.Union(
96+
let rec CharacterType =
97+
Define.Union(
9698
name = "Character",
9799
description = "A character in the Star Wars Trilogy",
98100
options = [ HumanType; DroidType ],
@@ -105,7 +107,8 @@ let rec CharacterType = Define.Union(
105107
| Human _ -> upcast HumanType
106108
| Droid _ -> upcast DroidType))
107109

108-
and HumanType : ObjectDef<Human> = Define.Object<Human>(
110+
and HumanType : ObjectDef<Human> =
111+
Define.Object<Human>(
109112
name = "Human",
110113
description = "A humanoid creature in the Star Wars universe.",
111114
isTypeOf = (fun o -> o :? Human),
@@ -120,7 +123,8 @@ and HumanType : ObjectDef<Human> = Define.Object<Human>(
120123
Define.Field("appearsIn", ListOf EpisodeType, "Which movies they appear in.", fun _ h -> upcast h.AppearsIn)
121124
Define.Field("homePlanet", Nullable String, "The home planet of the human, or null if unknown.", fun _ h -> h.HomePlanet) ])
122125

123-
and DroidType = Define.Object<Droid>(
126+
and DroidType =
127+
Define.Object<Droid>(
124128
name = "Droid",
125129
description = "A mechanical creature in the Star Wars universe.",
126130
isTypeOf = (fun o -> o :? Droid),
@@ -132,11 +136,12 @@ and DroidType = Define.Object<Droid>(
132136
Define.Field("appearsIn", ListOf EpisodeType, "Which movies they appear in.", fun _ d -> upcast d.AppearsIn)
133137
Define.Field("primaryFunction", Nullable String, "The primary function of the droid.", fun _ d -> d.PrimaryFunction) ])
134138

135-
let Query = Define.Object(
139+
let Query =
140+
Define.Object(
136141
name = "Query",
137142
fields = [
138-
Define.Field("hero", Nullable HumanType, "Gets human hero", [ Define.Input("id", String) ], fun ctx () -> getHuman (ctx.Arg("id").Value))
139-
Define.Field("droid", Nullable DroidType, "Gets droid", [ Define.Input("id", String) ], fun ctx () -> getDroid (ctx.Arg("id").Value)) ])
143+
Define.Field("hero", Nullable HumanType, "Gets human hero", [ Define.Input("id", String) ], fun ctx () -> getHuman (ctx.Arg("id")))
144+
Define.Field("droid", Nullable DroidType, "Gets droid", [ Define.Input("id", String) ], fun ctx () -> getDroid (ctx.Arg("id"))) ])
140145

141146
let schema = Schema(Query)
142147

src/FSharp.Data.GraphQL.Client/FSharp.Data.GraphQL.Client.fsproj

100644100755
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020
<Optimize>false</Optimize>
2121
<Tailcalls>false</Tailcalls>
2222
<OutputPath>bin\Debug\</OutputPath>
23-
<DefineConstants>DEBUG;TRACE</DefineConstants>
23+
<DefineConstants>TRACE;DEBUG;NO_GENERATIVE</DefineConstants>
2424
<WarningLevel>3</WarningLevel>
2525
<DocumentationFile>bin\Debug\FSharp.Data.GraphQL.Client.XML</DocumentationFile>
26+
<StartAction>Program</StartAction>
27+
<StartProgram>C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\devenv.exe</StartProgram>
28+
<StartArguments>"../../../../samples/client-provider/query.fsx"</StartArguments>
2629
</PropertyGroup>
2730
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
2831
<DebugType>pdbonly</DebugType>

src/FSharp.Data.GraphQL.Client/GraphQlProvider.fs

100644100755
Lines changed: 186 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,32 @@ open Microsoft.FSharp.Core.CompilerServices
1111

1212
open FSharp.Data.GraphQL.Types.Introspection
1313
open TypeCompiler
14+
open System.Collections.Generic
15+
open Newtonsoft.Json.Linq
16+
open Newtonsoft.Json
17+
open Microsoft.FSharp.Quotations
18+
open QuotationHelpers
1419

15-
type internal ProviderSchemaConfig =
16-
{ Namespace: string
17-
DefinedTypes: Map<string, ProvidedTypeDefinition option> }
20+
module Util =
21+
open System.Text.RegularExpressions
22+
open FSharp.Data.GraphQL
1823

19-
//[<TypeProvider>]
20-
type GraphQlProvider (config : TypeProviderConfig) as this =
21-
inherit TypeProviderForNamespaces ()
22-
23-
let asm = System.Reflection.Assembly.GetExecutingAssembly()
24+
let getOrFail (err: string) = function
25+
| Some v -> v
26+
| None -> failwith err
27+
28+
let tryOrFail (err: string) (f: unit->'T) =
29+
try f()
30+
with ex -> Exception(err, ex) |> raise
31+
32+
let firstToUpper (str: string) =
33+
if str <> null && str.Length > 0
34+
then str.[0].ToString().ToUpper() + str.Substring(1)
35+
else str
2436

2537
let requestSchema (url: string) =
2638
async {
27-
let requestUrl = Uri(Uri(url), ("/?query=" + FSharp.Data.GraphQL.Introspection.introspectionQuery), false)
39+
let requestUrl = Uri(Uri(url), ("/?query=" + FSharp.Data.GraphQL.Introspection.introspectionQuery))
2840
let req = WebRequest.CreateHttp(requestUrl)
2941
req.Method <- "GET"
3042
use! resp = req.GetResponseAsync() |> Async.AwaitTask
@@ -40,37 +52,181 @@ type GraphQlProvider (config : TypeProviderConfig) as this =
4052
return Choice2Of2 errors
4153
}
4254

43-
let compileTypesFromSchema ns (schema: IntrospectionSchema) =
55+
let compileTypesFromSchema asm ns (schema: IntrospectionSchema) =
56+
let underlyingType (t: TypeReference) =
57+
t.UnderlyingType
4458
let ctx = {
4559
Assembly = asm
4660
Namespace = ns
4761
KnownTypes = ProviderSessionContext.CoreTypes }
4862
let typeDefinitions =
49-
schema.Types
50-
|> Array.fold (fun acc t -> Map.add t.Name (ProvidedType (initType ctx t, t)) acc) ctx.KnownTypes
63+
(ctx.KnownTypes, schema.Types)
64+
||> Array.fold (fun acc t ->
65+
if acc.ContainsKey t.Name
66+
then acc
67+
else Map.add t.Name (ProvidedType (initType ctx t, t)) acc)
5168
let defctx = { ctx with KnownTypes = typeDefinitions }
5269
typeDefinitions
53-
|> Map.iter (fun k t ->
54-
match t with
55-
| NativeType _ -> ()
56-
| ProvidedType (t, itype) -> genType defctx itype t)
70+
|> Seq.iter (fun kv ->
71+
match kv.Value with
72+
| NativeType t -> ()
73+
| ProvidedType (t, itype) ->
74+
genType defctx itype t)
5775
typeDefinitions
58-
|> Map.toSeq
59-
|> Seq.map snd
60-
|> Seq.choose (
61-
function
62-
| NativeType _ -> None
63-
| ProvidedType (t, _) -> Some t)
64-
|> Seq.toList
6576

66-
do
67-
let choice = requestSchema("http://localhost:8083") |> Async.RunSynchronously
68-
match choice with
69-
| Choice1Of2 schema ->
70-
let types = compileTypesFromSchema "GraphQLNamespace" schema
71-
this.AddNamespace("GraphQLNamespace", types)
72-
| Choice2Of2 ex -> ()
77+
let rec jsonToObject (token: JToken) =
78+
match token.Type with
79+
| JTokenType.Object ->
80+
token.Children<JProperty>()
81+
|> Seq.map (fun prop -> prop.Name, jsonToObject prop.Value)
82+
|> dict :> obj
83+
| JTokenType.Array ->
84+
token |> Seq.map jsonToObject |> Seq.toArray :> obj
85+
| _ ->
86+
(token :?> JValue).Value
87+
88+
let launchQuery (serverUrl: string) (queryName: string) (cont: obj->'T) (query: string) =
89+
async {
90+
use client = new WebClient()
91+
let queryJson = Map["query", query] |> JsonConvert.SerializeObject
92+
let! json = client.UploadStringTaskAsync(Uri(serverUrl), queryJson) |> Async.AwaitTask
93+
let res = JToken.Parse json |> jsonToObject :?> IDictionary<string,obj>
94+
if res.ContainsKey("errors") then
95+
res.["errors"] :?> obj[] |> Seq.map string |> String.concat "\n" |> failwith
96+
let data =
97+
// Options are problematic within quotations so we just use null here
98+
if queryName <> null
99+
then (res.["data"] :?> IDictionary<string,obj>).[queryName]
100+
else res.["data"]
101+
return cont(data)
102+
}
73103

104+
let buildQuery (queryName: string) (queryFields: string)
105+
(argNames: string[]) (argValues: obj[]) =
106+
let queryFields, queryFragments =
107+
let mutable i = 0
108+
let mutable openBraces = 0
109+
let mutable closeBraces = 0
110+
while closeBraces = 0 || closeBraces < openBraces do
111+
match queryFields.Chars(i) with
112+
| '{' -> openBraces <- openBraces + 1
113+
| '}' -> closeBraces <- closeBraces + 1
114+
| _ -> ()
115+
i <- i + 1
116+
queryFields.Substring(0, i), queryFields.Substring(i)
117+
Seq.zip argNames argValues
118+
|> Seq.map (fun (k,v) -> sprintf "%s: %s" k (JsonConvert.SerializeObject v))
119+
|> String.concat ", "
120+
|> fun args -> sprintf "{ %s(%s) %s }%s" queryName args queryFields queryFragments
121+
122+
let createMethod (tdef: ProvidedTypeDefinition) (schemaTypes: Map<string,TypeReference>)
123+
(serverUrl: string) (query: IntrospectionField) =
124+
let findType (t: IntrospectionTypeRef) =
125+
TypeReference.findType t schemaTypes
126+
let makeExprArray (exprs: Expr list) =
127+
Expr.NewArray(typeof<obj>, exprs |> List.map (fun e -> Expr.Coerce(e, typeof<obj>)))
128+
let resType = findType query.Type
129+
let asyncType = typedefof<Async<obj>>.MakeGenericType(resType)
130+
let args =
131+
query.Args
132+
|> Seq.map (fun x -> ProvidedParameter(x.Name, findType x.Type))
133+
|> Seq.toList
134+
let m = ProvidedMethod(firstToUpper query.Name, args, asyncType, IsStaticMethod=true)
135+
let sargs = [ProvidedStaticParameter("query", typeof<string>)]
136+
m.DefineStaticParameters(sargs, fun methName sargValues ->
137+
match sargValues with
138+
| [| :? string as queryFields |] ->
139+
// This will fail if the query is not well formed
140+
do Parser.parse queryFields |> ignore
141+
let queryName = query.Name
142+
let argNames = args |> Seq.map (fun x -> x.Name) |> Seq.toArray
143+
let m2 = ProvidedMethod(methName, args, asyncType, IsStaticMethod = true)
144+
m2.InvokeCode <-
145+
if resType.Name = "FSharpOption`1" then
146+
fun argValues ->
147+
<@@
148+
(%%makeExprArray argValues: obj[])
149+
|> buildQuery queryName queryFields argNames
150+
|> launchQuery serverUrl queryName Option.ofObj
151+
@@>
152+
else
153+
fun argValues ->
154+
<@@
155+
(%%makeExprArray argValues: obj[])
156+
|> buildQuery queryName queryFields argNames
157+
|> launchQuery serverUrl queryName id
158+
@@>
159+
tdef.AddMember m2
160+
m2
161+
| _ -> failwith "unexpected parameter values")
162+
m.InvokeCode <- fun _ -> <@@ null @@> // Dummy code
163+
m
164+
165+
let createMethods (tdef: ProvidedTypeDefinition) (serverUrl: string)
166+
(schema: IntrospectionSchema) (schemaTypes: Map<string,TypeReference>)
167+
(opType: IntrospectionTypeRef option) (wrapperName: string) =
168+
match opType with
169+
| Some op when op.Name.IsSome ->
170+
let opName = op.Name.Value
171+
let wrapper = ProvidedTypeDefinition(wrapperName, Some typeof<obj>)
172+
schema.Types
173+
|> Seq.collect (fun t ->
174+
if t.Name = opName then defaultArg t.Fields [||] else [||])
175+
|> Seq.map (createMethod wrapper schemaTypes serverUrl)
176+
|> Seq.toList
177+
|> function
178+
| [] -> ()
179+
| ops ->
180+
wrapper.AddMembers ops
181+
tdef.AddMember wrapper
182+
| _ -> ()
183+
184+
type internal ProviderSchemaConfig =
185+
{ Namespace: string
186+
DefinedTypes: Map<string, ProvidedTypeDefinition option> }
187+
188+
[<TypeProvider>]
189+
type GraphQlProvider (config : TypeProviderConfig) as this =
190+
inherit TypeProviderForNamespaces ()
191+
192+
let asm = System.Reflection.Assembly.GetExecutingAssembly()
193+
194+
do
195+
let ns = "FSharp.Data.GraphQL"
196+
let generator = ProvidedTypeDefinition(asm, ns, "GraphQLProvider", Some typeof<obj>)
197+
generator.DefineStaticParameters([ProvidedStaticParameter("url", typeof<string>)], fun typeName parameterValues ->
198+
match parameterValues with
199+
| [| :? string as serverUrl|] ->
200+
let choice = Util.requestSchema(serverUrl) |> Async.RunSynchronously
201+
match choice with
202+
| Choice1Of2 schema ->
203+
let tdef = ProvidedTypeDefinition(asm, ns, typeName, Some typeof<obj>)
204+
let schemaTypes =
205+
Util.compileTypesFromSchema asm "GraphQLTypes" schema
206+
// Inner types
207+
let typesWrapper = ProvidedTypeDefinition("Types", Some typeof<obj>)
208+
schemaTypes
209+
|> Seq.choose (fun kv ->
210+
match kv.Value with
211+
| ProvidedType(t,_) -> Some t
212+
| NativeType _ -> None)
213+
|> Seq.toList
214+
|> typesWrapper.AddMembers
215+
tdef.AddMember typesWrapper
216+
// Static methods
217+
Util.createMethods tdef serverUrl schema schemaTypes (Some schema.QueryType) "Queries"
218+
Util.createMethods tdef serverUrl schema schemaTypes schema.MutationType "Mutations"
219+
Util.createMethods tdef serverUrl schema schemaTypes schema.SubscriptionType "Subscriptions"
220+
// Generic query method
221+
let m = ProvidedMethod("Query", [ProvidedParameter("query", typeof<string>)], typeof<Async<obj>>)
222+
m.IsStaticMethod <- true
223+
m.InvokeCode <- fun argValues ->
224+
<@@ Util.launchQuery serverUrl null id (%%argValues.[0]: string) @@>
225+
tdef.AddMember m
226+
tdef
227+
| Choice2Of2 ex -> String.concat "\n" ex |> failwithf "%s"
228+
| _ -> failwith "unexpected parameter values")
229+
this.AddNamespace(ns, [generator])
74230

75231
[<assembly:TypeProviderAssembly>]
76232
do ()

0 commit comments

Comments
 (0)