1 /**
2  *          __                        __                  __     __
3  *         / /                       /_/ _____            \ \   /_/
4  *   _____/ / _____     __   ___    __  / ___ \  _____    / /  __  __  ___  _____
5  *  / ___  / / ___ \    \ \_/__/   / / / _____/ / ___ \  / /  / / / /_/  / / ___ \
6  * / /__/ / / /__/  \   / _/      / / / /__/ / / _____/ / /  / / / _/ / / / /__/ /
7  * \_____/  \_____/\_\ /_/  __   / /  \_____/ / /__/ /  \_\ /_/ /_/  /_/ _\___  /
8  *                         / /__/ /           \_____/                   / /__/ /
9  *                         \_____/                                      \_____/
10  *
11  * A module for results each of which is a value or an exception
12  *
13  * Copyright: (C) Kazuhiro Matsushima 2016.
14  * License:   <a href="http://www.boost.org/LICENSE_1_0.txt">Boost License 1.0</a>.
15  * Authors:   Kazuhiro Matsushima
16  */
17 module darjeeling.trial;
18 
19 import std.format : format;
20 import std.traits : fullyQualifiedName;
21 import darjeeling.either: Either;
22 import darjeeling.assertion;
23 
24 /**
25  * A interface to represent whether a value or an exception.
26  *
27  * Example:
28  * -----
29  * auto trial = Trial!int.trying({ return 9; });
30  * if (trial.isSuccess)
31  * {
32  *     writeln(trial.get());
33  * }
34  * -----
35  * Output:
36  * -----
37  * 9
38  * -----
39  */
40 interface Trial(T)
41 {
42     /**
43      * Return true if $(D this) is the Success!T instance, otherwise false.
44      */
45     @property bool isSuccess() nothrow pure @safe;
46 
47     /**
48      * Return true if $(D this) is the Failure!T instance, otherwise false.
49      */
50     @property bool isFailure() nothrow pure @safe;
51 
52     /**
53      * Return $(D value) if $(D this) is the Success!T instance,
54      * otherwise throw Exception in the Failure!T instance.
55      */
56     T get() pure @safe;
57 
58     /**
59      * Return Right!(Exception, T) value if $(D this) is the Success!T instance,
60      * otherwise Left!(Exception, T) value.
61      */
62     Either!(Exception, T) getOrLeft() nothrow pure @safe;
63 
64     /**
65      * A factory method to create Trial(T) instance.
66      *
67      * Params:
68      *  expr = a function block to return T value, and it may also raise exception.
69      *
70      * Returns:
71      *  A Success!T instance if (D_PARAM expr) is evaluated successfully,
72      *  a Failure!T instance if (D_PARAM expr) raises an exception(not an error),
73      *  otherwise raise an error.
74      */
75     static Trial!T trying(T function() expr) nothrow
76     {
77         try
78         {
79             auto value = expr();
80             return new Success!T(value);
81         }
82         catch (Exception ex)
83         {
84             return new Failure!T(ex);
85         }
86     }
87 }
88 
89 final class Success(T) : Trial!T
90 {
91     private T value;
92 
93     package this(T value)
94     {
95         this.value = value;
96     }
97 
98     @property bool isSuccess() nothrow pure @safe
99     {
100         return true;
101     }
102 
103     unittest
104     {
105         auto success = new Success!int(9);
106         assert(success.isSuccess);
107     }
108 
109     @property bool isFailure() nothrow pure @safe
110     {
111         return !this.isSuccess;
112     }
113 
114     unittest
115     {
116         auto success = new Success!string("success");
117         assert(!success.isFailure);
118     }
119 
120     T get() pure @safe
121     {
122         return this.value;
123     }
124 
125     unittest
126     {
127         auto expected = 3.14;
128         auto success = new Success!double(expected);
129         auto actual = success.get();
130         assertEquals(expected, actual);
131     }
132 
133     Either!(Exception, T) getOrLeft() nothrow pure @safe
134     {
135         return Either!(Exception, T).right(this.value);
136     }
137 
138     unittest
139     {
140         auto expected = 3.14;
141         auto success = new Success!double(expected);
142         auto actual = success.getOrLeft();
143         assert(actual.isRight);
144         assertEquals(expected, actual.right());
145     }
146 
147     override string toString()
148     {
149         return format("Success!%s(%s)", fullyQualifiedName!T, this.value);
150     }
151 
152     override bool opEquals(Object o)
153     {
154         if (typeid(o) == typeid(this))
155         {
156             auto other = cast(Success!T)(o);
157             return this.value == other.value;
158         }
159         else
160         {
161             return false;
162         }
163     }
164 }
165 
166 final class Failure(T): Trial!T
167 {
168     private Exception exception;
169 
170     package this(Exception exception)
171     {
172         this.exception = exception;
173     }
174 
175     @property bool isSuccess() nothrow pure @safe
176     {
177         return !this.isFailure;
178     }
179 
180     unittest
181     {
182         auto failure = new Failure!int(new Exception("10"));
183         assert(!failure.isSuccess);
184     }
185 
186     @property bool isFailure() nothrow pure @safe
187     {
188         return true;
189     }
190 
191     unittest
192     {
193         auto failure = new Failure!string(new Exception("failure"));
194         assert(failure.isFailure);
195     }
196 
197     T get() pure @safe
198     {
199         throw this.exception;
200     }
201 
202     unittest
203     {
204         auto expected = new Exception("2.718");
205         auto failure = new Failure!double(expected);
206         auto actual = trap!(Failure!double, double)(failure, function(Failure!double fl) => fl.get());
207         assertEquals(typeid(expected), typeid(actual));
208         assertEquals(expected.msg, actual.msg);
209     }
210 
211     Either!(Exception, T) getOrLeft() nothrow pure @safe
212     {
213         return Either!(Exception, T).left(this.exception);
214     }
215 
216     unittest
217     {
218         auto expected = new Exception("2.718");
219         auto failure = new Failure!double(expected);
220         auto actual = failure.getOrLeft();
221         assert(actual.isLeft);
222         auto ex = actual.left();
223         assertEquals(typeid(expected), typeid(ex));
224         assertEquals(expected.msg, ex.msg);
225     }
226 
227     override string toString()
228     {
229         return format("Failure!%s(%s(%s))", fullyQualifiedName!T, typeid(this.exception), this.exception.msg);
230     }
231 
232     override bool opEquals(Object o)
233     {
234         if (typeid(o) == typeid(this))
235         {
236             auto other = cast(Failure!T)(o);
237             return (typeid(this.exception) == typeid(other.exception))
238                 && (this.exception.msg == other.exception.msg);
239         }
240         else
241         {
242             return false;
243         }
244     }
245 }
246 
247 unittest
248 {
249     // Success(T)
250     {
251         Trial!int trial = Trial!int.trying(function int() { return -12; });
252         assert(trial.isSuccess);
253         assert(!trial.isFailure);
254         auto success = trial.getOrLeft();
255         assert(success.isRight);
256         auto actual = success.right();
257         assertEquals(-12, actual);
258     }
259     // Failure(T)
260     {
261         {
262             Trial!int trial = Trial!int.trying(function int() { throw new Exception("hoge"); });
263             assert(!trial.isSuccess);
264             assert(trial.isFailure);
265             auto success = trial.getOrLeft();
266             assert(success.isLeft);
267             auto actual = success.left();
268             assertEquals(typeid(Exception), typeid(actual));
269             assertEquals("hoge", actual.msg);
270         }
271         {
272             auto actual = trap!(Trial!int)(function Trial!int(){
273                 return Trial!int.trying({
274                     auto x = 1;
275                     if (x > 0) throw new Error("It's error!");
276                     return 1;
277                 });
278             });
279             auto expected = new Error("It's error!");
280             assertEquals(typeid(expected), typeid(actual));
281             assertEquals(expected.msg, actual.msg);
282         }
283     }
284     // equality
285     {
286         {
287             auto trial1 = Trial!int.trying({ return 0; });
288             auto trial2 = Trial!int.trying({ return 0; });
289             assertEquals(trial1, trial2);
290         }
291         {
292             auto trial1 = Trial!int.trying({ return 1; });
293             auto trial2 = Trial!int.trying({ return -1; });
294             assertNotEquals(trial1, trial2);
295         }
296         {
297             auto trial1 = Trial!short.trying(function short() { return 0; });
298             auto trial2 = Trial!long.trying(function long() { return 0L; });
299             assertNotObjEquals(trial1, trial2);
300         }
301         {
302             auto trial1 = Trial!int.trying(function int() { throw new Exception("fail"); });
303             auto trial2 = Trial!int.trying(function int() { throw new Exception("fail"); });
304             assertEquals(trial1, trial2);
305         }
306         {
307             auto trial1 = Trial!int.trying(function int() { throw new Exception("fail"); });
308             auto trial2 = Trial!int.trying(function int() { throw new Exception("Fail"); });
309             assertNotEquals(trial1, trial2);
310         }
311         {
312             import core.exception : UnicodeException;
313 
314             auto trial1 = Trial!int.trying(function int() { throw new Exception("fail"); });
315             auto trial2 = Trial!int.trying(function int() { throw new UnicodeException("fail", 0); });
316             assertNotEquals(trial1, trial2);
317         }
318         {
319             auto trial1 = Trial!string.trying(function string() { throw new Exception("fail"); });
320             auto trial2 = Trial!double.trying(function double() { throw new Exception("fail"); });
321             assertNotObjEquals(trial1, trial2);
322         }
323         {
324             auto trial1 = Trial!int.trying({ return 0; });
325             auto trial2 = Trial!int.trying({
326                 auto x = 1;
327                 if (x > 0) throw new Exception("positive");
328                 return x;
329             });
330             assertNotEquals(trial1, trial2);
331         }
332     }
333 }