1 module composer.writer;
2 
3 import std.traits: isSomeChar, isSomeString, isArray, isIntegral, isPointer;
4 
5 version(unittest) import unit_threaded;
6 else enum ShouldFail;
7 
8 struct Buffer(Char) {
9     Char[] buffer;
10     alias buffer this;
11 
12     size_t numCharsWritten;
13 }
14 
15 /**
16 Writes a value to the buffer.
17 
18 Supported types:
19     * bool
20     * char
21     * string
22     * byte/short/int/long and their unsigned counterparts
23     * structs (todo)
24     * classes (todo)
25     * arrays
26     * floating-point values (todo)
27     * pointers (in hexadecimal)
28 
29 Params:
30     buffer = The buffer to write to
31     value  = The value to write to the buffer
32 
33 Returns:
34     The remainder of the buffer that has not been written to
35     yet, and the number of characters that were written by
36     the function. In the event of error, `numCharsWritten`
37     will be 0.
38 */
39 Buffer!Char write(Char, A...)(auto ref Buffer!Char buffer, A args)
40 {
41     auto activeBuffer = buffer;
42 
43     foreach(ref arg; args)
44     {
45         activeBuffer = write(activeBuffer, arg);
46 
47         if (activeBuffer.numCharsWritten == 0)
48             return result(buffer, 0);
49     }
50 
51     return activeBuffer;
52 }
53 
54 @("write.all")
55 @safe @nogc pure nothrow unittest
56 {
57     char[128] chars;
58     auto result = write(Buffer!char(chars[]), "testMSG ", 20, 'c', " alo ", false);
59 
60     assert(chars[0 .. result.numCharsWritten] == "testMSG 20c alo false");
61 }
62 
63 ///ditto
64 @safe @nogc
65 Buffer!Char write(Char, T)(ref Buffer!Char buffer, T integral)
66     if (isSomeChar!Char && isIntegral!T && !is(T == enum))
67 {
68     import std.traits: Unqual, Unsigned;
69 
70     enum charBufferLength = 32;
71     Char[charBufferLength] buf = void;
72     size_t charIndex = charBufferLength - 1;
73 
74     const negative = integral < 0;
75     Unqual!(Unsigned!T) value = negative ? -integral : integral;
76 
77     while (value >= 10)
78     {
79         buf[charIndex] = cast(Char) ((value % 10) + '0');
80         value /= 10;
81         charIndex--;
82     }
83 
84     buf[charIndex] = cast(char) (value + '0');
85 
86     if (negative)
87     {
88         charIndex--;
89         buf[charIndex] = '-';
90     }
91 
92     const strLength = charBufferLength - charIndex;
93     buffer[0 .. strLength] = buf[charIndex .. $];
94 
95     return result(buffer, strLength);
96 }
97 
98 ///ditto
99 @safe @nogc
100 Buffer!Char write(Char, T)(ref Buffer!Char buffer, T boolValue)
101     if (isSomeChar!Char && is(T == bool))
102 {
103     import std.utf: byUTF;
104     import std.array: array;
105 
106     static immutable strings = ["false".byUTF!Char.array, "true".byUTF!Char.array];
107 
108     const stringLength = strings[boolValue].length;
109     if (buffer.length >= stringLength)
110         buffer[0 .. stringLength] = strings[boolValue];
111 
112     return result(buffer, stringLength);
113 }
114 
115 @("write.bool")
116 @safe @nogc pure nothrow unittest
117 {
118     char[32] chars;
119     auto result = Buffer!char(chars[]).write(true);
120     assert(chars[0..result.numCharsWritten] == "true");
121 
122     result = Buffer!char(chars[]).write(false);
123     assert(chars[0..result.numCharsWritten] == "false");
124 }
125 
126 ///ditto
127 @safe @nogc
128 Buffer!Char write(Char, T)(ref Buffer!Char buffer, T character)
129     if (isSomeChar!Char && isSomeChar!T)
130 {
131     import std.utf: byUTF, codeLength;
132 
133     const numRequiredChars = character.codeLength!Char;
134     if (numRequiredChars > buffer.length)
135         return result(buffer, 0);
136     
137     T[1] slice = [character];
138 
139     size_t index;
140     foreach(c; slice[].byUTF!Char)
141     {
142         buffer[index] = c;
143         index++;
144     }
145 
146     assert(index == numRequiredChars);
147 
148     return result(buffer, numRequiredChars);
149 }
150 
151 ///ditto
152 @safe @nogc
153 Buffer!Char write(Char, T)(ref Buffer!Char buffer, T pointer)
154     if (isSomeChar!Char && isPointer!T)
155 {
156     import std.traits: PointerTarget, fullyQualifiedName;
157     import std.conv: toChars, LetterCase;
158 
159     //to allow for @nogc annotation
160     static immutable string tName = fullyQualifiedName!T;
161     
162     auto activeBuffer = buffer.write(tName, '(');
163     const value = cast(size_t) pointer;
164 
165     if (pointer is null)
166         activeBuffer = activeBuffer.write("null");
167     else
168     {//write address as hexadecimal
169         activeBuffer = activeBuffer.write("0x");
170         foreach(hexChar; value.toChars!(16, Char, LetterCase.upper))
171         {
172             activeBuffer = activeBuffer.write(hexChar);
173 
174             if (activeBuffer.numCharsWritten == 0)
175                 return result(buffer, 0);
176         }
177     }
178 
179     activeBuffer = activeBuffer.write(')');
180 
181     if (activeBuffer.numCharsWritten == 0)
182         return result(buffer, 0);
183 
184     return activeBuffer;
185 }
186 
187 @("write.pointer")
188 @safe @nogc pure nothrow
189 unittest
190 {
191     char[32] chars;
192     auto buffer = Buffer!char(chars[]);
193     auto result = buffer.write(()@trusted{return cast(char*) 0xDEADBEEF;}());
194     auto resultStr = chars[0..result.numCharsWritten];
195 
196     assert(resultStr == "char*(0xDEADBEEF)");
197 }
198 
199 ///ditto
200 @safe @nogc
201 Buffer!Char write(Char, T)(ref Buffer!Char buffer, T array)
202     if (isSomeChar!Char && isArray!T && !isSomeString!T)
203 {
204     import std.traits: fullyQualifiedName;
205     static immutable name = fullyQualifiedName!T;
206     auto activeBuffer = buffer.write(name);
207 
208     //take of the trailing `]` character
209     activeBuffer.buffer = buffer[fullyQualifiedName!T.length - 1 .. $];
210     activeBuffer.numCharsWritten -= 1;
211 
212     // if the array is not empty, print contents in comma-separated list
213     if (array.length > 0)
214     {
215         activeBuffer = activeBuffer.write(array[0]);
216 
217         foreach(value; array[1 .. $])
218         {
219             activeBuffer = activeBuffer.write(", ", value);
220 
221             if (activeBuffer.numCharsWritten == 0)
222                 return result(buffer, 0);
223         }
224     }
225 
226     activeBuffer = activeBuffer.write(']');
227 
228     if (activeBuffer.numCharsWritten == 0)
229         return result(buffer, 0);
230 
231     return activeBuffer;
232 }
233 
234 @("write.array")
235 @safe pure nothrow unittest
236 {
237     static immutable array = [0, 1, -2, 3, 400];
238 
239     char[32] chars;
240     auto buffer = Buffer!char(chars[]);
241     auto result = buffer.write(array);
242     const str = chars[0 .. result.numCharsWritten];
243     assert(chars[0 .. result.numCharsWritten] == "immutable(int)[0, 1, -2, 3, 400]");
244 }
245 
246 ///ditto
247 @safe @nogc
248 Buffer!Char write(Char, T)(ref Buffer!Char buffer, T str)
249     if (isSomeChar!Char && isSomeString!T)
250 {
251     import std.range: ElementEncodingType;
252 
253     static if (is(ElementEncodingType!T == Char))
254     {
255         if (str.length > buffer.length)
256             return result(buffer, 0);
257 
258         buffer[0..str.length] = str[];
259 
260         return result(buffer, str.length);
261     }
262     else
263     {
264         import std.utf: byUTF;
265 
266         size_t numCharsWritten = 0;
267         foreach(strChar; str.byUTF!Char)
268         {
269             if (numCharsWritten == buffer.length)
270                 return result(buffer, 0);
271 
272             buffer[numCharsWritten] = strChar;
273             numCharsWritten++;
274         }
275 
276         return result(buffer, numCharsWritten);
277     }
278 }
279 
280 version(unittest)
281 @Types!(char, wchar, dchar)
282 @safe writeString(T)() {
283     import std.utf: byUTF;
284     import std.array: array;
285 
286     static immutable testString = "this is a string".byUTF!T.array;
287 
288     @safe void testWrite(Char)()
289     {
290         Char[32] chars;
291         auto buffer = Buffer!Char(chars[]);
292         auto result = buffer.write(testString);
293 
294         result.numCharsWritten.shouldEqual(testString.length);
295         result.buffer.length.shouldEqual(chars.length - testString.length);
296         chars[0..result.numCharsWritten].shouldEqual(testString);
297     }
298 
299     testWrite!char;
300     testWrite!wchar;
301     testWrite!dchar;
302 }
303 
304 ///ditto
305 @safe @nogc
306 Buffer!Char write(Char, T)(ref Buffer!Char buffer, T enumeration)
307     if (isSomeChar!Char && is(T == enum))
308 {
309     import std.traits: fullyQualifiedName, EnumMembers;
310     import std.meta: NoDuplicates;
311     import std.conv: to;
312 
313     static immutable fqn = fullyQualifiedName!T;
314     auto activeBuffer = buffer.write(fqn, '.');
315 
316     if (activeBuffer.numCharsWritten == 0)
317         return result(buffer, 0);
318 
319     //write enum name
320     //generate if statements
321     switch(enumeration)
322     {
323         foreach(member; NoDuplicates!(EnumMembers!T))
324         {
325         case member:
326             enum memberName = "name_" ~ member.to!string;
327             mixin("static immutable " ~ memberName ~ " = member.to!string;");
328             mixin("activeBuffer = activeBuffer.write(" ~ memberName ~ ");");
329 
330             if (activeBuffer.numCharsWritten == 0)
331                 return result(buffer, 0);
332             return activeBuffer;
333         }
334         default:
335     }
336     
337     return activeBuffer;
338 }
339 
340 @("write.enum")
341 @safe @nogc pure nothrow unittest
342 {
343     import std.traits: fullyQualifiedName;
344     enum Enum { value1, value2 }
345 
346     char[128] chars;
347     auto result = Buffer!char(chars[]).write(Enum.value1);
348     assert(chars[0..result.numCharsWritten] == fullyQualifiedName!(Enum.value1));
349 }
350 
351 ///ditto
352 Buffer!Char write(Char, T)(ref Buffer!Char buffer, T object)
353     if (isSomeChar!Char && (is(T == class) || is(T == interface)))
354 {
355     import std.traits: hasMember;
356 
357     if (object is null)
358         return buffer.write("null");
359     
360     static if (hasMember!(T, "toString"))
361         return buffer.write(object.toString());
362     else
363         return buffer.dumpObjectContents(structure);
364 }
365 
366 ///ditto
367 Buffer!Char write(Char, T)(ref Buffer!Char buffer, auto ref T structure)
368     if (isSomeChar!Char && is(T == struct))
369 {
370     import std.traits: hasMember;
371 
372     static if (hasMember!(T, "toString"))
373         return buffer.write(structure.toString());
374     else
375         return buffer.dumpObjectContents(structure);
376 }
377 
378 @("write.struct")
379 @safe @nogc pure nothrow unittest
380 {
381     struct TestStruct
382     {
383         int value;
384         bool flag;
385         string msg;
386     }
387 
388     char[128] chars;
389     auto buffer = Buffer!char(chars[]);
390     auto result = buffer.write(TestStruct(-129873, false, "Hello!"));
391     auto resultStr = chars[0..result.numCharsWritten];
392     // writelnUt(resultStr);
393 }
394 
395 @safe @nogc
396 Buffer!Char dumpObjectContents(Char, T)(ref Buffer!Char buffer, auto ref T object) pure nothrow
397 {
398     import std.traits: FieldNameTuple, fullyQualifiedName;
399 
400     auto activeBuffer = buffer;
401 
402     activeBuffer = activeBuffer.write(fullyQualifiedName!T, "{ ");
403 
404     if (activeBuffer.numCharsWritten == 0)
405         return result(buffer, 0);
406 
407     enum fields = FieldNameTuple!T;
408     
409     if (fields.length > 0)
410     {
411         mixin("activeBuffer = activeBuffer.write(fields[0], \": \", object." ~ fields[0] ~ ");");
412         
413         if (activeBuffer.numCharsWritten == 0)
414             return result(buffer, 0);
415     }
416 
417     static foreach(field; fields)
418     {
419         activeBuffer = activeBuffer.write(", ");
420         mixin("activeBuffer = activeBuffer.write(field, \": \", object." ~ field ~ ");");
421 
422         if (activeBuffer.numCharsWritten == 0)
423             return result(buffer, 0);
424     }
425 
426     activeBuffer = activeBuffer.write(" }");
427 
428     return activeBuffer;
429 }
430 
431 pragma(inline)
432 private auto result(Char)(ref Buffer!Char buffer, size_t numCharsWritten)
433 {
434     return Buffer!Char(buffer[numCharsWritten .. $], buffer.numCharsWritten + numCharsWritten);
435 }