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 }