Skip to content
Open
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
28 changes: 28 additions & 0 deletions AStar.Dev.Testing.Dashboard.sln
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AStar.Dev.Testing.Dashboard
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AStar.Dev.Testing.Dashboard.Server.Tests.Unit", "test\AStar.Dev.Testing.Dashboard.Server.Tests.Unit\AStar.Dev.Testing.Dashboard.Server.Tests.Unit.csproj", "{6060F8F6-FDAC-4379-81D3-8A3CA0627F47}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AStar.Dev.Functional.Extensions", "src\AStar.Dev.Functional.Extensions\AStar.Dev.Functional.Extensions.csproj", "{F83287F8-9CB1-4B20-A469-2D58DADAC9DC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AStar.Dev.Functional.Extensions.Tests.Unit", "test\AStar.Dev.Functional.Extensions.Tests.Unit\AStar.Dev.Functional.Extensions.Tests.Unit.csproj", "{4DB946D7-421E-4602-9D75-3DD5CA64F1F1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AStar.Dev.Utilities.Tests.Unit", "test\AStar.Dev.Utilities.Tests.Unit\AStar.Dev.Utilities.Tests.Unit.csproj", "{3E3708D2-28A5-4B08-8750-E0FC995FF9A8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AStar.Dev.Utilities", "src\AStar.Dev.Utilities\AStar.Dev.Utilities.csproj", "{1E48615F-F35C-4932-A4D8-456510B6172E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -43,6 +51,22 @@ Global
{6060F8F6-FDAC-4379-81D3-8A3CA0627F47}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6060F8F6-FDAC-4379-81D3-8A3CA0627F47}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6060F8F6-FDAC-4379-81D3-8A3CA0627F47}.Release|Any CPU.Build.0 = Release|Any CPU
{F83287F8-9CB1-4B20-A469-2D58DADAC9DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F83287F8-9CB1-4B20-A469-2D58DADAC9DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F83287F8-9CB1-4B20-A469-2D58DADAC9DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F83287F8-9CB1-4B20-A469-2D58DADAC9DC}.Release|Any CPU.Build.0 = Release|Any CPU
{4DB946D7-421E-4602-9D75-3DD5CA64F1F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4DB946D7-421E-4602-9D75-3DD5CA64F1F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4DB946D7-421E-4602-9D75-3DD5CA64F1F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4DB946D7-421E-4602-9D75-3DD5CA64F1F1}.Release|Any CPU.Build.0 = Release|Any CPU
{3E3708D2-28A5-4B08-8750-E0FC995FF9A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3E3708D2-28A5-4B08-8750-E0FC995FF9A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3E3708D2-28A5-4B08-8750-E0FC995FF9A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3E3708D2-28A5-4B08-8750-E0FC995FF9A8}.Release|Any CPU.Build.0 = Release|Any CPU
{1E48615F-F35C-4932-A4D8-456510B6172E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1E48615F-F35C-4932-A4D8-456510B6172E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1E48615F-F35C-4932-A4D8-456510B6172E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1E48615F-F35C-4932-A4D8-456510B6172E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{FC3B1F57-9BAD-4928-B155-74A76E687852} = {06382787-BE50-4C3B-A709-57FA0B6EAADE}
Expand All @@ -51,5 +75,9 @@ Global
{FCC70445-C25B-4DCF-8407-03593B3DE15F} = {06382787-BE50-4C3B-A709-57FA0B6EAADE}
{BBE3B9B2-B4B8-43FE-9A96-3B6C943E98C0} = {F33B86B1-5AC4-403B-A30D-E65848BA0FE6}
{6060F8F6-FDAC-4379-81D3-8A3CA0627F47} = {F33B86B1-5AC4-403B-A30D-E65848BA0FE6}
{F83287F8-9CB1-4B20-A469-2D58DADAC9DC} = {06382787-BE50-4C3B-A709-57FA0B6EAADE}
{4DB946D7-421E-4602-9D75-3DD5CA64F1F1} = {F33B86B1-5AC4-403B-A30D-E65848BA0FE6}
{3E3708D2-28A5-4B08-8750-E0FC995FF9A8} = {F33B86B1-5AC4-403B-A30D-E65848BA0FE6}
{1E48615F-F35C-4932-A4D8-456510B6172E} = {06382787-BE50-4C3B-A709-57FA0B6EAADE}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PackageId>AStar.Dev.Functional.Extensions</PackageId>
<Version>0.2.2-alpha</Version>
<PackageReadmeFile>Readme.md</PackageReadmeFile>
<Authors>Jason</Authors>
<Company>AStar Development</Company>
<Description>F#-inspired Result type with functional combinators, async support, and LINQ integration.</Description>
<PackageTags>result functional monad async linq</PackageTags>
<RepositoryUrl>https://github.com/astar-development/astar-dev-functional-extensions.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/astar-development/astar-dev-functional-extensions</PackageProjectUrl>
<RootNamespace>AStar.Dev.Functional.Extensions</RootNamespace>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<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>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
</ItemGroup>

<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>
24 changes: 24 additions & 0 deletions src/AStar.Dev.Functional.Extensions/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace AStar.Dev.Functional.Extensions;

/// <summary>
/// Contains extensions for <see cref="IEnumerable{T}" />.
/// </summary>
public static class EnumerableExtensions
{
/// <summary>
/// An extension method that, rather than returning null if no object matches the predicate, it will return a suitable instance of <see cref="Option{T}.None" />.
/// </summary>
/// <typeparam name="T">The type of the parameter.</typeparam>
/// <param name="sequence">The sequence of objects to search.</param>
/// <param name="predicate">The predicate to apply.</param>
/// <returns>The option, containing the item or a suitable instance of <see cref="Option{T}.None" />.</returns>
public static Option<T> FirstOrNone<T>(this IEnumerable<T> sequence, Func<T, bool> predicate) =>
sequence.Where(predicate)
.Select<T, Option<T>>(x => x)
.DefaultIfEmpty(Option.None<T>())
.First();
}
13 changes: 13 additions & 0 deletions src/AStar.Dev.Functional.Extensions/ErrorResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace AStar.Dev.Functional.Extensions;

/// <summary>
///
/// </summary>
/// <param name="Message"></param>
public class ErrorResponse
{
/// <summary>
/// Represents the message associated with an error response.
/// </summary>
public string Message { get; set; } = string.Empty;
}
17 changes: 17 additions & 0 deletions src/AStar.Dev.Functional.Extensions/Option.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace AStar.Dev.Functional.Extensions;

/// <summary>
/// Factory methods for creating instances of <see cref="Option{T}" />.
/// </summary>
public static class Option
{
/// <summary>
/// Creates a <c>Some</c> instance containing the specified non-null value.
/// </summary>
public static Option<T> Some<T>(T value) => new Option<T>.Some(value);

/// <summary>
/// Returns a <c>None</c> instance representing an absent value.
/// </summary>
public static Option<T> None<T>() => Option<T>.None.Instance;
}
66 changes: 66 additions & 0 deletions src/AStar.Dev.Functional.Extensions/OptionAsyncExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System;
using System.Threading.Tasks;

namespace AStar.Dev.Functional.Extensions;

/// <summary>
/// Functional async extensions for working with <see cref="Task{Option}" />.
/// </summary>
public static class OptionAsyncExtensions
{
/// <summary>
/// Asynchronously transforms the value inside an <see cref="Option{T}" /> if it exists.
/// </summary>
/// <typeparam name="T">The type of the wrapped value.</typeparam>
/// <typeparam name="TResult">The type of the transformed value.</typeparam>
/// <param name="task">The asynchronous <see cref="Option{T}" /> to await and transform.</param>
/// <param name="func">A mapping function to apply if a value is present.</param>
/// <returns>
/// A <see cref="Task{TResult}" /> wrapping an <see cref="Option{TResult}" /> containing the transformed value, or <c>None</c>.
/// </returns>
public static async Task<Option<TResult>> MapAsync<T, TResult>(
this Task<Option<T>> task,
Func<T, TResult> func)
{
var option = await task;

return option is Option<T>.Some some
? new Option<TResult>.Some(func(some.Value))
: Option.None<TResult>();
}

/// <summary>
/// Asynchronously pattern-matches on an <see cref="Option{T}" />.
/// </summary>
/// <typeparam name="T">The type of the wrapped value.</typeparam>
/// <typeparam name="TResult">The return type of the match operation.</typeparam>
/// <param name="task">The asynchronous <see cref="Option{T}" /> to await.</param>
/// <param name="onSome">A function to invoke if a value is present.</param>
/// <param name="onNone">A function to invoke if no value is present.</param>
/// <returns>
/// A <see cref="Task{TResult}" /> containing the result of the match operation.
/// </returns>
public static async Task<TResult> MatchAsync<T, TResult>(
this Task<Option<T>> task,
Func<T, Task<TResult>> onSome,
Func<Task<TResult>> onNone)
{
var option = await task;

return option is Option<T>.Some some
? await onSome(some.Value)
: await onNone();
}

/// <summary>
/// Asynchronously binds the value to another <see cref="Task{Option}" /> if present.
/// </summary>
public static async Task<Option<TResult>> BindAsync<T, TResult>(
this Task<Option<T>> task,
Func<T, Task<Option<TResult>>> func)
{
var option = await task;

return option is Option<T>.Some some ? await func(some.Value) : Option.None<TResult>();
}
}
115 changes: 115 additions & 0 deletions src/AStar.Dev.Functional.Extensions/OptionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;

namespace AStar.Dev.Functional.Extensions;

/// <summary>
/// Functional helpers and utilities for working with <see cref="Option{T}" />.
/// </summary>
public static class OptionExtensions
{
/// <summary>
/// Attempts to extract the value from an <see cref="Option{T}" />.
/// </summary>
public static bool TryGetValue<T>(this Option<T> option, out T value)
{
if (option is Option<T>.Some some)
{
value = some.Value;

return true;
}

value = default!;

return false;
}

/// <summary>
/// Converts a value to an <see cref="Option{T}" />, treating default/null as <c>None</c>.
/// </summary>
public static Option<T> ToOption<T>(this T value) =>
EqualityComparer<T>.Default.Equals(value, default!)
? Option.None<T>()
: new Option<T>.Some(value);

/// <summary>
/// Converts a value to an <see cref="Option{T}" /> if it satisfies the predicate.
/// </summary>
public static Option<T> ToOption<T>(this T value, Func<T, bool> predicate) =>
predicate(value)
? new Option<T>.Some(value)
: Option.None<T>();

/// <summary>
/// Converts a nullable value type to an <see cref="Option{T}" />.
/// </summary>
public static Option<T> ToOption<T>(this T? nullable) where T : struct =>
nullable.HasValue
? new Option<T>.Some(nullable.Value)
: Option.None<T>();

/// <summary>
/// Transforms the value inside an <see cref="Option{T}" /> if present.
/// </summary>
public static Option<TResult> Map<T, TResult>(this Option<T> option, Func<T, TResult> map) =>
option.Match(
some => new Option<TResult>.Some(map(some)),
Option.None<TResult>);

/// <summary>
/// Chains another <see cref="Option{T}" />-producing function.
/// </summary>
public static Option<TResult> Bind<T, TResult>(this Option<T> option, Func<T, Option<TResult>> bind) => option.Match(bind, Option.None<TResult>);

/// <summary>
/// Converts an <see cref="Option{T}" /> to a <see cref="Result{T, TError}" />.
/// </summary>
public static Result<T, TError> ToResult<T, TError>(this Option<T> option, Func<TError> errorFactory) =>
option.Match<Result<T, TError>>(
some => new Result<T, TError>.Ok(some),
() => new Result<T, TError>.Error(errorFactory()));

/// <summary>
/// Determines whether the option contains a value.
/// </summary>
public static bool IsSome<T>(this Option<T> option) => option is Option<T>.Some;

/// <summary>
/// Determines whether the option is empty.
/// </summary>
public static bool IsNone<T>(this Option<T> option) => option is Option<T>.None;

/// <summary>
/// Converts an <see cref="Option{T}" /> to a nullable type.
/// </summary>
public static T? ToNullable<T>(this Option<T> option) where T : struct => option is Option<T>.Some some ? some.Value : null;

/// <summary>
/// Converts an <see cref="Option{T}" /> to a single-element enumerable or an empty sequence.
/// </summary>
public static IEnumerable<T> ToEnumerable<T>(this Option<T> option) => option is Option<T>.Some some ? [ some.Value ] : [];

/// <summary>
/// Gets the value of the option or returns a fallback value.
/// </summary>
public static T OrElse<T>(this Option<T> option, T fallback) => option is Option<T>.Some some ? some.Value : fallback;

/// <summary>
/// Gets the value of the option or throws an exception if absent.
/// </summary>
public static T OrThrow<T>(this Option<T> option, Exception? ex = null) => option is Option<T>.Some some ? some.Value : throw ex ?? new InvalidOperationException("No value present");

/// <summary>
/// Enables deconstruction of an option into a boolean and value pair.
/// </summary>
/// <param name="option"></param>
/// <param name="isSome"></param>
/// <param name="value"></param>
/// <typeparam name="T"></typeparam>
public static void Deconstruct<T>(this Option<T> option, out bool isSome, out T? value)
{
isSome = option is Option<T>.Some;
value = isSome ? ((Option<T>.Some)option).Value : default;
}
}
38 changes: 38 additions & 0 deletions src/AStar.Dev.Functional.Extensions/OptionLinqExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Threading.Tasks;

namespace AStar.Dev.Functional.Extensions;

/// <summary>
/// LINQ-style query support for <see cref="Option{T}" />.
/// </summary>
public static class OptionLinqExtensions
{
/// <summary>
/// Projects the value of a <see cref="Option{T}" /> using the specified function.
/// </summary>
public static Option<TResult> Select<T, TResult>(this Option<T> option, Func<T, TResult> selector) => option.Map(selector);

/// <summary>
/// Projects and flattens nested <see cref="Option{T}" /> structures using a LINQ-style binding function.
/// </summary>
public static Option<TResult> SelectMany<T, TIntermediate, TResult>(
this Option<T> option,
Func<T, Option<TIntermediate>> bind,
Func<T, TIntermediate, TResult> project) =>
option.Bind(x => bind(x).Map(y => project(x, y)));

/// <summary>
/// Asynchronously projects the value of a <see cref="Task{Option}" /> using the specified function.
/// </summary>
public static async Task<Option<TResult>> SelectAwait<T, TResult>(
this Task<Option<T>> task,
Func<T, Task<TResult>> selector)
{
var option = await task;

return option is Option<T>.Some some
? new Option<TResult>.Some(await selector(some.Value))
: Option.None<TResult>();
}
}
Loading
Loading