OLD | NEW |
| (Empty) |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library barback.utils; | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:typed_data'; | |
9 | |
10 import 'package:stack_trace/stack_trace.dart'; | |
11 | |
12 /// A class that represents a value or an error. | |
13 class Fallible<E> { | |
14 /// Whether [this] has a [value], as opposed to an [error]. | |
15 final bool hasValue; | |
16 | |
17 /// Whether [this] has an [error], as opposed to a [value]. | |
18 bool get hasError => !hasValue; | |
19 | |
20 /// The value. | |
21 /// | |
22 /// This will be `null` if [this] has an [error]. | |
23 final E _value; | |
24 | |
25 /// The value. | |
26 /// | |
27 /// This will throw a [StateError] if [this] has an [error]. | |
28 E get value { | |
29 if (hasValue) return _value; | |
30 throw new StateError("Fallible has no value.\n" | |
31 "$_error$_stackTraceSuffix"); | |
32 } | |
33 | |
34 /// The error. | |
35 /// | |
36 /// This will be `null` if [this] has a [value]. | |
37 final _error; | |
38 | |
39 /// The error. | |
40 /// | |
41 /// This will throw a [StateError] if [this] has a [value]. | |
42 get error { | |
43 if (hasError) return _error; | |
44 throw new StateError("Fallible has no error."); | |
45 } | |
46 | |
47 /// The stack trace for [_error]. | |
48 /// | |
49 /// This will be `null` if [this] has a [value], or if no stack trace was | |
50 /// provided. | |
51 final StackTrace _stackTrace; | |
52 | |
53 /// The stack trace for [error]. | |
54 /// | |
55 /// This will throw a [StateError] if [this] has a [value]. | |
56 StackTrace get stackTrace { | |
57 if (hasError) return _stackTrace; | |
58 throw new StateError("Fallible has no error."); | |
59 } | |
60 | |
61 Fallible.withValue(this._value) | |
62 : _error = null, | |
63 _stackTrace = null, | |
64 hasValue = true; | |
65 | |
66 Fallible.withError(this._error, [this._stackTrace]) | |
67 : _value = null, | |
68 hasValue = false; | |
69 | |
70 /// Returns a completed Future with the same value or error as [this]. | |
71 Future toFuture() { | |
72 if (hasValue) return new Future.value(value); | |
73 return new Future.error(error, stackTrace); | |
74 } | |
75 | |
76 String toString() { | |
77 if (hasValue) return "Fallible value: $value"; | |
78 return "Fallible error: $error$_stackTraceSuffix"; | |
79 } | |
80 | |
81 String get _stackTraceSuffix { | |
82 if (stackTrace == null) return ""; | |
83 return "\nStack trace:\n${new Chain.forTrace(_stackTrace).terse}"; | |
84 } | |
85 } | |
86 | |
87 /// Converts a number in the range [0-255] to a two digit hex string. | |
88 /// | |
89 /// For example, given `255`, returns `ff`. | |
90 String byteToHex(int byte) { | |
91 assert(byte >= 0 && byte <= 255); | |
92 | |
93 const DIGITS = "0123456789abcdef"; | |
94 return DIGITS[(byte ~/ 16) % 16] + DIGITS[byte % 16]; | |
95 } | |
96 | |
97 /// Returns a sentence fragment listing the elements of [iter]. | |
98 /// | |
99 /// This converts each element of [iter] to a string and separates them with | |
100 /// commas and/or "and" where appropriate. | |
101 String toSentence(Iterable iter) { | |
102 if (iter.length == 1) return iter.first.toString(); | |
103 return iter.take(iter.length - 1).join(", ") + " and ${iter.last}"; | |
104 } | |
105 | |
106 /// Returns [name] if [number] is 1, or the plural of [name] otherwise. | |
107 /// | |
108 /// By default, this just adds "s" to the end of [name] to get the plural. If | |
109 /// [plural] is passed, that's used instead. | |
110 String pluralize(String name, int number, {String plural}) { | |
111 if (number == 1) return name; | |
112 if (plural != null) return plural; | |
113 return '${name}s'; | |
114 } | |
115 | |
116 /// Converts [input] into a [Uint8List]. | |
117 /// | |
118 /// If [input] is a [TypedData], this just returns a view on [input]. | |
119 Uint8List toUint8List(List<int> input) { | |
120 if (input is Uint8List) return input; | |
121 if (input is TypedData) { | |
122 // TODO(nweiz): remove "as" when issue 11080 is fixed. | |
123 return new Uint8List.view((input as TypedData).buffer); | |
124 } | |
125 return new Uint8List.fromList(input); | |
126 } | |
127 | |
128 /// Group the elements in [iter] by the value returned by [fn]. | |
129 /// | |
130 /// This returns a map whose keys are the return values of [fn] and whose values | |
131 /// are lists of each element in [iter] for which [fn] returned that key. | |
132 Map<Object, List> groupBy(Iterable iter, fn(element)) { | |
133 var map = {}; | |
134 for (var element in iter) { | |
135 var list = map.putIfAbsent(fn(element), () => []); | |
136 list.add(element); | |
137 } | |
138 return map; | |
139 } | |
140 | |
141 /// Flattens nested lists inside an iterable into a single list containing only | |
142 /// non-list elements. | |
143 List flatten(Iterable nested) { | |
144 var result = []; | |
145 helper(list) { | |
146 for (var element in list) { | |
147 if (element is List) { | |
148 helper(element); | |
149 } else { | |
150 result.add(element); | |
151 } | |
152 } | |
153 } | |
154 helper(nested); | |
155 return result; | |
156 } | |
157 | |
158 /// Returns the union of all elements in each set in [sets]. | |
159 Set unionAll(Iterable<Set> sets) => | |
160 sets.fold(new Set(), (union, set) => union.union(set)); | |
161 | |
162 /// Creates a new map from [map] with new keys and values. | |
163 /// | |
164 /// The return values of [keyFn] are used as the keys and the return values of | |
165 /// [valueFn] are used as the values for the new map. | |
166 Map mapMap(Map map, keyFn(key, value), valueFn(key, value)) => | |
167 new Map.fromIterable(map.keys, | |
168 key: (key) => keyFn(key, map[key]), | |
169 value: (key) => valueFn(key, map[key])); | |
170 | |
171 /// Creates a new map from [map] with the same keys. | |
172 /// | |
173 /// The return values of [fn] are used as the values for the new map. | |
174 Map mapMapValues(Map map, fn(key, value)) => mapMap(map, (key, _) => key, fn); | |
175 | |
176 /// Creates a new map from [map] with the same keys. | |
177 /// | |
178 /// The return values of [fn] are used as the keys for the new map. | |
179 Map mapMapKeys(Map map, fn(key, value)) => mapMap(map, fn, (_, value) => value); | |
180 | |
181 /// Returns whether [set1] has exactly the same elements as [set2]. | |
182 bool setEquals(Set set1, Set set2) => | |
183 set1.length == set2.length && set1.containsAll(set2); | |
184 | |
185 /// Merges [streams] into a single stream that emits events from all sources. | |
186 /// | |
187 /// If [broadcast] is true, this will return a broadcast stream; otherwise, it | |
188 /// will return a buffered stream. | |
189 Stream mergeStreams(Iterable<Stream> streams, {bool broadcast: false}) { | |
190 streams = streams.toList(); | |
191 var doneCount = 0; | |
192 // Use a sync stream to preserve the synchrony behavior of the input streams. | |
193 // If the inputs are sync, then this will be sync as well; if the inputs are | |
194 // async, then the events we receive will also be async, and forwarding them | |
195 // sync won't change that. | |
196 var controller = broadcast ? new StreamController.broadcast(sync: true) | |
197 : new StreamController(sync: true); | |
198 | |
199 for (var stream in streams) { | |
200 stream.listen( | |
201 controller.add, | |
202 onError: controller.addError, | |
203 onDone: () { | |
204 doneCount++; | |
205 if (doneCount == streams.length) controller.close(); | |
206 }); | |
207 } | |
208 | |
209 return controller.stream; | |
210 } | |
211 | |
212 /// Prepends each line in [text] with [prefix]. If [firstPrefix] is passed, the | |
213 /// first line is prefixed with that instead. | |
214 String prefixLines(String text, {String prefix: '| ', String firstPrefix}) { | |
215 var lines = text.split('\n'); | |
216 if (firstPrefix == null) { | |
217 return lines.map((line) => '$prefix$line').join('\n'); | |
218 } | |
219 | |
220 var firstLine = "$firstPrefix${lines.first}"; | |
221 lines = lines.skip(1).map((line) => '$prefix$line').toList(); | |
222 lines.insert(0, firstLine); | |
223 return lines.join('\n'); | |
224 } | |
225 | |
226 /// Returns a [Future] that completes after pumping the event queue [times] | |
227 /// times. By default, this should pump the event queue enough times to allow | |
228 /// any code to run, as long as it's not waiting on some external event. | |
229 Future pumpEventQueue([int times=20]) { | |
230 if (times == 0) return new Future.value(); | |
231 // We use a delayed future to allow microtask events to finish. The | |
232 // Future.value or Future() constructors use scheduleMicrotask themselves and | |
233 // would therefore not wait for microtask callbacks that are scheduled after | |
234 // invoking this method. | |
235 return new Future.delayed(Duration.ZERO, () => pumpEventQueue(times - 1)); | |
236 } | |
237 | |
238 /// Like `new Future`, but avoids issue 11911 by using `new Future.value` under | |
239 /// the covers. | |
240 // TODO(jmesserly): doc comment changed to due 14601. | |
241 Future newFuture(callback()) => new Future.value().then((_) => callback()); | |
242 | |
243 /// Like [Future.sync], but wraps the Future in [Chain.track] as well. | |
244 Future syncFuture(callback()) => Chain.track(new Future.sync(callback)); | |
245 | |
246 /// Returns a buffered stream that will emit the same values as the stream | |
247 /// returned by [future] once [future] completes. | |
248 /// | |
249 /// If [future] completes to an error, the return value will emit that error and | |
250 /// then close. | |
251 /// | |
252 /// If [broadcast] is true, a broadcast stream is returned. This assumes that | |
253 /// the stream returned by [future] will be a broadcast stream as well. | |
254 /// [broadcast] defaults to false. | |
255 Stream futureStream(Future<Stream> future, {bool broadcast: false}) { | |
256 var subscription; | |
257 var controller; | |
258 | |
259 future = future.catchError((e, stackTrace) { | |
260 // Since [controller] is synchronous, it's likely that emitting an error | |
261 // will cause it to be cancelled before we call close. | |
262 if (controller != null) controller.addError(e, stackTrace); | |
263 if (controller != null) controller.close(); | |
264 controller = null; | |
265 }); | |
266 | |
267 onListen() { | |
268 future.then((stream) { | |
269 if (controller == null) return; | |
270 subscription = stream.listen( | |
271 controller.add, | |
272 onError: controller.addError, | |
273 onDone: controller.close); | |
274 }); | |
275 } | |
276 | |
277 onCancel() { | |
278 if (subscription != null) subscription.cancel(); | |
279 subscription = null; | |
280 controller = null; | |
281 } | |
282 | |
283 if (broadcast) { | |
284 controller = new StreamController.broadcast( | |
285 sync: true, onListen: onListen, onCancel: onCancel); | |
286 } else { | |
287 controller = new StreamController( | |
288 sync: true, onListen: onListen, onCancel: onCancel); | |
289 } | |
290 return controller.stream; | |
291 } | |
292 | |
293 /// Returns a [Stream] that will emit the same values as the stream returned by | |
294 /// [callback]. | |
295 /// | |
296 /// [callback] will only be called when the returned [Stream] gets a subscriber. | |
297 Stream callbackStream(Stream callback()) { | |
298 var subscription; | |
299 var controller; | |
300 controller = new StreamController(onListen: () { | |
301 subscription = callback().listen(controller.add, | |
302 onError: controller.addError, | |
303 onDone: controller.close); | |
304 }, | |
305 onCancel: () => subscription.cancel(), | |
306 onPause: () => subscription.pause(), | |
307 onResume: () => subscription.resume(), | |
308 sync: true); | |
309 return controller.stream; | |
310 } | |
311 | |
312 /// Creates a single-subscription stream from a broadcast stream. | |
313 /// | |
314 /// The returned stream will enqueue events from [broadcast] until a listener is | |
315 /// attached, then pipe events to that listener. | |
316 Stream broadcastToSingleSubscription(Stream broadcast) { | |
317 if (!broadcast.isBroadcast) return broadcast; | |
318 | |
319 // TODO(nweiz): Implement this using a transformer when issues 18588 and 18586 | |
320 // are fixed. | |
321 var subscription; | |
322 var controller = new StreamController(onCancel: () => subscription.cancel()); | |
323 subscription = broadcast.listen(controller.add, | |
324 onError: controller.addError, | |
325 onDone: controller.close); | |
326 return controller.stream; | |
327 } | |
328 | |
329 /// A regular expression to match the exception prefix that some exceptions' | |
330 /// [Object.toString] values contain. | |
331 final _exceptionPrefix = new RegExp(r'^([A-Z][a-zA-Z]*)?(Exception|Error): '); | |
332 | |
333 /// Get a string description of an exception. | |
334 /// | |
335 /// Many exceptions include the exception class name at the beginning of their | |
336 /// [toString], so we remove that if it exists. | |
337 String getErrorMessage(error) => | |
338 error.toString().replaceFirst(_exceptionPrefix, ''); | |
339 | |
340 /// Returns a human-friendly representation of [duration]. | |
341 String niceDuration(Duration duration) { | |
342 var result = duration.inMinutes > 0 ? "${duration.inMinutes}:" : ""; | |
343 | |
344 var s = duration.inSeconds % 59; | |
345 var ms = (duration.inMilliseconds % 1000) ~/ 100; | |
346 return result + "$s.${ms}s"; | |
347 } | |
OLD | NEW |