Skip to content

Commit

Permalink
feature/specification-pattern (#94)
Browse files Browse the repository at this point in the history
Implemented the Specification pattern.
  • Loading branch information
MrMatthewLayton authored Jan 14, 2025
1 parent 7a905b5 commit 7a6aa9f
Show file tree
Hide file tree
Showing 13 changed files with 617 additions and 17 deletions.
6 changes: 3 additions & 3 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
<NeutralLanguage>en</NeutralLanguage>
<Copyright>Copyright © ONIXLabs 2020</Copyright>
<RepositoryUrl>https://github.com/onix-labs/onixlabs-dotnet</RepositoryUrl>
<Version>11.2.0</Version>
<PackageVersion>11.2.0</PackageVersion>
<AssemblyVersion>11.2.0</AssemblyVersion>
<Version>11.3.0</Version>
<PackageVersion>11.3.0</PackageVersion>
<AssemblyVersion>11.3.0</AssemblyVersion>
</PropertyGroup>
</Project>
24 changes: 24 additions & 0 deletions OnixLabs.Core.UnitTests.Data/Location.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2020 ONIXLabs
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace OnixLabs.Core.UnitTests.Data;

public sealed record Location(string City, string Country)
{
public static readonly Location London = new("London", "England");
public static readonly Location Paris = new("Paris", "France");
public static readonly Location Lisbon = new("Lisbon", "Postugal");
public static readonly Location Berlin = new("Berlin", "Germany");
public static readonly Location Brussels = new("Brussels", "Belgium");
}
26 changes: 26 additions & 0 deletions OnixLabs.Core.UnitTests.Data/Person.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2020 ONIXLabs
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Collections.Generic;

namespace OnixLabs.Core.UnitTests.Data;

public sealed record Person(string Name, int Age, IEnumerable<Location> Locations)
{
public static readonly Person Alice = new("Alice", 12, [Location.London, Location.Paris]);
public static readonly Person Bob = new("Bob", 23, [Location.Lisbon, Location.London]);
public static readonly Person Charlie = new("Charlie", 34, [Location.Berlin, Location.Brussels]);

public static readonly IEnumerable<Person> People = [Alice, Bob, Charlie];
}
37 changes: 37 additions & 0 deletions OnixLabs.Core.UnitTests.Data/PersonSpecifications.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2020 ONIXLabs
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Linq;
using System.Linq.Expressions;

namespace OnixLabs.Core.UnitTests.Data;

public class PersonSpecification(Expression<Func<Person, bool>> expression) :
CriteriaSpecification<Person>(expression);

public sealed class PersonNameEqualsSpecification(string name) :
PersonSpecification(person => person.Name == name);

public sealed class PersonNameStartsWithSpecification(string name) :
PersonSpecification(person => person.Name.StartsWith(name));

public sealed class PersonOlderThanSpecification(int age) :
PersonSpecification(person => person.Age > age);

public sealed class PersonHasLocationSpecification(Location location) :
PersonSpecification(person => person.Locations.Contains(location));

public sealed class PersonHasLocationCitySpecification(string city) :
PersonSpecification(person => person.Locations.Any(location => location.City == city));
2 changes: 1 addition & 1 deletion OnixLabs.Core.UnitTests/OptionalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ public void OptionalNoneGetValueOrDefaultShouldProduceExpectedResult()

// Then
Assert.Equal(0, actualNumber);
Assert.Equal(null, actualText);
Assert.Null(actualText);
}

[Fact(DisplayName = "Optional Some.GetValueOrDefault with default value should produce the expected result.")]
Expand Down
175 changes: 175 additions & 0 deletions OnixLabs.Core.UnitTests/SpecificationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Copyright 2020 ONIXLabs
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Collections.Generic;
using System.Linq;
using OnixLabs.Core.Linq;
using OnixLabs.Core.UnitTests.Data;
using Xunit;

namespace OnixLabs.Core.UnitTests;

public sealed class SpecificationTests
{
[Fact(DisplayName = "PersonNameEqualsSpecification should return the expected result")]
public void PersonNameEqualsSpecificationShouldReturnExpectedResult()
{
// Given
PersonNameEqualsSpecification specification = new("Alice");

// When
IEnumerable<Person> result = Person.People.Where(specification).ToList();

// Then
Assert.Single(result);
Assert.Contains(Person.Alice, result);
}

[Fact(DisplayName = "PersonNameStartsWithSpecification should return the expected result")]
public void PersonNameStartsWithSpecificationShouldReturnExpectedResult()
{
// Given
PersonNameStartsWithSpecification specification = new("A");

// When
IEnumerable<Person> result = Person.People.Where(specification).ToList();

// Then
Assert.Single(result);
Assert.Contains(Person.Alice, result);
}

[Fact(DisplayName = "PersonOlderThanSpecification should return the expected result")]
public void PersonOlderThanSpecificationShouldReturnExpectedResult()
{
// Given
PersonOlderThanSpecification specification = new(20);

// When
IEnumerable<Person> result = Person.People.Where(specification).ToList();

// Then
Assert.Equal(2, IEnumerableExtensions.Count(result));
Assert.Contains(Person.Bob, result);
Assert.Contains(Person.Charlie, result);
}

[Fact(DisplayName = "PersonHasLocationSpecification should return the expected result")]
public void PersonHasLocationSpecificationShouldReturnExpectedResult()
{
// Given
PersonHasLocationSpecification specification = new(Location.London);

// When
IEnumerable<Person> result = Person.People.Where(specification).ToList();

// Then
Assert.Equal(2, IEnumerableExtensions.Count(result));
Assert.Contains(Person.Alice, result);
Assert.Contains(Person.Bob, result);
}

[Fact(DisplayName = "PersonHasLocationCitySpecification should return the expected result")]
public void PersonHasLocationCitySpecificationShouldReturnExpectedResult()
{
// Given
PersonHasLocationCitySpecification specification = new(Location.London.City);

// When
IEnumerable<Person> result = Person.People.Where(specification).ToList();

// Then
Assert.Equal(2, IEnumerableExtensions.Count(result));
Assert.Contains(Person.Alice, result);
Assert.Contains(Person.Bob, result);
}

[Fact(DisplayName = "PersonSpecification.And should return the expected result")]
public void PersonSpecificationAndShouldReturnExpectedResult()
{
// Given
Specification<Person> specification = PersonSpecification.And(
new PersonOlderThanSpecification(20),
new PersonHasLocationCitySpecification("London")
);

// When
IEnumerable<Person> result = Person.People.Where(specification).ToList();

// Then
Assert.Single(result);
Assert.Contains(Person.Bob, result);
}

[Fact(DisplayName = "PersonSpecification.And should return true for an empty collection")]
public void PersonSpecificationAndShouldReturnTrueForEmptyCollection()
{
// Given
Specification<Person> specification = PersonSpecification.And();

// When
bool result = specification.IsSatisfiedBy(Person.Alice);

// Then
Assert.True(result);
}

[Fact(DisplayName = "PersonSpecification.Or should return the expected result")]
public void PersonSpecificationOrShouldReturnExpectedResult()
{
// Given
Specification<Person> specification = PersonSpecification.Or(
new PersonNameStartsWithSpecification("A"),
new PersonHasLocationCitySpecification("Lisbon")
);

// When
IEnumerable<Person> result = Person.People.Where(specification).ToList();

// Then
Assert.Equal(2, result.Count());
Assert.Contains(Person.Alice, result);
Assert.Contains(Person.Bob, result);
}

[Fact(DisplayName = "PersonSpecification.Or should return false for an empty collection")]
public void PersonSpecificationOrShouldReturnFalseForEmptyCollection()
{
// Given
Specification<Person> specification = PersonSpecification.Or();

// When
bool result = specification.IsSatisfiedBy(Person.Alice);

// Then
Assert.False(result);
}

[Fact(DisplayName = "PersonSpecification.Not should return the expected result")]
public void PersonSpecificationNotShouldReturnExpectedResult()
{
// Given
Specification<Person> specification = PersonSpecification.Or(
new PersonNameStartsWithSpecification("A"),
new PersonHasLocationCitySpecification("Lisbon")
);

// When
IEnumerable<Person> result = Person.People.WhereNot(specification).ToList();

// Then
Assert.Single(result);
Assert.Contains(Person.Charlie, result);
}
}
2 changes: 1 addition & 1 deletion OnixLabs.Core/Extensions.DateTime.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020-2024 ONIXLabs
// Copyright 2020 ONIXLabs
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down
4 changes: 3 additions & 1 deletion OnixLabs.Core/IBinaryConvertible.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System;

namespace OnixLabs.Core;

/// <summary>
/// Defines a type that is convertible to an instance of <see cref="T:Byte[]"/>.
/// Defines a type that is convertible to a <see cref="ReadOnlySpan{T}"/>, or <see cref="ReadOnlyMemory{T}"/>.
/// </summary>
public interface IBinaryConvertible : ISpanBinaryConvertible, IMemoryBinaryConvertible;
2 changes: 1 addition & 1 deletion OnixLabs.Core/IMemoryBinaryConvertible.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020-2024 ONIXLabs
// Copyright 2020 ONIXLabs
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down
Loading

0 comments on commit 7a6aa9f

Please sign in to comment.