1 /**
2  * This module defines "mapping" input and output streams, which map elements
3  * of a wrapped stream to another type. For example, a mapping input stream
4  * could wrap around an input stream of strings, and parse each one as an int.
5  *
6  * Mapping streams generally operate using a templated function alias; that is,
7  * you provide a function at compile-time, and a stream on which to operate.
8  * The mapping function should take a single argument, and produce a result of
9  * any type. However, if your function returns a `MapResult` type (as defined
10  * in this module), the mapping streams will acknowledge any stream errors that
11  * occur.
12  */
13 module streams.types.mapping;
14 
15 import streams.primitives;
16 import streams.utils;
17 
18 /**
19  * A return type to use when a mapping function could encounter an error.
20  * Mapping streams have a special case to treat this return type.
21  */
22 alias MapResult(E) = Either!(StreamError, "error", E, "element");
23 
24 /**
25  * An input stream that wraps another stream, and applies a function to each
26  * element read from that stream.
27  */
28 struct MappingInputStream(alias f, S) if (isSomeInputStream!S && isMappingFunction!(typeof(f), StreamType!S)) {
29     alias Ein = StreamType!S;
30     alias Eout = MappingFunctionReturnType!(typeof(f));
31     
32     private S stream;
33 
34     this(S stream) {
35         this.stream = stream;
36     }
37 
38     StreamResult readFromStream(Eout[] buffer) {
39         Ein[1] inputBuffer;
40         for (uint i = 0; i < buffer.length; i++) {
41             StreamResult result = this.stream.readFromStream(inputBuffer);
42             if (result.hasError) return result;
43             if (result.count == 0) return StreamResult(i);
44             static if (returnsMapResult!(typeof(f))) {
45                 MapResult!Eout mapResult = f(inputBuffer[0]);
46                 if (mapResult.hasError) return StreamResult(mapResult.error);
47                 buffer[i] = mapResult.element;
48             } else {
49                 buffer[i] = f(inputBuffer[0]);
50             }
51         }
52         return StreamResult(cast(uint) buffer.length);
53     }
54 }
55 
56 /**
57  * Creates a mapping input stream that applies the function `f` to all elements
58  * read from the stream.
59  * Params:
60  *   stream = The stream to wrap.
61  * Returns: A mapping input stream.
62  */
63 MappingInputStream!(f, S) mappingInputStreamFor(alias f, S)(
64     S stream
65 ) if (isSomeInputStream!S && isMappingFunction!(typeof(f), StreamType!S)) {
66     return MappingInputStream!(f, S)(stream);
67 }
68 
69 unittest {
70     import streams;
71     int[3] buf1 = [1, 2, 3];
72     auto sIn1 = arrayInputStreamFor(buf1);
73     auto map1 = MappingInputStream!((int a) => a + 1, typeof(&sIn1))(&sIn1);
74     assert(isInputStream!(typeof(map1), int));
75     int[3] bufOut1;
76     assert(map1.readFromStream(bufOut1) == StreamResult(3));
77     assert(bufOut1 == [2, 3, 4]);
78     sIn1.reset();
79 
80     import std.stdio;
81     // Test that the function helper works too.
82     auto map2 = mappingInputStreamFor!((int a) => a - 1)(&sIn1);
83     int[3] bufOut2;
84     assert(map2.readFromStream(bufOut2) == StreamResult(3));
85     assert(bufOut2 == [0, 1, 2]);
86 }
87 
88 /**
89  * An output stream that applies a function to each element that's written to
90  * it before writing to the underlying stream.
91  */
92 struct MappingOutputStream(alias f, S) if (
93     isSomeOutputStream!S &&
94     isSomeMappingFunction!(typeof(f)) &&
95     is(MappingFunctionReturnType!(typeof(f)) == StreamType!S)
96 ) {
97     alias Ein = MappingFunctionInputType!(typeof(f));
98     alias Eout = StreamType!S;
99 
100     private S stream;
101 
102     this(S stream) {
103         this.stream = stream;
104     }
105 
106     StreamResult writeToStream(Ein[] buffer) {
107         Eout[1] outputBuffer = [Eout.init];
108         foreach (element; buffer) {
109             static if (returnsMapResult!(typeof(f))) {
110                 MapResult!Eout mapResult = f(element);
111                 if (mapResult.hasError) return StreamResult(mapResult.error);
112                 outputBuffer[0] = mapResult.element;
113             } else {
114                 outputBuffer[0] = f(element);
115             }
116             StreamResult writeResult = this.stream.writeToStream(outputBuffer);
117             if (writeResult.hasError) return writeResult;
118             if (writeResult.count != 1) {
119                 return StreamResult(StreamError("Failed to write element.", writeResult.count));
120             }
121         }
122         return StreamResult(cast(uint) buffer.length);
123     }
124 }
125 
126 /**
127  * Creates a mapping output stream that applies the function `f` to all elements
128  * written to the stream.
129  * Params:
130  *   stream = The stream to wrap.
131  * Returns: A mapping output stream.
132  */
133 MappingOutputStream!(f, S) mappingOutputStreamFor(alias f, S)(
134     S stream
135 ) if (
136     isSomeOutputStream!S &&
137     isSomeMappingFunction!(typeof(f)) &&
138     is(MappingFunctionReturnType!(typeof(f)) == StreamType!S)
139 ) {
140     return MappingOutputStream!(f, S)(stream);
141 }
142 
143 unittest {
144     import streams;
145     int[3] buf1 = [1, 2, 3];
146     auto sOut1 = arrayOutputStreamFor!double;
147     auto mapOut1 = mappingOutputStreamFor!((int a) => 0.25 * a)(&sOut1);
148     assert(isOutputStream!(typeof(mapOut1), int));
149     assert(mapOut1.writeToStream(buf1) == StreamResult(3));
150     assert(sOut1.toArrayRaw() == [0.25, 0.5, 0.75]);
151 
152     // Test writing to an error stream.
153     auto sOut2 = ErrorOutputStream!ubyte();
154     auto mapOut2 = mappingOutputStreamFor!((ulong a) => cast(ubyte) (a & 0xFF))(&sOut2);
155     ulong[2] buf2 = [123, 456];
156     assert(mapOut2.writeToStream(buf2).hasError);
157     // Also for a stream that doesn't write anything.
158     auto sOut3 = NoOpOutputStream!ubyte();
159     auto mapOut3 = mappingOutputStreamFor!((ulong a) => cast(ubyte) (a & 0xFF))(&sOut3);
160     assert(mapOut3.writeToStream(buf2).hasError);
161 }
162 
163 private bool isSomeMappingFunction(F)() {
164     import std.traits : isSomeFunction, ReturnType, isInstanceOf, TemplateArgsOf, Parameters;
165     static if (isSomeFunction!F) {
166         static if (Parameters!(F).length == 1) {
167             return !is(ReturnType!F == void);
168         } else {
169             return false;
170         }
171     } else {
172         return false;
173     }
174 }
175 
176 unittest {
177     auto f1 = (int a) => cast(char) a;
178     assert(isSomeMappingFunction!(typeof(f1)));
179     auto f2 = (ubyte u) => MapResult!bool(true);
180     assert(isSomeMappingFunction!(typeof(f2)));
181     auto f3 = () => -1;
182     assert(!isSomeMappingFunction!(typeof(f3)));
183 }
184 
185 private template MappingFunctionInputType(F) if (isSomeMappingFunction!F) {
186     import std.traits : Parameters;
187     alias MappingFunctionInputType = Parameters!(F)[0];
188 }
189 
190 private bool isMappingFunction(F, Ein)() if (isSomeMappingFunction!F) {
191     return is(MappingFunctionInputType!F == Ein);
192 }
193 
194 private template MappingFunctionReturnType(F) if (isSomeMappingFunction!F) {
195     import std.traits : ReturnType, TemplateArgsOf, isInstanceOf;
196     alias r = ReturnType!F;
197     static if (isInstanceOf!(Either, r)) {
198         alias args = TemplateArgsOf!r;
199         static if (args[1] == "element") {
200             alias MappingFunctionReturnType = args[0];
201         } else {
202             alias MappingFunctionReturnType = args[2];
203         }
204     } else {
205         alias MappingFunctionReturnType = r;
206     }
207 }
208 
209 unittest {
210     auto f1 = (int a) => 0.5f * a;
211     assert(is(MappingFunctionReturnType!(typeof(f1)) == float));
212     auto f2 = (bool b) => !b;
213     assert(is(MappingFunctionReturnType!(typeof(f2)) == bool));
214     auto f3 = (ubyte b) => MapResult!char('A');
215     assert(is(MappingFunctionReturnType!(typeof(f3)) == char));
216 }
217 
218 private bool returnsMapResult(F)() if (isSomeMappingFunction!F) {
219     import std.traits : ReturnType, isInstanceOf;
220     alias r = ReturnType!F;
221     return isInstanceOf!(Either, r);
222 }
223 
224 unittest {
225     auto f1 = (int a) => MapResult!bool(a > 5);
226     assert(returnsMapResult!(typeof(f1)));
227     auto f2 = (bool b) => !b;
228     assert(!returnsMapResult!(typeof(f2)));
229 }