Skip to content

Commit

Permalink
All kinds of stuff
Browse files Browse the repository at this point in the history
* Filled out the Terminal offering a bunch hoisting up stuff from my other cli tooling
* Bugfixes
* New utilities
* Better tests
* Cleanup
  • Loading branch information
scottbilas committed Dec 1, 2024
1 parent f3b7bb8 commit e531e72
Show file tree
Hide file tree
Showing 39 changed files with 1,833 additions and 324 deletions.
11 changes: 11 additions & 0 deletions src/Core/Core-Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,15 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>$(NoWarn);CA1311</NoWarn>
</PropertyGroup>

<ItemGroup>
<None Update="SpanExtensions.t.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>SpanExtensions.t.cs</LastGenOutput>
</None>
<Compile Update="SpanExtensions.t.cs">
<DependentUpon>SpanExtensions.t.tt</DependentUpon>
</Compile>
</ItemGroup>

</Project>
24 changes: 15 additions & 9 deletions src/Core/Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,18 @@
<!-- nuget -->
<PropertyGroup>
<GeneratePackageOnBuild>$(_IsPublishing)</GeneratePackageOnBuild>
<Title>Some OK core utility functions</Title>
<Title>Some OK core utility functions.</Title>
<RepositoryUrl>https://github.com/scottbilas/OkTools</RepositoryUrl>
<PackageVersion>1.0.18</PackageVersion>
<PackageVersion>1.0.19</PackageVersion>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseFile>LICENSE.md</PackageLicenseFile>
</PropertyGroup>
<ItemGroup>
<Using Include="OkTools.Core" />
<Using Include="OkTools.Core.Extensions" />

<None Include="README.md" Pack="true" PackagePath="/" />
<None Include="../../LICENSE.md" Pack="true" PackagePath="/" />
<None Update="WideTypes.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>WideTypes.cs</LastGenOutput>
</None>
</ItemGroup>

<!-- dependencies -->
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
Expand All @@ -47,10 +41,22 @@
<PackageReference Include="Microsoft.CSharp" Condition="'$(TargetFramework)' == 'netstandard2.1'" />
</ItemGroup>

<!-- t4 (TODO: move into Library.targets, also get build-based gen working again)-->
<ItemGroup>
<None Update="WideTypes.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>WideTypes.cs</LastGenOutput>
</None>
<Compile Update="WideTypes.cs">
<DependentUpon>WideTypes.tt</DependentUpon>
</Compile>
<None Update="Extensions\SpanExtensions.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>SpanExtensions.cs</LastGenOutput>
</None>
<Compile Update="Extensions\SpanExtensions.cs">
<DependentUpon>SpanExtensions.tt</DependentUpon>
</Compile>
</ItemGroup>

</Project>
164 changes: 164 additions & 0 deletions src/Core/DebugDump.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
using System.Diagnostics;
using System.Reflection;
using System.Text;

public static class DebugDump
{
public static string ToEscapedString(this string @this)
{
var count = 0;
for (var i = 0; i != @this.Length; ++i)
{
switch (@this[i])
{
case '\n':
case '\r':
case '\t':
++count;
break;
}
}

var buf = new char[@this.Length + count];
for (var (i, o) = (0, 0); i != @this.Length; ++i, ++o)
{
switch (@this[i])
{
case '\n':
buf[o++] = '\\';
buf[o] = 'n';
break;
case '\r':
buf[o++] = '\\';
buf[o] = 'r';
break;
case '\t':
buf[o++] = '\\';
buf[o] = 't';
break;
default:
buf[o] = @this[i];
break;
}
}

return new string(buf);
}

public static string ToArgString(this string? @this) =>
@this != null ? $"\"{@this.ToEscapedString()}\"" : "null";

const int k_indentMultiple = 2;

// TODO(TRIM): consider something like https://github.com/byme8/Apparatus.AOT.Reflection

public static string ToDumpString(this object @this, string? name = null, bool quiet = true, Func<PropertyInfo, bool>? filter = null, int? wrapWidth = null, int maxWrapLines = 0)
{
// i hate this function impl, but i'm not going to let that stop me from making it worse!

var sb = new StringBuilder();
var indent = 0;

if (name != null)
{
sb.AppendLine($"['{name}' {@this.GetType().FullName}]");
++indent;
}

var properties = @this
.GetType()
.GetProperties()
.Where(pi => filter?.Invoke(pi) ?? true)
.OrderBy(pi => pi.Name)
.Select(pi =>
{
string? text = null;

var value = pi.GetValue(@this);
switch (value)
{
case null:
if (!quiet)
text = "null";
break;
case bool b:
if (b || !quiet)
text = b.ToString().ToLower();
break;
case IntPtr p:
if (p != default || !quiet)
text = $"0x{p:x}";
break;
case string str:
text = str.ToArgString();
break;
case IEnumerable<string> enumerable:
text = '[' + enumerable.Select(s => s.ToArgString()).StringJoin(", ") + ']';
break;
case IEnumerable<KeyValuePair<string, string?>> stringDict:
if (@this is ProcessStartInfo && pi.Name == "Environment")
{
stringDict = stringDict
.OrderBy(kv => Environment.GetEnvironmentVariable(kv.Key) != kv.Value ? -1 : 1) // inherited env vars go at the bottom
.ThenBy(kv => kv.Key);
}
else
stringDict = stringDict.OrderBy(kv => kv.Key);
text = '[' + stringDict.Select(kv => $"{kv.Key}={kv.Value.ToArgString()}").StringJoin(", ") + ']';
break;
case NPath npath:
text = npath.ToString(SlashMode.Native);
break;
default:
text = value.ToString();
break;
}

return (name: pi.Name, text);
})
.Where(p => p.text != null)
.ToArray();

var maxNameLen = properties.Max(p => p.name.Length);

foreach (var p in properties)
{
var text = p.text!;

var wrapIndent = sb.Length;
sb.Append(' ', indent * k_indentMultiple);
sb.Append(p.name);
sb.Append(' ', maxNameLen - p.name.Length);
sb.Append(" = ");
wrapIndent = sb.Length - wrapIndent;

if (wrapWidth != null && wrapWidth > wrapIndent + 20) // 20 just a reasonable "any less than this and wrapping looks way worse than not"
{
var written = 0;
var wrappedLines = 0;

while (written < text.Length)
{
if (written > 0)
sb.Append(' ', wrapIndent);

var end = Math.Min(text.Length, written + wrapWidth.Value - wrapIndent);
sb.AppendLine(text[written..end]);
written += end - written;
++wrappedLines;

if (maxWrapLines > 0 && wrappedLines >= maxWrapLines)
{
sb.Append(' ', wrapIndent);
sb.AppendLine("(...remainder truncated...)");
break;
}
}
}
else
sb.AppendLine(text);
}

return sb.ToString();
}
}
11 changes: 11 additions & 0 deletions src/Core/Extensions/DictionaryExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Diagnostics.CodeAnalysis;

namespace OkTools.Core.Extensions;

[PublicAPI]
Expand Down Expand Up @@ -56,4 +58,13 @@ public static IDictionary<TKey, TValue> AddOrUpdateRange<TKey, TValue>(this IDic

return @this;
}

public static bool TryGetValue<TValue>(this Dictionary<string, TValue> @this, ReadOnlySpan<char> key, [MaybeNullWhen(false)] out TValue value)
{
# if NET9_0_OR_GREATER
return @this.GetAlternateLookup<ReadOnlySpan<char>>().TryGetValue(key, out value);
# else
return @this.TryGetValue(key.ToString(), out value); // have to alloc :/
# endif
}
}
48 changes: 45 additions & 3 deletions src/Core/Extensions/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,44 @@ public static T SingleOr<T>(this IEnumerable<T> @this, Func<T> defaultValueGener
return value;
}

public static IEnumerable<(T item, int index, bool isLast)> SelectWithPositions<T>(this IEnumerable<T> @this)
{
using var enumerator = @this.GetEnumerator();
if (!enumerator.MoveNext())
yield break;

for (var index = 0;;++index)
{
var current = enumerator.Current;
var isLast = !enumerator.MoveNext();
yield return (current, index, isLast);
if (isLast)
break;
}
}

public static IEnumerable<(TResult item, int index, bool isLast)> SelectWithPositions<TSource, TResult>(this IEnumerable<TSource> @this, Func<TSource, TResult> selector)
{
using var enumerator = @this.GetEnumerator();
if (!enumerator.MoveNext())
yield break;

for (var index = 0;;++index)
{
var current = selector(enumerator.Current);
var isLast = !enumerator.MoveNext();
yield return (current, index, isLast);
if (isLast)
break;
}
}

public static IEnumerable<T> SelectItem<T>(this IEnumerable<(T item, int index, bool isLast)> @this)
{
foreach (var item in @this)
yield return item.item;
}

public static T First<T>(this IReadOnlyList<T> @this) =>
@this[0];
public static T Last<T>(this IReadOnlyList<T> @this) =>
Expand Down Expand Up @@ -140,17 +178,21 @@ public static bool TryLast<T>(this IReadOnlyList<T> @this, out T? found)

public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> @this) where T: class =>
@this.Where(item => item is not null)!;

public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> @this) where T: struct =>
@this.Where(item => item.HasValue).Select(item => item!.Value);

public static ParallelQuery<T> WhereNotNull<T>(this ParallelQuery<T?> @this) where T: class =>
@this.Where(item => item is not null)!;
public static ParallelQuery<T> WhereNotNull<T>(this ParallelQuery<T?> @this) where T: struct =>
@this.Where(item => item.HasValue).Select(item => item!.Value);

public static IEnumerable<TResult> SelectWhere<TSource, TResult>(
this IEnumerable<TSource> @this,
Func<TSource, (TResult selected, bool shouldSelect)> selectWhere)
Func<TSource, (bool shouldSelect, TResult selected)> selectWhere)
{
foreach (var item in @this)
{
var (selected, shouldSelect) = selectWhere(item);
var (shouldSelect, selected) = selectWhere(item);
if (shouldSelect)
yield return selected;
}
Expand Down
29 changes: 29 additions & 0 deletions src/Core/Extensions/EnumerableExtensions.t.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections;
// ReSharper disable UseArrayEmptyMethod

class EnumerableExtensionsTests
{
Expand Down Expand Up @@ -51,6 +52,34 @@ class DirectIndexTest<T>(int count, int index, T value) : IReadOnlyList<T>
public T this[int i] => i == index ? value : throw new InvalidOperationException();
}

static IEnumerable SelectWithPositionsCases() => new TestCaseData[] {
new(new char[0], new (char, int, bool)[0]),
new(new[] { 'a' },
new[] { ('a', 0, true) }),
new(new[] { 'a','b','c' },
new[] { ('a', 0, false), ('b', 1, false), ('c', 2, true) })
};

[TestCaseSource(nameof(SelectWithPositionsCases))]
public void SelectWithPositions(char[] items, (char c, int i, bool l)[] expected)
{
items.SelectWithPositions().ShouldBe(expected);
}

static IEnumerable SelectWithPositionsSelectorCases() => new TestCaseData[] {
new(new char[0], new (string, int, bool)[0]),
new(new[] { 'a' },
new[] { ("a", 0, true) }),
new(new[] { 'a','b','c' },
new[] { ("a", 0, false), ("b", 1, false), ("c", 2, true) })
};

[TestCaseSource(nameof(SelectWithPositionsSelectorCases))]
public void SelectWithPositions_WithSelector(char[] items, (string c, int i, bool l)[] expected)
{
items.SelectWithPositions(c => c.ToString()).ShouldBe(expected);
}

[Test]
public void First_WithReadOnlyList_ShouldOnlyCallIndexer()
{
Expand Down
24 changes: 24 additions & 0 deletions src/Core/Extensions/SmallExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using System.Text;

namespace OkTools.Core.Extensions;

Expand Down Expand Up @@ -107,3 +108,26 @@ public static class TupleExtensions
yield return @this[i];
}
}

[PublicAPI]
public static class StringBuilderExtensions
{
public static StringBuilder AppendLf(this StringBuilder @this) =>
@this.Append('\n');
public static StringBuilder AppendLf(this StringBuilder @this, string? value)
{ @this.Append(value); return @this.Append('\n'); }
public static bool IsEmpty(this StringBuilder @this) =>
@this.Length == 0;
public static bool Any(this StringBuilder @this) =>
@this.Length != 0;
}

[PublicAPI]
public static class ExceptionExtensions
{
public static T WithData<T>(this T @this, object key, object value) where T : Exception
{
@this.Data[key] = value;
return @this;
}
}
Loading

0 comments on commit e531e72

Please sign in to comment.