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           "`, TMessageType.CALL, ++seqid_));\n";
189         code ~= "args.write(oprot_);\n";
190         code ~= "oprot_.writeMessageEnd();\n";
191         code ~= "oprot_.transport.flush();\n";
192 
193         // If this is not a oneway method, generate the recieving code.
194         if (!methodMetaFound || methodMeta.type != TMethodType.ONEWAY) {
195           code ~= "TPresultStruct!(Interface, `" ~ methodName ~ "`) result;\n";
196 
197           if (!is(ReturnType!(mixin("Interface." ~ methodName)) == void)) {
198             code ~= "ReturnType!(Interface." ~ methodName ~ ") _return;\n";
199             code ~= "result.success = &_return;\n";
200           }
201 
202           // TODO: The C++ implementation checks for matching method name here,
203           // should we do as well?
204           code ~= q{
205             auto msg = iprot_.readMessageBegin();
206             scope (exit) {
207               iprot_.readMessageEnd();
208               iprot_.transport.readEnd();
209             }
210 
211             if (msg.type == TMessageType.EXCEPTION) {
212               auto x = new TApplicationException(null);
213               x.read(iprot_);
214               iprot_.transport.readEnd();
215               throw x;
216             }
217             if (msg.type != TMessageType.REPLY) {
218               skip(iprot_, TType.STRUCT);
219               iprot_.transport.readEnd();
220             }
221             if (msg.seqid != seqid_) {
222               throw new TApplicationException(
223                 methodName ~ " failed: Out of sequence response.",
224                 TApplicationException.Type.BAD_SEQUENCE_ID
225               );
226             }
227             result.read(iprot_);
228           };
229 
230           if (methodMetaFound) {
231             foreach (e; methodMeta.exceptions) {
232               code ~= "if (result.isSet!`" ~ e.name ~ "`) throw result." ~
233                 e.name ~ ";\n";
234             }
235           }
236 
237           if (!is(ReturnType!(mixin("Interface." ~ methodName)) == void)) {
238             code ~= q{
239               if (result.isSet!`success`) return _return;
240               throw new TApplicationException(
241                 methodName ~ " failed: Unknown result.",
242                 TApplicationException.Type.MISSING_RESULT
243               );
244             };
245           }
246         }
247         code ~= "}\n";
248       }
249     }
250 
251     code ~= "}\n";
252     return code;
253   }());
254 }
255 
256 /**
257  * TClient construction helper to avoid having to explicitly specify
258  * the protocol types, i.e. to allow the constructor being called using IFTI
259  * (see $(DMDBUG 6082, D Bugzilla enhancement requet 6082)).
260  */
261 TClient!(Interface, Prot) tClient(Interface, Prot)(Prot prot) if (
262   isService!Interface && isTProtocol!Prot
263 ) {
264   return new TClient!(Interface, Prot)(prot);
265 }
266 
267 /// Ditto
268 TClient!(Interface, IProt, Oprot) tClient(Interface, IProt, OProt)
269   (IProt iprot, OProt oprot) if (
270   isService!Interface && isTProtocol!IProt && isTProtocol!OProt
271 ) {
272   return new TClient!(Interface, IProt, OProt)(iprot, oprot);
273 }
274 
275 /**
276  * Represents the arguments of a Thrift method call, as pointers to the (const)
277  * parameter type to avoid copying.
278  *
279  * There should usually be no reason to use this struct directly without the
280  * help of TClient, but it is documented publicly to help debugging in case
281  * of CTFE errors.
282  *
283  * Consider this example:
284  * ---
285  * interface Foo {
286  *   int bar(string a, bool b);
287  *
288  *   enum methodMeta = [
289  *     TMethodMeta("bar", [TParamMeta("a", 1), TParamMeta("b", 2)])
290  *   ];
291  * }
292  *
293  * alias TPargsStruct!(Foo, "bar") FooBarPargs;
294  * ---
295  *
296  * The definition of FooBarPargs is equivalent to (ignoring the necessary
297  * metadata to assign the field IDs):
298  * ---
299  * struct FooBarPargs {
300  *   const(string)* a;
301  *   const(bool)* b;
302  *
303  *   void write(Protocol)(Protocol proto) const if (isTProtocol!Protocol);
304  * }
305  * ---
306  */
307 template TPargsStruct(Interface, string methodName) {
308   static assert(is(typeof(mixin("Interface." ~ methodName))),
309     "Could not find method '" ~ methodName ~ "' in '" ~ Interface.stringof ~ "'.");
310   mixin({
311     bool methodMetaFound;
312     TMethodMeta methodMeta;
313     static if (is(typeof(Interface.methodMeta) : TMethodMeta[])) {
314       auto meta = find!`a.name == b`(Interface.methodMeta, methodName);
315       if (!meta.empty) {
316         methodMetaFound = true;
317         methodMeta = meta.front;
318       }
319     }
320 
321     string memberCode;
322     string[] fieldMetaCodes;
323     foreach (i, _; ParameterTypeTuple!(mixin("Interface." ~ methodName))) {
324       // If we have no meta information, just use param1, param2, etc. as
325       // field names, it shouldn't really matter anyway. 1-based »indexing«
326       // is used to match the common scheme in the Thrift world.
327       string memberId;
328       string memberName;
329       if (methodMetaFound && i < methodMeta.params.length) {
330         memberId = to!string(methodMeta.params[i].id);
331         memberName = methodMeta.params[i].name;
332       } else {
333         memberId = to!string(i + 1);
334         memberName = "param" ~ to!string(i + 1);
335       }
336 
337       // Workaround for DMD @@BUG@@ 6056: make an intermediary alias for the
338       // parameter type, and declare the member using const(memberNameType)*.
339       memberCode ~= "alias ParameterTypeTuple!(Interface." ~ methodName ~
340         ")[" ~ to!string(i) ~ "] " ~ memberName ~ "Type;\n";
341       memberCode ~= "const(" ~ memberName ~ "Type)* " ~ memberName ~ ";\n";
342 
343       fieldMetaCodes ~= "TFieldMeta(`" ~ memberName ~ "`, " ~ memberId ~
344         ", TReq.OPT_IN_REQ_OUT)";
345     }
346 
347     string code = "struct TPargsStruct {\n";
348     code ~= memberCode;
349     version (TVerboseCodegen) {
350       if (!methodMetaFound &&
351         ParameterTypeTuple!(mixin("Interface." ~ methodName)).length > 0)
352       {
353         code ~= "pragma(msg, `[thrift.codegen.base.TPargsStruct] Warning: No " ~
354           "meta information for method '" ~ methodName ~ "' in service '" ~
355           Interface.stringof ~ "' found.`);\n";
356       }
357     }
358     code ~= "void write(P)(P proto) const if (isTProtocol!P) {\n";
359     code ~= "writeStruct!(typeof(this), P, [" ~ ctfeJoin(fieldMetaCodes) ~
360       "], true)(this, proto);\n";
361     code ~= "}\n";
362     code ~= "}\n";
363     return code;
364   }());
365 }
366 
367 /**
368  * Represents the result of a Thrift method call, using a pointer to the return
369  * value to avoid copying.
370  *
371  * There should usually be no reason to use this struct directly without the
372  * help of TClient, but it is documented publicly to help debugging in case
373  * of CTFE errors.
374  *
375  * Consider this example:
376  * ---
377  * interface Foo {
378  *   int bar(string a);
379  *
380  *   alias .FooException FooException;
381  *
382  *   enum methodMeta = [
383  *     TMethodMeta("bar",
384  *       [TParamMeta("a", 1)],
385  *       [TExceptionMeta("fooe", 1, "FooException")]
386  *     )
387  *   ];
388  * }
389  * alias TPresultStruct!(Foo, "bar") FooBarPresult;
390  * ---
391  *
392  * The definition of FooBarPresult is equivalent to (ignoring the necessary
393  * metadata to assign the field IDs):
394  * ---
395  * struct FooBarPresult {
396  *   int* success;
397  *   Foo.FooException fooe;
398  *
399  *   struct IsSetFlags {
400  *     bool success;
401  *   }
402  *   IsSetFlags isSetFlags;
403  *
404  *   bool isSet(string fieldName)() const @property;
405  *   void read(Protocol)(Protocol proto) if (isTProtocol!Protocol);
406  * }
407  * ---
408  */
409 template TPresultStruct(Interface, string methodName) {
410   static assert(is(typeof(mixin("Interface." ~ methodName))),
411     "Could not find method '" ~ methodName ~ "' in '" ~ Interface.stringof ~ "'.");
412 
413   mixin({
414     string code = "struct TPresultStruct {\n";
415 
416     string[] fieldMetaCodes;
417 
418     alias ReturnType!(mixin("Interface." ~ methodName)) ResultType;
419     static if (!is(ResultType == void)) {
420       code ~= q{
421         ReturnType!(mixin("Interface." ~ methodName))* success;
422       };
423       fieldMetaCodes ~= "TFieldMeta(`success`, 0, TReq.OPTIONAL)";
424 
425       static if (!isNullable!ResultType) {
426         code ~= q{
427           struct IsSetFlags {
428             bool success;
429           }
430           IsSetFlags isSetFlags;
431         };
432         fieldMetaCodes ~= "TFieldMeta(`isSetFlags`, 0, TReq.IGNORE)";
433       }
434     }
435 
436     bool methodMetaFound;
437     static if (is(typeof(Interface.methodMeta) : TMethodMeta[])) {
438       auto meta = find!`a.name == b`(Interface.methodMeta, methodName);
439       if (!meta.empty) {
440         foreach (e; meta.front.exceptions) {
441           code ~= "Interface." ~ e.type ~ " " ~ e.name ~ ";\n";
442           fieldMetaCodes ~= "TFieldMeta(`" ~ e.name ~ "`, " ~ to!string(e.id) ~
443             ", TReq.OPTIONAL)";
444         }
445         methodMetaFound = true;
446       }
447     }
448 
449     version (TVerboseCodegen) {
450       if (!methodMetaFound &&
451         ParameterTypeTuple!(mixin("Interface." ~ methodName)).length > 0)
452       {
453         code ~= "pragma(msg, `[thrift.codegen.base.TPresultStruct] Warning: No " ~
454           "meta information for method '" ~ methodName ~ "' in service '" ~
455           Interface.stringof ~ "' found.`);\n";
456       }
457     }
458 
459     code ~= q{
460       bool isSet(string fieldName)() const @property if (
461         is(MemberType!(typeof(this), fieldName))
462       ) {
463         static if (fieldName == "success") {
464           static if (isNullable!(typeof(*success))) {
465             return *success !is null;
466           } else {
467             return isSetFlags.success;
468           }
469         } else {
470           // We are dealing with an exception member, which, being a nullable
471           // type (exceptions are always classes), has no isSet flag.
472           return __traits(getMember, this, fieldName) !is null;
473         }
474       }
475     };
476 
477     code ~= "void read(P)(P proto) if (isTProtocol!P) {\n";
478     code ~= "readStruct!(typeof(this), P, [" ~ ctfeJoin(fieldMetaCodes) ~
479       "], true)(this, proto);\n";
480     code ~= "}\n";
481     code ~= "}\n";
482     return code;
483   }());
484 }