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