Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename TId > TValue #44

Merged
merged 1 commit into from
Dec 21, 2024
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
8 changes: 4 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ Unlike other such libraries for .NET, StructId introduces several unique feature
1. Leverages newest language and runtime features for cleaner and more efficient code,
such as:
1. `IParsable<T>`/`ISpanParsable<T>` for parsing from strings.
1. Static interface members, for consistent `TSelf.New(TId value)` factory
method and proper type constraint (via a provided `INewable<TSelf, TId>` interface).
1. Static interface members, for consistent `TSelf.New(TValue value)` factory
method and proper type constraint (via a provided `INewable<TSelf, TValue>` interface).
1. File-scoped C# templates for unparalelled authoring and extensibility experience.

## Usage

After installing the [StructId package](https://nuget.org/packages/StructId), the project
(with a direct reference to the `StructId` package) will contain the main interfaces
`IStruct` (for string-typed IDs) and `IStructId<TId>`.
`IStruct` (for string-typed IDs) and `IStructId<TValue>`.

> NOTE: the package only needs to be installed in the top-level project in your solution,
> since analyzers/generators will [automatically propagate to referencing projects]((https://github.com/dotnet/sdk/issues/1212)).
Expand All @@ -44,7 +44,7 @@ publish one that uses struct ids).
The default target namespace for the included types will match the `RootNamespace` of the
project, but can be customized by setting the `StructIdNamespace` property.

You can simply declare a new ID type by implementing `IStructId<TId>`:
You can simply declare a new ID type by implementing `IStructId<TValue>`:

```csharp
public readonly partial record struct UserId : IStructId<Guid>;
Expand Down
6 changes: 3 additions & 3 deletions src/StructId.Analyzer/BaseGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public abstract class BaseGenerator(string referenceType, string stringTemplate,
SyntaxNode? stringSyntax;
SyntaxNode? typedSyntax;

protected record struct TemplateArgs(INamedTypeSymbol TSelf, INamedTypeSymbol TId, INamedTypeSymbol ReferenceType, KnownTypes KnownTypes);
protected record struct TemplateArgs(INamedTypeSymbol TSelf, INamedTypeSymbol TValue, INamedTypeSymbol ReferenceType, KnownTypes KnownTypes);

public virtual void Initialize(IncrementalGeneratorInitializationContext context)
{
Expand Down Expand Up @@ -60,7 +60,7 @@ public virtual void Initialize(IncrementalGeneratorInitializationContext context
});

if (referenceCheck == ReferenceCheck.ValueIsType)
combined = combined.Where(x => x.TId.Is(x.ReferenceType));
combined = combined.Where(x => x.TValue.Is(x.ReferenceType));

combined = OnInitialize(context, combined);

Expand All @@ -73,7 +73,7 @@ void GenerateCode(SourceProductionContext context, TemplateArgs args)
=> AddFromTemplate(context, args, $"{args.TSelf.ToFileName()}.cs", SelectTemplate(args));

protected virtual SyntaxNode SelectTemplate(TemplateArgs args)
=> args.TId.Equals(args.KnownTypes.String, SymbolEqualityComparer.Default) ?
=> args.TValue.Equals(args.KnownTypes.String, SymbolEqualityComparer.Default) ?
(stringSyntax ??= CodeTemplate.Parse(stringTemplate, args.KnownTypes.Compilation.GetParseOptions())) :
(typedSyntax ??= CodeTemplate.Parse(typeTemplate, args.KnownTypes.Compilation.GetParseOptions()));

Expand Down
8 changes: 4 additions & 4 deletions src/StructId.Analyzer/DapperExtensions.sbn
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ public static partial class DapperExtensions
return connection;
}

partial class DapperTypeHandler<TSelf, TId, THandler> : TypeHandler<TSelf>
where TSelf : IStructId<TId>, INewable<TSelf, TId>
where THandler : TypeHandler<TId>, new()
where TId : struct
partial class DapperTypeHandler<TSelf, TValue, THandler> : TypeHandler<TSelf>
where TSelf : IStructId<TValue>, INewable<TSelf, TValue>
where THandler : TypeHandler<TValue>, new()
where TValue : struct
{
readonly THandler handler = new();

Expand Down
12 changes: 6 additions & 6 deletions src/StructId.Analyzer/DapperGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ protected override IncrementalValuesProvider<TemplateArgs> OnInitialize(Incremen
_ => false
};

var builtInHandled = source.Where(x => IsBuiltIn(x.TId.ToFullName()));
var builtInHandled = source.Where(x => IsBuiltIn(x.TValue.ToFullName()));

var customHandlers = context.CompilationProvider
.SelectMany((x, _) => x.Assembly.GetAllTypes().OfType<INamedTypeSymbol>())
Expand All @@ -52,12 +52,12 @@ protected override IncrementalValuesProvider<TemplateArgs> OnInitialize(Incremen
{
(TemplateArgs args, (ImmutableArray<INamedTypeSymbol> handlers, ImmutableArray<TValueTemplate> templatized)) = x;

var handlerType = args.ReferenceType.Construct(args.TId);
var handlerType = args.ReferenceType.Construct(args.TValue);
var handler = handlers.FirstOrDefault(x => x.Is(handlerType, false));

if (handler == null)
{
var templated = templatized.Where(x => x.TValue.Equals(args.TId, SymbolEqualityComparer.Default))
var templated = templatized.Where(x => x.TValue.Equals(args.TValue, SymbolEqualityComparer.Default))
.FirstOrDefault();
// Consider templatized handlers that will be emitted as custom handlers too for registration.
if (templated != null)
Expand Down Expand Up @@ -94,14 +94,14 @@ void GenerateHandlers(SourceProductionContext context, ((ImmutableArray<Template
// Avoid registering twice the same templatized value handlers since they are
// already added at the end of the scriban rendering.
.Where(x => !templatizedHandlers.Contains(x.Key))
.Select(x => new ValueHandlerModel(x.First().TId.ToFullName(), x.Key))
.Select(x => new ValueHandlerModel(x.First().TValue.ToFullName(), x.Key))
.ToArray();

var model = new SelectorModel(
structIdNamespace,
// Built-in use the Name of the value type since it's used as a suffix for well-known provided implementations.
builtInHandled.Select(x => new StructIdModel(x.TSelf.ToFullName(), x.TId.Name)),
customHandled.Select(x => new StructIdCustomModel(x.TSelf.ToFullName(), x.TId.ToFullName(), x.ReferenceType.ToFullName())),
builtInHandled.Select(x => new StructIdModel(x.TSelf.ToFullName(), x.TValue.Name)),
customHandled.Select(x => new StructIdCustomModel(x.TSelf.ToFullName(), x.TValue.ToFullName(), x.ReferenceType.ToFullName())),
customValueHandlers,
templatizedValues.Select(x => new ValueHandlerModelCode(x)));

Expand Down
24 changes: 12 additions & 12 deletions src/StructId.Analyzer/EntityFrameworkGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ protected override IncrementalValuesProvider<TemplateArgs> OnInitialize(Incremen

protected override SyntaxNode SelectTemplate(TemplateArgs args)
{
if (args.TId.Equals(args.KnownTypes.String, SymbolEqualityComparer.Default) ||
builtInTypesMap.ContainsKey(args.TId.ToDisplayString(NamespacedTypeName)))
if (args.TValue.Equals(args.KnownTypes.String, SymbolEqualityComparer.Default) ||
builtInTypesMap.ContainsKey(args.TValue.ToDisplayString(NamespacedTypeName)))
return idTemplate ??= CodeTemplate.Parse(ThisAssembly.Resources.Templates.EntityFramework.Text, args.KnownTypes.Compilation.GetParseOptions());
else if (args.TId.Is(args.KnownTypes.Compilation.GetTypeByMetadataName("System.IParsable`1")) &&
args.TId.Is(args.KnownTypes.Compilation.GetTypeByMetadataName("System.IFormattable")))
else if (args.TValue.Is(args.KnownTypes.Compilation.GetTypeByMetadataName("System.IParsable`1")) &&
args.TValue.Is(args.KnownTypes.Compilation.GetTypeByMetadataName("System.IFormattable")))
return parsableIdTemplate ??= CodeTemplate.Parse(ThisAssembly.Resources.Templates.EntityFrameworkParsable.Text, args.KnownTypes.Compilation.GetParseOptions());
else
return idTemplate ??= CodeTemplate.Parse(ThisAssembly.Resources.Templates.EntityFramework.Text, args.KnownTypes.Compilation.GetParseOptions());
Expand All @@ -90,16 +90,16 @@ void GenerateValueSelector(SourceProductionContext context, ((ImmutableArray<Tem

var model = new SelectorModel(
structIds.Select(x => new StructIdModel(x.TSelf.ToFullName(),
// The TId is used as the ProviderClrType for EF, which should be either a built-in
// The TValue is used as the ProviderClrType for EF, which should be either a built-in
// supported type or a parsable one. We default to using the type as-is for future-proofing,
// but that may be subject to change.
!builtInTypesMap.ContainsKey(x.TId.ToDisplayString(NamespacedTypeName))
? x.TId.Is(x.KnownTypes.Compilation.GetTypeByMetadataName("System.IParsable`1")) &&
x.TId.Is(x.KnownTypes.Compilation.GetTypeByMetadataName("System.IFormattable"))
!builtInTypesMap.ContainsKey(x.TValue.ToDisplayString(NamespacedTypeName))
? x.TValue.Is(x.KnownTypes.Compilation.GetTypeByMetadataName("System.IParsable`1")) &&
x.TValue.Is(x.KnownTypes.Compilation.GetTypeByMetadataName("System.IFormattable"))
// parsable+formattable will result in the parsable template being used as the converter
// so we use string as the underlying EF type.
? "string" : x.TId.ToFullName()
: x.TId.ToFullName())),
? "string" : x.TValue.ToFullName()
: x.TValue.ToFullName())),
customConverters.Select(x => new ConverterModel(x.BaseType!.TypeArguments[0].ToFullName(), x.BaseType!.TypeArguments[1].ToFullName(), x.ToFullName())),
templatizedConverters
.Where(x => !builtInTypesMap.ContainsKey(x.TValue.ToDisplayString(NamespacedTypeName)))
Expand All @@ -110,9 +110,9 @@ void GenerateValueSelector(SourceProductionContext context, ((ImmutableArray<Tem
context.AddSource($"ValueConverterSelector.cs", output);
}

record StructIdModel(string TSelf, string TIdType)
record StructIdModel(string TSelf, string TValueType)
{
public string TId => builtInTypesMap.TryGetValue(TIdType, out var value) ? value : TIdType;
public string TValue => builtInTypesMap.TryGetValue(TValueType, out var value) ? value : TValueType;
}

record ConverterModel(string TModel, string TProvider, string TConverter);
Expand Down
2 changes: 1 addition & 1 deletion src/StructId.Analyzer/EntityFrameworkSelector.sbn
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public static class StructIdDbContextOptionsBuilderExtensions
{{~ for id in Ids ~}}
if (modelClrType == typeof({{ id.TSelf }}))
yield return converters.GetOrAdd((modelClrType, providerClrType), key => new ValueConverterInfo(
key.ModelClrType, key.ProviderClrType ?? typeof({{ id.TId }}),
key.ModelClrType, key.ProviderClrType ?? typeof({{ id.TValue }}),
info => new {{ id.TSelf }}.EntityFrameworkValueConverter(info.MappingHints)));

{{~ end ~}}
Expand Down
2 changes: 1 addition & 1 deletion src/StructId.Analyzer/RecordAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ static void Analyze(SyntaxNodeAnalysisContext context)
return;

// If there are parameters, it must be only one, be named Value and be either
// type string (if implementing IStructId) or the TId (if implementing IStructId<TId>)
// type string (if implementing IStructId) or the TValue (if implementing IStructId<TValue>)
if (typeDeclaration.ParameterList.Parameters.Count != 1)
{
context.ReportDiagnostic(Diagnostic.Create(MustHaveValueConstructor, typeDeclaration.ParameterList.GetLocation(), symbol.Name));
Expand Down
14 changes: 7 additions & 7 deletions src/StructId.Analyzer/TValueTemplateExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ public bool AppliesTo(INamedTypeSymbol valueType)
return true;

// If the template had a generic attribute, we'd be looking at an intermediate
// type (typically TValue or TId) being used to define multiple constraints on
// type (typically TValue) being used to define multiple constraints on
// the struct id's value type, such as implementing multiple interfaces. In
// this case, the tid would never equal or inherit from the template's TId,
// this case, the tid would never equal or inherit from the template's TValue,
// but we want instead to check for base type compatibility plus all interfaces.
return TValue.IsFileLocal &&
// TId is a derived class of the template's TId base type (i.e. object or ValueType)
// TValue is a derived class of the template's TValue base type (i.e. object or ValueType)
valueType.Is(TValue.BaseType) &&
// All template provided TId interfaces must be implemented by the struct id's TId
// All template provided TValue interfaces must be implemented by the struct id's TValue
TValue.AllInterfaces.All(iface =>
valueType.AllInterfaces.Any(tface => tface.Is(iface)));
}
Expand Down Expand Up @@ -99,7 +99,7 @@ public static IncrementalValuesProvider<TValueTemplate> SelectTemplatizedValues(
.SelectMany((x, _) =>
{
var ((id, known), templates) = x;
// Locate the IStructId<TId> interface implemented by the id
// Locate the IStructId<TValue> interface implemented by the id
var structId = id.AllInterfaces.First(i => i.Is(known.IStructIdT));
var tvalue = (INamedTypeSymbol)structId.TypeArguments[0];
return templates
Expand All @@ -113,9 +113,9 @@ public static IncrementalValuesProvider<TValueTemplate> SelectTemplatizedValues(
//void GenerateCode(SourceProductionContext context, TIdTemplate source)
//{
// var templateFile = Path.GetFileNameWithoutExtension(source.Template.Syntax.SyntaxTree.FilePath);
// var hintName = $"{source.TId.ToFileName()}/{templateFile}.cs";
// var hintName = $"{source.TValue.ToFileName()}/{templateFile}.cs";

// var applied = source.Template.Syntax.Apply(source.TId);
// var applied = source.Template.Syntax.Apply(source.TValue);
// var output = applied.ToFullString();

// context.AddSource(hintName, SourceText.From(output, Encoding.UTF8));
Expand Down
40 changes: 20 additions & 20 deletions src/StructId.Analyzer/TemplatedGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,46 +15,46 @@ public partial class TemplatedGenerator : IIncrementalGenerator
/// Represents a template for struct ids.
/// </summary>
/// <param name="StructId">The struct id type, either IStructId or IStructId{T}.</param>
/// <param name="TId">The type of value the struct id holds, such as Guid or string.</param>
/// <param name="TValue">The type of value the struct id holds, such as Guid or string.</param>
/// <param name="Template">The template to apply to it.</param>
record IdTemplate(INamedTypeSymbol StructId, INamedTypeSymbol TId, Template Template);
record IdTemplate(INamedTypeSymbol StructId, INamedTypeSymbol TValue, Template Template);

record Template(INamedTypeSymbol TSelf, INamedTypeSymbol TId, AttributeData Attribute, KnownTypes KnownTypes)
record Template(INamedTypeSymbol TSelf, INamedTypeSymbol TValue, AttributeData Attribute, KnownTypes KnownTypes)
{
public INamedTypeSymbol? OriginalTId { get; init; }
public INamedTypeSymbol? OriginalTValue { get; init; }

// A custom TId is a file-local type declaration.
public bool IsLocalTId => OriginalTId?.IsFileLocal == true;
// A custom TValue is a file-local type declaration.
public bool IsLocalTValue => OriginalTValue?.IsFileLocal == true;

public SyntaxNode Syntax { get; } = TSelf.DeclaringSyntaxReferences[0].GetSyntax().SyntaxTree.GetRoot();

public bool NoString { get; } = new NoStringSyntaxWalker().Accept(
TSelf.DeclaringSyntaxReferences[0].GetSyntax().SyntaxTree.GetRoot());

/// <summary>
/// Checks the value type against the template's TId for compatibility
/// Checks the value type against the template's TValue for compatibility
/// </summary>
public bool AppliesTo(INamedTypeSymbol valueType)
{
if (NoString && valueType.Equals(KnownTypes.String, SymbolEqualityComparer.Default))
return false;

if (valueType.Equals(TId, SymbolEqualityComparer.Default))
if (valueType.Equals(TValue, SymbolEqualityComparer.Default))
return true;

if (valueType.Is(TId))
if (valueType.Is(TValue))
return true;

// The underlying TId may be an intermediate type (typically TValue or TId)
// The underlying TValue may be an intermediate type (typically TValue or TValue)
// being used to define multiple constraints on the struct id's value type,
// such as implementing multiple interfaces. In this case, the tid would never equal
// or inherit from the template's TId, but we want instead to check for base
// or inherit from the template's TValue, but we want instead to check for base
// type compatibility plus all interfaces.
return IsLocalTId &&
// TId is a derived class of the template's TId base type (i.e. object or ValueType)
valueType.Is(TId.BaseType) &&
// All template provided TId interfaces must be implemented by the struct id's TId
TId.AllInterfaces.All(iface =>
return IsLocalTValue &&
// TValue is a derived class of the template's TValue base type (i.e. object or ValueType)
valueType.Is(TValue.BaseType) &&
// All template provided TValue interfaces must be implemented by the struct id's TValue
TValue.AllInterfaces.All(iface =>
valueType.AllInterfaces.Any(tface => tface.Is(iface)));
}
}
Expand Down Expand Up @@ -93,7 +93,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
var type = idType.DeclaringSyntaxReferences[0].GetSyntax(cancellation) as TypeDeclarationSyntax;
var iface = type?.BaseList?.Types.FirstOrDefault()?.Type;
if (type == null || iface == null)
return new Template(structId, idType, attribute, known) { OriginalTId = idType };
return new Template(structId, idType, attribute, known) { OriginalTValue = idType };

if (x.Right.Compilation.GetSemanticModel(type.SyntaxTree).GetSymbolInfo(iface).Symbol is not INamedTypeSymbol ifaceType)
return new Template(structId, idType, attribute, known);
Expand All @@ -105,7 +105,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)

return new Template(structId, ifaceType, attribute, known)
{
OriginalTId = idType
OriginalTValue = idType
};
})
.Collect();
Expand All @@ -125,10 +125,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.SelectMany((x, _) =>
{
var ((id, known), templates) = x;
// Locate the IStructId<TId> interface implemented by the id
// Locate the IStructId<TValue> interface implemented by the id
var structId = id.AllInterfaces.First(i => i.Is(known.IStructId) || i.Is(known.IStructIdT));
var tid = structId.IsGenericType ? (INamedTypeSymbol)structId.TypeArguments[0] : known.String;
// If the TId/Value implements or inherits from the template base type and/or its interfaces
// If the TValue/Value implements or inherits from the template base type and/or its interfaces
return templates
.Where(template => template.AppliesTo(tid))
.Select(template => new IdTemplate(id, tid, template));
Expand Down
2 changes: 1 addition & 1 deletion src/StructId.Tests/CodeTemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ file partial record struct TSelf(string Value)
// from template
}

file record struct TId;
file record struct TValue;
""";

var id = compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(
Expand Down
Loading
Loading