1 /**
2   * Defines the exceptions that Jsonizer may throw.
3   *
4   * Authors: <a href="https://github.com/rcorre">rcorre</a>
5   * License: <a href="http://opensource.org/licenses/MIT">MIT</a>
6   * Copyright: Copyright © 2015, rcorre
7   * Date: 3/24/15
8   */
9 module jsonizer.exceptions;
10 
11 import std.json      : JSONValue, JSON_TYPE;
12 import std..string    : format, join;
13 import std.traits    : ParameterTypeTuple, ParameterIdentifierTuple;
14 import std.typecons  : staticIota;
15 import std.typetuple : staticMap;
16 
17 /// Base class of any exception thrown by `jsonizer`.
18 class JsonizeException : Exception {
19   this(string msg) {
20     super(msg);
21   }
22 }
23 
24 /// Thrown when `fromJSON` cannot convert a `JSONValue` into the requested type.
25 class JsonizeTypeException : Exception {
26   private enum fmt =
27     "fromJSON!%s expected json type to be one of %s but got json type %s. json input: %s";
28 
29   const {
30     TypeInfo targetType;  /// Type jsonizer was attempting to deserialize to.
31     JSONValue json;       /// The json value that was being deserialized
32     JSON_TYPE[] expected; /// The JSON_TYPEs that would have been acceptable
33   }
34 
35   this(TypeInfo targetType, JSONValue json, JSON_TYPE[] expected ...) {
36     super(fmt.format(targetType, expected, json.type, json));
37 
38     this.targetType = targetType;
39     this.json       = json;
40     this.expected   = expected;
41   }
42 }
43 
44 unittest {
45   import std.algorithm : canFind;
46 
47   auto json       = JSONValue(4.2f);
48   auto targetType = typeid(bool);
49   auto expected   = [JSON_TYPE.TRUE, JSON_TYPE.FALSE];
50 
51   auto e = new JsonizeTypeException(targetType, json, JSON_TYPE.TRUE, JSON_TYPE.FALSE);
52 
53   assert(e.json == json);
54   assert(e.targetType == targetType);
55   assert(e.expected == expected);
56   assert(e.msg.canFind("fromJSON!bool"),
57       "JsonizeTypeException should report type argument");
58   assert(e.msg.canFind("TRUE") && e.msg.canFind("FALSE"),
59       "JsonizeTypeException should report all acceptable json types");
60 }
61 
62 /// Thrown when the keys of a json object fail to match up with the members of the target type.
63 class JsonizeMismatchException : JsonizeException {
64   private enum fmt =
65     "Failed to deserialize %s.\n" ~
66     "Missing non-optional members: %s.\n" ~
67     "Extra keys in json: %s.\n";
68 
69   const {
70     TypeInfo targetType;  /// Type jsonizer was attempting to deserialize to.
71     string[] extraKeys;   /// keys present in json that do not match up to a member.
72     string[] missingKeys; /// non-optional members that were not found in the json.
73   }
74 
75   this(TypeInfo targetType, string[] extraKeys, string[] missingKeys) {
76     super(fmt.format(targetType, missingKeys, extraKeys));
77 
78     this.targetType  = targetType;
79     this.extraKeys   = extraKeys;
80     this.missingKeys = missingKeys;
81   }
82 }
83 
84 unittest {
85   import std.algorithm : all, canFind;
86   import std.conv : to;
87 
88   static class MyClass { }
89 
90   auto targetType  = typeid(MyClass);
91   auto extraKeys   = [ "who", "what" ];
92   auto missingKeys = [ "where" ];
93 
94   auto e = new JsonizeMismatchException(targetType, extraKeys, missingKeys);
95   assert(e.targetType == targetType);
96   assert(e.extraKeys == extraKeys);
97   assert(e.missingKeys == missingKeys);
98   assert(e.msg.canFind(targetType.to!string),
99       "JsonizeMismatchException should report type argument");
100   assert(extraKeys.all!(x => e.msg.canFind(x)),
101       "JsonizeTypeException should report all extra keys");
102   assert(missingKeys.all!(x => e.msg.canFind(x)),
103       "JsonizeTypeException should report all missing keys");
104 }
105 
106 /// Thrown when a type has no default constructor and the custom constructor cannot be fulfilled.
107 class JsonizeConstructorException : JsonizeException {
108   private enum fmt =
109     "%s has no default constructor, and none of the following constructors could be fulfilled: \n" ~
110     "%s\n" ~
111     "json object:\n %s";
112 
113   const {
114     TypeInfo targetType; /// Tye type jsonizer was attempting to deserialize to.
115     JSONValue json;      /// The json value that was being deserialized
116   }
117 
118   /// Construct and throw a `JsonizeConstructorException`
119   /// Params:
120   ///   T = Type being deserialized
121   ///   Ctors = constructors that were attempted
122   ///   json = json object being deserialized
123   static void doThrow(T, Ctors ...)(JSONValue json) {
124     static if (Ctors.length > 0) {
125       auto signatures = [staticMap!(ctorSignature, Ctors)].join("\n");
126     }
127     else {
128       auto signatures = "<no @jsonized constructors>";
129     }
130 
131     throw new JsonizeConstructorException(typeid(T), signatures, json);
132   }
133 
134   private this(TypeInfo targetType, string ctorSignatures, JSONValue json) {
135     super(fmt.format(targetType, ctorSignatures, json));
136 
137     this.targetType  = targetType;
138     this.json = json;
139   }
140 }
141 
142 private:
143 // Represent the function signature of a constructor as a string.
144 template ctorSignature(alias ctor) {
145   alias params = ParameterIdentifierTuple!ctor;
146   alias types  = ParameterTypeTuple!ctor;
147 
148   // build a string "type1 param1, type2 param2, ..., typeN paramN"
149   static string paramString() {
150     string s = "";
151 
152     foreach(i ; staticIota!(0, params.length)) {
153       s ~= types[i].stringof ~ " " ~ params[i];
154 
155       static if (i < params.length - 1) {
156         s ~= ", ";
157       }
158     }
159 
160     return s;
161   }
162 
163   enum ctorSignature = "this(%s)".format(paramString);
164 }
165 
166 unittest {
167   static class Foo {
168     this(string s, int i, float f) { }
169   }
170 
171   assert(ctorSignature!(Foo.__ctor) == "this(string s, int i, float f)");
172 }