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
14 changes: 14 additions & 0 deletions PowerKit/Disposable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,31 @@

namespace PowerKit;

/// <summary>
/// Provides utility methods for creating and composing <see cref="IDisposable" /> instances.
/// </summary>
internal partial class Disposable(Action dispose) : IDisposable
{
/// <inheritdoc />
public void Dispose() => dispose();
}

internal partial class Disposable
{
/// <summary>
/// Gets a disposable that performs no action when disposed.
/// </summary>
public static IDisposable Null { get; } = Create(() => { });

/// <summary>
/// Creates a disposable that invokes the specified action when disposed.
/// </summary>
public static IDisposable Create(Action dispose) => new Disposable(dispose);

/// <summary>
/// Creates a disposable that disposes all specified disposables when disposed,
/// aggregating any exceptions thrown during disposal.
/// </summary>
public static IDisposable Merge(params IEnumerable<IDisposable> disposables) =>
Create(() =>
{
Expand Down
4 changes: 4 additions & 0 deletions PowerKit/Extensions/AggregateExceptionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ internal static class AggregateExceptionExtensions
{
extension(AggregateException exception)
{
/// <summary>
/// Returns the single inner exception if the aggregate contains exactly one after flattening;
/// otherwise, returns <see langword="null" />.
/// </summary>
public Exception? TryGetSingle() =>
exception.Flatten().InnerExceptions is [var single] ? single : null;
}
Expand Down
13 changes: 13 additions & 0 deletions PowerKit/Extensions/AsyncEnumerableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ internal static class AsyncEnumerableExtensions
{
extension<T>(IAsyncEnumerable<T> source)
{
/// <summary>
/// Returns a specified number of elements from the start of the async sequence.
/// </summary>
public async IAsyncEnumerable<T> TakeAsync(
int count,
[EnumeratorCancellation] CancellationToken cancellationToken = default
Expand All @@ -32,6 +35,10 @@ public async IAsyncEnumerable<T> TakeAsync(
}
}

/// <summary>
/// Projects each element of the async sequence to an <see cref="IEnumerable{TResult}" />
/// and flattens the resulting sequences into one async sequence.
/// </summary>
public async IAsyncEnumerable<TResult> SelectManyAsync<TResult>(
Func<T, IEnumerable<TResult>> transform,
[EnumeratorCancellation] CancellationToken cancellationToken = default
Expand All @@ -50,6 +57,9 @@ var item in source
}
}

/// <summary>
/// Materializes the async sequence into a <see cref="List{T}" />.
/// </summary>
public async ValueTask<List<T>> ToListAsync(
CancellationToken cancellationToken = default
)
Expand All @@ -68,6 +78,9 @@ var item in source
return list;
}

/// <summary>
/// Enables directly awaiting the async sequence, materializing it into a <see cref="List{T}" />.
/// </summary>
public ValueTaskAwaiter<List<T>> GetAwaiter() => source.ToListAsync().GetAwaiter();
}
}
9 changes: 9 additions & 0 deletions PowerKit/Extensions/ComparableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ internal static class ComparableExtensions
{
extension<T>(T value) where T : IComparable<T>
{
/// <summary>
/// Clamps the value to the specified range.
/// </summary>
public T Clamp(T min, T max)
{
if (value.CompareTo(min) < 0)
Expand All @@ -17,8 +20,14 @@ public T Clamp(T min, T max)
return value;
}

/// <summary>
/// Returns the smaller of the current value and the specified value.
/// </summary>
public T Min(T other) => value.CompareTo(other) <= 0 ? value : other;

/// <summary>
/// Returns the larger of the current value and the specified value.
/// </summary>
public T Max(T other) => value.CompareTo(other) >= 0 ? value : other;
}
}
24 changes: 24 additions & 0 deletions PowerKit/Extensions/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ internal static class EnumerableExtensions
{
extension<T>(T obj)
{
/// <summary>
/// Wraps the object in an enumerable containing a single element.
/// </summary>
public IEnumerable<T> ToSingletonEnumerable()
{
yield return obj;
Expand All @@ -16,6 +19,9 @@ public IEnumerable<T> ToSingletonEnumerable()
extension<T>(IEnumerable<T?> source)
where T : class
{
/// <summary>
/// Filters out <see langword="null" /> elements from the sequence.
/// </summary>
public IEnumerable<T> WhereNotNull()
{
foreach (var item in source)
Expand All @@ -31,6 +37,9 @@ public IEnumerable<T> WhereNotNull()
extension<T>(IEnumerable<T?> source)
where T : struct
{
/// <summary>
/// Filters out <see langword="null" /> elements from the sequence of nullable value types.
/// </summary>
public IEnumerable<T> WhereNotNull()
{
foreach (var item in source)
Expand All @@ -45,6 +54,9 @@ public IEnumerable<T> WhereNotNull()

extension(IEnumerable<string?> source)
{
/// <summary>
/// Filters out <see langword="null" /> and empty strings from the sequence.
/// </summary>
public IEnumerable<string> WhereNotNullOrEmpty()
{
foreach (var item in source)
Expand All @@ -56,6 +68,9 @@ public IEnumerable<string> WhereNotNullOrEmpty()
}
}

/// <summary>
/// Filters out <see langword="null" />, empty, and whitespace-only strings from the sequence.
/// </summary>
public IEnumerable<string> WhereNotNullOrWhiteSpace()
{
foreach (var item in source)
Expand All @@ -71,6 +86,9 @@ public IEnumerable<string> WhereNotNullOrWhiteSpace()
extension<T>(IEnumerable<T> source)
where T : struct
{
/// <summary>
/// Returns the first element of the sequence, or <see langword="null" /> if the sequence is empty.
/// </summary>
public T? FirstOrNull()
{
foreach (var item in source)
Expand All @@ -81,6 +99,9 @@ public IEnumerable<string> WhereNotNullOrWhiteSpace()
return null;
}

/// <summary>
/// Returns the last element of the sequence, or <see langword="null" /> if the sequence is empty.
/// </summary>
public T? LastOrNull()
{
if (source is IReadOnlyList<T> list)
Expand All @@ -98,6 +119,9 @@ public IEnumerable<string> WhereNotNullOrWhiteSpace()
return last;
}

/// <summary>
/// Returns the element at the specified index, or <see langword="null" /> if the index is out of range.
/// </summary>
public T? ElementAtOrNull(int index)
{
var list = source as IReadOnlyList<T> ?? source.ToArray();
Expand Down
4 changes: 4 additions & 0 deletions PowerKit/Extensions/ExceptionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ internal static class ExceptionExtensions
{
extension(Exception exception)
{
/// <summary>
/// Returns a flat list containing the exception itself and all of its
/// nested inner exceptions, recursively unwrapping <see cref="AggregateException" /> instances.
/// </summary>
public IReadOnlyList<Exception> GetSelfAndDescendants()
{
static void PopulateDescendants(Exception ex, ICollection<Exception> result)
Expand Down
16 changes: 16 additions & 0 deletions PowerKit/Extensions/FunctionalExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,38 @@ internal static class FunctionalExtensions
{
extension<TIn>(TIn input)
{
/// <summary>
/// Passes the value through the specified transform function and returns the result.
/// </summary>
public TOut Pipe<TOut>(Func<TIn, TOut> transform) => transform(input);
}

extension<T>(T value)
where T : struct
{
/// <summary>
/// Returns <see langword="null" /> if the value matches the specified predicate; otherwise, returns the value.
/// </summary>
public T? NullIf(Func<T, bool> predicate) => !predicate(value) ? value : null;

/// <summary>
/// Returns <see langword="null" /> if the value equals the default value for its type; otherwise, returns the value.
/// </summary>
public T? NullIfDefault() =>
value.NullIf(v => EqualityComparer<T>.Default.Equals(v, default));
}

extension(string value)
{
/// <summary>
/// Returns <see langword="null" /> if the string is <see langword="null" /> or empty; otherwise, returns the string.
/// </summary>
public string? NullIfEmpty() => !string.IsNullOrEmpty(value) ? value : null;

/// <summary>
/// Returns <see langword="null" /> if the string is <see langword="null" />, empty, or consists only of whitespace;
/// otherwise, returns the string.
/// </summary>
public string? NullIfWhiteSpace() => !string.IsNullOrWhiteSpace(value) ? value : null;
}
}
15 changes: 15 additions & 0 deletions PowerKit/Extensions/PathExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,31 @@ internal static class PathExtensions
{
extension(Path)
{
/// <summary>
/// Gets the characters that are invalid in file names.
/// When <paramref name="crossPlatform" /> is <see langword="true" />, returns characters
/// invalid across all major filesystems; otherwise, returns the OS-specific set.
/// </summary>
public static char[] GetInvalidFileNameChars(bool crossPlatform) =>
crossPlatform
? PathEx.CrossPlatformInvalidFileNameChars
: Path.GetInvalidFileNameChars();

/// <summary>
/// Gets the characters that are invalid in paths.
/// When <paramref name="crossPlatform" /> is <see langword="true" />, returns characters
/// invalid across all major filesystems; otherwise, returns the OS-specific set.
/// </summary>
public static char[] GetInvalidPathChars(bool crossPlatform) =>
crossPlatform
? PathEx.CrossPlatformInvalidPathChars
: Path.GetInvalidPathChars();

/// <summary>
/// Replaces invalid file name characters with underscores and strips trailing dots and whitespace.
/// When <paramref name="crossPlatform" /> is <see langword="true" />, considers characters
/// invalid across all major filesystems.
/// </summary>
public static string EscapeFileName(string fileName, bool crossPlatform = true)
{
var invalidChars = new HashSet<char>(Path.GetInvalidFileNameChars(crossPlatform));
Expand Down
11 changes: 11 additions & 0 deletions PowerKit/Extensions/StreamExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ internal static class StreamExtensions
{
extension(Stream source)
{
/// <summary>
/// Copies the contents of the stream to the destination stream, optionally flushing after each write.
/// </summary>
public async Task CopyToAsync(
Stream destination,
bool autoFlush,
Expand Down Expand Up @@ -40,6 +43,10 @@ await destination
}
}

/// <summary>
/// Copies the contents of the stream to the destination stream, reporting progress
/// as a ratio of bytes read to <paramref name="contentLength" />.
/// </summary>
public async ValueTask CopyToAsync(
Stream destination,
long contentLength,
Expand Down Expand Up @@ -75,6 +82,10 @@ await destination
}
}

/// <summary>
/// Copies the contents of the stream to the destination stream, reporting progress
/// based on the source stream's length when available.
/// </summary>
public async ValueTask CopyToAsync(
Stream destination,
IProgress<double>? progress = null,
Expand Down
6 changes: 6 additions & 0 deletions PowerKit/Extensions/StringBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ internal static class StringBuilderExtensions
{
extension(StringBuilder builder)
{
/// <summary>
/// Appends the specified character only if the builder is not empty.
/// </summary>
public StringBuilder AppendIfNotEmpty(char value) =>
builder.Length > 0 ? builder.Append(value) : builder;

/// <summary>
/// Removes leading and trailing whitespace characters from the builder.
/// </summary>
public StringBuilder Trim()
{
var start = 0;
Expand Down
28 changes: 28 additions & 0 deletions PowerKit/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ internal static class StringExtensions
{
extension(string str)
{
/// <summary>
/// Returns the substring before the first occurrence of the specified substring.
/// If the substring is not found, returns the original string.
/// </summary>
public string SubstringUntil(
string sub,
StringComparison comparison = StringComparison.Ordinal
Expand All @@ -17,6 +21,10 @@ public string SubstringUntil(
_ => str,
};

/// <summary>
/// Returns the substring before the last occurrence of the specified substring.
/// If the substring is not found, returns the original string.
/// </summary>
public string SubstringUntilLast(
string sub,
StringComparison comparison = StringComparison.Ordinal
Expand All @@ -27,6 +35,10 @@ public string SubstringUntilLast(
_ => str,
};

/// <summary>
/// Returns the substring after the first occurrence of the specified substring.
/// If the substring is not found, returns an empty string.
/// </summary>
public string SubstringAfter(
string sub,
StringComparison comparison = StringComparison.Ordinal
Expand All @@ -37,6 +49,10 @@ public string SubstringAfter(
_ => "",
};

/// <summary>
/// Returns the substring after the last occurrence of the specified substring.
/// If the substring is not found, returns an empty string.
/// </summary>
public string SubstringAfterLast(
string sub,
StringComparison comparison = StringComparison.Ordinal
Expand All @@ -47,8 +63,14 @@ public string SubstringAfterLast(
_ => "",
};

/// <summary>
/// Truncates the string to the specified maximum number of characters.
/// </summary>
public string Truncate(int charCount) => str.Length > charCount ? str[..charCount] : str;

/// <summary>
/// Inserts the specified separator before each uppercase letter, splitting PascalCase words.
Comment thread
Tyrrrz marked this conversation as resolved.
/// </summary>
public string SeparateWords(char separator)
{
var builder = new StringBuilder(str.Length * 2);
Expand All @@ -66,8 +88,14 @@ public string SeparateWords(char separator)
return builder.ToString();
}

/// <summary>
/// Converts the PascalCase string to kebab-case (e.g., "FooBar" → "foo-bar").
/// </summary>
public string ToKebabCase() => str.SeparateWords('-').ToLowerInvariant();

/// <summary>
/// Converts the PascalCase string to snake_case (e.g., "FooBar" → "foo_bar").
/// </summary>
public string ToSnakeCase() => str.SeparateWords('_').ToLowerInvariant();
}
}
Loading