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 }