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         import core.stdc..string: memcpy;
256 
257         if (str.length > buffer.length)
258             return result(buffer, 0);
259 
260         memcpy(&buffer[0], &str[0], str.length * Char.sizeof);
261 
262         return result(buffer, str.length);
263     }
264     else
265     {
266         import std.utf: byUTF;
267 
268         size_t numCharsWritten;
269 
270         foreach(strChar; str.byUTF!Char)
271         {
272             assert(numCharsWritten < buffer.length);
273             buffer[numCharsWritten] = strChar;
274             numCharsWritten++;
275 
276             if (numCharsWritten >= buffer.length)
277                 return result(buffer, 0);
278         }
279 
280         return result(buffer, numCharsWritten);
281     }
282 }
283 
284 version(unittest)
285 @Types!(char, wchar, dchar)
286 @safe writeString(T)() {
287     import std.utf: byUTF;
288     import std.array: array;
289 
290     static immutable testString = "this is a string".byUTF!T.array;
291 
292     @safe void testWrite(Char)()
293     {
294         Char[32] chars;
295         auto buffer = Buffer!Char(chars[]);
296         auto result = buffer.write(testString);
297 
298         result.numCharsWritten.shouldEqual(testString.length);
299         result.buffer.length.shouldEqual(chars.length - testString.length);
300         chars[0..result.numCharsWritten].shouldEqual(testString);
301     }
302 
303     testWrite!char;
304     testWrite!wchar;
305     testWrite!dchar;
306 }
307 
308 ///ditto
309 @safe @nogc
310 Buffer!Char write(Char, T)(ref Buffer!Char buffer, T enumeration)
311     if (isSomeChar!Char && is(T == enum))
312 {
313     import std.traits: fullyQualifiedName, EnumMembers;
314     import std.meta: NoDuplicates;
315     import std.conv: to;
316 
317     auto activeBuffer = buffer.write(fullyQualifiedName!T, '.');
318 
319     if (activeBuffer.numCharsWritten == 0)
320         return result(buffer, 0);
321 
322     //write enum name
323     //generate if statements
324     switch(enumeration)
325     {
326         foreach(member; NoDuplicates!(EnumMembers!T))
327         {
328         case member:
329             enum memberName = "name_" ~ member.to!string;
330             mixin("static immutable " ~ memberName ~ " = member.to!string;");
331             mixin("activeBuffer = activeBuffer.write(" ~ memberName ~ ");");
332 
333             if (activeBuffer.numCharsWritten == 0)
334                 return result(buffer, 0);
335             return activeBuffer;
336         }
337         default:
338     }
339     
340     return activeBuffer;
341 }
342 
343 @("write.enum")
344 @safe @nogc pure nothrow unittest
345 {
346     import std.traits: fullyQualifiedName;
347     enum Enum { value1, value2 }
348 
349     char[128] chars;
350     auto result = Buffer!char(chars[]).write(Enum.value1);
351     assert(chars[0..result.numCharsWritten] == fullyQualifiedName!(Enum.value1));
352 }
353 
354 ///ditto
355 Buffer!Char write(Char, T)(ref Buffer!Char buffer, T object)
356     if (isSomeChar!Char && (is(T == class) || is(T == interface)))
357 {
358     import std.traits: hasMember;
359 
360     if (object is null)
361         return buffer.write("null");
362     
363     static if (hasMember!(T, "toString"))
364         return buffer.write(object.toString());
365     else
366         return buffer.dumpObjectContents(structure);
367 }
368 
369 ///ditto
370 Buffer!Char write(Char, T)(ref Buffer!Char buffer, auto ref T structure)
371     if (isSomeChar!Char && is(T == struct))
372 {
373     import std.traits: hasMember;
374 
375     static if (hasMember!(T, "toString"))
376         return buffer.write(structure.toString());
377     else
378         return buffer.dumpObjectContents(structure);
379 }
380 
381 @("write.struct")
382 @safe @nogc pure nothrow unittest
383 {
384     struct TestStruct
385     {
386         int value;
387         bool flag;
388         string msg;
389     }
390 
391     char[128] chars;
392     auto buffer = Buffer!char(chars[]);
393     auto result = buffer.write(TestStruct(-129873, false, "Hello!"));
394     auto resultStr = chars[0..result.numCharsWritten];
395     // writelnUt(resultStr);
396 }
397 
398 @safe @nogc
399 Buffer!Char dumpObjectContents(Char, T)(ref Buffer!Char buffer, auto ref T object) pure nothrow
400 {
401     import std.traits: FieldNameTuple, fullyQualifiedName;
402 
403     auto activeBuffer = buffer;
404 
405     activeBuffer = activeBuffer.write(fullyQualifiedName!T, "{ ");
406 
407     if (activeBuffer.numCharsWritten == 0)
408         return result(buffer, 0);
409 
410     enum fields = FieldNameTuple!T;
411     
412     if (fields.length > 0)
413     {
414         mixin("activeBuffer = activeBuffer.write(fields[0], \": \", object." ~ fields[0] ~ ");");
415         
416         if (activeBuffer.numCharsWritten == 0)
417             return result(buffer, 0);
418     }
419 
420     static foreach(field; fields)
421     {
422         activeBuffer = activeBuffer.write(", ");
423         mixin("activeBuffer = activeBuffer.write(field, \": \", object." ~ field ~ ");");
424 
425         if (activeBuffer.numCharsWritten == 0)
426             return result(buffer, 0);
427     }
428 
429     activeBuffer = activeBuffer.write(" }");
430 
431     return activeBuffer;
432 }
433 
434 pragma(inline)
435 private auto result(Char)(ref Buffer!Char buffer, size_t numCharsWritten)
436 {
437     return Buffer!Char(buffer[numCharsWritten .. $], buffer.numCharsWritten + numCharsWritten);
438 }