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 }