diff --git a/lib/src/errors.dart b/lib/src/errors.dart index 8940590..34f6787 100644 --- a/lib/src/errors.dart +++ b/lib/src/errors.dart @@ -1,5 +1,5 @@ /// Used to notify unexpected errors -class StockError extends Error { +final class StockError extends Error { /// Constructor StockError(this.message); diff --git a/lib/src/fetcher.dart b/lib/src/fetcher.dart index fdd7528..88ec78b 100644 --- a/lib/src/fetcher.dart +++ b/lib/src/fetcher.dart @@ -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 { - Fetcher._(); - +base class Fetcher { /// "Creates" a [Fetcher] from a [futureFactory] and translates the results /// into a [StockResponse]. /// diff --git a/lib/src/implementations/factory_fetcher.dart b/lib/src/implementations/factory_fetcher.dart index 58d1d94..0209991 100644 --- a/lib/src/implementations/factory_fetcher.dart +++ b/lib/src/implementations/factory_fetcher.dart @@ -2,17 +2,17 @@ import 'package:stock/src/fetcher.dart'; -class FactoryFetcher implements Fetcher { +sealed class FactoryFetcher extends Fetcher { FactoryFetcher(this.factory); Stream Function(Key key) factory; } -class FutureFetcher extends FactoryFetcher { +final class FutureFetcher extends FactoryFetcher { FutureFetcher(Future Function(Key key) factory) : super((key) => Stream.fromFuture(factory(key))); } -class StreamFetcher extends FactoryFetcher { +final class StreamFetcher extends FactoryFetcher { StreamFetcher(super.factory); } diff --git a/lib/src/implementations/source_of_truth_impl.dart b/lib/src/implementations/source_of_truth_impl.dart index 85120ba..5eca7fe 100644 --- a/lib/src/implementations/source_of_truth_impl.dart +++ b/lib/src/implementations/source_of_truth_impl.dart @@ -2,7 +2,7 @@ import 'package:stock/src/source_of_truth.dart'; -class SourceOfTruthImpl implements SourceOfTruth { +final class SourceOfTruthImpl implements SourceOfTruth { SourceOfTruthImpl(this._reader, this._writer, this._delete, this._deleteAll); final Stream Function(Key key) _reader; @@ -23,7 +23,8 @@ class SourceOfTruthImpl implements SourceOfTruth { Future deleteAll() async => await _deleteAll?.call(); } -class WriteWrappedSourceOfTruth extends CachedSourceOfTruth { +final class WriteWrappedSourceOfTruth + extends CachedSourceOfTruth { WriteWrappedSourceOfTruth(this._realSourceOfTruth); final SourceOfTruth? _realSourceOfTruth; diff --git a/lib/src/source_of_truth.dart b/lib/src/source_of_truth.dart index d9374b2..9791ef2 100644 --- a/lib/src/source_of_truth.dart +++ b/lib/src/source_of_truth.dart @@ -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 { +abstract interface class SourceOfTruth { /// Creates a source of truth that is accessed via [reader], [writer], /// [delete] and [deleteAll]. /// diff --git a/lib/src/stock.dart b/lib/src/stock.dart index 41de66f..6d9a99c 100644 --- a/lib/src/stock.dart +++ b/lib/src/stock.dart @@ -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 { +abstract interface class Stock { /// Stock constructor factory Stock({ required Fetcher fetcher, diff --git a/lib/src/stock_request.dart b/lib/src/stock_request.dart index 7a57187..dcf1359 100644 --- a/lib/src/stock_request.dart +++ b/lib/src/stock_request.dart @@ -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 { +final class StockRequest { /// [StockRequest] constructor StockRequest({ required this.key, diff --git a/lib/src/stock_response.dart b/lib/src/stock_response.dart index f7fbe6f..d58afa8 100644 --- a/lib/src/stock_response.dart +++ b/lib/src/stock_response.dart @@ -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 { +sealed class StockResponse { const StockResponse._(this.origin); /// Loading event dispatched by [Stock] to signal the [Fetcher] is currently @@ -35,7 +35,7 @@ class StockResponse { /// Loading event dispatched by [Stock] to signal the [Fetcher] is currently /// running. @immutable -class StockResponseLoading extends StockResponse { +final class StockResponseLoading extends StockResponse { /// StockResponseLoading constructor const StockResponseLoading(super.origin) : super._(); @@ -56,7 +56,7 @@ class StockResponseLoading extends StockResponse { /// Data dispatched by [Stock] @immutable -class StockResponseData extends StockResponse { +final class StockResponseData extends StockResponse { /// StockResponseData constructor const StockResponseData(super.origin, this.value) : super._(); @@ -80,7 +80,7 @@ class StockResponseData extends StockResponse { /// Error dispatched by a pipeline @immutable -class StockResponseError extends StockResponse { +final class StockResponseError extends StockResponse { /// StockResponseError constructor const StockResponseError(super.origin, this.error, [this.stackTrace]) : super._(); diff --git a/lib/src/stock_response_extensions.dart b/lib/src/stock_response_extensions.dart index 7ab7984..24f04d9 100644 --- a/lib/src/stock_response_extensions.dart +++ b/lib/src/stock_response_extensions.dart @@ -5,25 +5,27 @@ import 'package:stock/src/stock_response.dart'; /// Useful [StockResponse] extensions. extension StockResponseExtensions on StockResponse { /// 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() => + throw StockError('There is no data in loading'), + StockResponseData(value: final value) => value, // ignore: only_throw_errors - onError: (_, error, __) => throw error, - ); + StockResponseError(error: final error) => throw error, + }; /// If there is data available, returns it; otherwise returns null. - T? getDataOrNull() => this is StockResponseData - ? (this as StockResponseData).value - : null; + T? getDataOrNull() => switch (this) { + StockResponseData(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(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. @@ -61,17 +63,12 @@ extension StockResponseExtensions on StockResponse { required E Function(StockResponseLoading value) onLoading, required E Function(StockResponseData value) onData, required E Function(StockResponseError value) onError, - }) { - if (this is StockResponseLoading) { - return onLoading(this as StockResponseLoading); - } else if (this is StockResponseData) { - return onData(this as StockResponseData); - } else if (this is StockResponseError) { - return onError(this as StockResponseError); - } else { - throw StockError('Unknown StockResponse type: $this'); - } - } + }) => + switch (this) { + StockResponseLoading() => onLoading(this as StockResponseLoading), + StockResponseData() => onData(this as StockResponseData), + StockResponseError() => onError(this as StockResponseError), + }; /// Invokes [onData] or [orElse] as fallback if the response is successful, /// [onLoading] or [orElse] as fallback if the response is loading, and diff --git a/lib/src/type_mapper.dart b/lib/src/type_mapper.dart index 3844cfa..ca8f70e 100644 --- a/lib/src/type_mapper.dart +++ b/lib/src/type_mapper.dart @@ -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 { +abstract interface class StockTypeMapper { /// Transform a [Input] into a [Output] Output fromInput(Input value); diff --git a/test/clear_and_clear_all_test.dart b/test/clear_and_clear_all_test.dart index 7026104..6c4eccd 100644 --- a/test/clear_and_clear_all_test.dart +++ b/test/clear_and_clear_all_test.dart @@ -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(); - final fetcher = MockFutureFetcher(); var timesCalled = 0; - when(fetcher.factory).thenReturn((key) => Stream.value(++timesCalled)); + final mockFetcher = + MockFutureFetcher((key) => Future.value(++timesCalled)); + final fetcher = FutureFetcher(mockFetcher.factory); final stock = Stock( fetcher: fetcher, @@ -28,9 +30,10 @@ void main() { test('Stock ClearAll invokes SOT clearAll', () async { final sourceOfTruth = createMockedSourceOfTruth(); - final fetcher = MockFutureFetcher(); var timesCalled = 0; - when(fetcher.factory).thenReturn((key) => Stream.value(++timesCalled)); + final mockFetcher = + MockFutureFetcher((key) => Future.value(++timesCalled)); + final fetcher = FutureFetcher(mockFetcher.factory); final stock = Stock( fetcher: fetcher, diff --git a/test/common/fetcher_mock.dart b/test/common/fetcher_mock.dart new file mode 100644 index 0000000..c805fdb --- /dev/null +++ b/test/common/fetcher_mock.dart @@ -0,0 +1,12 @@ +class MockFutureFetcher { + MockFutureFetcher(Future Function(Key key) factory) { + this.factory = (key) { + invocations++; + return factory(key); + }; + } + + int invocations = 0; + + late Future Function(Key key) factory; +} diff --git a/test/common_mocks.dart b/test/common_mocks.dart index 1995ff6..6c0a65b 100644 --- a/test/common_mocks.dart +++ b/test/common_mocks.dart @@ -1,5 +1,4 @@ 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'; @@ -7,8 +6,6 @@ import 'stock_response_extension_test.dart'; @GenerateMocks([ CallbackVoid, CallbackInt, - FutureFetcher, - StreamFetcher, SourceOfTruth, ]) void main() {} diff --git a/test/common_mocks.mocks.dart b/test/common_mocks.mocks.dart index ec07837..54ccc83 100644 --- a/test/common_mocks.mocks.dart +++ b/test/common_mocks.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.3.0 from annotations +// Mocks generated by Mockito 5.4.2 from annotations // in stock/test/common_mocks.dart. // Do not manually edit this file. @@ -6,8 +6,7 @@ import 'dart:async' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'package:stock/src/implementations/factory_fetcher.dart' as _i3; -import 'package:stock/src/source_of_truth.dart' as _i5; +import 'package:stock/src/source_of_truth.dart' as _i3; import 'stock_response_extension_test.dart' as _i2; @@ -58,59 +57,11 @@ class MockCallbackInt extends _i1.Mock implements _i2.CallbackInt { ) as int); } -/// A class which mocks [FutureFetcher]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockFutureFetcher extends _i1.Mock - implements _i3.FutureFetcher { - MockFutureFetcher() { - _i1.throwOnMissingStub(this); - } - - @override - _i4.Stream Function(Key) get factory => (super.noSuchMethod( - Invocation.getter(#factory), - returnValue: (Key key) => _i4.Stream.empty(), - ) as _i4.Stream Function(Key)); - @override - set factory(_i4.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 _i3.StreamFetcher { - MockStreamFetcher() { - _i1.throwOnMissingStub(this); - } - - @override - _i4.Stream Function(Key) get factory => (super.noSuchMethod( - Invocation.getter(#factory), - returnValue: (Key key) => _i4.Stream.empty(), - ) as _i4.Stream Function(Key)); - @override - set factory(_i4.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 _i5.SourceOfTruth { + implements _i3.SourceOfTruth { MockSourceOfTruth() { _i1.throwOnMissingStub(this); } @@ -123,6 +74,7 @@ class MockSourceOfTruth extends _i1.Mock ), returnValue: _i4.Stream.empty(), ) as _i4.Stream); + @override _i4.Future write( Key? key, @@ -139,6 +91,7 @@ class MockSourceOfTruth extends _i1.Mock returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + @override _i4.Future delete(Key? key) => (super.noSuchMethod( Invocation.method( @@ -148,6 +101,7 @@ class MockSourceOfTruth extends _i1.Mock returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + @override _i4.Future deleteAll() => (super.noSuchMethod( Invocation.method( diff --git a/test/fresh_and_get_test.dart b/test/fresh_and_get_test.dart index 62d0796..6e6c1ab 100644 --- a/test/fresh_and_get_test.dart +++ b/test/fresh_and_get_test.dart @@ -1,15 +1,16 @@ 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('Fresh tests', () { test('Sot is not called when fresh is invoked', () async { - final fetcher = MockFutureFetcher(); - when(fetcher.factory).thenReturn((key) => Stream.value(1)); + final mockFetcher = MockFutureFetcher((key) => Future.value(1)); + final fetcher = FutureFetcher(mockFetcher.factory); final sourceOfTruth = createMockedSourceOfTruthFromMethods( (key) => Stream.value(-1), (key, output) => Future.value(), @@ -29,8 +30,8 @@ void main() { group('Get tests', () { test('Fetcher is not called when get is invoked and sot has data', () async { - final fetcher = MockFutureFetcher(); - when(fetcher.factory).thenReturn((key) => Stream.value(1)); + final mockFetcher = MockFutureFetcher((key) => Future.value(1)); + final fetcher = FutureFetcher(mockFetcher.factory); final sourceOfTruth = createMockedSourceOfTruthFromMethods( (key) => Stream.value(-1), (key, output) => Future.value(), @@ -42,15 +43,15 @@ void main() { final result = await stock.get(1); expect(result, equals(-1)); - verifyNever(fetcher.factory); + expect(mockFetcher.invocations, equals(0)); verify(sourceOfTruth.reader(any)).called(1); verifyNever(sourceOfTruth.write(any, any)); }); test('Fetcher is called when get is invoked and sot has not data', () async { - final fetcher = MockFutureFetcher(); - when(fetcher.factory).thenReturn((key) => Stream.value(1)); + final mock = MockFutureFetcher((key) => Future.value(1)); + final fetcher = FutureFetcher(mock.factory); final sourceOfTruth = createMockedSourceOfTruth(); final stock = Stock( fetcher: fetcher, @@ -59,7 +60,7 @@ void main() { final result = await stock.get(1); expect(result, equals(1)); - verify(fetcher.factory).called(1); + expect(mock.invocations, equals(1)); verify(sourceOfTruth.reader(any)).called(1); verify(sourceOfTruth.write(any, any)).called(1); }); diff --git a/test/refresh_test.dart b/test/refresh_test.dart index b0a9752..a177f90 100644 --- a/test/refresh_test.dart +++ b/test/refresh_test.dart @@ -1,20 +1,20 @@ -import 'package:mockito/mockito.dart'; +import 'package:stock/src/implementations/factory_fetcher.dart'; import 'package:stock/src/source_of_truth.dart'; import 'package:stock/src/stock.dart'; import 'package:stock/src/stock_response.dart'; import 'package:test/test.dart'; +import 'common/fetcher_mock.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/stock_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 { - final fetcher = MockFutureFetcher(); - when(fetcher.factory).thenReturn((key) => Stream.value(1)); + final mockFetcher = MockFutureFetcher((key) => Future.value(1)); + final fetcher = FutureFetcher(mockFetcher.factory); final sourceOfTruth = CachedSourceOfTruthWithDefaultValue(-1); final stock = Stock( fetcher: fetcher, @@ -28,12 +28,13 @@ void main() { const StockResponse.data(ResponseOrigin.sourceOfTruth, -1), ]), ); - verifyNever(fetcher.factory); + expect(mockFetcher.invocations, equals(0)); }); test('Fetcher is called if sot has data and refresh is true', () async { - final fetcher = MockFutureFetcher(); - when(fetcher.factory).thenReturn((key) => Stream.value(1)); + final mockFetcher = MockFutureFetcher((key) => Future.value(1)); + final fetcher = FutureFetcher(mockFetcher.factory); + final sourceOfTruth = CachedSourceOfTruthWithDefaultValue(-1); final stock = Stock( fetcher: fetcher, @@ -49,13 +50,13 @@ void main() { const StockResponse.data(ResponseOrigin.fetcher, 1), ]), ); - verify(fetcher.factory).called(1); + expect(mockFetcher.invocations, equals(1)); }); test('Fetcher is called if sot has not data and refresh is false', () async { - final fetcher = MockFutureFetcher(); - when(fetcher.factory).thenReturn((key) => Stream.value(1)); + final mockFetcher = MockFutureFetcher((key) => Future.value(1)); + final fetcher = FutureFetcher(mockFetcher.factory); final sourceOfTruth = CachedSourceOfTruth(); final stock = Stock( fetcher: fetcher, @@ -70,13 +71,13 @@ void main() { const StockResponse.data(ResponseOrigin.fetcher, 1), ]), ); - verify(fetcher.factory).called(1); + expect(mockFetcher.invocations, equals(1)); }); test('Fetcher is called if sot returns an error and refresh is false', () async { - final fetcher = MockFutureFetcher(); - when(fetcher.factory).thenReturn((key) => Stream.value(1)); + final mockFetcher = MockFutureFetcher((key) => Future.value(1)); + final fetcher = FutureFetcher(mockFetcher.factory); final sourceOfTruth = SourceOfTruthWithError(null, throwReadErrorCount: 1); final stock = Stock( @@ -97,13 +98,13 @@ void main() { const StockResponse.data(ResponseOrigin.fetcher, 1), ]), ); - verify(fetcher.factory).called(1); + expect(mockFetcher.invocations, equals(1)); }); test('Fetcher is called if sot returns an error and refresh is true', () async { - final fetcher = MockFutureFetcher(); - when(fetcher.factory).thenReturn((key) => Stream.value(1)); + final mockFetcher = MockFutureFetcher((key) => Future.value(1)); + final fetcher = FutureFetcher(mockFetcher.factory); final sourceOfTruth = SourceOfTruthWithError(null, throwReadErrorCount: 1); final stock = Stock( @@ -124,7 +125,7 @@ void main() { const StockResponse.data(ResponseOrigin.fetcher, 1), ]), ); - verify(fetcher.factory).called(1); + expect(mockFetcher.invocations, equals(1)); }); }); } diff --git a/test/stock_response_extension_test.dart b/test/stock_response_extension_test.dart index ae085bd..7cf675c 100644 --- a/test/stock_response_extension_test.dart +++ b/test/stock_response_extension_test.dart @@ -1,6 +1,5 @@ import 'package:mockito/mockito.dart'; import 'package:stock/src/errors.dart'; -import 'package:stock/src/extensions/stock_response_internal_extensions.dart'; import 'package:stock/src/stock_response.dart'; import 'package:stock/src/stock_response_extensions.dart'; import 'package:test/test.dart'; @@ -155,32 +154,6 @@ void main() { }); }); - group('Unknown type', () { - test('Require data throws error if the type is not recognized', () async { - expect( - _FakeType().requireData, - throwsA( - (dynamic e) => - e is StockError && - e.toString().contains('Unknown StockResponse type:'), - ), - ); - }); - - test('Swap type throws error if the type is not recognized', () async { - expect( - _FakeType().swapType, - throwsA( - (dynamic e) => - e is StockError && - e.toString().contains( - "Unknown StockResponse type: Instance of '_FakeType'", - ), - ), - ); - }); - }); - group('Map', () { test('mapData', () { final loading = const StockResponseLoading(ResponseOrigin.fetcher) @@ -245,29 +218,6 @@ void main() { verifyNever(mockDataCallback()); verify(mockErrorCallback()).called(1); }); - - test('Map with unknown type', () async { - final mockLoadingCallback = MockCallbackVoid(); - final mockDataCallback = MockCallbackVoid(); - final mockErrorCallback = MockCallbackVoid(); - expect( - () { - _FakeType().map( - onLoading: (_) => mockLoadingCallback(), - onData: (_) => mockDataCallback(), - onError: (_) => mockErrorCallback(), - ); - }, - throwsA( - (dynamic e) => - e is StockError && - e.message.startsWith('Unknown StockResponse type: '), - ), - ); - verifyNever(mockLoadingCallback()); - verifyNever(mockDataCallback()); - verifyNever(mockErrorCallback()); - }); }); group('MaybeMap', () { @@ -680,8 +630,3 @@ abstract class Callback { abstract class CallbackVoid implements Callback {} abstract class CallbackInt implements Callback {} - -class _FakeType implements StockResponse { - @override - ResponseOrigin get origin => ResponseOrigin.fetcher; -} diff --git a/test/stock_without_key_test.dart b/test/stock_without_key_test.dart index f908812..4312a8d 100644 --- a/test/stock_without_key_test.dart +++ b/test/stock_without_key_test.dart @@ -1,17 +1,18 @@ -import 'package:mockito/mockito.dart'; +import 'package:stock/src/implementations/factory_fetcher.dart'; import 'package:stock/src/stock.dart'; import 'package:stock/src/stock_response.dart'; import 'package:test/test.dart'; +import 'common/fetcher_mock.dart'; import 'common/source_of_truth/cached_source_of_truth_with_default_value.dart'; import 'common/stock_test_extensions.dart'; -import 'common_mocks.mocks.dart'; void main() { group('Stock without specific key', () { test('Simple request using dynamic', () async { - final fetcher = MockFutureFetcher(); - when(fetcher.factory).thenReturn((_) => Stream.value(1)); + final mockFetcher = + MockFutureFetcher((key) => Future.value(1)); + final fetcher = FutureFetcher(mockFetcher.factory); final sourceOfTruth = CachedSourceOfTruthWithDefaultValue(-1); final stock = Stock( fetcher: fetcher,