1 module json.serialization;
2 
3 // this module is not ready for use just yet
4 version( none ):
5 
6 private{
7     import json.parser : parseJson;
8     import json.value;
9 
10     import std.meta;
11     import std.string;
12     import std.traits;
13     import std.typecons;
14 
15     void ensureNotNull( T )( JsonValue value )
16     {
17         if( value.isNull )
18             throw new JsonException( "cannot convert null to %s".format( T.stringof ) );
19     }
20 
21     void ensureDeserializable( T, string name )()
22     {
23         enum canConvert = is( typeof( {
24             auto _ = &JsonValue.opCast!T;
25         } ) );
26 
27         static assert(
28             canConvert,
29             "cannot convert to type %s for field %s".format( T.stringof, name )
30         );
31     }
32 
33     template isNullable( T, bool typeconsNullable )
34     {
35         static if( !typeconsNullable )
36             enum isNullable = is( typeof( { T _ = null; } ) );
37         else
38         {
39             enum isNullable = is( typeof( {
40                 import std.algorithm.searching : startsWith;
41 
42                 enum name = fullyQualifiedName!( Unqual!T );
43                 static assert( name.startsWith( "std.typecons.Nullable" ) );
44             } ) );
45         }
46     }
47 
48     enum typeIsJsonObject( T, bool hasDeserializer = true, bool hasSerializer = true ) = is( typeof( {
49         static if( hasDeserializer )
50             T.fromJson( JsonValue.init );
51 
52         static if( hasSerializer )
53             JsonValue _ = T.init.toJson();
54     } ) );
55 
56     enum typeIsJsonValue( T ) = is( typeof( {
57         auto _ = JsonValue( T.init );
58     } ) );
59 
60     enum isRefNullable( T ) = is( typeof( {
61         import std.algorithm.searching : startsWith;
62 
63         enum name = fullyQualifiedName!( Unqual!T );
64         static assert( name.startsWith( "std.typecons.NullableRef" ) );
65     } ) );
66 
67     void deserialize( string name, T )( JsonValue value, T* member )
68         if( isNullable!( T, false ) && !typeIsJsonObject!( T, true, false ) )
69     {
70         ensureDeserializable!( T, name );
71         *member = value.isNull ? null : value.to!T;
72     }
73 
74     void deserialize( string name, T )( JsonValue value, T* member )
75         if( typeIsJsonObject!( T, true, false ) )
76     {
77         *member = value.isNull ? null : T.fromJson( value );
78     }
79 
80     void deserialize( string name, T )( JsonValue value, T* member )
81         if( isNullable!( T, true ) && !isRefNullable!T )
82     {
83         alias TDest = Unqual!( ReturnType!( &(*member).get ) );
84         ensureDeserializable!( TDest, name );
85 
86         *member = value.isNull ? T.init : T( value.to!TDest );
87     }
88 
89     void deserialize( string name, T )( JsonValue value, T* member )
90         if( isNullable!( T, true ) && isRefNullable!T )
91     {
92         alias TDest = Unqual!( ReturnType!( &(*member).get ) );
93         ensureDeserializable!( TDest, name );
94 
95         TDest* ptr = void;
96 
97         if( value.isNull )
98         {
99             *member = T.init;
100             return;
101         }
102 
103         *ptr = value.to!TDest;
104         *member = T( ptr );
105     }
106 
107     void deserialize( string name, T )( JsonValue value, T* member )
108         if( ( !isNullable!( T, true ) && !isNullable!( T, false ) ) && typeIsJsonObject!( T, true, false ) )
109     {
110         value.ensureNotNull!T;
111         *member = T.fromJson( value );
112     }
113 
114     void deserialize( string name, T )( JsonValue value, T* member )
115         if( ( !isNullable!( T, true ) && !isNullable!( T, false ) ) && !typeIsJsonObject!( T, true, false ) )
116     {
117         ensureDeserializable!( T, name );
118         value.ensureNotNull!T;
119         *member = value.to!T;
120     }
121 }
122 
123 enum Required
124 {
125     no,
126     allowNull,
127     disallowNull,
128     always,
129 }
130 
131 enum SerializationMode
132 {
133     optIn,
134     optOut,
135 }
136 
137 struct JsonSerialization
138 {
139     immutable SerializationMode mode;
140 
141     this() @disable;
142 
143     this( SerializationMode mode )
144     {
145         this.mode = mode;
146     }
147 }
148 
149 struct JsonIgnore { }
150 
151 struct JsonProperty
152 {
153     private string _name;
154     auto name() const @property { return this._name; }
155 
156     immutable Required required;
157 
158     this( string name, Required required = Required.no )
159     {
160         this._name = name;
161         this.required = required;
162     }
163 }
164 
165 private template Reflector( This ) if( is( This == class ) || is( This == struct ) )
166 {
167     SerializationMode serializationMode()
168     {
169         static if( hasUDA!( This, JsonSerialization ) )
170             return getUDAs!( This, JsonSerialization )[0].mode;
171         else
172             return SerializationMode.optOut;
173     }
174 
175     enum isJsonIgnored( T... ) = hasUDA!( __traits( getMember, This, T[0] ), JsonIgnore );
176     enum isJsonProperty( T... ) = hasUDA!( __traits( getMember, This, T[0] ), JsonProperty );
177 
178     template shouldInclude( T... )
179     {
180         static if( serializationMode == SerializationMode.optIn )
181             enum shouldInclude = isJsonProperty!T;
182         else static if( serializationMode == SerializationMode.optOut )
183             enum shouldInclude = !isJsonIgnored!T;
184         else
185             static assert( false, "programmer forgot to implement something" );
186     }
187 
188     enum isImmutable( T... ) = is( typeof( __traits( getMember, This, T[0] ) ) == immutable )
189                             || is( typeof( __traits( getMember, This, T[0] ) ) == const );
190 
191     template not( alias x )
192     {
193         enum not( T... ) = !x!T;
194     }
195 
196     JsonProperty getProperty( alias name )()
197     {
198         static if( isJsonProperty!name )
199             return getUDAs!( __traits( getMember, This, name ), JsonProperty )[0];
200         else
201             static assert( false, "'%s' is not a json property".formaT( name ) );
202     }
203 
204     string fieldName( alias name )()
205     {
206         static if( isJsonProperty!name )
207         {
208             enum prop = getProperty!name;
209             return prop.name !is null && prop.name.length ? prop.name : name;
210         }
211         else return name;
212     }
213 
214     Required getRequirement( alias name )()
215     {
216         static if( isJsonProperty!name )
217             return getProperty!( name ).required;
218         else
219             return Required.no;
220     }
221 
222     static if( ( FieldNameTuple!This ).length )
223         enum members = Filter!( not!isImmutable, Filter!( shouldInclude, FieldNameTuple!This ) );
224     else
225         enum members = AliasSeq!();
226 }
227 
228 mixin template JsonObject()
229 {
230     static assert(
231         is( typeof( this ) == class ) || is( typeof( this ) == struct ),
232         "JsonObject template can only be mixed in to a class or struct"
233     );
234 
235     T toJsonString( T = string )( bool indented = false ) if( isSomeString!T )
236     {
237         return this.toJson().toJsonString( indented );
238     }
239 
240     JsonValue toJson()
241     {
242         alias This = typeof( this );
243         alias reflector = Reflector!This;
244 
245         void ensureConversion( T, string name )()
246         {
247             enum canConvert = is( typeof( {
248                 auto _ = JsonValue( T.init );
249             } ) );
250 
251             static assert(
252                 canConvert,
253                 "cannot convert from type %s for field %s".format( T.stringof, name )
254             );
255         }
256 
257         auto json = JsonValue.newObject();
258 
259         foreach( i, name; reflector.members )
260         {
261             enum field = reflector.fieldName!name;
262             alias type = typeof( __traits( getMember, this, name ) );
263 
264             try
265             {
266                 static if( isNullable!type )
267                 {
268                     static if( is( typeof( { type _ = null; } ) ) && !typeIsJsonObject!type )
269                     {
270                         ensureConversion!( type, name );
271                         mixin( "json[\"%s\"] = this.%s.isNull ? jnull : JsonValue( this );".format( field, name ) );
272                     }
273                     else static if( typeIsJsonObject!( type, true ) )
274                         mixin( "json[\"%s\"] = this.%2$s.isNull ? jnull : this.%2$s.toJson();".format( field, name ) );
275                     else // field is Nullable!T or NullableRef!T
276                     {
277                         alias dest = Unqual!( typeof( __traits( getMember, mixin( "instance." ~ name ), "get" ) ) );
278                         ensureConversion!( dest, name );
279 
280                         static if( !isRef!type )
281                             mixin( "instance.%s = value.isNull ? type.init : type( value.to!dest );".format( name ) );
282                         else
283                         {
284                             dest* ptr = value.isNull ? null : new dest( value.to!dest );
285                             mixin( "instance.%s = value.isNull ? type.init : type( ptr );".format( name ) );
286                         }
287                     }
288                 }
289                 else // field is not nullable
290                 {
291                     if( value.isNull )
292                         throw new JsonException( "cannot convert null to %s".format( type.stringof ) );
293 
294                     static if( typeIsJsonObject!( type, true ) )
295                         mixin( "instanse.%s = type.fromJson( value );".format( name ) );
296                     else
297                     {
298                         ensureConversion!( type, name );
299                         mixin( "instance.%s = value.to!type;".format( name ) );
300                     }
301                 }
302             }
303             catch( JsonException ex )
304             {
305                 throw new JsonException(
306                     "error deserializing field '%s': %s".format( field, ex.msg ),
307                     __FILE__,
308                     __LINE__,
309                     ex
310                 );
311             }
312         }
313 
314         return json;
315     }
316 
317     static typeof( this ) fromJson( S )( S str ) if( isSomeString!S )
318     {
319         auto json = str.parseJson();
320         return fromJson( json );
321     }
322 
323     static typeof( this ) fromJson( JsonValue json )
324     {
325         if( !json.isObject )
326             throw new JsonException( "JSON value is not an object" );
327 
328         alias This = typeof( this );
329         alias reflector = Reflector!This;
330 
331         auto instance = {
332             static if( is( This == struct ) )
333                 return This();
334             else static if( is( This == class ) )
335                 return new This();
336             else
337                 static assert( false, "programmer messed up template constraints" );
338         }();
339 
340         foreach( i, name; reflector.members )
341         {
342             enum field = reflector.fieldName!name;
343             enum required = reflector.getRequirement!name;
344             alias type = typeof( __traits( getMember, instance, name ) );
345 
346             immutable exists = json.hasKey( field );
347 
348             if( !exists && ( required == Required.always || required == Required.allowNull ) )
349                 throw new JsonException( "missing required field '%s'".format( field ) );
350 
351             if( !exists ) continue;
352 
353             auto value = json[field];
354             if( value.isNull && ( required == Required.always || required == Required.disallowNull ) )
355                 throw new JsonException( "value for '%s' cannot be null".format( field ) );
356 
357             try
358             {
359                 type* member = &__traits( getMember, instance, name );
360                 value.deserialize!( field )( member );
361             }
362             catch( JsonException ex )
363             {
364                 throw new JsonException(
365                     "error deserializing field '%s': %s".format( field, ex.msg ),
366                     __FILE__,
367                     __LINE__,
368                     ex
369                 );
370             }
371         }
372 
373         return instance;
374     }
375 }
376 
377 version( unittest )
378 {
379     private class Invalid { }
380 
381     @JsonSerialization( SerializationMode.optIn )
382     private struct Person
383     {
384         private {
385             @JsonProperty( "first_name", Required.always )
386             string _first;
387 
388             @JsonProperty( "last_name", Required.always )
389             string _last;
390 
391             @JsonProperty( "age", Required.always )
392             ubyte _age;
393 
394             @JsonIgnore
395             Invalid _invalid;
396         }
397 
398         string firstName() @property { return _first; }
399         string lastName() @property { return _last; }
400         ubyte age() @property { return _age; }
401 
402         mixin JsonObject;
403     }
404 }
405 
406 unittest
407 {
408     auto text = q{{
409         "first_name": "John",
410         "last_name": "Doe",
411         "age": 35
412     }};
413 
414     auto john = Person.fromJson( text );
415 
416     assert( john.firstName == "John" );
417     assert( john.lastName == "Doe" );
418     assert( john.age == 35 );
419 }