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