From 76fe329a312c8531d3e1681e1cacaaf55764e253 Mon Sep 17 00:00:00 2001 From: LP Date: Mon, 6 Jan 2025 10:22:36 +0000 Subject: [PATCH] Updated IComputed interface and tests Also had to add a basic observer class and extension to make computed usage easier. --- .../Computeds/ComputedFromDataTests.cs | 63 +++++++++++--- .../Computeds/ComputedFromObservableTests.cs | 84 +++++++++++++++++++ .../Plugins/Computeds/Models/DummyData.cs | 6 ++ .../Computeds/Models/TestComputedFromData.cs | 5 -- .../Models/TestComputedFromObservable.cs | 13 +++ .../Collections/IComputedCollection.cs | 2 +- .../Computeds/Data/ComputedFromObservable.cs | 1 - src/SystemsR3/Computeds/IComputed.cs | 4 +- .../Extensions/IComputedExtensions.cs | 15 ++++ src/SystemsR3/Observers/BasicObserver.cs | 29 +++++++ 10 files changed, 204 insertions(+), 18 deletions(-) create mode 100644 src/SystemsR3.Tests/Plugins/Computeds/ComputedFromObservableTests.cs create mode 100644 src/SystemsR3.Tests/Plugins/Computeds/Models/DummyData.cs create mode 100644 src/SystemsR3.Tests/Plugins/Computeds/Models/TestComputedFromObservable.cs create mode 100644 src/SystemsR3/Extensions/IComputedExtensions.cs create mode 100644 src/SystemsR3/Observers/BasicObserver.cs diff --git a/src/SystemsR3.Tests/Plugins/Computeds/ComputedFromDataTests.cs b/src/SystemsR3.Tests/Plugins/Computeds/ComputedFromDataTests.cs index 691e56d..6c0634c 100644 --- a/src/SystemsR3.Tests/Plugins/Computeds/ComputedFromDataTests.cs +++ b/src/SystemsR3.Tests/Plugins/Computeds/ComputedFromDataTests.cs @@ -1,4 +1,5 @@ using R3; +using SystemsR3.Extensions; using SystemsR3.Tests.Plugins.Computeds.Models; using Xunit; @@ -17,45 +18,87 @@ public void should_populate_on_creation() } [Fact] - public void should_refresh_value_when_changed_and_value_requested() + public void should_refresh_value_and_raise_event_when_data_changed_and_refreshed_implicitly() { var expectedData = 20; + var hasNotified = false; var data = new DummyData{Data = 10}; var computedData = new TestComputedFromData(data); + computedData.Subscribe(x => hasNotified = true); data.Data = expectedData; computedData.ManuallyRefresh.OnNext(Unit.Default); var actualData = computedData.Value; Assert.Equal(expectedData, actualData); + Assert.True(hasNotified); } [Fact] - public void should_not_refresh_value_when_not_changed_and_value_requested() + public void should_refresh_value_and_raise_event_when_data_changed_and_refreshed_explicitly() { var expectedData = 20; - var data = new DummyData{Data = expectedData}; + var hasNotified = false; + var data = new DummyData{Data = 10}; var computedData = new TestComputedFromData(data); + computedData.Subscribe(x => hasNotified = true); - data.Data = 10; + data.Data = expectedData; + computedData.RefreshData(); var actualData = computedData.Value; Assert.Equal(expectedData, actualData); + Assert.True(hasNotified); } [Fact] - public void should_refresh_data_when_changed_with_subs() + public void should_not_refresh_value_when_datasource_changed_but_not_refreshed() { - var expectedData = 10; - var data = new DummyData{Data = 20}; + var expectedData = 20; + var hasNotified = false; + var data = new DummyData{Data = expectedData}; var computedData = new TestComputedFromData(data); - data.Data = expectedData; + computedData.Subscribe(x => hasNotified = true); + data.Data = 10; + + var actualData = computedData.Value; + Assert.Equal(expectedData, actualData); + Assert.False(hasNotified); + } + + [Fact] + public void should_not_refresh_value_or_notify_when_datasource_not_changed_even_when_refreshed_implicitly() + { + var expectedData = 20; + var hasNotified = false; + var data = new DummyData{Data = expectedData}; + + var computedData = new TestComputedFromData(data); + computedData.Subscribe(x => hasNotified = true); computedData.ManuallyRefresh.OnNext(Unit.Default); + + var actualData = computedData.Value; + Assert.Equal(expectedData, actualData); + Assert.False(hasNotified); + } + + [Fact] + public void should_not_refresh_value_or_notify_when_datasource_not_changed_even_when_refreshed_explicitly() + { + var expectedData = 20; + var hasNotified = false; + var data = new DummyData{Data = expectedData}; - Assert.Equal(expectedData, computedData.CachedData); - } + var computedData = new TestComputedFromData(data); + computedData.Subscribe(x => hasNotified = true); + computedData.RefreshData(); + + var actualData = computedData.Value; + Assert.Equal(expectedData, actualData); + Assert.False(hasNotified); + } } } \ No newline at end of file diff --git a/src/SystemsR3.Tests/Plugins/Computeds/ComputedFromObservableTests.cs b/src/SystemsR3.Tests/Plugins/Computeds/ComputedFromObservableTests.cs new file mode 100644 index 0000000..1521c0d --- /dev/null +++ b/src/SystemsR3.Tests/Plugins/Computeds/ComputedFromObservableTests.cs @@ -0,0 +1,84 @@ +using R3; +using SystemsR3.Extensions; +using SystemsR3.Tests.Plugins.Computeds.Models; +using Xunit; + +namespace SystemsR3.Tests.Plugins.Computeds +{ + public class ComputedFromObservableTests + { + [Fact] + public void should_populate_on_creation() + { + var expectedData = 10; + var data = new ReactiveProperty(expectedData); + + var computedData = new TestComputedFromObservable(data); + Assert.Equal(expectedData, computedData.CachedData); + } + + [Fact] + public void should_refresh_value_and_raise_event_when_data_changed_and_refreshed_implicitly() + { + var expectedData = 20; + var hasNotified = false; + var data = new ReactiveProperty(10); + + var computedData = new TestComputedFromObservable(data); + computedData.Subscribe(x => hasNotified = true); + data.Value = expectedData; + + var actualData = computedData.Value; + Assert.Equal(expectedData, actualData); + Assert.True(hasNotified); + } + + [Fact] + public void should_refresh_value_and_raise_event_when_data_changed_and_refreshed_explicitly() + { + var expectedData = 20; + var hasNotified = false; + var data = new ReactiveProperty(10); + + var computedData = new TestComputedFromObservable(data); + computedData.Subscribe(x => hasNotified = true); + computedData.RefreshData(expectedData); + + var actualData = computedData.Value; + Assert.Equal(expectedData, actualData); + Assert.True(hasNotified); + } + + [Fact] + public void should_not_refresh_value_or_notify_when_datasource_not_changed_even_when_refreshed_implicitly() + { + var expectedData = 20; + var hasNotified = false; + var data = new ReactiveProperty(expectedData); + + var computedData = new TestComputedFromObservable(data); + computedData.Subscribe(x => hasNotified = true); + data.OnNext(expectedData); + + var actualData = computedData.Value; + Assert.Equal(expectedData, actualData); + Assert.False(hasNotified); + } + + [Fact] + public void should_not_refresh_value_or_notify_when_datasource_not_changed_even_when_refreshed_explicitly() + { + var expectedData = 20; + var hasNotified = false; + var data = new ReactiveProperty(expectedData); + + var computedData = new TestComputedFromObservable(data); + computedData.Subscribe(x => hasNotified = true); + computedData.RefreshData(expectedData); + + var actualData = computedData.Value; + Assert.Equal(expectedData, actualData); + Assert.False(hasNotified); + } + } +} \ No newline at end of file diff --git a/src/SystemsR3.Tests/Plugins/Computeds/Models/DummyData.cs b/src/SystemsR3.Tests/Plugins/Computeds/Models/DummyData.cs new file mode 100644 index 0000000..6ca33bd --- /dev/null +++ b/src/SystemsR3.Tests/Plugins/Computeds/Models/DummyData.cs @@ -0,0 +1,6 @@ +namespace SystemsR3.Tests.Plugins.Computeds.Models; + +public class DummyData +{ + public int Data { get; set; } +} \ No newline at end of file diff --git a/src/SystemsR3.Tests/Plugins/Computeds/Models/TestComputedFromData.cs b/src/SystemsR3.Tests/Plugins/Computeds/Models/TestComputedFromData.cs index 772a378..957ea09 100644 --- a/src/SystemsR3.Tests/Plugins/Computeds/Models/TestComputedFromData.cs +++ b/src/SystemsR3.Tests/Plugins/Computeds/Models/TestComputedFromData.cs @@ -3,11 +3,6 @@ namespace SystemsR3.Tests.Plugins.Computeds.Models { - public class DummyData - { - public int Data { get; set; } - } - public class TestComputedFromData : ComputedFromData { public Subject ManuallyRefresh = new Subject(); diff --git a/src/SystemsR3.Tests/Plugins/Computeds/Models/TestComputedFromObservable.cs b/src/SystemsR3.Tests/Plugins/Computeds/Models/TestComputedFromObservable.cs new file mode 100644 index 0000000..e7f081a --- /dev/null +++ b/src/SystemsR3.Tests/Plugins/Computeds/Models/TestComputedFromObservable.cs @@ -0,0 +1,13 @@ +using R3; +using SystemsR3.Computeds.Data; + +namespace SystemsR3.Tests.Plugins.Computeds.Models; + +public class TestComputedFromObservable : ComputedFromObservable +{ + public TestComputedFromObservable(ReactiveProperty observable) : base(observable) + {} + + public override int Transform(int dataSource) + { return dataSource; } +} \ No newline at end of file diff --git a/src/SystemsR3/Computeds/Collections/IComputedCollection.cs b/src/SystemsR3/Computeds/Collections/IComputedCollection.cs index 66d4b67..5d56846 100644 --- a/src/SystemsR3/Computeds/Collections/IComputedCollection.cs +++ b/src/SystemsR3/Computeds/Collections/IComputedCollection.cs @@ -6,7 +6,7 @@ namespace SystemsR3.Computeds.Collections /// Represents a computed collection of elements /// /// The data to contain - public interface IComputedCollection : IComputed>, IEnumerable + public interface IComputedCollection : IComputed>, IEnumerable { /// /// Get an element by its index diff --git a/src/SystemsR3/Computeds/Data/ComputedFromObservable.cs b/src/SystemsR3/Computeds/Data/ComputedFromObservable.cs index 61a5093..6c9f786 100644 --- a/src/SystemsR3/Computeds/Data/ComputedFromObservable.cs +++ b/src/SystemsR3/Computeds/Data/ComputedFromObservable.cs @@ -33,7 +33,6 @@ public IDisposable Subscribe(Observer observer) public void MonitorChanges() { DataSource.Subscribe(RefreshData).AddTo(Subscriptions); } - public void RefreshData(TInput data) { lock (_lock) diff --git a/src/SystemsR3/Computeds/IComputed.cs b/src/SystemsR3/Computeds/IComputed.cs index e74d077..d6b9fb8 100644 --- a/src/SystemsR3/Computeds/IComputed.cs +++ b/src/SystemsR3/Computeds/IComputed.cs @@ -1,9 +1,11 @@ using System; +using R3; namespace SystemsR3.Computeds { - public interface IComputed + public interface IComputed { T Value { get; } + IDisposable Subscribe(Observer observer); } } \ No newline at end of file diff --git a/src/SystemsR3/Extensions/IComputedExtensions.cs b/src/SystemsR3/Extensions/IComputedExtensions.cs new file mode 100644 index 0000000..92fd846 --- /dev/null +++ b/src/SystemsR3/Extensions/IComputedExtensions.cs @@ -0,0 +1,15 @@ +using System; +using SystemsR3.Computeds; +using SystemsR3.Observers; + +namespace SystemsR3.Extensions +{ + public static class IComputedExtensions + { + public static IDisposable Subscribe(this IComputed computed, Action onNext) + { return computed.Subscribe(new BasicObserver(onNext)); } + + public static IDisposable Subscribe(this IComputed computed, Action onNext, Action onError) + { return computed.Subscribe(new BasicObserver(onNext, onError)); } + } +} \ No newline at end of file diff --git a/src/SystemsR3/Observers/BasicObserver.cs b/src/SystemsR3/Observers/BasicObserver.cs new file mode 100644 index 0000000..b388378 --- /dev/null +++ b/src/SystemsR3/Observers/BasicObserver.cs @@ -0,0 +1,29 @@ +using System; +using R3; + +namespace SystemsR3.Observers +{ + public class BasicObserver : Observer + { + public Action OnNext { get; } + public Action OnErrorResume { get; } + public Action OnCompleted { get; } + + public BasicObserver(Action onNext, Action onErrorResume = null, Action onCompleted = null) + { + OnNext = onNext; + OnErrorResume = onErrorResume ?? ObservableSystem.GetUnhandledExceptionHandler(); + OnCompleted = onCompleted ?? AutoHandleResult; + } + + private void AutoHandleResult(Result result) + { + if (result.IsFailure) + { ObservableSystem.GetUnhandledExceptionHandler().Invoke(result.Exception); } + } + + protected override void OnNextCore(T value) => OnNext(value); + protected override void OnErrorResumeCore(Exception error) => OnErrorResume(error); + protected override void OnCompletedCore(Result complete) => OnCompleted(complete); + } +} \ No newline at end of file