1 module google.protobuf.any; 2 3 import std.json : JSONValue; 4 import std.typecons : Flag, No, Yes; 5 import google.protobuf; 6 7 enum defaultUrlPrefix = "type.googleapis.com"; 8 9 struct Any 10 { 11 private struct _Message 12 { 13 @Proto(1) string typeUrl = protoDefaultValue!string; 14 @Proto(2) bytes value = protoDefaultValue!bytes; 15 } 16 17 string typeUrl; 18 private bool valueIsJSON; 19 private union { 20 bytes protoValue; 21 JSONValue jsonValue; 22 } 23 24 T to(T)(string urlPrefix = defaultUrlPrefix) 25 { 26 import std.exception : enforce; 27 import std.format : format; 28 29 enforce!ProtobufException(isMessageType!T(urlPrefix), 30 "Incompatible target type `%s` for Any message of type '%s'".format(messageTypeUrl!T(urlPrefix), typeUrl)); 31 32 if (valueIsJSON) 33 { 34 return jsonValue.fromJSONValue!T; 35 } 36 else 37 { 38 return protoValue.fromProtobuf!T; 39 } 40 } 41 42 Any from(T)(T value, string urlPrefix = defaultUrlPrefix) 43 { 44 import std.array : array; 45 46 typeUrl = messageTypeUrl!T(urlPrefix); 47 protoValue = value.toProtobuf.array; 48 valueIsJSON = false; 49 50 return this; 51 } 52 53 bool isMessageType(T)(string urlPrefix = defaultUrlPrefix) 54 { 55 return typeUrl == messageTypeUrl!T(urlPrefix); 56 } 57 58 auto toProtobuf() 59 { 60 import std.array : array; 61 import std.exception : enforce; 62 import std.format : format; 63 import std.json : JSON_TYPE; 64 65 if (valueIsJSON) { 66 enforce!ProtobufException(typeUrl in messageTypes, 67 "Cannot handle 'Any' message: type '%s' is not registered".format(typeUrl)); 68 enforce!ProtobufException(jsonValue.type == JSON_TYPE.OBJECT, 69 "'Any' message JSON encoding must be an object"); 70 71 JSONValue jsonMapping; 72 73 if (messageTypes[typeUrl].hasSpecialJSONMapping) 74 { 75 enforce!ProtobufException("value" in jsonValue.object, 76 "'Any' message with special JSON mapping must have a 'value' entry"); 77 jsonMapping = jsonValue.object["value"]; 78 } 79 else 80 { 81 jsonMapping = jsonValue; 82 } 83 84 return _Message(typeUrl, messageTypes[typeUrl].JSONValueToProtobuf(jsonMapping).array).toProtobuf; 85 } 86 87 return _Message(typeUrl, protoValue).toProtobuf; 88 } 89 90 Any fromProtobuf(R)(ref R inputRange) 91 { 92 auto message = inputRange.fromProtobuf!_Message; 93 94 typeUrl = message.typeUrl; 95 protoValue = message.value; 96 valueIsJSON = false; 97 98 return this; 99 } 100 101 JSONValue toJSONValue()() 102 { 103 import std.format : format; 104 import std.exception : enforce; 105 import std.json : JSON_TYPE; 106 107 if (!valueIsJSON) { 108 enforce!ProtobufException(typeUrl in messageTypes, 109 "Cannot handle 'Any' message: type '%s' is not registered".format(typeUrl)); 110 111 auto result = messageTypes[typeUrl].protobufToJSONValue(protoValue); 112 113 if (messageTypes[typeUrl].hasSpecialJSONMapping) 114 { 115 return JSONValue([ 116 "@type": JSONValue(typeUrl), 117 "value": result, 118 ]); 119 } 120 121 enforce!ProtobufException(result.type == JSON_TYPE.OBJECT, 122 "'Any' message of type '%s' with no special JSON mapping is not an JSON object".format(typeUrl)); 123 result.object["@type"] = typeUrl; 124 return result; 125 } 126 127 enforce!ProtobufException(jsonValue.type == JSON_TYPE.OBJECT, "'Any' message JSON encoding must be an object"); 128 jsonValue.object["@type"] = typeUrl; 129 130 return jsonValue; 131 } 132 133 Any fromJSONValue()(JSONValue value) 134 { 135 import std.exception : enforce; 136 import std.json : JSON_TYPE; 137 138 if (value.type == JSON_TYPE.NULL) 139 { 140 typeUrl = ""; 141 protoValue = []; 142 valueIsJSON = false; 143 144 return this; 145 } 146 147 enforce!ProtobufException(value.type == JSON_TYPE.OBJECT, "Invalid 'Any' JSON encoding"); 148 enforce!ProtobufException("@type" in value.object, "No type specified for 'Any' JSON encoding"); 149 enforce!ProtobufException(value.object["@type"].type == JSON_TYPE.STRING, "Any typeUrl should be a string"); 150 151 typeUrl = value.object["@type"].str; 152 jsonValue = value; 153 valueIsJSON = true; 154 155 return this; 156 } 157 158 static EncodingInfo[string] messageTypes; 159 160 static registerMessageType(T, Flag!"hasSpecialJSONMapping" hasSpecialJSONMapping = No.hasSpecialJSONMapping) 161 (string urlPrefix = defaultUrlPrefix) 162 { 163 import std.array : array; 164 import std.exception : enforce; 165 import std.format : format; 166 167 string url = messageTypeUrl!T(urlPrefix); 168 enforce!ProtobufException(url !in messageTypes, "Message type '%s' already registered".format(url)); 169 170 messageTypes[url] = EncodingInfo(function(bytes input) => input.fromProtobuf!T.toJSONValue, 171 function(JSONValue input) => input.fromJSONValue!T.toProtobuf.array, hasSpecialJSONMapping); 172 } 173 174 struct EncodingInfo 175 { 176 JSONValue function(bytes) protobufToJSONValue; 177 bytes function(JSONValue) JSONValueToProtobuf; 178 bool hasSpecialJSONMapping; 179 } 180 } 181 182 // move following imports to static this() when dmd issue 18188 is solved 183 import google.protobuf.duration : Duration; 184 import google.protobuf.empty : Empty; 185 import google.protobuf.field_mask : FieldMask; 186 import google.protobuf.struct_ : ListValue, NullValue, Struct, Value; 187 import google.protobuf.timestamp : Timestamp; 188 import google.protobuf.wrappers : BoolValue, BytesValue, DoubleValue, FloatValue, Int32Value, Int64Value, StringValue, 189 UInt32Value, UInt64Value; 190 191 static this() 192 { 193 Any.registerMessageType!(Any, Yes.hasSpecialJSONMapping); 194 Any.registerMessageType!(BoolValue, Yes.hasSpecialJSONMapping); 195 Any.registerMessageType!(BytesValue, Yes.hasSpecialJSONMapping); 196 Any.registerMessageType!(DoubleValue, Yes.hasSpecialJSONMapping); 197 Any.registerMessageType!(Duration, Yes.hasSpecialJSONMapping); 198 Any.registerMessageType!(Empty, Yes.hasSpecialJSONMapping); 199 Any.registerMessageType!(FieldMask, Yes.hasSpecialJSONMapping); 200 Any.registerMessageType!(FloatValue, Yes.hasSpecialJSONMapping); 201 Any.registerMessageType!(Int32Value, Yes.hasSpecialJSONMapping); 202 Any.registerMessageType!(Int64Value, Yes.hasSpecialJSONMapping); 203 Any.registerMessageType!(ListValue, Yes.hasSpecialJSONMapping); 204 Any.registerMessageType!(NullValue, Yes.hasSpecialJSONMapping); 205 Any.registerMessageType!(StringValue, Yes.hasSpecialJSONMapping); 206 Any.registerMessageType!(Struct, Yes.hasSpecialJSONMapping); 207 Any.registerMessageType!(Timestamp, Yes.hasSpecialJSONMapping); 208 Any.registerMessageType!(UInt32Value, Yes.hasSpecialJSONMapping); 209 Any.registerMessageType!(UInt64Value, Yes.hasSpecialJSONMapping); 210 Any.registerMessageType!(Value, Yes.hasSpecialJSONMapping); 211 } 212 213 unittest 214 { 215 struct Foo 216 { 217 enum messageTypeFullName = "Foo"; 218 219 @Proto(1) int intField = protoDefaultValue!int; 220 @Proto(2) string stringField = protoDefaultValue!string; 221 } 222 auto foo1 = Foo(42, "abc"); 223 Any anyFoo; 224 225 assert(!anyFoo.isMessageType!Foo("my.prefix")); 226 227 anyFoo.from(foo1, "my.prefix"); 228 auto foo2 = anyFoo.to!Foo("my.prefix"); 229 230 assert(anyFoo.isMessageType!Foo("my.prefix")); 231 assert(anyFoo.typeUrl == "my.prefix/Foo"); 232 assert(foo1 == foo2); 233 } 234 235 236 enum messageTypeFullName(T) = { 237 import std.algorithm : splitter; 238 import std.array : join, split; 239 import std.traits : fullyQualifiedName, hasMember; 240 241 static if (hasMember!(T, "messageTypeFullName")) 242 { 243 return T.messageTypeFullName; 244 } 245 else 246 { 247 enum splitName = fullyQualifiedName!T.split("."); 248 249 return (splitName[0 .. $ - 2] ~ splitName[$ - 1]).join("."); 250 } 251 }(); 252 253 unittest 254 { 255 import google.protobuf.duration : Duration; 256 import google.protobuf.empty : Empty; 257 import google.protobuf.field_mask : FieldMask; 258 import google.protobuf.struct_ : ListValue, NullValue, Struct, Value; 259 import google.protobuf.timestamp : Timestamp; 260 import google.protobuf.wrappers : BoolValue, BytesValue, DoubleValue, FloatValue, Int32Value, Int64Value, 261 StringValue, UInt32Value, UInt64Value; 262 263 static assert(messageTypeFullName!Any == "google.protobuf.Any"); 264 static assert(messageTypeFullName!BoolValue == "google.protobuf.BoolValue"); 265 static assert(messageTypeFullName!BytesValue == "google.protobuf.BytesValue"); 266 static assert(messageTypeFullName!DoubleValue == "google.protobuf.DoubleValue"); 267 static assert(messageTypeFullName!Duration == "google.protobuf.Duration"); 268 static assert(messageTypeFullName!Empty == "google.protobuf.Empty"); 269 static assert(messageTypeFullName!FieldMask == "google.protobuf.FieldMask"); 270 static assert(messageTypeFullName!FloatValue == "google.protobuf.FloatValue"); 271 static assert(messageTypeFullName!Int32Value == "google.protobuf.Int32Value"); 272 static assert(messageTypeFullName!Int64Value == "google.protobuf.Int64Value"); 273 static assert(messageTypeFullName!ListValue == "google.protobuf.ListValue"); 274 static assert(messageTypeFullName!NullValue == "google.protobuf.NullValue"); 275 static assert(messageTypeFullName!StringValue == "google.protobuf.StringValue"); 276 static assert(messageTypeFullName!Struct == "google.protobuf.Struct"); 277 static assert(messageTypeFullName!Timestamp == "google.protobuf.Timestamp"); 278 static assert(messageTypeFullName!UInt32Value == "google.protobuf.UInt32Value"); 279 static assert(messageTypeFullName!UInt64Value == "google.protobuf.UInt64Value"); 280 static assert(messageTypeFullName!Value == "google.protobuf.Value"); 281 } 282 283 string messageTypeUrl(T)(string urlPrefix = defaultUrlPrefix) 284 { 285 return urlPrefix ~ "/" ~ messageTypeFullName!T; 286 }