Skip to content

Commit

Permalink
Add support for select and select many with an without tasks (#33)
Browse files Browse the repository at this point in the history
* add support for select and select many with an without tasks

* add select and select many to option

* add utils for lifting async values into the monads

* Add query expression support to result type generator
Add tests for generated query expression extension methods

* - Funicular.Switch version to 5.0 because Select on Option and Result is now implemented by us
- fix tests
- use fluent assertions generators in tests

* - link to verify test blog

---------

Co-authored-by: Alexander Wiedemann <[email protected]>
  • Loading branch information
Tyrrx and ax0l0tl authored Nov 6, 2023
1 parent e1ba031 commit 4c6311a
Show file tree
Hide file tree
Showing 18 changed files with 546 additions and 181 deletions.
12 changes: 12 additions & 0 deletions Source/FunicularSwitch.Generators.Templates/ResultType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,18 @@ public static MyResult<T1> As<T, T1>(this MyResult<T> result, Func<MyError> erro

public static MyResult<T1> As<T1>(this MyResult<object> result, Func<MyError> errorIsNotT1) =>
result.As<object, T1>(errorIsNotT1);

#region query-expression pattern

public static MyResult<T1> Select<T, T1>(this MyResult<T> result, Func<T, T1> selector) => result.Map(selector);
public static Task<MyResult<T1>> Select<T, T1>(this Task<MyResult<T>> result, Func<T, T1> selector) => result.Map(selector);

public static MyResult<T2> SelectMany<T, T1, T2>(this MyResult<T> result, Func<T, MyResult<T1>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<MyResult<T2>> SelectMany<T, T1, T2>(this Task<MyResult<T>> result, Func<T, Task<MyResult<T1>>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<MyResult<T2>> SelectMany<T, T1, T2>(this Task<MyResult<T>> result, Func<T, MyResult<T1>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<MyResult<T2>> SelectMany<T, T1, T2>(this MyResult<T> result, Func<T, Task<MyResult<T1>>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));

#endregion
}
}

Expand Down
2 changes: 1 addition & 1 deletion Source/FunicularSwitch/FunicularSwitch.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</VersionSuffixLocal>

<!--#region adapt versions here-->
<MajorVersion>4</MajorVersion>
<MajorVersion>5</MajorVersion>
<MinorAndPatchVersion>0.0</MinorAndPatchVersion>
<!--#endregion-->

Expand Down
26 changes: 25 additions & 1 deletion Source/FunicularSwitch/Option.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public abstract class Option
{
public static Option<T> Some<T>(T value) => new Some<T>(value);
public static Option<T> None<T>() => Option<T>.None;
public static async Task<Option<T>> Some<T>(Task<T> value) => Some(await value);
public static Task<Option<T>> NoneAsync<T>() => Task.FromResult(Option<T>.None);
}

public abstract class Option<T> : Option, IEnumerable<T>
Expand Down Expand Up @@ -68,6 +70,7 @@ public async Task<TResult> Match<TResult>(Func<T, Task<TResult>> some, Func<Task
{
return await some(iAmSome.Value).ConfigureAwait(false);
}

return await none().ConfigureAwait(false);
}

Expand All @@ -78,6 +81,7 @@ public async Task<TResult> Match<TResult>(Func<T, Task<TResult>> some, Func<TRes
{
return await some(iAmSome.Value).ConfigureAwait(false);
}

return none();
}

Expand All @@ -88,6 +92,7 @@ public async Task<TResult> Match<TResult>(Func<T, Task<TResult>> some, TResult n
{
return await some(iAmSome.Value).ConfigureAwait(false);
}

return none;
}

Expand All @@ -105,7 +110,7 @@ public async Task<TResult> Match<TResult>(Func<T, Task<TResult>> some, TResult n

public T GetValueOrThrow(string? errorMessage = null) => Match(v => v, () => throw new InvalidOperationException(errorMessage ?? "Cannot access value of none option"));

public Option<TOther> Convert<TOther>() => Match(s => Some((TOther) (object)s!), None<TOther>);
public Option<TOther> Convert<TOther>() => Match(s => Some((TOther)(object)s!), None<TOther>);

public override string ToString() => Match(v => v?.ToString() ?? "", () => $"None {GetType().BeautifulName()}");
}
Expand Down Expand Up @@ -208,5 +213,24 @@ public static Option<T> ToOption<T>(this Result<T> result, Action<string>? logEr

public static Result<T> ToResult<T>(this Option<T> option, Func<string> errorIfNone) =>
option.Match(s => Result.Ok(s), () => Result.Error<T>(errorIfNone()));

#region query-expression pattern

public static Option<T1> Select<T, T1>(this Option<T> result, Func<T, T1> selector) => result.Map(selector);
public static Task<Option<T1>> Select<T, T1>(this Task<Option<T>> result, Func<T, T1> selector) => result.Map(selector);

public static Option<T2> SelectMany<T, T1, T2>(this Option<T> result, Func<T, Option<T1>> selector, Func<T, T1, T2> resultSelector) =>
result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));

public static Task<Option<T2>> SelectMany<T, T1, T2>(this Task<Option<T>> result, Func<T, Task<Option<T1>>> selector, Func<T, T1, T2> resultSelector) =>
result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));

public static Task<Option<T2>> SelectMany<T, T1, T2>(this Task<Option<T>> result, Func<T, Option<T1>> selector, Func<T, T1, T2> resultSelector) =>
result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));

public static Task<Option<T2>> SelectMany<T, T1, T2>(this Option<T> result, Func<T, Task<Option<T1>>> selector, Func<T, T1, T2> resultSelector) =>
result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));

#endregion
}
}
17 changes: 17 additions & 0 deletions Source/FunicularSwitch/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public abstract class Result
{
public static Result<T> Error<T>(string message) => new Error<T>(message);
public static Result<T> Ok<T>(T value) => new Ok<T>(value);
public static async Task<Result<T>> Ok<T>(Task<T> value) => Ok(await value.ConfigureAwait(false));
public static Task<Result<T>> ErrorAsync<T>(string message) => Task.FromResult(Error<T>(message));
public bool IsError => GetType().GetGenericTypeDefinition() == typeof(Error<>);
public bool IsOk => !IsError;
public abstract string? GetErrorOrDefault();
Expand Down Expand Up @@ -728,6 +730,21 @@ public static Result<T> Validate<T>(this T item, Validate<T, string> validate, s
var errors = validate(item).JoinErrors(errorSeparator);
return !string.IsNullOrEmpty(errors) ? Result.Error<T>(errors) : item;
}

#region query-expression pattern

public static Result<T1> Select<T, T1>(this Result<T> result, Func<T, T1> selector) => result.Map(selector);
public static Task<Result<T1>> Select<T, T1>(this Task<Result<T>> result, Func<T, T1> selector) => result.Map(selector);

public static Result<T2> SelectMany<T, T1, T2>(this Result<T> result, Func<T, Result<T1>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<Result<T2>> SelectMany<T, T1, T2>(this Task<Result<T>> result, Func<T, Task<Result<T1>>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<Result<T2>> SelectMany<T, T1, T2>(this Task<Result<T>> result, Func<T, Result<T1>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<Result<T2>> SelectMany<T, T1, T2>(this Result<T> result, Func<T, Task<Result<T1>>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));

#endregion



}

public delegate IEnumerable<TError> Validate<in T, out TError>(T item);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="FunicularSwitch" Version="3.0.4" />
<PackageReference Include="FunicularSwitch.Generators" Version="1.3.3" />
<PackageReference Include="FunicularSwitch" Version="4.0.0" />
<PackageReference Include="FunicularSwitch.Generators" Version="2.1.0" />
<PackageReference Include="FunicularSwitch.Generators.FluentAssertions" Version="1.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,18 @@ public static OperationResult<T1> As<T, T1>(this OperationResult<T> result, Func

public static OperationResult<T1> As<T1>(this OperationResult<object> result, Func<Error> errorIsNotT1) =>
result.As<object, T1>(errorIsNotT1);

#region query-expression pattern

public static OperationResult<T1> Select<T, T1>(this OperationResult<T> result, Func<T, T1> selector) => result.Map(selector);
public static Task<OperationResult<T1>> Select<T, T1>(this Task<OperationResult<T>> result, Func<T, T1> selector) => result.Map(selector);

public static OperationResult<T2> SelectMany<T, T1, T2>(this OperationResult<T> result, Func<T, OperationResult<T1>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<OperationResult<T2>> SelectMany<T, T1, T2>(this Task<OperationResult<T>> result, Func<T, Task<OperationResult<T1>>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<OperationResult<T2>> SelectMany<T, T1, T2>(this Task<OperationResult<T>> result, Func<T, OperationResult<T1>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<OperationResult<T2>> SelectMany<T, T1, T2>(this OperationResult<T> result, Func<T, Task<OperationResult<T1>>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));

#endregion
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,18 @@ public static Result<T1> As<T, T1>(this Result<T> result, Func<String> errorTIsN

public static Result<T1> As<T1>(this Result<object> result, Func<String> errorIsNotT1) =>
result.As<object, T1>(errorIsNotT1);

#region query-expression pattern

public static Result<T1> Select<T, T1>(this Result<T> result, Func<T, T1> selector) => result.Map(selector);
public static Task<Result<T1>> Select<T, T1>(this Task<Result<T>> result, Func<T, T1> selector) => result.Map(selector);

public static Result<T2> SelectMany<T, T1, T2>(this Result<T> result, Func<T, Result<T1>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<Result<T2>> SelectMany<T, T1, T2>(this Task<Result<T>> result, Func<T, Task<Result<T1>>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<Result<T2>> SelectMany<T, T1, T2>(this Task<Result<T>> result, Func<T, Result<T1>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<Result<T2>> SelectMany<T, T1, T2>(this Result<T> result, Func<T, Task<Result<T1>>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));

#endregion
}
}

Expand Down
76 changes: 76 additions & 0 deletions Source/Tests/FunicularSwitch.Generators.Consumer/GeneratorSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,84 @@ public async Task ExceptionsAreTurnedIntoErrors()
static IEnumerable<string> BuggyValidate(int number) => throw new InvalidOperationException("Boom");

}

[TestMethod]
public void QueryExpressionSelect()
{
Result<int> subject = 42;
var result =
from r in subject
select r;
result.Should().BeEquivalentTo(Result.Ok(42));
}

[TestMethod]
public void QueryExpressionSelectMany()
{
Result<int> ok = 42;
var error = Result.Error<int>("fail");

(
from r in ok
from r1 in error
select r1
).Should().BeEquivalentTo(error);

(
from r in error
from r1 in ok
select r1
).Should().BeEquivalentTo(error);

(
from r in ok
let x = r * 2
from r1 in ok
select x
).Should().BeEquivalentTo(ok.Map(r => r * 2));
}

[TestMethod]
public async Task QueryExpressionSelectManyAsync()
{
Task<Result<int>> okAsync = Task.FromResult(Result.Ok(42));
var errorAsync = Task.FromResult(Result.Error<int>("fail"));

var ok = Result.Ok(1);

(await (
from r in okAsync
from r1 in errorAsync
select r1
)).Should().BeEquivalentTo(await errorAsync);

(await (
from r in errorAsync
from r1 in okAsync
select r1
)).Should().BeEquivalentTo(await errorAsync);

(await (
from r in okAsync
let x = r * 2
from r1 in okAsync
select x
)).Should().BeEquivalentTo(await okAsync.Map(r => r * 2));

(await (
from r in ok
let x = r * 2
from r1 in okAsync
select x
)).Should().BeEquivalentTo( ok.Map(r => r * 2));

(await (
from r in okAsync
let x = r * 2
from r1 in ok
select x
)).Should().BeEquivalentTo(await okAsync.Map(r => r * 2));
}
}

[ResultType(ErrorType = typeof(string))]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using FluentAssertions;
using FunicularSwitch;
using FunicularSwitch.Generators.FluentAssertions;
using FunicularSwitch.Generators.FluentAssertions.Consumer.Dependency;
using Xunit.Sdk;

Expand Down
1 change: 1 addition & 0 deletions Source/Tests/FunicularSwitch.Generators.Test/ReadMe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Test are build with verify, nice explanation can be found [here](https://andrewlock.net/creating-a-source-generator-part-2-testing-an-incremental-generator-with-snapshot-testing/)
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,18 @@ public static OperationResult<T1> As<T, T1>(this OperationResult<T> result, Func

public static OperationResult<T1> As<T1>(this OperationResult<object> result, Func<MyError> errorIsNotT1) =>
result.As<object, T1>(errorIsNotT1);

#region query-expression pattern

public static OperationResult<T1> Select<T, T1>(this OperationResult<T> result, Func<T, T1> selector) => result.Map(selector);
public static Task<OperationResult<T1>> Select<T, T1>(this Task<OperationResult<T>> result, Func<T, T1> selector) => result.Map(selector);

public static OperationResult<T2> SelectMany<T, T1, T2>(this OperationResult<T> result, Func<T, OperationResult<T1>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<OperationResult<T2>> SelectMany<T, T1, T2>(this Task<OperationResult<T>> result, Func<T, Task<OperationResult<T1>>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<OperationResult<T2>> SelectMany<T, T1, T2>(this Task<OperationResult<T>> result, Func<T, OperationResult<T1>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<OperationResult<T2>> SelectMany<T, T1, T2>(this OperationResult<T> result, Func<T, Task<OperationResult<T1>>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));

#endregion
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,18 @@ public static Result<T1> As<T, T1>(this Result<T> result, Func<String> errorTIsN

public static Result<T1> As<T1>(this Result<object> result, Func<String> errorIsNotT1) =>
result.As<object, T1>(errorIsNotT1);

#region query-expression pattern

public static Result<T1> Select<T, T1>(this Result<T> result, Func<T, T1> selector) => result.Map(selector);
public static Task<Result<T1>> Select<T, T1>(this Task<Result<T>> result, Func<T, T1> selector) => result.Map(selector);

public static Result<T2> SelectMany<T, T1, T2>(this Result<T> result, Func<T, Result<T1>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<Result<T2>> SelectMany<T, T1, T2>(this Task<Result<T>> result, Func<T, Task<Result<T1>>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<Result<T2>> SelectMany<T, T1, T2>(this Task<Result<T>> result, Func<T, Result<T1>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<Result<T2>> SelectMany<T, T1, T2>(this Result<T> result, Func<T, Task<Result<T1>>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));

#endregion
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,18 @@ public static OperationResult<T1> As<T, T1>(this OperationResult<T> result, Func

public static OperationResult<T1> As<T1>(this OperationResult<object> result, Func<MyError> errorIsNotT1) =>
result.As<object, T1>(errorIsNotT1);

#region query-expression pattern

public static OperationResult<T1> Select<T, T1>(this OperationResult<T> result, Func<T, T1> selector) => result.Map(selector);
public static Task<OperationResult<T1>> Select<T, T1>(this Task<OperationResult<T>> result, Func<T, T1> selector) => result.Map(selector);

public static OperationResult<T2> SelectMany<T, T1, T2>(this OperationResult<T> result, Func<T, OperationResult<T1>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<OperationResult<T2>> SelectMany<T, T1, T2>(this Task<OperationResult<T>> result, Func<T, Task<OperationResult<T1>>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<OperationResult<T2>> SelectMany<T, T1, T2>(this Task<OperationResult<T>> result, Func<T, OperationResult<T1>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<OperationResult<T2>> SelectMany<T, T1, T2>(this OperationResult<T> result, Func<T, Task<OperationResult<T1>>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));

#endregion
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,18 @@ public static OperationResult<T1> As<T, T1>(this OperationResult<T> result, Func

public static OperationResult<T1> As<T1>(this OperationResult<object> result, Func<MyError> errorIsNotT1) =>
result.As<object, T1>(errorIsNotT1);

#region query-expression pattern

public static OperationResult<T1> Select<T, T1>(this OperationResult<T> result, Func<T, T1> selector) => result.Map(selector);
public static Task<OperationResult<T1>> Select<T, T1>(this Task<OperationResult<T>> result, Func<T, T1> selector) => result.Map(selector);

public static OperationResult<T2> SelectMany<T, T1, T2>(this OperationResult<T> result, Func<T, OperationResult<T1>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<OperationResult<T2>> SelectMany<T, T1, T2>(this Task<OperationResult<T>> result, Func<T, Task<OperationResult<T1>>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<OperationResult<T2>> SelectMany<T, T1, T2>(this Task<OperationResult<T>> result, Func<T, OperationResult<T1>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));
public static Task<OperationResult<T2>> SelectMany<T, T1, T2>(this OperationResult<T> result, Func<T, Task<OperationResult<T1>>> selector, Func<T, T1, T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t, t1)));

#endregion
}
}

Expand Down
Loading

0 comments on commit 4c6311a

Please sign in to comment.