1 module jsonizer.common;
2 
3 /// use @jsonize to mark members to be (de)serialized from/to json
4 /// use @jsonize to mark a single contructor to use when creating an object using extract
5 /// use @jsonize("name") to make a member use the json key "name"
6 /// use @jsonize(Jsonize.[yes/opt]) to choose whether the parameter is optional
7 /// use @jsonize(JsonizeIn.[yes/opt/no]) to choose whether the parameter is optional for deserialization
8 /// use @jsonize(JsonizeOut.[yes/opt/no]) to choose whether the parameter is optional for serialization
9 struct jsonize {
10   /// alternate name used to identify member in json
11   string key;
12 
13   /// whether member is required during deserialization
14   JsonizeIn perform_in = JsonizeIn.unspecified;
15   /// whether serialized member
16   JsonizeOut perform_out = JsonizeOut.unspecified;
17 
18   /// parameters to @jsonize may be specified in any order
19   /// valid uses of @jsonize include:
20   ///   @jsonize
21   ///   @jsonize("foo")
22   ///   @jsonize(Jsonize.optional)
23   ///   @jsonize("bar", Jsonize.optional)
24   ///   @jsonize(Jsonize.optional, "bar")
25   this(T ...)(T params) {
26     foreach(idx , param ; params) {
27       alias type = T[idx];
28       static if (is(type == Jsonize)) {
29         perform_in = cast(JsonizeIn)param;
30         perform_out = cast(JsonizeOut)param;
31       }
32       else static if (is(type == JsonizeIn)) {
33         perform_in = param;
34       }
35       else static if (is(type == JsonizeOut)) {
36         perform_out = param;
37       }
38       else static if (is(type : string)) {
39         key = param;
40       }
41       else {
42         assert(0, "invalid @jsonize parameter of type " ~ typeid(type));
43       }
44     }
45   }
46 }
47 
48 /// Control the strictness with which a field is deserialized
49 enum JsonizeIn
50 {
51   /// The default. Equivalent to `yes` unless overridden by another UDA.
52   unspecified = 0,
53   /// always deserialize this field, fail if it is not present
54   yes = 1,
55   /// deserialize if found, but continue without error if it is missing
56   opt = 2,
57   /// never deserialize this field
58   no = 3
59 }
60 
61 /// Control the strictness with which a field is serialized
62 enum JsonizeOut
63 {
64   /// the default value -- equivalent to `yes`
65   unspecified = 0,
66   /// always serialize this field
67   yes = 1,
68   /// serialize only if it not equal to the initial value of the type
69   opt = 2,
70   /// never serialize this field
71   no = 3
72 }
73 
74 /// Shortcut for setting both `JsonizeIn` and `JsonizeOut`
75 enum Jsonize
76 {
77   /// equivalent to JsonizeIn.yes, JsonizeOut.yes
78   yes = 1,
79   /// equivalent to  JsonizeIn.opt, JsonizeOut.opt
80   opt = 2
81 }
82 
83 /// Use of `Jsonize(In,Out)`:
84 unittest {
85   import std.json            : parseJSON;
86   import std.exception       : collectException, assertNotThrown;
87   import jsonizer.jsonize    : JsonizeMe;
88   import jsonizer.fromjson   : fromJSON;
89   import jsonizer.exceptions : JsonizeMismatchException;
90   static struct S {
91     mixin JsonizeMe;
92 
93     @jsonize {
94       int i; // i is non-opt (default)
95       @jsonize(Jsonize.opt) {
96         @jsonize("_s") string s; // s is optional
97         @jsonize(Jsonize.yes) float f; // f is non-optional (overrides outer attribute)
98       }
99     }
100   }
101 
102   assertNotThrown(`{ "i": 5, "f": 0.2}`.parseJSON.fromJSON!S);
103   auto ex = collectException!JsonizeMismatchException(`{ "i": 5 }`.parseJSON.fromJSON!S);
104 
105   assert(ex !is null, "missing non-optional field 'f' should trigger JsonizeMismatchException");
106   assert(ex.targetType == typeid(S));
107   assert(ex.missingKeys == [ "f" ]);
108   assert(ex.extraKeys == [ ]);
109 }
110 
111 /// Whether to silently ignore json keys that do not map to serialized members.
112 enum JsonizeIgnoreExtraKeys {
113   no, /// silently ignore extra keys in the json object being deserialized
114   yes /// fail if the json object contains a keys that does not map to a serialized field
115 }
116 
117 /// Use of `JsonizeIgnoreExtraKeys`:
118 unittest {
119   import std.json            : parseJSON;
120   import std.exception       : collectException, assertNotThrown;
121   import jsonizer.jsonize    : JsonizeMe;
122   import jsonizer.fromjson   : fromJSON;
123   import jsonizer.exceptions : JsonizeMismatchException;
124 
125   static struct NoCares {
126     mixin JsonizeMe;
127     @jsonize {
128       int i;
129       float f;
130     }
131   }
132 
133   static struct VeryStrict {
134     mixin JsonizeMe!(JsonizeIgnoreExtraKeys.no);
135     @jsonize {
136       int i;
137       float f;
138     }
139   }
140 
141   // no extra fields, neither should throw
142   assertNotThrown(`{ "i": 5, "f": 0.2}`.parseJSON.fromJSON!NoCares);
143   assertNotThrown(`{ "i": 5, "f": 0.2}`.parseJSON.fromJSON!VeryStrict);
144 
145   // extra field "s"
146   // `NoCares` ignores extra keys, so it will not throw
147   assertNotThrown(`{ "i": 5, "f": 0.2, "s": "hi"}`.parseJSON.fromJSON!NoCares);
148   // `VeryStrict` does not ignore extra keys
149   auto ex = collectException!JsonizeMismatchException(
150       `{ "i": 5, "f": 0.2, "s": "hi"}`.parseJSON.fromJSON!VeryStrict);
151 
152   assert(ex !is null, "extra field 's' should trigger JsonizeMismatchException");
153   assert(ex.targetType == typeid(VeryStrict));
154   assert(ex.missingKeys == [ ]);
155   assert(ex.extraKeys == [ "s" ]);
156 }
157 
158 /// Customize the behavior of `toJSON` and `fromJSON`.
159 struct JsonizeOptions {
160   /**
161    * A default-constructed `JsonizeOptions`.
162    * Used implicilty if no explicit options are given to `fromJSON` or `toJSON`.
163    */
164   static immutable defaults = JsonizeOptions.init;
165 
166   /**
167    * The key of a field identifying the D type of a json object.
168    *
169    * If this key is found in the json object, `fromJSON` will try to factory
170    * construct an object of the type identified.
171    *
172    * This is useful when deserializing a collection of some type `T`, where the
173    * actual instances may be different subtypes of `T`.
174    *
175    * Setting `classKey` to null will disable factory construction.
176    */
177   string classKey = "class";
178 
179   /**
180    * A function to attempt identifier remapping from the name found under `classKey`.
181    *
182    * If this function is provided, then when the `classKey` is found, this function
183    * will attempt to remap the value.  This function should return either the fully
184    * qualified class name or null.  Returned non-null values indicate that the
185    * remapping has succeeded.  A null value will indicate the mapping has failed
186    * and the original value will be used in the object factory.
187    *
188    * This is particularly useful when input JSON has not originated from D.
189    */
190   string delegate(string) classMap;
191 }
192 
193 package:
194 // Get the json key corresponding to  `T.member`.
195 template jsonKey(T, string member) {
196     alias attrs = T._getUDAs!(member, jsonize);
197     static if (!attrs.length)
198       enum jsonKey = member;
199     else static if (attrs[$ - 1].key)
200       enum jsonKey = attrs[$ - 1].key;
201     else
202       enum jsonKey = member;
203 }