diff --git a/Bahla.Backend.sln b/Bahla.Backend.sln index 9061b29..784d2b0 100644 --- a/Bahla.Backend.sln +++ b/Bahla.Backend.sln @@ -3,7 +3,31 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.11.35303.130 MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bahla.Tests", "src\Bahla.Tests\Bahla.Tests.csproj", "{F1392CEF-BD63-4208-8ADA-95DF407E0D28}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bahla.Domain", "src\Bahla.Domain\Bahla.Domain.csproj", "{543F323C-31BF-4BEA-9503-F9A26E963FC8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bahla.Persistence", "src\Bahla.Persistence\Bahla.Persistence.csproj", "{FBEE999C-7DFE-451B-AB01-7E7AE5C3ED95}" +EndProject Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F1392CEF-BD63-4208-8ADA-95DF407E0D28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F1392CEF-BD63-4208-8ADA-95DF407E0D28}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1392CEF-BD63-4208-8ADA-95DF407E0D28}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F1392CEF-BD63-4208-8ADA-95DF407E0D28}.Release|Any CPU.Build.0 = Release|Any CPU + {543F323C-31BF-4BEA-9503-F9A26E963FC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {543F323C-31BF-4BEA-9503-F9A26E963FC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {543F323C-31BF-4BEA-9503-F9A26E963FC8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {543F323C-31BF-4BEA-9503-F9A26E963FC8}.Release|Any CPU.Build.0 = Release|Any CPU + {FBEE999C-7DFE-451B-AB01-7E7AE5C3ED95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBEE999C-7DFE-451B-AB01-7E7AE5C3ED95}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBEE999C-7DFE-451B-AB01-7E7AE5C3ED95}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBEE999C-7DFE-451B-AB01-7E7AE5C3ED95}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection diff --git a/src/Bahla.Domain/Bahla.Domain.csproj b/src/Bahla.Domain/Bahla.Domain.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/src/Bahla.Domain/Bahla.Domain.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/src/Bahla.Domain/Builders/Base/IEntityBuilder.cs b/src/Bahla.Domain/Builders/Base/IEntityBuilder.cs new file mode 100644 index 0000000..d0ee200 --- /dev/null +++ b/src/Bahla.Domain/Builders/Base/IEntityBuilder.cs @@ -0,0 +1,13 @@ +using Bahla.Domain.Entities.Base; + +namespace Bahla.Domain.Builders.Base +{ + public interface IEntityBuilder where T : IEntity + { + /// + /// Build the entity. + /// + /// Entity + T Build(); + } +} diff --git a/src/Bahla.Domain/Builders/Base/IRoleBuilder.cs b/src/Bahla.Domain/Builders/Base/IRoleBuilder.cs new file mode 100644 index 0000000..13a40bd --- /dev/null +++ b/src/Bahla.Domain/Builders/Base/IRoleBuilder.cs @@ -0,0 +1,37 @@ +using Bahla.Domain.Entities; +using Bahla.Domain.Types; + +namespace Bahla.Domain.Builders.Base +{ + public interface IRoleBuilder : IEntityBuilder + { + /// + /// Defines the identifier for the role, if not provided, a new identifier will be generated. + /// + /// Identifier + /// Builder + IRoleBuilder WithIdentifier(Identifier identifier); + + /// + /// Defines the name for the role. + /// + /// Role name + /// Builder + IRoleBuilder WithName(string roleName); + + + /// + /// Defines the value mask for the role. Maximum allowed is 32 bits (1 << 32). + /// + /// Role bitmask + /// Builder + IRoleBuilder WithValueMask(uint valueMask); + + /// + /// Clones the role. + /// + /// Role + /// Builder + IRoleBuilder Clone(Role role); + } +} diff --git a/src/Bahla.Domain/Builders/RoleBuilder.cs b/src/Bahla.Domain/Builders/RoleBuilder.cs new file mode 100644 index 0000000..0385d3f --- /dev/null +++ b/src/Bahla.Domain/Builders/RoleBuilder.cs @@ -0,0 +1,61 @@ +using Bahla.Domain.Builders.Base; +using Bahla.Domain.Entities; +using Bahla.Domain.Exceptions; +using Bahla.Domain.Specifications.Base; +using Bahla.Domain.Types; + +namespace Bahla.Domain.Builders +{ + internal sealed class RoleNameIsNotEmptySpecification : CompositeSpecification + { + public override bool IsSatisfiedBy(RoleBuilder entity) + => !string.IsNullOrEmpty(entity._roleName) && !string.IsNullOrWhiteSpace(entity._roleName); + } + + + public sealed class RoleBuilder : IRoleBuilder + { + internal Identifier _identifier = Identifier.Generate; + + internal string _roleName = string.Empty; + + internal uint _valueMask; + + public Role Build() + { + if (new RoleNameIsNotEmptySpecification() + .IsSatisfiedBy(this)) { + return new Role(this); + } + + throw new EntityBuilderException("Role name cannot be null or empty"); + } + + public IRoleBuilder WithIdentifier(Identifier identifier) + { + _identifier = identifier; + return this; + } + + public IRoleBuilder WithName(string roleName) + { + _roleName = roleName; + return this; + } + + public IRoleBuilder WithValueMask(uint valueMask) + { + _valueMask = valueMask; + return this; + } + + public IRoleBuilder Clone(Role role) + { + _identifier = role.UUID; + _roleName = role.RoleName; + _valueMask = role.ValueMask; + + return this; + } + } +} diff --git a/src/Bahla.Domain/Entities/Base/Entity.cs b/src/Bahla.Domain/Entities/Base/Entity.cs new file mode 100644 index 0000000..334d493 --- /dev/null +++ b/src/Bahla.Domain/Entities/Base/Entity.cs @@ -0,0 +1,6 @@ +using Bahla.Domain.Types; + +namespace Bahla.Domain.Entities.Base +{ + public abstract record Entity(Identifier UUID) : IEntity; +} diff --git a/src/Bahla.Domain/Entities/Base/IEntity.cs b/src/Bahla.Domain/Entities/Base/IEntity.cs new file mode 100644 index 0000000..742b226 --- /dev/null +++ b/src/Bahla.Domain/Entities/Base/IEntity.cs @@ -0,0 +1,15 @@ +using Bahla.Domain.Types; + +namespace Bahla.Domain.Entities.Base +{ + /// + /// Interface that describes an independent entity. + /// + public interface IEntity + { + /// + /// Unique identifier for the entity. + /// + Identifier UUID { get; init; } + } +} diff --git a/src/Bahla.Domain/Entities/Role.cs b/src/Bahla.Domain/Entities/Role.cs new file mode 100644 index 0000000..62c60af --- /dev/null +++ b/src/Bahla.Domain/Entities/Role.cs @@ -0,0 +1,26 @@ +using Bahla.Domain.Builders; +using Bahla.Domain.Builders.Base; +using Bahla.Domain.Entities.Base; +using Bahla.Domain.Types; + +namespace Bahla.Domain.Entities +{ + public sealed record Role : Entity + { + public string RoleName { get; init; } + public uint ValueMask { get; init; } + internal Role(Identifier identifier, string roleName, uint valueMask) : base(identifier) + { + RoleName = roleName; + ValueMask = valueMask; + } + + internal Role(RoleBuilder builder) : base(builder._identifier) + { + RoleName = builder._roleName; + ValueMask = builder._valueMask; + } + + public static IRoleBuilder Builder => new RoleBuilder(); + } + } diff --git a/src/Bahla.Domain/Exceptions/DomainException.cs b/src/Bahla.Domain/Exceptions/DomainException.cs new file mode 100644 index 0000000..b65e74d --- /dev/null +++ b/src/Bahla.Domain/Exceptions/DomainException.cs @@ -0,0 +1,4 @@ +namespace Bahla.Domain.Exceptions +{ + public abstract class DomainException(string message) : Exception(string.Concat("Domain Exception: ", " ", message)) { } +} diff --git a/src/Bahla.Domain/Exceptions/EntityBuilderException.cs b/src/Bahla.Domain/Exceptions/EntityBuilderException.cs new file mode 100644 index 0000000..6d26df4 --- /dev/null +++ b/src/Bahla.Domain/Exceptions/EntityBuilderException.cs @@ -0,0 +1,7 @@ +namespace Bahla.Domain.Exceptions +{ + public sealed class EntityBuilderException : DomainException + { + internal EntityBuilderException(string message) : base(message) { } + } +} diff --git a/src/Bahla.Domain/Specifications/AndNotSpecification.cs b/src/Bahla.Domain/Specifications/AndNotSpecification.cs new file mode 100644 index 0000000..259a826 --- /dev/null +++ b/src/Bahla.Domain/Specifications/AndNotSpecification.cs @@ -0,0 +1,14 @@ +using Bahla.Domain.Entities.Base; +using Bahla.Domain.Specifications.Base; + +namespace Bahla.Domain.Specifications +{ + public sealed class AndNotSpecification(ISpecification left, ISpecification right) : CompositeSpecification where T : class + { + private readonly ISpecification _left = left; + private readonly ISpecification _right = right; + + public override bool IsSatisfiedBy(T entity) + => _left.IsSatisfiedBy(entity) && !_right.IsSatisfiedBy(entity); + } +} diff --git a/src/Bahla.Domain/Specifications/AndSpecification.cs b/src/Bahla.Domain/Specifications/AndSpecification.cs new file mode 100644 index 0000000..1840fb0 --- /dev/null +++ b/src/Bahla.Domain/Specifications/AndSpecification.cs @@ -0,0 +1,12 @@ +using Bahla.Domain.Specifications.Base; + +namespace Bahla.Domain.Specifications +{ + public sealed class AndSpecification(ISpecification left, ISpecification right) : CompositeSpecification where T : class + { + private readonly ISpecification _left = left; + private readonly ISpecification _right = right; + public override bool IsSatisfiedBy(T entity) + => _left.IsSatisfiedBy(entity) && _right.IsSatisfiedBy(entity); + } +} diff --git a/src/Bahla.Domain/Specifications/Base/CompositeSpecification.cs b/src/Bahla.Domain/Specifications/Base/CompositeSpecification.cs new file mode 100644 index 0000000..e229eda --- /dev/null +++ b/src/Bahla.Domain/Specifications/Base/CompositeSpecification.cs @@ -0,0 +1,17 @@ +namespace Bahla.Domain.Specifications.Base +{ + public abstract class CompositeSpecification : ISpecification where T : class + { + public abstract bool IsSatisfiedBy(T entity); + public ISpecification And(ISpecification other) + => new AndSpecification(this, other); + public ISpecification AndNot(ISpecification other) + => new AndNotSpecification(this, other); + public ISpecification Not() + => new NotSpecification(this); + public ISpecification Or(ISpecification other) + => new OrSpecification(this, other); + public ISpecification OrNot(ISpecification other) + => new OrNotSpecification(this, other); + } +} diff --git a/src/Bahla.Domain/Specifications/Base/ISpecification.cs b/src/Bahla.Domain/Specifications/Base/ISpecification.cs new file mode 100644 index 0000000..bbd42b6 --- /dev/null +++ b/src/Bahla.Domain/Specifications/Base/ISpecification.cs @@ -0,0 +1,13 @@ +namespace Bahla.Domain.Specifications.Base +{ + public interface ISpecification where T : class + { + bool IsSatisfiedBy(T entity); + ISpecification And(ISpecification other); + ISpecification AndNot(ISpecification other); + ISpecification Or(ISpecification other); + ISpecification OrNot(ISpecification other); + ISpecification Not(); + + } +} diff --git a/src/Bahla.Domain/Specifications/NotSpecification.cs b/src/Bahla.Domain/Specifications/NotSpecification.cs new file mode 100644 index 0000000..3324370 --- /dev/null +++ b/src/Bahla.Domain/Specifications/NotSpecification.cs @@ -0,0 +1,12 @@ +using Bahla.Domain.Entities.Base; +using Bahla.Domain.Specifications.Base; + +namespace Bahla.Domain.Specifications +{ + public sealed class NotSpecification(ISpecification other) : CompositeSpecification where T : class + { + private readonly ISpecification _other = other; + public override bool IsSatisfiedBy(T entity) + => !_other.IsSatisfiedBy(entity); + } +} diff --git a/src/Bahla.Domain/Specifications/OrNotSpecification.cs b/src/Bahla.Domain/Specifications/OrNotSpecification.cs new file mode 100644 index 0000000..23d7a6a --- /dev/null +++ b/src/Bahla.Domain/Specifications/OrNotSpecification.cs @@ -0,0 +1,14 @@ +using Bahla.Domain.Entities.Base; +using Bahla.Domain.Specifications.Base; + +namespace Bahla.Domain.Specifications +{ + public sealed class OrNotSpecification(ISpecification left, ISpecification right) : CompositeSpecification where T : class + { + private readonly ISpecification _left = left; + private readonly ISpecification _right = right; + + public override bool IsSatisfiedBy(T entity) + => _left.IsSatisfiedBy(entity) || !_right.IsSatisfiedBy(entity); + } +} diff --git a/src/Bahla.Domain/Specifications/OrSpecification.cs b/src/Bahla.Domain/Specifications/OrSpecification.cs new file mode 100644 index 0000000..3de8938 --- /dev/null +++ b/src/Bahla.Domain/Specifications/OrSpecification.cs @@ -0,0 +1,13 @@ +using Bahla.Domain.Specifications.Base; + +namespace Bahla.Domain.Specifications +{ + public sealed class OrSpecification(ISpecification left, ISpecification right) : CompositeSpecification where T : class + { + private readonly ISpecification _left = left; + private readonly ISpecification _right = right; + + public override bool IsSatisfiedBy(T entity) + => _left.IsSatisfiedBy(entity) || _right.IsSatisfiedBy(entity); + } +} diff --git a/src/Bahla.Domain/Types/Identifier.cs b/src/Bahla.Domain/Types/Identifier.cs new file mode 100644 index 0000000..fc3abe4 --- /dev/null +++ b/src/Bahla.Domain/Types/Identifier.cs @@ -0,0 +1,12 @@ +namespace Bahla.Domain.Types +{ + public readonly record struct Identifier(Guid UUID) : IComparable + { + public static implicit operator Identifier(Guid uuid) => new(uuid); + public static implicit operator Identifier(string uuid) => new(Guid.Parse(uuid)); + public static Identifier Generate => Guid.NewGuid(); + public override string ToString() => UUID.ToString(); + public int CompareTo(Identifier other) + => UUID.CompareTo(other.UUID); + } +} diff --git a/src/Bahla.Persistence/Bahla.Persistence.csproj b/src/Bahla.Persistence/Bahla.Persistence.csproj new file mode 100644 index 0000000..675a71d --- /dev/null +++ b/src/Bahla.Persistence/Bahla.Persistence.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/src/Bahla.Persistence/Class1.cs b/src/Bahla.Persistence/Class1.cs new file mode 100644 index 0000000..5728cf0 --- /dev/null +++ b/src/Bahla.Persistence/Class1.cs @@ -0,0 +1,7 @@ +namespace Bahla.Persistence +{ + public class Class1 + { + + } +} diff --git a/src/Bahla.Tests/Bahla.Tests.csproj b/src/Bahla.Tests/Bahla.Tests.csproj index 8b5797e..d190730 100644 --- a/src/Bahla.Tests/Bahla.Tests.csproj +++ b/src/Bahla.Tests/Bahla.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -17,8 +17,17 @@ + + + + + + + + + diff --git a/src/Bahla.Tests/Builders/BuilderTests.cs b/src/Bahla.Tests/Builders/BuilderTests.cs new file mode 100644 index 0000000..225ce22 --- /dev/null +++ b/src/Bahla.Tests/Builders/BuilderTests.cs @@ -0,0 +1,44 @@ +using Bahla.Domain.Entities; +using Bahla.Domain.Exceptions; + +namespace Bahla.Tests.Builders +{ + internal class BuilderTests + { + + [Test, Description("Test to create a role through builder.")] + public void TestRoleBuilder() + { + var role = Role.Builder + .WithName("Administrator") + .WithValueMask(1 << 0) + .Build(); + + var clonedRole = Role.Builder + .Clone(role) + .Build(); + + Assert.Multiple(() => + { + Assert.That(role.RoleName, Is.EqualTo(clonedRole.RoleName)); + Assert.That(role.ValueMask, Is.EqualTo(clonedRole.ValueMask)); + Assert.That(role.UUID, Is.EqualTo(clonedRole.UUID)); + + Assert.That(clonedRole, Is.Not.SameAs(role)); + }); + } + + [Test, Description("Test to create a role with a empty or null name.")] + public void TestRoleBuilderBadName() + { + Assert.Throws(() => + { + Role.Builder + .WithName("") + .WithValueMask(1 << 0) + .Build(); + + }); + } + } +} diff --git a/src/Bahla.Tests/Types/TypeIdentifierTests.cs b/src/Bahla.Tests/Types/TypeIdentifierTests.cs new file mode 100644 index 0000000..2099bf1 --- /dev/null +++ b/src/Bahla.Tests/Types/TypeIdentifierTests.cs @@ -0,0 +1,39 @@ +using Bahla.Domain.Types; + +namespace Bahla.Tests.Entities +{ + internal class TypeIdentifierTests + { + const string StringIdentifier = "316f94a7-fb3b-44f0-abe1-88c2a3c99109"; + const string StringIdentifier2 = "2c304fa1-19a4-432d-a729-73bdd45586ed"; + + [Test] + public void TestAssignmentGuid() + { + Identifier firstIdentifier = Guid.Parse(StringIdentifier); + Identifier secondIdentifier = Guid.Parse(StringIdentifier2); + + Assert.Multiple(() => + { + Assert.That(firstIdentifier, Is.Not.EqualTo(secondIdentifier)); + Assert.That(firstIdentifier.ToString(), Is.EqualTo(StringIdentifier)); + Assert.That(secondIdentifier.ToString(), Is.EqualTo(StringIdentifier2)); + }); + } + + [Test] + public void TestAssignmentString() + { + Identifier firstIdentifier = StringIdentifier; + Identifier secondIdentifier = StringIdentifier2; + + Assert.Multiple(() => + { + Assert.That(firstIdentifier, Is.Not.EqualTo(secondIdentifier)); + Assert.That(firstIdentifier.ToString(), Is.EqualTo(StringIdentifier)); + Assert.That(secondIdentifier.ToString(), Is.EqualTo(StringIdentifier2)); + }); + } + + } +} diff --git a/src/Bahla.Tests/UnitTest1.cs b/src/Bahla.Tests/UnitTest1.cs deleted file mode 100644 index 9d13716..0000000 --- a/src/Bahla.Tests/UnitTest1.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Bahla.Tests -{ - public class Tests - { - [SetUp] - public void Setup() - { - } - - [Test] - public void Test1() - { - Assert.Pass(); - } - } -} \ No newline at end of file