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

Inheritance implementation for Fluent API #32

Merged
merged 15 commits into from
Sep 27, 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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Accompanying blog post: [www.m31coding.com>blog>fluent-api](https://www.m31codin
- Optional (skippable) builder methods
- Forking and branching capabilities
- Support for returning arbitrary types
- Support for generics and partial classes
- Support for inheritance, generics, and partial classes

## Installing via NuGet

Expand All @@ -37,7 +37,7 @@ PM> Install-Package M31.FluentApi
A package reference will be added to your `csproj` file. Moreover, since this library provides code via source code generation, consumers of your project don't need the reference to `M31.FluentApi`. Therefore, it is recommended to use the `PrivateAssets` metadata tag:

```xml
<PackageReference Include="M31.FluentApi" Version="1.8.0" PrivateAssets="all"/>
<PackageReference Include="M31.FluentApi" Version="1.9.0" PrivateAssets="all"/>
```

If you would like to examine the generated code, you may emit it by adding the following lines to your `csproj` file:
Expand Down
14 changes: 14 additions & 0 deletions src/ExampleProject/ExchangeStudent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Non-nullable member is uninitialized
#pragma warning disable CS8618
// ReSharper disable All

using M31.FluentApi.Attributes;

namespace ExampleProject;

[FluentApi]
public class ExchangeStudent : Student
{
[FluentMember(6)]
public string HomeCountry { get; private set; }
}
8 changes: 8 additions & 0 deletions src/ExampleProject/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
Console.WriteLine(JsonSerializer.Serialize(student1));
Console.WriteLine(JsonSerializer.Serialize(student2));

// ExchangeStudent (inherited from Student)
//

ExchangeStudent exchangeStudent = CreateExchangeStudent.Named("Bob", "Bishop").BornOn(new DateOnly(2002, 8, 3))
.InSemester(2).LivingInBoston().WithUnknownMood().WhoseFriendIs("Alice").WithHomeCountry("United States");

Console.WriteLine(JsonSerializer.Serialize(exchangeStudent));

// Person
//

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ protected override void InitializeInfoField(string fieldName, MemberSymbolInfo s
// semesterPropertyInfo = typeof(Student<T1, T2>)
// .GetProperty("Semester", BindingFlags.Instance | BindingFlags.NonPublic););
string code = $"{fieldName} =" +
$" typeof({CodeBoard.Info.FluentApiClassNameWithTypeParameters})" +
$" typeof({symbolInfo.DeclaringClassNameWithTypeParameters})" +
$".Get{SymbolType(symbolInfo)}(\"{symbolInfo.Name}\", " +
$"{InfoFieldBindingFlagsArgument(symbolInfo)})!;";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ protected override void InitializeInfoField(string fieldName, MethodSymbolInfo s
// Generic types are created via Type.MakeGenericMethodParameter(int position). In addition, a ref type is
// specified via MakeByRefType().
staticConstructor.AppendBodyLine($"{fieldName} = " +
$"typeof({CodeBoard.Info.FluentApiClassNameWithTypeParameters}).GetMethod(");
$"typeof({symbolInfo.DeclaringClassNameWithTypeParameters}).GetMethod(");
staticConstructor.AppendBodyLine($"{indentation}\"{symbolInfo.Name}\",");
staticConstructor.AppendBodyLine($"{indentation}{GetGenericParameterCount(symbolInfo.GenericInfo)},");
staticConstructor.AppendBodyLine($"{indentation}{InfoFieldBindingFlagsArgument(symbolInfo)},");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@ internal BuilderAndTargetInfo(
{
Namespace = @namespace;
FluentApiClassName = fluentApiClassName;
FluentApiClassNameWithTypeParameters = WithTypeParameters(fluentApiClassName, genericInfo);
FluentApiClassNameWithTypeParameters =
ClassInfoFactory.AugmentTypeNameWithGenericParameters(fluentApiClassName, genericInfo);
GenericInfo = genericInfo;
FluentApiTypeIsStruct = fluentApiTypeIsStruct;
FluentApiTypeIsInternal = fluentApiTypeIsInternal;
DefaultAccessModifier = fluentApiTypeIsInternal ? "internal" : "public";
FluentApiTypeConstructorInfo = fluentApiTypeConstructorInfo;
BuilderClassName = builderClassName;
BuilderClassNameWithTypeParameters = WithTypeParameters(builderClassName, genericInfo);
BuilderClassNameWithTypeParameters =
ClassInfoFactory.AugmentTypeNameWithGenericParameters(builderClassName, genericInfo);
BuilderInstanceName = builderClassName.FirstCharToLower();
ClassInstanceName = fluentApiClassName.FirstCharToLower();
InitialStepInterfaceName = $"I{builderClassName}";
Expand All @@ -43,10 +45,4 @@ internal BuilderAndTargetInfo(
internal string BuilderInstanceName { get; }
internal string ClassInstanceName { get; }
internal string InitialStepInterfaceName { get; }

private static string WithTypeParameters(string typeName, GenericInfo? genericInfo)
{
string parameterListInAngleBrackets = genericInfo?.ParameterListInAngleBrackets ?? string.Empty;
return $"{typeName}{parameterListInAngleBrackets}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,29 @@ namespace M31.FluentApi.Generator.CodeGeneration.CodeBoardElements;

internal abstract class FluentApiSymbolInfo
{
internal FluentApiSymbolInfo(string name, Accessibility accessibility, bool requiresReflection)
internal FluentApiSymbolInfo(
string name,
string declaringClassNameWithTypeParameters,
Accessibility accessibility,
bool requiresReflection)
{
Name = name;
NameInCamelCase = Name.TrimStart('_').FirstCharToLower();
DeclaringClassNameWithTypeParameters = declaringClassNameWithTypeParameters;
Accessibility = accessibility;
RequiresReflection = requiresReflection;
}

internal string Name { get; }
internal string NameInCamelCase { get; }
internal string DeclaringClassNameWithTypeParameters { get; }
internal Accessibility Accessibility { get; }
internal bool RequiresReflection { get; }

protected bool Equals(FluentApiSymbolInfo other)
{
return Name == other.Name &&
DeclaringClassNameWithTypeParameters == other.DeclaringClassNameWithTypeParameters &&
Accessibility == other.Accessibility &&
RequiresReflection == other.RequiresReflection;
}
Expand All @@ -35,6 +42,6 @@ public override bool Equals(object? obj)

public override int GetHashCode()
{
return new HashCode().Add(Name, Accessibility, RequiresReflection);
return new HashCode().Add(Name, DeclaringClassNameWithTypeParameters, Accessibility, RequiresReflection);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ internal class MemberSymbolInfo : FluentApiSymbolInfo
internal MemberSymbolInfo(
string name,
string type,
string declaringClassNameWithTypeParameters,
Accessibility accessibility,
bool requiresReflection,
string typeForCodeGeneration,
bool isNullable,
bool isProperty,
CollectionType? collectionType)
: base(name, accessibility, requiresReflection)
: base(name, declaringClassNameWithTypeParameters, accessibility, requiresReflection)
{
Type = type;
TypeForCodeGeneration = typeForCodeGeneration;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ internal class MethodSymbolInfo : FluentApiSymbolInfo
{
internal MethodSymbolInfo(
string name,
string declaringClassNameWithTypeParameters,
Accessibility accessibility,
bool requiresReflection,
GenericInfo? genericInfo,
IReadOnlyCollection<ParameterSymbolInfo> parameterInfos,
string returnType)
: base(name, accessibility, requiresReflection)
: base(name, declaringClassNameWithTypeParameters, accessibility, requiresReflection)
{
GenericInfo = genericInfo;
ParameterInfos = parameterInfos;
Expand Down
2 changes: 1 addition & 1 deletion src/M31.FluentApi.Generator/M31.FluentApi.Generator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<PackageVersion>1.8.0</PackageVersion>
<PackageVersion>1.9.0</PackageVersion>
<Authors>Kevin Schaal</Authors>
<Description>The generator package for M31.FluentAPI. Don't install this package explicitly, install M31.FluentAPI instead.</Description>
<PackageTags>fluentapi fluentbuilder fluentinterface fluentdesign fluent codegeneration</PackageTags>
Expand Down
88 changes: 65 additions & 23 deletions src/M31.FluentApi.Generator/SourceGenerators/ClassInfoFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,7 @@ private ClassInfoResult CreateFluentApiClassInfoInternal(
bool isStruct = syntaxKind is SyntaxKind.StructDeclaration or SyntaxKind.RecordStructDeclaration;

FluentApiClassInfo? classInfo = CreateFluentApiClassInfo(
typeData.Type,
typeData.GenericInfo,
typeData.AttributeData,
typeData.UsingStatements,
typeData,
isStruct,
generatorConfig.NewLineString,
cancellationToken);
Expand All @@ -75,35 +72,48 @@ private ClassInfoResult CreateFluentApiClassInfoInternal(
}

private FluentApiClassInfo? CreateFluentApiClassInfo(
INamedTypeSymbol type,
GenericInfo? genericInfo,
AttributeDataExtended attributeDataExtended,
IReadOnlyCollection<string> usingStatements,
TypeData typeData,
bool isStruct,
string newLineString,
CancellationToken cancellationToken)
{
string className = type.Name;
string? @namespace = type.ContainingNamespace.IsGlobalNamespace ? null : type.ContainingNamespace.ToString();
bool isInternal = type.DeclaredAccessibility == Accessibility.Internal;
ConstructorInfo? constructorInfo = TryGetConstructorInfo(type);
string className = typeData.Type.Name;
string? @namespace = typeData.Type.ContainingNamespace.IsGlobalNamespace
? null
: typeData.Type.ContainingNamespace.ToString();
bool isInternal = typeData.Type.DeclaredAccessibility == Accessibility.Internal;
ConstructorInfo? constructorInfo = TryGetConstructorInfo(typeData.Type);
FluentApiAttributeInfo fluentApiAttributeInfo =
FluentApiAttributeInfo.Create(attributeDataExtended.AttributeData, className);
FluentApiAttributeInfo.Create(typeData.AttributeDataExtended.AttributeData, className);

List<FluentApiInfo> infos = new List<FluentApiInfo>();
(ITypeSymbol declaringType, ISymbol[] members)[] allMembers =
GetMembersOfTypeAndBaseTypes(typeData.Type).ToArray();

foreach (var member in type.GetMembers().Where(m => m.CanBeReferencedByName && m.Name != string.Empty))
foreach ((ITypeSymbol declaringType, ISymbol[] members) in allMembers)
{
if (cancellationToken.IsCancellationRequested)
if (declaringType is not INamedTypeSymbol namedTypeSymbol)
{
return null;
throw new GenerationException($"The type {declaringType.Name} is not a named type symbol.");
}

FluentApiInfo? fluentApiInfo = TryCreateFluentApiInfo(member);
GenericInfo? genericInfo = GenericInfo.TryCreate(namedTypeSymbol);
string declaringClassNameWithGenericParameters =
AugmentTypeNameWithGenericParameters(namedTypeSymbol.Name, genericInfo);

if (fluentApiInfo != null)
foreach (ISymbol member in members)
{
infos.Add(fluentApiInfo);
if (cancellationToken.IsCancellationRequested)
{
return null;
}

FluentApiInfo? fluentApiInfo = TryCreateFluentApiInfo(member, declaringClassNameWithGenericParameters);

if (fluentApiInfo != null)
{
infos.Add(fluentApiInfo);
}
}
}

Expand All @@ -112,17 +122,43 @@ private ClassInfoResult CreateFluentApiClassInfoInternal(
return new FluentApiClassInfo(
className,
@namespace,
genericInfo,
typeData.GenericInfo,
isStruct,
isInternal,
constructorInfo!,
fluentApiAttributeInfo.BuilderClassName,
newLineString,
infos,
usingStatements,
typeData.UsingStatements,
new FluentApiClassAdditionalInfo(groups));
}

private static List<(ITypeSymbol declaringType, ISymbol[] members)> GetMembersOfTypeAndBaseTypes(
ITypeSymbol typeSymbol)
{
List<(ITypeSymbol declaringType, ISymbol[] members)> result =
new List<(ITypeSymbol declaringType, ISymbol[] members)>();

GetMembers(typeSymbol);
return result;

void GetMembers(ITypeSymbol currentTypeSymbol)
{
ISymbol[] members = currentTypeSymbol.GetMembers()
.Where(m => m.CanBeReferencedByName && m.Name != string.Empty).ToArray();

result.Add((currentTypeSymbol, members));

if (currentTypeSymbol.BaseType == null ||
currentTypeSymbol.BaseType.SpecialType == SpecialType.System_Object)
{
return;
}

GetMembers(currentTypeSymbol.BaseType);
}
}

private ConstructorInfo? TryGetConstructorInfo(INamedTypeSymbol type)
{
/* Look for the default constructor. If it is not present, take the constructor
Expand Down Expand Up @@ -165,7 +201,7 @@ with the fewest parameters that is explicitly declared. */
constructors[0].DeclaredAccessibility != Accessibility.Public);
}

private FluentApiInfo? TryCreateFluentApiInfo(ISymbol symbol)
private FluentApiInfo? TryCreateFluentApiInfo(ISymbol symbol, string declaringClassNameWithTypeParameters)
{
AttributeDataExtractor extractor = new AttributeDataExtractor(report);
FluentApiAttributeData? attributeData = extractor.GetAttributeData(symbol);
Expand All @@ -182,6 +218,12 @@ with the fewest parameters that is explicitly declared. */
}

FluentApiInfoCreator fluentApiInfoCreator = new FluentApiInfoCreator(report);
return fluentApiInfoCreator.Create(symbol, attributeData);
return fluentApiInfoCreator.Create(symbol, attributeData, declaringClassNameWithTypeParameters);
}

public static string AugmentTypeNameWithGenericParameters(string typeName, GenericInfo? genericInfo)
{
string parameterListInAngleBrackets = genericInfo?.ParameterListInAngleBrackets ?? string.Empty;
return $"{typeName}{parameterListInAngleBrackets}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ internal FluentApiInfoCreator(ClassInfoReport classInfoReport)
this.classInfoReport = classInfoReport;
}

internal FluentApiInfo? Create(ISymbol symbol, FluentApiAttributeData attributeData)
internal FluentApiInfo? Create(
ISymbol symbol,
FluentApiAttributeData attributeData,
string declaringClassNameWithTypeParameters)
{
FluentApiSymbolInfo symbolInfo = SymbolInfoCreator.Create(symbol);
FluentApiSymbolInfo symbolInfo = SymbolInfoCreator.Create(symbol, declaringClassNameWithTypeParameters);
AttributeInfoBase? attributeInfo = CreateAttributeInfo(attributeData.MainAttributeData, symbol, symbolInfo);

if (attributeInfo == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ private GenericInfo(IReadOnlyCollection<GenericTypeParameter> parameters)
Parameters = parameters;
}

internal static GenericInfo? TryCreate(INamedTypeSymbol type)
{
return type.IsGenericType ? Create(type.TypeParameters) : null;
}

internal static GenericInfo Create(IEnumerable<ITypeParameterSymbol> typeParameters)
{
GenericTypeParameter[] parameters =
Expand Down
Loading
Loading