OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012, 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 utils; | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:convert'; | |
9 import 'dart:typed_data'; | |
10 | |
11 import 'package:stack_trace/stack_trace.dart'; | |
12 | |
13 import 'byte_stream.dart'; | |
14 | |
15 /// Converts a URL query string (or `application/x-www-form-urlencoded` body) | |
16 /// into a [Map] from parameter names to values. | |
17 /// | |
18 /// queryToMap("foo=bar&baz=bang&qux"); | |
19 /// //=> {"foo": "bar", "baz": "bang", "qux": ""} | |
20 Map<String, String> queryToMap(String queryList, {Encoding encoding}) { | |
21 var map = {}; | |
22 for (var pair in queryList.split("&")) { | |
23 var split = split1(pair, "="); | |
24 if (split.isEmpty) continue; | |
25 var key = Uri.decodeQueryComponent(split[0], encoding: encoding); | |
26 var value = Uri.decodeQueryComponent(split.length > 1 ? split[1] : "", | |
27 encoding: encoding); | |
28 map[key] = value; | |
29 } | |
30 return map; | |
31 } | |
32 | |
33 /// Converts a [Map] from parameter names to values to a URL query string. | |
34 /// | |
35 /// mapToQuery({"foo": "bar", "baz": "bang"}); | |
36 /// //=> "foo=bar&baz=bang" | |
37 String mapToQuery(Map<String, String> map, {Encoding encoding}) { | |
38 var pairs = <List<String>>[]; | |
39 map.forEach((key, value) => | |
40 pairs.add([Uri.encodeQueryComponent(key, encoding: encoding), | |
41 Uri.encodeQueryComponent(value, encoding: encoding)])); | |
42 return pairs.map((pair) => "${pair[0]}=${pair[1]}").join("&"); | |
43 } | |
44 | |
45 /// Like [String.split], but only splits on the first occurrence of the pattern. | |
46 /// This will always return an array of two elements or fewer. | |
47 /// | |
48 /// split1("foo,bar,baz", ","); //=> ["foo", "bar,baz"] | |
49 /// split1("foo", ","); //=> ["foo"] | |
50 /// split1("", ","); //=> [] | |
51 List<String> split1(String toSplit, String pattern) { | |
52 if (toSplit.isEmpty) return <String>[]; | |
53 | |
54 var index = toSplit.indexOf(pattern); | |
55 if (index == -1) return [toSplit]; | |
56 return [ | |
57 toSplit.substring(0, index), | |
58 toSplit.substring(index + pattern.length) | |
59 ]; | |
60 } | |
61 | |
62 /// Returns the [Encoding] that corresponds to [charset]. Returns [fallback] if | |
63 /// [charset] is null or if no [Encoding] was found that corresponds to | |
64 /// [charset]. | |
65 Encoding encodingForCharset(String charset, [Encoding fallback = LATIN1]) { | |
66 if (charset == null) return fallback; | |
67 var encoding = Encoding.getByName(charset); | |
68 return encoding == null ? fallback : encoding; | |
69 } | |
70 | |
71 | |
72 /// Returns the [Encoding] that corresponds to [charset]. Throws a | |
73 /// [FormatException] if no [Encoding] was found that corresponds to [charset]. | |
74 /// [charset] may not be null. | |
75 Encoding requiredEncodingForCharset(String charset) { | |
76 var encoding = Encoding.getByName(charset); | |
77 if (encoding != null) return encoding; | |
78 throw new FormatException('Unsupported encoding "$charset".'); | |
79 } | |
80 | |
81 /// A regular expression that matches strings that are composed entirely of | |
82 /// ASCII-compatible characters. | |
83 final RegExp _ASCII_ONLY = new RegExp(r"^[\x00-\x7F]+$"); | |
84 | |
85 /// Returns whether [string] is composed entirely of ASCII-compatible | |
86 /// characters. | |
87 bool isPlainAscii(String string) => _ASCII_ONLY.hasMatch(string); | |
88 | |
89 /// Converts [input] into a [Uint8List]. | |
90 /// | |
91 /// If [input] is a [TypedData], this just returns a view on [input]. | |
92 Uint8List toUint8List(List<int> input) { | |
93 if (input is Uint8List) return input; | |
94 if (input is TypedData) { | |
95 // TODO(nweiz): remove "as" when issue 11080 is fixed. | |
96 return new Uint8List.view((input as TypedData).buffer); | |
97 } | |
98 return new Uint8List.fromList(input); | |
99 } | |
100 | |
101 /// If [stream] is already a [ByteStream], returns it. Otherwise, wraps it in a | |
102 /// [ByteStream]. | |
103 ByteStream toByteStream(Stream<List<int>> stream) { | |
104 if (stream is ByteStream) return stream; | |
105 return new ByteStream(stream); | |
106 } | |
107 | |
108 /// Calls [onDone] once [stream] (a single-subscription [Stream]) is finished. | |
109 /// The return value, also a single-subscription [Stream] should be used in | |
110 /// place of [stream] after calling this method. | |
111 Stream onDone(Stream stream, void onDone()) { | |
112 var pair = tee(stream); | |
113 pair.first.listen((_) {}, onError: (_) {}, onDone: onDone); | |
114 return pair.last; | |
115 } | |
116 | |
117 // TODO(nweiz): remove this when issue 7786 is fixed. | |
118 /// Pipes all data and errors from [stream] into [sink]. When [stream] is done, | |
119 /// [sink] is closed and the returned [Future] is completed. | |
120 Future store(Stream stream, EventSink sink) { | |
121 var completer = new Completer(); | |
122 stream.listen(sink.add, | |
123 onError: sink.addError, | |
124 onDone: () { | |
125 sink.close(); | |
126 completer.complete(); | |
127 }); | |
128 return completer.future; | |
129 } | |
130 | |
131 /// Pipes all data and errors from [stream] into [sink]. Completes [Future] once | |
132 /// [stream] is done. Unlike [store], [sink] remains open after [stream] is | |
133 /// done. | |
134 Future writeStreamToSink(Stream stream, EventSink sink) { | |
135 var completer = new Completer(); | |
136 stream.listen(sink.add, | |
137 onError: sink.addError, | |
138 onDone: () => completer.complete()); | |
139 return completer.future; | |
140 } | |
141 | |
142 /// Returns a [Future] that asynchronously completes to `null`. | |
143 Future get async => new Future.value(); | |
144 | |
145 /// Returns a closed [Stream] with no elements. | |
146 Stream get emptyStream => streamFromIterable([]); | |
147 | |
148 /// Creates a single-subscription stream that emits the items in [iter] and then | |
149 /// ends. | |
150 Stream streamFromIterable(Iterable iter) { | |
151 var controller = new StreamController(sync: true); | |
152 iter.forEach(controller.add); | |
153 controller.close(); | |
154 return controller.stream; | |
155 } | |
156 | |
157 // TODO(nweiz): remove this when issue 7787 is fixed. | |
158 /// Creates two single-subscription [Stream]s that each emit all values and | |
159 /// errors from [stream]. This is useful if [stream] is single-subscription but | |
160 /// multiple subscribers are necessary. | |
161 Pair<Stream, Stream> tee(Stream stream) { | |
162 var controller1 = new StreamController(sync: true); | |
163 var controller2 = new StreamController(sync: true); | |
164 stream.listen((value) { | |
165 controller1.add(value); | |
166 controller2.add(value); | |
167 }, onError: (error, [StackTrace stackTrace]) { | |
168 controller1.addError(error, stackTrace); | |
169 controller2.addError(error, stackTrace); | |
170 }, onDone: () { | |
171 controller1.close(); | |
172 controller2.close(); | |
173 }); | |
174 return new Pair<Stream, Stream>(controller1.stream, controller2.stream); | |
175 } | |
176 | |
177 /// A pair of values. | |
178 class Pair<E, F> { | |
179 E first; | |
180 F last; | |
181 | |
182 Pair(this.first, this.last); | |
183 | |
184 String toString() => '($first, $last)'; | |
185 | |
186 bool operator==(other) { | |
187 if (other is! Pair) return false; | |
188 return other.first == first && other.last == last; | |
189 } | |
190 | |
191 int get hashCode => first.hashCode ^ last.hashCode; | |
192 } | |
193 | |
194 /// Configures [future] so that its result (success or exception) is passed on | |
195 /// to [completer]. | |
196 void chainToCompleter(Future future, Completer completer) { | |
197 future.then(completer.complete, onError: completer.completeError); | |
198 } | |
199 | |
200 /// Like [Future.sync], but wraps the Future in [Chain.track] as well. | |
201 Future syncFuture(callback()) => Chain.track(new Future.sync(callback)); | |
OLD | NEW |