Skip to content
This repository was archived by the owner on Oct 22, 2024. It is now read-only.

Commit 2fc1ab2

Browse files
committed
feature: Upgrade TypeMatcher, deprecate isInstanceOf
TypeMatcher - No longer abstract - Added type argument - Deprecate the existing `name` parameter, tell folks to the type arg - Moved to its own file - Added `having` method which allows chained validation of features - Eliminated 13 private implementations from the package - Just use it directly. Deprecate isInstanceOf class. - Tell folks to use TypeMatcher<T> instead - Run away from weirdly named classes Tests - centralizing tests in type_matcher_test - Removed isInstanceOf tests from core_matchers_test
1 parent 798c25b commit 2fc1ab2

9 files changed

+309
-155
lines changed

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
## 0.12.3
2+
3+
- Many improvements to `TypeMatcher`
4+
- Can now be used directly as `const TypeMatcher<MyType>()`.
5+
- Added a type parameter to specify the target `Type`.
6+
- Made the `name` constructor parameter optional and marked it deprecated.
7+
It's redundant to the type parameter.
8+
- Migrated all `isType` matchers to `TypeMatcher`.
9+
- Added a `having` function that allows chained validations of specific
10+
features of the target type.
11+
12+
```dart
13+
/// Validates that the object is a [RangeError] with a message containing
14+
/// the string 'details' and `start` and `end` properties that are `null`.
15+
final _rangeMatcher = isRangeError
16+
.having((e) => e.message, 'message', contains('details'))
17+
.having((e) => e.start, 'start', isNull)
18+
.having((e) => e.end, 'end', isNull);
19+
```
20+
21+
- Deprecated the `isInstanceOf` class. Use `TypeMatcher` instead.
22+
123
## 0.12.2+1
224
325
- Updated SDK version to 2.0.0-dev.17.0

lib/matcher.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ export 'src/numeric_matchers.dart';
1515
export 'src/operator_matchers.dart';
1616
export 'src/order_matchers.dart';
1717
export 'src/string_matchers.dart';
18+
export 'src/type_matcher.dart';
1819
export 'src/util.dart';

lib/src/core_matchers.dart

Lines changed: 9 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'interfaces.dart';
6+
import 'type_matcher.dart';
67
import 'util.dart';
78

89
/// Returns a matcher that matches the isEmpty property.
@@ -103,24 +104,14 @@ class _IsAnything extends Matcher {
103104
Description describe(Description description) => description.add('anything');
104105
}
105106

107+
/// **DEPRECATED** Use [TypeMatcher] instead.
108+
///
106109
/// Returns a matcher that matches if an object is an instance
107110
/// of [T] (or a subtype).
108-
///
109-
/// As types are not first class objects in Dart we can only
110-
/// approximate this test by using a generic wrapper class.
111-
///
112-
/// For example, to test whether 'bar' is an instance of type
113-
/// 'Foo', we would write:
114-
///
115-
/// expect(bar, new isInstanceOf<Foo>());
111+
@Deprecated('Use `const TypeMatcher<MyType>()` instead.')
116112
// ignore: camel_case_types
117-
class isInstanceOf<T> extends Matcher {
113+
class isInstanceOf<T> extends TypeMatcher<T> {
118114
const isInstanceOf();
119-
120-
bool matches(item, Map matchState) => item is T;
121-
122-
Description describe(Description description) =>
123-
description.add('an instance of $T');
124115
}
125116

126117
/// A matcher that matches a function call against no exception.
@@ -157,48 +148,11 @@ class _ReturnsNormally extends Matcher {
157148
}
158149
}
159150

160-
/*
161-
* Matchers for different exception types. Ideally we should just be able to
162-
* use something like:
163-
*
164-
* final Matcher throwsException =
165-
* const _Throws(const isInstanceOf<Exception>());
166-
*
167-
* Unfortunately instanceOf is not working with dart2js.
168-
*
169-
* Alternatively, if static functions could be used in const expressions,
170-
* we could use:
171-
*
172-
* bool _isException(x) => x is Exception;
173-
* final Matcher isException = const _Predicate(_isException, "Exception");
174-
* final Matcher throwsException = const _Throws(isException);
175-
*
176-
* But currently using static functions in const expressions is not supported.
177-
* For now the only solution for all platforms seems to be separate classes
178-
* for each exception type.
179-
*/
180-
181-
abstract class TypeMatcher extends Matcher {
182-
final String _name;
183-
const TypeMatcher(this._name);
184-
Description describe(Description description) => description.add(_name);
185-
}
186-
187-
/// A matcher for Map types.
188-
const Matcher isMap = const _IsMap();
189-
190-
class _IsMap extends TypeMatcher {
191-
const _IsMap() : super("Map");
192-
bool matches(item, Map matchState) => item is Map;
193-
}
194-
195-
/// A matcher for List types.
196-
const Matcher isList = const _IsList();
151+
/// A matcher for [Map].
152+
const isMap = const TypeMatcher<Map>();
197153

198-
class _IsList extends TypeMatcher {
199-
const _IsList() : super('List');
200-
bool matches(item, Map matchState) => item is List;
201-
}
154+
/// A matcher for [List].
155+
const isList = const TypeMatcher<List>();
202156

203157
/// Returns a matcher that matches if an object has a length property
204158
/// that matches [matcher].

lib/src/error_matchers.dart

Lines changed: 25 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -2,94 +2,39 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
import 'core_matchers.dart';
6-
import 'interfaces.dart';
5+
import 'type_matcher.dart';
76

8-
/// A matcher for ArgumentErrors.
9-
const Matcher isArgumentError = const _ArgumentError();
7+
/// A matcher for [ArgumentError].
8+
const isArgumentError = const TypeMatcher<ArgumentError>();
109

11-
class _ArgumentError extends TypeMatcher {
12-
const _ArgumentError() : super("ArgumentError");
13-
bool matches(item, Map matchState) => item is ArgumentError;
14-
}
10+
/// A matcher for [ConcurrentModificationError].
11+
const isConcurrentModificationError =
12+
const TypeMatcher<ConcurrentModificationError>();
1513

16-
/// A matcher for ConcurrentModificationError.
17-
const Matcher isConcurrentModificationError =
18-
const _ConcurrentModificationError();
14+
/// A matcher for [CyclicInitializationError].
15+
const isCyclicInitializationError =
16+
const TypeMatcher<CyclicInitializationError>();
1917

20-
class _ConcurrentModificationError extends TypeMatcher {
21-
const _ConcurrentModificationError() : super("ConcurrentModificationError");
22-
bool matches(item, Map matchState) => item is ConcurrentModificationError;
23-
}
18+
/// A matcher for [Exception].
19+
const isException = const TypeMatcher<Exception>();
2420

25-
/// A matcher for CyclicInitializationError.
26-
const Matcher isCyclicInitializationError = const _CyclicInitializationError();
21+
/// A matcher for [FormatException].
22+
const isFormatException = const TypeMatcher<FormatException>();
2723

28-
class _CyclicInitializationError extends TypeMatcher {
29-
const _CyclicInitializationError() : super("CyclicInitializationError");
30-
bool matches(item, Map matchState) => item is CyclicInitializationError;
31-
}
24+
/// A matcher for [NoSuchMethodError].
25+
const isNoSuchMethodError = const TypeMatcher<NoSuchMethodError>();
3226

33-
/// A matcher for Exceptions.
34-
const Matcher isException = const _Exception();
27+
/// A matcher for [NullThrownError].
28+
const isNullThrownError = const TypeMatcher<NullThrownError>();
3529

36-
class _Exception extends TypeMatcher {
37-
const _Exception() : super("Exception");
38-
bool matches(item, Map matchState) => item is Exception;
39-
}
30+
/// A matcher for [RangeError].
31+
const isRangeError = const TypeMatcher<RangeError>();
4032

41-
/// A matcher for FormatExceptions.
42-
const Matcher isFormatException = const _FormatException();
33+
/// A matcher for [StateError].
34+
const isStateError = const TypeMatcher<StateError>();
4335

44-
class _FormatException extends TypeMatcher {
45-
const _FormatException() : super("FormatException");
46-
bool matches(item, Map matchState) => item is FormatException;
47-
}
36+
/// A matcher for [UnimplementedError].
37+
const isUnimplementedError = const TypeMatcher<UnimplementedError>();
4838

49-
/// A matcher for NoSuchMethodErrors.
50-
const Matcher isNoSuchMethodError = const _NoSuchMethodError();
51-
52-
class _NoSuchMethodError extends TypeMatcher {
53-
const _NoSuchMethodError() : super("NoSuchMethodError");
54-
bool matches(item, Map matchState) => item is NoSuchMethodError;
55-
}
56-
57-
/// A matcher for NullThrownError.
58-
const Matcher isNullThrownError = const _NullThrownError();
59-
60-
class _NullThrownError extends TypeMatcher {
61-
const _NullThrownError() : super("NullThrownError");
62-
bool matches(item, Map matchState) => item is NullThrownError;
63-
}
64-
65-
/// A matcher for RangeErrors.
66-
const Matcher isRangeError = const _RangeError();
67-
68-
class _RangeError extends TypeMatcher {
69-
const _RangeError() : super("RangeError");
70-
bool matches(item, Map matchState) => item is RangeError;
71-
}
72-
73-
/// A matcher for StateErrors.
74-
const Matcher isStateError = const _StateError();
75-
76-
class _StateError extends TypeMatcher {
77-
const _StateError() : super("StateError");
78-
bool matches(item, Map matchState) => item is StateError;
79-
}
80-
81-
/// A matcher for UnimplementedErrors.
82-
const Matcher isUnimplementedError = const _UnimplementedError();
83-
84-
class _UnimplementedError extends TypeMatcher {
85-
const _UnimplementedError() : super("UnimplementedError");
86-
bool matches(item, Map matchState) => item is UnimplementedError;
87-
}
88-
89-
/// A matcher for UnsupportedError.
90-
const Matcher isUnsupportedError = const _UnsupportedError();
91-
92-
class _UnsupportedError extends TypeMatcher {
93-
const _UnsupportedError() : super("UnsupportedError");
94-
bool matches(item, Map matchState) => item is UnsupportedError;
95-
}
39+
/// A matcher for [UnsupportedError].
40+
const isUnsupportedError = const TypeMatcher<UnsupportedError>();

lib/src/having_matcher.dart

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'custom_matcher.dart';
6+
import 'interfaces.dart';
7+
import 'type_matcher.dart';
8+
import 'util.dart';
9+
10+
/// A package-private [TypeMatcher] implementation that handles is returned
11+
/// by calls to [TypeMatcher.having].
12+
class HavingMatcher<T> implements TypeMatcher<T> {
13+
final TypeMatcher<T> _parent;
14+
final List<_FunctionMatcher> _functionMatchers;
15+
16+
HavingMatcher(TypeMatcher<T> parent, String description,
17+
Object feature(T source), Object matcher,
18+
[Iterable<_FunctionMatcher> existing])
19+
: this._parent = parent,
20+
this._functionMatchers = <_FunctionMatcher>[]
21+
..addAll(existing ?? [])
22+
..add(new _FunctionMatcher<T>(description, feature, matcher));
23+
24+
TypeMatcher<T> having(
25+
Object feature(T source), String description, Object matcher) =>
26+
new HavingMatcher(
27+
_parent, description, feature, matcher, _functionMatchers);
28+
29+
bool matches(item, Map matchState) {
30+
for (var matcher in <Matcher>[_parent].followedBy(_functionMatchers)) {
31+
if (!matcher.matches(item, matchState)) {
32+
addStateInfo(matchState, {'matcher': matcher});
33+
return false;
34+
}
35+
}
36+
return true;
37+
}
38+
39+
Description describeMismatch(
40+
item, Description mismatchDescription, Map matchState, bool verbose) {
41+
var matcher = matchState['matcher'] as Matcher;
42+
matcher.describeMismatch(
43+
item, mismatchDescription, matchState['state'] as Map, verbose);
44+
return mismatchDescription;
45+
}
46+
47+
Description describe(Description description) => description
48+
.add('')
49+
.addDescriptionOf(_parent)
50+
.add(' with ')
51+
.addAll('', ' and ', '', _functionMatchers);
52+
}
53+
54+
class _FunctionMatcher<T> extends CustomMatcher {
55+
final dynamic Function(T value) _feature;
56+
57+
_FunctionMatcher(String name, this._feature, matcher)
58+
: super('`$name`:', '`$name`', matcher);
59+
60+
@override
61+
Object featureValueOf(covariant T actual) => _feature(actual);
62+
}

lib/src/type_matcher.dart

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'having_matcher.dart';
6+
import 'interfaces.dart';
7+
8+
/// A [Matcher] subclass that supports validating the [Type] of the target
9+
/// object.
10+
///
11+
/// ```dart
12+
/// expect(shouldBeDuration, new TypeMatcher<Duration>());
13+
/// ```
14+
///
15+
/// If you want to further validate attributes of the specified [Type], use the
16+
/// [having] function.
17+
///
18+
/// ```dart
19+
/// void shouldThrowRangeError(int value) {
20+
/// throw new RangeError.range(value, 10, 20);
21+
/// }
22+
///
23+
/// expect(
24+
/// () => shouldThrowRangeError(5),
25+
/// throwsA(const TypeMatcher<RangeError>()
26+
/// .having((e) => e.start, 'start', greaterThanOrEqualTo(10))
27+
/// .having((e) => e.end, 'end', lessThanOrEqualTo(20))));
28+
/// ```
29+
///
30+
/// Notice that you can chain multiple calls to [having] to verify multiple
31+
/// aspects of an object.
32+
///
33+
/// Note: All of the top-level `isType` matchers exposed by this package are
34+
/// instances of [TypeMatcher], so you can use the [having] function without
35+
/// creating your own instance.
36+
///
37+
/// ```dart
38+
/// expect(
39+
/// () => shouldThrowRangeError(5),
40+
/// throwsA(isRangeError
41+
/// .having((e) => e.start, 'start', greaterThanOrEqualTo(10))
42+
/// .having((e) => e.end, 'end', lessThanOrEqualTo(20))));
43+
/// ```
44+
class TypeMatcher<T> extends Matcher {
45+
final String _name;
46+
const TypeMatcher(
47+
[@Deprecated('Provide a type argument to TypeMatcher and omit the name. '
48+
'This argument will be removed in the next release.')
49+
String name])
50+
: this._name =
51+
// ignore: deprecated_member_use
52+
name;
53+
54+
/// Returns a new [TypeMatcher] that validates the existing type as well as
55+
/// a specific [feature] of the object with the provided [matcher].
56+
///
57+
/// Provides a human-readable [description] of the [feature] to make debugging
58+
/// failures easier.
59+
///
60+
/// ```dart
61+
/// /// Validates that the object is a [RangeError] with a message containing
62+
/// /// the string 'details' and `start` and `end` properties that are `null`.
63+
/// final _rangeMatcher = isRangeError
64+
/// .having((e) => e.message, 'message', contains('details'))
65+
/// .having((e) => e.start, 'start', isNull)
66+
/// .having((e) => e.end, 'end', isNull);
67+
/// ```
68+
TypeMatcher<T> having(
69+
Object feature(T source), String description, Object matcher) =>
70+
new HavingMatcher(this, description, feature, matcher);
71+
72+
Description describe(Description description) {
73+
var name = _name ?? _stripDynamic(T);
74+
return description.add("<Instance of '$name'>");
75+
}
76+
77+
bool matches(Object item, Map matchState) => item is T;
78+
}
79+
80+
final _dart2DynamicArgs = new RegExp('<dynamic(, dynamic)*>');
81+
82+
/// With this expression `{}.runtimeType.toString`,
83+
/// Dart 1: "<Instance of Map>
84+
/// Dart 2: "<Instance of Map<dynamic, dynamic>>"
85+
///
86+
/// This functions returns the Dart 1 output, when Dart 2 runtime semantics
87+
/// are enabled.
88+
String _stripDynamic(Type type) =>
89+
type.toString().replaceAll(_dart2DynamicArgs, '');

0 commit comments

Comments
 (0)