1 module dutils.message.message;
2 
3 import core.time : Duration, seconds;
4 import std.datetime.systime : SysTime, Clock;
5 import std.uuid : UUID;
6 
7 import dutils.data.bson : BSON, serializeToBSON;
8 import dutils.data.json : JSON;
9 import dutils.validation.validate : validate, ValidationError;
10 import dutils.random : randomUUID;
11 import dutils.message.exceptions : TypeMismatchException;
12 
13 struct Message {
14   string type;
15   BSON payload;
16   string token;
17   string replyToQueueName;
18   UUID correlationId;
19   SysTime created;
20   SysTime expires;
21   ResponseStatus responseStatus = ResponseStatus.NOT_APPLICABLE;
22 
23   static Message opCall() {
24     auto message = Message.init;
25 
26     message.correlationId = randomUUID();
27     message.expires = Clock.currTime() + 10.seconds;
28     message.created = Clock.currTime();
29 
30     return message;
31   }
32 
33   static Message from(T)(T payload) {
34     return Message.from(payload, "");
35   }
36 
37   static Message from(T)(T payload, string token) {
38     return Message.from(payload, token, randomUUID());
39   }
40 
41   static Message from(T)(T payload, UUID correlationId) {
42     return Message.from(payload, "", correlationId);
43   }
44 
45   static Message from(T)(T payload, string token, UUID correlationId) {
46     validate(payload);
47 
48     auto message = Message();
49 
50     message.type = T.stringof;
51     message.payload = serializeToBSON(payload);
52     message.token = token;
53 
54     if (!correlationId.empty()) {
55       message.correlationId = correlationId;
56     }
57 
58     return message;
59   }
60 
61   T to(T)() {
62     import dutils.data.bson : populateFromBSON;
63 
64     if (T.stringof != this.type) {
65       throw new TypeMismatchException(T.stringof, this.type);
66     }
67 
68     T result;
69     populateFromBSON(result, this.payload);
70 
71     return result;
72   }
73 
74   @property bool isEmpty() {
75     return this.type == "";
76   }
77 
78   @property bool hasExpired() {
79     return Clock.currTime() > this.expires;
80   }
81 
82   Message createResponse(T)(T payload) {
83     import std.traits : getUDAs;
84 
85     auto status = ResponseStatus.OK;
86     auto udas = getUDAs!(T, SetResponseStatus);
87     static if (udas.length > 0) {
88       status = udas[0].status;
89     }
90 
91     return this.createResponse(status, payload);
92   }
93 
94   Message createResponse(T)(ResponseStatus responseStatus, T payload) {
95     validate(payload);
96 
97     auto message = Message();
98 
99     message.type = T.stringof;
100     message.payload = serializeToBSON(payload);
101     message.token = this.token;
102     message.correlationId = this.correlationId;
103     message.responseStatus = responseStatus;
104     message.expires = this.expires;
105 
106     return message;
107   }
108 
109   void setToExpireAfter(Duration duration) {
110     this.expires = Clock.currTime() + duration;
111   }
112 
113   JSON toJSON() {
114     return serializeToBSON(this).toJSON();
115   }
116 
117   string toString() {
118     return this.toJSON().toString();
119   }
120 }
121 
122 /**
123  * Message#from/to
124  */
125 unittest {
126   struct Page {
127     string title;
128     string body;
129   }
130 
131   auto expectedOutput = Page("A title", "Lorem ipsum dolor sit amet, consectetur...");
132 
133   auto message = Message.from(expectedOutput);
134   auto output = message.to!Page();
135 
136   assert(output.title == expectedOutput.title, "Expected title to match");
137   assert(output..body == expectedOutput..body, "Expected body to match");
138 }
139 
140 /**
141  * Message#to - bad type
142  */
143 unittest {
144   struct Page {
145     string title;
146     string body;
147   }
148 
149   struct Author {
150     string name;
151   }
152 
153   auto expectedOutput = Page("A title", "Lorem ipsum dolor sit amet, consectetur...");
154 
155   auto message = Message.from(expectedOutput);
156 
157   TypeMismatchException gotException;
158   try {
159     auto output = message.to!Author();
160   } catch (TypeMismatchException exception) {
161     gotException = exception;
162   }
163 
164   assert(gotException.msg == "Expected type Author but got Page");
165 }
166 
167 /**
168  * Message#createResponse - verify that response status is set
169  */
170 unittest {
171   import std.conv : to;
172 
173   struct Ping {
174     string message;
175   }
176 
177   auto ping = Ping("Lorem ipsum...");
178   auto request = Message.from(ping);
179   auto response = request.createResponse(ResponseForbidden());
180 
181   assert(response.type == "ResponseForbidden",
182       "Expected response.type to equal ResponseForbidden, got " ~ response.type);
183   assert(response.responseStatus == ResponseStatus.FORBIDDEN,
184       "Expected responseStatus to be ResponseStatus.FORBIDDEN got "
185       ~ response.responseStatus.to!string);
186   assert(response.correlationId == request.correlationId,
187       "response.correlationId should equal request.correlationId");
188 }
189 
190 /**
191  * The standard message type to send back on success when no response data is needed
192  */
193 @SetResponseStatus(ResponseStatus.OK)
194 struct ResponseOK {
195 }
196 
197 /**
198  * The standard message type to send back when the request message has an error,
199  * being less specific than ResponseInvalid or ResponseBadType it is better
200  * to try to use one of them as much as possible over this payload.
201  */
202 @SetResponseStatus(ResponseStatus.BAD_REQUEST)
203 struct ResponseBadRequest {
204   /**
205    * An array with the validation errors
206    */
207   string message;
208 }
209 
210 /**
211  * The standard message type to send back when there are validation errors from the data that the client sent
212  */
213 @SetResponseStatus(ResponseStatus.INVALID)
214 struct ResponseInvalid {
215   /**
216    * An array with the validation errors
217    */
218   ValidationError[] errors;
219 }
220 
221 /**
222  * The standard message type to send back when the type recieved is not supported
223  */
224 @SetResponseStatus(ResponseStatus.BAD_TYPE)
225 struct ResponseBadType {
226   /**
227    * The name of the bad type
228    */
229   string type;
230 
231   /**
232    * An array with the expected types
233    */
234   string[] supportedTypes;
235 }
236 
237 /**
238  * The standard message type to send back when the supplied token is not valid or lacks required permissions
239  */
240 @SetResponseStatus(ResponseStatus.FORBIDDEN)
241 struct ResponseForbidden {
242 }
243 
244 /**
245  * The standard message type to send back when there was an internal error, this message will not reveal any details
246  * but best practice is to also log the error on the service side.
247  */
248 @SetResponseStatus(ResponseStatus.INTERNAL_ERROR)
249 struct ResponseInternalError {
250 }
251 
252 /**
253  * The standard message type to send back when the service did not produce a response message before the timeout.
254  */
255 @SetResponseStatus(ResponseStatus.TIMEOUT)
256 struct ResponseTimeout {
257 }
258 
259 struct SetResponseStatus {
260   ResponseStatus status;
261 }
262 
263 enum ResponseStatus {
264   NOT_APPLICABLE = 0,
265   OK = 200,
266   BAD_REQUEST = 400,
267   FORBIDDEN = 403,
268   BAD_TYPE = 470,
269   INVALID = 471,
270   INTERNAL_ERROR = 500,
271   TIMEOUT = 601
272 }