diff --git a/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriver.cs b/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriver.cs new file mode 100644 index 000000000..3c4dd5653 --- /dev/null +++ b/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriver.cs @@ -0,0 +1,31 @@ +using Moryx.AbstractionLayer.Drivers; +using System.Threading.Tasks; + +namespace Moryx.Drivers.Camera.Interfaces +{ + /// + /// Interface for camera devices, that provide image data + /// + public interface ICameraDriver : IDriver where TImage : class + { + /// + /// Registers an ICameraDriverListener that should be provided + /// with images. + /// + void Register(ICameraDriverListener listener); + + /// + /// Unregisters an ICameraDriverListener + /// + void Unregister(ICameraDriverListener listener); + + /// + /// Capture a single image from the camera + /// + /// + /// The image that was captured or null in case no image + /// could be retrieved + /// + Task CaptureImage(); + } +} diff --git a/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriverListener.cs b/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriverListener.cs new file mode 100644 index 000000000..da7a7fe34 --- /dev/null +++ b/src/Moryx.AbstractionLayer/Drivers/Camera/ICameraDriverListener.cs @@ -0,0 +1,18 @@ +using Moryx.AbstractionLayer.Drivers; +using System.Threading.Tasks; + +namespace Moryx.Drivers.Camera.Interfaces +{ + /// + /// Interface for objects that register as listeners to camera drivers + /// + public interface ICameraDriverListener where T : class + { + /// + /// Invoked, when a new image is received by the camera + /// + /// + /// + Task OnImage(T image); + } +} diff --git a/src/Moryx.CommandCenter.Web/src/common/container/App.tsx b/src/Moryx.CommandCenter.Web/src/common/container/App.tsx index 1fe1e8c8a..dd81dd397 100644 --- a/src/Moryx.CommandCenter.Web/src/common/container/App.tsx +++ b/src/Moryx.CommandCenter.Web/src/common/container/App.tsx @@ -104,7 +104,7 @@ function App(props: AppPropModel & AppDispatchPropModel) { } /> } /> - } /> + } /> diff --git a/src/Moryx.Resources.Management/Facades/ResourceManagementFacade.cs b/src/Moryx.Resources.Management/Facades/ResourceManagementFacade.cs index baa5f7a23..95cc8075d 100644 --- a/src/Moryx.Resources.Management/Facades/ResourceManagementFacade.cs +++ b/src/Moryx.Resources.Management/Facades/ResourceManagementFacade.cs @@ -9,7 +9,7 @@ namespace Moryx.Resources.Management { - internal class ResourceManagementFacade : IResourceManagement, IFacadeControl + internal class ResourceManagementFacade : FacadeBase, IResourceManagement { #region Dependency Injection @@ -22,11 +22,9 @@ internal class ResourceManagementFacade : IResourceManagement, IFacadeControl #endregion #region IFacadeControl - /// - public Action ValidateHealthState { get; set; } /// - public void Activate() + public override void Activate() { Manager.ResourceAdded += OnResourceAdded; Manager.CapabilitiesChanged += OnCapabilitiesChanged; @@ -34,7 +32,7 @@ public void Activate() } /// - public void Deactivate() + public override void Deactivate() { Manager.ResourceAdded -= OnResourceAdded; Manager.CapabilitiesChanged -= OnCapabilitiesChanged; diff --git a/src/Moryx/Tools/FunctionResult.cs b/src/Moryx/Tools/FunctionResult.cs new file mode 100644 index 000000000..527691f29 --- /dev/null +++ b/src/Moryx/Tools/FunctionResult.cs @@ -0,0 +1,246 @@ +// Copyright (c) 2023, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using System; + +namespace Moryx.Tools.FunctionResult +{ + /// + /// Generic type that allows functions to always return a proper result, + /// that either contains a valid value or an error and helps exercising + /// error handling. + /// + /// + public class FunctionResult + { + /// + /// Result value in case of success + /// + public TResult? Result { get; } = default; + + /// + /// Error in case of failure + /// + public FunctionResultError? Error { get; } = null; + + /// + /// Indicates if the result contains a valid value + /// or not + /// + public bool Success => Error == null; + + /// + /// Creates a result with a value + /// + /// + public FunctionResult(TResult result) + { + Result = result; + } + + /// + /// Creates an error result with + /// + /// + public FunctionResult(FunctionResultError error) + { + Error = error; + } + + /// + public override string ToString() + { + return Success + ? Result?.ToString() ?? "null" + : Error!.ToString(); + } + + /// + /// Process result value and errors in a 'pattern matching '-like way + /// + /// Function to be excecuted in case of success + /// Function to be excecuted in case of an error + /// of the executed function + public FunctionResult Match(Func> success, Func> error) + => Success ? success(Result!) : error(Error!); + + /// + /// Process result value and errors in a 'pattern matching '-like way + /// + /// Action to be excecuted in case of success + /// Action to be excecuted in case of an error + /// The current + public FunctionResult Match(Action success, Action error) + => Match( + s => + { + success(s); + return this; + }, + e => + { + error(e); + return this; + }); + } + + /// + /// of type to be used + /// for functions that would return + /// + public class FunctionResult : FunctionResult + { + /// + /// Creates a `successful` with 'no' value + /// + public FunctionResult() : base(new Nothing()) + { + } + + + /// + /// Creates an error result with + /// + /// + public FunctionResult(FunctionResultError error) : base(error) + { + } + + /// + /// Helper to create an Ok in a descriptive way. + /// + /// + public static FunctionResult Ok() + => new FunctionResult(); + + /// + /// Helper to create a with an error message in a descriptive way. + /// + /// + public static FunctionResult WithError(string message) + => new(new FunctionResultError(message)); + + /// + /// Helper to create a with an in a descriptive way. + /// + /// + public static FunctionResult WithError(Exception exception) + => new(new FunctionResultError(exception)); + + /// + /// Helper to create an Ok in a descriptive way. + /// + /// of + public static FunctionResult Ok(TResult result) + => new(result); + + /// + /// Helper to create a with an error message in a descriptive way. + /// + /// + public static FunctionResult WithError(string message) + => new(new FunctionResultError(message)); + + /// + /// Helper to create an Ok in a descriptive way. + /// + /// + public static FunctionResult WithError(Exception exception) + => new(new FunctionResultError(exception)); + + } + + /// + /// Holds a description of the error and optionally an + /// + public class FunctionResultError + { + /// + /// Error message + /// + public string Message { get; } + + /// + /// Exception that might be the reason for the error + /// + public Exception? Exception { get; } = null; + + + /// + /// Creates an error with error message + /// + /// In case of is null + public FunctionResultError(string message) + { + if (message is null) + throw new ArgumentNullException(nameof(message)); + + Message = message; + } + + /// + /// Creates an error with error message + /// + /// In case of is null + public FunctionResultError(Exception exception) + { + if (exception is null) + throw new ArgumentNullException(nameof(exception)); + + Message = exception.Message; + Exception = exception; + } + + /// + public override string ToString() + { + return Exception != null + ? Exception.Message + : Message; + } + + } + + + /// + /// Placeholder type to return nothing when for example + /// would be returned + /// + public class Nothing + { + } + + /// + /// Extensions for + /// + public static class FunctionResultExtensions + { + /// + /// Executes the provided function in case of a successful result + /// + /// returned by + public static FunctionResult Then(this FunctionResult result, Func> func) + => result.Match(func, _ => result); + + /// + /// Executes the provided function in case of an error result + /// + /// returned by + public static FunctionResult Catch(this FunctionResult result, Func> func) + => result.Match(_ => result, func); + + /// + /// Executes the provided action in case of a successful result + /// + /// The underlying + public static FunctionResult Then(this FunctionResult result, Action action) + => result.Match(action, _ => { }); + + /// + /// Executes the provided action in case of a error result + /// + /// The underlying + public static FunctionResult Catch(this FunctionResult result, Action action) + => result.Match(_ => { }, action); + } +} diff --git a/src/Tests/Moryx.Tests/Moryx.Tests.csproj b/src/Tests/Moryx.Tests/Moryx.Tests.csproj index 39faf789d..75f356f3b 100644 --- a/src/Tests/Moryx.Tests/Moryx.Tests.csproj +++ b/src/Tests/Moryx.Tests/Moryx.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Tests/Moryx.Tests/Tools/FunctionResultTestsBase.cs b/src/Tests/Moryx.Tests/Tools/FunctionResultTestsBase.cs new file mode 100644 index 000000000..f79eec0f9 --- /dev/null +++ b/src/Tests/Moryx.Tests/Tools/FunctionResultTestsBase.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2023, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using NUnit.Framework; + +namespace Moryx.Tests.Tools +{ + [TestFixture] + public class FunctionResultTestsBase + { + protected const string Message = "Error occured!"; + protected const string ExceptionMessage = "Exception Message"; + } +} diff --git a/src/Tests/Moryx.Tests/Tools/FunctionResultWithNothingTests.cs b/src/Tests/Moryx.Tests/Tools/FunctionResultWithNothingTests.cs new file mode 100644 index 000000000..7057077cc --- /dev/null +++ b/src/Tests/Moryx.Tests/Tools/FunctionResultWithNothingTests.cs @@ -0,0 +1,171 @@ +// Copyright (c) 2023, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using NUnit.Framework; +using Moryx.Tools.FunctionResult; +using System; +using Moq; + +namespace Moryx.Tests.Tools; + +[TestFixture] +public class FunctionResultWithNothingTests : FunctionResultTestsBase +{ + protected Mock>> _funcMockSuccess; + protected Mock>> _funcMockError; + + protected Mock> _actionMockSuccess; + protected Mock> _actionMockError; + + [SetUp] + public void Setup() + { + _funcMockSuccess = new Mock>>(); + _funcMockSuccess + .Setup(f => f(It.IsAny())) + .Returns((Nothing arg) => new FunctionResult(arg)); + _funcMockError = new Mock>>(); + _funcMockError + .Setup(f => f(It.IsAny())) + .Returns((FunctionResultError arg) => new FunctionResult(arg)); + + _actionMockSuccess = new Mock>(); + _actionMockError = new Mock>(); + } + + + [Test] + public void ResultWithValueGetCreated() + { + var result = new FunctionResult(); + + Assert.That(result.Success, Is.True); + Assert.That(result.Error, Is.Null); + Assert.That(result.Result, Is.TypeOf()); + Assert.That(result.ToString(), Contains.Substring("Nothing")); + } + + [Test] + public void ErrorResultWithMessageGetsCreated() + { + var result = new FunctionResult(new FunctionResultError(Message)); + + Assert.That(result.Success, Is.False); + Assert.That(result.Result, Is.Null); + + Assert.That(result.Error.Message, Is.EqualTo(Message)); + Assert.That(result.Error.Exception, Is.Null); + Assert.That(result.ToString(), Is.EqualTo(Message)); + } + + [Test] + public void ErrorResultWithExceptionGetsCreated() + { + var result = new FunctionResult(new FunctionResultError(new Exception(ExceptionMessage))); + + Assert.That(result.Success, Is.False); + Assert.That(result.Result, Is.Null); + + Assert.That(result.Error.Message, Is.EqualTo(ExceptionMessage)); + Assert.That(result.Error.Exception, Is.TypeOf()); + Assert.That(result.ToString(), Is.EqualTo(ExceptionMessage)); + } + + [Test] + public void ResultWithNothingGetsCreatedByUsingExtension() + { + var result = FunctionResult.Ok(); + + Assert.That(result.Success, Is.True); + Assert.That(result.Result, Is.TypeOf()); + Assert.That(result.Error, Is.Null); + Assert.That(result.ToString(), Contains.Substring("Nothing")); + } + + [Test] + public void ErrorResultWithMessageGetsCreatedByUsingExtension() + { + var result = FunctionResult.WithError(Message); + + Assert.That(result.Success, Is.False); + Assert.That(result.Result, Is.Null); + + + Assert.That(result.Error.Message, Is.EqualTo(Message)); + Assert.That(result.Error.Exception, Is.Null); + Assert.That(result.ToString(), Is.EqualTo(Message)); + } + + [Test] + public void ErrorResultWithExceptionGetsCreatedByUsingExtension() + { + FunctionResult result = FunctionResult.WithError(new Exception(ExceptionMessage)); + + Assert.That(result.Success, Is.False); + Assert.That(result.Result, Is.Null); + + Assert.That(result.Error.Message, Is.EqualTo(ExceptionMessage)); + Assert.That(result.Error.Exception, Is.TypeOf()); + Assert.That(result.ToString(), Is.EqualTo(ExceptionMessage)); + } + + [Test] + public void CannotCreateErrorResultWithNullExeption() + { + Assert.Throws(() => { FunctionResult.WithError((Exception)null); }); + } + + [Test] + public void CannotCreateErrorResultWithNullMessage() + { + Assert.Throws(() => { FunctionResult.WithError((string)null); }); + } + + [Test] + public void ExecutesFuncOnError() + { + FunctionResult.WithError("500") + .Then(_funcMockSuccess.Object) + .Catch(_funcMockError.Object) + ; + + _funcMockSuccess.Verify(f => f(It.IsAny()), Times.Never); + _funcMockError.Verify(f => f(It.IsAny()), Times.Once); + } + + [Test] + public void ExecutesFuncOnSuccess() + { + FunctionResult.Ok() + .Catch(_funcMockError.Object) + .Then(_funcMockSuccess.Object) + ; + + _funcMockSuccess.Verify(f => f(It.IsAny()), Times.Once); + _funcMockError.Verify(f => f(It.IsAny()), Times.Never); + } + + [Test] + public void ExecutesActionOnError() + { + FunctionResult.WithError("500") + .Then(_actionMockSuccess.Object) + .Catch(_actionMockError.Object) + ; + + _actionMockSuccess.Verify(a => a(It.IsAny()), Times.Never); + _actionMockError.Verify(a => a(It.IsAny()), Times.Once); + } + + [Test] + public void ExecutesActionOnSuccess() + { + FunctionResult.Ok() + .Catch(_actionMockError.Object) + .Then(_actionMockSuccess.Object) + ; + + _actionMockSuccess.Verify(a => a(It.IsAny()), Times.Once); + _actionMockError.Verify(a => a(It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Moryx.Tests/Tools/FunctionResultWithTypeTests.cs b/src/Tests/Moryx.Tests/Tools/FunctionResultWithTypeTests.cs new file mode 100644 index 000000000..c84b6b755 --- /dev/null +++ b/src/Tests/Moryx.Tests/Tools/FunctionResultWithTypeTests.cs @@ -0,0 +1,293 @@ +// Copyright (c) 2023, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using NUnit.Framework; +using System; +using Moq; +using Moryx.Tools.FunctionResult; + +namespace Moryx.Tests.Tools; + +[TestFixture] +public class FunctionResultWithTypeTests : FunctionResultTestsBase +{ + protected Mock>> _funcMockSuccess; + protected Mock>> _funcMockError; + + protected Mock> _actionMockSuccess; + protected Mock> _actionMockError; + + [SetUp] + public void Setup() + { + _funcMockSuccess = new Mock>>(); + _funcMockSuccess + .Setup(f => f(It.IsAny())) + .Returns((int arg) => new FunctionResult(arg)); + _funcMockError = new Mock>>(); + _funcMockError + .Setup(f => f(It.IsAny())) + .Returns((FunctionResultError arg) => new FunctionResult(arg)); + + _actionMockSuccess = new Mock>(); + _actionMockError = new Mock>(); + } + + [Test] + public void ResultWithValueGetsCreated() + { + var result = new FunctionResult(1); + + Assert.That(result.Success, Is.True); + Assert.That(result.Error, Is.Null); + Assert.That(result.Result, Is.EqualTo(1)); + Assert.That(result.ToString(), Is.EqualTo("1")); + } + + [Test] + public void ErrorResultWithMessageGetsCreated() + { + var result = new FunctionResult(new FunctionResultError(Message)); + + Assert.That(result.Success, Is.False); + Assert.That(result.Result, Is.EqualTo(0)); + + Assert.That(result.Error.Message, Is.EqualTo(Message)); + Assert.That(result.Error.Exception, Is.Null); + Assert.That(result.ToString(), Is.EqualTo(Message)); + } + + [Test] + public void ErrorResultWithExceptionGetsCreated() + { + var result = new FunctionResult(new FunctionResultError(new Exception(ExceptionMessage))); + + Assert.That(result.Success, Is.False); + Assert.That(result.Result, Is.EqualTo(0)); + + Assert.That(result.Error.Message, Is.EqualTo(ExceptionMessage)); + Assert.That(result.Error.Exception, Is.TypeOf()); + Assert.That(result.ToString(), Is.EqualTo(ExceptionMessage)); + } + + [Test] + public void ResultWithValueGetsCreatedByUsingExtension() + { + var result = FunctionResult.Ok(10); + + Assert.That(result.Success, Is.True); + Assert.That(result.Result, Is.EqualTo(10)); + + Assert.That(result.Error, Is.Null); + Assert.That(result.ToString(), Is.EqualTo("10")); + } + + [Test] + public void ErrorResultWithMessageGetsCreatedByUsingExtension() + { + var result = FunctionResult.WithError(Message); + + Assert.That(result.Success, Is.False); + Assert.That(result.Result, Is.EqualTo(0)); + + + Assert.That(result.Error.Message, Is.EqualTo(Message)); + Assert.That(result.Error.Exception, Is.Null); + Assert.That(result.ToString(), Is.EqualTo(Message)); + } + + [Test] + public void ErrorResultWithExceptionGetsCreatedByUsingExtension() + { + var result = FunctionResult.WithError(new Exception(ExceptionMessage)); + + Assert.That(result.Success, Is.False); + Assert.That(result.Result, Is.EqualTo(0)); + + Assert.That(result.Error.Message, Is.EqualTo(ExceptionMessage)); + Assert.That(result.Error.Exception, Is.TypeOf()); + Assert.That(result.ToString(), Is.EqualTo(ExceptionMessage)); + } + + [Test] + public void ResultToStringEqualsTheResultsToStringReturnValue() + { + var floatResult = FunctionResult.Ok(3.14f); + string floatAsString = Convert.ToString(3.14f); // avoid localization issues + var noResult = FunctionResult.Ok(new Nothing()); + var nullResult = FunctionResult.Ok(null); + + Assert.Multiple(() => + { + Assert.That($"{floatResult}", Is.EqualTo(floatAsString)); + Assert.That($"{noResult}", Is.EqualTo(new Nothing().ToString())); + Assert.That($"{nullResult}", Is.EqualTo("null")); + }); + } + + [Test] + public void CannotCreateErrorResultWithNullExeption() + { + Assert.Throws(() => { FunctionResult.WithError((Exception)null); }); + } + + [Test] + public void CannotCreateErrorResultWithNullMessage() + { + Assert.Throws(() => { FunctionResult.WithError((string)null); }); + } + + [Test] + public void SuccessResultMatchesSuccess() + { + var result = FunctionResult.Ok(200); + + var matchResult = result + .Match( + success: _funcMockSuccess.Object, + error: _funcMockError.Object + ); + + _funcMockSuccess.Verify(f => f(It.IsAny()), Times.Once); + _funcMockError.Verify(f => f(It.IsAny()), Times.Never); + + Assert.That(result, Is.Not.SameAs(matchResult)); + } + + + [Test] + public void ErrorResultMatchesErrorWithException() + { + var result = FunctionResult.WithError(new Exception("Internal server error")); + + var matchResult = result + .Match( + success: _funcMockSuccess.Object, + error: _funcMockError.Object + ); + + _funcMockSuccess.Verify(f => f(It.IsAny()), Times.Never); + _funcMockError.Verify(f => f(It.IsAny()), Times.Once); + + // The assertion verifies, that the result can be different from the original + Assert.That(result, Is.Not.SameAs(matchResult)); + } + + [Test] + public void ErrorResultMatchesErrorWithMessage() + { + var result = FunctionResult.WithError("500"); + + var matchResult = result + .Match( + success: _funcMockSuccess.Object, + error: _funcMockError.Object + ); + + _funcMockSuccess.Verify(f => f(It.IsAny()), Times.Never); + _funcMockError.Verify(f => f(It.IsAny()), Times.Once); + + // The assertion verifies, that the result can be different from the original + Assert.That(result, Is.Not.SameAs(matchResult)); + } + + [Test] + public void SuccessResultMatchesSuccessAction() + { + var result = FunctionResult.Ok(200); + + var matchResult = result + .Match( + success: _actionMockSuccess.Object, + error: _actionMockError.Object + ); + + _actionMockSuccess.Verify(a => a(It.IsAny()), Times.Once); + _actionMockError.Verify(a => a(It.IsAny()), Times.Never); + + Assert.That(result, Is.SameAs(matchResult)); + } + + [Test] + public void ExceptionResultMatchesErrorAction() + { + var result = FunctionResult.WithError(new Exception("Internal server error")); + + var matchResult = result + .Match( + success: _actionMockSuccess.Object, + error: _actionMockError.Object + ); + + _actionMockSuccess.Verify(a => a(It.IsAny()), Times.Never); + _actionMockError.Verify(a => a(It.IsAny()), Times.Once); + + Assert.That(result, Is.SameAs(matchResult)); + } + + [Test] + public void ErrorMessageResultMatchesErrorAction() + { + var result = FunctionResult.WithError("500"); + + var matchResult = result + .Match( + success: _actionMockSuccess.Object, + error: _actionMockError.Object + ); + + _actionMockSuccess.Verify(a => a(It.IsAny()), Times.Never); + _actionMockError.Verify(a => a(It.IsAny()), Times.Once); + + Assert.That(result, Is.SameAs(matchResult)); + } + + [Test] + public void ExecutesFuncOnError() + { + FunctionResult.WithError("500") + .Then(_funcMockSuccess.Object) + .Catch(_funcMockError.Object) + ; + + _funcMockSuccess.Verify(f => f(It.IsAny()), Times.Never); + _funcMockError.Verify(f => f(It.IsAny()), Times.Once); + } + + [Test] + public void ExecutesFuncOnSuccess() + { + FunctionResult.Ok(201) + .Catch(_funcMockError.Object) + .Then(_funcMockSuccess.Object) + ; + + _funcMockSuccess.Verify(f => f(It.IsAny()), Times.Once); + _funcMockError.Verify(f => f(It.IsAny()), Times.Never); + } + + [Test] + public void ExecutesActionOnError() + { + FunctionResult.WithError("500") + .Then(_actionMockSuccess.Object) + .Catch(_actionMockError.Object) + ; + + _actionMockSuccess.Verify(a => a(It.IsAny()), Times.Never); + _actionMockError.Verify(a => a(It.IsAny()), Times.Once); + } + + [Test] + public void ExecutesActionOnSuccess() + { + FunctionResult.Ok(42) + .Catch(_actionMockError.Object) + .Then(_actionMockSuccess.Object) + ; + + _actionMockSuccess.Verify(a => a(It.IsAny()), Times.Once); + _actionMockError.Verify(a => a(It.IsAny()), Times.Never); + } + +}