1 /** User-friendly dump. 2 * 3 * See_Also: https://github.com/dlang/phobos/pull/4318 4 */ 5 module nxt.dump; 6 7 import std.exception : enforce; 8 import std.traits : isCallable, ReturnType; 9 import std.range.primitives : isOutputRange, empty, put; 10 import std.format : formatValue, FormatSpec, FormatException; 11 12 private alias enforceFmt = enforce!FormatException; 13 14 /** 15 Pretty-print variables with name and values. 16 It can print will print all compile-time and runtime arguments. 17 The first runtime argument can be a custom OutputRange. The following argument can 18 be a custom format can be given in the $(LREF format) Syntax, where the first 19 value is the name of the variable (or number for runtime arguments), the second 20 it's value and the third it's type. Positional specifier can be used too. 21 If no OutputRange is given, a `string` will be returned. 22 Callable objects will be evaluated. 23 Params: 24 T = names of the variables to print 25 w = The $(D OutputRange) to write to. 26 fmt = The format of the data to read. 27 xs = runtime arguments to print 28 Returns: 29 `void` if an output range was provided as first runtime argument and a `string` 30 otherwise. 31 */ 32 template dump(T...) 33 { 34 // formats compile-time parameters 35 private void dumpCT(Writer, Char)(Writer w, in Char[] fmt, bool printLastSeparator) 36 if (isOutputRange!(Writer, string)) 37 { 38 alias FPfmt = void delegate(Writer wr) @safe; 39 enum lengthMax = 3; 40 41 foreach (k, el; T) 42 { 43 auto spec = FormatSpec!Char(fmt); 44 45 // we expose three variables: name, value and typo 46 FPfmt[lengthMax] funs; 47 48 // special case for lambdas/alias functions 49 static if (isCallable!el) 50 { 51 funs[0] = (Writer wr) { formatValue(wr, "()", spec); }; 52 funs[1] = (Writer wr) { formatValue(wr, el(), spec); }; 53 funs[2] = (Writer wr) { 54 formatValue(wr, ReturnType!(typeof(el)).stringof, spec); 55 }; 56 } 57 else 58 { 59 funs[0] = (Writer wr) { formatValue(wr, T[k].stringof, spec); }; 60 funs[1] = (Writer wr) { formatValue(wr, el, spec); }; 61 funs[2] = (Writer wr) { 62 formatValue(wr, typeof(el).stringof, spec); 63 }; 64 } 65 66 applySpecToFunctions(w, spec, funs); 67 68 // don't print the trailing bit for the last function except we have 69 // runtime arguments too 70 if (k < T.length - 1 || printLastSeparator) 71 w.put(spec.trailing); 72 } 73 } 74 75 void dump(Writer, Char, Xs...)(Writer w, in Char[] fmt, lazy Xs xs) 76 if (isOutputRange!(Writer, string)) 77 { 78 static if (T.length > 0) 79 dumpCT(w, fmt, Xs.length > 0); 80 81 static if (Xs.length > 0) 82 dumpRuntime(w, fmt, xs); 83 } 84 85 // if no writer passed, return string 86 string dump(Char, Xs...)(in Char[] fmt, lazy Xs xs) 87 { 88 import std.array : appender; 89 90 auto w = appender!string; 91 dump(w, fmt, xs); 92 return w.data; 93 } 94 } 95 96 // formats runtime parameters 97 private void dumpRuntime(Writer, Char, Xs...)(Writer w, in Char[] fmt, lazy Xs xs) 98 if (isOutputRange!(Writer, string)) 99 { 100 import std.format : formatValue, FormatSpec; 101 102 alias FPfmt = void delegate(Writer wr) @safe; 103 enum lengthMax = 3; 104 105 foreach (k, el; xs) 106 { 107 auto spec = FormatSpec!Char(fmt); 108 109 // we expose three variables: name, value and typo 110 FPfmt[lengthMax] funs; 111 112 static if (isCallable!el) 113 { 114 funs[0] = (Writer wr) { formatValue(wr, "()", spec); }; 115 funs[1] = (Writer wr) { formatValue(wr, el(), spec); }; 116 funs[2] = (Writer wr) { 117 formatValue(wr, ReturnType!(typeof(el)).stringof, spec); 118 }; 119 } 120 else 121 { 122 funs[0] = (Writer wr) { formatValue(wr, k, spec); }; 123 funs[1] = (Writer wr) { formatValue(wr, el, spec); }; 124 funs[2] = (Writer wr) { formatValue(wr, typeof(el).stringof, spec); }; 125 } 126 127 applySpecToFunctions(w, spec, funs); 128 129 if (k < Xs.length - 1) 130 w.put(spec.trailing); 131 } 132 } 133 134 // apply given spec with given printing functions 135 private void applySpecToFunctions(Writer, Char, Fun)(Writer w, ref FormatSpec!Char spec, Fun funs) 136 { 137 enum lengthMax = 3; 138 139 uint currentArg = 0; 140 while (spec.writeUpToNextSpecWithoutEnd(w)) 141 { 142 if (currentArg == funs.length && !spec.indexStart) 143 { 144 import std.conv : text; 145 146 // Something went wrong here? 147 enforceFmt(text("Orphan format specifier: %", spec.spec)); 148 break; 149 } 150 if (spec.indexStart > 0) 151 { 152 // positional parameters 153 static if (lengthMax > 0) 154 { 155 foreach (i; spec.indexStart - 1 .. spec.indexEnd) 156 { 157 if (funs.length <= i) 158 break; 159 funs[i](w); 160 } 161 } 162 if (currentArg < spec.indexEnd) 163 currentArg = spec.indexEnd; 164 } 165 else 166 { 167 // parameters in given order 168 funs[currentArg++](w); 169 } 170 } 171 } 172 173 // writes the non-spec parts of a format string except for the trailing end 174 private bool writeUpToNextSpecWithoutEnd(Char, OutputRange)( 175 ref FormatSpec!Char spec, OutputRange writer) 176 { 177 with (spec) 178 { 179 if (trailing.empty) 180 return false; 181 for (size_t i = 0; i < trailing.length; ++i) 182 { 183 if (trailing[i] != '%') 184 continue; 185 put(writer, trailing[0 .. i]); 186 trailing = trailing[i .. $]; 187 enforceFmt(trailing.length >= 2, `Unterminated format specifier: "%"`); 188 trailing = trailing[1 .. $]; 189 190 if (trailing[0] != '%') 191 { 192 // Spec found. Fill up the spec, and bailout 193 /+ TODO: fillUp(); +/ 194 return true; 195 } 196 // Doubled! Reset and Keep going 197 i = 0; 198 } 199 } 200 return false; 201 } 202 203 /// 204 unittest { 205 int x = 5, y = 3; 206 import std.stdio; 207 208 assert(dump!(x, y)("%s = %s, ") == "x = 5, y = 3"); 209 210 // with order 211 assert(dump!(x, y)("%2$s = %1$s, ") == "5 = x, 3 = y"); 212 213 // with runtime args 214 assert(dump!(x, y)("%s = %s, ", () => 42) == "x = 5, y = 3, () = 42"); 215 216 // with runtime args & position-specifier 217 assert(dump!(x, y)("%1$s = %2$s; ", "var1") == "x = 5; y = 3; 0 = var1"); 218 219 // with types 220 assert(dump!(x, y)("(%s: %3$s) = %2$s, ") == "(x: int) = 5, (y: int) = 3"); 221 assert(dump!(x, y)("(%s!%3$s) = %2$s, ") == "(x!int) = 5, (y!int) = 3"); 222 223 // custom separator 224 assert(dump!(x, y)("%s = %s; ") == "x = 5; y = 3"); 225 226 // all printf formatting works 227 assert(dump!(x, y)("%-4s = %4s, ") == "x = 5, y = 3"); 228 229 // special formatting (if applicable for all types) 230 auto z1 = 2.0, z2 = 4.0; 231 assert(dump!(z1, z2)("%s = %.3f & ") == "z1 = 2.000 & z2 = 4.000"); 232 233 // functions 234 assert(dump!(x, y, () => x + y)("%s = %s; ") == "x = 5; y = 3; () = 8"); 235 236 // runtime paramters 237 auto b = (int a) => ++a; 238 assert(dump!(x, y)("%s = %s, ", b(x), x - y) == "x = 5, y = 3, 0 = 6, 1 = 2"); 239 240 // validate laziness 241 auto c = (ref int a) => ++a; 242 assert(dump!(x, y, () => x + y)("%s = %s, ", c(x), x - y) == "x = 5, y = 3, () = 8, 0 = 6, 1 = 3"); 243 assert(dump!(x, y, () => x + y)("%s = %s, ", c(x), x - y) == "x = 6, y = 3, () = 9, 0 = 7, 1 = 4"); 244 } 245 246 // test with output range 247 unittest { 248 import std.array : appender; 249 250 auto x = 2; 251 long y = 4; 252 auto w = appender!string; 253 dump!(x, y)(w, "%s = %s, "); 254 assert(w.data == "x = 2, y = 4"); 255 256 import std.stdio : stdout; 257 258 if (false) 259 dump!(x, y)(stdout.lockingTextWriter(), "%s = %s, "); 260 }