-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Ths is a requirement for using struct ids in ASP.NET core endpoints, for example. This is implemented such that if in a given `IStructId<T>`, the `T : IParsable<T>`, the parsable implementation is provided as a pass-through to the value implementation. For `IStructId` which is for string-typed values, we always emit a simple implementation that just create a new struct id.
- Loading branch information
Showing
17 changed files
with
438 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
using Sample; | ||
|
||
var builder = WebApplication.CreateBuilder(args); | ||
var app = builder.Build(); | ||
|
||
app.MapGet("/{id}", (UserId id) => id); | ||
|
||
app.Run(); | ||
|
||
readonly partial record struct UserId : IStructId; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
{ | ||
"$schema": "http://json.schemastore.org/launchsettings.json", | ||
"iisSettings": { | ||
"windowsAuthentication": false, | ||
"anonymousAuthentication": true, | ||
"iisExpress": { | ||
"applicationUrl": "http://localhost:26295", | ||
"sslPort": 44329 | ||
} | ||
}, | ||
"profiles": { | ||
"http": { | ||
"commandName": "Project", | ||
"dotnetRunMessages": true, | ||
"launchBrowser": true, | ||
"applicationUrl": "http://localhost:5047", | ||
"environmentVariables": { | ||
"ASPNETCORE_ENVIRONMENT": "Development" | ||
} | ||
}, | ||
"https": { | ||
"commandName": "Project", | ||
"dotnetRunMessages": true, | ||
"launchBrowser": true, | ||
"applicationUrl": "https://localhost:7120;http://localhost:5047", | ||
"environmentVariables": { | ||
"ASPNETCORE_ENVIRONMENT": "Development" | ||
} | ||
}, | ||
"IIS Express": { | ||
"commandName": "IISExpress", | ||
"launchBrowser": true, | ||
"environmentVariables": { | ||
"ASPNETCORE_ENVIRONMENT": "Development" | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,15 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
<Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<Nullable>enable</Nullable> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<RestoreSources>https://api.nuget.org/v3/index.json;$(PackageOutputPath)</RestoreSources> | ||
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="StructId" Version="42.*" /> | ||
<PackageReference Include="StructId" Version="42.253.1273" /> | ||
</ItemGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"Logging": { | ||
"LogLevel": { | ||
"Default": "Information", | ||
"Microsoft.AspNetCore": "Warning" | ||
} | ||
}, | ||
"AllowedHosts": "*" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
using System.Linq; | ||
using System.Text; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Text; | ||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; | ||
|
||
namespace StructId; | ||
|
||
[Generator(LanguageNames.CSharp)] | ||
public class ParsableGenerator : IIncrementalGenerator | ||
{ | ||
public void Initialize(IncrementalGeneratorInitializationContext context) | ||
{ | ||
// Locate the IParseable<T> type | ||
var parseable = context.CompilationProvider | ||
.Select((x, _) => x.GetTypeByMetadataName("System.IParsable`1")); | ||
|
||
var ids = context.CompilationProvider | ||
.SelectMany((x, _) => x.Assembly.GetAllTypes().OfType<INamedTypeSymbol>()) | ||
.Where(x => x.IsStructId()) | ||
.Where(x => x.IsPartial()); | ||
|
||
var combined = ids.Combine(parseable) | ||
.Where(x => | ||
{ | ||
var (id, parseable) = x; | ||
|
||
// NOTE: we never generate for compilations that don't have the IParsable<T> type (i.e. .NET6) | ||
if (parseable == null) | ||
return false; | ||
|
||
var type = id.AllInterfaces | ||
.First(x => x.Name == "IStructId") | ||
.TypeArguments.FirstOrDefault(); | ||
|
||
// If we don't have a generic type of IStructId, then it's the string-based one | ||
// which we can always parse | ||
if (type == null) | ||
return true; | ||
|
||
return type.Is(parseable); | ||
}) | ||
.Select((x, _) => x.Left); | ||
|
||
context.RegisterImplementationSourceOutput(combined, GenerateCode); | ||
} | ||
|
||
void GenerateCode(SourceProductionContext context, INamedTypeSymbol symbol) | ||
{ | ||
var ns = symbol.ContainingNamespace.Equals(symbol.ContainingModule.GlobalNamespace, SymbolEqualityComparer.Default) | ||
? null | ||
: symbol.ContainingNamespace.ToDisplayString(); | ||
|
||
// Generic IStructId<T> -> T, otherwise string | ||
var type = symbol.AllInterfaces.First(x => x.Name == "IStructId").TypeArguments. | ||
Select(x => x.GetTypeName(ns)).FirstOrDefault() ?? "string"; | ||
|
||
var template = type == "string" | ||
? ThisAssembly.Resources.Templates.SParseable.Text | ||
: ThisAssembly.Resources.Templates.TParseable.Text; | ||
|
||
// parse template into a C# compilation unit | ||
var parseable = CSharpSyntaxTree.ParseText(template).GetCompilationUnitRoot(); | ||
|
||
// if we got a ns, move all members after a file-scoped namespace declaration | ||
if (ns != null) | ||
{ | ||
var members = parseable.Members; | ||
var fsns = FileScopedNamespaceDeclaration(ParseName(ns).WithLeadingTrivia(Whitespace(" "))) | ||
.WithLeadingTrivia(LineFeed) | ||
.WithTrailingTrivia(LineFeed) | ||
.WithMembers(members); | ||
parseable = parseable.WithMembers(SingletonList<MemberDeclarationSyntax>(fsns)); | ||
} | ||
|
||
// replace all nodes with the identifier TStruct/SStruct with symbol.Name | ||
var structIds = parseable.DescendantNodes() | ||
.OfType<IdentifierNameSyntax>() | ||
.Where(x => x.Identifier.Text == "TStruct" || x.Identifier.Text == "SStruct"); | ||
parseable = parseable.ReplaceNodes(structIds, (node, _) => IdentifierName(symbol.Name) | ||
.WithLeadingTrivia(node.GetLeadingTrivia()) | ||
.WithTrailingTrivia(node.GetTrailingTrivia())); | ||
|
||
var structTokens = parseable.DescendantTokens() | ||
.OfType<SyntaxToken>() | ||
.Where(x => x.IsKind(SyntaxKind.IdentifierToken)) | ||
.Where(x => x.Text == "TStruct" || x.Text == "SStruct"); | ||
// replace with a new identifier with symbol.name | ||
parseable = parseable.ReplaceTokens(structTokens, (token, _) => Identifier(symbol.Name) | ||
.WithLeadingTrivia(token.LeadingTrivia) | ||
.WithTrailingTrivia(token.TrailingTrivia)); | ||
|
||
// replace all nodes with the identifier TValue with actual type | ||
var placeholder = parseable.DescendantNodes().OfType<IdentifierNameSyntax>().Where(x => x.Identifier.Text == "TValue"); | ||
parseable = parseable.ReplaceNodes(placeholder, (_, _) => IdentifierName(type)); | ||
|
||
context.AddSource($"{symbol.ToFileName()}.parsable.cs", SourceText.From(parseable.ToFullString(), Encoding.UTF8)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
using System; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.ComponentModel; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using Newtonsoft.Json.Linq; | ||
|
||
namespace StructId.Functional; | ||
|
||
[TypeConverter(typeof(ProductId.TypeConverter))] | ||
public readonly partial record struct ProductId : IStructId<Guid> | ||
{ | ||
public class TypeConverter : ParseableTypeConverter<ProductId, Guid> | ||
{ | ||
protected override ProductId Create(Guid value) => new(value); | ||
} | ||
} | ||
|
||
public readonly partial record struct WalletId : IStructId | ||
{ | ||
public class TypeConverter : StructIdConverters.StringTypeConverter<WalletId> | ||
{ | ||
protected override WalletId Create(string value) => new(value); | ||
} | ||
} | ||
|
||
public abstract class ParseableTypeConverter<TStruct, TValue> : TypeConverter | ||
where TStruct : IStructId<TValue> | ||
where TValue : struct, IParsable<TValue> | ||
{ | ||
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string); | ||
|
||
public override object? ConvertFrom(ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object value) => value switch | ||
{ | ||
string s => Create(TValue.Parse(s, culture)), | ||
null => null, | ||
_ => throw new ArgumentException($"Cannot convert from {value} to {typeof(TStruct).Name}", nameof(value)) | ||
}; | ||
|
||
protected abstract TStruct Create(TValue value); | ||
|
||
public override object? ConvertTo(ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, Type destinationType) | ||
{ | ||
if (destinationType == typeof(string)) | ||
{ | ||
return value switch | ||
{ | ||
TValue id => id.ToString(), | ||
TStruct id => id.Value.ToString(), | ||
null => null, | ||
_ => throw new ArgumentException($"Cannot convert {value} to string", nameof(value)) | ||
}; | ||
} | ||
|
||
throw new ArgumentException($"Cannot convert {value ?? "(null)"} to {destinationType}", nameof(destinationType)); | ||
} | ||
} | ||
|
||
public class FunctionalTests | ||
{ | ||
[Fact] | ||
public void Test() | ||
{ | ||
var guid = Guid.NewGuid(); | ||
var id1 = new ProductId(guid); | ||
var id2 = new ProductId(guid); | ||
|
||
Assert.Equal(id1, id2); | ||
Assert.True(id1 == id2); | ||
} | ||
} |
Oops, something went wrong.