Add a Server interface.

A Server represents an adapter that knows its own URL. It's a useful
abstraction for code that needs to know its location in URL-space but
doesn't want to tightly couple itself to a given server implementation.

R=kevmoo@google.com

Review URL: https://codereview.chromium.org//1411553006 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 46f2735..e4b2939 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,12 @@
+## 0.6.4
+
+* Add a `Server` interface representing an adapter that knows its own URL.
+
+* Add a `ServerHandler` class that exposes a `Server` backed by a `Handler`.
+
+* Add an `IOServer` class that implements `Server` in terms of `dart:io`'s
+  `HttpServer`.
+
 ## 0.6.3+1
 
 * Cleaned up handling of certain `Map` instances and related dependencies.
diff --git a/README.md b/README.md
index b363709..c45554e 100644
--- a/README.md
+++ b/README.md
@@ -148,6 +148,11 @@
 }
 ```
 
+An adapter that knows its own URL should provide an implementation of the
+[`Server`][server] interface.
+
+[server]: http://www.dartdocs.org/documentation/shelf/latest/index.html#shelf/shelf@id_Server
+
 ## Inspiration
 
 * [Connect](http://www.senchalabs.org/connect/) for NodeJS.
diff --git a/lib/shelf.dart b/lib/shelf.dart
index 98d7815..238f94b 100644
--- a/lib/shelf.dart
+++ b/lib/shelf.dart
@@ -12,3 +12,5 @@
 export 'src/pipeline.dart';
 export 'src/request.dart';
 export 'src/response.dart';
+export 'src/server.dart';
+export 'src/server_handler.dart';
diff --git a/lib/shelf_io.dart b/lib/shelf_io.dart
index bc2835e..7d0c053 100644
--- a/lib/shelf_io.dart
+++ b/lib/shelf_io.dart
@@ -25,6 +25,8 @@
 import 'shelf.dart';
 import 'src/util.dart';
 
+export 'src/io_server.dart';
+
 /// Starts an [HttpServer] that listens on the specified [address] and
 /// [port] and sends requests to [handler].
 ///
diff --git a/lib/src/io_server.dart b/lib/src/io_server.dart
new file mode 100644
index 0000000..854e341
--- /dev/null
+++ b/lib/src/io_server.dart
@@ -0,0 +1,60 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library shelf.io_server;
+
+import 'dart:async';
+
+import 'dart:io';
+
+import '../shelf_io.dart';
+import 'handler.dart';
+import 'server.dart';
+
+/// A [Server] backed by a `dart:io` [HttpServer].
+class IOServer implements Server {
+  /// The underlying [HttpServer].
+  final HttpServer server;
+
+  /// Whether [mount] has been called.
+  bool _mounted = false;
+
+  Uri get url {
+    if (server.address.isLoopback) {
+      return new Uri(scheme: "http", host: "localhost", port: server.port);
+    }
+
+    // IPv6 addresses in URLs need to be enclosed in square brackets to avoid
+    // URL ambiguity with the ":" in the address.
+    if (server.address.type == InternetAddressType.IP_V6) {
+      return new Uri(
+          scheme: "http",
+          host: "[${server.address.address}]",
+          port: server.port);
+    }
+
+    return new Uri(
+        scheme: "http", host: server.address.address, port: server.port);
+  }
+
+  /// Calls [HttpServer.bind] and wraps the result in an [IOServer].
+  static Future<IOServer> bind(address, int port, {int backlog}) async {
+    backlog ??= 0;
+    var server = await HttpServer.bind(address, port, backlog: backlog);
+    return new IOServer(server);
+  }
+
+  IOServer(this.server);
+
+  void mount(Handler handler) {
+    if (_mounted) {
+      throw new StateError("Can't mount two handlers for the same server.");
+    }
+    _mounted = true;
+
+    serveRequests(server, handler);
+  }
+
+  Future close() => server.close();
+}
diff --git a/lib/src/server.dart b/lib/src/server.dart
new file mode 100644
index 0000000..6a5c9d7
--- /dev/null
+++ b/lib/src/server.dart
@@ -0,0 +1,53 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library shelf.server;
+
+import 'dart:async';
+
+import 'handler.dart';
+
+/// An [adapter][] with a concrete URL.
+///
+/// [adapter]: https://github.com/dart-lang/shelf#adapters
+///
+/// The most basic definiton of "adapter" includes any function that passes
+/// incoming requests to a [Handler] and passes its responses to some external
+/// client. However, in practice, most adapters are also *servers*—that is,
+/// they're serving requests that are made to a certain well-known URL.
+///
+/// This interface represents those servers in a general way. It's useful for
+/// writing code that needs to know its own URL without tightly coupling that
+/// code to a single server implementation.
+///
+/// There are two built-in implementations of this interface. You can create a
+/// server backed by `dart:io` using [IOServer], or you can create a server
+/// that's backed by a normal [Handler] using [ServerHandler].
+///
+/// Implementations of this interface are responsible for ensuring that the
+/// members work as documented.
+abstract class Server {
+  /// The URL of the server.
+  ///
+  /// Requests to this URL or any URL beneath it are handled by the handler
+  /// passed to [mount]. If [mount] hasn't yet been called, the requests wait
+  /// until it is. If [close] has been called, [handler] will not be invoked;
+  /// otherwise, the behavior is implementation-dependent.
+  Uri get url;
+
+  /// Mounts [handler] as the base handler for this server.
+  ///
+  /// All requests to [url] or and URLs beneath it will be sent to [handler]
+  /// until [close] is called.
+  ///
+  /// Throws a [StateError] if there's already a handler mounted.
+  void mount(Handler handler);
+
+  /// Closes the server and returns a Future that completes when all resources
+  /// are released.
+  ///
+  /// Once this is called, no more requests will be passed to this server's
+  /// handler. Otherwise, the cleanup behavior is implementation-dependent.
+  Future close();
+}
diff --git a/lib/src/server_handler.dart b/lib/src/server_handler.dart
new file mode 100644
index 0000000..77b4ddc
--- /dev/null
+++ b/lib/src/server_handler.dart
@@ -0,0 +1,90 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library shelf.server_handler;
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+
+import 'request.dart';
+import 'handler.dart';
+import 'server.dart';
+
+/// A connected pair of a [Server] and a [Handler].
+///
+/// Requests to the handler are sent to the server's mounted handler once it's
+/// available. This is used to expose a virtual [Server] that's actually one
+/// part of a larger URL-space.
+class ServerHandler {
+  /// The server.
+  ///
+  /// Once this has a handler mounted, it's passed all requests to [handler]
+  /// until this server is closed.
+  Server get server => _server;
+  final _HandlerServer _server;
+
+  /// The handler.
+  ///
+  /// This passes requests to [server]'s handler. If that handler isn't mounted
+  /// yet, the requests are handled once it is.
+  Handler get handler => _onRequest;
+
+  /// Creates a new connected pair of a [Server] with the given [url] and a
+  /// [Handler].
+  ///
+  /// The caller is responsible for ensuring that requests to [url] or any URL
+  /// beneath it are handled by [handler].
+  ///
+  /// If [onClose] is passed, it's called when [server] is closed. It may return
+  /// a [Future] or `null`; its return value is returned by [Server.close].
+  ServerHandler(Uri url, {onClose()})
+      : _server = new _HandlerServer(url, onClose);
+
+  /// Pipes requests to [server]'s handler.
+  _onRequest(Request request) {
+    if (_server._closeMemo.hasRun) {
+      throw new StateError("Request received after the server was closed.");
+    }
+
+    if (_server._handler != null) return _server._handler(request);
+
+    // Avoid async/await so that the common case of a handler already being
+    // mounted doesn't involve any extra asynchronous delays.
+    return _server._onMounted.then((_) => _server._handler(request));
+  }
+}
+
+/// The [Server] returned by [ServerHandler].
+class _HandlerServer implements Server {
+  final Uri url;
+
+  /// The callback to call when [close] is called, or `null`.
+  final ZoneCallback _onClose;
+
+  /// The mounted handler.
+  ///
+  /// This is `null` until [mount] is called.
+  Handler _handler;
+
+  /// A future that fires once [mount] has been called.
+  Future get _onMounted => _onMountedCompleter.future;
+  final _onMountedCompleter = new Completer();
+
+  _HandlerServer(this.url, this._onClose);
+
+  void mount(Handler handler) {
+    if (_handler != null) {
+      throw new StateError("Can't mount two handlers for the same server.");
+    }
+
+    _handler = handler;
+    _onMountedCompleter.complete();
+  }
+
+  Future close() => _closeMemo.runOnce(() {
+    return _onClose == null ? null : _onClose();
+  });
+  final _closeMemo = new AsyncMemoizer();
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index dfdfeaf..6463c4b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,11 +1,12 @@
 name: shelf
-version: 0.6.3+1
+version: 0.6.4
 author: Dart Team <misc@dartlang.org>
 description: Web Server Middleware for Dart
 homepage: https://github.com/dart-lang/shelf
 environment:
-  sdk: '>=1.9.0 <2.0.0'
+  sdk: '>=1.12.0 <2.0.0'
 dependencies:
+  async: '^1.3.0'
   http_parser: '^1.0.0'
   path: '^1.0.0'
   stack_trace: '^1.0.0'
diff --git a/test/io_server_test.dart b/test/io_server_test.dart
new file mode 100644
index 0000000..7f35323
--- /dev/null
+++ b/test/io_server_test.dart
@@ -0,0 +1,41 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('vm')
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:http/http.dart' as http;
+import 'package:shelf/shelf_io.dart';
+import 'package:test/test.dart';
+
+import 'test_util.dart';
+
+void main() {
+  var server;
+  setUp(() async {
+    server = await IOServer.bind(InternetAddress.LOOPBACK_IP_V4, 0);
+  });
+
+  tearDown(() => server.close());
+
+  test("serves HTTP requests with the mounted handler", () async {
+    server.mount(syncHandler);
+    expect(await http.read(server.url), equals('Hello from /'));
+  });
+
+  test("delays HTTP requests until a handler is mounted", () async {
+    expect(http.read(server.url), completion(equals('Hello from /')));
+    await new Future.delayed(Duration.ZERO);
+
+    server.mount(asyncHandler);
+  });
+
+  test("disallows more than one handler from being mounted", () async {
+    server.mount((_) {});
+    expect(() => server.mount((_) {}), throwsStateError);
+    expect(() => server.mount((_) {}), throwsStateError);
+  });
+}
diff --git a/test/server_handler_test.dart b/test/server_handler_test.dart
new file mode 100644
index 0000000..9cf9063
--- /dev/null
+++ b/test/server_handler_test.dart
@@ -0,0 +1,76 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:shelf/shelf.dart';
+import 'package:test/test.dart';
+
+import 'test_util.dart';
+
+void main() {
+  test("passes the URL to the server", () {
+    var serverHandler = new ServerHandler(LOCALHOST_URI);
+    expect(serverHandler.server.url, equals(LOCALHOST_URI));
+  });
+
+  test("pipes a request from ServerHandler.handler to a mounted handler",
+      () async {
+    var serverHandler = new ServerHandler(LOCALHOST_URI);
+    serverHandler.server.mount(asyncHandler);
+
+    var response = await makeSimpleRequest(serverHandler.handler);
+    expect(response.statusCode, equals(200));
+    expect(response.readAsString(), completion(equals('Hello from /')));
+  });
+
+  test("waits until the server's handler is mounted to service a request",
+      () async {
+    var serverHandler = new ServerHandler(LOCALHOST_URI);
+    var future = makeSimpleRequest(serverHandler.handler);
+    await new Future.delayed(Duration.ZERO);
+
+    serverHandler.server.mount(syncHandler);
+    var response = await future;
+    expect(response.statusCode, equals(200));
+    expect(response.readAsString(), completion(equals('Hello from /')));
+  });
+
+  test("stops servicing requests after Server.close is called", () {
+    var serverHandler = new ServerHandler(LOCALHOST_URI);
+    serverHandler.server.mount(expectAsync((_) {}, count: 0));
+    serverHandler.server.close();
+
+    expect(makeSimpleRequest(serverHandler.handler), throwsStateError);
+  });
+
+  test("calls onClose when Server.close is called", () async {
+    var onCloseCalled = false;
+    var completer = new Completer();
+    var serverHandler = new ServerHandler(LOCALHOST_URI, onClose: () {
+      onCloseCalled = true;
+      return completer.future;
+    });
+
+    var closeDone = false;
+    serverHandler.server.close().then((_) {
+      closeDone = true;
+    });
+    expect(onCloseCalled, isTrue);
+    await new Future.delayed(Duration.ZERO);
+
+    expect(closeDone, isFalse);
+    completer.complete();
+    await new Future.delayed(Duration.ZERO);
+
+    expect(closeDone, isTrue);
+  });
+
+  test("doesn't allow Server.mount to be called multiple times", () {
+    var serverHandler = new ServerHandler(LOCALHOST_URI);
+    serverHandler.server.mount((_) {});
+    expect(() => serverHandler.server.mount((_) {}), throwsStateError);
+    expect(() => serverHandler.server.mount((_) {}), throwsStateError);
+  });
+}