1 module dshould.ShouldType;
2 
3 import std.algorithm : map;
4 import std.format : format;
5 import std.range : iota;
6 import std..string : empty, join;
7 import std.traits : TemplateArgsOf;
8 public import std.traits : isInstanceOf;
9 import std.typecons : Tuple;
10 
11 public auto should(T)(lazy T lhs) pure
12 {
13     const delegate_ = { return lhs; };
14 
15     auto should = ShouldType!().init;
16     should.refs = new int;
17     *should.refs = 1;
18 
19     return should.addData!"lhs"(delegate_);
20 }
21 
22 public struct ShouldType(Data = Tuple!(), string[] Words = [])
23 {
24     import std.algorithm : canFind;
25 
26     public Data data;
27 
28     private int* refs = null;
29 
30     public auto addWord(string Word)()
31     {
32         return ShouldType!(Data, Words ~ Word)(this.data, this.refs);
33     }
34 
35     @disable this();
36 
37     this(Data data, int* refs) @trusted
38     in
39     {
40         assert(*refs != CHAIN_TERMINATED, "don't copy Should that's been terminated");
41     }
42     body
43     {
44         this.data = data;
45         this.refs = refs;
46         addref;
47     }
48 
49     this(this) @trusted
50     {
51         addref;
52     }
53 
54     ~this() @trusted
55     {
56         delref;
57         assert(*this.refs > 0, "unterminated should-chain!");
58     }
59 
60     public enum gencheck(string TestString, string[] ArgNames) = gencheckImpl(TestString, ArgNames);
61 
62     private static string gencheckImpl(string testString, string[] argNames)
63     {
64         auto args = argNames.join(", ");
65         auto argStrings = argNames.length.iota.map!(i => format!`"%s"`(argNames[i])).join(", ");
66 
67         return format!q{{
68             import std.format : format;
69 
70             with (data)
71             {
72                 check(mixin(format!q{%s}(%s)), format(": %s", %s), file, line);
73             }
74         }}(testString, argStrings, testString, args);
75     }
76 
77     public void check(bool condition, lazy string msg, string file, size_t line) pure @safe
78     {
79         terminateChain;
80 
81         if (!condition)
82         {
83             throw new FluentException("test failed", msg, file, line);
84         }
85     }
86 
87     public void allowOnlyWordsBefore(string[] AllowedWords, string NewWord)()
88     {
89         static foreach (Word; Words)
90         {
91             static assert(AllowedWords.canFind(Word), `bad grammar: "` ~ Word ~ ` ` ~ NewWord ~ `"`);
92         }
93     }
94 
95     public void terminateChain()
96     {
97         *this.refs = CHAIN_TERMINATED; // terminate chain, safe ref checker
98     }
99 
100     public void requireWord(string RequiredWord, string NewWord)()
101     {
102         static assert(
103             Words.canFind(RequiredWord),
104             `bad grammar: expected "` ~ RequiredWord ~ `" before "` ~ NewWord ~ `"`
105         );
106     }
107 
108     public enum hasWord(string Word) = Words.canFind(Word);
109 
110     public template addData(string Name)
111     {
112         auto addData(T)(T value)
113         {
114             alias NewTuple = Tuple!(TemplateArgsOf!Data, T, Name);
115 
116             return ShouldType!(NewTuple, Words)(NewTuple(this.data.tupleof, value), this.refs);
117         }
118     }
119 
120     private void addref()
121     in
122     {
123         assert(*this.refs != CHAIN_TERMINATED);
124     }
125     body
126     {
127         *this.refs = *this.refs + 1;
128     }
129 
130     private void delref()
131     {
132         *this.refs = *this.refs - 1;
133     }
134 
135     // work around https://issues.dlang.org/show_bug.cgi?id=18839
136     public auto empty()() { return this.empty_; }
137 
138     private enum CHAIN_TERMINATED = int.max;
139 }
140 
141 // must be here due to https://issues.dlang.org/show_bug.cgi?id=18839
142 void empty_(Should)(Should should, string file = __FILE__, size_t line = __LINE__)
143 if (isInstanceOf!(ShouldType, Should))
144 {
145     import std.range : empty;
146 
147     should.allowOnlyWordsBefore!(["be", "not"], "empty");
148     should.requireWord!("be", "empty");
149 
150     with (should)
151     {
152         terminateChain;
153 
154         auto lhs = data.lhs();
155 
156         static if (hasWord!"not")
157         {
158             check(!lhs.empty, `: expected nonempty array`, file, line);
159         }
160         else
161         {
162             check(lhs.empty, format(": expected empty array, but got %s", lhs), file, line);
163         }
164     }
165 }
166 
167 class FluentException : Exception
168 {
169     private const string leftPart = null; // before reason
170     public const string reason = null;
171     private const string rightPart = null; // after reason
172 
173     public this(string leftPart, string rightPart, string file, size_t line) pure @safe
174     {
175         this.leftPart = leftPart;
176         this.rightPart = rightPart;
177 
178         super(genMessage, file, line);
179     }
180 
181     public this(string leftPart, string reason, string rightPart, string file, size_t line) pure @safe
182     {
183         this.leftPart = leftPart;
184         this.reason = reason;
185         this.rightPart = rightPart;
186 
187         super(genMessage, file, line);
188     }
189 
190     public this(string msg, string file, size_t line) pure @safe
191     {
192         this.leftPart = msg;
193 
194         super(genMessage, file, line);
195     }
196 
197     public FluentException because(string reason) pure @safe
198     {
199         return new FluentException(this.leftPart, reason, this.rightPart, this.file, this.line);
200     }
201 
202     private string genMessage() pure @safe
203     {
204         string message = "";
205 
206         if (!this.leftPart.empty)
207         {
208             message = this.leftPart;
209         }
210 
211         if (!this.reason.empty)
212         {
213             message ~= message.empty ? "" : " ";
214             message ~= format!`because %s`(this.reason);
215         }
216 
217         if (!this.rightPart.empty)
218         {
219             message ~= this.rightPart;
220         }
221 
222         return message;
223     }
224 }