From fa967b9d5afa3af99ef1c8b31931deefead0a5dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Irland?= Date: Wed, 24 Aug 2022 16:39:11 -0300 Subject: [PATCH] Add initial version (#1) --- .gitattributes | 6 + .github/CODEOWNERS | 1 + .gitignore | 33 + .metadata | 10 + CHANGELOG.md | 3 + LICENSE | 1 + analysis_options.yaml | 4 + lib/errors.dart | 8 + lib/fetcher.dart | 11 + lib/source_of_truth.dart | 43 ++ .../extensions/future_stream_extensions.dart | 20 + .../extensions/store_response_extensions.dart | 36 ++ lib/src/factory_fetcher.dart | 16 + lib/src/store_impl.dart | 181 ++++++ lib/src/wrapped_source_of_truth.dart | 13 + lib/store.dart | 20 + lib/store_extensions.dart | 18 + lib/store_request.dart | 9 + lib/store_response.dart | 28 + lib/store_response.freezed.dart | 606 ++++++++++++++++++ pubspec.lock | 482 ++++++++++++++ pubspec.yaml | 24 + scripts/checks.sh | 14 + scripts/clean_up.sh | 9 + test/common/response_extensions.dart | 7 + .../cached_and_mocked_source_of_truth.dart | 34 + ...ed_source_of_truth_with_default_value.dart | 15 + .../source_of_truth_with_delay.dart | 28 + .../source_of_truth_with_error.dart | 35 + test/common/store_test_extensions.dart | 27 + test/common_errors_test.dart | 70 ++ test/common_mocks.dart | 10 + test/common_mocks.mocks.dart | 90 +++ test/fresh_and_get_test.dart | 64 ++ test/multiple_request_test.dart | 39 ++ test/refresh_test.dart | 121 ++++ test/store_response_extension_test.dart | 79 +++ test/store_valid_result_store_test.dart | 91 +++ test/store_without_key_test.dart | 31 + 39 files changed, 2337 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/CODEOWNERS create mode 100644 .gitignore create mode 100644 .metadata create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 analysis_options.yaml create mode 100644 lib/errors.dart create mode 100644 lib/fetcher.dart create mode 100644 lib/source_of_truth.dart create mode 100644 lib/src/extensions/future_stream_extensions.dart create mode 100644 lib/src/extensions/store_response_extensions.dart create mode 100644 lib/src/factory_fetcher.dart create mode 100644 lib/src/store_impl.dart create mode 100644 lib/src/wrapped_source_of_truth.dart create mode 100644 lib/store.dart create mode 100644 lib/store_extensions.dart create mode 100644 lib/store_request.dart create mode 100644 lib/store_response.dart create mode 100644 lib/store_response.freezed.dart create mode 100644 pubspec.lock create mode 100644 pubspec.yaml create mode 100755 scripts/checks.sh create mode 100755 scripts/clean_up.sh create mode 100644 test/common/response_extensions.dart create mode 100644 test/common/source_of_truth/cached_and_mocked_source_of_truth.dart create mode 100644 test/common/source_of_truth/cached_source_of_truth_with_default_value.dart create mode 100644 test/common/source_of_truth/source_of_truth_with_delay.dart create mode 100644 test/common/source_of_truth/source_of_truth_with_error.dart create mode 100644 test/common/store_test_extensions.dart create mode 100644 test/common_errors_test.dart create mode 100644 test/common_mocks.dart create mode 100644 test/common_mocks.mocks.dart create mode 100644 test/fresh_and_get_test.dart create mode 100644 test/multiple_request_test.dart create mode 100644 test/refresh_test.dart create mode 100644 test/store_response_extension_test.dart create mode 100644 test/store_valid_result_store_test.dart create mode 100644 test/store_without_key_test.dart diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..16b0ba3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +*.mocks.dart linguist-generated=true +*.freezed.dart linguist-generated=true +*.g.dart linguist-generated=true +*.gr.dart linguist-generated=true +pubspec.lock linguist-generated=true +Gemfile.lock linguist-generated=true diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..70c05c2 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @xmartlabs/flutter-open-source diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aaf74ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ + +# Coverage +/coverage diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..016f934 --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: bcea432bce54a83306b3c00a7ad0ed98f777348d + channel: beta + +project_type: package diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/lib/errors.dart b/lib/errors.dart new file mode 100644 index 0000000..5227f57 --- /dev/null +++ b/lib/errors.dart @@ -0,0 +1,8 @@ +class StockError extends Error { + final String message; + + StockError(this.message); + + @override + String toString() => "StockError: $message"; +} diff --git a/lib/fetcher.dart b/lib/fetcher.dart new file mode 100644 index 0000000..4c4642c --- /dev/null +++ b/lib/fetcher.dart @@ -0,0 +1,11 @@ +import 'package:stock/src/factory_fetcher.dart'; + +class Fetcher { + static Fetcher ofFuture( + Future Function(Key key) futureFactory) => + FutureFetcher(futureFactory); + + static Fetcher ofStream( + Stream Function(Key key) streamFactory) => + StreamFetcher(streamFactory); +} diff --git a/lib/source_of_truth.dart b/lib/source_of_truth.dart new file mode 100644 index 0000000..7cb0fe9 --- /dev/null +++ b/lib/source_of_truth.dart @@ -0,0 +1,43 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; + +class SourceOfTruth { + Stream Function(Key key) reader; + Future Function(Key key, Output? output) writer; + + SourceOfTruth({required this.reader, required this.writer}); +} + +class CachedSourceOfTruth implements SourceOfTruth { + final _streamController = StreamController.broadcast(); + + late Map _cachedValues; + + @override + late Stream Function(Key key) reader; + @override + late Future Function(Key key, T? output) writer; + + CachedSourceOfTruth([Map? cachedValues]) { + _cachedValues = {if (cachedValues != null) ...cachedValues}; + reader = generateReader; + writer = generateWriter; + } + + @protected + @visibleForTesting + void setCachedValue(Key key, T? t) => _cachedValues[key] = t; + + @protected + Stream generateReader(Key key) async* { + yield _cachedValues[key]; + yield* _streamController.stream; + } + + @protected + Future generateWriter(Key key, T? value) async { + setCachedValue(key, value); + _streamController.add(value); + } +} diff --git a/lib/src/extensions/future_stream_extensions.dart b/lib/src/extensions/future_stream_extensions.dart new file mode 100644 index 0000000..db1d80c --- /dev/null +++ b/lib/src/extensions/future_stream_extensions.dart @@ -0,0 +1,20 @@ +import 'package:rxdart/rxdart.dart'; +import 'package:stock/store_response.dart'; + +extension StreamExtensions on Stream { + Stream> mapToResponse(ResponseOrigin origin) => + map((data) => StoreResponse.data(origin, data)) + .onErrorReturnWith((error, stacktrace) { + return StoreResponse.error(origin, error, stacktrace); + }); +} + +extension FutureExtensions on Future { + Future> mapToResponse(ResponseOrigin origin) async { + try { + return StoreResponse.data(origin, await this); + } catch (error, stacktrace) { + return StoreResponse.error(origin, error, stacktrace); + } + } +} diff --git a/lib/src/extensions/store_response_extensions.dart b/lib/src/extensions/store_response_extensions.dart new file mode 100644 index 0000000..d90c4ff --- /dev/null +++ b/lib/src/extensions/store_response_extensions.dart @@ -0,0 +1,36 @@ +import 'package:stock/errors.dart'; +import 'package:stock/store_response.dart'; + +extension StoreResponseExtensions on StoreResponse { + StoreResponse swapType() { + return map( + data: (response) => StoreResponse.data(origin, response.value as R), + loading: (_) => StoreResponse.loading(origin), + error: (response) => + StoreResponse.error(origin, response.error, response.stackTrace), + ); + } + + T requireData() => map( + data: (response) => response.value, + loading: (_) => throw StockError('There is no data in loading'), + error: (response) => throw response.error, + ); + + T? get data => map( + data: (response) => response.value, + loading: (_) => null, + error: (response) => null, + ); + + bool get isLoading => this is StoreResponseLoading; + + bool get isError => this is StoreResponseError; +} + +extension StoreResponseStreamExtensions on Stream> { + Stream> whereDataNotNull() => where( + (event) => + event is StoreResponseData ? event.requireData() != null : true, + ).map((event) => event.swapType()); +} diff --git a/lib/src/factory_fetcher.dart b/lib/src/factory_fetcher.dart new file mode 100644 index 0000000..d45003d --- /dev/null +++ b/lib/src/factory_fetcher.dart @@ -0,0 +1,16 @@ +import 'package:stock/fetcher.dart'; + +class FactoryFetcher implements Fetcher { + Stream Function(Key key) factory; + + FactoryFetcher(this.factory); +} + +class FutureFetcher extends FactoryFetcher { + FutureFetcher(Future Function(Key key) factory) + : super((key) => Stream.fromFuture(factory(key))); +} + +class StreamFetcher extends FactoryFetcher { + StreamFetcher(factory) : super(factory); +} diff --git a/lib/src/store_impl.dart b/lib/src/store_impl.dart new file mode 100644 index 0000000..3911957 --- /dev/null +++ b/lib/src/store_impl.dart @@ -0,0 +1,181 @@ +import 'dart:async'; + +import 'package:mutex/mutex.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:stock/fetcher.dart'; +import 'package:stock/source_of_truth.dart'; +import 'package:stock/src/extensions/future_stream_extensions.dart'; +import 'package:stock/src/extensions/store_response_extensions.dart'; +import 'package:stock/src/factory_fetcher.dart'; +import 'package:stock/src/wrapped_source_of_truth.dart'; +import 'package:stock/store.dart'; +import 'package:stock/store_request.dart'; +import 'package:stock/store_response.dart'; + +class StoreImpl implements Store { + final Fetcher _fetcher; + final SourceOfTruth? _sourceOfTruth; + + final Map _writingMap = {}; + final _writingLock = Mutex(); + + StoreImpl({ + required Fetcher fetcher, + required SourceOfTruth? sourceOfTruth, + }) : _fetcher = fetcher, + _sourceOfTruth = sourceOfTruth; + + @override + Future fresh(Key key) => + _generateCombinedNetworkAndSourceOfTruthStream( + StoreRequest(key: key, refresh: true), + WriteWrappedSourceOfTruth(_sourceOfTruth), + _fetcher as FactoryFetcher, + ) + .where((event) => event is! StoreResponseLoading) + .where((event) => event.origin == ResponseOrigin.fetcher) + .first + .then((value) => value.requireData()); + + @override + Future get(Key key) => stream(key, refresh: false) + .where((event) => event is! StoreResponseLoading) + .first + .then((value) => value.requireData()); + + @override + Stream> stream(Key key, {refresh = true}) => + streamFromRequest(StoreRequest( + key: key, + refresh: refresh, + )); + + Stream> streamFromRequest(StoreRequest request) => + _generateCombinedNetworkAndSourceOfTruthStream( + request, + _sourceOfTruth == null ? CachedSourceOfTruth() : _sourceOfTruth!, + _fetcher as FactoryFetcher, + ); + + Stream> _generateCombinedNetworkAndSourceOfTruthStream( + StoreRequest request, + SourceOfTruth sourceOfTruth, + FactoryFetcher fetcher, + ) async* { + final StreamController> controller = + StreamController.broadcast(); + final syncLock = Mutex(); + await syncLock.acquire(); + + final fetcherSubscription = _generateNetworkStream( + dataStreamController: controller, + request: request, + sourceOfTruth: sourceOfTruth, + fetcher: fetcher, + emitMutex: syncLock, + ); + + final sourceOfTruthSubscription = _generateSourceOfTruthStreamSubscription( + request: request, + sourceOfTruth: sourceOfTruth, + dataStreamController: controller, + dbLock: syncLock, + ); + + yield* controller.stream.whereDataNotNull().doOnCancel(() async { + await fetcherSubscription.cancel(); + await sourceOfTruthSubscription.cancel(); + }); + } + + StreamSubscription _generateNetworkStream({ + required StoreRequest request, + required SourceOfTruth? sourceOfTruth, + required FactoryFetcher fetcher, + required Mutex emitMutex, + required StreamController> dataStreamController, + }) => + Stream.fromFuture( + _shouldStartNetworkStream(request, dataStreamController)) + .flatMap((shouldFetchNewValue) => _startNetworkFlow( + shouldFetchNewValue, dataStreamController, fetcher, request)) + .listen((response) => emitMutex.protect(() async { + if (response is StoreResponseData) { + await _writingLock + .protect(() async => _incrementWritingMap(request, 1)); + var writerResult = await sourceOfTruth + ?.writer(request.key, response.value) + .mapToResponse(ResponseOrigin.fetcher); + if (writerResult is StoreResponseError) { + dataStreamController.add(writerResult.swapType()); + await _writingLock + .protect(() async => _incrementWritingMap(request, -1)); + } + } else { + dataStreamController.add(response); + } + })); + + int _incrementWritingMap(StoreRequest request, int increment) => + _writingMap[request.key] = (_writingMap[request.key] ?? 0) + increment; + + Stream> _startNetworkFlow( + bool shouldFetchNewValue, + StreamController> dataStreamController, + FactoryFetcher fetcher, + StoreRequest request) { + if (shouldFetchNewValue) { + dataStreamController + .add(StoreResponseLoading(ResponseOrigin.fetcher)); + return fetcher.factory(request.key).mapToResponse(ResponseOrigin.fetcher); + } else { + return Rx.never>(); + } + } + + Future _shouldStartNetworkStream(StoreRequest request, + StreamController> dataStreamController) async { + if (request.refresh) { + return true; + } + return await dataStreamController.stream + .where((event) => event.origin == ResponseOrigin.sourceOfTruth) + .where((event) => !event.isLoading) + .first + .then((value) => value.data == null); + } + + StreamSubscription _generateSourceOfTruthStreamSubscription({ + required StoreRequest request, + required SourceOfTruth sourceOfTruth, + required Mutex dbLock, + required StreamController> dataStreamController, + }) { + var initialSyncDone = false; + final sourceOfTruthSubscription = sourceOfTruth + .reader(request.key) + .mapToResponse(ResponseOrigin.sourceOfTruth) + .listen((response) async { + if (response is StoreResponseData) { + final fetcherData = await _writingLock.protect(() async { + final writingKeyData = (_writingMap[request.key] ?? -1) > 0; + if (writingKeyData) { + _incrementWritingMap(request, -1); + } + return writingKeyData; + }); + dataStreamController.add(StoreResponseData( + fetcherData ? ResponseOrigin.fetcher : response.origin, + response.value, + )); + } else { + dataStreamController.add(response.swapType()); + } + if (dbLock.isLocked && !initialSyncDone) { + initialSyncDone = true; + dbLock.release(); + } + }); + return sourceOfTruthSubscription; + } +} diff --git a/lib/src/wrapped_source_of_truth.dart b/lib/src/wrapped_source_of_truth.dart new file mode 100644 index 0000000..c43e2eb --- /dev/null +++ b/lib/src/wrapped_source_of_truth.dart @@ -0,0 +1,13 @@ +import 'package:stock/source_of_truth.dart'; + +class WriteWrappedSourceOfTruth extends CachedSourceOfTruth { + final SourceOfTruth? _realSourceOfTruth; + + WriteWrappedSourceOfTruth(this._realSourceOfTruth); + + @override + Future generateWriter(Key key, T? value) async { + await _realSourceOfTruth?.writer(key, value); + await super.generateWriter(key, value); + } +} diff --git a/lib/store.dart b/lib/store.dart new file mode 100644 index 0000000..fa6d1df --- /dev/null +++ b/lib/store.dart @@ -0,0 +1,20 @@ +import 'dart:async'; + +import 'package:stock/fetcher.dart'; +import 'package:stock/source_of_truth.dart'; +import 'package:stock/src/store_impl.dart'; +import 'package:stock/store_response.dart'; + +abstract class Store { + factory Store({ + required Fetcher fetcher, + required SourceOfTruth? sourceOfTruth, + }) => + StoreImpl(fetcher: fetcher, sourceOfTruth: sourceOfTruth); + + Stream> stream(Key key, {refresh = true}); + + Future fresh(Key key); + + Future get(Key key); +} diff --git a/lib/store_extensions.dart b/lib/store_extensions.dart new file mode 100644 index 0000000..496462c --- /dev/null +++ b/lib/store_extensions.dart @@ -0,0 +1,18 @@ +import 'source_of_truth.dart'; + +abstract class StoreConverter { + T1 fromT0(T0 t0); + + T0 fromT1(T1 t1); +} + +extension SourceOfTruthExtensions on SourceOfTruth { + SourceOfTruth mapTo( + StoreConverter converter) => + SourceOfTruth( + reader: (key) => reader(key) + .map((value) => value == null ? null : converter.fromT0(value)), + writer: (key, value) => + writer(key, value == null ? null : converter.fromT1(value)), + ); +} diff --git a/lib/store_request.dart b/lib/store_request.dart new file mode 100644 index 0000000..f31d8a6 --- /dev/null +++ b/lib/store_request.dart @@ -0,0 +1,9 @@ +class StoreRequest { + Key key; + bool refresh; + + StoreRequest({ + required this.key, + required this.refresh, + }); +} diff --git a/lib/store_response.dart b/lib/store_response.dart new file mode 100644 index 0000000..ae52d3e --- /dev/null +++ b/lib/store_response.dart @@ -0,0 +1,28 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'store_response.freezed.dart'; + +@freezed +class StoreResponse with _$StoreResponse { + @With<_ResponseWithOrigin>() + factory StoreResponse.data(ResponseOrigin origin, Output value) = + StoreResponseData; + + @With<_ResponseWithOrigin>() + const factory StoreResponse.loading(ResponseOrigin origin) = + StoreResponseLoading; + + @With<_ResponseWithOrigin>() + const factory StoreResponse.error(ResponseOrigin origin, Object error, + [StackTrace? stackTrace]) = StoreResponseError; +} + +mixin _ResponseWithOrigin { + ResponseOrigin get origin; +} + +enum ResponseOrigin { + cache, + sourceOfTruth, + fetcher, +} diff --git a/lib/store_response.freezed.dart b/lib/store_response.freezed.dart new file mode 100644 index 0000000..526a3da --- /dev/null +++ b/lib/store_response.freezed.dart @@ -0,0 +1,606 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'store_response.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$StoreResponse { + ResponseOrigin get origin => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(ResponseOrigin origin, Output value) data, + required TResult Function(ResponseOrigin origin) loading, + required TResult Function( + ResponseOrigin origin, Object error, StackTrace? stackTrace) + error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function(ResponseOrigin origin, Output value)? data, + TResult Function(ResponseOrigin origin)? loading, + TResult Function( + ResponseOrigin origin, Object error, StackTrace? stackTrace)? + error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(ResponseOrigin origin, Output value)? data, + TResult Function(ResponseOrigin origin)? loading, + TResult Function( + ResponseOrigin origin, Object error, StackTrace? stackTrace)? + error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(StoreResponseData value) data, + required TResult Function(StoreResponseLoading value) loading, + required TResult Function(StoreResponseError value) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(StoreResponseData value)? data, + TResult Function(StoreResponseLoading value)? loading, + TResult Function(StoreResponseError value)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(StoreResponseData value)? data, + TResult Function(StoreResponseLoading value)? loading, + TResult Function(StoreResponseError value)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $StoreResponseCopyWith> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $StoreResponseCopyWith { + factory $StoreResponseCopyWith(StoreResponse value, + $Res Function(StoreResponse) then) = + _$StoreResponseCopyWithImpl; + $Res call({ResponseOrigin origin}); +} + +/// @nodoc +class _$StoreResponseCopyWithImpl + implements $StoreResponseCopyWith { + _$StoreResponseCopyWithImpl(this._value, this._then); + + final StoreResponse _value; + // ignore: unused_field + final $Res Function(StoreResponse) _then; + + @override + $Res call({ + Object? origin = freezed, + }) { + return _then(_value.copyWith( + origin: origin == freezed + ? _value.origin + : origin // ignore: cast_nullable_to_non_nullable + as ResponseOrigin, + )); + } +} + +/// @nodoc +abstract class _$$StoreResponseDataCopyWith + implements $StoreResponseCopyWith { + factory _$$StoreResponseDataCopyWith(_$StoreResponseData value, + $Res Function(_$StoreResponseData) then) = + __$$StoreResponseDataCopyWithImpl; + @override + $Res call({ResponseOrigin origin, Output value}); +} + +/// @nodoc +class __$$StoreResponseDataCopyWithImpl + extends _$StoreResponseCopyWithImpl + implements _$$StoreResponseDataCopyWith { + __$$StoreResponseDataCopyWithImpl(_$StoreResponseData _value, + $Res Function(_$StoreResponseData) _then) + : super(_value, (v) => _then(v as _$StoreResponseData)); + + @override + _$StoreResponseData get _value => + super._value as _$StoreResponseData; + + @override + $Res call({ + Object? origin = freezed, + Object? value = freezed, + }) { + return _then(_$StoreResponseData( + origin == freezed + ? _value.origin + : origin // ignore: cast_nullable_to_non_nullable + as ResponseOrigin, + value == freezed + ? _value.value + : value // ignore: cast_nullable_to_non_nullable + as Output, + )); + } +} + +/// @nodoc + +class _$StoreResponseData + with _ResponseWithOrigin + implements StoreResponseData { + _$StoreResponseData(this.origin, this.value); + + @override + final ResponseOrigin origin; + @override + final Output value; + + @override + String toString() { + return 'StoreResponse<$Output>.data(origin: $origin, value: $value)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$StoreResponseData && + const DeepCollectionEquality().equals(other.origin, origin) && + const DeepCollectionEquality().equals(other.value, value)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(origin), + const DeepCollectionEquality().hash(value)); + + @JsonKey(ignore: true) + @override + _$$StoreResponseDataCopyWith> + get copyWith => __$$StoreResponseDataCopyWithImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(ResponseOrigin origin, Output value) data, + required TResult Function(ResponseOrigin origin) loading, + required TResult Function( + ResponseOrigin origin, Object error, StackTrace? stackTrace) + error, + }) { + return data(origin, value); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function(ResponseOrigin origin, Output value)? data, + TResult Function(ResponseOrigin origin)? loading, + TResult Function( + ResponseOrigin origin, Object error, StackTrace? stackTrace)? + error, + }) { + return data?.call(origin, value); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(ResponseOrigin origin, Output value)? data, + TResult Function(ResponseOrigin origin)? loading, + TResult Function( + ResponseOrigin origin, Object error, StackTrace? stackTrace)? + error, + required TResult orElse(), + }) { + if (data != null) { + return data(origin, value); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(StoreResponseData value) data, + required TResult Function(StoreResponseLoading value) loading, + required TResult Function(StoreResponseError value) error, + }) { + return data(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(StoreResponseData value)? data, + TResult Function(StoreResponseLoading value)? loading, + TResult Function(StoreResponseError value)? error, + }) { + return data?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(StoreResponseData value)? data, + TResult Function(StoreResponseLoading value)? loading, + TResult Function(StoreResponseError value)? error, + required TResult orElse(), + }) { + if (data != null) { + return data(this); + } + return orElse(); + } +} + +abstract class StoreResponseData + implements StoreResponse, _ResponseWithOrigin { + factory StoreResponseData(final ResponseOrigin origin, final Output value) = + _$StoreResponseData; + + @override + ResponseOrigin get origin; + Output get value; + @override + @JsonKey(ignore: true) + _$$StoreResponseDataCopyWith> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$StoreResponseLoadingCopyWith + implements $StoreResponseCopyWith { + factory _$$StoreResponseLoadingCopyWith(_$StoreResponseLoading value, + $Res Function(_$StoreResponseLoading) then) = + __$$StoreResponseLoadingCopyWithImpl; + @override + $Res call({ResponseOrigin origin}); +} + +/// @nodoc +class __$$StoreResponseLoadingCopyWithImpl + extends _$StoreResponseCopyWithImpl + implements _$$StoreResponseLoadingCopyWith { + __$$StoreResponseLoadingCopyWithImpl(_$StoreResponseLoading _value, + $Res Function(_$StoreResponseLoading) _then) + : super(_value, (v) => _then(v as _$StoreResponseLoading)); + + @override + _$StoreResponseLoading get _value => + super._value as _$StoreResponseLoading; + + @override + $Res call({ + Object? origin = freezed, + }) { + return _then(_$StoreResponseLoading( + origin == freezed + ? _value.origin + : origin // ignore: cast_nullable_to_non_nullable + as ResponseOrigin, + )); + } +} + +/// @nodoc + +class _$StoreResponseLoading + with _ResponseWithOrigin + implements StoreResponseLoading { + const _$StoreResponseLoading(this.origin); + + @override + final ResponseOrigin origin; + + @override + String toString() { + return 'StoreResponse<$Output>.loading(origin: $origin)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$StoreResponseLoading && + const DeepCollectionEquality().equals(other.origin, origin)); + } + + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(origin)); + + @JsonKey(ignore: true) + @override + _$$StoreResponseLoadingCopyWith> + get copyWith => __$$StoreResponseLoadingCopyWithImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(ResponseOrigin origin, Output value) data, + required TResult Function(ResponseOrigin origin) loading, + required TResult Function( + ResponseOrigin origin, Object error, StackTrace? stackTrace) + error, + }) { + return loading(origin); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function(ResponseOrigin origin, Output value)? data, + TResult Function(ResponseOrigin origin)? loading, + TResult Function( + ResponseOrigin origin, Object error, StackTrace? stackTrace)? + error, + }) { + return loading?.call(origin); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(ResponseOrigin origin, Output value)? data, + TResult Function(ResponseOrigin origin)? loading, + TResult Function( + ResponseOrigin origin, Object error, StackTrace? stackTrace)? + error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(origin); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(StoreResponseData value) data, + required TResult Function(StoreResponseLoading value) loading, + required TResult Function(StoreResponseError value) error, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(StoreResponseData value)? data, + TResult Function(StoreResponseLoading value)? loading, + TResult Function(StoreResponseError value)? error, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(StoreResponseData value)? data, + TResult Function(StoreResponseLoading value)? loading, + TResult Function(StoreResponseError value)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class StoreResponseLoading + implements StoreResponse, _ResponseWithOrigin { + const factory StoreResponseLoading(final ResponseOrigin origin) = + _$StoreResponseLoading; + + @override + ResponseOrigin get origin; + @override + @JsonKey(ignore: true) + _$$StoreResponseLoadingCopyWith> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$StoreResponseErrorCopyWith + implements $StoreResponseCopyWith { + factory _$$StoreResponseErrorCopyWith(_$StoreResponseError value, + $Res Function(_$StoreResponseError) then) = + __$$StoreResponseErrorCopyWithImpl; + @override + $Res call({ResponseOrigin origin, Object error, StackTrace? stackTrace}); +} + +/// @nodoc +class __$$StoreResponseErrorCopyWithImpl + extends _$StoreResponseCopyWithImpl + implements _$$StoreResponseErrorCopyWith { + __$$StoreResponseErrorCopyWithImpl(_$StoreResponseError _value, + $Res Function(_$StoreResponseError) _then) + : super(_value, (v) => _then(v as _$StoreResponseError)); + + @override + _$StoreResponseError get _value => + super._value as _$StoreResponseError; + + @override + $Res call({ + Object? origin = freezed, + Object? error = freezed, + Object? stackTrace = freezed, + }) { + return _then(_$StoreResponseError( + origin == freezed + ? _value.origin + : origin // ignore: cast_nullable_to_non_nullable + as ResponseOrigin, + error == freezed + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as Object, + stackTrace == freezed + ? _value.stackTrace + : stackTrace // ignore: cast_nullable_to_non_nullable + as StackTrace?, + )); + } +} + +/// @nodoc + +class _$StoreResponseError + with _ResponseWithOrigin + implements StoreResponseError { + const _$StoreResponseError(this.origin, this.error, [this.stackTrace]); + + @override + final ResponseOrigin origin; + @override + final Object error; + @override + final StackTrace? stackTrace; + + @override + String toString() { + return 'StoreResponse<$Output>.error(origin: $origin, error: $error, stackTrace: $stackTrace)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$StoreResponseError && + const DeepCollectionEquality().equals(other.origin, origin) && + const DeepCollectionEquality().equals(other.error, error) && + const DeepCollectionEquality() + .equals(other.stackTrace, stackTrace)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(origin), + const DeepCollectionEquality().hash(error), + const DeepCollectionEquality().hash(stackTrace)); + + @JsonKey(ignore: true) + @override + _$$StoreResponseErrorCopyWith> + get copyWith => __$$StoreResponseErrorCopyWithImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(ResponseOrigin origin, Output value) data, + required TResult Function(ResponseOrigin origin) loading, + required TResult Function( + ResponseOrigin origin, Object error, StackTrace? stackTrace) + error, + }) { + return error(origin, this.error, stackTrace); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function(ResponseOrigin origin, Output value)? data, + TResult Function(ResponseOrigin origin)? loading, + TResult Function( + ResponseOrigin origin, Object error, StackTrace? stackTrace)? + error, + }) { + return error?.call(origin, this.error, stackTrace); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(ResponseOrigin origin, Output value)? data, + TResult Function(ResponseOrigin origin)? loading, + TResult Function( + ResponseOrigin origin, Object error, StackTrace? stackTrace)? + error, + required TResult orElse(), + }) { + if (error != null) { + return error(origin, this.error, stackTrace); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(StoreResponseData value) data, + required TResult Function(StoreResponseLoading value) loading, + required TResult Function(StoreResponseError value) error, + }) { + return error(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(StoreResponseData value)? data, + TResult Function(StoreResponseLoading value)? loading, + TResult Function(StoreResponseError value)? error, + }) { + return error?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(StoreResponseData value)? data, + TResult Function(StoreResponseLoading value)? loading, + TResult Function(StoreResponseError value)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this); + } + return orElse(); + } +} + +abstract class StoreResponseError + implements StoreResponse, _ResponseWithOrigin { + const factory StoreResponseError( + final ResponseOrigin origin, final Object error, + [final StackTrace? stackTrace]) = _$StoreResponseError; + + @override + ResponseOrigin get origin; + Object get error; + StackTrace? get stackTrace; + @override + @JsonKey(ignore: true) + _$$StoreResponseErrorCopyWith> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..d0b9309 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,482 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "46.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "4.6.0" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.3" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.4.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0+1" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.6.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + mockito: + dependency: "direct dev" + description: + name: mockito + url: "https://pub.dartlang.org" + source: hosted + version: "5.3.0" + mutex: + dependency: "direct main" + description: + name: mutex + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + rxdart: + dependency: "direct main" + description: + name: rxdart + url: "https://pub.dartlang.org" + source: hosted + version: "0.27.5" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.2" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.9" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" +sdks: + dart: ">=2.17.0 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..6f2bf4c --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,24 @@ +name: stock +description: Dart Library for Async Data Loading and Caching +version: 0.0.1 +homepage: https://github.com/xmartlabs/dstore + +environment: + sdk: '>=2.17.0 <3.0.0' + +dependencies: + flutter: + sdk: flutter + freezed_annotation: 2.1.0 + mutex: 3.0.0 + rxdart: 0.27.5 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: 2.2.0 + flutter_lints: 2.0.1 + freezed: 2.1.0+1 + mockito: 5.3.0 + +flutter: diff --git a/scripts/checks.sh b/scripts/checks.sh new file mode 100755 index 0000000..cc2b800 --- /dev/null +++ b/scripts/checks.sh @@ -0,0 +1,14 @@ +#!/bin/bash +RED='\033[0;31m' + +echo ':: Get dependencies ::' +flutter pub get + +echo ':: Check code format ::' +flutter format --set-exit-if-changed . || { echo -e "${RED}Invalid format" ; exit 1; } + +echo ':: Run linter ::' +flutter analyze . || { echo -e "${RED}Linter error" ; exit 1; } + +echo ':: Run tests ::' +flutter test || { echo -e "${RED}Test error" ; exit 1; } diff --git a/scripts/clean_up.sh b/scripts/clean_up.sh new file mode 100755 index 0000000..5d54390 --- /dev/null +++ b/scripts/clean_up.sh @@ -0,0 +1,9 @@ +#!/bin/bash +echo ':: flutter clean ::' +flutter clean + +echo ':: flutter pub get ::' +flutter pub get + +echo ':: flutter pub run build_runner build --delete-conflicting-outputs ::' +flutter pub run build_runner build --delete-conflicting-outputs diff --git a/test/common/response_extensions.dart b/test/common/response_extensions.dart new file mode 100644 index 0000000..4f16f2a --- /dev/null +++ b/test/common/response_extensions.dart @@ -0,0 +1,7 @@ +import 'package:stock/store_response.dart'; + +extension ResponseExtensions on StoreResponse { + StoreResponse removeStacktraceIfNeeded() => this is StoreResponseError + ? (this as StoreResponseError).copyWith(stackTrace: null) + : this; +} diff --git a/test/common/source_of_truth/cached_and_mocked_source_of_truth.dart b/test/common/source_of_truth/cached_and_mocked_source_of_truth.dart new file mode 100644 index 0000000..37713c6 --- /dev/null +++ b/test/common/source_of_truth/cached_and_mocked_source_of_truth.dart @@ -0,0 +1,34 @@ +import 'package:mockito/mockito.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:stock/source_of_truth.dart'; + +import '../../common_mocks.mocks.dart'; +import 'cached_source_of_truth_with_default_value.dart'; + +SourceOfTruth createMockedSourceOfTruth( + [Value? defaultValue]) { + final cachedSourceOfTruth = + CachedSourceOfTruthWithDefaultValue(defaultValue); + final sourceOfTruth = MockSourceOfTruth(); + when(sourceOfTruth.reader).thenReturn(cachedSourceOfTruth.reader); + when(sourceOfTruth.writer).thenReturn(cachedSourceOfTruth.writer); + return sourceOfTruth; +} + +SourceOfTruth createMockedSourceOfTruthWithDefaultNegativeIntKey() { + final sot = _NegativeKeyIntSource(); + final sourceOfTruth = MockSourceOfTruth(); + when(sourceOfTruth.reader).thenReturn(sot.reader); + when(sourceOfTruth.writer).thenReturn(sot.writer); + return sourceOfTruth; +} + +class _NegativeKeyIntSource extends CachedSourceOfTruth { + _NegativeKeyIntSource(); + + @override + Stream generateReader(int key) => + super.generateReader(key).flatMap((response) async* { + yield response ?? -key; + }); +} diff --git a/test/common/source_of_truth/cached_source_of_truth_with_default_value.dart b/test/common/source_of_truth/cached_source_of_truth_with_default_value.dart new file mode 100644 index 0000000..6d3bdc1 --- /dev/null +++ b/test/common/source_of_truth/cached_source_of_truth_with_default_value.dart @@ -0,0 +1,15 @@ +import 'package:rxdart/rxdart.dart'; +import 'package:stock/source_of_truth.dart'; + +class CachedSourceOfTruthWithDefaultValue + extends CachedSourceOfTruth { + final T? _defaultValue; + + CachedSourceOfTruthWithDefaultValue([this._defaultValue]); + + @override + Stream generateReader(Key key) => + super.generateReader(key).flatMap((response) async* { + yield response ?? _defaultValue; + }); +} diff --git a/test/common/source_of_truth/source_of_truth_with_delay.dart b/test/common/source_of_truth/source_of_truth_with_delay.dart new file mode 100644 index 0000000..a81e58e --- /dev/null +++ b/test/common/source_of_truth/source_of_truth_with_delay.dart @@ -0,0 +1,28 @@ +import 'package:rxdart/transformers.dart'; + +import 'cached_source_of_truth_with_default_value.dart'; + +class DelayedSourceOfTruth + extends CachedSourceOfTruthWithDefaultValue { + final Duration readDelayTime; + final Duration writeDelayTime; + + DelayedSourceOfTruth([ + T? cachedValue, + this.readDelayTime = const Duration(milliseconds: 100), + this.writeDelayTime = const Duration(milliseconds: 100), + ]) : super(cachedValue); + + @override + Stream generateReader(Key key) => + super.generateReader(key).flatMap((response) async* { + await Future.delayed(readDelayTime); + yield response; + }); + + @override + Future generateWriter(Key key, T? value) async { + await Future.delayed(readDelayTime); + await super.generateWriter(key, value); + } +} diff --git a/test/common/source_of_truth/source_of_truth_with_error.dart b/test/common/source_of_truth/source_of_truth_with_error.dart new file mode 100644 index 0000000..8cbf689 --- /dev/null +++ b/test/common/source_of_truth/source_of_truth_with_error.dart @@ -0,0 +1,35 @@ +import 'package:rxdart/transformers.dart'; + +import 'cached_source_of_truth_with_default_value.dart'; + +class SourceOfTruthWithError + extends CachedSourceOfTruthWithDefaultValue { + static final readException = Exception('Read Test Exception'); + static final writeException = Exception('Write Test Exception'); + + int throwReadErrorCount; + int throwWriteErrorCount; + + SourceOfTruthWithError(T? cachedValue, + {this.throwReadErrorCount = 0, this.throwWriteErrorCount = 0}) + : super(cachedValue); + + @override + Stream generateReader(Key key) => + super.generateReader(key).flatMap((response) async* { + if (throwReadErrorCount > 0) { + throwReadErrorCount--; + throw readException; + } + yield response; + }); + + @override + Future generateWriter(Key key, T? value) async { + if (throwWriteErrorCount > 0) { + throwWriteErrorCount--; + throw writeException; + } + await super.generateWriter(key, value); + } +} diff --git a/test/common/store_test_extensions.dart b/test/common/store_test_extensions.dart new file mode 100644 index 0000000..e56f140 --- /dev/null +++ b/test/common/store_test_extensions.dart @@ -0,0 +1,27 @@ +import 'package:stock/store.dart'; +import 'package:stock/store_response.dart'; + +import 'response_extensions.dart'; + +extension StoreExtensions on Store { + Future>> getFreshResult( + Key key, { + refresh = true, + Duration delay = const Duration(milliseconds: 300), + }) async { + List> resultList = []; + final subscription = stream(key, refresh: refresh) + .listen((element) => resultList.add(element)); + await Future.delayed(delay); + await subscription.cancel(); + return resultList; + } + + Future>> getFreshResultRemovingErrorStackTraces( + Key key, { + refresh = true, + Duration delay = const Duration(milliseconds: 300), + }) => + getFreshResult(key, refresh: refresh, delay: delay).then((value) => + value.map((items) => items.removeStacktraceIfNeeded()).toList()); +} diff --git a/test/common_errors_test.dart b/test/common_errors_test.dart new file mode 100644 index 0000000..cc72f92 --- /dev/null +++ b/test/common_errors_test.dart @@ -0,0 +1,70 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:stock/fetcher.dart'; +import 'package:stock/store.dart'; +import 'package:stock/store_response.dart'; + +import 'common/source_of_truth/source_of_truth_with_error.dart'; +import 'common/store_test_extensions.dart'; + +void main() { + group("Store requests with errors", () { + test('Source of truth with read error and fetcher ok', () async { + final sourceOfTruth = + SourceOfTruthWithError(-1, throwReadErrorCount: 1); + final fetcher = Fetcher.ofFuture((int key) async => 1); + final store = Store( + fetcher: fetcher, + sourceOfTruth: sourceOfTruth, + ); + + final resultList = await store.getFreshResultRemovingErrorStackTraces(1); + expect( + resultList, + equals([ + const StoreResponseLoading(ResponseOrigin.fetcher), + StoreResponseError(ResponseOrigin.sourceOfTruth, + SourceOfTruthWithError.readException), + StoreResponse.data(ResponseOrigin.fetcher, 1), + ])); + }); + + test('Source of truth with write error and fetcher ok', () async { + final sourceOfTruth = + SourceOfTruthWithError(-1, throwWriteErrorCount: 1); + final fetcher = Fetcher.ofFuture((int key) async => 1); + final store = Store( + fetcher: fetcher, + sourceOfTruth: sourceOfTruth, + ); + + final resultList = await store.getFreshResultRemovingErrorStackTraces(1); + expect( + resultList, + equals([ + const StoreResponseLoading(ResponseOrigin.fetcher), + StoreResponse.data(ResponseOrigin.sourceOfTruth, -1), + StoreResponseError( + ResponseOrigin.fetcher, SourceOfTruthWithError.writeException), + ])); + }); + + test('Source of truth ok and fetcher with error', () async { + final sourceOfTruth = SourceOfTruthWithError(-1); + final fetcherError = Exception('Fetcher error'); + final fetcher = Fetcher.ofFuture((int key) async => throw fetcherError); + final store = Store( + fetcher: fetcher, + sourceOfTruth: sourceOfTruth, + ); + + final resultList = await store.getFreshResultRemovingErrorStackTraces(1); + expect( + resultList, + equals([ + const StoreResponseLoading(ResponseOrigin.fetcher), + StoreResponse.data(ResponseOrigin.sourceOfTruth, -1), + StoreResponseError(ResponseOrigin.fetcher, fetcherError), + ])); + }); + }); +} diff --git a/test/common_mocks.dart b/test/common_mocks.dart new file mode 100644 index 0000000..25d77c0 --- /dev/null +++ b/test/common_mocks.dart @@ -0,0 +1,10 @@ +import 'package:mockito/annotations.dart'; +import 'package:stock/source_of_truth.dart'; +import 'package:stock/src/factory_fetcher.dart'; + +@GenerateMocks([ + FutureFetcher, + StreamFetcher, + SourceOfTruth, +]) +void main() {} diff --git a/test/common_mocks.mocks.dart b/test/common_mocks.mocks.dart new file mode 100644 index 0000000..7c3e471 --- /dev/null +++ b/test/common_mocks.mocks.dart @@ -0,0 +1,90 @@ +// Mocks generated by Mockito 5.3.0 from annotations +// in stock/test/common_mocks.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:stock/source_of_truth.dart' as _i4; +import 'package:stock/src/factory_fetcher.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [FutureFetcher]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFutureFetcher extends _i1.Mock + implements _i2.FutureFetcher { + MockFutureFetcher() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Stream Function(Key) get factory => + (super.noSuchMethod(Invocation.getter(#factory), + returnValue: (Key key) => _i3.Stream.empty()) + as _i3.Stream Function(Key)); + @override + set factory(_i3.Stream Function(Key)? _factory) => + super.noSuchMethod(Invocation.setter(#factory, _factory), + returnValueForMissingStub: null); +} + +/// A class which mocks [StreamFetcher]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockStreamFetcher extends _i1.Mock + implements _i2.StreamFetcher { + MockStreamFetcher() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Stream Function(Key) get factory => + (super.noSuchMethod(Invocation.getter(#factory), + returnValue: (Key key) => _i3.Stream.empty()) + as _i3.Stream Function(Key)); + @override + set factory(_i3.Stream Function(Key)? _factory) => + super.noSuchMethod(Invocation.setter(#factory, _factory), + returnValueForMissingStub: null); +} + +/// A class which mocks [SourceOfTruth]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSourceOfTruth extends _i1.Mock + implements _i4.SourceOfTruth { + MockSourceOfTruth() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Stream Function(Key) get reader => + (super.noSuchMethod(Invocation.getter(#reader), + returnValue: (Key key) => _i3.Stream.empty()) + as _i3.Stream Function(Key)); + @override + set reader(_i3.Stream Function(Key)? _reader) => + super.noSuchMethod(Invocation.setter(#reader, _reader), + returnValueForMissingStub: null); + @override + _i3.Future Function(Key, Output?) get writer => (super.noSuchMethod( + Invocation.getter(#writer), + returnValue: (Key key, Output? output) => _i3.Future.value()) + as _i3.Future Function(Key, Output?)); + @override + set writer(_i3.Future Function(Key, Output?)? _writer) => + super.noSuchMethod(Invocation.setter(#writer, _writer), + returnValueForMissingStub: null); +} diff --git a/test/fresh_and_get_test.dart b/test/fresh_and_get_test.dart new file mode 100644 index 0000000..3a993f4 --- /dev/null +++ b/test/fresh_and_get_test.dart @@ -0,0 +1,64 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stock/store.dart'; + +import 'common/source_of_truth/cached_and_mocked_source_of_truth.dart'; +import 'common_mocks.mocks.dart'; + +void main() { + group("Fresh tests", () { + test('Sot is not called when fresh is invoked', () async { + var fetcher = MockFutureFetcher(); + final sourceOfTruth = MockSourceOfTruth(); + when(fetcher.factory).thenReturn((key) => Stream.value(1)); + when(sourceOfTruth.reader).thenReturn((key) => Stream.value(-1)); + when(sourceOfTruth.writer).thenReturn((key, value) async {}); + final store = Store( + fetcher: fetcher, + sourceOfTruth: sourceOfTruth, + ); + + final result = await store.fresh(1); + expect(result, equals(1)); + verifyNever(sourceOfTruth.reader); + verify(sourceOfTruth.writer).called(1); + }); + }); + group("Get tests", () { + test('Fetcher is not called when get is invoked and sot has data', + () async { + var fetcher = MockFutureFetcher(); + final sourceOfTruth = MockSourceOfTruth(); + when(fetcher.factory).thenReturn((key) => Stream.value(1)); + when(sourceOfTruth.reader).thenReturn((key) => Stream.value(-1)); + when(sourceOfTruth.writer).thenReturn((key, value) async {}); + final store = Store( + fetcher: fetcher, + sourceOfTruth: sourceOfTruth, + ); + + final result = await store.get(1); + expect(result, equals(-1)); + verifyNever(fetcher.factory); + verify(sourceOfTruth.reader).called(1); + verifyNever(sourceOfTruth.writer); + }); + + test('Fetcher is called when get is invoked and sot has not data', + () async { + var fetcher = MockFutureFetcher(); + when(fetcher.factory).thenReturn((key) => Stream.value(1)); + final sourceOfTruth = createMockedSourceOfTruth(); + final store = Store( + fetcher: fetcher, + sourceOfTruth: sourceOfTruth, + ); + + final result = await store.get(1); + expect(result, equals(1)); + verify(fetcher.factory).called(1); + verify(sourceOfTruth.reader).called(1); + verify(sourceOfTruth.writer).called(1); + }); + }); +} diff --git a/test/multiple_request_test.dart b/test/multiple_request_test.dart new file mode 100644 index 0000000..feb95a1 --- /dev/null +++ b/test/multiple_request_test.dart @@ -0,0 +1,39 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:stock/fetcher.dart'; +import 'package:stock/store.dart'; +import 'package:stock/store_response.dart'; + +import 'common/source_of_truth/cached_and_mocked_source_of_truth.dart'; +import 'common/store_test_extensions.dart'; + +void main() { + group('Multiple requests', () { + test('Two simple requests with cached data', () async { + final sourceOfTruth = + createMockedSourceOfTruthWithDefaultNegativeIntKey(); + final fetcher = Fetcher.ofFuture((int key) async => key); + final store = Store( + fetcher: fetcher, + sourceOfTruth: sourceOfTruth, + ); + + var resultList = await store.getFreshResultRemovingErrorStackTraces(1); + expect( + resultList, + equals([ + const StoreResponseLoading(ResponseOrigin.fetcher), + StoreResponse.data(ResponseOrigin.sourceOfTruth, -1), + StoreResponse.data(ResponseOrigin.fetcher, 1), + ])); + + resultList = await store.getFreshResultRemovingErrorStackTraces(2); + expect( + resultList, + equals([ + const StoreResponseLoading(ResponseOrigin.fetcher), + StoreResponse.data(ResponseOrigin.sourceOfTruth, -2), + StoreResponse.data(ResponseOrigin.fetcher, 2), + ])); + }); + }); +} diff --git a/test/refresh_test.dart b/test/refresh_test.dart new file mode 100644 index 0000000..58836a0 --- /dev/null +++ b/test/refresh_test.dart @@ -0,0 +1,121 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stock/source_of_truth.dart'; +import 'package:stock/store.dart'; +import 'package:stock/store_response.dart'; + +import 'common/source_of_truth/cached_source_of_truth_with_default_value.dart'; +import 'common/source_of_truth/source_of_truth_with_error.dart'; +import 'common/store_test_extensions.dart'; +import 'common_mocks.mocks.dart'; + +void main() { + group('Refresh tests', () { + test('Fetcher is not called if sot has data and refresh is false', + () async { + var fetcher = MockFutureFetcher(); + when(fetcher.factory).thenReturn((key) => Stream.value(1)); + final sourceOfTruth = CachedSourceOfTruthWithDefaultValue(-1); + final store = Store( + fetcher: fetcher, + sourceOfTruth: sourceOfTruth, + ); + + final resultList = await store.getFreshResult(1, refresh: false); + expect( + resultList, + equals([ + StoreResponse.data(ResponseOrigin.sourceOfTruth, -1), + ])); + verifyNever(fetcher.factory); + }); + + test('Fetcher is called if sot has data and refresh is true', () async { + var fetcher = MockFutureFetcher(); + when(fetcher.factory).thenReturn((key) => Stream.value(1)); + final sourceOfTruth = CachedSourceOfTruthWithDefaultValue(-1); + final store = Store( + fetcher: fetcher, + sourceOfTruth: sourceOfTruth, + ); + + final resultList = await store.getFreshResult(1, refresh: true); + expect( + resultList, + equals([ + const StoreResponseLoading(ResponseOrigin.fetcher), + StoreResponse.data(ResponseOrigin.sourceOfTruth, -1), + StoreResponse.data(ResponseOrigin.fetcher, 1), + ])); + verify(fetcher.factory).called(1); + }); + + test('Fetcher is called if sot has not data and refresh is false', + () async { + var fetcher = MockFutureFetcher(); + when(fetcher.factory).thenReturn((key) => Stream.value(1)); + final sourceOfTruth = CachedSourceOfTruth(null); + final store = Store( + fetcher: fetcher, + sourceOfTruth: sourceOfTruth, + ); + + final resultList = await store.getFreshResult(1, refresh: false); + expect( + resultList, + equals([ + const StoreResponseLoading(ResponseOrigin.fetcher), + StoreResponse.data(ResponseOrigin.fetcher, 1), + ])); + verify(fetcher.factory).called(1); + }); + + test('Fetcher is called if sot returns an error and refresh is false', + () async { + var fetcher = MockFutureFetcher(); + when(fetcher.factory).thenReturn((key) => Stream.value(1)); + final sourceOfTruth = + SourceOfTruthWithError(null, throwReadErrorCount: 1); + final store = Store( + fetcher: fetcher, + sourceOfTruth: sourceOfTruth, + ); + + final resultList = + await store.getFreshResultRemovingErrorStackTraces(1, refresh: false); + expect( + resultList, + equals([ + StoreResponseError(ResponseOrigin.sourceOfTruth, + SourceOfTruthWithError.readException), + const StoreResponseLoading(ResponseOrigin.fetcher), + StoreResponse.data(ResponseOrigin.fetcher, 1), + ])); + verify(fetcher.factory).called(1); + }); + + test('Fetcher is called if sot returns an error and refresh is true', + () async { + var fetcher = MockFutureFetcher(); + when(fetcher.factory).thenReturn((key) => Stream.value(1)); + final sourceOfTruth = + SourceOfTruthWithError(null, throwReadErrorCount: 1); + final store = Store( + fetcher: fetcher, + sourceOfTruth: sourceOfTruth, + ); + + final resultList = + await store.getFreshResultRemovingErrorStackTraces(1, refresh: true); + expect( + resultList, + equals([ + const StoreResponseLoading(ResponseOrigin.fetcher), + StoreResponseError(ResponseOrigin.sourceOfTruth, + SourceOfTruthWithError.readException), + StoreResponse.data(ResponseOrigin.fetcher, 1), + ])); + verify(fetcher.factory).called(1); + }); + }); +} diff --git a/test/store_response_extension_test.dart b/test/store_response_extension_test.dart new file mode 100644 index 0000000..5faf506 --- /dev/null +++ b/test/store_response_extension_test.dart @@ -0,0 +1,79 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:stock/errors.dart'; +import 'package:stock/src/extensions/store_response_extensions.dart'; +import 'package:stock/store_response.dart'; + +void main() { + group('Require data extensions', () { + test('requireData of an error throws the exception', () async { + final customEx = Exception("Custom ex"); + expect( + StoreResponse.error(ResponseOrigin.fetcher, customEx).requireData, + throwsA((e) => e == customEx), + ); + }); + test('requireData of a loading response throws a exception', () async { + expect( + const StoreResponse.loading(ResponseOrigin.fetcher).requireData, + throwsA((e) => e is StockError), + ); + }); + test('requireData of a data returns the data', () async { + expect( + StoreResponse.data(ResponseOrigin.fetcher, 1).requireData(), + equals(1), + ); + }); + }); + group('Get data extensions', () { + test('getData of an error returns null', () async { + final customEx = Exception("Custom ex"); + expect( + StoreResponse.error(ResponseOrigin.fetcher, customEx).data, + equals(null), + ); + }); + test('getData of a loading response returns null', () async { + expect( + const StoreResponse.loading(ResponseOrigin.fetcher).data, + equals(null), + ); + }); + test('getData of a data response returns the data', () async { + expect( + StoreResponse.data(ResponseOrigin.fetcher, 1).data, + equals(1), + ); + }); + }); + group('Property extensions', () { + test('Loading returns true if loading', () async { + expect( + StoreResponse.error(ResponseOrigin.fetcher, Error()).isLoading, + equals(false), + ); + expect( + StoreResponse.data(ResponseOrigin.fetcher, 1).isLoading, + equals(false), + ); + expect( + const StoreResponse.loading(ResponseOrigin.fetcher).isLoading, + equals(true), + ); + }); + test('Error returns true if the response is an error', () async { + expect( + StoreResponse.error(ResponseOrigin.fetcher, Error()).isError, + equals(true), + ); + expect( + StoreResponse.data(ResponseOrigin.fetcher, 1).isError, + equals(false), + ); + expect( + const StoreResponse.loading(ResponseOrigin.fetcher).isError, + equals(false), + ); + }); + }); +} diff --git a/test/store_valid_result_store_test.dart b/test/store_valid_result_store_test.dart new file mode 100644 index 0000000..4526e48 --- /dev/null +++ b/test/store_valid_result_store_test.dart @@ -0,0 +1,91 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:stock/fetcher.dart'; +import 'package:stock/store.dart'; +import 'package:stock/store_response.dart'; + +import 'common/source_of_truth/cached_source_of_truth_with_default_value.dart'; +import 'common/source_of_truth/source_of_truth_with_delay.dart'; +import 'common/store_test_extensions.dart'; + +void main() { + group("Valid results", () { + test('Source of truth and fetcher are called', () async { + final sourceOfTruth = CachedSourceOfTruthWithDefaultValue(-1); + final fetcher = Fetcher.ofFuture((int key) async => 1); + final store = Store( + fetcher: fetcher, + sourceOfTruth: sourceOfTruth, + ); + + final resultList = await store.getFreshResult(1); + expect( + resultList, + equals([ + const StoreResponseLoading(ResponseOrigin.fetcher), + StoreResponse.data(ResponseOrigin.sourceOfTruth, -1), + StoreResponse.data(ResponseOrigin.fetcher, 1), + ])); + }); + + test('Source of truth data is returned before fetcher data', () async { + final sourceOfTruth = DelayedSourceOfTruth(-1); + final fetcher = Fetcher.ofFuture((int key) async => 1); + final store = Store( + fetcher: fetcher, + sourceOfTruth: sourceOfTruth, + ); + + final resultList = await store.getFreshResult( + 1, + delay: const Duration(milliseconds: 500), + ); + expect( + resultList, + equals([ + const StoreResponseLoading(ResponseOrigin.fetcher), + StoreResponse.data(ResponseOrigin.sourceOfTruth, -1), + StoreResponse.data(ResponseOrigin.fetcher, 1), + ])); + }); + + test('Source of truth and stream fetcher are called', () async { + final sourceOfTruth = CachedSourceOfTruthWithDefaultValue(-1); + final fetcher = + Fetcher.ofStream((int key) => Stream.fromIterable([1, 2, 3])); + final store = Store( + fetcher: fetcher, + sourceOfTruth: sourceOfTruth, + ); + + final resultList = await store.getFreshResult(1); + expect( + resultList, + equals([ + const StoreResponseLoading(ResponseOrigin.fetcher), + StoreResponse.data(ResponseOrigin.sourceOfTruth, -1), + StoreResponse.data(ResponseOrigin.fetcher, 1), + StoreResponse.data(ResponseOrigin.fetcher, 2), + StoreResponse.data(ResponseOrigin.fetcher, 3), + ])); + }); + + test('Test a store with only a stream fetcher', () async { + final fetcher = + Fetcher.ofStream((int key) => Stream.fromIterable([1, 2, 3])); + final store = Store( + fetcher: fetcher, + sourceOfTruth: null, + ); + + final resultList = await store.getFreshResult(1); + expect( + resultList, + equals([ + const StoreResponseLoading(ResponseOrigin.fetcher), + StoreResponse.data(ResponseOrigin.fetcher, 1), + StoreResponse.data(ResponseOrigin.fetcher, 2), + StoreResponse.data(ResponseOrigin.fetcher, 3), + ])); + }); + }); +} diff --git a/test/store_without_key_test.dart b/test/store_without_key_test.dart new file mode 100644 index 0000000..8b9a776 --- /dev/null +++ b/test/store_without_key_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stock/store.dart'; +import 'package:stock/store_response.dart'; + +import 'common/source_of_truth/cached_source_of_truth_with_default_value.dart'; +import 'common/store_test_extensions.dart'; +import 'common_mocks.mocks.dart'; + +void main() { + group('Store without specific key', () { + test('Simple request using dynamic', () async { + var fetcher = MockFutureFetcher(); + when(fetcher.factory).thenReturn((_) => Stream.value(1)); + final sourceOfTruth = CachedSourceOfTruthWithDefaultValue(-1); + final store = Store( + fetcher: fetcher, + sourceOfTruth: sourceOfTruth, + ); + + final resultList = await store.getFreshResult(null); + expect( + resultList, + equals([ + const StoreResponseLoading(ResponseOrigin.fetcher), + StoreResponse.data(ResponseOrigin.sourceOfTruth, -1), + StoreResponse.data(ResponseOrigin.fetcher, 1), + ])); + }); + }); +}