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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ namespace Pathy
{
[System.ComponentModel.TypeConverter(typeof(Pathy.ChainablePathTypeConverter))]
[System.Text.Json.Serialization.JsonConverter(typeof(Pathy.ChainablePathJsonConverter))]
public readonly struct ChainablePath : System.IEquatable<Pathy.ChainablePath>
public readonly struct ChainablePath : System.IEquatable<Pathy.ChainablePath>, System.IFormattable
{
public Pathy.ChainablePath Directory { get; }
public bool DirectoryExists { get; }
Expand Down Expand Up @@ -32,6 +32,7 @@ namespace Pathy
public System.IO.DirectoryInfo ToDirectoryInfo() { }
public System.IO.FileInfo ToFileInfo() { }
public override string ToString() { }
public string ToString(string? format, System.IFormatProvider? formatProvider) { }
public System.Uri ToUri() { }
public static Pathy.ChainablePath FindFirst(params Pathy.ChainablePath[] paths) { }
public static Pathy.ChainablePath FindFirst(params string[] paths) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ namespace Pathy
{
[System.ComponentModel.TypeConverter(typeof(Pathy.ChainablePathTypeConverter))]
[System.Text.Json.Serialization.JsonConverter(typeof(Pathy.ChainablePathJsonConverter))]
public readonly struct ChainablePath : System.IEquatable<Pathy.ChainablePath>
public readonly struct ChainablePath : System.IEquatable<Pathy.ChainablePath>, System.IFormattable, System.ISpanFormattable
{
public Pathy.ChainablePath Directory { get; }
public bool DirectoryExists { get; }
Expand Down Expand Up @@ -33,7 +33,9 @@ namespace Pathy
public System.IO.DirectoryInfo ToDirectoryInfo() { }
public System.IO.FileInfo ToFileInfo() { }
public override string ToString() { }
public string ToString(string? format, System.IFormatProvider? formatProvider) { }
public System.Uri ToUri() { }
public bool TryFormat(System.Span<char> destination, out int charsWritten, System.ReadOnlySpan<char> format, System.IFormatProvider? provider) { }
public static Pathy.ChainablePath FindFirst(params Pathy.ChainablePath[] paths) { }
public static Pathy.ChainablePath FindFirst(params string[] paths) { }
public static Pathy.ChainablePath From(string path) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ namespace Pathy
{
[System.ComponentModel.TypeConverter(typeof(Pathy.ChainablePathTypeConverter))]
[System.Text.Json.Serialization.JsonConverter(typeof(Pathy.ChainablePathJsonConverter))]
public readonly struct ChainablePath : System.IEquatable<Pathy.ChainablePath>
public readonly struct ChainablePath : System.IEquatable<Pathy.ChainablePath>, System.IFormattable
{
public Pathy.ChainablePath Directory { get; }
public bool DirectoryExists { get; }
Expand Down Expand Up @@ -32,6 +32,7 @@ namespace Pathy
public System.IO.DirectoryInfo ToDirectoryInfo() { }
public System.IO.FileInfo ToFileInfo() { }
public override string ToString() { }
public string ToString(string? format, System.IFormatProvider? formatProvider) { }
public System.Uri ToUri() { }
public static Pathy.ChainablePath FindFirst(params Pathy.ChainablePath[] paths) { }
public static Pathy.ChainablePath FindFirst(params string[] paths) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ namespace Pathy
{
[System.ComponentModel.TypeConverter(typeof(Pathy.ChainablePathTypeConverter))]
[System.Text.Json.Serialization.JsonConverter(typeof(Pathy.ChainablePathJsonConverter))]
public readonly struct ChainablePath : System.IEquatable<Pathy.ChainablePath>
public readonly struct ChainablePath : System.IEquatable<Pathy.ChainablePath>, System.IFormattable
{
public Pathy.ChainablePath Directory { get; }
public bool DirectoryExists { get; }
Expand Down Expand Up @@ -33,6 +33,7 @@ namespace Pathy
public System.IO.DirectoryInfo ToDirectoryInfo() { }
public System.IO.FileInfo ToFileInfo() { }
public override string ToString() { }
public string ToString(string? format, System.IFormatProvider? formatProvider) { }
public System.Uri ToUri() { }
public static Pathy.ChainablePath FindFirst(params Pathy.ChainablePath[] paths) { }
public static Pathy.ChainablePath FindFirst(params string[] paths) { }
Expand Down
62 changes: 61 additions & 1 deletion Pathy.Specs/ChainablePathSpecs.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using System.IO;
using System.Reflection;
using FluentAssertions;
Expand Down Expand Up @@ -60,6 +61,65 @@ public void Can_build_a_path_from_a_drive_letter(string drive)
path.IsRooted.Should().BeTrue();
}

[Fact]
public void Can_use_a_path_inside_an_interpolated_string()
{
// Arrange
var path = ChainablePath.From(@"C:\some\file.txt");

// Act
string result = $"The path is {path}";

// Assert
result.Should().Be(@"The path is C:\some\file.txt");
}

[Fact]
public void Can_format_a_path_using_ToString_with_format_and_provider()
{
// Arrange
var path = ChainablePath.From(@"C:\some\file.txt");

// Act
string result = path.ToString("whatever", CultureInfo.InvariantCulture);

// Assert
result.Should().Be(@"C:\some\file.txt");
}

#if NET6_0_OR_GREATER
[Fact]
public void Can_try_format_a_path_into_a_span_that_is_large_enough()
{
// Arrange
var path = ChainablePath.From(@"C:\some\file.txt");
Span<char> destination = stackalloc char[path.ToString().Length];

// Act
bool success = path.TryFormat(destination, out int charsWritten, default, null);

// Assert
success.Should().BeTrue();
charsWritten.Should().Be(path.ToString().Length);
destination.ToString().Should().Be(path.ToString());
}

[Fact]
public void Cannot_try_format_a_path_into_a_span_that_is_too_small()
{
// Arrange
var path = ChainablePath.From(@"C:\some\file.txt");
Span<char> destination = stackalloc char[2];

// Act
bool success = path.TryFormat(destination, out int charsWritten, default, null);

// Assert
success.Should().BeFalse();
charsWritten.Should().Be(0);
}
#endif

[Theory]
[InlineData("")]
[InlineData(null)]
Expand Down Expand Up @@ -681,7 +741,7 @@ public void Matches_with_multiple_patterns_throws_when_no_patterns_provided()
var path = ChainablePath.Temp / "file.cs";

// Act & Assert
var act = () => path.Matches(new string[0]);
var act = () => path.Matches(Array.Empty<string>());

act.Should().Throw<ArgumentException>()
.WithMessage("*At least one glob pattern must be provided*")
Expand Down
57 changes: 55 additions & 2 deletions Pathy/ChainablePath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,38 @@ namespace Pathy
/// Represents an absolute or relative path to a directory or file that simplifies operations like combining paths using
/// the <c>/</c> operator
/// </summary>
#if NET6_0_OR_GREATER
#if PATHY_PUBLIC
[TypeConverter(typeof(ChainablePathTypeConverter))]
#if PATHY_SYSTEM_TEXT_JSON
[JsonConverter(typeof(ChainablePathJsonConverter))]
#endif
public readonly record struct ChainablePath
public readonly record struct ChainablePath : IFormattable, ISpanFormattable
#else
[global::Microsoft.CodeAnalysis.Embedded]
[global::System.Diagnostics.DebuggerNonUserCode]
[TypeConverter(typeof(ChainablePathTypeConverter))]
#if PATHY_SYSTEM_TEXT_JSON
[JsonConverter(typeof(ChainablePathJsonConverter))]
#endif
internal readonly record struct ChainablePath
internal readonly record struct ChainablePath : IFormattable, ISpanFormattable
#endif
#else
#if PATHY_PUBLIC
[TypeConverter(typeof(ChainablePathTypeConverter))]
#if PATHY_SYSTEM_TEXT_JSON
[JsonConverter(typeof(ChainablePathJsonConverter))]
#endif
public readonly record struct ChainablePath : IFormattable
#else
[global::Microsoft.CodeAnalysis.Embedded]
[global::System.Diagnostics.DebuggerNonUserCode]
[TypeConverter(typeof(ChainablePathTypeConverter))]
#if PATHY_SYSTEM_TEXT_JSON
[JsonConverter(typeof(ChainablePathJsonConverter))]
#endif
internal readonly record struct ChainablePath : IFormattable
#endif
#endif
{
private readonly string path = string.Empty;
Expand Down Expand Up @@ -481,6 +499,41 @@ public override string ToString()
return path;
}

/// <summary>
/// Returns the string representation of the current <see cref="ChainablePath"/> instance.
/// </summary>
/// <remarks>
/// A <see cref="ChainablePath"/> has no format specifiers of its own, so <paramref name="format"/> and
/// <paramref name="formatProvider"/> are ignored and the underlying path is returned as-is.
/// </remarks>
public string ToString(string? format, IFormatProvider? formatProvider)
{
return path;
}

#if NET6_0_OR_GREATER
/// <summary>
/// Tries to write the string representation of the current <see cref="ChainablePath"/> instance into the
/// provided span of characters, avoiding the intermediate string allocation that would otherwise be needed,
/// for instance when the path is used inside an interpolated string.
/// </summary>
/// <remarks>
/// A <see cref="ChainablePath"/> has no format specifiers of its own, so <paramref name="format"/> and
/// <paramref name="provider"/> are ignored.
/// </remarks>
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
{
if (!path.AsSpan().TryCopyTo(destination))
{
charsWritten = 0;
return false;
}

charsWritten = path.Length;
return true;
}
#endif

/// <summary>
/// Allows casting a <see cref="ChainablePath"/> instance to a string so it can be used anywhere where a path as string is expected.
/// </summary>
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ string rawPath = path.ToString();
string rawPath = (string)path;
```

`ChainablePath` also implements `IFormattable`, and, on .NET 6.0 and later, `ISpanFormattable`, so it can be used directly inside interpolated strings and composite format strings without an intermediate allocation:

```csharp
Console.WriteLine($"Deploying from {path}"); // no extra string allocation on .NET 6+
string message = string.Format("Path: {0}", path);
```

`ChainablePath` has no format specifiers of its own, so any format string you pass (e.g. `path.ToString("X")`) is ignored and the plain path is returned.

### Working with paths

Know that `ChainablePath` overrides `Equals` and `GetHashCode`, so you can always compare two instances as you're used to.
Expand Down