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