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 |