diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 4afa6ab..ab223bd 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -51,7 +51,7 @@ jobs: shell: powershell run: | dotnet tool install --global dotnet-coverage - .\.sonar\scanner\dotnet-sonarscanner begin /k:"astar-development_astar-dev-functional-extensions" /o:"astar-development" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" + .\.sonar\scanner\dotnet-sonarscanner begin /k:"astar-development_astar-dev-functional-extensions" /o:"astar-development" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.scanner.scanAll=false dotnet build dotnet-coverage collect 'dotnet test --filter "FullyQualifiedName!~Tests.EndToEnd"' -f xml -o 'coverage.xml' .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" diff --git a/samples/AStar.Dev.ConsoleSample/AStar.Dev.ConsoleSample.csproj b/samples/AStar.Dev.ConsoleSample/AStar.Dev.ConsoleSample.csproj index 6eedf0d..bf41120 100644 --- a/samples/AStar.Dev.ConsoleSample/AStar.Dev.ConsoleSample.csproj +++ b/samples/AStar.Dev.ConsoleSample/AStar.Dev.ConsoleSample.csproj @@ -9,7 +9,7 @@ - + diff --git a/samples/AStar.Dev.SampleApi/AStar.Dev.SampleApi.csproj b/samples/AStar.Dev.SampleApi/AStar.Dev.SampleApi.csproj index bef8201..f3c3654 100644 --- a/samples/AStar.Dev.SampleApi/AStar.Dev.SampleApi.csproj +++ b/samples/AStar.Dev.SampleApi/AStar.Dev.SampleApi.csproj @@ -12,7 +12,7 @@ - + diff --git a/samples/AStar.Dev.SampleBlazor/AStar.Dev.SampleBlazor.csproj b/samples/AStar.Dev.SampleBlazor/AStar.Dev.SampleBlazor.csproj index ad52d32..54627c2 100644 --- a/samples/AStar.Dev.SampleBlazor/AStar.Dev.SampleBlazor.csproj +++ b/samples/AStar.Dev.SampleBlazor/AStar.Dev.SampleBlazor.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/AStar.Dev.Functional.Extensions/AStar.Dev.Functional.Extensions.csproj b/src/AStar.Dev.Functional.Extensions/AStar.Dev.Functional.Extensions.csproj index 354cf8d..8b92d90 100644 --- a/src/AStar.Dev.Functional.Extensions/AStar.Dev.Functional.Extensions.csproj +++ b/src/AStar.Dev.Functional.Extensions/AStar.Dev.Functional.Extensions.csproj @@ -9,7 +9,7 @@ true snupkg AStar.Dev.Functional.Extensions - 0.2.1-alpha + 0.2.2-alpha Readme.md Jason AStar Development @@ -25,6 +25,7 @@ true AStar.Dev.Functional.Extensions AStar Development 2025 + No changes in this version, just extending the documentation @@ -33,6 +34,8 @@ + + diff --git a/src/AStar.Dev.Functional.Extensions/Option{T}.cs b/src/AStar.Dev.Functional.Extensions/Option{T}.cs index 783321b..652a18b 100644 --- a/src/AStar.Dev.Functional.Extensions/Option{T}.cs +++ b/src/AStar.Dev.Functional.Extensions/Option{T}.cs @@ -81,7 +81,7 @@ public Some(T value) public sealed class None : Option { /// - /// A helper method to create an instance of + /// A helper method to create an instance of /// public static readonly None Instance = new (); diff --git a/src/AStar.Dev.Functional.Extensions/Readme-option.md b/src/AStar.Dev.Functional.Extensions/Readme-option.md new file mode 100644 index 0000000..7c2b434 --- /dev/null +++ b/src/AStar.Dev.Functional.Extensions/Readme-option.md @@ -0,0 +1,103 @@ +# 🎯 Option Functional Cheat Sheet + +## 🧩 Option Overview + +Represents a value that might exist (`Some`) or not (`None`), avoiding nulls and enabling functional composition. + +```csharp +Option maybeNumber = Option.Some(42); +Option emptyName = Option.None(); +``` + +--- + +## πŸ— Construction + +| Syntax | Description | +|-----------------------------|-----------------------------------------| +| `Option.Some(value)` | Wraps a non-null value as `Some` | +| `Option.None()` | Creates a `None` of type `T` | +| `value.ToOption()` | Converts value or default to Option | +| `value.ToOption(predicate)` | Converts only if predicate returns true | +| `nullable.ToOption()` | Converts nullable struct to Option | + +--- + +## πŸ§ͺ Pattern Matching + +```csharp +option.Match( + some => $"Value: {some}", + () => "No value" +); +``` + +Or via deconstruction: + +```csharp +var (isSome, value) = option; +``` + +Or with TryGet: + +```csharp +if (option.TryGetValue(out var value)) { /* use value */ } +``` + +--- + +## πŸ”§ Transformation + +| Method | Description | +|---------------------|---------------------------------------------| +| `Map(func)` | Transforms value inside Some | +| `Bind(func)` | Chains function that returns another Option | +| `ToResult(errorFn)` | Converts Option to `Result` | +| `ToNullable()` | Converts to nullable (structs only) | +| `ToEnumerable()` | Converts to `IEnumerable` | + +--- + +## πŸͺ„ LINQ Support + +```csharp +var result = + from name in Option.Some("Jason") + from greeting in Option.Some($"Hello, {name}") + select greeting; +``` + +Via `Select`, `SelectMany`, or `SelectAwait` (async LINQ) + +--- + +## πŸ” Async Support + +| Method | Description | +|------------------------------|-----------------------------------------------| +| `MapAsync(func)` | Awaits and maps value | +| `BindAsync(func)` | Awaits and chains async Option-returning func | +| `MatchAsync(onSome, onNone)` | Async pattern match | +| `SelectAwait(func)` | LINQ-friendly async projection | + +--- + +## 🧯 Fallbacks and Conversions + +```csharp +option.OrElse("fallback"); // returns value or fallback +option.OrThrow(); // throws if None +option.IsSome(); // true if Some +option.IsNone(); // true if None +``` + +--- + +## πŸ› Debugging & Output + +```csharp +option.ToString(); // Outputs "Some(value)" or "None" +``` + +--- + diff --git a/src/AStar.Dev.Functional.Extensions/Readme-result.md b/src/AStar.Dev.Functional.Extensions/Readme-result.md new file mode 100644 index 0000000..5c211ca --- /dev/null +++ b/src/AStar.Dev.Functional.Extensions/Readme-result.md @@ -0,0 +1,96 @@ +# 🧭 Result Cheat Sheet + +## 🧱 Core Concept + +Result encapsulates either: + +βœ… Ok(T) β€” a success value + +❌ Error(TError) β€” an error reason + +```csharp +Result nameResult = new Result.Ok("Jason"); +Result errorResult = new Result.Error("Invalid input"); +``` + +## πŸ— Construction Helpers + +| Expression | Outcome | +|--------------------------------|-----------------------------| +| new Result.Ok(value) | Constructs a success result | +| new Result.Error(reason) | Constructs an error result | + +## πŸ”§ Transformation + +| Method | Description | +|-------------|--------------------------------------------------| +| Map(fn) | Transforms the success value | +| Bind(fn) | Chains to another result-returning function | +| Tap(action) | Invokes side effect on success, returns original | + +```csharp +result.Map(value => value.ToUpper()); +result.Bind(value => Validate(value)); +result.Tap(Console.WriteLine); +``` + +## πŸ§ͺ Pattern Matching + +```csharp +result.Match( +onSuccess: value => $"βœ… {value}", +onError: reason => $"❌ {reason}" +); +``` + +## 🧞 LINQ Composition + +```csharp +var final = +from input in GetInput() +from valid in Validate(input) +select $"Welcome, {valid}"; +``` + +## LINQ Methods + +| Method | Description | +|----------------------|---------------------------------------------| +| Select(fn) | Maps over success value | +| SelectMany(fn) | Binds to next result | +| SelectMany(..., ...) | Binds and projects from intermediate result | + +## ⚑ Async Support + +```csharp +var asyncResult = await resultTask.MapAsync(val => val.Length); +var finalValue = await resultTask.MatchAsync(...); +``` + +## Async LINQ + +```csharp +var result = +await GetAsync() +.SelectMany(asyncValue => ValidateAsync(asyncValue), (a, b) => $"{a}-{b}"); +``` + +## 🧯 Error Handling + +```csharp +if (result is Result.Error err) +Log(err.Reason); +``` + +Or selectively tap into errors: + +```csharp +public static Result TapError( +this Result result, +Action handler) +{ +if (result is Result.Error error) +handler(error.Reason); +return result; +} +``` diff --git a/src/AStar.Dev.Functional.Extensions/Readme.md b/src/AStar.Dev.Functional.Extensions/Readme.md index 7c2b434..dbf2f9d 100644 --- a/src/AStar.Dev.Functional.Extensions/Readme.md +++ b/src/AStar.Dev.Functional.Extensions/Readme.md @@ -1,103 +1,29 @@ -# 🎯 Option Functional Cheat Sheet +# AStar Dev Functional Extensions -## 🧩 Option Overview +## Overview -Represents a value that might exist (`Some`) or not (`None`), avoiding nulls and enabling functional composition. +This project contains a bunch of classes and extension methods to facilitate a more functional approach to handling errors and optional objects. -```csharp -Option maybeNumber = Option.Some(42); -Option emptyName = Option.None(); -``` +## Cheat Sheets ---- +### Result<T> and associated extensions -## πŸ— Construction +Cheat sheet is [here](Readme-result.md) -| Syntax | Description | -|-----------------------------|-----------------------------------------| -| `Option.Some(value)` | Wraps a non-null value as `Some` | -| `Option.None()` | Creates a `None` of type `T` | -| `value.ToOption()` | Converts value or default to Option | -| `value.ToOption(predicate)` | Converts only if predicate returns true | -| `nullable.ToOption()` | Converts nullable struct to Option | +### Option<T> and associated extensions ---- +Cheat sheet is [here](Readme-option.md) -## πŸ§ͺ Pattern Matching +## Build and analysis -```csharp -option.Match( - some => $"Value: {some}", - () => "No value" -); -``` +### GitHub build -Or via deconstruction: +[![SonarQube](https://github.com/astar-development/astar-dev-functional-extensions/actions/workflows/dotnet.yml/badge.svg)](https://github.com/astar-development/astar-dev-functional-extensions/actions/workflows/dotnet.yml) -```csharp -var (isSome, value) = option; -``` +### SonarQube details -Or with TryGet: - -```csharp -if (option.TryGetValue(out var value)) { /* use value */ } -``` - ---- - -## πŸ”§ Transformation - -| Method | Description | -|---------------------|---------------------------------------------| -| `Map(func)` | Transforms value inside Some | -| `Bind(func)` | Chains function that returns another Option | -| `ToResult(errorFn)` | Converts Option to `Result` | -| `ToNullable()` | Converts to nullable (structs only) | -| `ToEnumerable()` | Converts to `IEnumerable` | - ---- - -## πŸͺ„ LINQ Support - -```csharp -var result = - from name in Option.Some("Jason") - from greeting in Option.Some($"Hello, {name}") - select greeting; -``` - -Via `Select`, `SelectMany`, or `SelectAwait` (async LINQ) - ---- - -## πŸ” Async Support - -| Method | Description | -|------------------------------|-----------------------------------------------| -| `MapAsync(func)` | Awaits and maps value | -| `BindAsync(func)` | Awaits and chains async Option-returning func | -| `MatchAsync(onSome, onNone)` | Async pattern match | -| `SelectAwait(func)` | LINQ-friendly async projection | - ---- - -## 🧯 Fallbacks and Conversions - -```csharp -option.OrElse("fallback"); // returns value or fallback -option.OrThrow(); // throws if None -option.IsSome(); // true if Some -option.IsNone(); // true if None -``` - ---- - -## πŸ› Debugging & Output - -```csharp -option.ToString(); // Outputs "Some(value)" or "None" -``` - ---- +| | | | | | +|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=astar-development_astar-dev-functional-extensions&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=astar-development_astar-dev-functional-extensions) | [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=astar-development_astar-dev-functional-extensions&metric=bugs)](https://sonarcloud.io/summary/new_code?id=astar-development_astar-dev-functional-extensions) | [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=astar-development_astar-dev-functional-extensions&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=astar-development_astar-dev-functional-extensions) | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=astar-development_astar-dev-functional-extensions&metric=coverage)](https://sonarcloud.io/summary/new_code?id=astar-development_astar-dev-functional-extensions) | [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=astar-development_astar-dev-functional-extensions&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=astar-development_astar-dev-functional-extensions) | +| [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=astar-development_astar-dev-functional-extensions&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=astar-development_astar-dev-functional-extensions) | [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=astar-development_astar-dev-functional-extensions&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=astar-development_astar-dev-functional-extensions) | [![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=astar-development_astar-dev-functional-extensions&metric=sqale_index)](https://sonarcloud.io/summary/new_code?id=astar-development_astar-dev-functional-extensions) | [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=astar-development_astar-dev-functional-extensions&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=astar-development_astar-dev-functional-extensions) | [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=astar-development_astar-dev-functional-extensions&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=astar-development_astar-dev-functional-extensions) | diff --git a/src/AStar.Dev.Functional.Extensions/ResultAsyncExtensions.cs b/src/AStar.Dev.Functional.Extensions/ResultAsyncExtensions.cs index 4155633..7737eb5 100644 --- a/src/AStar.Dev.Functional.Extensions/ResultAsyncExtensions.cs +++ b/src/AStar.Dev.Functional.Extensions/ResultAsyncExtensions.cs @@ -5,18 +5,22 @@ namespace AStar.Dev.Functional.Extensions; /// +/// Provides asynchronous functional operations for wrapped in . /// public static class ResultAsyncExtensions { /// + /// Asynchronously transforms the success value of a if present. /// - /// - /// - /// - /// - /// - /// - /// + /// The type of the original success value. + /// The type of the error value. + /// The type of the transformed success value. + /// A that wraps the result to transform. + /// A mapping function applied to the success value. + /// An optional cancellation token to cancel the operation. + /// + /// A containing either a transformed Ok result or the original Error. + /// public static async ValueTask> MapAsync( this ValueTask> task, Func map, @@ -29,14 +33,17 @@ public static async ValueTask> MapAsync( } /// + /// Asynchronously chains a function that returns another if the current result is successful. /// - /// - /// - /// - /// - /// - /// - /// + /// The type of the original success value. + /// The type of the error value. + /// The type of the success value returned by the binding function. + /// A containing the result to bind. + /// A function that returns a new asynchronous result. + /// An optional cancellation token to cancel the operation. + /// + /// A containing the bound result if successful, or the original Error. + /// public static async ValueTask> BindAsync( this ValueTask> task, Func>> bind, @@ -51,15 +58,18 @@ public static async ValueTask> BindAsync( } /// + /// Asynchronously matches on a , executing the appropriate function depending on success or error. /// - /// - /// - /// - /// - /// - /// - /// - /// + /// The type of the success value. + /// The type of the error value. + /// The return type of the match operation. + /// A to evaluate. + /// A function invoked if the result is Ok. + /// A function invoked if the result is Error. + /// An optional cancellation token to cancel the operation. + /// + /// A containing the return value of the appropriate match function. + /// public static async ValueTask MatchAsync( this ValueTask> task, Func onSuccess, diff --git a/src/AStar.Dev.Functional.Extensions/ResultAsyncLinqExtensions.cs b/src/AStar.Dev.Functional.Extensions/ResultAsyncLinqExtensions.cs index d5f44f8..eb18ea0 100644 --- a/src/AStar.Dev.Functional.Extensions/ResultAsyncLinqExtensions.cs +++ b/src/AStar.Dev.Functional.Extensions/ResultAsyncLinqExtensions.cs @@ -5,20 +5,32 @@ namespace AStar.Dev.Functional.Extensions; /// +/// Provides LINQ-style asynchronous binding for wrapped in . /// public static class ResultAsyncLinqExtensions { /// + /// Asynchronously binds and projects over a using LINQ-style composition. /// - /// - /// - /// - /// - /// - /// - /// - /// - /// + /// The type of the original success value. + /// The type of the error value. + /// The type of the intermediate value returned by the binding function. + /// The final projected result type. + /// + /// An asynchronous to be bound and projected. + /// + /// + /// A function that returns an asynchronous from the source value. + /// + /// + /// A projection function that combines the source and collection values into a final result. + /// + /// + /// A token used to cancel the operation before completion. + /// + /// + /// A containing either a projected Ok value or an Error from any failure. + /// public static async ValueTask> SelectMany( this ValueTask> source, Func>> bind, diff --git a/src/AStar.Dev.Functional.Extensions/ResultExtensions.cs b/src/AStar.Dev.Functional.Extensions/ResultExtensions.cs index b6bfc10..5c335ac 100644 --- a/src/AStar.Dev.Functional.Extensions/ResultExtensions.cs +++ b/src/AStar.Dev.Functional.Extensions/ResultExtensions.cs @@ -3,17 +3,22 @@ namespace AStar.Dev.Functional.Extensions; /// +/// Provides functional operations for transforming and composing . /// public static class ResultExtensions { /// + /// Transforms the success value of a using the specified mapping function. /// - /// - /// - /// - /// - /// - /// + /// The original type of the success value. + /// The type of the error value. + /// The type of the transformed success value. + /// The result to transform. + /// A function that maps the original value to a new value. + /// + /// A new containing the mapped success value if present, + /// or the original error if unsuccessful. + /// public static Result Map( this Result result, Func map) @@ -25,13 +30,17 @@ public static Result Map( } /// + /// Chains the current result to another -producing function, + /// allowing for functional composition across result types. /// - /// - /// - /// - /// - /// - /// + /// The original type of the success value. + /// The type of the error value. + /// The type of the new result's success value. + /// The result to bind. + /// A function that returns a new . + /// + /// The result of the binding function if the original was successful; otherwise, the original error. + /// public static Result Bind( this Result result, Func> bind) @@ -43,12 +52,16 @@ public static Result Bind( } /// + /// Executes a side-effect action on the success value of a , + /// returning the original result unchanged. /// - /// - /// - /// - /// - /// + /// The type of the success value. + /// The type of the error value. + /// The result to inspect. + /// An action to invoke if the result is successful. + /// + /// The original instance, unchanged. + /// public static Result Tap( this Result result, Action action) diff --git a/src/AStar.Dev.Functional.Extensions/ResultLinqExtensions.cs b/src/AStar.Dev.Functional.Extensions/ResultLinqExtensions.cs index 9ebc779..ba3c5e4 100644 --- a/src/AStar.Dev.Functional.Extensions/ResultLinqExtensions.cs +++ b/src/AStar.Dev.Functional.Extensions/ResultLinqExtensions.cs @@ -3,21 +3,28 @@ namespace AStar.Dev.Functional.Extensions; /// +/// Enables LINQ-style composition for types using Select and SelectMany. /// public static class ResultLinqExtensions { /// + /// Binds and projects over a using LINQ-style composition. /// - /// - /// - /// - /// - /// - /// - /// - /// - public static Result SelectMany(this Result source, Func> bind, - Func project) + /// The type of the original success value. + /// The type of the error value. + /// The intermediate value returned by the binding function. + /// The final projected result value. + /// The initial result to bind. + /// A function that returns a result from the source value. + /// A projection that combines the source and bound values. + /// + /// A containing the projected success value, + /// or the first encountered error. + /// + public static Result SelectMany( + this Result source, + Func> bind, + Func project) { return source.Match( ok => bind(ok).Match>( @@ -29,27 +36,37 @@ public static Result SelectMany + /// Binds a result-producing function to a , enabling LINQ-style monadic chaining. /// - /// - /// - /// - /// - /// - /// - public static Result SelectMany(this Result result, Func> binder) + /// The original type of the success value. + /// The type of the error value. + /// The type of the bound result’s success value. + /// The original result to bind. + /// A function returning a new result from the success value. + /// + /// The bound result if successful, or the original error. + /// + public static Result SelectMany( + this Result result, + Func> binder) { return result.Bind(binder); } /// + /// Transforms the success value of a using a mapping function. /// - /// - /// - /// - /// - /// - /// - public static Result Select(this Result result, Func selector) + /// The original type of the success value. + /// The type of the error value. + /// The type of the transformed value. + /// The result to map. + /// A function that transforms the success value. + /// + /// A result containing the mapped value if successful, or the original error. + /// + public static Result Select( + this Result result, + Func selector) { return result.Map(selector); } diff --git a/test/AStar.Dev.Functional.Extensions.Tests.Unit/AStar.Dev.Functional.Extensions.Tests.Unit.csproj b/test/AStar.Dev.Functional.Extensions.Tests.Unit/AStar.Dev.Functional.Extensions.Tests.Unit.csproj index 4ce6b8a..0a950d3 100644 --- a/test/AStar.Dev.Functional.Extensions.Tests.Unit/AStar.Dev.Functional.Extensions.Tests.Unit.csproj +++ b/test/AStar.Dev.Functional.Extensions.Tests.Unit/AStar.Dev.Functional.Extensions.Tests.Unit.csproj @@ -29,7 +29,7 @@ - +