1 // Written in D programming language
2 /**
3 *   This module defines rest api to communicate with rpc server.
4 *
5 *   Copyright: © 2014 DSoftOut
6 *   License: Subject to the terms of the MIT license, as written in the included LICENSE file.
7 *   Authors: NCrashed <ncrashed@gmail.com>
8 */
9 module client.rpcapi;
10 
11 import vibe.data.json;
12 import vibe.data.serialization;
13 import std.array;
14 import std.random;
15 import std.conv;
16 import std.typecons;
17 import std.traits;
18 
19 interface IRpcApi
20 {
21     Json rpc(string jsonrpc, string method, Json[] params, uint id);
22     Json rpc(string jsonrpc, string method, Json[] params, uint id, string[string] auth);
23     
24     final RpcRespond runRpc(string method, T...)(T params)
25     	if(T.length == 0 || !isAssociativeArray!(T[0]))
26     {
27         auto builder = appender!(Json[]);
28         foreach(param; params)
29             builder.put(param.serializeToJson);
30         
31         return new RpcRespond(rpc("2.0", method, builder.data, uniform(uint.min, uint.max)));    
32     }
33     
34     final RpcRespond runRpc(string method)(string[] params)
35     {
36         auto builder = appender!(Json[]);
37         foreach(param; params)
38             builder.put(param.serializeToJson);
39         
40         return new RpcRespond(rpc("2.0", method, builder.data, uniform(uint.min, uint.max)));    
41     }
42     
43     final RpcRespond runRpc(string method, T...)(T[string] params)
44     {
45         auto builder = appender!(Json[]);
46         foreach(param; params)
47             builder.put(param.serializeToJson);
48         
49         return new RpcRespond(rpc("2.0", method, builder.data, uniform(uint.min, uint.max)));    
50     }
51     
52     final RpcRespond runRpc(string method, T...)(string[string] auth, T params)
53     {
54     	auto builderParams = appender!(Json[]);
55         foreach(param; params)
56             builderParams.put(param.serializeToJson);
57     	
58     	return new RpcRespond(rpc("2.0", method, builderParams.data, uniform(uint.min, uint.max), auth));
59     }
60 }
61 
62 struct RpcError
63 {
64     int code;
65     string message;
66 }
67 
68 /**
69 *   Declares template for holding compile time info
70 *   about column format: name and element type.
71 *
72 *   First column parameter is an element type, second
73 *   parameter is column name in response.
74 *
75 *   Example:
76 *   ---------
77 *   alias col = Column!(uint, "column_name");
78 *   static assert(is(col.type == uint));
79 *   static assert(col.name == "column_name");
80 *   ---------
81 */
82 template Column(T...)
83 {
84     static assert(T.length >= 2);
85     
86     alias T[0] type;
87     enum name = T[1];
88 }
89 /**
90 *   Checks is $(B U) actually
91 *   equal $(B Column) semantic, i.e. holding
92 *   type and name.
93 */
94 template isColumn(US...)
95 {
96     static if(US.length > 0)
97     {
98         alias US[0] U;
99         
100         enum isColumn = __traits(compiles, U.name) && is(typeof(U.name) == string) &&
101             __traits(compiles, U.type) && is(U.type);
102     } else
103     {
104         enum isColumn = false;
105     }
106 }
107 unittest
108 {
109    alias col = Column!(uint, "column_name");
110    static assert(is(col.type == uint));
111    static assert(col.name == "column_name");
112    static assert(isColumn!col);
113    static assert(!isColumn!string);
114 }
115 
116 /**
117 *   Structure represents normal response from RPC server
118 *   with desired columns. Columns element type and name
119 *   is specified by $(B Column) template.
120 */
121 struct RpcOk(Cols...)
122 {
123     static assert(checkTypes!Cols, "RpcOk compile arguments have to be of type kind: Column!(ColumnType, string ColumnName)");
124     
125     mixin(genColFields!Cols());
126     
127     this(Json result)
128     {
129         template column(alias T) { enum column = "columns[\""~T.name~"\"]"; }
130         
131         auto columns = result.get!(Json[string]);
132         foreach(ColInfo; Cols)
133         {
134             static if(is(ColInfo.type T : Nullable!T))
135             {
136                 mixin(ColInfo.name) = [];
137                 
138                 void readJson(Json json)
139                 {
140                     if(json.type == Json.Type.null_)
141                     {
142                         mixin(ColInfo.name) ~= Nullable!T();
143                     }
144                     else
145                     {
146                         mixin(ColInfo.name) ~= Nullable!T(mixin(column!ColInfo).deserializeJson!T);
147                     } 
148                 }
149                 
150                 if(mixin(column!ColInfo).type == Json.Type.array)
151                 {
152                     foreach(json; mixin(column!ColInfo).get!(Json[]))
153                     {
154                         readJson(json);              
155                     }
156                 } else
157                 {
158                     readJson(mixin(column!ColInfo));     
159                 }
160             }
161             else
162             {
163                 assert(ColInfo.name in columns, text("Cannot find column '", ColInfo.name, "' in response ", result));
164                 if(mixin(column!ColInfo).type == Json.Type.array)
165                 {
166                     mixin(ColInfo.name) = mixin(column!ColInfo).deserializeJson!(ColInfo.type[]);
167                 } else
168                 {
169                     mixin(ColInfo.name) = [mixin(column!ColInfo).deserializeJson!(ColInfo.type)];
170                 }
171             }
172         }
173     }
174     
175     // private generation 
176     private static string colField(U...)()
177     {
178         return U[0].type.stringof ~ "[] "~U[0].name~";";
179     }
180     
181     private static string genColFields(U...)()
182     {
183         string res;
184         foreach(ColInfo; U)
185         {
186             res ~= colField!(ColInfo)()~"\n";
187         }
188         return res;
189     }
190     
191     private template checkTypes(U...)
192     {
193         static if(U.length == 0)
194             enum checkTypes = true;
195         else
196             enum checkTypes = isColumn!(U[0]) && checkTypes!(U[1..$]); 
197     }
198 }
199 
200 class RpcRespond
201 {
202     this(Json respond)
203     {
204         this.respond = respond;
205     }
206     
207     RpcError assertError()
208     {
209         scope(failure)
210         {
211             assert(false, text("Expected respond with error! But got: ", respond));
212         }
213         
214         return RpcError(respond.error.code.get!int, respond.error.message.get!string);
215     }
216     
217     RpcOk!RowTypes assertOk(RowTypes...)(size_t i = 0)
218     {
219         try
220         {
221             assert(respond.result.type != Json.Type.undefined);
222         
223             if(respond.result.type == Json.Type.array)
224             {
225                 auto jsons = respond.result.get!(Json[]);
226                 assert(i < jsons.length);
227                 return RpcOk!RowTypes(jsons[i]);
228             } 
229             else if(respond.result.type == Json.Type.object)
230             {
231                 assert(i == 0, "Single query result, but i != 0");
232                 return RpcOk!RowTypes(respond.result);
233             }
234             else
235             {
236                 throw new Exception("Unknown format of result from server!");
237             }
238         } 
239         catch(Throwable th)
240         {
241             assert(false, text("Expected successful respond! But got: ", respond, "\n", th.msg));
242         }
243     }
244     
245     Json raw()
246     {
247         return respond;
248     }
249     
250     private Json respond;
251 }