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 }