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);
+ });
+}