Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\AStar.Dev.Functional.Extensions\AStar.Dev.Functional.Extensions.csproj" />
<ProjectReference Include="..\..\src\AStar.Dev.Functional.Extensions\AStar.Dev.Functional.Extensions.csproj"/>
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion samples/AStar.Dev.SampleApi/AStar.Dev.SampleApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\AStar.Dev.Functional.Extensions\AStar.Dev.Functional.Extensions.csproj" />
<ProjectReference Include="..\..\src\AStar.Dev.Functional.Extensions\AStar.Dev.Functional.Extensions.csproj"/>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\AStar.Dev.Functional.Extensions\AStar.Dev.Functional.Extensions.csproj" />
<ProjectReference Include="..\..\src\AStar.Dev.Functional.Extensions\AStar.Dev.Functional.Extensions.csproj"/>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PackageId>AStar.Dev.Functional.Extensions</PackageId>
<Version>0.2.1-alpha</Version>
<Version>0.2.2-alpha</Version>
<PackageReadmeFile>Readme.md</PackageReadmeFile>
<Authors>Jason</Authors>
<Company>AStar Development</Company>
Expand All @@ -25,6 +25,7 @@
<IsPackable>true</IsPackable>
<Title>AStar.Dev.Functional.Extensions</Title>
<Copyright>AStar Development 2025</Copyright>
<PackageReleaseNotes>No changes in this version, just extending the documentation</PackageReleaseNotes>
</PropertyGroup>

<ItemGroup>
Expand All @@ -33,6 +34,8 @@

<ItemGroup>
<None Include="Readme.md" Pack="true" PackagePath="\"/>
<None Include="Readme-result.md" Pack="true" PackagePath="\"/>
<None Include="Readme-option.md" Pack="true" PackagePath="\"/>
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/AStar.Dev.Functional.Extensions/Option{T}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public Some(T value)
public sealed class None : Option<T>
{
/// <summary>
/// A helper method to create an instance of <see cref="None"/>
/// A helper method to create an instance of <see cref="None" />
/// </summary>
public static readonly None Instance = new ();

Expand Down
103 changes: 103 additions & 0 deletions src/AStar.Dev.Functional.Extensions/Readme-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# 🎯 Option<T> Functional Cheat Sheet

## 🧩 Option Overview

Represents a value that might exist (`Some`) or not (`None`), avoiding nulls and enabling functional composition.

```csharp
Option<int> maybeNumber = Option.Some(42);
Option<string> emptyName = Option.None<string>();
```

---

## 🏗 Construction

| Syntax | Description |
|-----------------------------|-----------------------------------------|
| `Option.Some(value)` | Wraps a non-null value as `Some` |
| `Option.None<T>()` | 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<T, TError>` |
| `ToNullable()` | Converts to nullable (structs only) |
| `ToEnumerable()` | Converts to `IEnumerable<T>` |

---

## 🪄 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"
```

---

96 changes: 96 additions & 0 deletions src/AStar.Dev.Functional.Extensions/Readme-result.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# 🧭 Result<T, TError> Cheat Sheet

## 🧱 Core Concept

Result<T, TError> encapsulates either:

✅ Ok(T) — a success value

❌ Error(TError) — an error reason

```csharp
Result<string, string> nameResult = new Result<string, string>.Ok("Jason");
Result<int, string> errorResult = new Result<int, string>.Error("Invalid input");
```

## 🏗 Construction Helpers

| Expression | Outcome |
|--------------------------------|-----------------------------|
| new Result<T, E>.Ok(value) | Constructs a success result |
| new Result<T, E>.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<T, TError>.Error err)
Log(err.Reason);
```

Or selectively tap into errors:

```csharp
public static Result<T, TError> TapError<T, TError>(
this Result<T, TError> result,
Action<TError> handler)
{
if (result is Result<T, TError>.Error error)
handler(error.Reason);
return result;
}
```
106 changes: 16 additions & 90 deletions src/AStar.Dev.Functional.Extensions/Readme.md
Original file line number Diff line number Diff line change
@@ -1,103 +1,29 @@
# 🎯 Option<T> 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<int> maybeNumber = Option.Some(42);
Option<string> emptyName = Option.None<string>();
```
## Cheat Sheets

---
### Result&lt;T&gt; 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<T>()` | 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&lt;T&gt; 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<T, TError>` |
| `ToNullable()` | Converts to nullable (structs only) |
| `ToEnumerable()` | Converts to `IEnumerable<T>` |

---

## 🪄 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) |

Loading