Expose a smaller surface area for custom watchers (#92)

- Remove the function to unregister a custom watcher, we can add it
  later when we have a specific use case for it.
- Remove the class `CustomFactoryWatcher` and take callbacks instead.
- Bump the version and add the changelog that was missed.
- Add missing copyright notice.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a65a6e3..a98ae20 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# 0.9.8
+
+* Add the ability to create custom Watcher types for specific file paths.
+
 # 0.9.7+15
 
 * Fix a bug on Mac where modifying a directory with a path exactly matching a
diff --git a/lib/src/custom_watcher_factory.dart b/lib/src/custom_watcher_factory.dart
index b7f81fa..3dc1bea 100644
--- a/lib/src/custom_watcher_factory.dart
+++ b/lib/src/custom_watcher_factory.dart
@@ -1,35 +1,48 @@
+// Copyright (c) 2020, 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 '../watcher.dart';
 
-/// Defines a way to create a custom watcher instead of the default ones.
-///
-/// This will be used when a [DirectoryWatcher] or [FileWatcher] would be
-/// created and will take precedence over the default ones.
-abstract class CustomWatcherFactory {
-  /// Uniquely identify this watcher.
-  String get id;
+/// A factory to produce custom watchers for specific file paths.
+class _CustomWatcherFactory {
+  final String id;
+  final DirectoryWatcher Function(String path, {Duration pollingDelay})
+      createDirectoryWatcher;
+  final FileWatcher Function(String path, {Duration pollingDelay})
+      createFileWatcher;
 
-  /// Tries to create a [DirectoryWatcher] for the provided path.
-  ///
-  /// Returns `null` if the path is not supported by this factory.
-  DirectoryWatcher createDirectoryWatcher(String path, {Duration pollingDelay});
-
-  /// Tries to create a [FileWatcher] for the provided path.
-  ///
-  /// Returns `null` if the path is not supported by this factory.
-  FileWatcher createFileWatcher(String path, {Duration pollingDelay});
+  _CustomWatcherFactory(
+      this.id, this.createDirectoryWatcher, this.createFileWatcher);
 }
 
 /// Registers a custom watcher.
 ///
-/// It's only allowed to register a watcher factory once per [id] and at most
-/// one factory should apply to any given file (creating a [Watcher] will fail
-/// otherwise).
-void registerCustomWatcherFactory(CustomWatcherFactory customFactory) {
-  if (_customWatcherFactories.containsKey(customFactory.id)) {
-    throw ArgumentError('A custom watcher with id `${customFactory.id}` '
+/// Each custom watcher must have a unique [id] and the same watcher may not be
+/// registered more than once.
+/// [createDirectoryWatcher] and [createFileWatcher] should return watchers for
+/// the file paths they are able to handle. If the custom watcher is not able to
+/// handle the path it should reuturn null.
+/// The paths handled by each custom watch may not overlap, at most one custom
+/// matcher may return a non-null watcher for a given path.
+///
+/// When a file or directory watcher is created the path is checked against each
+/// registered custom watcher, and if exactly one custom watcher is available it
+/// will be used instead of the default.
+void registerCustomWatcher(
+  String id,
+  DirectoryWatcher Function(String path, {Duration pollingDelay})
+      createDirectoryWatcher,
+  FileWatcher Function(String path, {Duration pollingDelay}) createFileWatcher,
+) {
+  if (_customWatcherFactories.containsKey(id)) {
+    throw ArgumentError('A custom watcher with id `$id` '
         'has already been registered');
   }
-  _customWatcherFactories[customFactory.id] = customFactory;
+  _customWatcherFactories[id] = _CustomWatcherFactory(
+      id,
+      createDirectoryWatcher ?? (_, {pollingDelay}) => null,
+      createFileWatcher ?? (_, {pollingDelay}) => null);
 }
 
 /// Tries to create a custom [DirectoryWatcher] and returns it.
@@ -40,7 +53,7 @@
     {Duration pollingDelay}) {
   DirectoryWatcher customWatcher;
   String customFactoryId;
-  for (var watcherFactory in customWatcherFactories) {
+  for (var watcherFactory in _customWatcherFactories.values) {
     if (customWatcher != null) {
       throw StateError('Two `CustomWatcherFactory`s applicable: '
           '`$customFactoryId` and `${watcherFactory.id}` for `$path`');
@@ -59,7 +72,7 @@
 FileWatcher createCustomFileWatcher(String path, {Duration pollingDelay}) {
   FileWatcher customWatcher;
   String customFactoryId;
-  for (var watcherFactory in customWatcherFactories) {
+  for (var watcherFactory in _customWatcherFactories.values) {
     if (customWatcher != null) {
       throw StateError('Two `CustomWatcherFactory`s applicable: '
           '`$customFactoryId` and `${watcherFactory.id}` for `$path`');
@@ -71,13 +84,4 @@
   return customWatcher;
 }
 
-/// Unregisters a custom watcher and returns it.
-///
-/// Returns `null` if the id was never registered.
-CustomWatcherFactory unregisterCustomWatcherFactory(String id) =>
-    _customWatcherFactories.remove(id);
-
-Iterable<CustomWatcherFactory> get customWatcherFactories =>
-    _customWatcherFactories.values;
-
-final _customWatcherFactories = <String, CustomWatcherFactory>{};
+final _customWatcherFactories = <String, _CustomWatcherFactory>{};
diff --git a/lib/watcher.dart b/lib/watcher.dart
index f5c7d3e..5ea8beb 100644
--- a/lib/watcher.dart
+++ b/lib/watcher.dart
@@ -9,11 +9,7 @@
 import 'src/file_watcher.dart';
 import 'src/watch_event.dart';
 
-export 'src/custom_watcher_factory.dart'
-    show
-        CustomWatcherFactory,
-        registerCustomWatcherFactory,
-        unregisterCustomWatcherFactory;
+export 'src/custom_watcher_factory.dart' show registerCustomWatcher;
 export 'src/directory_watcher.dart';
 export 'src/directory_watcher/polling.dart';
 export 'src/file_watcher.dart';
diff --git a/test/custom_watcher_factory_test.dart b/test/custom_watcher_factory_test.dart
index 6c9ffd8..8210c06 100644
--- a/test/custom_watcher_factory_test.dart
+++ b/test/custom_watcher_factory_test.dart
@@ -3,19 +3,17 @@
 import 'package:test/test.dart';
 import 'package:watcher/watcher.dart';
 
-import 'utils.dart';
-
 void main() {
   _MemFs memFs;
   final defaultFactoryId = 'MemFs';
 
-  setUp(() {
+  setUpAll(() {
     memFs = _MemFs();
-    registerCustomWatcherFactory(_MemFsWatcherFactory(defaultFactoryId, memFs));
-  });
-
-  tearDown(() async {
-    unregisterCustomWatcherFactory(defaultFactoryId);
+    var watcherFactory = _MemFsWatcherFactory(memFs);
+    registerCustomWatcher(
+        defaultFactoryId,
+        watcherFactory.createDirectoryWatcher,
+        watcherFactory.createFileWatcher);
   });
 
   test('notifes for files', () async {
@@ -44,34 +42,19 @@
     expect(event.path, 'dir');
   });
 
-  test('unregister works', () async {
-    unregisterCustomWatcherFactory(defaultFactoryId);
-
-    watcherFactory = (path) => FileWatcher(path);
-    try {
-      // This uses standard files, so it wouldn't trigger an event in
-      // _MemFsWatcher.
-      writeFile('file.txt');
-      await startWatcher(path: 'file.txt');
-      deleteFile('file.txt');
-    } finally {
-      watcherFactory = null;
-    }
-
-    await expectRemoveEvent('file.txt');
-  });
-
   test('registering twice throws', () async {
     expect(
-        () => registerCustomWatcherFactory(
-            _MemFsWatcherFactory(defaultFactoryId, memFs)),
+        () => registerCustomWatcher(defaultFactoryId,
+            (_, {pollingDelay}) => null, (_, {pollingDelay}) => null),
         throwsA(isA<ArgumentError>()));
   });
 
   test('finding two applicable factories throws', () async {
     // Note that _MemFsWatcherFactory always returns a watcher, so having two
     // will always produce a conflict.
-    registerCustomWatcherFactory(_MemFsWatcherFactory('Different id', memFs));
+    var watcherFactory = _MemFsWatcherFactory(memFs);
+    registerCustomWatcher('Different id', watcherFactory.createDirectoryWatcher,
+        watcherFactory.createFileWatcher);
     expect(() => FileWatcher('file.txt'), throwsA(isA<StateError>()));
     expect(() => DirectoryWatcher('dir'), throwsA(isA<StateError>()));
   });
@@ -129,18 +112,14 @@
   Future<void> get ready async {}
 }
 
-class _MemFsWatcherFactory implements CustomWatcherFactory {
-  @override
-  final String id;
+class _MemFsWatcherFactory {
   final _MemFs _memFs;
-  _MemFsWatcherFactory(this.id, this._memFs);
+  _MemFsWatcherFactory(this._memFs);
 
-  @override
   DirectoryWatcher createDirectoryWatcher(String path,
           {Duration pollingDelay}) =>
       _MemFsWatcher(path, _memFs.watchStream(path));
 
-  @override
   FileWatcher createFileWatcher(String path, {Duration pollingDelay}) =>
       _MemFsWatcher(path, _memFs.watchStream(path));
 }