diff --git a/.build/Common.props b/.build/Common.props index d5b059be3..efec47d4d 100644 --- a/.build/Common.props +++ b/.build/Common.props @@ -1,16 +1,16 @@ - 0.0.0 + 8.0.0 $(MORYX_ASSEMBLY_VERSION) - 0.0.0.0 + 8.0.0.0 $(MORYX_FILE_VERSION) - 0.0.0.0 + 8.0.0.0 $(MORYX_INFORMATIONAL_VERSION) - 0.0.0 + 8.0.0 $(MORYX_PACKAGE_VERSION) PHOENIXCONTACT diff --git a/MORYX-Framework.sln b/MORYX-Framework.sln index a62cf04a1..144390c6e 100644 --- a/MORYX-Framework.sln +++ b/MORYX-Framework.sln @@ -60,6 +60,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DFC092A6-B935-4D19-A564-9AEDEEF999B9}" ProjectSection(SolutionItems) = preProject Build.ps1 = Build.ps1 + .build\Common.props = .build\Common.props Directory.build.props = Directory.build.props Directory.Build.targets = Directory.Build.targets LICENSE = LICENSE @@ -118,9 +119,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Runtime.Endpoints.Tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Runtime.Endpoints.IntegrationTests", "src\Tests\Moryx.Runtime.Endpoints.IntegrationTests\Moryx.Runtime.Endpoints.IntegrationTests.csproj", "{4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.TestTools.NUnit", "src\Moryx.TestTools.NUnit\Moryx.TestTools.NUnit.csproj", "{6FF878E0-AF61-4C3A-9B9C-71C35A949E51}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.TestTools.NUnit", "src\Moryx.TestTools.NUnit\Moryx.TestTools.NUnit.csproj", "{6FF878E0-AF61-4C3A-9B9C-71C35A949E51}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.TestTools.IntegrationTest", "src\Moryx.TestTools.IntegrationTest\Moryx.TestTools.IntegrationTest.csproj", "{C949164C-0345-4893-9E4C-A79BC1F93F85}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.TestTools.IntegrationTest", "src\Moryx.TestTools.IntegrationTest\Moryx.TestTools.IntegrationTest.csproj", "{C949164C-0345-4893-9E4C-A79BC1F93F85}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -356,8 +357,8 @@ Global {C949164C-0345-4893-9E4C-A79BC1F93F85} = {953AAE25-26C8-4A28-AB08-61BAFE41B22F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - RESX_ShowErrorsInErrorList = True - RESX_TaskErrorCategory = Message SolutionGuid = {36EFC961-F4E7-49DC-A36A-99594FFB8243} + RESX_TaskErrorCategory = Message + RESX_ShowErrorsInErrorList = True EndGlobalSection EndGlobal diff --git a/src/Moryx.AbstractionLayer.Resources.Endpoints/Moryx.AbstractionLayer.Resources.Endpoints.csproj b/src/Moryx.AbstractionLayer.Resources.Endpoints/Moryx.AbstractionLayer.Resources.Endpoints.csproj index 8c981b0b2..d83e545d2 100644 --- a/src/Moryx.AbstractionLayer.Resources.Endpoints/Moryx.AbstractionLayer.Resources.Endpoints.csproj +++ b/src/Moryx.AbstractionLayer.Resources.Endpoints/Moryx.AbstractionLayer.Resources.Endpoints.csproj @@ -6,6 +6,7 @@ REST Api in order to interact with the resource management true MORYX;IIoT;IoT;Resource + true diff --git a/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs b/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs index db75310fd..6959f84f7 100644 --- a/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs +++ b/src/Moryx.AbstractionLayer.Resources.Endpoints/ResourceManagementController.cs @@ -18,6 +18,7 @@ using Moryx.Runtime.Container; using Moryx.Configuration; using System.Runtime.Serialization; +using System.ComponentModel.DataAnnotations; namespace Moryx.AbstractionLayer.Resources.Endpoints { @@ -33,8 +34,8 @@ public class ResourceModificationController : ControllerBase private readonly IResourceTypeTree _resourceTypeTree; private readonly ResourceSerialization _serialization; - public ResourceModificationController(IResourceManagement resourceManagement, - IResourceTypeTree resourceTypeTree, + public ResourceModificationController(IResourceManagement resourceManagement, + IResourceTypeTree resourceTypeTree, IModuleManager moduleManager, IServiceProvider serviceProvider) { @@ -123,7 +124,7 @@ public ActionResult InvokeMethod(long id, string method, Entry parameters return true; }); } - catch(MissingMethodException) + catch (MissingMethodException) { return BadRequest("Method could not be invoked. Please check spelling and access modifier (has to be `public` or `internal`)."); } @@ -144,30 +145,48 @@ public ActionResult InvokeMethod(long id, string method, Entry parameters public ActionResult ConstructWithParameters(string type, string method = null, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] Entry arguments = null) { var trustedType = WebUtility.HtmlEncode(type); - - return method == null ? Construct(trustedType, null) - : Construct(trustedType, new MethodEntry { Name = method, Parameters = arguments }); + if (method is null) + return Construct(trustedType); + else + return Construct(trustedType, new MethodEntry { Name = method, Parameters = arguments }); } - private ActionResult Construct(string type, MethodEntry method) + private ActionResult Construct(string type) { - var resource = (Resource)Activator.CreateInstance(_resourceTypeTree[type].ResourceType); - if (resource is null) + Resource resource; + try + { + resource = (Resource)Activator.CreateInstance(_resourceTypeTree[type].ResourceType); + } + catch (Exception) + { return NotFound(new MoryxExceptionResponse { Title = Strings.RESOURCE_NOT_FOUND }); + } ValueProviderExecutor.Execute(resource, new ValueProviderExecutorSettings() .AddFilter(new DataMemberAttributeValueProviderFilter(false)) .AddDefaultValueProvider()); - if (method != null) - EntryConvert.InvokeMethod(resource, method, _serialization); - var model = new ResourceToModelConverter(_resourceTypeTree, _serialization).GetDetails(resource); model.Methods = Array.Empty(); // Reset methods because they can not be invoked on new objects - return model; } + private ActionResult Construct(string type, MethodEntry method) + { + try + { + var id = _resourceManagement.Create(_resourceTypeTree[type].ResourceType, r => EntryConvert.InvokeMethod(r, method, _serialization)); + return GetDetails(id); + } + catch (Exception e) + { + if (e is ArgumentException or SerializationException or ValidationException) + return BadRequest(e.Message); + throw; + } + } + [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status409Conflict)] @@ -187,11 +206,11 @@ public ActionResult Save(ResourceModel model) resourcesToSave.Skip(1).ForEach(id => _resourceManagement.Modify(id, r => true)); }); - return Ok(GetDetails(id)); + return GetDetails(id); } catch (Exception e) { - if (e is ArgumentException or SerializationException) + if (e is ArgumentException or SerializationException or ValidationException) return BadRequest(e.Message); throw; } @@ -325,7 +344,7 @@ private void UpdateReferences(Resource instance, HashSet resourcesToSave, [Authorize(Policy = ResourcePermissions.CanEdit)] public ActionResult Update(long id, ResourceModel model) { - if (_resourceManagement.GetAllResources(r=>r.Id == id) is null) + if (_resourceManagement.GetAllResources(r => r.Id == id) is null) return NotFound(new MoryxExceptionResponse { Title = string.Format(Strings.ResourceNotFoundException_ById_Message, id) }); try @@ -341,7 +360,7 @@ public ActionResult Update(long id, ResourceModel model) } catch (Exception e) { - if (e is ArgumentException or SerializationException) + if (e is ArgumentException or SerializationException or ValidationException) return BadRequest(e.Message); throw; } diff --git a/src/Moryx.AbstractionLayer/Moryx.AbstractionLayer.csproj b/src/Moryx.AbstractionLayer/Moryx.AbstractionLayer.csproj index 6ce2a84f3..4dcbd846d 100644 --- a/src/Moryx.AbstractionLayer/Moryx.AbstractionLayer.csproj +++ b/src/Moryx.AbstractionLayer/Moryx.AbstractionLayer.csproj @@ -6,6 +6,7 @@ Domain model types and definitions of Cyber-physical systems in IIoT projects. true MORYX;IIoT;IoT;Hardware;Communication;Manufacturing;Industrial;Abstraction;Realtime + true diff --git a/src/Moryx.Asp.Extensions/Moryx.Asp.Extensions.csproj b/src/Moryx.Asp.Extensions/Moryx.Asp.Extensions.csproj index ea47f4474..465fb0f26 100644 --- a/src/Moryx.Asp.Extensions/Moryx.Asp.Extensions.csproj +++ b/src/Moryx.Asp.Extensions/Moryx.Asp.Extensions.csproj @@ -5,6 +5,7 @@ Extensions for the Integration of MORYX in ASP.NET Core true MORYX;ASP + true diff --git a/src/Moryx.Container/Moryx.Container.csproj b/src/Moryx.Container/Moryx.Container.csproj index d4aaf59c8..530083d7a 100644 --- a/src/Moryx.Container/Moryx.Container.csproj +++ b/src/Moryx.Container/Moryx.Container.csproj @@ -6,6 +6,7 @@ MORYX container functionality based on Castle.Windsor. true MORYX;Container;IoC + true diff --git a/src/Moryx.Model.InMemory/Moryx.Model.InMemory.csproj b/src/Moryx.Model.InMemory/Moryx.Model.InMemory.csproj index 7331a134a..106450304 100644 --- a/src/Moryx.Model.InMemory/Moryx.Model.InMemory.csproj +++ b/src/Moryx.Model.InMemory/Moryx.Model.InMemory.csproj @@ -6,6 +6,7 @@ InMemory extension for unit tests referencing Moryx.Model true MORYX;Entity;Framework;EntityFramework;DataModel;Model;Database;InMemory;Effort + true diff --git a/src/Moryx.Model.PostgreSQL/Moryx.Model.PostgreSQL.csproj b/src/Moryx.Model.PostgreSQL/Moryx.Model.PostgreSQL.csproj index 19320c878..6bf832f3b 100644 --- a/src/Moryx.Model.PostgreSQL/Moryx.Model.PostgreSQL.csproj +++ b/src/Moryx.Model.PostgreSQL/Moryx.Model.PostgreSQL.csproj @@ -6,6 +6,7 @@ Adapter for Moryx.Model on PostgreSQL true MORYX;Entity;Framework;EntityFramework;DataModel;Model;Database;Npgsql;PostgreSQL;Postgres + true diff --git a/src/Moryx.Model.Sqlite/Moryx.Model.Sqlite.csproj b/src/Moryx.Model.Sqlite/Moryx.Model.Sqlite.csproj index 416751019..32dcb0945 100644 --- a/src/Moryx.Model.Sqlite/Moryx.Model.Sqlite.csproj +++ b/src/Moryx.Model.Sqlite/Moryx.Model.Sqlite.csproj @@ -6,6 +6,7 @@ Adapter for Moryx.Model on Sqlite true MORYX;Entity;Framework;EntityFramework;DataModel;Model;Database;Sqlite + true diff --git a/src/Moryx.Model/Moryx.Model.csproj b/src/Moryx.Model/Moryx.Model.csproj index 9b3e63695..a60ec2853 100644 --- a/src/Moryx.Model/Moryx.Model.csproj +++ b/src/Moryx.Model/Moryx.Model.csproj @@ -6,6 +6,7 @@ Datamodel integration for MORYX applications based on Entity Framework true MORYX;Entity;Framework;EntityFramework;DataModel;Model;Database + true diff --git a/src/Moryx.Notifications/Moryx.Notifications.csproj b/src/Moryx.Notifications/Moryx.Notifications.csproj index 8a7719414..f90151d3b 100644 --- a/src/Moryx.Notifications/Moryx.Notifications.csproj +++ b/src/Moryx.Notifications/Moryx.Notifications.csproj @@ -6,6 +6,7 @@ Types and interfaces for notifications within MORYX applications true MORYX;IIoT;IoT;Notifications + true diff --git a/src/Moryx.Resources.Management/Facades/ResourceManagementFacade.cs b/src/Moryx.Resources.Management/Facades/ResourceManagementFacade.cs index 177ab9f06..c3db7f894 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.Resources.Management/Moryx.Resources.Management.csproj b/src/Moryx.Resources.Management/Moryx.Resources.Management.csproj index fb46a416c..871672ac4 100644 --- a/src/Moryx.Resources.Management/Moryx.Resources.Management.csproj +++ b/src/Moryx.Resources.Management/Moryx.Resources.Management.csproj @@ -7,6 +7,7 @@ ResourceManagement module composing and maintaining the resource graph as the habitat for digital twins of manufacturing assets. true MORYX;IIoT;IoT;Manufacturing;API;Resource + true diff --git a/src/Moryx.Resources.Management/Resources/ResourceTypeController.cs b/src/Moryx.Resources.Management/Resources/ResourceTypeController.cs index 893ef29a1..5140fb217 100644 --- a/src/Moryx.Resources.Management/Resources/ResourceTypeController.cs +++ b/src/Moryx.Resources.Management/Resources/ResourceTypeController.cs @@ -264,17 +264,19 @@ private ResourceProxy InstantiateProxy(string typeName, Resource instance) /// private void ProvideProxyType(Type resourceType) { - // Step 1: Find the least specific base type that offers the same amount of interfaces + // Step 1: Find the least specific base type that offers the same amount of interfaces and is not a generic itself // ReSharper disable once AssignNullToNotNullAttribute -> FullName should be not null var targetType = _typeCache[resourceType.ResourceType()]; var linker = targetType; var interfaces = RelevantInterfaces(linker); - // Move up the type tree until the parent offers less interfaces than the current linker - while (linker.BaseType != null && interfaces.Count == RelevantInterfaces(linker.BaseType).Count) + // Move up the type tree until the parent offers less interfaces than the current linker, is abstract or a generic + while (linker.BaseType != null && !linker.BaseType.ResourceType.IsGenericType + && interfaces.Count == RelevantInterfaces(linker.BaseType).Count) { linker = linker.BaseType; } + // Step 2: Check if we already created a proxy for this type. If we already // did use this one for the requested type as well. diff --git a/src/Moryx.Runtime.Kernel/Moryx.Runtime.Kernel.csproj b/src/Moryx.Runtime.Kernel/Moryx.Runtime.Kernel.csproj index fdc0e91d4..af0f71a82 100644 --- a/src/Moryx.Runtime.Kernel/Moryx.Runtime.Kernel.csproj +++ b/src/Moryx.Runtime.Kernel/Moryx.Runtime.Kernel.csproj @@ -6,6 +6,7 @@ Kernel components that comprise the MORYX server side runtime environment true MORYX;Kernel;Runtime;Server + true diff --git a/src/Moryx.Runtime/Moryx.Runtime.csproj b/src/Moryx.Runtime/Moryx.Runtime.csproj index 80aec6d44..af03381fa 100644 --- a/src/Moryx.Runtime/Moryx.Runtime.csproj +++ b/src/Moryx.Runtime/Moryx.Runtime.csproj @@ -6,6 +6,7 @@ Server side component types and implementations for MORYX applications true MORYX;Runtime;Server + true diff --git a/src/Moryx/Moryx.csproj b/src/Moryx/Moryx.csproj index 76d3188cd..42282fad1 100644 --- a/src/Moryx/Moryx.csproj +++ b/src/Moryx/Moryx.csproj @@ -7,6 +7,7 @@ Core package of the MORYX ecosystem. It defines the necessary types for MORYX compatibility as well as commonly required tools. true MORYX;Serialization;Configuration;Logging;Core;Modules;Workplans + true 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.Resources.Management.Tests/Mocks/ResourceWithGenericMethod.cs b/src/Tests/Moryx.Resources.Management.Tests/Mocks/ResourceWithGenericMethod.cs index ee751b561..07ef67d5c 100644 --- a/src/Tests/Moryx.Resources.Management.Tests/Mocks/ResourceWithGenericMethod.cs +++ b/src/Tests/Moryx.Resources.Management.Tests/Mocks/ResourceWithGenericMethod.cs @@ -37,24 +37,18 @@ public class ResourceWithGenericMethod : Resource, IGenericMethodCall, ISimpleRe public event EventHandler SomeEvent; public event EventHandler CapabilitiesChanged; - public IList GenericMethod(string identifier) - { - throw new NotImplementedException(); - } - - public int MultiplyFoo(int factor) - { - throw new NotImplementedException(); - } - - public int MultiplyFoo(int factor, ushort offset) - { - throw new NotImplementedException(); - } - - public void RaiseEvent() - { - throw new NotImplementedException(); - } + public IList GenericMethod(string identifier) => throw new NotImplementedException(); + + public int MultiplyFoo(int factor) => throw new NotImplementedException(); + + public int MultiplyFoo(int factor, ushort offset) => throw new NotImplementedException(); + + public void RaiseEvent() => throw new NotImplementedException(); } + + public interface IGenericBaseResourceInterface : IResource { } + + public class GenericBaseResource : Resource, IGenericBaseResourceInterface { } + + public class InheritingFromGenericResource : GenericBaseResource { } } diff --git a/src/Tests/Moryx.Resources.Management.Tests/TypeControllerTests.cs b/src/Tests/Moryx.Resources.Management.Tests/TypeControllerTests.cs index 8ef3f51f7..ccb8a8f9c 100644 --- a/src/Tests/Moryx.Resources.Management.Tests/TypeControllerTests.cs +++ b/src/Tests/Moryx.Resources.Management.Tests/TypeControllerTests.cs @@ -222,6 +222,20 @@ public void ProxyBuilderFiltersGenericInterfaces() Assert.IsFalse(typeof(IGenericMethodCall).IsAssignableFrom(proxy.GetType())); } + [Test] + public void ProxyBuilderSkipsGenericBaseTypes() + { + // Arrange + var driver = new InheritingFromGenericResource { Id = 42, Name = "A non generic resource inheriting from a generic base type" }; + + // Act + var proxy = _typeController.GetProxy(driver); + + // Assert + Assert.IsNotNull(proxy); + Assert.IsFalse(typeof(GenericBaseResource).IsAssignableFrom(proxy.GetType())); + } + [Test] public void FacadeExceptionForGenericProxy() { diff --git a/src/Tests/Moryx.Tests/Moryx.Tests.csproj b/src/Tests/Moryx.Tests/Moryx.Tests.csproj index ce8ad8b6e..16f9536dc 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); + } + +}