1 module jsonizer.internal.attribute;
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(JsonizeOptional.[yes/no]) to choose whether the parameter is optional
7 struct jsonize {
8   /// alternate name used to identify member in json
9   string key;
10   /// whether member is required during deserialization
11   JsonizeOptional optional = JsonizeOptional.unspecified;
12 
13   /// parameters to @jsonize may be specified in any order
14   /// valid uses of @jsonize include:
15   ///   @jsonize
16   ///   @jsonize("foo")
17   ///   @jsonize(JsonizeOptional.yes)
18   ///   @jsonize("bar", JsonizeOptional.yes)
19   ///   @jsonize(JsonizeOptional.yes, "bar")
20   this(T ...)(T params) {
21     foreach(idx , param ; params) {
22       alias type = T[idx];
23       static if (is(type == JsonizeOptional)) {
24         optional = param;
25       }
26       else static if (is(type : string)) {
27         key = param;
28       }
29       else {
30         assert(0, "invalid @jsonize parameter of type " ~ typeid(type));
31       }
32     }
33   }
34 }
35 
36 /// whether to fail deserialization if field is not found in json
37 enum JsonizeOptional {
38   unspecified, /// optional status not specified (currently defaults to `no`)
39   no,          /// field is required -- fail deserialization if not found in json
40   yes          /// field is optional -- deserialization can continue if field is not found in json
41 }
42 
43 /// Use of `JsonizeOptional`:
44 unittest {
45   import std.json            : parseJSON;
46   import std.exception       : collectException, assertNotThrown;
47   import jsonizer.jsonize    : JsonizeMe;
48   import jsonizer.fromjson   : fromJSON;
49   import jsonizer.exceptions : JsonizeMismatchException;
50   static struct S {
51     mixin JsonizeMe;
52 
53     @jsonize {
54       int i; // i is non-optional (default)
55       @jsonize(JsonizeOptional.yes) {
56         @jsonize("_s") string s; // s is optional
57         @jsonize(JsonizeOptional.no) float f; // f is non-optional (overrides outer attribute)
58       }
59     }
60   }
61 
62   assertNotThrown(`{ "i": 5, "f": 0.2}`.parseJSON.fromJSON!S);
63   auto ex = collectException!JsonizeMismatchException(`{ "i": 5 }`.parseJSON.fromJSON!S);
64 
65   assert(ex !is null, "missing non-optional field 'f' should trigger JsonizeMismatchException");
66   assert(ex.targetType == typeid(S));
67   assert(ex.missingKeys == [ "f" ]);
68   assert(ex.extraKeys == [ ]);
69 }
70 
71 // TODO: use std.typecons : Flag instead? Would likely need to public import.
72 /// Whether to silently ignore json keys that do not map to serialized members.
73 enum JsonizeIgnoreExtraKeys {
74   no, /// silently ignore extra keys in the json object being deserialized
75   yes /// fail if the json object contains a keys that does not map to a serialized field
76 }
77 
78 
79 /// Use of `JsonizeIgnoreExtraKeys`:
80 unittest {
81   import std.json            : parseJSON;
82   import std.exception       : collectException, assertNotThrown;
83   import jsonizer.jsonize    : JsonizeMe;
84   import jsonizer.fromjson   : fromJSON;
85   import jsonizer.exceptions : JsonizeMismatchException;
86 
87   static struct NoCares {
88     mixin JsonizeMe;
89     @jsonize {
90       int i;
91       float f;
92     }
93   }
94 
95   static struct VeryStrict {
96     mixin JsonizeMe!(JsonizeIgnoreExtraKeys.no);
97     @jsonize {
98       int i;
99       float f;
100     }
101   }
102 
103   // no extra fields, neither should throw
104   assertNotThrown(`{ "i": 5, "f": 0.2}`.parseJSON.fromJSON!NoCares);
105   assertNotThrown(`{ "i": 5, "f": 0.2}`.parseJSON.fromJSON!VeryStrict);
106 
107   // extra field "s"
108   // `NoCares` ignores extra keys, so it will not throw
109   assertNotThrown(`{ "i": 5, "f": 0.2, "s": "hi"}`.parseJSON.fromJSON!NoCares);
110   // `VeryStrict` does not ignore extra keys
111   auto ex = collectException!JsonizeMismatchException(
112       `{ "i": 5, "f": 0.2, "s": "hi"}`.parseJSON.fromJSON!VeryStrict);
113 
114   assert(ex !is null, "extra field 's' should trigger JsonizeMismatchException");
115   assert(ex.targetType == typeid(VeryStrict));
116   assert(ex.missingKeys == [ ]);
117   assert(ex.extraKeys == [ "s" ]);
118 }