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 }