1 /**
2     An expected result type
3 
4     Useful for functions that are expected to return something but could result in an error. The expected type is parameterized over
5     two types - the expected one and the `Unexpected`. When you want to assign an unexpected type you must use the provided type constructor
6     to make an unexpected assignment.
7 
8     An `Expect!(T, U)` type also has a static `expected` and `unexpected` methods to create the given `Expect!(U, V)` with the desired
9     state.
10 */
11 module ddash.utils.expect;
12 
13 ///
14 @nogc unittest {
15     Expect!(int, string) even(int i) @nogc {
16         if (i % 2 == 0) {
17             return typeof(return).expected(i);
18         } else {
19             return typeof(return).unexpected("not even");
20         }
21     }
22 
23     assert(even(1) == unexpected("not even"));
24     assert(even(2) == 2);
25 }
26 
27 import std.variant;
28 import ddash.common;
29 
30 struct AnyUnexpected {}
31 
32 /**
33     Can be used to compare for any unexpected, i.e. `Expected<U, V> == anyUnexpected`
34 */
35 immutable anyUnexpected = AnyUnexpected();
36 
37 /**
38     Used in the `Expect` type to denote an unexpected value
39 */
40 private struct Unexpected(E) if (!is(E == AnyUnexpected)) {
41     E value = E.init;
42     alias value this;
43 }
44 
45 /**
46     Type constructor for an Unexpected value. This must be used when assigning or passing
47     an unexpected type to an `Expect!(T, U)`
48 */
49 auto unexpected(E)(E value) {
50     return Unexpected!E(value);
51 }
52 
53 /**
54     The expect type can be used to return values and error codes from functions
55 
56     The "expected" value is the success value and the unexpected (which is also typed
57     as `Unexpected`) is the failure value.
58 
59     The default value is always the init value of the expected case
60 */
61 struct Expect(T, E = Variant) if (!is(E == void)) {
62     import sumtype;
63     import ddash.lang: Void, isVoid;
64 
65     static if (isVoid!T) {
66         alias Expected = Void;
67     } else {
68         alias Expected = T;
69     }
70     alias Unexpected = .Unexpected!E;
71 
72     private SumType!(Expected, Unexpected) data = Expected.init;
73     ref get() { return data; }
74     alias get this;
75 
76     /**
77         Constructor takes a Expected and creates a success result. Or takes an E and
78         creates an unexpected result
79     */
80     static if (!isVoid!Expected) {
81         this(Expected value) {
82             data = value;
83         }
84     }
85 
86     /// Ditto
87     this(Unexpected value)  {
88         data = value;
89     }
90 
91     /**
92         Pass in 2 handlers, one that handles `Expected`` and another that
93         handles `Unexpected`
94     */
95     auto match(handlers...)() {
96         return data.match!handlers;
97     }
98 
99     /// Create an `Expect` with an expected value
100     static expected(V : Expected)(auto ref V value) {
101         return Expect!(Expected, E)(value);
102     }
103 
104     /// Create an `Expect` with an unexpected value
105     static unexpected(V)(auto ref V value) if (is(E == Variant) || is(V : E)) {
106         // If E is a variant type than then we allow any V and just store it as a variant
107         static if (is(E == Variant) && !is(V == Variant)) {
108             return Expect!(Expected, E)(Unexpected(Variant(value)));
109         } else {
110             return Expect!(Expected, E)(Unexpected(value));
111         }
112     }
113 
114     /// Returns true if the value is expected
115     bool isExpected() const {
116         return data.match!(
117             (const Expected _) => true,
118             (const Unexpected _) => false,
119         );
120     }
121 
122     /**
123         compares a value or an unexpected value. To compare with an unepexted value
124         you must used either `Unexpected!E` as the rhs or it's type contructor.
125 
126         If you do not care about the value of the unexpected then you can compare
127         against `anyUnexpected`
128     */
129     bool opEquals(Expected rhs) const {
130         if (!isExpected) return false;
131         return data.match!(
132             (const Expected lhs) => lhs,
133             (const Unexpected _) => Expected.init,
134         ) == rhs;
135     }
136 
137     /// Ditto
138     bool opEquals(Unexpected rhs) const {
139         if (isExpected) return false;
140         return data.match!(
141             (const Expected _) => Unexpected.init,
142             (const Unexpected lhs) => lhs,
143         ) == rhs;
144     }
145 
146     /// Ditto
147     bool opEquals(AnyUnexpected) const {
148         return !isExpected;
149     }
150 
151     /// Calls std.conv.to!string on T or E
152     string toString() {
153         import std.conv: to;
154         return data.match!(
155             (ref Expected value) => value.to!string,
156             (ref Unexpected value) => value.to!string,
157         );
158     }
159 }
160 
161 ///
162 unittest {
163     Expect!int toInt(string str) {
164         alias Result = typeof(return);
165         import std.conv: to;
166         try {
167             return Result.expected(str.to!int);
168         } catch (Exception ex) {
169             return Result.unexpected(ex.msg);
170         }
171     }
172 
173     assert(toInt("33") == 33);
174     assert(toInt("!33") == anyUnexpected);
175 }
176