diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 217f7cb..130b9be 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -1,14 +1,21 @@
-# This workflow will build a .NET project
-# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
-
-name: .NET
+name: .NET Build and Test
on:
+ workflow_dispatch:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
+permissions:
+ contents: read
+ checks: write
+ pull-requests: write
+
+concurrency:
+ group: dotnet-ci-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
build:
@@ -16,13 +23,17 @@ jobs:
steps:
- uses: actions/checkout@v4
+
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
- dotnet-version: 8.0.x
+ dotnet-version: 10.0.x
+
- name: Restore dependencies
run: dotnet restore
+
- name: Build
run: dotnet build --no-restore
+
- name: Test
run: dotnet test --no-build --verbosity normal
diff --git a/src/AStar.Dev.CloudSyncFunctional/AStar.Dev.CloudSyncFunctional.csproj b/src/AStar.Dev.CloudSyncFunctional/AStar.Dev.CloudSyncFunctional.csproj
index ed9781c..24dad45 100644
--- a/src/AStar.Dev.CloudSyncFunctional/AStar.Dev.CloudSyncFunctional.csproj
+++ b/src/AStar.Dev.CloudSyncFunctional/AStar.Dev.CloudSyncFunctional.csproj
@@ -5,6 +5,46 @@
net10.0
enable
enable
+ true
+
+
+
+
+
+
+
+
+
+
+ None
+ All
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AStar.Dev.CloudSyncFunctional/appsettings.json b/src/AStar.Dev.CloudSyncFunctional/appsettings.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/src/AStar.Dev.CloudSyncFunctional/appsettings.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/AStar.Dev.FunctionalParadigm/AStar.Dev.FunctionalParadigm.csproj b/src/AStar.Dev.FunctionalParadigm/AStar.Dev.FunctionalParadigm.csproj
index ed9781c..b760144 100644
--- a/src/AStar.Dev.FunctionalParadigm/AStar.Dev.FunctionalParadigm.csproj
+++ b/src/AStar.Dev.FunctionalParadigm/AStar.Dev.FunctionalParadigm.csproj
@@ -1,7 +1,6 @@
- Exe
net10.0
enable
enable
diff --git a/src/AStar.Dev.FunctionalParadigm/Option.cs b/src/AStar.Dev.FunctionalParadigm/Option.cs
new file mode 100644
index 0000000..95faa28
--- /dev/null
+++ b/src/AStar.Dev.FunctionalParadigm/Option.cs
@@ -0,0 +1,44 @@
+namespace AStar.Dev.FunctionalParadigm;
+
+public abstract record Option
+{
+ public static implicit operator TResult(Option option) =>
+ option switch
+ {
+ Some some => some.Value,
+ _ => default!
+ };
+
+ public static implicit operator string(Option option) =>
+ option switch
+ {
+ None => "missing",
+ Option.None => "missing",
+ _ => string.Empty
+ };
+
+ ///
+ /// Represents the absence of a value.
+ ///
+ public sealed record None : Option
+ {
+ ///
+ /// A helper method to create an instance of
+ ///
+ public static readonly None Instance = new();
+
+ private None()
+ {
+ }
+
+ ///
+ /// Overrides the ToString method to return the type as a simple string.
+ ///
+ /// The overridden ToString
+ public override string ToString() => "None";
+ }
+}
+
+public record Some(TResult Value) : Option;
+
+public record None() : Option;
diff --git a/src/AStar.Dev.FunctionalParadigm/OptionExtensions.cs b/src/AStar.Dev.FunctionalParadigm/OptionExtensions.cs
new file mode 100644
index 0000000..bd2023b
--- /dev/null
+++ b/src/AStar.Dev.FunctionalParadigm/OptionExtensions.cs
@@ -0,0 +1,72 @@
+namespace AStar.Dev.FunctionalParadigm;
+
+public static class OptionExtensions
+{
+ public static Option Tap(this Option option, Action onSome, Action? onNone = null)
+ {
+ if (option is Some some)
+ {
+ onSome(some.Value);
+ return some;
+ }
+
+ if (option is None none)
+ {
+ onNone?.Invoke(none);
+ return none;
+ }
+
+ throw new InvalidOperationException("Unexpected option type.");
+ }
+
+ public static Option Map(this Option option, Func selector)
+ => option switch
+ {
+ Some some => new Some(selector(some.Value)),
+ None => new None(),
+ _ => throw new InvalidOperationException("Unexpected option type.")
+ };
+
+ public static Option Bind(this Option option, Func> binder)
+ => option switch
+ {
+ Some some => binder(some.Value),
+ None => new None(),
+ _ => throw new InvalidOperationException("Unexpected option type.")
+ };
+
+ public static TOut Match(this Option option, Func onSome, Func onNone)
+ => option switch
+ {
+ Some some => onSome(some.Value),
+ None none => onNone(none),
+ _ => throw new InvalidOperationException("Unexpected option type.")
+ };
+
+ public static TOut Match(this Option option, Func onSome)
+ => option switch
+ {
+ Some some => onSome(some.Value),
+ None none => new None(),
+ _ => throw new InvalidOperationException("Unexpected option type.")
+ };
+
+ public static async Task MatchAsync(this Task
diff --git a/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/AStar.Dev.CloudSyncFunctional.Tests.Unit.csproj b/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/AStar.Dev.CloudSyncFunctional.Tests.Unit.csproj
index c3f1265..7128f75 100644
--- a/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/AStar.Dev.CloudSyncFunctional.Tests.Unit.csproj
+++ b/test/AStar.Dev.CloudSyncFunctional.Tests.Unit/AStar.Dev.CloudSyncFunctional.Tests.Unit.csproj
@@ -5,8 +5,10 @@
enable
Exe
AStar.Dev.CloudSyncFunctional.Tests.Unit
- net8.0
+ net10.0
true
+ true
+ NU1902;NU1903;CA1859
@@ -15,10 +17,27 @@
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
-
+
+
+
+
+
\ No newline at end of file
diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/AStar.Dev.FunctionalParadigm.Tests.Unit.csproj b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/AStar.Dev.FunctionalParadigm.Tests.Unit.csproj
index 1f4c96a..25592bb 100644
--- a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/AStar.Dev.FunctionalParadigm.Tests.Unit.csproj
+++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/AStar.Dev.FunctionalParadigm.Tests.Unit.csproj
@@ -4,9 +4,12 @@
enable
enable
Exe
- AStar.Dev.FunctionalParadigm.Tests.Unit
- net8.0
+ AStar.Dev.FunctionsParadigm.Tests.Unit
+ net10.0
true
+ true
+ true
+ NU1902;CA1859
@@ -15,10 +18,20 @@
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResult.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResult.cs
new file mode 100644
index 0000000..f00e94c
--- /dev/null
+++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResult.cs
@@ -0,0 +1,26 @@
+using AStar.Dev.FunctionalParadigm;
+
+namespace AStar.Dev.FunctionsParadigm.Tests.Unit;
+
+public class GivenAResult
+{
+ [Fact]
+ public void when_an_ok_result_is_created_then_implicit_conversion_returns_the_value()
+ {
+ Result result = new Ok(42);
+
+ int value = result;
+
+ value.ShouldBe(42);
+ }
+
+ [Fact]
+ public void when_a_fail_result_is_created_then_implicit_conversion_returns_the_error()
+ {
+ Result result = new Fail("boom");
+
+ string error = result;
+
+ error.ShouldBe("boom");
+ }
+}
diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultBind.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultBind.cs
new file mode 100644
index 0000000..7720d55
--- /dev/null
+++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultBind.cs
@@ -0,0 +1,26 @@
+using AStar.Dev.FunctionalParadigm;
+
+namespace AStar.Dev.FunctionsParadigm.Tests.Unit;
+
+public class GivenAResultBind
+{
+ [Fact]
+ public void when_binding_an_ok_result_then_the_bound_result_is_returned()
+ {
+ Result result = new Ok(7);
+
+ Result bound = result.Bind(value => new Ok(value + 5));
+
+ bound.Match(value => value, _ => -1).ShouldBe(12);
+ }
+
+ [Fact]
+ public void when_binding_a_fail_result_then_the_failure_is_preserved()
+ {
+ Result result = new Fail("nope");
+
+ Result bound = result.Bind(value => new Ok(value + 1));
+
+ bound.Match(_ => string.Empty, error => error).ShouldBe("nope");
+ }
+}
diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultMap.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultMap.cs
new file mode 100644
index 0000000..6d5e9c8
--- /dev/null
+++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultMap.cs
@@ -0,0 +1,26 @@
+using AStar.Dev.FunctionalParadigm;
+
+namespace AStar.Dev.FunctionsParadigm.Tests.Unit;
+
+public class GivenAResultMap
+{
+ [Fact]
+ public void when_mapping_an_ok_result_then_the_value_is_transformed()
+ {
+ Result result = new Ok(7);
+
+ Result mapped = result.Map(value => value * 3);
+
+ mapped.Match(value => value, _ => -1).ShouldBe(21);
+ }
+
+ [Fact]
+ public void when_mapping_a_fail_result_then_the_error_is_preserved()
+ {
+ Result result = new Fail("nope");
+
+ Result mapped = result.Map(value => value * 3);
+
+ mapped.Match(_ => string.Empty, error => error).ShouldBe("nope");
+ }
+}
diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultMatch.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultMatch.cs
new file mode 100644
index 0000000..6a660af
--- /dev/null
+++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultMatch.cs
@@ -0,0 +1,99 @@
+using AStar.Dev.FunctionalParadigm;
+
+namespace AStar.Dev.FunctionsParadigm.Tests.Unit;
+
+public class GivenAResultMatch
+{
+ [Fact]
+ public void when_matching_an_ok_result_then_the_success_handler_is_used()
+ {
+ Result result = new Ok(8);
+
+ string matched = result.Match(value => $"ok:{value}", error => $"fail:{error}");
+
+ matched.ShouldBe("ok:8");
+ }
+
+ [Fact]
+ public void when_matching_a_fail_result_then_the_failure_handler_is_used()
+ {
+ Result result = new Fail("bad");
+
+ string matched = result.Match(value => $"ok:{value}", error => $"fail:{error}");
+
+ matched.ShouldBe("fail:bad");
+ }
+
+ // ... existing code ...
+ [Fact]
+ public async Task when_task_result_is_fail_then_func_overload_returns_failure_value()
+ {
+ Task> task = Task.FromResult>(new Fail("err"));
+
+ var result = await task.MatchAsync(
+ ok => ok * 2,
+ _ => -1);
+
+ result.ShouldBe(-1);
+ }
+
+ [Fact]
+ public async Task when_task_result_is_ok_then_async_func_overload_returns_mapped_value()
+ {
+ Task> task = Task.FromResult>(new Ok(5));
+
+ var result = await task.MatchAsync(
+ ok => Task.FromResult(ok * 2),
+ _ => Task.FromResult(-1));
+
+ result.ShouldBe(10);
+ }
+
+ [Fact]
+ public async Task when_task_result_is_fail_then_async_func_overload_returns_failure_value()
+ {
+ Task> task = Task.FromResult>(new Fail("err"));
+
+ var result = await task.MatchAsync(
+ ok => Task.FromResult(ok * 2),
+ _ => Task.FromResult(-1));
+
+ result.ShouldBe(-1);
+ }
+
+ [Fact]
+ public async Task when_task_result_is_ok_then_async_func_overload_does_not_call_failure_branch()
+ {
+ var failureCalled = false;
+ Task> task = Task.FromResult>(new Ok(5));
+
+ var result = await task.MatchAsync(
+ ok => Task.FromResult(ok * 2),
+ _ =>
+ {
+ failureCalled = true;
+ return Task.FromResult(-1);
+ });
+
+ result.ShouldBe(10);
+ failureCalled.ShouldBeFalse();
+ }
+
+ [Fact]
+ public async Task when_task_result_is_fail_then_async_func_overload_does_not_call_success_branch()
+ {
+ var successCalled = false;
+ Task> task = Task.FromResult>(new Fail("err"));
+
+ var result = await task.MatchAsync(
+ ok =>
+ {
+ successCalled = true;
+ return Task.FromResult(ok * 2);
+ },
+ _ => Task.FromResult(-1));
+
+ result.ShouldBe(-1);
+ successCalled.ShouldBeFalse();
+ }
+}
diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultTap.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultTap.cs
new file mode 100644
index 0000000..761e6fc
--- /dev/null
+++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAResultTap.cs
@@ -0,0 +1,28 @@
+using AStar.Dev.FunctionalParadigm;
+
+namespace AStar.Dev.FunctionsParadigm.Tests.Unit;
+
+public class GivenAResultTap
+{
+ [Fact]
+ public void when_tapping_an_ok_result_then_the_success_action_runs()
+ {
+ Result result = new Ok(5);
+ var observed = 0;
+
+ result.Tap(value => observed = value * 2);
+
+ observed.ShouldBe(10);
+ }
+
+ [Fact]
+ public void when_tapping_a_fail_result_then_the_failure_action_runs()
+ {
+ Result result = new Fail("boom");
+ string? observed = null;
+
+ result.Tap(_ => observed = "unexpected", error => observed = error);
+
+ observed.ShouldBe("boom");
+ }
+}
diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOption.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOption.cs
new file mode 100644
index 0000000..6873a83
--- /dev/null
+++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOption.cs
@@ -0,0 +1,26 @@
+using AStar.Dev.FunctionalParadigm;
+
+namespace AStar.Dev.FunctionsParadigm.Tests.Unit;
+
+public class GivenAnOption
+{
+ [Fact]
+ public void when_a_some_option_is_created_then_implicit_conversion_returns_the_value()
+ {
+ Option result = new Some(42);
+
+ int value = result;
+
+ value.ShouldBe(42);
+ }
+
+ [Fact]
+ public void when_a_none_option_is_created_then_implicit_conversion_returns_the_error()
+ {
+ Option result = new None();
+
+ string error = result;
+
+ error.ShouldBe("missing");
+ }
+}
diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionBind.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionBind.cs
new file mode 100644
index 0000000..5c50d39
--- /dev/null
+++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionBind.cs
@@ -0,0 +1,26 @@
+using AStar.Dev.FunctionalParadigm;
+
+namespace AStar.Dev.FunctionsParadigm.Tests.Unit;
+
+public class GivenAnOptionBind
+{
+ [Fact]
+ public void when_binding_a_some_option_then_the_bound_result_is_returned()
+ {
+ Option result = new Some(7);
+
+ var bound = result.Bind(value => new Some(value + 5));
+
+ bound.Match(value => value, _ => -1).ShouldBe(12);
+ }
+
+ [Fact]
+ public void when_binding_a_none_option_then_the_failure_is_preserved()
+ {
+ Option result = new None();
+
+ var bound = result.Bind(value => new None());
+
+ bound.Match(_ => string.Empty, error => error).ShouldBe("missing");
+ }
+}
diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionMap.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionMap.cs
new file mode 100644
index 0000000..189922d
--- /dev/null
+++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionMap.cs
@@ -0,0 +1,26 @@
+using AStar.Dev.FunctionalParadigm;
+
+namespace AStar.Dev.FunctionsParadigm.Tests.Unit;
+
+public class GivenAnOptionMap
+{
+ [Fact]
+ public void when_mapping_a_some_option_then_the_value_is_transformed()
+ {
+ Option result = new Some(7);
+
+ var mapped = result.Map(value => value * 3);
+
+ mapped.Match(value => value, _ => -1).ShouldBe(21);
+ }
+
+ [Fact]
+ public void when_mapping_a_none_option_then_the_error_is_preserved()
+ {
+ Option result = new None();
+
+ var mapped = result.Map(value => value * 3);
+
+ mapped.Match(_ => string.Empty, error => error).ShouldBe("missing");
+ }
+}
diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionMatch.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionMatch.cs
new file mode 100644
index 0000000..eb703a8
--- /dev/null
+++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionMatch.cs
@@ -0,0 +1,26 @@
+using AStar.Dev.FunctionalParadigm;
+
+namespace AStar.Dev.FunctionsParadigm.Tests.Unit;
+
+public class GivenAnOptionMatch
+{
+ [Fact]
+ public void when_matching_a_some_option_then_the_success_handler_is_used()
+ {
+ Option result = new Some(8);
+
+ string matched = result.Match(value => $"ok:{value}", error => $"fail:{error}");
+
+ matched.ShouldBe("ok:8");
+ }
+
+ [Fact]
+ public void when_matching_a_none_option_then_the_failure_handler_is_used()
+ {
+ Option result = new None();
+
+ string matched = result.Match(value => $"ok:{value}", error => $"fail:{error}");
+
+ matched.ShouldBe("fail:missing");
+ }
+}
diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionTap.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionTap.cs
new file mode 100644
index 0000000..085fe07
--- /dev/null
+++ b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/GivenAnOptionTap.cs
@@ -0,0 +1,28 @@
+using AStar.Dev.FunctionalParadigm;
+
+namespace AStar.Dev.FunctionsParadigm.Tests.Unit;
+
+public class GivenAnOptionTap
+{
+ [Fact]
+ public void when_tapping_a_some_option_then_the_success_action_runs()
+ {
+ Option result = new Some(5);
+ var observed = 0;
+
+ result.Tap(value => observed = value * 2, _ => observed = -1);
+
+ observed.ShouldBe(10);
+ }
+
+ [Fact]
+ public void when_tapping_a_none_option_then_the_failure_action_runs()
+ {
+ Option result = new None();
+ string? observed = null;
+
+ result.Tap(_ => observed = "unexpected", error => observed = "error");
+
+ observed.ShouldBe("error");
+ }
+}
diff --git a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/UnitTest1.cs b/test/AStar.Dev.FunctionalParadigm.Tests.Unit/UnitTest1.cs
deleted file mode 100644
index b87107e..0000000
--- a/test/AStar.Dev.FunctionalParadigm.Tests.Unit/UnitTest1.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace AStar.Dev.FunctionalParadigm.Tests.Unit;
-
-public class UnitTest1
-{
- [Fact]
- public void Test1()
- {
- Assert.True(true);
- }
-}