1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 module thrift.codegen.client;
20
21 import std.algorithm : find;
22 import std.array : empty, front;
23 import std.conv : to;
24 import std.traits : isSomeFunction, ParameterStorageClass,
25 ParameterStorageClassTuple, ParameterTypeTuple, ReturnType;
26 import thrift.codegen.base;
27 import thrift.internal.codegen;
28 import thrift.internal.ctfe;
29 import thrift.protocol.base;
30
31 /**
32 * Thrift service client, which implements an interface by synchronously
33 * calling a server over a TProtocol.
34 *
35 * TClientBase simply extends Interface with generic input/output protocol
36 * properties to serve as a supertype for all TClients for the same service,
37 * which might be instantiated with different concrete protocol types (there
38 * is no covariance for template type parameters). If Interface is derived
39 * from another interface BaseInterface, it also extends
40 * TClientBase!BaseInterface.
41 *
42 * TClient is the class that actually implements TClientBase. Just as
43 * TClientBase, it is also derived from TClient!BaseInterface for inheriting
44 * services.
45 *
46 * TClient takes two optional template arguments which can be used for
47 * specifying the actual TProtocol implementation used for optimization
48 * purposes, as virtual calls can completely be eliminated then. If
49 * OutputProtocol is not specified, it is assumed to be the same as
50 * InputProtocol. The protocol properties defined by TClientBase are exposed
51 * with their concrete type (return type covariance).
52 *
53 * In addition to implementing TClientBase!Interface, TClient offers the
54 * following constructors:
55 * ---
56 * this(InputProtocol iprot, OutputProtocol oprot);
57 * // Only if is(InputProtocol == OutputProtocol), to use the same protocol
58 * // for both input and output:
59 * this(InputProtocol prot);
60 * ---
61 *
62 * The sequence id of the method calls starts at zero and is automatically
63 * incremented.
64 */
65 interface TClientBase(Interface) if (isBaseService!Interface) : Interface {
66 /**
67 * The input protocol used by the client.
68 */
69 TProtocol inputProtocol() @property;
70
71 /**
72 * The output protocol used by the client.
73 */
74 TProtocol outputProtocol() @property;
75 }
76
77 /// Ditto
78 interface TClientBase(Interface) if (isDerivedService!Interface) :
79 TClientBase!(BaseService!Interface), Interface {}
80
81 /// Ditto
82 template TClient(Interface, InputProtocol = TProtocol, OutputProtocol = void) if (
83 isService!Interface && isTProtocol!InputProtocol &&
84 (isTProtocol!OutputProtocol || is(OutputProtocol == void))
85 ) {
86 mixin({
87 static if (isDerivedService!Interface) {
88 string code = "class TClient : TClient!(BaseService!Interface, " ~
89 "InputProtocol, OutputProtocol), TClientBase!Interface {\n";
90 code ~= q{
91 this(IProt iprot, OProt oprot) {
92 super(iprot, oprot);
93 }
94
95 static if (is(IProt == OProt)) {
96 this(IProt prot) {
97 super(prot);
98 }
99 }
100
101 // DMD @@BUG@@: If these are not present in this class (would be)
102 // inherited anyway, »not implemented« errors are raised.
103 override IProt inputProtocol() @property {
104 return super.inputProtocol;
105 }
106 override OProt outputProtocol() @property {
107 return super.outputProtocol;
108 }
109 };
110 } else {
111 string code = "class TClient : TClientBase!Interface {";
112 code ~= q{
113 alias InputProtocol IProt;
114 static if (isTProtocol!OutputProtocol) {
115 alias OutputProtocol OProt;
116 } else {
117 static assert(is(OutputProtocol == void));
118 alias InputProtocol OProt;
119 }
120
121 this(IProt iprot, OProt oprot) {
122 iprot_ = iprot;
123 oprot_ = oprot;
124 }
125
126 static if (is(IProt == OProt)) {
127 this(IProt prot) {
128 this(prot, prot);
129 }
130 }
131
132 IProt inputProtocol() @property {
133 return iprot_;
134 }
135
136 OProt outputProtocol() @property {
137 return oprot_;
138 }
139
140 protected IProt iprot_;
141 protected OProt oprot_;
142 protected int seqid_;
143 };
144 }
145
146 foreach (methodName; __traits(derivedMembers, Interface)) {
147 static if (isSomeFunction!(mixin("Interface." ~ methodName))) {
148 bool methodMetaFound;
149 TMethodMeta methodMeta;
150 static if (is(typeof(Interface.methodMeta) : TMethodMeta[])) {
151 enum meta = find!`a.name == b`(Interface.methodMeta, methodName);
152 if (!meta.empty) {
153 methodMetaFound = true;
154 methodMeta = meta.front;
155 }
156 }
157
158 // Generate the code for sending.
159 string[] paramList;
160 string paramAssignCode;
161 foreach (i, _; ParameterTypeTuple!(mixin("Interface." ~ methodName))) {
162 // Use the param name speficied in the meta information if any –
163 // just cosmetics in this case.
164 string paramName;
165 if (methodMetaFound && i < methodMeta.params.length) {
166 paramName = methodMeta.params[i].name;
167 } else {
168 paramName = "param" ~ to!string(i + 1);
169 }
170
171 immutable storage = ParameterStorageClassTuple!(
172 mixin("Interface." ~ methodName))[i];
173 paramList ~= ((storage & ParameterStorageClass.ref_) ? "ref " : "") ~
174 "ParameterTypeTuple!(Interface." ~ methodName ~ ")[" ~
175 to!string(i) ~ "] " ~ paramName;
176 paramAssignCode ~= "args." ~ paramName ~ " = &" ~ paramName ~ ";\n";
177 }
178 code ~= "ReturnType!(Interface." ~ methodName ~ ") " ~ methodName ~
179 "(" ~ ctfeJoin(paramList) ~ ") {\n";
180
181 code ~= "immutable methodName = `" ~ methodName ~ "`;\n";
182
183 immutable paramStructType =
184 "TPargsStruct!(Interface, `" ~ methodName ~ "`)";
185 code ~= paramStructType ~ " args = " ~ paramStructType ~ "();\n";
186 code ~= paramAssignCode;
187 code ~= "oprot_.writeMessageBegin(TMessage(`" ~ methodName ~ "`, ";
188 code ~= ((methodMetaFound && methodMeta.type == TMethodType.ONEWAY)
189 ? "TMessageType.ONEWAY" : "TMessageType.CALL");
190 code ~= ", ++seqid_));\n";
191 code ~= "args.write(oprot_);\n";
192 code ~= "oprot_.writeMessageEnd();\n";
193 code ~= "oprot_.transport.flush();\n";
194
195 // If this is not a oneway method, generate the receiving code.
196 if (!methodMetaFound || methodMeta.type != TMethodType.ONEWAY) {
197 code ~= "TPresultStruct!(Interface, `" ~ methodName ~ "`) result;\n";
198
199 if (!is(ReturnType!(mixin("Interface." ~ methodName)) == void)) {
200 code ~= "ReturnType!(Interface." ~ methodName ~ ") _return;\n";
201 code ~= "result.success = &_return;\n";
202 }
203
204 // TODO: The C++ implementation checks for matching method name here,
205 // should we do as well?
206 code ~= q{
207 auto msg = iprot_.readMessageBegin();
208 scope (exit) {
209 iprot_.readMessageEnd();
210 iprot_.transport.readEnd();
211 }
212
213 if (msg.type == TMessageType.EXCEPTION) {
214 auto x = new TApplicationException(null);
215 x.read(iprot_);
216 iprot_.transport.readEnd();
217 throw x;
218 }
219 if (msg.type != TMessageType.REPLY) {
220 skip(iprot_, TType.STRUCT);
221 iprot_.transport.readEnd();
222 }
223 if (msg.seqid != seqid_) {
224 throw new TApplicationException(
225 methodName ~ " failed: Out of sequence response.",
226 TApplicationException.Type.BAD_SEQUENCE_ID
227 );
228 }
229 result.read(iprot_);
230 };
231
232 if (methodMetaFound) {
233 foreach (e; methodMeta.exceptions) {
234 code ~= "if (result.isSet!`" ~ e.name ~ "`) throw result." ~
235 e.name ~ ";\n";
236 }
237 }
238
239 if (!is(ReturnType!(mixin("Interface." ~ methodName)) == void)) {
240 code ~= q{
241 if (result.isSet!`success`) return _return;
242 throw new TApplicationException(
243 methodName ~ " failed: Unknown result.",
244 TApplicationException.Type.MISSING_RESULT
245 );
246 };
247 }
248 }
249 code ~= "}\n";
250 }
251 }
252
253 code ~= "}\n";
254 return code;
255 }());
256 }
257
258 /**
259 * TClient construction helper to avoid having to explicitly specify
260 * the protocol types, i.e. to allow the constructor being called using IFTI
261 * (see $(DMDBUG 6082, D Bugzilla enhancement requet 6082)).
262 */
263 TClient!(Interface, Prot) tClient(Interface, Prot)(Prot prot) if (
264 isService!Interface && isTProtocol!Prot
265 ) {
266 return new TClient!(Interface, Prot)(prot);
267 }
268
269 /// Ditto
270 TClient!(Interface, IProt, Oprot) tClient(Interface, IProt, OProt)
271 (IProt iprot, OProt oprot) if (
272 isService!Interface && isTProtocol!IProt && isTProtocol!OProt
273 ) {
274 return new TClient!(Interface, IProt, OProt)(iprot, oprot);
275 }
276
277 /**
278 * Represents the arguments of a Thrift method call, as pointers to the (const)
279 * parameter type to avoid copying.
280 *
281 * There should usually be no reason to use this struct directly without the
282 * help of TClient, but it is documented publicly to help debugging in case
283 * of CTFE errors.
284 *
285 * Consider this example:
286 * ---
287 * interface Foo {
288 * int bar(string a, bool b);
289 *
290 * enum methodMeta = [
291 * TMethodMeta("bar", [TParamMeta("a", 1), TParamMeta("b", 2)])
292 * ];
293 * }
294 *
295 * alias TPargsStruct!(Foo, "bar") FooBarPargs;
296 * ---
297 *
298 * The definition of FooBarPargs is equivalent to (ignoring the necessary
299 * metadata to assign the field IDs):
300 * ---
301 * struct FooBarPargs {
302 * const(string)* a;
303 * const(bool)* b;
304 *
305 * void write(Protocol)(Protocol proto) const if (isTProtocol!Protocol);
306 * }
307 * ---
308 */
TPargsStruct(Interface,string methodName)309 template TPargsStruct(Interface, string methodName) {
310 static assert(is(typeof(mixin("Interface." ~ methodName))),
311 "Could not find method '" ~ methodName ~ "' in '" ~ Interface.stringof ~ "'.");
312 mixin({
313 bool methodMetaFound;
314 TMethodMeta methodMeta;
315 static if (is(typeof(Interface.methodMeta) : TMethodMeta[])) {
316 auto meta = find!`a.name == b`(Interface.methodMeta, methodName);
317 if (!meta.empty) {
318 methodMetaFound = true;
319 methodMeta = meta.front;
320 }
321 }
322
323 string memberCode;
324 string[] fieldMetaCodes;
325 foreach (i, _; ParameterTypeTuple!(mixin("Interface." ~ methodName))) {
326 // If we have no meta information, just use param1, param2, etc. as
327 // field names, it shouldn't really matter anyway. 1-based »indexing«
328 // is used to match the common scheme in the Thrift world.
329 string memberId;
330 string memberName;
331 if (methodMetaFound && i < methodMeta.params.length) {
332 memberId = to!string(methodMeta.params[i].id);
333 memberName = methodMeta.params[i].name;
334 } else {
335 memberId = to!string(i + 1);
336 memberName = "param" ~ to!string(i + 1);
337 }
338
339 // Workaround for DMD @@BUG@@ 6056: make an intermediary alias for the
340 // parameter type, and declare the member using const(memberNameType)*.
341 memberCode ~= "alias ParameterTypeTuple!(Interface." ~ methodName ~
342 ")[" ~ to!string(i) ~ "] " ~ memberName ~ "Type;\n";
343 memberCode ~= "const(" ~ memberName ~ "Type)* " ~ memberName ~ ";\n";
344
345 fieldMetaCodes ~= "TFieldMeta(`" ~ memberName ~ "`, " ~ memberId ~
346 ", TReq.OPT_IN_REQ_OUT)";
347 }
348
349 string code = "struct TPargsStruct {\n";
350 code ~= memberCode;
351 version (TVerboseCodegen) {
352 if (!methodMetaFound &&
353 ParameterTypeTuple!(mixin("Interface." ~ methodName)).length > 0)
354 {
355 code ~= "pragma(msg, `[thrift.codegen.base.TPargsStruct] Warning: No " ~
356 "meta information for method '" ~ methodName ~ "' in service '" ~
357 Interface.stringof ~ "' found.`);\n";
358 }
359 }
360 code ~= "void write(P)(P proto) const if (isTProtocol!P) {\n";
361 code ~= "writeStruct!(typeof(this), P, [" ~ ctfeJoin(fieldMetaCodes) ~
362 "], true)(this, proto);\n";
363 code ~= "}\n";
364 code ~= "}\n";
365 return code;
366 }());
367 }
368
369 /**
370 * Represents the result of a Thrift method call, using a pointer to the return
371 * value to avoid copying.
372 *
373 * There should usually be no reason to use this struct directly without the
374 * help of TClient, but it is documented publicly to help debugging in case
375 * of CTFE errors.
376 *
377 * Consider this example:
378 * ---
379 * interface Foo {
380 * int bar(string a);
381 *
382 * alias .FooException FooException;
383 *
384 * enum methodMeta = [
385 * TMethodMeta("bar",
386 * [TParamMeta("a", 1)],
387 * [TExceptionMeta("fooe", 1, "FooException")]
388 * )
389 * ];
390 * }
391 * alias TPresultStruct!(Foo, "bar") FooBarPresult;
392 * ---
393 *
394 * The definition of FooBarPresult is equivalent to (ignoring the necessary
395 * metadata to assign the field IDs):
396 * ---
397 * struct FooBarPresult {
398 * int* success;
399 * Foo.FooException fooe;
400 *
401 * struct IsSetFlags {
402 * bool success;
403 * }
404 * IsSetFlags isSetFlags;
405 *
406 * bool isSet(string fieldName)() const @property;
407 * void read(Protocol)(Protocol proto) if (isTProtocol!Protocol);
408 * }
409 * ---
410 */
TPresultStruct(Interface,string methodName)411 template TPresultStruct(Interface, string methodName) {
412 static assert(is(typeof(mixin("Interface." ~ methodName))),
413 "Could not find method '" ~ methodName ~ "' in '" ~ Interface.stringof ~ "'.");
414
415 mixin({
416 string code = "struct TPresultStruct {\n";
417
418 string[] fieldMetaCodes;
419
420 alias ReturnType!(mixin("Interface." ~ methodName)) ResultType;
421 static if (!is(ResultType == void)) {
422 code ~= q{
423 ReturnType!(mixin("Interface." ~ methodName))* success;
424 };
425 fieldMetaCodes ~= "TFieldMeta(`success`, 0, TReq.OPTIONAL)";
426
427 static if (!isNullable!ResultType) {
428 code ~= q{
429 struct IsSetFlags {
430 bool success;
431 }
432 IsSetFlags isSetFlags;
433 };
434 fieldMetaCodes ~= "TFieldMeta(`isSetFlags`, 0, TReq.IGNORE)";
435 }
436 }
437
438 bool methodMetaFound;
439 static if (is(typeof(Interface.methodMeta) : TMethodMeta[])) {
440 auto meta = find!`a.name == b`(Interface.methodMeta, methodName);
441 if (!meta.empty) {
442 foreach (e; meta.front.exceptions) {
443 code ~= "Interface." ~ e.type ~ " " ~ e.name ~ ";\n";
444 fieldMetaCodes ~= "TFieldMeta(`" ~ e.name ~ "`, " ~ to!string(e.id) ~
445 ", TReq.OPTIONAL)";
446 }
447 methodMetaFound = true;
448 }
449 }
450
451 version (TVerboseCodegen) {
452 if (!methodMetaFound &&
453 ParameterTypeTuple!(mixin("Interface." ~ methodName)).length > 0)
454 {
455 code ~= "pragma(msg, `[thrift.codegen.base.TPresultStruct] Warning: No " ~
456 "meta information for method '" ~ methodName ~ "' in service '" ~
457 Interface.stringof ~ "' found.`);\n";
458 }
459 }
460
461 code ~= q{
462 bool isSet(string fieldName)() const @property if (
463 is(MemberType!(typeof(this), fieldName))
464 ) {
465 static if (fieldName == "success") {
466 static if (isNullable!(typeof(*success))) {
467 return *success !is null;
468 } else {
469 return isSetFlags.success;
470 }
471 } else {
472 // We are dealing with an exception member, which, being a nullable
473 // type (exceptions are always classes), has no isSet flag.
474 return __traits(getMember, this, fieldName) !is null;
475 }
476 }
477 };
478
479 code ~= "void read(P)(P proto) if (isTProtocol!P) {\n";
480 code ~= "readStruct!(typeof(this), P, [" ~ ctfeJoin(fieldMetaCodes) ~
481 "], true)(this, proto);\n";
482 code ~= "}\n";
483 code ~= "}\n";
484 return code;
485 }());
486 }
487