Skip to content

Commit

Permalink
Add TypeShapeProvider.Combine static method
Browse files Browse the repository at this point in the history
  • Loading branch information
AArnott committed Jan 26, 2025
1 parent c9b91e1 commit f931a0c
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<IsPackable>false</IsPackable>
<LangVersion>12</LangVersion>
<LangVersion>13</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RepoRoot>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))</RepoRoot>
Expand Down
36 changes: 36 additions & 0 deletions src/PolyType/TypeShapeProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using PolyType.Abstractions;

namespace PolyType;

/// <summary>
/// Provides behaviors for manipulating or implementing <see cref="ITypeShapeProvider"/> instances.
/// </summary>
public static class TypeShapeProvider
{
/// <summary>
/// Creates a new <see cref="ITypeShapeProvider"/> that returns the first shape found
/// from iterating over a given list of providers.
/// </summary>
/// <param name="providers">The sequence of providers to solicit for type shapes.</param>
/// <returns>The aggregating <see cref="ITypeShapeProvider"/>.</returns>
public static ITypeShapeProvider Combine(params ReadOnlySpan<ITypeShapeProvider> providers)
{
return new AggregatingTypeShapeProvider(providers.ToArray());
}

private sealed class AggregatingTypeShapeProvider(ITypeShapeProvider[] providers) : ITypeShapeProvider
{
public ITypeShape? GetShape(Type type)
{
foreach (ITypeShapeProvider provider in providers)
{
if (provider.GetShape(type) is ITypeShape shape)
{
return shape;
}
}

return null;
}
}
}
146 changes: 146 additions & 0 deletions tests/PolyType.Tests/TypeShapeProviderStaticTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using System.Reflection;

namespace PolyType.Tests;

public class TypeShapeProviderStaticTests
{
[Fact]
public void Combine_EmptyList()
{
ITypeShapeProvider aggregate = TypeShapeProvider.Combine();
Assert.NotNull(aggregate);
Assert.Null(aggregate.GetShape(typeof(int)));
}

[Fact]
public void Combine_One_Null()
{
ITypeShapeProvider aggregate = TypeShapeProvider.Combine(new MockTypeShapeProvider());
Assert.Null(aggregate.GetShape(typeof(int)));
}


[Fact]
public void Combine_One_NonNull()
{
MockTypeShapeProvider first = new()
{
Shapes =
{
[typeof(int)] = new MockTypeShape<int>(),
},
};
ITypeShapeProvider aggregate = TypeShapeProvider.Combine(first);
Assert.Same(first.Shapes[typeof(int)], aggregate.GetShape(typeof(int)));
}

[Fact]
public void Combine_Many()
{
MockTypeShapeProvider first = new()
{
Shapes =
{
[typeof(int)] = new MockTypeShape<int>(),
},
};
MockTypeShapeProvider second = new()
{
Shapes =
{
[typeof(string)] = new MockTypeShape<int>(),
},
};
ITypeShapeProvider aggregate = TypeShapeProvider.Combine(first, second);
Assert.Same(first.Shapes[typeof(int)], aggregate.GetShape(typeof(int)));
Assert.Same(second.Shapes[typeof(string)], aggregate.GetShape(typeof(string)));
Assert.Null(aggregate.GetShape(typeof(bool)));
}

/// <summary>
/// Validates that the aggregating provider does not cache results.
/// </summary>
/// <remarks>
/// Whether it caches or not is something of an arbitrary design decision.
/// This test is to ensure that the behavior is documented and intentional.
/// </remarks>
[Fact]
public void CachePolicy_ShapeNotCached()
{
MockTypeShapeProvider first = new()
{
Shapes =
{
[typeof(int)] = new MockTypeShape<int>(),
},
};
ITypeShapeProvider aggregate = TypeShapeProvider.Combine(first);
Assert.Same(first.Shapes[typeof(int)], aggregate.GetShape(typeof(int)));

// Remove the shape, and verify that the aggregator no longer returns it.
first.Shapes.Remove(typeof(int));
Assert.Null(aggregate.GetShape(typeof(int)));
}

/// <summary>
/// Validates that the aggregating provider does not cache the provider that was used previously.
/// </summary>
/// <remarks>
/// Whether it caches or not is something of an arbitrary design decision.
/// This test is to ensure that the behavior is documented and intentional.
/// </remarks>
[Fact]
public void CachePolicy_ShapeProviderNotCached()
{
MockTypeShapeProvider first = new()
{
Shapes =
{
[typeof(int)] = new MockTypeShape<int>(),
},
};
MockTypeShapeProvider second = new()
{
Shapes =
{
[typeof(string)] = new MockTypeShape<string>(),
},
};
ITypeShapeProvider aggregate = TypeShapeProvider.Combine(first, second);
Assert.NotNull(aggregate.GetShape(typeof(string)));

// Add a new string shape to the first provider.
first.Shapes[typeof(string)] = new MockTypeShape<string>();

// The aggregator should return the new shape.
Assert.Same(first.Shapes[typeof(string)], aggregate.GetShape(typeof(string)));
}

private class MockTypeShapeProvider : ITypeShapeProvider
{
internal Dictionary<Type, ITypeShape> Shapes { get; } = new();

public ITypeShape? GetShape(Type type) => this.Shapes.TryGetValue(type, out ITypeShape? shape) ? shape : null;
}

private class MockTypeShape<T> : ITypeShape<T>
{
public Type Type => throw new NotImplementedException();

public TypeShapeKind Kind => throw new NotImplementedException();

public ITypeShapeProvider Provider => throw new NotImplementedException();

public ICustomAttributeProvider? AttributeProvider => throw new NotImplementedException();

public object? Accept(ITypeShapeVisitor visitor, object? state = null)
{
throw new NotImplementedException();
}

public object? Invoke(ITypeShapeFunc func, object? state = null)
{
throw new NotImplementedException();
}
}
}
2 changes: 2 additions & 0 deletions tests/PolyType.Tests/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
global using PolyType.Abstractions;
global using Xunit;

0 comments on commit f931a0c

Please sign in to comment.