From 749cc1502b1421f7ff6e4e85dea49266de624fd4 Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Mon, 12 Feb 2024 22:54:53 +0100 Subject: [PATCH 1/9] Started implementing backing stores --- lib/kiota_abstractions.dart | 5 ++ lib/src/store/backed_model.dart | 7 ++ lib/src/store/backing_store.dart | 46 +++++++++++ lib/src/store/backing_store_factory.dart | 7 ++ .../backing_store_subscription_callback.dart | 9 +++ lib/src/store/in_memory_backing_store.dart | 79 +++++++++++++++++++ 6 files changed, 153 insertions(+) create mode 100644 lib/src/store/backed_model.dart create mode 100644 lib/src/store/backing_store.dart create mode 100644 lib/src/store/backing_store_factory.dart create mode 100644 lib/src/store/backing_store_subscription_callback.dart create mode 100644 lib/src/store/in_memory_backing_store.dart diff --git a/lib/kiota_abstractions.dart b/lib/kiota_abstractions.dart index 521cf22..ac6c0a9 100644 --- a/lib/kiota_abstractions.dart +++ b/lib/kiota_abstractions.dart @@ -49,3 +49,8 @@ part 'src/serialization/serialization_writer_factory.dart'; part 'src/serialization/serialization_writer_factory_registry.dart'; part 'src/time_only.dart'; part 'src/serialization/serialization_writer_proxy_factory.dart'; +part 'src/store/backed_model.dart'; +part 'src/store/backing_store.dart'; +part 'src/store/backing_store_factory.dart'; +part 'src/store/backing_store_subscription_callback.dart'; +part 'src/store/in_memory_backing_store.dart'; diff --git a/lib/src/store/backed_model.dart b/lib/src/store/backed_model.dart new file mode 100644 index 0000000..bb8d8cd --- /dev/null +++ b/lib/src/store/backed_model.dart @@ -0,0 +1,7 @@ +part of '../../kiota_abstractions.dart'; + +/// Defines the contract for a model that is backed by a store. +abstract class BackedModel { + /// Gets the store that is backing the model. + BackingStore? get backingStore; +} diff --git a/lib/src/store/backing_store.dart b/lib/src/store/backing_store.dart new file mode 100644 index 0000000..85d2744 --- /dev/null +++ b/lib/src/store/backing_store.dart @@ -0,0 +1,46 @@ +part of '../../kiota_abstractions.dart'; + +/// Stores model information in a different location than the object properties. +/// +/// Implementations can provide dirty tracking and caching capabilities or +/// integration with 3rd party stores. +abstract class BackingStore { + /// Gets a value from the backing store based on its key. Returns `null` if + /// the value hasn't changed and [returnOnlyChangedValues] is `true`. + T? get(String key); + + /// Sets or updates the stored value for the given key. + /// + /// Will trigger subscriptions callbacks. + void set(String key, T value); + + /// Iterates all the values stored in the backing store. Values will be + /// filtered if [returnOnlyChangedValues] is `true`. + Iterable> iterate(); + + /// Iterates the keys for all values that changed to `null`. + Iterable iterateKeysForValuesChangedToNull(); + + /// Creates a subscription to any data change happening, optionally specifying + /// a [subscriptionId] to be able to unsubscribe later. + /// + /// The given [callback] is invoked on data changes. + String subscribe( + BackingStoreSubscriptionCallback callback, [ + String? subscriptionId, + ]); + + /// Unsubscribes a subscription by its [subscriptionId]. + void unsubscribe(String subscriptionId); + + /// Clears all the stored values. Doesn't trigger any subscription callbacks. + void clear(); + + /// Whether to return only values that have changed since the initialization + /// of the object when calling [get] and [iterate] methods. + abstract bool returnOnlyChangedValues; + + /// Whether the initialization of the object and/or the initial + /// deserialization has been competed to track whether objects have changed. + abstract bool initializationCompleted; +} diff --git a/lib/src/store/backing_store_factory.dart b/lib/src/store/backing_store_factory.dart new file mode 100644 index 0000000..5aa3366 --- /dev/null +++ b/lib/src/store/backing_store_factory.dart @@ -0,0 +1,7 @@ +part of '../../kiota_abstractions.dart'; + +/// Defines the contract for a factory that creates a [BackingStore]. +abstract class BackingStoreFactory { + /// Creates a new instance of the [BackingStore]. + BackingStore createBackingStore(); +} diff --git a/lib/src/store/backing_store_subscription_callback.dart b/lib/src/store/backing_store_subscription_callback.dart new file mode 100644 index 0000000..ed6bda6 --- /dev/null +++ b/lib/src/store/backing_store_subscription_callback.dart @@ -0,0 +1,9 @@ +part of '../../kiota_abstractions.dart'; + +/// Defines the contract for a callback that is invoked when a value in the +/// [BackingStore] changes. +typedef BackingStoreSubscriptionCallback = void Function( + String dataKey, + Object? previousValue, + Object? newValue, +); diff --git a/lib/src/store/in_memory_backing_store.dart b/lib/src/store/in_memory_backing_store.dart new file mode 100644 index 0000000..4780f2d --- /dev/null +++ b/lib/src/store/in_memory_backing_store.dart @@ -0,0 +1,79 @@ +part of '../../kiota_abstractions.dart'; + +class InMemoryBackingStore implements BackingStore { + final Map _store = {}; + final Map _subscriptions = {}; + + bool _initializationCompleted = true; + + bool get initializationCompleted => _initializationCompleted; + + set initializationCompleted(bool value) { + _initializationCompleted = value; + + for (final key in _store.keys) { + final tuple = _store[key]!; + final obj = tuple.$2; + + if (obj is BackedModel) { + obj.backingStore?.initializationCompleted = value; + } + + ensureCollectionPropertyIsConsistent(key, obj); + + _store[key] = (!value, obj); + } + } + + void ensureCollectionPropertyIsConsistent(String key, Object? value) { + // TODO(rbo): Implement this method. + } + + @override + bool returnOnlyChangedValues = false; + + @override + void clear() => _store.clear(); + + @override + T? get(String key) { + if (key.isEmpty) { + throw ArgumentError('The key cannot be empty.'); + } + + if (!_store.containsKey(key)) { + return null; + } + + final value = _store[key]; + } + + @override + Iterable> iterate() { + // TODO: implement iterate + throw UnimplementedError(); + } + + @override + Iterable iterateKeysForValuesChangedToNull() { + // TODO: implement iterateKeysForValuesChangedToNull + throw UnimplementedError(); + } + + @override + void set(String key, T value) { + // TODO: implement set + } + + @override + String subscribe(BackingStoreSubscriptionCallback callback, + [String? subscriptionId]) { + // TODO: implement subscribe + throw UnimplementedError(); + } + + @override + void unsubscribe(String subscriptionId) { + // TODO: implement unsubscribe + } +} From 638270c1030c585f2123a93d4ccb99302ce6da2f Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Tue, 13 Feb 2024 02:28:45 +0100 Subject: [PATCH 2/9] Finished implementing InMemoryBackingStore and wrote some tests for it --- lib/src/store/in_memory_backing_store.dart | 125 ++++++++++++++++++--- test/in_memory_backing_store_test.dart | 75 +++++++++++++ 2 files changed, 186 insertions(+), 14 deletions(-) create mode 100644 test/in_memory_backing_store_test.dart diff --git a/lib/src/store/in_memory_backing_store.dart b/lib/src/store/in_memory_backing_store.dart index 4780f2d..351ea9f 100644 --- a/lib/src/store/in_memory_backing_store.dart +++ b/lib/src/store/in_memory_backing_store.dart @@ -6,8 +6,10 @@ class InMemoryBackingStore implements BackingStore { bool _initializationCompleted = true; + @override bool get initializationCompleted => _initializationCompleted; + @override set initializationCompleted(bool value) { _initializationCompleted = value; @@ -26,7 +28,28 @@ class InMemoryBackingStore implements BackingStore { } void ensureCollectionPropertyIsConsistent(String key, Object? value) { - // TODO(rbo): Implement this method. + // check if we put in a collection annotated with the size + if (value is (Iterable, int)) { + value.$1.whereType().forEach((model) { + model.backingStore?.iterate().forEach((item) { + // Call get() on nested properties so that this method may be called + // recursively to ensure collections are consistent + model.backingStore?.get(item.key); + }); + }); + + // (and the size has changed since we last updated) + if (value.$1.length != value.$2) { + // ensure the store is notified the collection property has changed + set(key, value.$1); + } + } else if (value is BackedModel) { + value.backingStore?.iterate().forEach((item) { + // Call get() on nested properties so that this method may be called + // recursively to ensure collections are consistent + value.backingStore?.get(item.key); + }); + } } @override @@ -45,35 +68,109 @@ class InMemoryBackingStore implements BackingStore { return null; } - final value = _store[key]; + final tuple = _store[key]!; + final changed = tuple.$1; + var obj = tuple.$2; + + ensureCollectionPropertyIsConsistent(key, obj); + + if (obj is (Iterable, int)) { + obj = obj.$1; + } + + return changed || !returnOnlyChangedValues ? obj as T? : null; } @override - Iterable> iterate() { - // TODO: implement iterate - throw UnimplementedError(); + Iterable> iterate() sync* { + for (final key in _store.keys) { + final tuple = _store[key]!; + final changed = tuple.$1; + final obj = tuple.$2; + + if (returnOnlyChangedValues) { + ensureCollectionPropertyIsConsistent(key, obj); + } + + if (changed || !returnOnlyChangedValues) { + yield MapEntry(key, obj); + } + } } @override - Iterable iterateKeysForValuesChangedToNull() { - // TODO: implement iterateKeysForValuesChangedToNull - throw UnimplementedError(); + Iterable iterateKeysForValuesChangedToNull() sync* { + for (final key in _store.keys) { + final tuple = _store[key]!; + final changed = tuple.$1; + final obj = tuple.$2; + + if (changed && obj == null) { + yield key; + } + } } @override void set(String key, T value) { - // TODO: implement set + if (key.isEmpty) { + throw ArgumentError('The key cannot be empty.'); + } + + (bool, Object?) valueToAdd = (initializationCompleted, value); + if (value is Iterable) { + valueToAdd = (initializationCompleted, (value, value.length)); + } + + (bool, Object?)? oldValue; + if (_store.containsKey(key)) { + oldValue = _store[key]; + _store[key] = valueToAdd; + } else if (value is BackedModel) { + value.backingStore?.subscribe( + (dataKey, previousValue, newValue) { + // all its properties are dirty as the model has been touched + value.backingStore!.initializationCompleted = false; + + set(key, value); + }, + key, + ); + } else { + _store[key] = valueToAdd; + } + + if (value is Iterable) { + value.whereType().forEach((model) { + model.backingStore?.initializationCompleted = false; + model.backingStore?.subscribe( + (dataKey, previousValue, newValue) { + set(key, value); + }, + key, + ); + }); + } + + for (final subscription in _subscriptions.values) { + subscription(key, oldValue?.$2, value); + } } @override - String subscribe(BackingStoreSubscriptionCallback callback, - [String? subscriptionId]) { - // TODO: implement subscribe - throw UnimplementedError(); + String subscribe( + BackingStoreSubscriptionCallback callback, [ + String? subscriptionId, + ]) { + subscriptionId ??= const Uuid().v4(); + + _subscriptions[subscriptionId] = callback; + + return subscriptionId; } @override void unsubscribe(String subscriptionId) { - // TODO: implement unsubscribe + _subscriptions.remove(subscriptionId); } } diff --git a/test/in_memory_backing_store_test.dart b/test/in_memory_backing_store_test.dart new file mode 100644 index 0000000..a77b0f4 --- /dev/null +++ b/test/in_memory_backing_store_test.dart @@ -0,0 +1,75 @@ +import 'package:kiota_abstractions/kiota_abstractions.dart'; +import 'package:mockito/annotations.dart'; +import 'package:test/test.dart'; + +import 'in_memory_backing_store_test.mocks.dart'; + +@GenerateMocks([BackedModel]) +void main() { + group('InMemoryBackingStore', () { + test('stores values by key', () { + final store = InMemoryBackingStore()..set('key', 'value'); + + expect(store.get('key'), 'value'); + }); + + test('clears all values', () { + final store = InMemoryBackingStore() + ..set('key', 'value') + ..set('key2', 'value2') + ..clear(); + + expect(store.get('key'), null); + }); + + test('iterates over all values', () { + final store = InMemoryBackingStore(); + + expect(store.iterate(), isEmpty); + + store + ..set('key', 'value') + ..set('key2', 'value2'); + + final entries = store.iterate().toList(); + + expect(entries, hasLength(2)); + expect( + entries.map((e) => e.key), + containsAll(['key', 'key2']), + ); + expect( + entries.map((e) => e.value), + containsAll(['value', 'value2']), + ); + }); + + test('iterates over all values that have changed to null', () { + final store = InMemoryBackingStore() + ..set('name', 'Peter Pan') + ..set('email', 'peterpan@neverland.com') + ..set('phone', null); + + final changedToNullEntries = + store.iterateKeysForValuesChangedToNull().toList(); + final entries = store.iterate().toList(); + + expect(entries, hasLength(3)); + expect(changedToNullEntries, hasLength(1)); + expect(changedToNullEntries, contains('phone')); + }); + + test('prevents duplicates in store', () { + final store = InMemoryBackingStore(); + + expect(store.iterate(), isEmpty); + + store + ..set('key', 'value') + ..set('key', 'value2'); + + expect(store.iterate(), hasLength(1)); + expect(store.get('key'), 'value2'); + }); + }); +} From 17ae70e4f682d51f466540a849ed9691a8451990 Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Tue, 13 Feb 2024 02:29:11 +0100 Subject: [PATCH 3/9] Removed unused import --- test/in_memory_backing_store_test.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/in_memory_backing_store_test.dart b/test/in_memory_backing_store_test.dart index a77b0f4..1a5ff6e 100644 --- a/test/in_memory_backing_store_test.dart +++ b/test/in_memory_backing_store_test.dart @@ -2,8 +2,6 @@ import 'package:kiota_abstractions/kiota_abstractions.dart'; import 'package:mockito/annotations.dart'; import 'package:test/test.dart'; -import 'in_memory_backing_store_test.mocks.dart'; - @GenerateMocks([BackedModel]) void main() { group('InMemoryBackingStore', () { From b324703cb82888740c52034af17d0aec7e7004d2 Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Wed, 14 Feb 2024 00:26:07 +0100 Subject: [PATCH 4/9] Check in generated mocks --- test/in_memory_backing_store_test.mocks.dart | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test/in_memory_backing_store_test.mocks.dart diff --git a/test/in_memory_backing_store_test.mocks.dart b/test/in_memory_backing_store_test.mocks.dart new file mode 100644 index 0000000..812e098 --- /dev/null +++ b/test/in_memory_backing_store_test.mocks.dart @@ -0,0 +1,29 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in kiota_abstractions/test/in_memory_backing_store_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:kiota_abstractions/kiota_abstractions.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// 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: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// 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 [BackedModel]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBackedModel extends _i1.Mock implements _i2.BackedModel { + MockBackedModel() { + _i1.throwOnMissingStub(this); + } +} From 24de673aa4c618c110ee01472f8ecb443266dfdb Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Wed, 14 Feb 2024 01:02:57 +0100 Subject: [PATCH 5/9] Added missing interfaces and implementations for backing stores --- lib/kiota_abstractions.dart | 4 ++ .../backing_store_factory_singleton.dart | 9 ++++ .../backing_store_parse_node_factory.dart | 28 +++++++++++++ ...re_serialization_writer_proxy_factory.dart | 42 +++++++++++++++++++ .../in_memory_backing_store_factory.dart | 7 ++++ 5 files changed, 90 insertions(+) create mode 100644 lib/src/store/backing_store_factory_singleton.dart create mode 100644 lib/src/store/backing_store_parse_node_factory.dart create mode 100644 lib/src/store/backing_store_serialization_writer_proxy_factory.dart create mode 100644 lib/src/store/in_memory_backing_store_factory.dart diff --git a/lib/kiota_abstractions.dart b/lib/kiota_abstractions.dart index ac6c0a9..f072e84 100644 --- a/lib/kiota_abstractions.dart +++ b/lib/kiota_abstractions.dart @@ -52,5 +52,9 @@ part 'src/serialization/serialization_writer_proxy_factory.dart'; part 'src/store/backed_model.dart'; part 'src/store/backing_store.dart'; part 'src/store/backing_store_factory.dart'; +part 'src/store/backing_store_factory_singleton.dart'; +part 'src/store/backing_store_parse_node_factory.dart'; +part 'src/store/backing_store_serialization_writer_proxy_factory.dart'; part 'src/store/backing_store_subscription_callback.dart'; part 'src/store/in_memory_backing_store.dart'; +part 'src/store/in_memory_backing_store_factory.dart'; diff --git a/lib/src/store/backing_store_factory_singleton.dart b/lib/src/store/backing_store_factory_singleton.dart new file mode 100644 index 0000000..f69b1cc --- /dev/null +++ b/lib/src/store/backing_store_factory_singleton.dart @@ -0,0 +1,9 @@ +part of '../../kiota_abstractions.dart'; + +/// This class is used to register the backing store factory. +class BackingStoreFactorySingleton { + static final BackingStoreFactory _instance = InMemoryBackingStoreFactory(); + + /// The backing store factory singleton instance. + static BackingStoreFactory get instance => _instance; +} diff --git a/lib/src/store/backing_store_parse_node_factory.dart b/lib/src/store/backing_store_parse_node_factory.dart new file mode 100644 index 0000000..2d18b1e --- /dev/null +++ b/lib/src/store/backing_store_parse_node_factory.dart @@ -0,0 +1,28 @@ +part of '../../kiota_abstractions.dart'; + +/// Proxy implementation of [ParseNodeFactory] that allows for the +/// [BackingStore] that automatically sets the state of the [BackingStore] +/// when deserializing. +class BackingStoreParseNodeFactory extends ParseNodeProxyFactory { + /// Creates a new instance of the [BackingStoreParseNodeFactory] class. + BackingStoreParseNodeFactory({ + required super.concrete, + }) : super( + onBefore: (parsable) { + if (parsable is BackedModel) { + final model = parsable as BackedModel; + if (model.backingStore != null) { + model.backingStore!.initializationCompleted = false; + } + } + }, + onAfter: (parsable) { + if (parsable is BackedModel) { + final model = parsable as BackedModel; + if (model.backingStore != null) { + model.backingStore!.initializationCompleted = true; + } + } + }, + ); +} diff --git a/lib/src/store/backing_store_serialization_writer_proxy_factory.dart b/lib/src/store/backing_store_serialization_writer_proxy_factory.dart new file mode 100644 index 0000000..71a4b86 --- /dev/null +++ b/lib/src/store/backing_store_serialization_writer_proxy_factory.dart @@ -0,0 +1,42 @@ +part of '../../kiota_abstractions.dart'; + +/// Proxy implementation of [SerializationWriterFactory] for the [BackingStore] +/// that automatically sets the state of the backing store when serializing. +class BackingStoreSerializationWriterProxyFactory + extends SerializationWriterProxyFactory { + /// Creates a new instance of [BackingStoreSerializationWriterProxyFactory] + /// with the provided concrete factory. + BackingStoreSerializationWriterProxyFactory({ + required super.concrete, + }) : super( + onBefore: (p) { + if (p is BackedModel) { + final model = p as BackedModel; + if (model.backingStore != null) { + model.backingStore!.returnOnlyChangedValues = true; + } + } + }, + onAfter: (p) { + if (p is BackedModel) { + final model = p as BackedModel; + if (model.backingStore != null) { + model.backingStore!.returnOnlyChangedValues = false; + model.backingStore!.initializationCompleted = true; + } + } + }, + onStart: (p, writer) { + if (p is BackedModel) { + final model = p as BackedModel; + if (model.backingStore != null) { + model.backingStore! + .iterateKeysForValuesChangedToNull() + .forEach((element) { + writer.writeNullValue(element); + }); + } + } + }, + ); +} diff --git a/lib/src/store/in_memory_backing_store_factory.dart b/lib/src/store/in_memory_backing_store_factory.dart new file mode 100644 index 0000000..6e8e405 --- /dev/null +++ b/lib/src/store/in_memory_backing_store_factory.dart @@ -0,0 +1,7 @@ +part of '../../kiota_abstractions.dart'; + +/// This class is used to create instances of [InMemoryBackingStore]. +class InMemoryBackingStoreFactory implements BackingStoreFactory { + @override + BackingStore createBackingStore() => InMemoryBackingStore(); +} From 2f25a2610141bf313ceda52df09e26101fc0764c Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Wed, 14 Feb 2024 01:04:38 +0100 Subject: [PATCH 6/9] Moved tests for store into their own folder --- test/{ => store}/in_memory_backing_store_test.dart | 0 test/{ => store}/in_memory_backing_store_test.mocks.dart | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename test/{ => store}/in_memory_backing_store_test.dart (100%) rename test/{ => store}/in_memory_backing_store_test.mocks.dart (100%) diff --git a/test/in_memory_backing_store_test.dart b/test/store/in_memory_backing_store_test.dart similarity index 100% rename from test/in_memory_backing_store_test.dart rename to test/store/in_memory_backing_store_test.dart diff --git a/test/in_memory_backing_store_test.mocks.dart b/test/store/in_memory_backing_store_test.mocks.dart similarity index 100% rename from test/in_memory_backing_store_test.mocks.dart rename to test/store/in_memory_backing_store_test.mocks.dart From 32ed67435a475793dda6402ff1ccac3c632a9817 Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Mon, 19 Feb 2024 23:11:45 +0100 Subject: [PATCH 7/9] Make ensureCollectionPropertyIsConsistent private --- lib/src/store/in_memory_backing_store.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/store/in_memory_backing_store.dart b/lib/src/store/in_memory_backing_store.dart index 351ea9f..89627fe 100644 --- a/lib/src/store/in_memory_backing_store.dart +++ b/lib/src/store/in_memory_backing_store.dart @@ -21,13 +21,13 @@ class InMemoryBackingStore implements BackingStore { obj.backingStore?.initializationCompleted = value; } - ensureCollectionPropertyIsConsistent(key, obj); + _ensureCollectionPropertyIsConsistent(key, obj); _store[key] = (!value, obj); } } - void ensureCollectionPropertyIsConsistent(String key, Object? value) { + void _ensureCollectionPropertyIsConsistent(String key, Object? value) { // check if we put in a collection annotated with the size if (value is (Iterable, int)) { value.$1.whereType().forEach((model) { @@ -72,7 +72,7 @@ class InMemoryBackingStore implements BackingStore { final changed = tuple.$1; var obj = tuple.$2; - ensureCollectionPropertyIsConsistent(key, obj); + _ensureCollectionPropertyIsConsistent(key, obj); if (obj is (Iterable, int)) { obj = obj.$1; @@ -89,7 +89,7 @@ class InMemoryBackingStore implements BackingStore { final obj = tuple.$2; if (returnOnlyChangedValues) { - ensureCollectionPropertyIsConsistent(key, obj); + _ensureCollectionPropertyIsConsistent(key, obj); } if (changed || !returnOnlyChangedValues) { From 9e3015bd73fb4560600396f1a8fe862b2172a29b Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Mon, 19 Feb 2024 23:12:16 +0100 Subject: [PATCH 8/9] Fixed issue with backed models and added tests for initializationCompleted propagation --- lib/src/store/in_memory_backing_store.dart | 5 +- test/store/in_memory_backing_store_test.dart | 49 ++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/lib/src/store/in_memory_backing_store.dart b/lib/src/store/in_memory_backing_store.dart index 89627fe..03f72a6 100644 --- a/lib/src/store/in_memory_backing_store.dart +++ b/lib/src/store/in_memory_backing_store.dart @@ -125,7 +125,6 @@ class InMemoryBackingStore implements BackingStore { (bool, Object?)? oldValue; if (_store.containsKey(key)) { oldValue = _store[key]; - _store[key] = valueToAdd; } else if (value is BackedModel) { value.backingStore?.subscribe( (dataKey, previousValue, newValue) { @@ -136,10 +135,10 @@ class InMemoryBackingStore implements BackingStore { }, key, ); - } else { - _store[key] = valueToAdd; } + _store[key] = valueToAdd; + if (value is Iterable) { value.whereType().forEach((model) { model.backingStore?.initializationCompleted = false; diff --git a/test/store/in_memory_backing_store_test.dart b/test/store/in_memory_backing_store_test.dart index 1a5ff6e..39ea69c 100644 --- a/test/store/in_memory_backing_store_test.dart +++ b/test/store/in_memory_backing_store_test.dart @@ -1,7 +1,10 @@ import 'package:kiota_abstractions/kiota_abstractions.dart'; import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; +import 'in_memory_backing_store_test.mocks.dart'; + @GenerateMocks([BackedModel]) void main() { group('InMemoryBackingStore', () { @@ -69,5 +72,51 @@ void main() { expect(store.iterate(), hasLength(1)); expect(store.get('key'), 'value2'); }); + + test('propagates initialization completed to backed models', () { + final aModel = MockBackedModel(); + final aStore = InMemoryBackingStore(); + + when(aModel.backingStore).thenReturn(aStore); + + final bStore = InMemoryBackingStore(); + + expect(aStore.initializationCompleted, isTrue); + expect(bStore.initializationCompleted, isTrue); + + bStore + ..set('aModel', aModel) + ..initializationCompleted = false; + + expect(aStore.initializationCompleted, isFalse); + expect(bStore.initializationCompleted, isFalse); + + bStore.initializationCompleted = true; + + expect(aStore.initializationCompleted, isTrue); + expect(bStore.initializationCompleted, isTrue); + + final cModel = MockBackedModel(); + final cStore = InMemoryBackingStore(); + when(cModel.backingStore).thenReturn(cStore); + + bStore.set('cModel', cModel); + }); + + test('returns only changed values', () + { + final store = InMemoryBackingStore() + ..set('name', 'Peter') + ..set('email', 'peter@neverland.com') + ..returnOnlyChangedValues = true; + + final changedEntries = store.iterate().toList(); + expect(changedEntries, hasLength(0)); + + store.set('name', 'Wendy'); + + final changedEntries2 = store.iterate().toList(); + expect(changedEntries2, hasLength(1)); + }); }); } From 8f159e8dca6cc00676b4563669ba8971305e88bf Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Tue, 20 Feb 2024 02:59:08 +0100 Subject: [PATCH 9/9] Added test for subscriptions --- test/store/in_memory_backing_store_test.dart | 29 ++++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/test/store/in_memory_backing_store_test.dart b/test/store/in_memory_backing_store_test.dart index 39ea69c..2f36ad3 100644 --- a/test/store/in_memory_backing_store_test.dart +++ b/test/store/in_memory_backing_store_test.dart @@ -103,20 +103,25 @@ void main() { bStore.set('cModel', cModel); }); - test('returns only changed values', () + test('subscriptions get notified', () { - final store = InMemoryBackingStore() - ..set('name', 'Peter') - ..set('email', 'peter@neverland.com') - ..returnOnlyChangedValues = true; - - final changedEntries = store.iterate().toList(); - expect(changedEntries, hasLength(0)); - - store.set('name', 'Wendy'); + final store = InMemoryBackingStore(); - final changedEntries2 = store.iterate().toList(); - expect(changedEntries2, hasLength(1)); + String? key; + Object? oldValue; + Object? newValue; + final subscriptionId = store.subscribe((k, a, b) { + key = k; + oldValue = a; + newValue = b; + }); + + store.set('name', 'Peter'); + + expect(key, 'name'); + expect(oldValue, null); + expect(newValue, 'Peter'); + expect(subscriptionId, isNotNull); }); }); }