1 module google.protobuf.json_decoding;
2 
3 import std.json : JSONValue, JSON_TYPE;
4 import std.traits : isArray, isAssociativeArray, isBoolean, isFloatingPoint, isIntegral, isSigned;
5 import google.protobuf.common;
6 
7 T fromJSONValue(T)(JSONValue value)
8 if (isBoolean!T)
9 {
10     switch (value.type)
11     {
12     case JSON_TYPE.NULL:
13         return protoDefaultValue!T;
14     case JSON_TYPE.TRUE:
15         return true;
16     case JSON_TYPE.FALSE:
17         return false;
18     default:
19         throw new ProtobufException("JSON boolean expected");
20     }
21 }
22 
23 T fromJSONValue(T)(JSONValue value)
24 if (isIntegral!T)
25 {
26     import std.conv : ConvException, to;
27     import std.exception : enforce;
28     import std.traits : OriginalType;
29 
30     try
31     {
32         switch (value.type)
33         {
34         case JSON_TYPE.NULL:
35             return protoDefaultValue!T;
36         case JSON_TYPE.STRING:
37             return value.str.to!T;
38         case JSON_TYPE.INTEGER:
39             return cast(T) value.integer.to!(OriginalType!T);
40         case JSON_TYPE.UINTEGER:
41             return cast(T) value.uinteger.to!(OriginalType!T);
42         case JSON_TYPE.FLOAT:
43         {
44             import core.stdc.math : fabs, modf;
45 
46             double integral;
47             double fractional = modf(value.floating, &integral);
48             double epsilon = double.epsilon * fabs(integral);
49 
50             enforce!ProtobufException(fabs(fractional) <= epsilon, "JSON integer expected");
51 
52             return value.floating.to!T;
53         }
54         default:
55             throw new ProtobufException("JSON integer expected");
56         }
57     }
58     catch (ConvException ConvException)
59     {
60         throw new ProtobufException("JSON integer expected");
61     }
62 }
63 
64 T fromJSONValue(T)(JSONValue value)
65 if (isFloatingPoint!T)
66 {
67     import std.conv : ConvException, to;
68     import std.math : isInfinity, isNaN;
69 
70     try
71     {
72         switch (value.type)
73         {
74         case JSON_TYPE.NULL:
75             return protoDefaultValue!T;
76         case JSON_TYPE.STRING:
77             switch (value.str)
78             {
79             case "NaN":
80                 return T.nan;
81             case "Infinity":
82                 return T.infinity;
83             case "-Infinity":
84                 return -T.infinity;
85             default:
86                 return value.str.to!T;
87             }
88         case JSON_TYPE.INTEGER:
89             return value.integer.to!T;
90         case JSON_TYPE.UINTEGER:
91             return value.uinteger.to!T;
92         case JSON_TYPE.FLOAT:
93             return value.floating;
94         default:
95             throw new ProtobufException("JSON float expected");
96         }
97     }
98     catch (ConvException ConvException)
99     {
100         throw new ProtobufException("JSON float expected");
101     }
102 }
103 
104 T fromJSONValue(T)(JSONValue value)
105 if (is(T == string))
106 {
107     import std.exception : enforce;
108 
109     if (value.isNull)
110         return protoDefaultValue!T;
111 
112     enforce!ProtobufException(value.type == JSON_TYPE.STRING, "JSON string expected");
113     return value.str;
114 }
115 
116 T fromJSONValue(T)(JSONValue value)
117 if (is(T == bytes))
118 {
119     import std.base64 : Base64;
120     import std.exception : enforce;
121     import std.json : JSON_TYPE;
122 
123     if (value.isNull)
124         return protoDefaultValue!T;
125 
126     enforce!ProtobufException(value.type == JSON_TYPE.STRING, "JSON base64 encoded binary expected");
127     return Base64.decode(value.str);
128 }
129 
130 T fromJSONValue(T)(JSONValue value)
131 if (isArray!T && !is(T == string) && !is(T == bytes))
132 {
133     import std.algorithm : map;
134     import std.array : array;
135     import std.exception : enforce;
136     import std.range : ElementType;
137 
138     if (value.isNull)
139         return protoDefaultValue!T;
140 
141     enforce!ProtobufException(value.type == JSON_TYPE.ARRAY, "JSON array expected");
142     return value.array.map!(a => a.fromJSONValue!(ElementType!T)).array;
143 }
144 
145 T fromJSONValue(T)(JSONValue value, T result = null)
146 if (isAssociativeArray!T)
147 {
148     import std.conv : ConvException, to;
149     import std.exception : enforce;
150     import std.traits : KeyType, ValueType;
151 
152     if (value.isNull)
153         return protoDefaultValue!T;
154 
155     enforce!ProtobufException(value.type == JSON_TYPE.OBJECT, "JSON object expected");
156     foreach (k, v; value.object)
157     {
158         try
159         {
160             result[k.to!(KeyType!T)] = v.fromJSONValue!(ValueType!T);
161         }
162         catch (ConvException exception)
163         {
164             throw new ProtobufException(exception.msg);
165         }
166     }
167 
168     return result;
169 }
170 
171 unittest
172 {
173     import std.exception : assertThrown;
174     import std.json : parseJSON;
175     import std.math : isInfinity, isNaN;
176 
177     assert(fromJSONValue!bool(JSONValue(false)) == false);
178     assert(fromJSONValue!bool(JSONValue(true)) == true);
179     assertThrown!ProtobufException(fromJSONValue!bool(JSONValue(1)));
180 
181     assert(fromJSONValue!int(JSONValue(1)) == 1);
182     assert(fromJSONValue!uint(JSONValue(1U)) == 1U);
183     assert(fromJSONValue!long(JSONValue(1L)) == 1);
184     assert(fromJSONValue!ulong(JSONValue(1UL)) == 1U);
185     assertThrown!ProtobufException(fromJSONValue!int(JSONValue(false)));
186     assertThrown!ProtobufException(fromJSONValue!ulong(JSONValue("foo")));
187 
188     assert(fromJSONValue!float(JSONValue(1.0f)) == 1.0);
189     assert(fromJSONValue!double(JSONValue(1.0)) == 1.0);
190     assert(fromJSONValue!float(JSONValue("NaN")).isNaN);
191     assert(fromJSONValue!double(JSONValue("Infinity")).isInfinity);
192     assert(fromJSONValue!double(JSONValue("-Infinity")).isInfinity);
193     assertThrown!ProtobufException(fromJSONValue!float(JSONValue(false)));
194     assertThrown!ProtobufException(fromJSONValue!double(JSONValue("foo")));
195 
196     assert(fromJSONValue!bytes(JSONValue("Zm9v")) == cast(bytes) "foo");
197     assertThrown!ProtobufException(fromJSONValue!bytes(JSONValue(1)));
198 
199     assert(fromJSONValue!(int[])(parseJSON(`[1, 2, 3]`)) == [1, 2, 3]);
200     assertThrown!ProtobufException(fromJSONValue!(int[])(JSONValue(`[1, 2, 3]`)));
201 
202     assert(fromJSONValue!(bool[int])(parseJSON(`{"1": false, "2": true}`)) == [1 : false, 2 : true]);
203     assertThrown!ProtobufException(fromJSONValue!(bool[int])(JSONValue(`{"1": false, "2": true}`)));
204     assertThrown!ProtobufException(fromJSONValue!(bool[int])(parseJSON(`{"foo": false, "2": true}`)));
205 }
206 
207 T fromJSONValue(T)(JSONValue value, T result = protoDefaultValue!T)
208 if (is(T == class) || is(T == struct))
209 {
210     import std.algorithm : findAmong;
211     import std.exception : enforce;
212     import std.meta : staticMap;
213     import std.range : empty;
214     import std.traits : hasMember;
215 
216     static if (is(T == class))
217     {
218         if (result is null)
219             result = new T;
220     }
221 
222     static if (hasMember!(T, "fromJSONValue"))
223     {
224         return result.fromJSONValue(value);
225     }
226     else
227     {
228         enum jsonName(string fieldName) = {
229             import std.algorithm : skipOver;
230 
231             string result = fieldName;
232 
233             if (fieldName[$ - 1] == '_')
234                 result = fieldName[0 .. $ - 1];
235 
236             static if (isOneof!(mixin("T." ~ fieldName)))
237                 result.skipOver("_");
238 
239             return result;
240         }();
241 
242         if (value.isNull)
243             return protoDefaultValue!T;
244 
245         enforce!ProtobufException(value.type == JSON_TYPE.OBJECT, "JSON object expected");
246 
247         JSONValue[string] members = value.object;
248 
249         foreach (fieldName; Message!T.fieldNames)
250         {
251             enum jsonFieldName = jsonName!fieldName;
252 
253             auto fieldValue = (jsonFieldName in members);
254             if (fieldValue !is null)
255             {
256                 static if (isOneof!(mixin("T." ~ fieldName)))
257                 {
258                     alias otherFields = staticMap!(jsonName, otherOneofFieldNames!(T, mixin("T." ~ fieldName)));
259                     enforce!ProtobufException(members.keys.findAmong([otherFields]).empty,
260                             "More than one oneof field in JSON Message");
261                 }
262 
263                 mixin("result." ~ fieldName) = fromJSONValue!(typeof(mixin("T." ~ fieldName)))(*fieldValue);
264             }
265         }
266         return result;
267     }
268 }
269 
270 unittest
271 {
272     import std.exception : assertThrown;
273     import std.json : parseJSON;
274 
275     struct Foo
276     {
277         @Proto(1) int a;
278         @Proto(3) string b;
279         @Proto(4) bool c;
280 
281         @Oneof("test")
282         union
283         {
284             @Proto(5) int _d;
285             @Proto(6) string _e;
286         }
287     }
288 
289     auto foo = Foo(10, "abc", false);
290 
291     assert(fromJSONValue!Foo(parseJSON(`{"a":10, "b":"abc"}`)) == Foo(10, "abc", false));
292     assert(fromJSONValue!Foo(parseJSON(`{"a": 10, "b": "abc", "c": false}`)) == Foo(10, "abc", false));
293     assertThrown!ProtobufException(fromJSONValue!Foo(parseJSON(`{"a":10, "b":100}`)));
294     assertThrown!ProtobufException(fromJSONValue!Foo(parseJSON(`{"d":10, "e":"abc"}`)));
295 }
296 
297 unittest
298 {
299     import std.json : parseJSON;
300 
301     struct EmptyMessage
302     {
303     }
304 
305     assert(fromJSONValue!EmptyMessage(parseJSON(`{}`)) == EmptyMessage());
306     assert(fromJSONValue!EmptyMessage(parseJSON(`{"a":10, "b":"abc"}`)) == EmptyMessage());
307 }
308 
309 private template oneofs(T)
310 {
311     import std.meta : NoDuplicates, staticMap;
312     import std.traits : getSymbolsByUDA;
313 
314     private alias oneofs = NoDuplicates!(staticMap!(oneofByField, getSymbolsByUDA!(T, Oneof)));
315 }
316 
317 private template oneofByField(alias field)
318 {
319     import std.traits : getUDAs;
320 
321     enum Oneof oneofByField = getUDAs!(field, Oneof)[0];
322 }
323 
324 private template otherOneofFieldNames(T, alias field)
325 {
326     import std.meta : Erase, Filter, staticMap;
327 
328     static assert(is(typeof(__traits(parent, field).init) == T));
329     static assert(isOneof!field);
330 
331     static enum hasSameOneofCase(alias field2) = oneofCaseFieldName!field == oneofCaseFieldName!field2;
332     static enum fieldName(alias field) = __traits(identifier, field);
333 
334     enum otherOneofFieldNames = staticMap!(fieldName, Erase!(field, Filter!(hasSameOneofCase, Filter!(isOneof,
335             Message!T.fields))));
336 }
337 
338 unittest
339 {
340     import std.meta : AliasSeq, staticMap;
341 
342     static struct Test
343     {
344         @Oneof("foo")
345         union
346         {
347             @Proto(1) int foo1;
348             @Proto(2) int foo2;
349         }
350         @Oneof("bar")
351         union
352         {
353             @Proto(11) int bar1;
354             @Proto(12) int bar2;
355             @Proto(13) int bar3;
356         }
357 
358         @Proto(20) int baz;
359     }
360 
361     static assert([otherOneofFieldNames!(Test, Test.foo1)] == ["foo2"]);
362     static assert([otherOneofFieldNames!(Test, Test.bar2)] == ["bar1", "bar3"]);
363 }