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:
8  * ```d
9  * StreamResult readFromStream(DataType[] buffer)
10  * ```
11  *
12  * ${B Output streams} are defined by the presence of a write function:
13  * ```d
14  * StreamResult writeToStream(DataType[] buffer)
15  * ```
16  *
17  * Usually these functions can be used as [Template Constraints](https://dlang.org/spec/template.html#template_constraints)
18  * when defining your own functions and symbols to work with streams.
19  * ```d
20  * void useBytes(S)(S stream) if (isInputStream!(S, ubyte)) {
21  *     ubyte[8192] buffer;
22  *     StreamResult result = stream.readFromStream(buffer);
23  *     if (!result.hasError) {
24  *       // Do something with the data.
25  *     }
26  * }
27  * ```
28  */
29 module streams.primitives;
30 
31 import streams.utils : Optional, Either;
32 import std.range : ElementType;
33 import std.traits : isCallable, ReturnType, Parameters, isDynamicArray;
34 
35 private const INPUT_STREAM_METHOD = "readFromStream";
36 private const OUTPUT_STREAM_METHOD = "writeToStream";
37 private const FLUSHABLE_STREAM_METHOD = "flushStream";
38 private const CLOSABLE_STREAM_METHOD = "closeStream";
39 private const SEEKABLE_STREAM_METHOD = "seekInStream";
40 
41 /**
42  * An error that occurred during a stream operation, which includes a short
43  * message, as well as an integer code which is usually the last stream
44  * operation return code.
45  */
46 struct StreamError {
47     const(char[]) message;
48     const int code;
49 }
50 
51 /**
52  * A convenience alias for an optional stream error, which is a common return
53  * type for many stream methods.
54  */
55 alias OptionalStreamError = Optional!StreamError;
56 
57 /**
58  * Either a number of items that have been read or written, or a stream error,
59  * as a common result type for many stream operations.
60  *
61  * As an instance of `Either`, it offers the following methods:
62  * - `result.hasError` to check if the result has an error.
63  * - `result.hasCount` to check if the result has an element count.
64  * - `result.error` to get the `StreamError` instance, if `result.hasError` returns `true`.
65  * - `result.count` to get the number of elements read or written, if `result.hasCount` returns `true`.
66  */
67 alias StreamResult = Either!(uint, "count", StreamError, "error");
68 
69 /** 
70  * Determines if the given template argument is some form of input stream.
71  * Returns: `true` if the given argument is an input stream type.
72  */
73 bool isSomeInputStream(StreamType)() {
74     // Note: We use a cascading static check style so the compiler runs these checks in this order.
75     static if (__traits(hasMember, StreamType, INPUT_STREAM_METHOD)) {
76         alias func = __traits(getMember, StreamType, INPUT_STREAM_METHOD);
77         static if (isCallable!func && is(ReturnType!func == StreamResult)) {
78             static if (Parameters!func.length == 1) {
79                 return isDynamicArray!(Parameters!func[0]);
80             } else { return false; }
81         } else { return false; }
82     } else { return false; }
83 }
84 
85 unittest {
86     struct S1 {
87         StreamResult readFromStream(ubyte[] buffer) { return StreamResult(0); } // cov-ignore
88     }
89     assert(isSomeInputStream!S1);
90     // Check that pointers to streams are also considered as streams.
91     assert(isSomeInputStream!(S1*));
92     // But double or triple pointers (or anything else) are not!
93     assert(!isSomeInputStream!(S1**));
94     assert(!isSomeInputStream!(S1***));
95     struct S2 {
96         StreamResult readFromStream(bool[] buffer) { return StreamResult(42); } // cov-ignore
97     }
98     assert(isSomeInputStream!S2);
99     struct S3 {
100         StreamResult readFromStream(bool[] buffer, int otherArg) { return StreamResult(0); } // cov-ignore
101     }
102     assert(!isSomeInputStream!S3);
103     struct S4 {
104         void readFromStream(long[] buffer) {}
105     }
106     assert(!isSomeInputStream!S4);
107     struct S5 {
108         StreamResult readFromStream = StreamResult(10);
109     }
110     assert(!isSomeInputStream!S5);
111     struct S6 {}
112     assert(!isSomeInputStream!S6);
113     
114     version (D_BetterC) {} else {
115         interface I1 {
116             StreamResult readFromStream(ubyte[] buffer);
117         }
118         assert(isSomeInputStream!I1);
119         class C1 {
120             StreamResult readFromStream(ubyte[] buffer) { return StreamResult(0); } // cov-ignore
121         }
122         assert(isSomeInputStream!C1);
123     }
124 }
125 
126 /** 
127  * Determines if the given template argument is some form of output stream.
128  * Returns: `true` if the given argument is an output stream type.
129  */
130 bool isSomeOutputStream(StreamType)() {
131     // Note: We use a cascading static check style so the compiler runs these checks in this order.
132     static if (__traits(hasMember, StreamType, OUTPUT_STREAM_METHOD)) {
133         alias func = __traits(getMember, StreamType, OUTPUT_STREAM_METHOD);
134         static if (isCallable!func && is(ReturnType!func == StreamResult)) {
135             static if (Parameters!func.length == 1) {
136                 return isDynamicArray!(Parameters!func[0]);
137             } else { return false; }
138         } else { return false; }
139     } else { return false; }
140 }
141 
142 unittest {
143     struct S1 {
144         StreamResult writeToStream(ubyte[] buffer) { return StreamResult(0); } // cov-ignore
145     }
146     assert(isSomeOutputStream!S1);
147     // Check that pointers to output streams are also considered streams (but not double and so on).
148     assert(isSomeOutputStream!(S1*));
149     assert(!isSomeOutputStream!(S1**));
150     assert(!isSomeOutputStream!(S1***));
151     struct S2 {
152         StreamResult writeToStream(bool[] buffer) { return StreamResult(42); } // cov-ignore
153     }
154     assert(isSomeOutputStream!S2);
155     struct S3 {
156         StreamResult writeToStream(bool[] buffer, int otherArg) { return StreamResult(0); } // cov-ignore
157     }
158     assert(!isSomeOutputStream!S3);
159     struct S4 {
160         void writeToStream(long[] buffer) {}
161     }
162     assert(!isSomeOutputStream!S4);
163     struct S5 {
164         StreamResult writeToStream = StreamResult(10);
165     }
166     assert(!isSomeOutputStream!S5);
167     struct S6 {}
168     assert(!isSomeOutputStream!S6);
169 }
170 
171 /** 
172  * A template that evaluates to the type of a given input or output stream.
173  * Params:
174  *   S = The stream to get the type of.
175  */
176 template StreamType(S) if (isSomeStream!S) {
177     static if (isSomeInputStream!S) {
178         alias StreamType = ElementType!(Parameters!(__traits(getMember, S, INPUT_STREAM_METHOD))[0]);
179     } else {
180         alias StreamType = ElementType!(Parameters!(__traits(getMember, S, OUTPUT_STREAM_METHOD))[0]);
181     }
182 }
183 
184 unittest {
185     struct S1 {
186         StreamResult readFromStream(bool[] buffer) {
187             return StreamResult(0); // cov-ignore
188         }
189     }
190     assert(is(StreamType!S1 == bool));
191     assert(is(StreamType!(S1*) == bool));
192 }
193 
194 /** 
195  * Determines if the given stream type is an input stream for reading data of
196  * the given type.
197  * Returns: `true` if the given stream type is an input stream.
198  */
199 bool isInputStream(StreamType, DataType)() {
200     static if (isSomeInputStream!StreamType) {
201         return is(Parameters!(__traits(getMember, StreamType, INPUT_STREAM_METHOD))[0] == DataType[]);
202     } else {
203         return false;
204     }
205 }
206 
207 unittest {
208     // Test a valid input stream.
209     struct S1 {
210         StreamResult readFromStream(ubyte[] buffer) {
211             return StreamResult(0); // cov-ignore
212         }
213     }
214     assert(isInputStream!(S1, ubyte));
215 
216     // Test a few invalid input streams.
217     struct S2 {}
218     assert(!isInputStream!(S2, ubyte));
219     struct S3 {
220         void readFromStream(ubyte[] buffer) {
221             // Invalid return type!
222         }
223     }
224     assert(!isInputStream!(S3, ubyte));
225     struct S4 {
226         StreamResult readFromStream() {
227             return StreamResult(0); // cov-ignore
228         }
229     }
230     assert(!isInputStream!(S4, ubyte));
231 
232     version (D_BetterC) {} else {
233         class C1 {
234             StreamResult readFromStream(char[] buffer) {
235                 return StreamResult(0); // cov-ignore
236             }
237         }
238         assert(isInputStream!(C1, char));
239     }
240 }
241 
242 /** 
243  * Determines if the given stream type is an output stream for writing data of
244  * the given type.
245  * Returns: `true` if the given stream type is an output stream.
246  */
247 bool isOutputStream(StreamType, DataType)() {
248     static if (isSomeOutputStream!StreamType) {
249         return is(Parameters!(__traits(getMember, StreamType, OUTPUT_STREAM_METHOD))[0] == DataType[]);
250     } else {
251         return false;
252     }
253 }
254 
255 unittest {
256     // Test a valid output stream.
257     struct S1 {
258         StreamResult writeToStream(ref ubyte[] buffer) {
259             return StreamResult(0); // cov-ignore
260         }
261     }
262     assert(isOutputStream!(S1, ubyte));
263 
264     // Test a few invalid output streams.
265     struct S2 {}
266     assert(!isOutputStream!(S2, ubyte));
267     struct S3 {
268         void writeToStream(ubyte[] buffer) {
269             // Invalid return type!
270         }
271     }
272     assert(!isOutputStream!(S3, ubyte));
273     struct S4 {
274         StreamResult writeToStream() {
275             return StreamResult(0); // cov-ignore
276         }
277     }
278     assert(!isOutputStream!(S4, ubyte));
279 }
280 
281 /** 
282  * Determines if the given template argument is a stream of any kind; that is,
283  * it is at least implementing the functions required to be an input or output
284  * stream.
285  * Returns: `true` if the given argument is some stream.
286  */
287 bool isSomeStream(StreamType)() {
288     return isSomeInputStream!StreamType || isSomeOutputStream!StreamType;
289 }
290 
291 unittest {
292     struct S1 {
293         StreamResult readFromStream(ubyte[] buffer) {
294             return StreamResult(0); // cov-ignore
295         }
296     }
297     assert(isSomeStream!S1);
298     struct S2 {
299         StreamResult writeToStream(ubyte[] buffer) {
300             return StreamResult(0); // cov-ignore
301         }
302     }
303     assert(isSomeStream!S2);
304     struct S3 {}
305     assert(!isSomeStream!S3);
306 }
307 
308 /** 
309  * Determines if the given stream type is an input or output stream for data of
310  * the given type.
311  * Returns: `true` if the stream type is an input or output stream for the
312  * given data type.
313  */
314 bool isSomeStream(StreamType, DataType)() {
315     return isInputStream!(StreamType, DataType) || isOutputStream!(StreamType, DataType);
316 }
317 
318 /** 
319  * Determines if the given stream type is an input stream for `ubyte` elements.
320  * Returns: `true` if the stream type is a byte input stream.
321  */
322 bool isByteInputStream(StreamType)() {
323     return isInputStream!(StreamType, ubyte);
324 }
325 
326 /** 
327  * Determines if the given stream type is an output stream for `ubyte` elements.
328  * Returns: `true` if the stream type is a byte output stream.
329  */
330 bool isByteOutputStream(StreamType)() {
331     return isOutputStream!(StreamType, ubyte);
332 }
333 
334 /** 
335  * Determines if the given template argument is a closable stream type, which
336  * defines a `Optional!StreamError closeStream()` method as a means to close
337  * and/or deallocate the underlying resource that the stream reads from or
338  * writes to.
339  *
340  * Returns: `true` if the given argument is a closable stream.
341  */
342 bool isClosableStream(S)() {
343     static if (
344         isSomeStream!S &&
345         __traits(hasMember, S, CLOSABLE_STREAM_METHOD) &&
346         isCallable!(__traits(getMember, S, CLOSABLE_STREAM_METHOD))
347     ) {
348         alias closeFunction = __traits(getMember, S, CLOSABLE_STREAM_METHOD);
349         alias params = Parameters!closeFunction;
350         return (is(ReturnType!closeFunction == OptionalStreamError) && params.length == 0);
351     } else {
352         return false;
353     }
354 }
355 
356 unittest {
357     struct S1 {
358         StreamResult readFromStream(ubyte[] buffer) {
359             return StreamResult(0); // cov-ignore
360         }
361         OptionalStreamError closeStream() {
362             return OptionalStreamError.init; // cov-ignore
363         }
364     }
365     assert(isClosableStream!S1);
366     assert(isClosableStream!(S1*));
367     struct S2 {
368         StreamResult readFromStream(ubyte[] buffer) {
369             return StreamResult(0); // cov-ignore
370         }
371     }
372     assert(!isClosableStream!S2);
373     struct S3 {}
374     assert(!isClosableStream!S3);
375 }
376 
377 /** 
378  * Determines if the given template argument is a flushable stream type, which
379  * is any output stream that defines a `Optional!StreamError flushStream()`
380  * method, which should cause any data buffered by the stream or its resources
381  * to be flushed. The exact nature of how a flush operates is implementation-
382  * dependent.
383  * Returns: `true` if the given argument is a flushable stream.
384  */
385 bool isFlushableStream(S)() {
386     static if (
387         isSomeOutputStream!S &&
388         __traits(hasMember, S, FLUSHABLE_STREAM_METHOD) &&
389         isCallable!(__traits(getMember, S, FLUSHABLE_STREAM_METHOD))
390     ) {
391         alias flushFunction = __traits(getMember, S, FLUSHABLE_STREAM_METHOD);
392         alias params = Parameters!flushFunction;
393         return (is(ReturnType!flushFunction == OptionalStreamError) && params.length == 0);
394     } else {
395         return false;
396     }
397 }
398 
399 unittest {
400     struct S1 {
401         StreamResult writeToStream(ubyte[] buffer) {
402             return StreamResult(0); // cov-ignore
403         }
404         OptionalStreamError flushStream() {
405             return OptionalStreamError.init; // cov-ignore
406         }
407     }
408     assert(isFlushableStream!S1);
409     assert(isFlushableStream!(S1*));
410     struct S2 {
411         StreamResult writeToStream(ubyte[] buffer) {
412             return StreamResult(0); // cov-ignore
413         }
414     }
415     assert(!isFlushableStream!S2);
416     struct S3 {}
417     assert(!isFlushableStream!S3);
418 }
419 
420 /** 
421  * Determines if the given template argument is a seekable stream type, which
422  * is any stream, input or output, that defines a `Optional!StreamError seekInStream(ulong offset)`
423  * function for seeking to a particular position in a stream, specified by the
424  * `offset` in terms of elements.
425  * Returns: `true` if the given argument is a seekable stream.
426  */
427 bool isSeekableStream(S)() {
428     static if (
429         isSomeStream!S &&
430         __traits(hasMember, S, SEEKABLE_STREAM_METHOD) &&
431         isCallable!(__traits(getMember, S, SEEKABLE_STREAM_METHOD))
432     ) {
433         alias seekFunction = __traits(getMember, S, SEEKABLE_STREAM_METHOD);
434         alias params = Parameters!seekFunction;
435         return (
436             is(ReturnType!seekFunction == OptionalStreamError) &&
437             params.length == 1 && is(params[0] == ulong)
438         );
439     } else {
440         return false;
441     }
442 }
443 
444 /**
445  * Determines if the given template argument is a pointer to a stream.
446  * Returns: `true` if the given argument is a pointer to a stream.
447  */
448 bool isPointerToStream(S)() {
449     return is(S == B*, B) && isSomeStream!S;
450 }
451 
452 unittest {
453     struct S1 {
454         StreamResult readFromStream(ubyte[] buf) {
455             return StreamResult(0); // cov-ignore
456         }
457     }
458     struct S2 {
459         void doStuff() {} // cov-ignore
460     }
461     assert(isPointerToStream!(S1*));
462     assert(!isPointerToStream!S1);
463     assert(!isPointerToStream!(S1**));
464     assert(!isPointerToStream!(S2*));
465 }
466 
467 /** 
468  * Determines if the given template argument is a direct stream type, and that
469  * it is not a pointer. If true, then this implies that the caller "owns" the
470  * stream, and the stream should not be used outside of owner's scope.
471  * Returns: `true` if the given argument is a stream, and not a pointer to one.
472  */
473 bool isNonPointerStream(S)() {
474     return !is(S == B*, B) && isSomeStream!S;
475 }
476 
477 unittest {
478     struct S1 {
479         StreamResult readFromStream(ubyte[] buf) {
480             return StreamResult(0); // cov-ignore
481         }
482     }
483     struct S2 {
484         void doStuff() {} // cov-ignore
485     }
486     assert(isNonPointerStream!(S1));
487     assert(!isNonPointerStream!(S1*));
488     assert(!isNonPointerStream!(S1**));
489     assert(!isNonPointerStream!(S2*));
490     assert(!isNonPointerStream!S2);
491 }
492 
493 /** 
494  * An input stream that always reads 0 elements.
495  */
496 struct NoOpInputStream(T) {
497     /** 
498      * Reads zero elements.
499      * Params:
500      *   buffer = A buffer.
501      * Returns: Always 0.
502      */
503     StreamResult readFromStream(T[] buffer) {
504         return StreamResult(0);
505     }
506 }
507 
508 /** 
509  * An output stream that always writes 0 elements.
510  */
511 struct NoOpOutputStream(T) {
512     /** 
513      * Writes zero elements.
514      * Params:
515      *   buffer = A buffer.
516      * Returns: Always 0.
517      */
518     StreamResult writeToStream(T[] buffer) {
519         return StreamResult(0);
520     }
521 }
522 
523 /** 
524  * An input stream that always returns an error response.
525  */
526 struct ErrorInputStream(T) {
527     /**
528      * Always emits an error response when called.
529      * Params:
530      *   buffer = A buffer.
531      * Returns: A stream error.
532      */
533     StreamResult readFromStream(T[] buffer) {
534         return StreamResult(StreamError("An error occurred.", -1));
535     }
536 }
537 
538 /** 
539  * An output stream that always returns an error response.
540  */
541 struct ErrorOutputStream(T) {
542     /**
543      * Always emits an error response when called.
544      * Params:
545      *   buffer = A buffer.
546      * Returns: A stream error.
547      */
548     StreamResult writeToStream(T[] buffer) {
549         return StreamResult(StreamError("An error occurred.", -1));
550     }
551 }
552 
553 unittest {
554     auto s1 = NoOpInputStream!ubyte();
555     ubyte[3] buffer;
556     assert(s1.readFromStream(buffer) == StreamResult(0));
557     assert(buffer == [0, 0, 0]);
558     
559     auto s2 = NoOpOutputStream!ubyte();
560     assert(s2.writeToStream(buffer) == StreamResult(0));
561 
562     auto s3 = ErrorInputStream!ubyte();
563     assert(s3.readFromStream(buffer).hasError);
564 
565     auto s4 = ErrorOutputStream!ubyte();
566     assert(s4.writeToStream(buffer).hasError);
567 }