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 maxLength = 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[maxLength] 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 maxLength = 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[maxLength] 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 maxLength = 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 (maxLength > 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 {
206     int x = 5, y = 3;
207     import std.stdio;
208 
209     assert(dump!(x, y)("%s = %s, ") == "x = 5, y = 3");
210 
211     // with order
212     assert(dump!(x, y)("%2$s = %1$s, ") == "5 = x, 3 = y");
213 
214     // with runtime args
215     assert(dump!(x, y)("%s = %s, ", () => 42) == "x = 5, y = 3, () = 42");
216 
217     // with runtime args & position-specifier
218     assert(dump!(x, y)("%1$s = %2$s; ", "var1") == "x = 5; y = 3; 0 = var1");
219 
220     // with types
221     assert(dump!(x, y)("(%s: %3$s) = %2$s, ") == "(x: int) = 5, (y: int) = 3");
222     assert(dump!(x, y)("(%s!%3$s) = %2$s, ") == "(x!int) = 5, (y!int) = 3");
223 
224     // custom separator
225     assert(dump!(x, y)("%s = %s; ") == "x = 5; y = 3");
226 
227     // all printf formatting works
228     assert(dump!(x, y)("%-4s = %4s, ") == "x    =    5, y    =    3");
229 
230     // special formatting (if applicable for all types)
231     auto z1 = 2.0, z2 = 4.0;
232     assert(dump!(z1, z2)("%s = %.3f & ") == "z1 = 2.000 & z2 = 4.000");
233 
234     // functions
235     assert(dump!(x, y, () => x + y)("%s = %s; ") == "x = 5; y = 3; () = 8");
236 
237     // runtime paramters
238     auto b = (int a) => ++a;
239     assert(dump!(x, y)("%s = %s, ", b(x), x - y) == "x = 5, y = 3, 0 = 6, 1 = 2");
240 
241     // validate laziness
242     auto c = (ref int a) => ++a;
243     assert(dump!(x, y, () => x + y)("%s = %s, ", c(x), x - y) == "x = 5, y = 3, () = 8, 0 = 6, 1 = 3");
244     assert(dump!(x, y, () => x + y)("%s = %s, ", c(x), x - y) == "x = 6, y = 3, () = 9, 0 = 7, 1 = 4");
245 }
246 
247 // test with output range
248 unittest
249 {
250     import std.array : appender;
251 
252     auto x = 2;
253     long y = 4;
254     auto w = appender!string;
255     dump!(x, y)(w, "%s = %s, ");
256     assert(w.data == "x = 2, y = 4");
257 
258     import std.stdio : stdout;
259 
260     if (false)
261         dump!(x, y)(stdout.lockingTextWriter(), "%s = %s, ");
262 }