Skip to content

Commit 9443551

Browse files
Merge branch 'main' into feature/units
2 parents c88583e + 60b3d8e commit 9443551

File tree

4 files changed

+194
-43
lines changed

4 files changed

+194
-43
lines changed

Directory.Build.props

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
<!--GLOBAL-->
44
<PropertyGroup>
5-
<Version>12.1.1</Version>
6-
<PackageVersion>12.1.1</PackageVersion>
7-
<AssemblyVersion>12.1.1</AssemblyVersion>
5+
<Version>12.2.0</Version>
6+
<PackageVersion>12.2.0</PackageVersion>
7+
<AssemblyVersion>12.2.0</AssemblyVersion>
88
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
99
<LangVersion>13</LangVersion>
1010
<Nullable>enable</Nullable>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2020-2025 ONIXLabs
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
namespace OnixLabs.Core.UnitTests.Data;
16+
17+
public sealed class Mutable
18+
{
19+
public required int Value { get; set; }
20+
}

OnixLabs.Core.UnitTests/ObjectExtensionTests.cs

Lines changed: 88 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,34 +20,31 @@ namespace OnixLabs.Core.UnitTests;
2020

2121
public sealed class ObjectExtensionTests
2222
{
23-
[Theory(DisplayName = "IsWithinRangeInclusive should produce the expected result")]
24-
[InlineData(2, 1, 3, true)]
25-
[InlineData(1, 1, 3, true)]
26-
[InlineData(3, 1, 3, true)]
27-
[InlineData(0, 1, 3, false)]
28-
[InlineData(4, 1, 3, false)]
29-
public void IsWithinRangeInclusiveShouldProduceExpectedResult(int value, int min, int max, bool expected)
23+
[Fact(DisplayName = "Apply should produce the expected result (reference type)")]
24+
public void ApplyShouldProduceExpectedResultReferenceType()
3025
{
26+
// Given
27+
Mutable value = new() { Value = 123 };
28+
3129
// When
32-
bool actual = value.IsWithinRangeInclusive(min, max);
30+
Mutable result = value.Apply(it => it.Value = 456);
3331

3432
// Then
35-
Assert.Equal(expected, actual);
33+
Assert.Equal(456, value.Value);
34+
Assert.Same(value, result);
3635
}
3736

38-
[Theory(DisplayName = "IsWithinRangeExclusive should produce the expected result")]
39-
[InlineData(2, 1, 3, true)]
40-
[InlineData(1, 1, 3, false)]
41-
[InlineData(3, 1, 3, false)]
42-
[InlineData(0, 1, 3, false)]
43-
[InlineData(4, 1, 3, false)]
44-
public void IsWithinRangeExclusiveShouldProduceExpectedResult(int value, int min, int max, bool expected)
37+
[Fact(DisplayName = "Apply should produce the expected result (value type)")]
38+
public void ApplyShouldProduceExpectedResultValueType()
4539
{
40+
// Given
41+
int value = 123;
42+
4643
// When
47-
bool actual = value.IsWithinRangeExclusive(min, max);
44+
value = value.Apply(it => it * 2);
4845

4946
// Then
50-
Assert.Equal(expected, actual);
47+
Assert.Equal(246, value);
5148
}
5249

5350
[Fact(DisplayName = "CompareToObject should produce zero if the current IComparable<T> is equal to the specified object.")]
@@ -112,6 +109,51 @@ public void CompareToObjectShouldThrowArgumentExceptionIfSpecifiedObjectIsOfInco
112109
Assert.Equal("Object must be of type System.Int32 (Parameter 'right')", exception.Message);
113110
}
114111

112+
[Theory(DisplayName = "IsWithinRangeInclusive should produce the expected result")]
113+
[InlineData(2, 1, 3, true)]
114+
[InlineData(1, 1, 3, true)]
115+
[InlineData(3, 1, 3, true)]
116+
[InlineData(0, 1, 3, false)]
117+
[InlineData(4, 1, 3, false)]
118+
public void IsWithinRangeInclusiveShouldProduceExpectedResult(int value, int min, int max, bool expected)
119+
{
120+
// When
121+
bool actual = value.IsWithinRangeInclusive(min, max);
122+
123+
// Then
124+
Assert.Equal(expected, actual);
125+
}
126+
127+
[Theory(DisplayName = "IsWithinRangeExclusive should produce the expected result")]
128+
[InlineData(2, 1, 3, true)]
129+
[InlineData(1, 1, 3, false)]
130+
[InlineData(3, 1, 3, false)]
131+
[InlineData(0, 1, 3, false)]
132+
[InlineData(4, 1, 3, false)]
133+
public void IsWithinRangeExclusiveShouldProduceExpectedResult(int value, int min, int max, bool expected)
134+
{
135+
// When
136+
bool actual = value.IsWithinRangeExclusive(min, max);
137+
138+
// Then
139+
Assert.Equal(expected, actual);
140+
}
141+
142+
[Fact(DisplayName = "Let should produce the expected result")]
143+
public void LetShouldProduceExpectedResult()
144+
{
145+
// Given
146+
const string value = "123";
147+
148+
// When
149+
int result = value
150+
.Let(int.Parse)
151+
.Let(it => it * 2);
152+
153+
// Then
154+
Assert.Equal(246, result);
155+
}
156+
115157
[Fact(DisplayName = "ToRecordString should produce null when the object is null")]
116158
public void ToRecordStringShouldProduceNullWhenObjectIsNull()
117159
{
@@ -360,4 +402,32 @@ public async Task ToSuccessAsyncShouldProduceTheExpectedResult()
360402
Success<string> success = Assert.IsType<Success<string>>(result);
361403
Assert.Equal(expected, success.Value);
362404
}
405+
406+
[Fact(DisplayName = "TryGetNonNull should produce the expected result (true)")]
407+
public void TryGetNotNullShouldProduceExpectedResultTrue()
408+
{
409+
// Given
410+
const string? value = "Hello, World!";
411+
412+
// When
413+
bool result = value.TryGetNonNull(out string output);
414+
415+
// Then
416+
Assert.True(result);
417+
Assert.NotNull(output);
418+
}
419+
420+
[Fact(DisplayName = "TryGetNonNull should produce the expected result (false)")]
421+
public void TryGetNotNullShouldProduceExpectedResultFalse()
422+
{
423+
// Given
424+
const string? value = null;
425+
426+
// When
427+
bool result = value.TryGetNonNull(out string? output);
428+
429+
// Then
430+
Assert.False(result);
431+
Assert.Null(output);
432+
}
363433
}

OnixLabs.Core/Extensions.Object.cs

Lines changed: 83 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using System.Collections;
1717
using System.Collections.Generic;
1818
using System.ComponentModel;
19+
using System.Diagnostics.CodeAnalysis;
1920
using System.Reflection;
2021
using System.Text;
2122
using System.Threading;
@@ -40,32 +41,31 @@ public static class ObjectExtensions
4041
private const string ObjectPropertyAssignment = " = ";
4142

4243
/// <summary>
43-
/// Determines whether the current <see cref="IComparable{T}"/> value falls within range, inclusive of the specified minimum and maximum values.
44+
/// Calls the specified <see cref="Action{T}"/> with the current <paramref name="value"/>.
4445
/// </summary>
45-
/// <param name="value">The value to test.</param>
46-
/// <param name="min">The inclusive minimum value.</param>
47-
/// <param name="max">The inclusive maximum value.</param>
48-
/// <typeparam name="T">The underlying <see cref="IComparable{T}"/> type.</typeparam>
49-
/// <returns>
50-
/// Returns <see langword="true"/> if the current <see cref="IComparable{T}"/> value falls within range,
51-
/// inclusive of the specified minimum and maximum values; otherwise, <see langword="false"/>.
52-
/// </returns>
53-
public static bool IsWithinRangeInclusive<T>(this T value, T min, T max) where T : IComparable<T> =>
54-
value.CompareTo(min) is 0 or 1 && value.CompareTo(max) is 0 or -1;
46+
/// <param name="value">The value to pass to the specified <see cref="Action{T}"/>.</param>
47+
/// <param name="action">The action into which the current <paramref name="value"/> will be passed.</param>
48+
/// <typeparam name="T">The underlying type of the current value.</typeparam>
49+
/// <returns>Returns the current <paramref name="value"/> once the specified <see cref="Action{T}"/> has been executed.</returns>
50+
public static T Apply<T>(this T value, Action<T> action) where T : class
51+
{
52+
RequireNotNull(action, "Action must not be null.", nameof(action));
53+
action(value);
54+
return value;
55+
}
5556

5657
/// <summary>
57-
/// Determines whether the current <see cref="IComparable{T}"/> value falls within range, exclusive of the specified minimum and maximum values.
58+
/// Calls the specified <see cref="Func{T, TResult}"/> with the current <paramref name="value"/>.
5859
/// </summary>
59-
/// <param name="value">The value to test.</param>
60-
/// <param name="min">The exclusive minimum value.</param>
61-
/// <param name="max">The exclusive maximum value.</param>
62-
/// <typeparam name="T">The underlying <see cref="IComparable{T}"/> type.</typeparam>
63-
/// <returns>
64-
/// Returns <see langword="true"/> if the current <see cref="IComparable{T}"/> value falls within range,
65-
/// exclusive of the specified minimum and maximum values; otherwise, <see langword="false"/>.
66-
/// </returns>
67-
public static bool IsWithinRangeExclusive<T>(this T value, T min, T max) where T : IComparable<T> =>
68-
value.CompareTo(min) is 1 && value.CompareTo(max) is -1;
60+
/// <param name="value">The value to pass to the specified <see cref="Func{T, TResult}"/>.</param>
61+
/// <param name="function">The function into which the current <paramref name="value"/> will be passed.</param>
62+
/// <typeparam name="T">The underlying type of the current value.</typeparam>
63+
/// <returns>Returns the result of the specified <see cref="Func{T, TResult}"/>.</returns>
64+
public static T Apply<T>(this T value, Func<T, T> function) where T : struct
65+
{
66+
RequireNotNull(function, "Function must not be null.", nameof(function));
67+
return function(value);
68+
}
6969

7070
/// <summary>
7171
/// Compares the current <typeparamref name="T"/> instance with the specified <typeparamref name="T"/> instance.
@@ -107,6 +107,48 @@ public static int CompareToNullable<T>(this T left, T? right) where T : struct,
107107
_ => throw new ArgumentException($"Object must be of type {typeof(T).FullName}", nameof(right))
108108
};
109109

110+
/// <summary>
111+
/// Determines whether the current <see cref="IComparable{T}"/> value falls within range, inclusive of the specified minimum and maximum values.
112+
/// </summary>
113+
/// <param name="value">The value to test.</param>
114+
/// <param name="min">The inclusive minimum value.</param>
115+
/// <param name="max">The inclusive maximum value.</param>
116+
/// <typeparam name="T">The underlying <see cref="IComparable{T}"/> type.</typeparam>
117+
/// <returns>
118+
/// Returns <see langword="true"/> if the current <see cref="IComparable{T}"/> value falls within range,
119+
/// inclusive of the specified minimum and maximum values; otherwise, <see langword="false"/>.
120+
/// </returns>
121+
public static bool IsWithinRangeInclusive<T>(this T value, T min, T max) where T : IComparable<T> =>
122+
value.CompareTo(min) is 0 or 1 && value.CompareTo(max) is 0 or -1;
123+
124+
/// <summary>
125+
/// Determines whether the current <see cref="IComparable{T}"/> value falls within range, exclusive of the specified minimum and maximum values.
126+
/// </summary>
127+
/// <param name="value">The value to test.</param>
128+
/// <param name="min">The exclusive minimum value.</param>
129+
/// <param name="max">The exclusive maximum value.</param>
130+
/// <typeparam name="T">The underlying <see cref="IComparable{T}"/> type.</typeparam>
131+
/// <returns>
132+
/// Returns <see langword="true"/> if the current <see cref="IComparable{T}"/> value falls within range,
133+
/// exclusive of the specified minimum and maximum values; otherwise, <see langword="false"/>.
134+
/// </returns>
135+
public static bool IsWithinRangeExclusive<T>(this T value, T min, T max) where T : IComparable<T> =>
136+
value.CompareTo(min) is 1 && value.CompareTo(max) is -1;
137+
138+
/// <summary>
139+
/// Calls the specified <see cref="Func{T, TResult}"/> with the current <paramref name="value"/> as its argument and returns the result.
140+
/// </summary>
141+
/// <param name="value">The value to pass to the specified <see cref="Func{T, TResult}"/>.</param>
142+
/// <param name="function">The function into which the current <paramref name="value"/> will be passed.</param>
143+
/// <typeparam name="TSource">The underlying type of the current value.</typeparam>
144+
/// <typeparam name="TResult">The underlying type of the result value.</typeparam>
145+
/// <returns>Returns the result of the specified <see cref="Func{T, TResult}"/>.</returns>
146+
public static TResult Let<TSource, TResult>(this TSource value, Func<TSource, TResult> function)
147+
{
148+
RequireNotNull(function, "Function must not be null.", nameof(function));
149+
return function(value);
150+
}
151+
110152
/// <summary>
111153
/// Gets a record-like <see cref="String"/> representation of the current <see cref="Object"/> instance.
112154
/// <remarks>This method is designed specifically for record-like objects and may produce undesirable results when applied to primitive-like objects.</remarks>
@@ -226,4 +268,23 @@ public static async Task<Optional<T>> ToOptionalAsync<T>(this Task<T?> value, Ca
226268
/// <returns>Returns a <see cref="Success{T}"/> representation of the current <see cref="Object"/>.</returns>
227269
public static async Task<Success<T>> ToSuccessAsync<T>(this Task<T> value, CancellationToken token = default) =>
228270
Result<T>.Success(await value.WaitAsync(token).ConfigureAwait(false));
271+
272+
/// <summary>
273+
/// Attempts to extract a non-null value from the current nullable <see cref="Object"/>.
274+
/// </summary>
275+
/// <param name="value">The current nullable <see cref="Object"/> to test for nullability.</param>
276+
/// <param name="result">The non-null value when this method returns <see langword="true"/>; otherwise, <see langword="null"/>.</param>
277+
/// <typeparam name="T">The underlying type of the value.</typeparam>
278+
/// <returns>Returns <see langword="true"/> if the current nullable <see cref="Object"/> is not null; otherwise, <see langword="false"/>.</returns>
279+
public static bool TryGetNonNull<T>(this T? value, [NotNullWhen(true)] out T result)
280+
{
281+
if (value is null)
282+
{
283+
result = default!;
284+
return false;
285+
}
286+
287+
result = value;
288+
return true;
289+
}
229290
}

0 commit comments

Comments
 (0)