1 module google.protobuf.json_decoding;
2 
3 import std.json : JSONValue, JSONType;
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 JSONType.null_:
13         return protoDefaultValue!T;
14     case JSONType.true_:
15         return true;
16     case JSONType.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 JSONType.null_:
35             return protoDefaultValue!T;
36         case JSONType..string:
37             return value.str.to!T;
38         case JSONType.integer:
39             return cast(T) value.integer.to!(OriginalType!T);
40         case JSONType.uinteger:
41             return cast(T) value.uinteger.to!(OriginalType!T);
42         case JSONType.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 JSONType.null_:
75             return protoDefaultValue!T;
76         case JSONType..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 JSONType.integer:
89             return value.integer.to!T;
90         case JSONType.uinteger:
91             return value.uinteger.to!T;
92         case JSONType.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 == JSONType..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 : JSONType;
122 
123     if (value.isNull)
124         return protoDefaultValue!T;
125 
126     enforce!ProtobufException(value.type == JSONType..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 == JSONType.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 == JSONType.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 == JSONType.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         enum TestCase
282         {
283             testNotSet = 0,
284             d = 5,
285             e = 6,
286         }
287         TestCase _testCase = TestCase.testNotSet;
288         @property TestCase testCase() { return _testCase; }
289         void clearTest() { _testCase = TestCase.testNotSet; }
290         @Oneof("_testCase") union
291         {
292             @Proto(5) int _d = protoDefaultValue!int; mixin(oneofAccessors!_d);
293             @Proto(6) string _e; mixin(oneofAccessors!_e);
294         }
295     }
296 
297     auto foo = Foo(10, "abc", false);
298 
299     assert(fromJSONValue!Foo(parseJSON(`{"a":10, "b":"abc"}`)) == Foo(10, "abc", false));
300     assert(fromJSONValue!Foo(parseJSON(`{"a": 10, "b": "abc", "c": false}`)) == Foo(10, "abc", false));
301     assertThrown!ProtobufException(fromJSONValue!Foo(parseJSON(`{"a":10, "b":100}`)));
302     assertThrown!ProtobufException(fromJSONValue!Foo(parseJSON(`{"d":10, "e":"abc"}`)));
303 }
304 
305 unittest
306 {
307     import std.json : parseJSON;
308 
309     struct EmptyMessage
310     {
311     }
312 
313     assert(fromJSONValue!EmptyMessage(parseJSON(`{}`)) == EmptyMessage());
314     assert(fromJSONValue!EmptyMessage(parseJSON(`{"a":10, "b":"abc"}`)) == EmptyMessage());
315 }
316 
317 private template oneofs(T)
318 {
319     import std.meta : NoDuplicates, staticMap;
320     import std.traits : getSymbolsByUDA;
321 
322     private alias oneofs = NoDuplicates!(staticMap!(oneofByField, getSymbolsByUDA!(T, Oneof)));
323 }
324 
325 private template oneofByField(alias field)
326 {
327     import std.traits : getUDAs;
328 
329     enum Oneof oneofByField = getUDAs!(field, Oneof)[0];
330 }
331 
332 private template otherOneofFieldNames(T, alias field)
333 {
334     import std.meta : Erase, Filter, staticMap;
335 
336     static assert(is(typeof(__traits(parent, field).init) == T));
337     static assert(isOneof!field);
338 
339     static enum hasSameOneofCase(alias field2) = oneofCaseFieldName!field == oneofCaseFieldName!field2;
340     static enum fieldName(alias field) = __traits(identifier, field);
341 
342     enum otherOneofFieldNames = staticMap!(fieldName, Erase!(field, Filter!(hasSameOneofCase, Filter!(isOneof,
343             Message!T.fields))));
344 }
345 
346 unittest
347 {
348     import std.meta : AliasSeq, staticMap;
349 
350     static struct Test
351     {
352         @Oneof("foo")
353         union
354         {
355             @Proto(1) int foo1;
356             @Proto(2) int foo2;
357         }
358         @Oneof("bar")
359         union
360         {
361             @Proto(11) int bar1;
362             @Proto(12) int bar2;
363             @Proto(13) int bar3;
364         }
365 
366         @Proto(20) int baz;
367     }
368 
369     static assert([otherOneofFieldNames!(Test, Test.foo1)] == ["foo2"]);
370     static assert([otherOneofFieldNames!(Test, Test.bar2)] == ["bar1", "bar3"]);
371 }