Skip to content

Commit

Permalink
Define a lightweight struct to represent paths (#257)
Browse files Browse the repository at this point in the history
  • Loading branch information
Corniel authored Mar 3, 2024
1 parent afe3d3b commit e7f03dd
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 0 deletions.
23 changes: 23 additions & 0 deletions src/Buildalyzer/Conversion/IOPathTypeConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#nullable enable

using System.ComponentModel;
using System.Globalization;
using Buildalyzer.IO;

namespace Buildalyzer.Conversion;

/// <summary>Implements a <see cref="TypeConverter"/> for <see cref="IOPath"/>.</summary>
internal sealed class IOPathTypeConverter : TypeConverter
{
/// <inheritdoc />
[Pure]
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
=> sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);

/// <inheritdoc />
[Pure]
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object? value)
=> value is null || value is string
? IOPath.Parse(value as string)
: base.ConvertFrom(context, culture, value);
}
85 changes: 85 additions & 0 deletions src/Buildalyzer/IO/IOPath.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#nullable enable

using System.ComponentModel;
using System.IO;

namespace Buildalyzer.IO;

/// <summary>Represents an (IO) path.</summary>
[TypeConverter(typeof(Conversion.IOPathTypeConverter))]
public readonly struct IOPath : IEquatable<IOPath>, IFormattable
{
/// <summary>Represents none/an empty path.</summary>
public static readonly IOPath Empty;

/// <inheritdoc cref="Path.DirectorySeparatorChar" />
public static char DirectorySeparatorChar => Path.DirectorySeparatorChar;

/// <summary>Returns true if the file system is case sensitive.</summary>
public static readonly bool IsCaseSensitive = InitCaseSensitivity();

[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly string _path;

private IOPath(string path) => _path = path;

/// <summary>Creates a <see cref="DirectoryInfo"/> based on the path.</summary>
[Pure]
public DirectoryInfo Directory() => new(ToString());

/// <summary>Creates a <see cref="FileInfo"/> based on the path.</summary>
[Pure]
public FileInfo File() => new(ToString());

/// <summary>Creates a new path.</summary>
[Pure]
public IOPath Combine(params string[] paths)
=> _path is null
? Parse(Path.Combine(paths))
: Parse(Path.Combine(_path, Path.Combine(paths)));

/// <inheritdoc />
[Pure]
public override bool Equals([NotNullWhen(true)] object? obj)
=> obj is IOPath other && Equals(other);

/// <inheritdoc />
[Pure]
public bool Equals(IOPath other) => Equals(other, IsCaseSensitive);

/// <inheritdoc />
[Pure]
public bool Equals(IOPath other, bool caseSensitive)
=> string.Equals(_path, other._path, caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase);

/// <inheritdoc />
[Pure]
public override int GetHashCode()
=> IsCaseSensitive
? _path?.GetHashCode() ?? 0
: _path?.ToUpperInvariant().GetHashCode() ?? 0;

/// <inheritdoc />
[Pure]
public override string ToString() => ToString(null, null);

/// <inheritdoc />
[Pure]
public string ToString(string? format, IFormatProvider? formatProvider) => format switch
{
"/" => _path ?? string.Empty,
"\\" => (_path ?? string.Empty).Replace('/', '\\'),
null => (_path ?? string.Empty).Replace('/', DirectorySeparatorChar),
_ => throw new FormatException($"The format '{format}' is a not supported directory separator char."),
};

[Pure]
public static IOPath Parse(string? s)
=> s?.Trim() is { Length: > 0 } p
? new(p.Replace('\\', '/'))
: Empty;

[Pure]
private static bool InitCaseSensitivity()
=> !new FileInfo(typeof(IOPath).Assembly.Location.ToUpperInvariant()).Exists;
}
2 changes: 2 additions & 0 deletions src/Buildalyzer/Properties/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
global using System;
global using System.Collections.Generic;
global using System.Collections.Immutable;
global using System.Diagnostics;
global using System.Diagnostics.CodeAnalysis;
global using System.Diagnostics.Contracts;
global using System.Linq;
global using System.Text;
21 changes: 21 additions & 0 deletions tests/Buildalyzer.Tests/IO/IOPathFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Buildalyzer.IO;
using FluentAssertions;

namespace Buildalyzer.Tests.IO;

public class IOPathFixture
{
#if Is_Windows
[Test]
public void Is_case_insensitive_on_windows()
=> IOPath.IsCaseSensitive.Should().BeFalse();
#endif

[Test]
public void is_seperator_agnostic()
=> IOPath.Parse(".\\root\\test\\somefile.txt").Should().Be(IOPath.Parse("./root/test/somefile.txt"));

[TestCase(@"c:\Program Files\Buildalyzer")]
public void supports_type_conversion(IOPath path)
=> path.Should().Be(IOPath.Parse(@"c:\Program Files\Buildalyzer"));
}

0 comments on commit e7f03dd

Please sign in to comment.