Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(282)

Unified Diff: generated/googleapis/lib/src/common_internal.dart

Issue 559053002: Generate 0.1.0 version of googleapis/googleapis_beta (Closed) Base URL: git@github.com:dart-lang/googleapis.git@master
Patch Set: Created 6 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: generated/googleapis/lib/src/common_internal.dart
diff --git a/generated/googleapis/lib/src/common_internal.dart b/generated/googleapis/lib/src/common_internal.dart
new file mode 100644
index 0000000000000000000000000000000000000000..fd20b4ff5af1635323e488c71b4306f348354df4
--- /dev/null
+++ b/generated/googleapis/lib/src/common_internal.dart
@@ -0,0 +1,894 @@
+library googleapis.common_internal;
+
+import "dart:async";
+import "dart:convert";
+import "dart:collection" as collection;
+
+import "package:crypto/crypto.dart" as crypto;
+import "../common/common.dart" as common_external;
+import "package:http/http.dart" as http;
+
+const String USER_AGENT_STRING =
+ 'google-api-dart-client googleapis/0.1.0';
+
+const CONTENT_TYPE_JSON_UTF8 = 'application/json; charset=utf-8';
+
+/**
+ * Base class for all API clients, offering generic methods for
+ * HTTP Requests to the API
+ */
+class ApiRequester {
+ final http.Client _httpClient;
+ final String _rootUrl;
+ final String _basePath;
+
+ ApiRequester(this._httpClient, this._rootUrl, this._basePath) {
+ assert(_rootUrl.endsWith('/'));
+ }
+
+
+ /**
+ * Sends a HTTPRequest using [method] (usually GET or POST) to [requestUrl]
+ * using the specified [urlParams] and [queryParams]. Optionally include a
+ * [body] and/or [uploadMedia] in the request.
+ *
+ * If [uploadMedia] was specified [downloadOptions] must be
+ * [DownloadOptions.Metadata] or `null`.
+ *
+ * If [downloadOptions] is [DownloadOptions.Metadata] the result will be
+ * decoded as JSON.
+ *
+ * If [downloadOptions] is `null` the result will be a Future completing with
+ * `null`.
+ *
+ * Otherwise the result will be downloaded as a [common_external.Media]
+ */
+ Future request(String requestUrl, String method,
+ {String body, Map queryParams,
+ common_external.Media uploadMedia,
+ common_external.UploadOptions uploadOptions,
+ common_external.DownloadOptions downloadOptions:
+ common_external.DownloadOptions.Metadata}) {
+ if (uploadMedia != null &&
+ downloadOptions != common_external.DownloadOptions.Metadata) {
+ throw new ArgumentError('When uploading a [Media] you cannot download a '
+ '[Media] at the same time!');
+ }
+ common_external.ByteRange downloadRange;
+ if (downloadOptions is common_external.PartialDownloadOptions &&
+ !downloadOptions.isFullDownload) {
+ downloadRange = downloadOptions.range;
+ }
+
+ return _request(requestUrl, method, body, queryParams,
+ uploadMedia, uploadOptions,
+ downloadOptions,
+ downloadRange)
+ .then(_validateResponse).then((http.StreamedResponse response) {
+ if (downloadOptions == null) {
+ // If no download options are given, the response is of no interest
+ // and we will drain the stream.
+ return response.stream.drain();
+ } else if (downloadOptions == common_external.DownloadOptions.Metadata) {
+ // Downloading JSON Metadata
+ var stringStream = _decodeStreamAsText(response);
+ if (stringStream != null) {
+ return stringStream.join('').then((String bodyString) {
+ if (bodyString == '') return null;
+ return JSON.decode(bodyString);
+ });
+ } else {
+ throw new common_external.ApiRequestError(
+ "Unable to read response with content-type "
+ "${response.headers['content-type']}.");
+ }
+ } else {
+ // Downloading Media.
+ var contentType = response.headers['content-type'];
+ if (contentType == null) {
+ throw new common_external.ApiRequestError(
+ "No 'content-type' header in media response.");
+ }
+ var contentLength;
+ try {
+ contentLength = int.parse(response.headers['content-length']);
+ } catch (_) {
+ // We silently ignore errors here. If no content-length was specified
+ // we use `null`.
+ // Please note that the code below still asserts the content-length
+ // is correct for range downloads.
+ }
+
+ if (downloadRange != null) {
+ if (contentLength != downloadRange.length) {
+ throw new common_external.ApiRequestError(
+ "Content length of response does not match requested range "
+ "length.");
+ }
+ var contentRange = response.headers['content-range'];
+ var expected = 'bytes ${downloadRange.start}-${downloadRange.end}/';
+ if (contentRange == null || !contentRange.startsWith(expected)) {
+ throw new common_external.ApiRequestError("Attempting partial "
+ "download but got invalid 'Content-Range' header "
+ "(was: $contentRange, expected: $expected).");
+ }
+ }
+
+ return new common_external.Media(
+ response.stream, contentLength, contentType: contentType);
+ }
+ });
+ }
+
+ Future _request(String requestUrl, String method,
+ String body, Map queryParams,
+ common_external.Media uploadMedia,
+ common_external.UploadOptions uploadOptions,
+ common_external.DownloadOptions downloadOptions,
+ common_external.ByteRange downloadRange) {
+ bool downloadAsMedia =
+ downloadOptions != null &&
+ downloadOptions != common_external.DownloadOptions.Metadata;
+
+ if (queryParams == null) queryParams = {};
+
+ if (uploadMedia != null) {
+ if (uploadOptions is common_external.ResumableUploadOptions) {
+ queryParams['uploadType'] = const ['resumable'];
+ } else if (body == null) {
+ queryParams['uploadType'] = const ['media'];
+ } else {
+ queryParams['uploadType'] = const ['multipart'];
+ }
+ }
+
+ if (downloadAsMedia) {
+ queryParams['alt'] = const ['media'];
+ } else if (downloadOptions != null) {
+ queryParams['alt'] = const ['json'];
+ }
+
+ var path;
+ if (requestUrl.startsWith('/')) {
+ path ="$_rootUrl${requestUrl.substring(1)}";
+ } else {
+ path ="$_rootUrl${_basePath.substring(1)}$requestUrl";
+ }
+
+ bool containsQueryParameter = path.contains('?');
+ addQueryParameter(String name, String value) {
+ name = Escaper.escapeQueryComponent(name);
+ value = Escaper.escapeQueryComponent(value);
+ if (containsQueryParameter) {
+ path = '$path&$name=$value';
+ } else {
+ path = '$path?$name=$value';
+ }
+ containsQueryParameter = true;
+ }
+ queryParams.forEach((String key, List<String> values) {
+ for (var value in values) {
+ addQueryParameter(key, value);
+ }
+ });
+
+ var uri = Uri.parse(path);
+
+ Future simpleUpload() {
+ var bodyStream = uploadMedia.stream;
+ var request = new RequestImpl(method, uri, bodyStream);
+ request.headers.addAll({
+ 'user-agent' : USER_AGENT_STRING,
+ 'content-type' : uploadMedia.contentType,
+ 'content-length' : '${uploadMedia.length}'
+ });
+ return _httpClient.send(request);
+ }
+
+ Future simpleRequest() {
+ var length = 0;
+ var bodyController = new StreamController<List<int>>();
+ if (body != null) {
+ var bytes = UTF8.encode(body);
+ bodyController.add(bytes);
+ length = bytes.length;
+ }
+ bodyController.close();
+
+ var headers;
+ if (downloadRange != null) {
+ headers = {
+ 'user-agent' : USER_AGENT_STRING,
+ 'content-type' : CONTENT_TYPE_JSON_UTF8,
+ 'content-length' : '$length',
+ 'range' : 'bytes=${downloadRange.start}-${downloadRange.end}',
+ };
+ } else {
+ headers = {
+ 'user-agent' : USER_AGENT_STRING,
+ 'content-type' : CONTENT_TYPE_JSON_UTF8,
+ 'content-length' : '$length',
+ };
+ }
+
+ var request = new RequestImpl(method, uri, bodyController.stream);
+ request.headers.addAll(headers);
+ return _httpClient.send(request);
+ }
+
+ if (uploadMedia != null) {
+ // Three upload types:
+ // 1. Resumable: Upload of data + metdata with multiple requests.
+ // 2. Simple: Upload of media.
+ // 3. Multipart: Upload of data + metadata.
+
+ if (uploadOptions is common_external.ResumableUploadOptions) {
+ var helper = new ResumableMediaUploader(
+ _httpClient, uploadMedia, body, uri, method, uploadOptions);
+ return helper.upload();
+ }
+
+ if (uploadMedia.length == null) {
+ throw new ArgumentError(
+ 'For non-resumable uploads you need to specify the length of the '
+ 'media to upload.');
+ }
+
+ if (body == null) {
+ return simpleUpload();
+ } else {
+ var uploader = new MultipartMediaUploader(
+ _httpClient, uploadMedia, body, uri, method);
+ return uploader.upload();
+ }
+ }
+ return simpleRequest();
+ }
+}
+
+
+/**
+ * Does media uploads using the multipart upload protocol.
+ */
+class MultipartMediaUploader {
+ static final _boundary = '314159265358979323846';
+ static final _base64Encoder = new Base64Encoder();
+
+ final http.Client _httpClient;
+ final common_external.Media _uploadMedia;
+ final Uri _uri;
+ final String _body;
+ final String _method;
+
+ MultipartMediaUploader(
+ this._httpClient, this._uploadMedia, this._body, this._uri, this._method);
+
+ Future<http.StreamedResponse> upload() {
+ var base64MediaStream =
+ _uploadMedia.stream.transform(_base64Encoder).transform(ASCII.encoder);
+ var base64MediaStreamLength =
+ Base64Encoder.lengthOfBase64Stream(_uploadMedia.length);
+
+ // NOTE: We assume that [_body] is encoded JSON without any \r or \n in it.
+ // This guarantees us that [_body] cannot contain a valid multipart
+ // boundary.
+ var bodyHead =
+ '--$_boundary\r\n'
+ "Content-Type: $CONTENT_TYPE_JSON_UTF8\r\n\r\n"
+ + _body +
+ '\r\n--$_boundary\r\n'
+ "Content-Type: ${_uploadMedia.contentType}\r\n"
+ "Content-Transfer-Encoding: base64\r\n\r\n";
+ var bodyTail = '\r\n--$_boundary--';
+
+ var totalLength =
+ bodyHead.length + base64MediaStreamLength + bodyTail.length;
+
+ var bodyController = new StreamController<List<int>>();
+ bodyController.add(UTF8.encode(bodyHead));
+ bodyController.addStream(base64MediaStream).then((_) {
+ bodyController.add(UTF8.encode(bodyTail));
+ }).catchError((error, stack) {
+ bodyController.addError(error, stack);
+ }).then((_) {
+ bodyController.close();
+ });
+
+ var headers = {
+ 'user-agent' : USER_AGENT_STRING,
+ 'content-type' : "multipart/related; boundary=\"$_boundary\"",
+ 'content-length' : '$totalLength'
+ };
+ var bodyStream = bodyController.stream;
+ var request = new RequestImpl(_method, _uri, bodyStream);
+ request.headers.addAll(headers);
+ return _httpClient.send(request);
+ }
+}
+
+
+/**
+ * Base64 encodes a stream of bytes.
+ */
+class Base64Encoder implements StreamTransformer<List<int>, String> {
+ static int lengthOfBase64Stream(int lengthOfByteStream) {
+ return ((lengthOfByteStream + 2) ~/ 3) * 4;
+ }
+
+ Stream<String> bind(Stream<List<int>> stream) {
+ StreamController<String> controller;
+
+ // Holds between 0 and 3 bytes and is used as a buffer.
+ List<int> remainingBytes = [];
+
+ void onData(List<int> bytes) {
+ if ((remainingBytes.length + bytes.length) < 3) {
+ remainingBytes.addAll(bytes);
+ return;
+ }
+ int start;
+ if (remainingBytes.length == 0) {
+ start = 0;
+ } else if (remainingBytes.length == 1) {
+ remainingBytes.add(bytes[0]);
+ remainingBytes.add(bytes[1]);
+ start = 2;
+ } else if (remainingBytes.length == 2) {
+ remainingBytes.add(bytes[0]);
+ start = 1;
+ }
+
+ // Convert & Send bytes from buffer (if necessary).
+ if (remainingBytes.length > 0) {
+ controller.add(crypto.CryptoUtils.bytesToBase64(remainingBytes));
+ remainingBytes.clear();
+ }
+
+ int chunksOf3 = (bytes.length - start) ~/ 3;
+ int end = start + 3 * chunksOf3;
+ int remaining = bytes.length - end;
+
+ // Convert & Send main bytes.
+ if (start == 0 && end == bytes.length) {
+ // Fast path if [bytes] are devisible by 3.
+ controller.add(crypto.CryptoUtils.bytesToBase64(bytes));
+ } else {
+ controller.add(
+ crypto.CryptoUtils.bytesToBase64(bytes.sublist(start, end)));
+
+ // Buffer remaining bytes if necessary.
+ if (end < bytes.length) {
+ remainingBytes.addAll(bytes.sublist(end));
+ }
+ }
+ }
+
+ void onError(error, stack) {
+ controller.addError(error, stack);
+ }
+
+ void onDone() {
+ if (remainingBytes.length > 0) {
+ controller.add(crypto.CryptoUtils.bytesToBase64(remainingBytes));
+ remainingBytes.clear();
+ }
+ controller.close();
+ }
+
+ var subscription;
+ controller = new StreamController<String>(
+ onListen: () {
+ subscription = stream.listen(
+ onData, onError: onError, onDone: onDone);
+ },
+ onPause: () {
+ subscription.pause();
+ },
+ onResume: () {
+ subscription.resume();
+ },
+ onCancel: () {
+ subscription.cancel();
+ });
+ return controller.stream;
+ }
+}
+
+
+// TODO: Buffer less if we know the content length in advance.
+/**
+ * Does media uploads using the resumable upload protocol.
+ */
+class ResumableMediaUploader {
+ final http.Client _httpClient;
+ final common_external.Media _uploadMedia;
+ final Uri _uri;
+ final String _body;
+ final String _method;
+ final common_external.ResumableUploadOptions _options;
+
+ ResumableMediaUploader(
+ this._httpClient, this._uploadMedia, this._body, this._uri, this._method,
+ this._options);
+
+ /**
+ * Returns the final [http.StreamedResponse] if the upload succeded and
+ * completes with an error otherwise.
+ *
+ * The returned response stream has not been listened to.
+ */
+ Future<http.StreamedResponse> upload() {
+ return _startSession().then((Uri uploadUri) {
+ StreamSubscription subscription;
+
+ var completer = new Completer<http.StreamedResponse>();
+ bool completed = false;
+
+ var chunkStack = new ChunkStack(_options.chunkSize);
+ subscription = _uploadMedia.stream.listen((List<int> bytes) {
+ chunkStack.addBytes(bytes);
+
+ // Upload all but the last chunk.
+ // The final send will be done in the [onDone] handler.
+ if (chunkStack.length > 1) {
+ // Pause the input stream.
+ subscription.pause();
+
+ // Upload all chunks except the last one.
+ var fullChunks = chunkStack.removeSublist(0, chunkStack.length - 1);
+ Future.forEach(fullChunks,
+ (c) => _uploadChunkDrained(uploadUri, c)).then((_) {
+ // All chunks uploaded, we can continue consuming data.
+ subscription.resume();
+ }).catchError((error, stack) {
+ subscription.cancel();
+ completed = true;
+ completer.completeError(error, stack);
+ });
+ }
+ }, onError: (error, stack) {
+ subscription.cancel();
+ if (!completed) {
+ completed = true;
+ completer.completeError(error, stack);
+ }
+ }, onDone: () {
+ if (!completed) {
+ chunkStack.finalize();
+
+ var lastChunk;
+ if (chunkStack.totalByteLength > 0) {
+ assert(chunkStack.length == 1);
+ lastChunk = chunkStack.removeSublist(0, chunkStack.length).first;
+ } else {
+ lastChunk = new ResumableChunk([], 0, 0);
+ }
+ var end = lastChunk.endOfChunk;
+
+ // Validate that we have the correct number of bytes if length was
+ // specified.
+ if (_uploadMedia.length != null) {
+ if (end < _uploadMedia.length) {
+ completer.completeError(new common_external.ApiRequestError(
+ 'Received less bytes than indicated by [Media.length].'));
+ return;
+ } else if (end > _uploadMedia.length) {
+ completer.completeError(
+ 'Received more bytes than indicated by [Media.length].');
+ return;
+ }
+ }
+
+ // Upload last chunk and *do not drain the response* but complete
+ // with it.
+ _uploadChunkResumable(uploadUri, lastChunk, lastChunk: true)
+ .then((response) {
+ completer.complete(response);
+ }).catchError((error, stack) {
+ completer.completeError(error, stack);
+ });
+ }
+ });
+
+ return completer.future;
+ });
+ }
+
+ /**
+ * Starts a resumable upload.
+ *
+ * Returns the [Uri] which should be used for uploading all content.
+ */
+ Future<Uri> _startSession() {
+ var length = 0;
+ var bytes;
+ if (_body != null) {
+ bytes = UTF8.encode(_body);
+ length = bytes.length;
+ }
+ var bodyStream = _bytes2Stream(bytes);
+
+ var request = new RequestImpl(_method, _uri, bodyStream);
+ request.headers.addAll({
+ 'user-agent' : USER_AGENT_STRING,
+ 'content-type' : CONTENT_TYPE_JSON_UTF8,
+ 'content-length' : '$length',
+ 'x-upload-content-type' : _uploadMedia.contentType,
+ 'x-upload-content-length' : '${_uploadMedia.length}',
+ });
+
+ return _httpClient.send(request).then((http.StreamedResponse response) {
+ return response.stream.drain().then((_) {
+ var uploadUri = response.headers['location'];
+ if (response.statusCode != 200 || uploadUri == null) {
+ throw new common_external.ApiRequestError(
+ 'Invalid response for resumable upload attempt '
+ '(status was: ${response.statusCode})');
+ }
+ return Uri.parse(uploadUri);
+ });
+ });
+ }
+
+ /**
+ * Uploads [chunk], retries upon server errors. The response stream will be
+ * drained.
+ */
+ Future _uploadChunkDrained(Uri uri, ResumableChunk chunk) {
+ return _uploadChunkResumable(uri, chunk).then((response) {
+ return response.stream.drain();
+ });
+ }
+
+ /**
+ * Does repeated attempts to upload [chunk].
+ */
+ Future _uploadChunkResumable(Uri uri,
+ ResumableChunk chunk,
+ {bool lastChunk: false}) {
+ tryUpload(int attemptsLeft) {
+ return _uploadChunk(uri, chunk, lastChunk: lastChunk)
+ .then((http.StreamedResponse response) {
+ var status = response.statusCode;
+ if (attemptsLeft > 0 &&
+ (status == 500 || (502 <= status && status < 504))) {
+ return response.stream.drain().then((_) {
+ // Delay the next attempt. Default backoff function is exponential.
+ int failedAttemts = _options.numberOfAttempts - attemptsLeft;
+ var duration = _options.backoffFunction(failedAttemts);
+ if (duration == null) {
+ throw new common_external.DetailedApiRequestError(
+ status,
+ 'Resumable upload: Uploading a chunk resulted in status '
+ '$status. Maximum number of retries reached.');
+ }
+
+ return new Future.delayed(duration).then((_) {
+ return tryUpload(attemptsLeft - 1);
+ });
+ });
+ } else if (!lastChunk && status != 308) {
+ return response.stream.drain().then((_) {
+ throw new common_external.DetailedApiRequestError(
+ status,
+ 'Resumable upload: Uploading a chunk resulted in status '
+ '$status instead of 308.');
+ });
+ } else if (lastChunk && status != 201 && status != 200) {
+ return response.stream.drain().then((_) {
+ throw new common_external.DetailedApiRequestError(
+ status,
+ 'Resumable upload: Uploading a chunk resulted in status '
+ '$status instead of 200 or 201.');
+ });
+ } else {
+ return response;
+ }
+ });
+ }
+
+ return tryUpload(_options.numberOfAttempts - 1);
+ }
+
+ /**
+ * Uploads [length] bytes in [byteArrays] and ensures the upload was
+ * successful.
+ *
+ * Content-Range: [start ... (start + length)[
+ *
+ * Returns the returned [http.StreamedResponse] or completes with an error if
+ * the upload did not succeed. The response stream will not be listened to.
+ */
+ Future _uploadChunk(Uri uri, ResumableChunk chunk, {bool lastChunk: false}) {
+ // If [uploadMedia.length] is null, we do not know the length.
+ var mediaTotalLength = _uploadMedia.length;
+ if (mediaTotalLength == null || lastChunk) {
+ if (lastChunk) {
+ mediaTotalLength = '${chunk.endOfChunk}';
+ } else {
+ mediaTotalLength = '*';
+ }
+ }
+
+ var headers = {
+ 'user-agent' : USER_AGENT_STRING,
+ 'content-type' : _uploadMedia.contentType,
+ 'content-length' : '${chunk.length}',
+ 'content-range' :
+ 'bytes ${chunk.offset}-${chunk.endOfChunk - 1}/$mediaTotalLength',
+ };
+
+ var stream = _listOfBytes2Stream(chunk.byteArrays);
+ var request = new RequestImpl('PUT', uri, stream);
+ request.headers.addAll(headers);
+ return _httpClient.send(request);
+ }
+
+ Stream<List<int>> _bytes2Stream(List<int> bytes) {
+ var bodyController = new StreamController<List<int>>();
+ if (bytes != null) {
+ bodyController.add(bytes);
+ }
+ bodyController.close();
+ return bodyController.stream;
+ }
+
+ Stream<List<int>> _listOfBytes2Stream(List<List<int>> listOfBytes) {
+ var controller = new StreamController();
+ for (var array in listOfBytes) {
+ controller.add(array);
+ }
+ controller.close();
+ return controller.stream;
+ }
+}
+
+
+/**
+ * Represents a stack of [ResumableChunk]s.
+ */
+class ChunkStack {
+ final int _chunkSize;
+ final List<ResumableChunk> _chunkStack = [];
+
+ // Currently accumulated data.
+ List<List<int>> _byteArrays = [];
+ int _length = 0;
+ int _totalLength = 0;
+ int _offset = 0;
+
+ bool _finalized = false;
+
+ ChunkStack(this._chunkSize);
+
+ int get length => _chunkStack.length;
+
+ int get totalByteLength => _offset;
+
+ /**
+ * Returns the chunks [from] ... [to] and deletes it from the stack.
+ */
+ List<ResumableChunk> removeSublist(int from, int to) {
+ var sublist = _chunkStack.sublist(from, to);
+ _chunkStack.removeRange(from, to);
+ return sublist;
+ }
+
+ /**
+ * Adds [bytes] to the buffer. If the buffer is larger than the given chunk
+ * size a new [ResumableChunk] will be created.
+ */
+ void addBytes(List<int> bytes) {
+ if (_finalized) {
+ throw new StateError('ChunkStack has already been finalized.');
+ }
+
+ var remaining = _chunkSize - _length;
+
+ if (bytes.length >= remaining) {
+ var left = bytes.sublist(0, remaining);
+ var right = bytes.sublist(remaining);
+
+ _byteArrays.add(left);
+ _length += left.length;
+
+ _chunkStack.add(new ResumableChunk(_byteArrays, _offset, _length));
+
+ _byteArrays = [];
+ _offset += _length;
+ _length = 0;
+
+ addBytes(right);
+ } else if (bytes.length > 0) {
+ _byteArrays.add(bytes);
+ _length += bytes.length;
+ }
+ }
+
+ /**
+ * Finalizes this [ChunkStack] and creates the last chunk (may have less bytes
+ * than the chunk size, but not zero).
+ */
+ void finalize() {
+ if (_finalized) {
+ throw new StateError('ChunkStack has already been finalized.');
+ }
+ _finalized = true;
+
+ if (_length > 0) {
+ _chunkStack.add(new ResumableChunk(_byteArrays, _offset, _length));
+ _offset += _length;
+ }
+ }
+}
+
+
+/**
+ * Represents a chunk of data that will be transferred in one http request.
+ */
+class ResumableChunk {
+ final List<List<int>> byteArrays;
+ final int offset;
+ final int length;
+
+ /**
+ * Index of the next byte after this chunk.
+ */
+ int get endOfChunk => offset + length;
+
+ ResumableChunk(this.byteArrays, this.offset, this.length);
+}
+
+class RequestImpl extends http.BaseRequest {
+ final Stream<List<int>> _stream;
+
+ RequestImpl(String method, Uri url, [Stream<List<int>> stream])
+ : _stream = stream == null ? new Stream.fromIterable([]) : stream,
+ super(method, url);
+
+ http.ByteStream finalize() {
+ super.finalize();
+ return new http.ByteStream(_stream);
+ }
+}
+
+
+class Escaper {
+ // Character class definitions from RFC 6570
+ // (see http://tools.ietf.org/html/rfc6570)
+ // ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
+ // DIGIT = %x30-39 ; 0
+ // HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
+ // pct-encoded = "%" HEXDIG HEXDIG
+ // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+ // reserved = gen-delims / sub-delims
+ // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
+ // sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
+ // / "*" / "+" / "," / ";" / "="
+
+ // NOTE: Uri.encodeQueryComponent() does the following:
+ // ...
+ // Then the resulting bytes are "percent-encoded". This transforms spaces
+ // (U+0020) to a plus sign ('+') and all bytes that are not the ASCII decimal
+ // digits, letters or one of '-._~' are written as a percent sign '%'
+ // followed by the two-digit hexadecimal representation of the byte.
+ // ...
+
+ // NOTE: Uri.encodeFull() does the following:
+ // ...
+ // All characters except uppercase and lowercase letters, digits and the
+ // characters !#$&'()*+,-./:;=?@_~ are percent-encoded.
+ // ...
+
+ static String ecapeVariableReserved(String name) {
+ // ... perform variable expansion, as defined in Section 3.2.1, with the
+ // allowed characters being those in the set
+ // (unreserved / reserved / pct-encoded)
+
+ // NOTE: The chracters [ and ] need (according to URI Template spec) not be
+ // percent encoded. The dart implementation does percent-encode [ and ].
+ // This gives us in effect a conservative encoding, since the server side
+ // must interpret percent-encoded parts anyway due to arbitrary unicode.
+
+ // NOTE: This is broken in the discovery protocol. It allows ? and & to be
+ // expanded via URI Templates which may generate completely bogus URIs.
+ // TODO/FIXME: Should we change this to _encodeUnreserved() as well
+ // (disadvantage, slashes get encoded at this point)?
+ return Uri.encodeFull(name);
+ }
+
+ static String ecapePathComponent(String name) {
+ // For each defined variable in the variable-list, append "/" to the
+ // result string and then perform variable expansion, as defined in
+ // Section 3.2.1, with the allowed characters being those in the
+ // *unreserved set*.
+ return _encodeUnreserved(name);
+ }
+
+ static String ecapeVariable(String name) {
+ // ... perform variable expansion, as defined in Section 3.2.1, with the
+ // allowed characters being those in the *unreserved set*.
+ return _encodeUnreserved(name);
+ }
+
+ static String escapeQueryComponent(String name) {
+ // This method will not be used by UriTemplate, but rather for encoding
+ // normal query name/value pairs.
+
+ // NOTE: For safety reasons we use '%20' instead of '+' here as well.
+ // TODO/FIXME: Should we do this?
+ return _encodeUnreserved(name);
+ }
+
+ static String _encodeUnreserved(String name) {
+ // The only difference between dart's [Uri.encodeQueryComponent] and the
+ // encoding defined by RFC 6570 for the above-defined unreserved character
+ // set is the encoding of space.
+ // Dart's Uri class will convert spaces to '+' which we replace by '%20'.
+ return Uri.encodeQueryComponent(name).replaceAll('+', '%20');
+ }
+}
+
+
+Future<http.StreamedResponse> _validateResponse(
+ http.StreamedResponse response) {
+ var statusCode = response.statusCode;
+
+ // TODO: We assume that status codes between [200..400[ are OK.
+ // Can we assume this?
+ if (statusCode < 200 || statusCode >= 400) {
+ throwGeneralError() {
+ throw new common_external.ApiRequestError(
+ 'No error details. Http status was: ${response.statusCode}.');
+ }
+
+ // Some error happened, try to decode the response and fetch the error.
+ Stream<String> stringStream = _decodeStreamAsText(response);
+ if (stringStream != null) {
+ return stringStream.transform(JSON.decoder).first.then((json) {
+ if (json is Map && json['error'] is Map) {
+ var error = json['error'];
+ var code = error['code'];
+ var message = error['message'];
+ throw new common_external.DetailedApiRequestError(code, message);
+ } else {
+ throwGeneralError();
+ }
+ });
+ } else {
+ throwGeneralError();
+ }
+ }
+
+ return new Future.value(response);
+}
+
+
+Stream<String> _decodeStreamAsText(http.StreamedResponse response) {
+ // TODO: Correctly handle the response content-types, using correct
+ // decoder.
+ // Currently we assume that the api endpoint is responding with json
+ // encoded in UTF8.
+ String contentType = response.headers['content-type'];
+ if (contentType != null &&
+ contentType.toLowerCase().startsWith('application/json')) {
+ return response.stream.transform(new Utf8Decoder(allowMalformed: true));
+ } else {
+ return null;
+ }
+}
+
+Map mapMap(Map source, [Object convert(Object source) = null]) {
+ assert(source != null);
+ var result = new collection.LinkedHashMap();
+ source.forEach((String key, value) {
+ assert(key != null);
+ if(convert == null) {
+ result[key] = value;
+ } else {
+ result[key] = convert(value);
+ }
+ });
+ return result;
+}
+

Powered by Google App Engine
This is Rietveld 408576698