1 /**
2  * A collection of compile-time functions to help in identifying stream types
3  * and related flavors of them.
4  *
5  * Streams come in two main flavors: ${B input} and ${B output} streams.
6  * 
7  * ${B Input streams} are defined by the presence of a `read` function with the
8  * following signature:
9  * ```d
10  * int read(DataType[] buffer)
11  * ```
12  *
13  * ${B Output streams} are defined by the presence of a `write` function with
14  * the following signature:
15  * ```d
16  * int read(DataType[] buffer)
17  * ```
18  *
19  * Usually these functions can be used as [Template Constraints](https://dlang.org/spec/template.html#template_constraints)
20  * when defining your own functions and symbols to work with streams.
21  * ```d
22  * void useBytes(S)(S stream) if (isInputStream!(S, ubyte)) {
23  *     ubyte[] buffer = new ubyte[8192];
24  *     int bytesRead = stream.read(buffer);
25  *     // Do something with the data.
26  * }
27  * ```
28  */
29 module streams.primitives;
30 
31 import std.traits;
32 import std.range;
33 
34 /** 
35  * Determines if the given template argument is some form of input stream,
36  * where an input stream is anything with a `read` method that takes a single
37  * array parameter, and returns an integer number of elements that were read,
38  * or -1 in case of error. This method does not care about the type of elements
39  * that can be read by the stream.
40  *
41  * Returns: `true` if the given argument is an input stream type.
42  */
43 bool isSomeInputStream(StreamType)() {
44     // Note: We use a cascading static check style so the compiler runs these checks in this order.
45     static if (hasMember!(StreamType, "read")) {
46         alias func = StreamType.read;
47         static if (isCallable!func && is(ReturnType!func == int)) {
48             static if (Parameters!func.length == 1) {
49                 return isDynamicArray!(Parameters!func[0]);
50             } else { return false; }
51         } else { return false; }
52     } else { return false; }
53 }
54 
55 unittest {
56     struct S1 {
57         int read(ubyte[] buffer) { return 0; }
58     }
59     assert(isSomeInputStream!S1);
60     struct S2 {
61         int read(bool[] buffer) { return 42; }
62     }
63     assert(isSomeInputStream!S2);
64     struct S3 {
65         int read(bool[] buffer, int otherArg) { return 0; }
66     }
67     assert(!isSomeInputStream!S3);
68     struct S4 {
69         void read(long[] buffer) {}
70     }
71     assert(!isSomeInputStream!S4);
72     struct S5 {
73         int read = 10;
74     }
75     assert(!isSomeInputStream!S5);
76     struct S6 {}
77     assert(!isSomeInputStream!S6);
78     interface I1 {
79         int read(ubyte[] buffer);
80     }
81     assert(isSomeInputStream!I1);
82     class C1 {
83         int read(ubyte[] buffer) { return 0; }
84     }
85     assert(isSomeInputStream!C1);
86 }
87 
88 /** 
89  * Determines if the given template argument is some form of output stream,
90  * where an output stream is anything with a `write` method that takes a single
91  * array parameter, and returns an integer number of elements that were read,
92  * or -1 in case of error. This method does not care about the type of elements
93  * that can be read by the stream.
94  *
95  * Returns: `true` if the given argument is an output stream type.
96  */
97 bool isSomeOutputStream(StreamType)() {
98     // Note: We use a cascading static check style so the compiler runs these checks in this order.
99     static if (hasMember!(StreamType, "write")) {
100         alias func = StreamType.write;
101         static if (isCallable!func && is(ReturnType!func == int)) {
102             static if (Parameters!func.length == 1) {
103                 return isDynamicArray!(Parameters!func[0]);
104             } else { return false; }
105         } else { return false; }
106     } else { return false; }
107 }
108 
109 unittest {
110     struct S1 {
111         int write(ubyte[] buffer) { return 0; }
112     }
113     assert(isSomeOutputStream!S1);
114     struct S2 {
115         int write(bool[] buffer) { return 42; }
116     }
117     assert(isSomeOutputStream!S2);
118     struct S3 {
119         int write(bool[] buffer, int otherArg) { return 0; }
120     }
121     assert(!isSomeOutputStream!S3);
122     struct S4 {
123         void write(long[] buffer) {}
124     }
125     assert(!isSomeOutputStream!S4);
126     struct S5 {
127         int write = 10;
128     }
129     assert(!isSomeOutputStream!S5);
130     struct S6 {}
131     assert(!isSomeOutputStream!S6);
132 }
133 
134 /** 
135  * Determines if the given stream type is an input stream for reading data of
136  * the given type.
137  * Returns: `true` if the given stream type is an input stream.
138  */
139 bool isInputStream(StreamType, DataType)() {
140     static if (isSomeInputStream!StreamType) {
141         return is(Parameters!(StreamType.read)[0] == DataType[]);
142     } else {
143         return false;
144     }
145 }
146 
147 unittest {
148     // Test a valid input stream.
149     struct S1 {
150         int read(ubyte[] buffer) {
151             return 0; // Don't do anything with the data.
152         }
153     }
154     assert(isInputStream!(S1, ubyte));
155 
156     // Test a few invalid input streams.
157     struct S2 {}
158     assert(!isInputStream!(S2, ubyte));
159     struct S3 {
160         void read(ubyte[] buffer) {
161             // Invalid return type!
162         }
163     }
164     assert(!isInputStream!(S3, ubyte));
165     struct S4 {
166         int read() {
167             return 0; // Missing required arguments.
168         }
169     }
170     assert(!isInputStream!(S4, ubyte));
171     class C1 {
172         int read(char[] buffer) {
173             return 0;
174         }
175     }
176     assert(isInputStream!(C1, char));
177 }
178 
179 /** 
180  * Wraps an existing input stream as a Phobos-style input range, to make any
181  * input stream compatible with functions that take input ranges. The given
182  * stream is stored as a pointer in the underlying range implementation, so you
183  * should still manage ownership of the original stream.
184  * 
185  * ```d
186  * import std.range.primitives : isInputRange;
187  * import streams;
188  *
189  * auto stream = inputStreamFor!int([1, 2, 3]);
190  * auto range = asInputRange!int(stream);
191  * assert(isInputRange!(typeof(range)));
192  * ```
193  * Params:
194  *   stream = The stream to wrap.
195  * Returns: The input range.
196  */
197 auto asInputRange(E, S)(ref S stream) if (isInputStream!(S, E)) {
198     struct InputStreamRange {
199         import std.typecons;
200 
201         private S* stream;
202         private Nullable!E lastElement;
203         private int lastRead;
204 
205         this(ref S stream) {
206             this.stream = &stream;
207             // Initialize the range with one element.
208             this.popFront();
209         }
210 
211         void popFront() {
212             E[1] buffer;
213             this.lastRead = this.stream.read(buffer);
214             if (this.lastRead > 0) {
215                 this.lastElement = nullable(buffer[0]);
216             } else {
217                 this.lastElement = Nullable!E.init;
218             }
219         }
220 
221         bool empty() {
222             return this.lastRead < 1;
223         }
224 
225         E front() {
226             return this.lastElement.get();
227         }
228     }
229     return InputStreamRange(stream);
230 }
231 
232 unittest {
233     import streams;
234     auto s = inputStreamFor!ubyte([1, 2, 3]);
235     auto r = asInputRange!ubyte(s);
236     assert(isInputRange!(typeof(r)));
237 }
238 
239 /** 
240  * Determines if the given stream type is an output stream for writing data of
241  * the given type.
242  * Returns: `true` if the given stream type is an output stream.
243  */
244 bool isOutputStream(StreamType, DataType)() {
245     static if (isSomeOutputStream!StreamType) {
246         return is(Parameters!(StreamType.write)[0] == DataType[]);
247     } else {
248         return false;
249     }
250 }
251 
252 unittest {
253     // Test a valid output stream.
254     struct S1 {
255         int write(ref ubyte[] buffer) {
256             return 0; // Don't do anything with the data.
257         }
258     }
259     assert(isOutputStream!(S1, ubyte));
260 
261     // Test a few invalid output streams.
262     struct S2 {}
263     assert(!isOutputStream!(S2, ubyte));
264     struct S3 {
265         void write(ubyte[] buffer) {
266             // Invalid return type!
267         }
268     }
269     assert(!isOutputStream!(S3, ubyte));
270     struct S4 {
271         int write() {
272             return 0; // Missing required arguments.
273         }
274     }
275     assert(!isOutputStream!(S4, ubyte));
276 }
277 
278 /** 
279  * Wraps an existing output stream as a Phobos-style output range with a
280  * `put` method, to make any output stream compatible with functions that take
281  * output ranges. The given stream is stored as a pointer in the underlying
282  * range implementation, so you should still manage ownership of the original
283  * stream.
284  * 
285  * ```d
286  * import std.range.primitives : isOutputRange;
287  * import streams;
288  *
289  * auto stream = ArrayOutputStream!int();
290  * auto range = asOutputRange!int(stream);
291  * assert(isOutputRange!(typeof(range), int));
292  * ```
293  * Params:
294  *   stream = The stream to wrap.
295  * Returns: The output range.
296  */
297 auto asOutputRange(E, S)(ref S stream) if (isOutputStream!(S, E)) {
298     struct StreamOutputRange {
299         private S* stream;
300         
301         void put(E[] buffer) {
302             this.stream.write(buffer);
303         }
304     }
305     return StreamOutputRange(&stream);
306 }
307 
308 unittest {
309     import streams;
310     auto s = ArrayOutputStream!ubyte();
311     auto o = asOutputRange!ubyte(s);
312     assert(isOutputRange!(typeof(o), ubyte));
313 }
314 
315 /** 
316  * Determines if the given template argument is a stream of any kind; that is,
317  * it is at least implementing the functions required to be an input or output
318  * stream.
319  * Returns: `true` if the given argument is some stream.
320  */
321 bool isSomeStream(StreamType)() {
322     return isSomeInputStream!StreamType || isSomeOutputStream!StreamType;
323 }
324 
325 unittest {
326     struct S1 {
327         int read(ubyte[] buffer) {
328             return 0;
329         }
330     }
331     assert(isSomeStream!S1);
332     struct S2 {
333         int write(ubyte[] buffer) {
334             return 0;
335         }
336     }
337     assert(isSomeStream!S2);
338     struct S3 {}
339     assert(!isSomeStream!S3);
340 }
341 
342 /** 
343  * Determines if the given stream type is an input or output stream for data of
344  * the given type.
345  * Returns: `true` if the stream type is an input or output stream for the given data type.
346  */
347 bool isSomeStream(StreamType, DataType)() {
348     return isInputStream!(StreamType, DataType) || isOutputStream(StreamType, DataType);
349 }
350 
351 /** 
352  * Determines if the given stream type is an input stream for `ubyte` elements.
353  * Returns: `true` if the stream type is a byte input stream.
354  */
355 bool isByteInputStream(StreamType)() {
356     return isInputStream!(StreamType, ubyte);
357 }
358 
359 /** 
360  * Determines if the given stream type is an output stream for `ubyte` elements.
361  * Returns: `true` if the stream type is a byte output stream.
362  */
363 bool isByteOutputStream(StreamType)() {
364     return isOutputStream!(StreamType, ubyte);
365 }
366 
367 /** 
368  * Determines if the given template argument is a closable stream type, which
369  * defines a `void close()` method as a means to close and/or deallocate the
370  * underlying resource that the stream reads from or writes to.
371  *
372  * Returns: `true` if the given argument is a closable stream.
373  */
374 bool isClosableStream(StreamType)() {
375     static if (
376         isSomeStream!StreamType &&
377         hasMember!(StreamType, "close") &&
378         isCallable!(StreamType.close)
379     ) {
380         alias closeFunction = StreamType.close;
381         alias params = Parameters!closeFunction;
382         return (
383             allSameType!(void, ReturnType!closeFunction) &&
384             params.length == 0
385         );
386     } else {
387         return false;
388     }
389 }
390 
391 unittest {
392     struct S1 {
393         int read(ubyte[] buffer) {
394             return 0;
395         }
396         void close() {}
397     }
398     assert(isClosableStream!S1);
399     struct S2 {
400         int read(ubyte[] buffer) {
401             return 0;
402         }
403     }
404     assert(!isClosableStream!S2);
405     struct S3 {}
406     assert(!isClosableStream!S3);
407 }
408 
409 /** 
410  * Determines if the given template argument is a flushable stream type, which
411  * is any output stream that defines a `void flush()` method, which should
412  * cause any data buffered by the stream or its resources to be flushed. The
413  * exact nature of how a flush operates is implementation-dependent.
414  *
415  * Returns: `true` if the given argument is a flushable stream.
416  */
417 bool isFlushableStream(StreamType)() {
418     import std.traits;
419     static if (
420         isSomeOutputStream!StreamType &&
421         hasMember!(StreamType, "flush") &&
422         isCallable!(StreamType.flush)
423     ) {
424         alias flushFunction = StreamType.flush;
425         alias params = Parameters!flushFunction;
426         return (
427             allSameType!(void, ReturnType!flushFunction) &&
428             params.length == 0
429         );
430     } else {
431         return false;
432     }
433 }
434 
435 unittest {
436     struct S1 {
437         int write(ubyte[] buffer) {
438             return 0;
439         }
440         void flush() {}
441     }
442     assert(isFlushableStream!S1);
443     struct S2 {
444         int write(ubyte[] buffer) {
445             return 0;
446         }
447     }
448     assert(!isFlushableStream!S2);
449     struct S3 {}
450     assert(!isFlushableStream!S3);
451 }
452 
453 /** 
454  * An exception that may be thrown if an illegal operation or error occurs
455  * while working with streams. Generally, if an exception is to be thrown while
456  * reading or writing in a stream's implementation, a `StreamException` should
457  * be wrapped around it to provide a common interface for error handling.
458  */
459 class StreamException : Exception {
460     import std.exception;
461 
462     mixin basicExceptionCtors;
463 }