Skip to content

Commit

Permalink
feat: use Dart 3 class modifiers (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
mirland authored May 7, 2024
1 parent d57a472 commit eb908d4
Show file tree
Hide file tree
Showing 18 changed files with 97 additions and 187 deletions.
2 changes: 1 addition & 1 deletion lib/src/errors.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// Used to notify unexpected errors
class StockError extends Error {
final class StockError extends Error {
/// Constructor
StockError(this.message);

Expand Down
4 changes: 1 addition & 3 deletions lib/src/fetcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ import 'package:stock/src/stock_response.dart';
/// See [ofFuture] for easily translating from a regular `Future` function.
/// See [ofStream], for easily translating to [StockResponse] (and
/// automatically transforming exceptions into [StockResponseError]).
abstract class Fetcher<Key, T> {
Fetcher._();

base class Fetcher<Key, T> {
/// "Creates" a [Fetcher] from a [futureFactory] and translates the results
/// into a [StockResponse].
///
Expand Down
6 changes: 3 additions & 3 deletions lib/src/implementations/factory_fetcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

import 'package:stock/src/fetcher.dart';

class FactoryFetcher<Key, Output> implements Fetcher<Key, Output> {
sealed class FactoryFetcher<Key, Output> extends Fetcher<Key, Output> {
FactoryFetcher(this.factory);

Stream<Output> Function(Key key) factory;
}

class FutureFetcher<Key, Output> extends FactoryFetcher<Key, Output> {
final class FutureFetcher<Key, Output> extends FactoryFetcher<Key, Output> {
FutureFetcher(Future<Output> Function(Key key) factory)
: super((key) => Stream.fromFuture(factory(key)));
}

class StreamFetcher<Key, Output> extends FactoryFetcher<Key, Output> {
final class StreamFetcher<Key, Output> extends FactoryFetcher<Key, Output> {
StreamFetcher(super.factory);
}
5 changes: 3 additions & 2 deletions lib/src/implementations/source_of_truth_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import 'package:stock/src/source_of_truth.dart';

class SourceOfTruthImpl<Key, T> implements SourceOfTruth<Key, T> {
final class SourceOfTruthImpl<Key, T> implements SourceOfTruth<Key, T> {
SourceOfTruthImpl(this._reader, this._writer, this._delete, this._deleteAll);

final Stream<T?> Function(Key key) _reader;
Expand All @@ -23,7 +23,8 @@ class SourceOfTruthImpl<Key, T> implements SourceOfTruth<Key, T> {
Future<void> deleteAll() async => await _deleteAll?.call();
}

class WriteWrappedSourceOfTruth<Key, T> extends CachedSourceOfTruth<Key, T> {
final class WriteWrappedSourceOfTruth<Key, T>
extends CachedSourceOfTruth<Key, T> {
WriteWrappedSourceOfTruth(this._realSourceOfTruth);

final SourceOfTruth<Key, T>? _realSourceOfTruth;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/source_of_truth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import 'package:stock/src/stock_extensions.dart';
/// local storage is needed.
/// For this case you can use the [mapTo] and [mapToUsingMapper] extensions.
///
abstract class SourceOfTruth<Key, T> {
abstract interface class SourceOfTruth<Key, T> {
/// Creates a source of truth that is accessed via [reader], [writer],
/// [delete] and [deleteAll].
///
Expand Down
2 changes: 1 addition & 1 deletion lib/src/stock.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import 'package:stock/src/stock_response.dart';
/// truth. Its purpose is to eliminate the need for waiting on a network update
/// before local modifications are available (via [Stock.stream]).
///
abstract class Stock<Key, T> {
abstract interface class Stock<Key, T> {
/// Stock constructor
factory Stock({
required Fetcher<Key, T> fetcher,
Expand Down
2 changes: 1 addition & 1 deletion lib/src/stock_request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import 'package:stock/src/stock.dart';
/// If it's `false`, [Stock] will try to return the [SourceOfTruth] data, and
/// if it doesn't exist, [Stock] will request fresh data using the [Fetcher].
///
class StockRequest<Key> {
final class StockRequest<Key> {
/// [StockRequest] constructor
StockRequest({
required this.key,
Expand Down
8 changes: 4 additions & 4 deletions lib/src/stock_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import 'package:stock/src/stock.dart';
/// uses this holder class to represent each response. This allows the [Stream]
/// to keep flowing even if an error happens so that if there is an observable
/// single source of truth, the application can keep observing it.
class StockResponse<Output> {
sealed class StockResponse<Output> {
const StockResponse._(this.origin);

/// Loading event dispatched by [Stock] to signal the [Fetcher] is currently
Expand All @@ -35,7 +35,7 @@ class StockResponse<Output> {
/// Loading event dispatched by [Stock] to signal the [Fetcher] is currently
/// running.
@immutable
class StockResponseLoading<T> extends StockResponse<T> {
final class StockResponseLoading<T> extends StockResponse<T> {
/// StockResponseLoading constructor
const StockResponseLoading(super.origin) : super._();

Expand All @@ -56,7 +56,7 @@ class StockResponseLoading<T> extends StockResponse<T> {

/// Data dispatched by [Stock]
@immutable
class StockResponseData<T> extends StockResponse<T> {
final class StockResponseData<T> extends StockResponse<T> {
/// StockResponseData constructor
const StockResponseData(super.origin, this.value) : super._();

Expand All @@ -80,7 +80,7 @@ class StockResponseData<T> extends StockResponse<T> {

/// Error dispatched by a pipeline
@immutable
class StockResponseError<T> extends StockResponse<T> {
final class StockResponseError<T> extends StockResponse<T> {
/// StockResponseError constructor
const StockResponseError(super.origin, this.error, [this.stackTrace])
: super._();
Expand Down
47 changes: 22 additions & 25 deletions lib/src/stock_response_extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,27 @@ import 'package:stock/src/stock_response.dart';
/// Useful [StockResponse] extensions.
extension StockResponseExtensions<T> on StockResponse<T> {
/// Returns the available data or throws error if there is no data.
T requireData() => when(
onData: (_, data) => data,
onLoading: (_) => throw StockError('There is no data in loading'),
T requireData() => switch (this) {
StockResponseLoading<T>() =>
throw StockError('There is no data in loading'),
StockResponseData<T>(value: final value) => value,
// ignore: only_throw_errors
onError: (_, error, __) => throw error,
);
StockResponseError<T>(error: final error) => throw error,
};

/// If there is data available, returns it; otherwise returns null.
T? getDataOrNull() => this is StockResponseData<T>
? (this as StockResponseData<T>).value
: null;
T? getDataOrNull() => switch (this) {
StockResponseData<T>(value: final value) => value,
_ => null
};

/// Returns the available data or throws error if there is no data.
Object requireError() => maybeMap(
onError: (response) => response.error,
orElse: () => throw StockError(
'Response is not an StockResponseError. Response: $this',
),
);
Object requireError() => switch (this) {
StockResponseError<T>(error: final error) => error,
_ => throw StockError(
'Response is not an StockResponseError. Response: $this',
),
};

/// If this [StockResponse] is of type [StockResponseError], throws the
/// exception. Otherwise, does nothing.
Expand Down Expand Up @@ -61,17 +63,12 @@ extension StockResponseExtensions<T> on StockResponse<T> {
required E Function(StockResponseLoading<T> value) onLoading,
required E Function(StockResponseData<T> value) onData,
required E Function(StockResponseError<T> value) onError,
}) {
if (this is StockResponseLoading<T>) {
return onLoading(this as StockResponseLoading<T>);
} else if (this is StockResponseData<T>) {
return onData(this as StockResponseData<T>);
} else if (this is StockResponseError<T>) {
return onError(this as StockResponseError<T>);
} else {
throw StockError('Unknown StockResponse type: $this');
}
}
}) =>
switch (this) {
StockResponseLoading<T>() => onLoading(this as StockResponseLoading<T>),
StockResponseData<T>() => onData(this as StockResponseData<T>),
StockResponseError<T>() => onError(this as StockResponseError<T>),
};

/// Invokes [onData] or [orElse] as fallback if the response is successful,
/// [onLoading] or [orElse] as fallback if the response is loading, and
Expand Down
2 changes: 1 addition & 1 deletion lib/src/type_mapper.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// Converts the [Input] type to the [Output] and vice versa.
/// Used to transform DB entity to network entity and vice versa.
abstract class StockTypeMapper<Input, Output> {
abstract interface class StockTypeMapper<Input, Output> {
/// Transform a [Input] into a [Output]
Output fromInput(Input value);

Expand Down
13 changes: 8 additions & 5 deletions test/clear_and_clear_all_test.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import 'package:mockito/mockito.dart';
import 'package:stock/src/implementations/factory_fetcher.dart';
import 'package:stock/src/stock.dart';
import 'package:test/test.dart';

import 'common/fetcher_mock.dart';
import 'common/source_of_truth/cached_and_mocked_source_of_truth.dart';
import 'common_mocks.mocks.dart';

void main() {
group('Clear tests', () {
test('Stock Clear invokes SOT clear', () async {
final sourceOfTruth = createMockedSourceOfTruth<int, int>();

final fetcher = MockFutureFetcher<int, int>();
var timesCalled = 0;
when(fetcher.factory).thenReturn((key) => Stream.value(++timesCalled));
final mockFetcher =
MockFutureFetcher<int, int>((key) => Future.value(++timesCalled));
final fetcher = FutureFetcher<int, int>(mockFetcher.factory);

final stock = Stock<int, int>(
fetcher: fetcher,
Expand All @@ -28,9 +30,10 @@ void main() {
test('Stock ClearAll invokes SOT clearAll', () async {
final sourceOfTruth = createMockedSourceOfTruth<int, int>();

final fetcher = MockFutureFetcher<int, int>();
var timesCalled = 0;
when(fetcher.factory).thenReturn((key) => Stream.value(++timesCalled));
final mockFetcher =
MockFutureFetcher<int, int>((key) => Future.value(++timesCalled));
final fetcher = FutureFetcher<int, int>(mockFetcher.factory);

final stock = Stock<int, int>(
fetcher: fetcher,
Expand Down
12 changes: 12 additions & 0 deletions test/common/fetcher_mock.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class MockFutureFetcher<Key, Output> {
MockFutureFetcher(Future<Output> Function(Key key) factory) {
this.factory = (key) {
invocations++;
return factory(key);
};
}

int invocations = 0;

late Future<Output> Function(Key key) factory;
}
3 changes: 0 additions & 3 deletions test/common_mocks.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import 'package:mockito/annotations.dart';
import 'package:stock/src/implementations/factory_fetcher.dart';
import 'package:stock/src/source_of_truth.dart';

import 'stock_response_extension_test.dart';

@GenerateMocks([
CallbackVoid,
CallbackInt,
FutureFetcher,
StreamFetcher,
SourceOfTruth,
])
void main() {}
58 changes: 6 additions & 52 deletions test/common_mocks.mocks.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit eb908d4

Please sign in to comment.