This commit is contained in:
Kyle Ratti 2023-08-01 22:12:54 -04:00
parent 6791f3b016
commit 0e7e161327
No known key found for this signature in database
GPG Key ID: 067C5D2D7C62E0F8
23 changed files with 254 additions and 234 deletions

View File

@ -2,19 +2,26 @@ name: ci
on: [push] on: [push]
# Allow one run of this workflow per branch and cancel existing runs if triggered again
concurrency:
group: fruityfoundation-ci-${{ github.ref_name }}
cancel-in-progress: true
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
dotnet-version: ['6.0.x'] dotnet-version: ['7.0.x', '6.0.x']
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-dotnet@v1 - uses: actions/setup-dotnet@v1
with: with:
dotnet-version: "6.0.x" dotnet-version: |
6.0.x
7.0.x
- name: Install dependencies - name: Install dependencies
run: dotnet restore run: dotnet restore
- name: Build - name: Build

2
.github/workflows/publish-release.yml vendored Executable file → Normal file
View File

@ -21,7 +21,7 @@ jobs:
fetch-depth: '0' # Load entire history fetch-depth: '0' # Load entire history
- uses: actions/setup-dotnet@v3 - uses: actions/setup-dotnet@v3
with: with:
dotnet-version: '6.x' dotnet-version: '7.x'
- run: dotnet tool restore - run: dotnet tool restore
- name: Generate Version - name: Generate Version
id: generate-version id: generate-version

4
Base.Tests/Base.Tests.csproj Executable file → Normal file
View File

@ -1,10 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -21,4 +22,5 @@
<ProjectReference Include="..\Base\Base.csproj" /> <ProjectReference Include="..\Base\Base.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,45 +0,0 @@
using System.Linq;
using FruityFoundation.Base.Extensions;
using NUnit.Framework;
namespace Base.Tests.Extensions;
public class EnumerableExtensionTests
{
[TestCase(new object[] { 0, 1, 2 }, true, new object[] { 85 }, ExpectedResult = new object[] { 0, 1, 2, 85 })]
[TestCase(new object[] { "hi" }, false, new object[] { "there" }, ExpectedResult = new object[] { "hi" })]
public object[] TestConditionalConcat(object[] input, bool isConditionValid, object[] second) =>
input.ConditionalConcat(isConditionValid, second).ToArray();
[TestCase(new object[] { 0, 1, 2 }, true, 85, ExpectedResult = new object[0])]
[TestCase(new object[] { "hi", "there" }, false, "there", ExpectedResult = new object[] { "hi", "there" })]
public object[] TestConditionalWhere(object[] input, bool isConditionValid, object valueToKeep) =>
input.ConditionalWhere(isConditionValid, x => x.Equals(valueToKeep)).ToArray();
[Test]
public void TestChooseWithRefType()
{
var input = new [] { "one", null, "two" };
var result = input.Choose(x => x).ToArray();
Assert.That(result.GetType(), Is.EqualTo(typeof(string[])));
Assert.That(result.Length, Is.EqualTo(2));
Assert.That(result[0], Is.EqualTo("one"));
Assert.That(result[1], Is.EqualTo("two"));
}
[Test]
public void TestChooseWithValueType()
{
var input = new int?[] { 1, null, 2 };
var result = input.Choose(x => x).ToArray();
Assert.That(result.GetType(), Is.EqualTo(typeof(int[])));
Assert.That(result.GetType(), Is.Not.EqualTo(typeof(int?[])));
Assert.That(result.Length, Is.EqualTo(2));
Assert.That(result[0], Is.EqualTo(1));
Assert.That(result[1], Is.EqualTo(2));
}
}

View File

@ -1,31 +0,0 @@
using System;
using FruityFoundation.Base.Extensions;
using FruityFoundation.Base.Structures;
using NUnit.Framework;
namespace Base.Tests.Extensions;
public class MaybeExtensionTests
{
[Test]
public void EnumerableFirstOrEmptyTests()
{
Assert.AreEqual(Maybe<string>.Empty(), Array.Empty<string>().FirstOrEmpty());
Assert.AreEqual(Maybe<string>.Create("banana"), new[] { "banana" }.FirstOrEmpty());
}
[Test]
public void TestToMaybe()
{
Assert.AreEqual(Maybe<int>.Empty(), Maybe<int>.Empty());
Assert.AreEqual(Maybe<string>.Create("banana"), "banana".ToMaybe());
Assert.AreNotEqual(Maybe<int>.Create(293921), Maybe<int>.Create(2));
}
[Test]
public void MaybeNullableTests()
{
Assert.IsNull(Maybe<int>.Empty().ToNullable());
Assert.IsNull(Maybe<int>.Create(0, _ => false).ToNullable());
}
}

View File

@ -1,24 +0,0 @@
using FruityFoundation.Base.Extensions;
using FruityFoundation.Base.Structures;
using NUnit.Framework;
namespace Base.Tests.Extensions;
public class NullableExtensionTests
{
[Test]
public void TestNullableStructOfNullToMaybe() =>
Assert.AreEqual(Maybe<int>.Empty(), ((int?)null).ToMaybe());
[Test]
public void TestNullableStructOfValueToMaybe() =>
Assert.AreEqual(Maybe<int>.Create(25), ((int?)25).ToMaybe());
[Test]
public void TestNullableRefOfNullToMaybe() =>
Assert.AreEqual(Maybe<object>.Empty(), ((object?)null).ToMaybe());
[Test]
public void TestNullableRefOfValueToMaybe() =>
Assert.AreEqual(Maybe<object>.Create(new {}), ((object?)new {}).ToMaybe());
}

View File

@ -0,0 +1,23 @@
using System.Linq;
using FruityFoundation.Base.Structures;
using NUnit.Framework;
namespace Base.Tests.Structures;
public class EnumerableExtensionTests
{
[TestCase(new object[] { 0, 1, 2 }, true, new object[] { 85 }, ExpectedResult = new object[] { 0, 1, 2, 85 })]
[TestCase(new object[] { "hi" }, false, new object[] { "there" }, ExpectedResult = new object[] { "hi" })]
public object[] TestConditionalConcat(object[] input, bool condition, object[] second) =>
input.ConditionalConcat(condition, second).ToArray();
[TestCase(new object[] { 0, 1, 2 }, true, 3, ExpectedResult = new object[] { 0, 1, 2, 3 })]
[TestCase(new object[] { 0, 1, 2 }, false, 3, ExpectedResult = new object[] { 0, 1, 2 })]
public object[] TestConditionalAppend(object[] input, bool condition, object second) =>
input.ConditionalAppend(condition, second).ToArray();
[TestCase(new object[] { 0, 1, 2 }, true, 85, ExpectedResult = new object[0])]
[TestCase(new object[] { "hi", "there" }, false, "there", ExpectedResult = new object[] { "hi", "there" })]
public object[] TestConditionalWhere(object[] input, bool condition, object valueToKeep) =>
input.ConditionalWhere(condition, x => x.Equals(valueToKeep)).ToArray();
}

View File

@ -0,0 +1,29 @@
using System;
using FruityFoundation.Base.Structures;
using NUnit.Framework;
namespace Base.Tests.Structures;
public class MaybeExtensionTests
{
[Test]
public void EnumerableFirstOrEmptyTests()
{
Assert.AreEqual(Maybe.Empty<string>(), Array.Empty<string>().FirstOrEmpty());
Assert.AreEqual(Maybe.Just<string>("banana"), new[] { "banana" }.FirstOrEmpty());
}
[Test]
public void TestToMaybe()
{
Assert.AreEqual(Maybe.Empty<int>(), Maybe.Empty<int>());
Assert.AreNotEqual(Maybe.Just(293921), Maybe.Just(2));
}
[Test]
public void MaybeNullableTests()
{
Assert.IsNull(Maybe.Empty<int>().ToNullable());
Assert.IsNull(Maybe.Just(0, hasValue: _ => false).ToNullable());
}
}

View File

@ -0,0 +1,15 @@
using FruityFoundation.Base.Structures;
using NUnit.Framework;
namespace Base.Tests.Structures;
public class NullableExtensionTests
{
[Test]
public void TestNullableStructOfNullToMaybe() =>
Assert.AreEqual(Maybe.Empty<int>(), ((int?)null).ToMaybe());
[Test]
public void TestNullableStructOfValueToMaybe() =>
Assert.AreEqual(Maybe.Just(25), ((int?)25).ToMaybe());
}

View File

@ -1,8 +1,7 @@
using FruityFoundation.Base.Extensions; using FruityFoundation.Base.Structures;
using FruityFoundation.Base.Structures;
using NUnit.Framework; using NUnit.Framework;
namespace Base.Tests.Extensions; namespace Base.Tests.Structures;
public class StringExtensionTests public class StringExtensionTests
{ {
@ -20,15 +19,6 @@ public class StringExtensionTests
public bool ContainsIgnoreCaseTests(string haystack, string needle) => public bool ContainsIgnoreCaseTests(string haystack, string needle) =>
haystack.ContainsIgnoreCase(needle); haystack.ContainsIgnoreCase(needle);
[Test]
public void StringToCouldBeTests()
{
#pragma warning disable CS8604
Assert.AreEqual(Maybe<string>.Empty(), (null as string).ToMaybe());
#pragma warning restore CS8604
Assert.AreEqual(Maybe<string>.Create("banana"), "banana".ToMaybe());
}
[Test] [Test]
[TestCase("banana", 1, ExpectedResult = "b")] [TestCase("banana", 1, ExpectedResult = "b")]
[TestCase("This is a longer sentence. I would like it capped at 30 characters.", 30, ExpectedResult = "This is a longer sentence. I w")] [TestCase("This is a longer sentence. I would like it capped at 30 characters.", 30, ExpectedResult = "This is a longer sentence. I w")]

2
Base/Base.csproj Executable file → Normal file
View File

@ -15,6 +15,8 @@
<Version>1.2.2</Version> <Version>1.2.2</Version>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageLicenseFile>LICENSE</PackageLicenseFile> <PackageLicenseFile>LICENSE</PackageLicenseFile>
<AssemblyName>FruityFoundation.Base</AssemblyName>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Include="..\LICENSE" Pack="true" PackagePath="" /> <None Include="..\LICENSE" Pack="true" PackagePath="" />

View File

@ -1,28 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace FruityFoundation.Base.Extensions;
public static class EnumerableExtensions
{
public static IEnumerable<T> ConditionalConcat<T>(this IEnumerable<T> enumerable, bool isConditionValid, IEnumerable<T> second) =>
!isConditionValid ? enumerable : enumerable.Concat(second);
public static IEnumerable<T> ConditionalWhere<T>(this IEnumerable<T> enumerable, bool isConditionValid, Func<T, bool> pred) =>
!isConditionValid ? enumerable : enumerable.Where(pred);
public static IEnumerable<TOutput> Choose<TInput, TOutput>(this IEnumerable<TInput> enumerable, Func<TInput, TOutput?> mapper)
where TOutput : class? =>
enumerable
.Select(mapper)
.Where(x => x is not null)
.Cast<TOutput>();
public static IEnumerable<TOutput> Choose<TInput, TOutput>(this IEnumerable<TInput> enumerable, Func<TInput, TOutput?> mapper)
where TOutput : struct =>
enumerable
.Select(mapper)
.Where(x => x.HasValue)
.Select(x => x!.Value);
}

View File

@ -1,12 +0,0 @@
using FruityFoundation.Base.Structures;
namespace FruityFoundation.Base.Extensions;
public static class NullableExtensions
{
public static Maybe<T> ToMaybe<T>(this T? item) where T : struct =>
item ?? Maybe<T>.Empty();
public static Maybe<T> ToMaybe<T>(this T? item) where T : class =>
item ?? Maybe<T>.Empty();
}

View File

@ -1,8 +1,7 @@
using System; using System;
using System.Data; using System.Data;
using FruityFoundation.Base.Structures;
namespace FruityFoundation.Base.Extensions; namespace FruityFoundation.Base.Structures;
public static class DataReaderExtensions public static class DataReaderExtensions
{ {
@ -42,6 +41,6 @@ public static class DataReaderExtensions
public static Maybe<string> TryGetString(this IDataReader reader, int ord) => public static Maybe<string> TryGetString(this IDataReader reader, int ord) =>
TryGet(reader, ord, reader.GetString); TryGet(reader, ord, reader.GetString);
private static Maybe<T> TryGet<T>(IDataReader reader, int ord, Func<int, T> valueGetter) => private static Maybe<T> TryGet<T>(IDataRecord reader, int ord, Func<int, T> valueGetter) =>
reader.IsDBNull(ord) ? Maybe<T>.Empty() : valueGetter(ord); reader.IsDBNull(ord) ? Maybe.Empty<T>() : valueGetter(ord);
} }

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace FruityFoundation.Base.Structures;
public static class EnumerableExtensions
{
public static IEnumerable<T> ConditionalConcat<T>(this IEnumerable<T> enumerable, bool condition, IEnumerable<T> second) =>
!condition ? enumerable : enumerable.Concat(second);
public static IEnumerable<T> ConditionalAppend<T>(this IEnumerable<T> enumerable, bool condition, T second) =>
condition ? enumerable.Append(second) : enumerable;
public static IEnumerable<T> ConditionalWhere<T>(this IEnumerable<T> enumerable, bool condition, Func<T, bool> pred) =>
!condition ? enumerable : enumerable.Where(pred);
public static IEnumerable<TOutput> Choose<TInput, TOutput>(this IEnumerable<TInput> enumerable, Func<TInput, Maybe<TOutput>> chooser) =>
enumerable.Select(chooser).Where(x => x.HasValue).Select(x => x.Value);
}

View File

@ -1,18 +1,20 @@
// Normally we wouldn't want to disable Nullable references, but in this case we want to. namespace FruityFoundation.Base.Structures;
// We're assuming that if you're following Maybe conventions, you won't be hitting null ref exceptions.
using System; using System;
#pragma warning disable CS8601 public static class Maybe
namespace FruityFoundation.Base.Structures; {
public static Maybe<T> Just<T>(T value) => new(value, hasValue: true);
public static Maybe<T> Just<T>(T value, Func<T, bool> hasValue) => new(value, hasValue: hasValue(value));
public static Maybe<T> Empty<T>() => new(val: default!, hasValue: false);
}
[Serializable] public readonly struct Maybe<T>
public struct Maybe<T>
{ {
private readonly T _value; private readonly T _value;
public readonly bool HasValue; public readonly bool HasValue;
private Maybe(T val = default!, bool hasValue = true) internal Maybe(T val = default!, bool hasValue = true)
{ {
_value = val; _value = val;
HasValue = hasValue; HasValue = hasValue;
@ -29,12 +31,13 @@ public struct Maybe<T>
} }
} }
public T OrValue(T orVal) => public T OrValue(T orVal) => HasValue ? Value : orVal;
HasValue ? Value : orVal;
public T OrEval(Func<T> valueFactory) => HasValue ? Value : valueFactory();
public bool Try(out T val) public bool Try(out T val)
{ {
val = HasValue ? Value : default; val = HasValue ? Value : default!;
return HasValue; return HasValue;
} }
@ -42,23 +45,34 @@ public struct Maybe<T>
public T OrThrow(string msg) => public T OrThrow(string msg) =>
HasValue ? Value : throw new Exception(msg); HasValue ? Value : throw new Exception(msg);
public T OrThrow(Func<string> messageFactory) =>
HasValue ? Value : throw new Exception(messageFactory());
public T OrThrow(Func<Exception> exFactory) =>
HasValue ? Value : throw exFactory();
public Maybe<TOutput> Map<TOutput>(Func<T, TOutput> transformer) => public Maybe<TOutput> Map<TOutput>(Func<T, TOutput> transformer) =>
HasValue HasValue ? Maybe.Just(transformer(Value)) : Maybe.Empty<TOutput>();
? Maybe<TOutput>.Create(transformer(Value))
: Maybe<TOutput>.Empty();
public object ToDbValue() => public Maybe<TOutput> Bind<TOutput>(Func<T, TOutput> binder) =>
HasValue && Value is not null HasValue ? binder(Value) : Maybe.Empty<TOutput>();
? Value
: DBNull.Value;
public static Maybe<T> Create(T val, bool hasValue = true) => new(val, hasValue); public Maybe<TOutput> Cast<TOutput>()
{
if (!HasValue)
return Maybe.Empty<TOutput>();
public static Maybe<T> Create(T val, Func<T, bool> hasValue) => new(val, hasValue(val)); try
{
return (TOutput)Convert.ChangeType(Value, typeof(TOutput))!;
}
catch (InvalidCastException)
{
return Maybe.Empty<TOutput>();
}
}
public static Maybe<T> Empty() => new(default!, hasValue: false); public static implicit operator Maybe<T>(T val) => Maybe.Just(val);
public static implicit operator Maybe<T>(T val) => Create(val);
public static explicit operator T(Maybe<T> val) => val.Value; public static explicit operator T(Maybe<T> val) => val.Value;

View File

@ -10,7 +10,7 @@ public static class MaybeExtensions
{ {
using var enumerator = collection.GetEnumerator(); using var enumerator = collection.GetEnumerator();
return !enumerator.MoveNext() ? Maybe<T>.Empty() : enumerator.Current; return !enumerator.MoveNext() ? Maybe.Empty<T>() : enumerator.Current;
} }
public static Maybe<T> FirstOrEmpty<T>(this IEnumerable<T> collection, Func<T, bool> pred) public static Maybe<T> FirstOrEmpty<T>(this IEnumerable<T> collection, Func<T, bool> pred)
@ -19,9 +19,12 @@ public static class MaybeExtensions
if (pred(item)) if (pred(item))
return item; return item;
return Maybe<T>.Empty(); return Maybe.Empty<T>();
} }
public static Maybe<TValue> TryGet<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dict, TKey key) =>
dict.TryGetValue(key, out var value) ? Maybe.Just(value) : Maybe.Empty<TValue>();
public static T? ToNullable<T>(this Maybe<T> item) where T : struct => public static T? ToNullable<T>(this Maybe<T> item) where T : struct =>
item.HasValue ? item.Value : null; item.HasValue ? item.Value : null;

View File

@ -0,0 +1,7 @@
namespace FruityFoundation.Base.Structures;
public static class NullableExtensions
{
public static Maybe<T> ToMaybe<T>(this T? item) where T : struct =>
item ?? Maybe.Empty<T>();
}

49
Base/Structures/Result.cs Normal file
View File

@ -0,0 +1,49 @@
using System;
namespace FruityFoundation.Base.Structures;
public readonly struct Result<TSuccess, TFailure>
{
private readonly Maybe<TSuccess> _successVal;
private readonly Maybe<TFailure> _failureVal;
private Result(Maybe<TSuccess> successVal, Maybe<TFailure> failureVal)
{
_successVal = successVal;
_failureVal = failureVal;
}
public static Result<TSuccess, TFailure> CreateSuccess(TSuccess val) =>
new(successVal: val, failureVal: Maybe.Empty<TFailure>());
public static Result<TSuccess, TFailure> CreateFailure(TFailure val) =>
new(successVal: Maybe.Empty<TSuccess>(), failureVal: val);
public bool IsSuccess => _successVal.HasValue;
public bool IsFailure => _failureVal.HasValue;
public bool TrySuccess(out TSuccess output)
{
if (!_successVal.Try(out output))
{
output = default!;
return false;
}
return true;
}
public bool TryFailure(out TFailure output)
{
if (!_failureVal.Try(out output))
{
output = default!;
return false;
}
return true;
}
public T Merge<T>(Func<TSuccess, T> onSuccess, Func<TFailure, T> onFailure) =>
IsSuccess ? onSuccess(_successVal.Value) : onFailure(_failureVal.Value);
}

View File

@ -1,6 +1,6 @@
using System; using System;
namespace FruityFoundation.Base.Extensions; namespace FruityFoundation.Base.Structures;
public static class StringExtensions public static class StringExtensions
{ {
@ -16,5 +16,5 @@ public static class StringExtensions
/// <param name="str"></param> /// <param name="str"></param>
/// <param name="maxLength">The maximum number of characters. If <paramref name="str"/> has fewer characters, it will be truncated to the length of <paramref name="str"/>.</param> /// <param name="maxLength">The maximum number of characters. If <paramref name="str"/> has fewer characters, it will be truncated to the length of <paramref name="str"/>.</param>
public static string Truncate(this string str, int maxLength) => public static string Truncate(this string str, int maxLength) =>
str.Substring(0, Math.Min(str.Length, maxLength)); str[..Math.Min(str.Length, maxLength)];
} }

0
Db/Db.fsproj Executable file → Normal file
View File

View File

@ -3,7 +3,7 @@ open FruityFoundation.Base.Structures
module Option = module Option =
let toMaybe : 'a option -> Maybe<'a> = function let toMaybe : 'a option -> Maybe<'a> = function
| Some x -> x |> Maybe.Create | Some x -> x |> Maybe.Just
| None -> Maybe.Empty () | None -> Maybe.Empty ()
let fromMaybe : Maybe<'a> -> 'a option = function let fromMaybe : Maybe<'a> -> 'a option = function

2
FsBase/FsBase.fsproj Executable file → Normal file
View File

@ -29,7 +29,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Update="FSharp.Core" Version="7.0.0" /> <PackageReference Update="FSharp.Core" Version="7.0.300" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>