diff --git a/PowerKit/Disposable.cs b/PowerKit/Disposable.cs index eb2179f..b144ef0 100644 --- a/PowerKit/Disposable.cs +++ b/PowerKit/Disposable.cs @@ -3,17 +3,31 @@ namespace PowerKit; +/// +/// Provides utility methods for creating and composing instances. +/// internal partial class Disposable(Action dispose) : IDisposable { + /// public void Dispose() => dispose(); } internal partial class Disposable { + /// + /// Gets a disposable that performs no action when disposed. + /// public static IDisposable Null { get; } = Create(() => { }); + /// + /// Creates a disposable that invokes the specified action when disposed. + /// public static IDisposable Create(Action dispose) => new Disposable(dispose); + /// + /// Creates a disposable that disposes all specified disposables when disposed, + /// aggregating any exceptions thrown during disposal. + /// public static IDisposable Merge(params IEnumerable disposables) => Create(() => { diff --git a/PowerKit/Extensions/AggregateExceptionExtensions.cs b/PowerKit/Extensions/AggregateExceptionExtensions.cs index c0d0fea..1b5d6f8 100644 --- a/PowerKit/Extensions/AggregateExceptionExtensions.cs +++ b/PowerKit/Extensions/AggregateExceptionExtensions.cs @@ -6,6 +6,10 @@ internal static class AggregateExceptionExtensions { extension(AggregateException exception) { + /// + /// Returns the single inner exception if the aggregate contains exactly one after flattening; + /// otherwise, returns . + /// public Exception? TryGetSingle() => exception.Flatten().InnerExceptions is [var single] ? single : null; } diff --git a/PowerKit/Extensions/AsyncEnumerableExtensions.cs b/PowerKit/Extensions/AsyncEnumerableExtensions.cs index b3ac565..ec506f9 100644 --- a/PowerKit/Extensions/AsyncEnumerableExtensions.cs +++ b/PowerKit/Extensions/AsyncEnumerableExtensions.cs @@ -10,6 +10,9 @@ internal static class AsyncEnumerableExtensions { extension(IAsyncEnumerable source) { + /// + /// Returns a specified number of elements from the start of the async sequence. + /// public async IAsyncEnumerable TakeAsync( int count, [EnumeratorCancellation] CancellationToken cancellationToken = default @@ -32,6 +35,10 @@ public async IAsyncEnumerable TakeAsync( } } + /// + /// Projects each element of the async sequence to an + /// and flattens the resulting sequences into one async sequence. + /// public async IAsyncEnumerable SelectManyAsync( Func> transform, [EnumeratorCancellation] CancellationToken cancellationToken = default @@ -50,6 +57,9 @@ var item in source } } + /// + /// Materializes the async sequence into a . + /// public async ValueTask> ToListAsync( CancellationToken cancellationToken = default ) @@ -68,6 +78,9 @@ var item in source return list; } + /// + /// Enables directly awaiting the async sequence, materializing it into a . + /// public ValueTaskAwaiter> GetAwaiter() => source.ToListAsync().GetAwaiter(); } } diff --git a/PowerKit/Extensions/ComparableExtensions.cs b/PowerKit/Extensions/ComparableExtensions.cs index b8e0639..2a96616 100644 --- a/PowerKit/Extensions/ComparableExtensions.cs +++ b/PowerKit/Extensions/ComparableExtensions.cs @@ -6,6 +6,9 @@ internal static class ComparableExtensions { extension(T value) where T : IComparable { + /// + /// Clamps the value to the specified range. + /// public T Clamp(T min, T max) { if (value.CompareTo(min) < 0) @@ -17,8 +20,14 @@ public T Clamp(T min, T max) return value; } + /// + /// Returns the smaller of the current value and the specified value. + /// public T Min(T other) => value.CompareTo(other) <= 0 ? value : other; + /// + /// Returns the larger of the current value and the specified value. + /// public T Max(T other) => value.CompareTo(other) >= 0 ? value : other; } } diff --git a/PowerKit/Extensions/EnumerableExtensions.cs b/PowerKit/Extensions/EnumerableExtensions.cs index 378c712..d7be50a 100644 --- a/PowerKit/Extensions/EnumerableExtensions.cs +++ b/PowerKit/Extensions/EnumerableExtensions.cs @@ -7,6 +7,9 @@ internal static class EnumerableExtensions { extension(T obj) { + /// + /// Wraps the object in an enumerable containing a single element. + /// public IEnumerable ToSingletonEnumerable() { yield return obj; @@ -16,6 +19,9 @@ public IEnumerable ToSingletonEnumerable() extension(IEnumerable source) where T : class { + /// + /// Filters out elements from the sequence. + /// public IEnumerable WhereNotNull() { foreach (var item in source) @@ -31,6 +37,9 @@ public IEnumerable WhereNotNull() extension(IEnumerable source) where T : struct { + /// + /// Filters out elements from the sequence of nullable value types. + /// public IEnumerable WhereNotNull() { foreach (var item in source) @@ -45,6 +54,9 @@ public IEnumerable WhereNotNull() extension(IEnumerable source) { + /// + /// Filters out and empty strings from the sequence. + /// public IEnumerable WhereNotNullOrEmpty() { foreach (var item in source) @@ -56,6 +68,9 @@ public IEnumerable WhereNotNullOrEmpty() } } + /// + /// Filters out , empty, and whitespace-only strings from the sequence. + /// public IEnumerable WhereNotNullOrWhiteSpace() { foreach (var item in source) @@ -71,6 +86,9 @@ public IEnumerable WhereNotNullOrWhiteSpace() extension(IEnumerable source) where T : struct { + /// + /// Returns the first element of the sequence, or if the sequence is empty. + /// public T? FirstOrNull() { foreach (var item in source) @@ -81,6 +99,9 @@ public IEnumerable WhereNotNullOrWhiteSpace() return null; } + /// + /// Returns the last element of the sequence, or if the sequence is empty. + /// public T? LastOrNull() { if (source is IReadOnlyList list) @@ -98,6 +119,9 @@ public IEnumerable WhereNotNullOrWhiteSpace() return last; } + /// + /// Returns the element at the specified index, or if the index is out of range. + /// public T? ElementAtOrNull(int index) { var list = source as IReadOnlyList ?? source.ToArray(); diff --git a/PowerKit/Extensions/ExceptionExtensions.cs b/PowerKit/Extensions/ExceptionExtensions.cs index ab721f7..08cd54c 100644 --- a/PowerKit/Extensions/ExceptionExtensions.cs +++ b/PowerKit/Extensions/ExceptionExtensions.cs @@ -7,6 +7,10 @@ internal static class ExceptionExtensions { extension(Exception exception) { + /// + /// Returns a flat list containing the exception itself and all of its + /// nested inner exceptions, recursively unwrapping instances. + /// public IReadOnlyList GetSelfAndDescendants() { static void PopulateDescendants(Exception ex, ICollection result) diff --git a/PowerKit/Extensions/FunctionalExtensions.cs b/PowerKit/Extensions/FunctionalExtensions.cs index 5e40566..894c3d1 100644 --- a/PowerKit/Extensions/FunctionalExtensions.cs +++ b/PowerKit/Extensions/FunctionalExtensions.cs @@ -7,22 +7,38 @@ internal static class FunctionalExtensions { extension(TIn input) { + /// + /// Passes the value through the specified transform function and returns the result. + /// public TOut Pipe(Func transform) => transform(input); } extension(T value) where T : struct { + /// + /// Returns if the value matches the specified predicate; otherwise, returns the value. + /// public T? NullIf(Func predicate) => !predicate(value) ? value : null; + /// + /// Returns if the value equals the default value for its type; otherwise, returns the value. + /// public T? NullIfDefault() => value.NullIf(v => EqualityComparer.Default.Equals(v, default)); } extension(string value) { + /// + /// Returns if the string is or empty; otherwise, returns the string. + /// public string? NullIfEmpty() => !string.IsNullOrEmpty(value) ? value : null; + /// + /// Returns if the string is , empty, or consists only of whitespace; + /// otherwise, returns the string. + /// public string? NullIfWhiteSpace() => !string.IsNullOrWhiteSpace(value) ? value : null; } } diff --git a/PowerKit/Extensions/PathExtensions.cs b/PowerKit/Extensions/PathExtensions.cs index 003ddc7..87e705d 100644 --- a/PowerKit/Extensions/PathExtensions.cs +++ b/PowerKit/Extensions/PathExtensions.cs @@ -42,16 +42,31 @@ internal static class PathExtensions { extension(Path) { + /// + /// Gets the characters that are invalid in file names. + /// When is , returns characters + /// invalid across all major filesystems; otherwise, returns the OS-specific set. + /// public static char[] GetInvalidFileNameChars(bool crossPlatform) => crossPlatform ? PathEx.CrossPlatformInvalidFileNameChars : Path.GetInvalidFileNameChars(); + /// + /// Gets the characters that are invalid in paths. + /// When is , returns characters + /// invalid across all major filesystems; otherwise, returns the OS-specific set. + /// public static char[] GetInvalidPathChars(bool crossPlatform) => crossPlatform ? PathEx.CrossPlatformInvalidPathChars : Path.GetInvalidPathChars(); + /// + /// Replaces invalid file name characters with underscores and strips trailing dots and whitespace. + /// When is , considers characters + /// invalid across all major filesystems. + /// public static string EscapeFileName(string fileName, bool crossPlatform = true) { var invalidChars = new HashSet(Path.GetInvalidFileNameChars(crossPlatform)); diff --git a/PowerKit/Extensions/StreamExtensions.cs b/PowerKit/Extensions/StreamExtensions.cs index e392e83..cb865a1 100644 --- a/PowerKit/Extensions/StreamExtensions.cs +++ b/PowerKit/Extensions/StreamExtensions.cs @@ -10,6 +10,9 @@ internal static class StreamExtensions { extension(Stream source) { + /// + /// Copies the contents of the stream to the destination stream, optionally flushing after each write. + /// public async Task CopyToAsync( Stream destination, bool autoFlush, @@ -40,6 +43,10 @@ await destination } } + /// + /// Copies the contents of the stream to the destination stream, reporting progress + /// as a ratio of bytes read to . + /// public async ValueTask CopyToAsync( Stream destination, long contentLength, @@ -75,6 +82,10 @@ await destination } } + /// + /// Copies the contents of the stream to the destination stream, reporting progress + /// based on the source stream's length when available. + /// public async ValueTask CopyToAsync( Stream destination, IProgress? progress = null, diff --git a/PowerKit/Extensions/StringBuilderExtensions.cs b/PowerKit/Extensions/StringBuilderExtensions.cs index d8e73df..4a12767 100644 --- a/PowerKit/Extensions/StringBuilderExtensions.cs +++ b/PowerKit/Extensions/StringBuilderExtensions.cs @@ -6,9 +6,15 @@ internal static class StringBuilderExtensions { extension(StringBuilder builder) { + /// + /// Appends the specified character only if the builder is not empty. + /// public StringBuilder AppendIfNotEmpty(char value) => builder.Length > 0 ? builder.Append(value) : builder; + /// + /// Removes leading and trailing whitespace characters from the builder. + /// public StringBuilder Trim() { var start = 0; diff --git a/PowerKit/Extensions/StringExtensions.cs b/PowerKit/Extensions/StringExtensions.cs index 2320da9..101fe1c 100644 --- a/PowerKit/Extensions/StringExtensions.cs +++ b/PowerKit/Extensions/StringExtensions.cs @@ -7,6 +7,10 @@ internal static class StringExtensions { extension(string str) { + /// + /// Returns the substring before the first occurrence of the specified substring. + /// If the substring is not found, returns the original string. + /// public string SubstringUntil( string sub, StringComparison comparison = StringComparison.Ordinal @@ -17,6 +21,10 @@ public string SubstringUntil( _ => str, }; + /// + /// Returns the substring before the last occurrence of the specified substring. + /// If the substring is not found, returns the original string. + /// public string SubstringUntilLast( string sub, StringComparison comparison = StringComparison.Ordinal @@ -27,6 +35,10 @@ public string SubstringUntilLast( _ => str, }; + /// + /// Returns the substring after the first occurrence of the specified substring. + /// If the substring is not found, returns an empty string. + /// public string SubstringAfter( string sub, StringComparison comparison = StringComparison.Ordinal @@ -37,6 +49,10 @@ public string SubstringAfter( _ => "", }; + /// + /// Returns the substring after the last occurrence of the specified substring. + /// If the substring is not found, returns an empty string. + /// public string SubstringAfterLast( string sub, StringComparison comparison = StringComparison.Ordinal @@ -47,8 +63,14 @@ public string SubstringAfterLast( _ => "", }; + /// + /// Truncates the string to the specified maximum number of characters. + /// public string Truncate(int charCount) => str.Length > charCount ? str[..charCount] : str; + /// + /// Inserts the specified separator before each uppercase letter, splitting PascalCase words. + /// public string SeparateWords(char separator) { var builder = new StringBuilder(str.Length * 2); @@ -66,8 +88,14 @@ public string SeparateWords(char separator) return builder.ToString(); } + /// + /// Converts the PascalCase string to kebab-case (e.g., "FooBar" → "foo-bar"). + /// public string ToKebabCase() => str.SeparateWords('-').ToLowerInvariant(); + /// + /// Converts the PascalCase string to snake_case (e.g., "FooBar" → "foo_bar"). + /// public string ToSnakeCase() => str.SeparateWords('_').ToLowerInvariant(); } } diff --git a/PowerKit/Extensions/TextReaderExtensions.cs b/PowerKit/Extensions/TextReaderExtensions.cs index 08e1323..72acb84 100644 --- a/PowerKit/Extensions/TextReaderExtensions.cs +++ b/PowerKit/Extensions/TextReaderExtensions.cs @@ -9,6 +9,9 @@ internal static class TextReaderExtensions { extension(TextReader reader) { + /// + /// Reads all lines from the text reader as an async sequence. + /// public async IAsyncEnumerable ReadLinesAsync( [EnumeratorCancellation] CancellationToken cancellationToken = default )