Tighten argument types to Uri (#507)

Closes #375

Make all arguments that were `Object` in order to allow either `String`
or `Uri` accept only `Uri`. This gives better static checking for
calling code.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e08586b..b88ea78 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@
 
 * Add `const` constructor to `ByteStream`.
 * Migrate `BrowserClient` from `blob` to `arraybuffer`.
+* **BREAKING** All APIs which previously allowed a `String` or `Uri` to be
+  passed now require a `Uri`.
 * **Breaking** Added a `body` and `encoding` argument to `Client.delete`. This
   is only breaking for implementations which override that method.
 
diff --git a/example/main.dart b/example/main.dart
index e53008f..d6b4997 100644
--- a/example/main.dart
+++ b/example/main.dart
@@ -4,7 +4,8 @@
 void main(List<String> arguments) async {
   // This example uses the Google Books API to search for books about http.
   // https://developers.google.com/books/docs/overview
-  var url = 'https://www.googleapis.com/books/v1/volumes?q={http}';
+  var url =
+      Uri.https('www.googleapis.com', '/books/v1/volumes', {'q': '{http}'});
 
   // Await the http get response, then decode the json-formatted response.
   var response = await http.get(url);
diff --git a/lib/http.dart b/lib/http.dart
index 7b42071..1ea751e 100644
--- a/lib/http.dart
+++ b/lib/http.dart
@@ -25,30 +25,27 @@
 export 'src/streamed_request.dart';
 export 'src/streamed_response.dart';
 
-/// Sends an HTTP HEAD request with the given headers to the given URL, which
-/// can be a [Uri] or a [String].
+/// Sends an HTTP HEAD request with the given headers to the given URL.
 ///
 /// This automatically initializes a new [Client] and closes that client once
 /// the request is complete. If you're planning on making multiple requests to
 /// the same server, you should use a single [Client] for all of those requests.
 ///
 /// For more fine-grained control over the request, use [Request] instead.
-Future<Response> head(Object url, {Map<String, String>? headers}) =>
+Future<Response> head(Uri url, {Map<String, String>? headers}) =>
     _withClient((client) => client.head(url, headers: headers));
 
-/// Sends an HTTP GET request with the given headers to the given URL, which can
-/// be a [Uri] or a [String].
+/// Sends an HTTP GET request with the given headers to the given URL.
 ///
 /// This automatically initializes a new [Client] and closes that client once
 /// the request is complete. If you're planning on making multiple requests to
 /// the same server, you should use a single [Client] for all of those requests.
 ///
 /// For more fine-grained control over the request, use [Request] instead.
-Future<Response> get(Object url, {Map<String, String>? headers}) =>
+Future<Response> get(Uri url, {Map<String, String>? headers}) =>
     _withClient((client) => client.get(url, headers: headers));
 
-/// Sends an HTTP POST request with the given headers and body to the given URL,
-/// which can be a [Uri] or a [String].
+/// Sends an HTTP POST request with the given headers and body to the given URL.
 ///
 /// [body] sets the body of the request. It can be a [String], a [List<int>] or
 /// a [Map<String, String>]. If it's a String, it's encoded using [encoding] and
@@ -66,13 +63,12 @@
 ///
 /// For more fine-grained control over the request, use [Request] or
 /// [StreamedRequest] instead.
-Future<Response> post(Object url,
+Future<Response> post(Uri url,
         {Map<String, String>? headers, Object? body, Encoding? encoding}) =>
     _withClient((client) =>
         client.post(url, headers: headers, body: body, encoding: encoding));
 
-/// Sends an HTTP PUT request with the given headers and body to the given URL,
-/// which can be a [Uri] or a [String].
+/// Sends an HTTP PUT request with the given headers and body to the given URL.
 ///
 /// [body] sets the body of the request. It can be a [String], a [List<int>] or
 /// a [Map<String, String>]. If it's a String, it's encoded using [encoding] and
@@ -90,13 +86,13 @@
 ///
 /// For more fine-grained control over the request, use [Request] or
 /// [StreamedRequest] instead.
-Future<Response> put(Object url,
+Future<Response> put(Uri url,
         {Map<String, String>? headers, Object? body, Encoding? encoding}) =>
     _withClient((client) =>
         client.put(url, headers: headers, body: body, encoding: encoding));
 
 /// Sends an HTTP PATCH request with the given headers and body to the given
-/// URL, which can be a [Uri] or a [String].
+/// URL.
 ///
 /// [body] sets the body of the request. It can be a [String], a [List<int>] or
 /// a [Map<String, String>]. If it's a String, it's encoded using [encoding] and
@@ -114,27 +110,25 @@
 ///
 /// For more fine-grained control over the request, use [Request] or
 /// [StreamedRequest] instead.
-Future<Response> patch(Object url,
+Future<Response> patch(Uri url,
         {Map<String, String>? headers, Object? body, Encoding? encoding}) =>
     _withClient((client) =>
         client.patch(url, headers: headers, body: body, encoding: encoding));
 
-/// Sends an HTTP DELETE request with the given headers to the given URL, which
-/// can be a [Uri] or a [String].
+/// Sends an HTTP DELETE request with the given headers to the given URL.
 ///
 /// This automatically initializes a new [Client] and closes that client once
 /// the request is complete. If you're planning on making multiple requests to
 /// the same server, you should use a single [Client] for all of those requests.
 ///
 /// For more fine-grained control over the request, use [Request] instead.
-Future<Response> delete(Object url,
+Future<Response> delete(Uri url,
         {Map<String, String>? headers, Object? body, Encoding? encoding}) =>
     _withClient((client) =>
         client.delete(url, headers: headers, body: body, encoding: encoding));
 
-/// Sends an HTTP GET request with the given headers to the given URL, which can
-/// be a [Uri] or a [String], and returns a Future that completes to the body of
-/// the response as a [String].
+/// Sends an HTTP GET request with the given headers to the given URL and
+/// returns a Future that completes to the body of the response as a [String].
 ///
 /// The Future will emit a [ClientException] if the response doesn't have a
 /// success status code.
@@ -145,12 +139,12 @@
 ///
 /// For more fine-grained control over the request and response, use [Request]
 /// instead.
-Future<String> read(Object url, {Map<String, String>? headers}) =>
+Future<String> read(Uri url, {Map<String, String>? headers}) =>
     _withClient((client) => client.read(url, headers: headers));
 
-/// Sends an HTTP GET request with the given headers to the given URL, which can
-/// be a [Uri] or a [String], and returns a Future that completes to the body of
-/// the response as a list of bytes.
+/// Sends an HTTP GET request with the given headers to the given URL and
+/// returns a Future that completes to the body of the response as a list of
+/// bytes.
 ///
 /// The Future will emit a [ClientException] if the response doesn't have a
 /// success status code.
@@ -161,7 +155,7 @@
 ///
 /// For more fine-grained control over the request and response, use [Request]
 /// instead.
-Future<Uint8List> readBytes(Object url, {Map<String, String>? headers}) =>
+Future<Uint8List> readBytes(Uri url, {Map<String, String>? headers}) =>
     _withClient((client) => client.readBytes(url, headers: headers));
 
 Future<T> _withClient<T>(Future<T> Function(Client) fn) async {
diff --git a/lib/src/base_client.dart b/lib/src/base_client.dart
index 52eae4b..efb065f 100644
--- a/lib/src/base_client.dart
+++ b/lib/src/base_client.dart
@@ -19,43 +19,42 @@
 /// maybe [close], and then they get various convenience methods for free.
 abstract class BaseClient implements Client {
   @override
-  Future<Response> head(Object url, {Map<String, String>? headers}) =>
+  Future<Response> head(Uri url, {Map<String, String>? headers}) =>
       _sendUnstreamed('HEAD', url, headers);
 
   @override
-  Future<Response> get(Object url, {Map<String, String>? headers}) =>
+  Future<Response> get(Uri url, {Map<String, String>? headers}) =>
       _sendUnstreamed('GET', url, headers);
 
   @override
-  Future<Response> post(Object url,
+  Future<Response> post(Uri url,
           {Map<String, String>? headers, Object? body, Encoding? encoding}) =>
       _sendUnstreamed('POST', url, headers, body, encoding);
 
   @override
-  Future<Response> put(Object url,
+  Future<Response> put(Uri url,
           {Map<String, String>? headers, Object? body, Encoding? encoding}) =>
       _sendUnstreamed('PUT', url, headers, body, encoding);
 
   @override
-  Future<Response> patch(Object url,
+  Future<Response> patch(Uri url,
           {Map<String, String>? headers, Object? body, Encoding? encoding}) =>
       _sendUnstreamed('PATCH', url, headers, body, encoding);
 
   @override
-  Future<Response> delete(Object url,
+  Future<Response> delete(Uri url,
           {Map<String, String>? headers, Object? body, Encoding? encoding}) =>
       _sendUnstreamed('DELETE', url, headers, body, encoding);
 
   @override
-  Future<String> read(Object url, {Map<String, String>? headers}) async {
+  Future<String> read(Uri url, {Map<String, String>? headers}) async {
     final response = await get(url, headers: headers);
     _checkResponseSuccess(url, response);
     return response.body;
   }
 
   @override
-  Future<Uint8List> readBytes(Object url,
-      {Map<String, String>? headers}) async {
+  Future<Uint8List> readBytes(Uri url, {Map<String, String>? headers}) async {
     final response = await get(url, headers: headers);
     _checkResponseSuccess(url, response);
     return response.bodyBytes;
@@ -73,9 +72,9 @@
 
   /// Sends a non-streaming [Request] and returns a non-streaming [Response].
   Future<Response> _sendUnstreamed(
-      String method, url, Map<String, String>? headers,
+      String method, Uri url, Map<String, String>? headers,
       [body, Encoding? encoding]) async {
-    var request = Request(method, _fromUriOrString(url));
+    var request = Request(method, url);
 
     if (headers != null) request.headers.addAll(headers);
     if (encoding != null) request.encoding = encoding;
@@ -95,17 +94,15 @@
   }
 
   /// Throws an error if [response] is not successful.
-  void _checkResponseSuccess(url, Response response) {
+  void _checkResponseSuccess(Uri url, Response response) {
     if (response.statusCode < 400) return;
     var message = 'Request to $url failed with status ${response.statusCode}';
     if (response.reasonPhrase != null) {
       message = '$message: ${response.reasonPhrase}';
     }
-    throw ClientException('$message.', _fromUriOrString(url));
+    throw ClientException('$message.', url);
   }
 
   @override
   void close() {}
 }
-
-Uri _fromUriOrString(uri) => uri is String ? Uri.parse(uri) : uri as Uri;
diff --git a/lib/src/client.dart b/lib/src/client.dart
index 19c2a88..12695e7 100644
--- a/lib/src/client.dart
+++ b/lib/src/client.dart
@@ -31,20 +31,18 @@
   /// `dart:html` is available, otherwise it will throw an unsupported error.
   factory Client() => createClient();
 
-  /// Sends an HTTP HEAD request with the given headers to the given URL, which
-  /// can be a [Uri] or a [String].
+  /// Sends an HTTP HEAD request with the given headers to the given URL.
   ///
   /// For more fine-grained control over the request, use [send] instead.
-  Future<Response> head(Object url, {Map<String, String>? headers});
+  Future<Response> head(Uri url, {Map<String, String>? headers});
 
-  /// Sends an HTTP GET request with the given headers to the given URL, which
-  /// can be a [Uri] or a [String].
+  /// Sends an HTTP GET request with the given headers to the given URL.
   ///
   /// For more fine-grained control over the request, use [send] instead.
-  Future<Response> get(Object url, {Map<String, String>? headers});
+  Future<Response> get(Uri url, {Map<String, String>? headers});
 
   /// Sends an HTTP POST request with the given headers and body to the given
-  /// URL, which can be a [Uri] or a [String].
+  /// URL.
   ///
   /// [body] sets the body of the request. It can be a [String], a [List<int>]
   /// or a [Map<String, String>]. If it's a String, it's encoded using
@@ -61,11 +59,11 @@
   /// [encoding] defaults to [utf8].
   ///
   /// For more fine-grained control over the request, use [send] instead.
-  Future<Response> post(Object url,
+  Future<Response> post(Uri url,
       {Map<String, String>? headers, Object? body, Encoding? encoding});
 
   /// Sends an HTTP PUT request with the given headers and body to the given
-  /// URL, which can be a [Uri] or a [String].
+  /// URL.
   ///
   /// [body] sets the body of the request. It can be a [String], a [List<int>]
   /// or a [Map<String, String>]. If it's a String, it's encoded using
@@ -82,11 +80,11 @@
   /// [encoding] defaults to [utf8].
   ///
   /// For more fine-grained control over the request, use [send] instead.
-  Future<Response> put(Object url,
+  Future<Response> put(Uri url,
       {Map<String, String>? headers, Object? body, Encoding? encoding});
 
   /// Sends an HTTP PATCH request with the given headers and body to the given
-  /// URL, which can be a [Uri] or a [String].
+  /// URL.
   ///
   /// [body] sets the body of the request. It can be a [String], a [List<int>]
   /// or a [Map<String, String>]. If it's a String, it's encoded using
@@ -103,37 +101,35 @@
   /// [encoding] defaults to [utf8].
   ///
   /// For more fine-grained control over the request, use [send] instead.
-  Future<Response> patch(Object url,
+  Future<Response> patch(Uri url,
       {Map<String, String>? headers, Object? body, Encoding? encoding});
 
-  /// Sends an HTTP DELETE request with the given headers to the given URL,
-  /// which can be a [Uri] or a [String].
+  /// Sends an HTTP DELETE request with the given headers to the given URL.
   ///
   /// For more fine-grained control over the request, use [send] instead.
-  Future<Response> delete(Object url,
+  Future<Response> delete(Uri url,
       {Map<String, String>? headers, Object? body, Encoding? encoding});
 
-  /// Sends an HTTP GET request with the given headers to the given URL, which
-  /// can be a [Uri] or a [String], and returns a Future that completes to the
-  /// body of the response as a String.
+  /// Sends an HTTP GET request with the given headers to the given URL and
+  /// returns a Future that completes to the body of the response as a String.
   ///
   /// The Future will emit a [ClientException] if the response doesn't have a
   /// success status code.
   ///
   /// For more fine-grained control over the request and response, use [send] or
   /// [get] instead.
-  Future<String> read(Object url, {Map<String, String>? headers});
+  Future<String> read(Uri url, {Map<String, String>? headers});
 
-  /// Sends an HTTP GET request with the given headers to the given URL, which
-  /// can be a [Uri] or a [String], and returns a Future that completes to the
-  /// body of the response as a list of bytes.
+  /// Sends an HTTP GET request with the given headers to the given URL and
+  /// returns a Future that completes to the body of the response as a list of
+  /// bytes.
   ///
   /// The Future will emit a [ClientException] if the response doesn't have a
   /// success status code.
   ///
   /// For more fine-grained control over the request and response, use [send] or
   /// [get] instead.
-  Future<Uint8List> readBytes(Object url, {Map<String, String>? headers});
+  Future<Uint8List> readBytes(Uri url, {Map<String, String>? headers});
 
   /// Sends an HTTP request and asynchronously returns the response.
   Future<StreamedResponse> send(BaseRequest request);
diff --git a/test/mock_client_test.dart b/test/mock_client_test.dart
index 3a18e34..db561c5 100644
--- a/test/mock_client_test.dart
+++ b/test/mock_client_test.dart
@@ -16,7 +16,7 @@
         json.encode(request.bodyFields), 200,
         request: request, headers: {'content-type': 'application/json'}));
 
-    var response = await client.post('http://example.com/foo',
+    var response = await client.post(Uri.http('example.com', '/foo'),
         body: {'field1': 'value1', 'field2': 'value2'});
     expect(
         response.body, parse(equals({'field1': 'value1', 'field2': 'value2'})));
@@ -30,7 +30,7 @@
       return http.StreamedResponse(stream, 200);
     });
 
-    var uri = Uri.parse('http://example.com/foo');
+    var uri = Uri.http('example.com', '/foo');
     var request = http.Request('POST', uri)..body = 'hello, world';
     var streamedResponse = await client.send(request);
     var response = await http.Response.fromStream(streamedResponse);
@@ -40,6 +40,7 @@
   test('handles a request with no body', () async {
     var client = MockClient((_) async => http.Response('you did it', 200));
 
-    expect(await client.read('http://example.com/foo'), equals('you did it'));
+    expect(await client.read(Uri.http('example.com', '/foo')),
+        equals('you did it'));
   });
 }