Skip to content

Commit 4df6f4f

Browse files
authored
feat: builder from existing instance (#53)
* chore: Employee2 generated code example * feat: constructor with instance * chore: expected results with constructor with instance * feat: FromExistingInterface * feat: FromExistingInterface * chore: adapt tests for any step interface * feat: FromExistingMethodGenerator * chore: overwrite tests with new generation * test: CanCreateThreeMemberClassFromExisting * docs(01_Basics): explain FromExisting * docs(README.md): add paragraph about `FromExisting` * docs(01_Basics): clarify FromExisting * chore: bump nuget version * docs(README): update doc comments image
1 parent 575c5b8 commit 4df6f4f

241 files changed

Lines changed: 3562 additions & 16 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ PM> Install-Package M31.FluentApi
4242
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:
4343

4444
```xml
45-
<PackageReference Include="M31.FluentApi" Version="2.0.0" PrivateAssets="all"/>
45+
<PackageReference Include="M31.FluentApi" Version="2.1.0" PrivateAssets="all"/>
4646
```
4747

4848
If you would like to examine the generated code, you may emit it by adding the following lines to your `csproj` file:
@@ -502,6 +502,18 @@ To simplify adding documentation comments, a code action is available to generat
502502
![doc-comments-action](https://raw.githubusercontent.com/m31coding/M31.FluentAPI/main/media/create-doc-comments-action.png)
503503

504504
For reference, you can view the documented version of the `Student` class in [DocumentedStudent.cs](src/M31.FluentApi.Tests/CodeGeneration/TestClasses/DocumentedStudentClass/DocumentedStudent.cs). The corresponding generated code is located in [DocumentedStudent.g.cs](src/M31.FluentApi.Tests/CodeGeneration/TestClasses/DocumentedStudentClass/CreateDocumentedStudent.g.cs)
505+
506+
507+
### Modifying an existing instance
508+
509+
The static `FromExisting` method can be used to modify an existing instance:
510+
511+
```cs
512+
Student student1 = CreateStudent.WithFirstName("Alice").WithLastName("King");
513+
Student student2 = CreateStudent.FromExisting(student1).WithLastName("Queen");
514+
```
515+
516+
After calling `FromExisting`, the builder can continue from any step. To avoid mutating the original instance, create a copy constructor on the `Student` class and pass a copy to the `FromExisting` method.
505517

506518

507519
### Lambda pattern
@@ -592,4 +604,4 @@ In particular, if your IDE visually indicates that there are errors in your code
592604

593605
This library is free. If you find it valuable and wish to express your support, please leave a star. You are kindly invited to contribute. If you see the possibility for enhancement, please create a GitHub issue and you will receive timely feedback.
594606

595-
Happy coding!
607+
Happy coding!
11.5 KB
Loading

src/M31.FluentApi.Generator/CodeGeneration/CodeBoardActors/BuilderMethodsGeneration/BuilderGenerator.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public void Modify(CodeBoard codeBoard)
2323

2424
List<Interface> interfaces = new List<Interface>(builderStepMethods.Interfaces.Count);
2525
interfaces.Add(CreateInitialStepInterface(builderStepMethods, codeBoard));
26+
interfaces.Add(CreateFromAnyStepInterface(builderStepMethods, codeBoard));
2627

2728
foreach (BuilderInterface builderInterface in builderStepMethods.Interfaces)
2829
{
@@ -92,6 +93,19 @@ private Interface CreateInitialStepInterface(BuilderStepMethods builderStepMetho
9293
return initialStepInterface;
9394
}
9495

96+
private Interface CreateFromAnyStepInterface(BuilderStepMethods builderStepMethods, CodeBoard codeBoard)
97+
{
98+
Interface fromExistingInterface =
99+
new Interface(codeBoard.Info.DefaultAccessModifier, codeBoard.Info.FromExistingInterfaceName);
100+
101+
foreach (BuilderInterface @interface in builderStepMethods.Interfaces)
102+
{
103+
fromExistingInterface.AddBaseInterface(@interface.InterfaceName);
104+
}
105+
106+
return fromExistingInterface;
107+
}
108+
95109
private void AddInterfacesToBuilderClass(List<Interface> interfaces, Class builderClass, string prefix)
96110
{
97111
foreach (Interface @interface in interfaces)

src/M31.FluentApi.Generator/CodeGeneration/CodeBoardActors/ConstructorGenerator.cs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,17 @@ namespace M31.FluentApi.Generator.CodeGeneration.CodeBoardActors;
88
internal class ConstructorGenerator : ICodeBoardActor
99
{
1010
public void Modify(CodeBoard codeBoard)
11+
{
12+
CreateParameterlessConstructor(codeBoard);
13+
CreateConstructorWithInstanceParameter(codeBoard);
14+
}
15+
16+
private static void CreateParameterlessConstructor(CodeBoard codeBoard)
1117
{
1218
string instanceName = codeBoard.Info.ClassInstanceName;
1319
string classNameWithTypeParameters = codeBoard.Info.FluentApiClassNameWithTypeParameters;
1420

15-
Method constructor = CreateConstructor(codeBoard.Info.BuilderClassName);
21+
Method constructor = CreateParameterlessConstructorMethod(codeBoard.Info.BuilderClassName);
1622
ConstructorInfo constructorInfo = codeBoard.Info.FluentApiTypeConstructorInfo;
1723

1824
if (codeBoard.Info.FluentApiTypeConstructorInfo.ConstructorIsNonPublic)
@@ -59,7 +65,17 @@ public void Modify(CodeBoard codeBoard)
5965
constructor.AppendBodyLine(codeBuilder.ToString());
6066
}
6167

62-
codeBoard.Constructor = constructor;
68+
codeBoard.BuilderClass.AddMethod(constructor);
69+
}
70+
71+
private static void CreateConstructorWithInstanceParameter(CodeBoard codeBoard)
72+
{
73+
Method constructor = CreateConstructorWithInstanceParameterMethod(codeBoard.Info);
74+
75+
CodeBuilder codeBuilder = new CodeBuilder(codeBoard.NewLineString);
76+
codeBuilder.Append($"this.{codeBoard.Info.ClassInstanceName} = {codeBoard.Info.ClassInstanceName};");
77+
constructor.AppendBodyLine(codeBuilder.ToString());
78+
6379
codeBoard.BuilderClass.AddMethod(constructor);
6480
}
6581

@@ -112,11 +128,22 @@ private static string CreateArgument(
112128
return "default!";
113129
}
114130

115-
private static Method CreateConstructor(string builderClassName)
131+
private static Method CreateParameterlessConstructorMethod(string builderClassName)
116132
{
117133
// private CreateStudent()
118134
MethodSignature signature = MethodSignature.CreateConstructorSignature(builderClassName);
119135
signature.AddModifiers("private");
120136
return new Method(signature);
121137
}
138+
139+
private static Method CreateConstructorWithInstanceParameterMethod(
140+
BuilderAndTargetInfo info)
141+
{
142+
// private CreateStudent(Student student)
143+
MethodSignature signature = MethodSignature.CreateConstructorSignature(info.BuilderClassName);
144+
Parameter parameter = new Parameter(info.FluentApiClassNameWithTypeParameters, info.ClassInstanceName);
145+
signature.AddParameter(parameter);
146+
signature.AddModifiers("private");
147+
return new Method(signature);
148+
}
122149
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using M31.FluentApi.Generator.CodeBuilding;
2+
using M31.FluentApi.Generator.CodeGeneration.CodeBoardElements;
3+
4+
namespace M31.FluentApi.Generator.CodeGeneration.CodeBoardActors;
5+
6+
internal class FromExistingMethodGenerator : ICodeBoardActor
7+
{
8+
public void Modify(CodeBoard codeBoard)
9+
{
10+
// public static ICreateStudentFromAnyStep FromExisting(Student student)
11+
// {
12+
// return new CreateStudent(student);
13+
// }
14+
BuilderAndTargetInfo info = codeBoard.Info;
15+
string methodName = "FromExisting";
16+
MethodSignature methodSignature = MethodSignature.Create(
17+
info.FromExistingInterfaceName,
18+
methodName,
19+
null,
20+
false);
21+
methodSignature.AddModifiers(info.DefaultAccessModifier, "static");
22+
Parameter parameter = new Parameter(
23+
info.FluentApiClassNameWithTypeParameters,
24+
info.ClassInstanceName);
25+
methodSignature.AddParameter(parameter);
26+
Method method = new Method(methodSignature);
27+
string parameterListInAngleBrackets = info.GenericInfo?.ParameterListInAngleBrackets ?? string.Empty;
28+
method.AppendBodyLine(
29+
$"return new {info.BuilderClassName}{parameterListInAngleBrackets}({info.ClassInstanceName});");
30+
codeBoard.BuilderClass.AddMethod(method);
31+
}
32+
}

src/M31.FluentApi.Generator/CodeGeneration/CodeBoardElements/BuilderAndTargetInfo.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ internal BuilderAndTargetInfo(
3030
BuilderInstanceName = builderClassName.FirstCharToLower();
3131
ClassInstanceName = fluentApiClassName.FirstCharToLower();
3232
InitialStepInterfaceName = $"I{builderClassName}";
33+
FromExistingInterfaceName = $"I{builderClassName}FromExisting";
3334
}
3435

3536
internal string? Namespace { get; }
@@ -45,4 +46,5 @@ internal BuilderAndTargetInfo(
4546
internal string BuilderInstanceName { get; }
4647
internal string ClassInstanceName { get; }
4748
internal string InitialStepInterfaceName { get; }
49+
internal string FromExistingInterfaceName { get; }
4850
}

src/M31.FluentApi.Generator/CodeGeneration/CodeBoardElements/CodeBoard.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ private CodeBoard(
3030
Info = builderAndTargetInfo;
3131
CodeFile = codeFile;
3232
BuilderClass = builderClass;
33-
Constructor = null;
34-
StaticConstructor = null;
3533
InnerBodyCreationDelegates = new InnerBodyCreationDelegates();
3634
TransformedComments = new TransformedComments();
3735
GroupsToMethods = new Dictionary<FluentApiInfoGroup, BuilderMethods>();
@@ -48,8 +46,6 @@ private CodeBoard(
4846
internal BuilderAndTargetInfo Info { get; }
4947
internal CodeFile CodeFile { get; }
5048
internal Class BuilderClass { get; }
51-
internal Method? Constructor { get; set; }
52-
internal Method? StaticConstructor { get; set; }
5349
internal InnerBodyCreationDelegates InnerBodyCreationDelegates { get; }
5450
internal TransformedComments TransformedComments { get; }
5551
internal Dictionary<FluentApiInfoGroup, BuilderMethods> GroupsToMethods { get; }

src/M31.FluentApi.Generator/CodeGeneration/CodeGenerator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ internal static CodeGeneratorResult GenerateCode(FluentApiClassInfo classInfo, C
2626
new ForkCreator(),
2727
new DuplicateMethodsChecker(),
2828
new InitialStepMethodGenerator(),
29+
new FromExistingMethodGenerator(),
2930
new BuilderGenerator(),
3031
};
3132

src/M31.FluentApi.Generator/M31.FluentApi.Generator.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<GenerateDocumentationFile>true</GenerateDocumentationFile>
1212
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
1313
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
14-
<PackageVersion>2.0.0</PackageVersion>
14+
<PackageVersion>2.1.0</PackageVersion>
1515
<Authors>Kevin Schaal</Authors>
1616
<Description>The generator package for M31.FluentAPI. Don't install this package explicitly, install M31.FluentAPI instead.</Description>
1717
<PackageTags>fluentapi fluentbuilder fluentinterface fluentdesign fluent codegeneration</PackageTags>

src/M31.FluentApi.Storybook/01_Basics.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@ namespace BasicExample
88
{
99
/* Generates a builder class with name CreateStudent and methods WithFirstName and WithLastName. The methods have to
1010
be called in the specified order, WithFirstName (builder step 0) has to be called before WithLastName (builder
11-
step 1). As shown in the usage examples below, a student can be either created by calling the static
12-
WithFirstName method on the CreateStudent class, or by first creating a new builder instance.
11+
step 1).
12+
As shown in the usage examples below, a student can be created by calling the static WithFirstName
13+
method on the CreateStudent class.
14+
Alternatively, an existing Student can be passed to the static FromExisting method. After calling FromExisting,
15+
the builder can continue from any step. The FromExisting method mutates the passed instance, and the last name
16+
is set to "Queen".
17+
The InitialStep method can be used to create a builder instance without creating a student. This is useful for
18+
advanced use cases such as the lambda pattern, as described in the README.md.
1319
Although I use classes with properties in all examples of this file, the FluentApi attribute can also be applied
1420
to structs and records, and the FluentMember attribute also works with fields. */
1521

@@ -29,8 +35,10 @@ public static void UseTheGeneratedFluentApi()
2935
{
3036
Student student1 = CreateStudent.WithFirstName("Alice").WithLastName("King");
3137

38+
Student student2 = CreateStudent.FromExisting(student1).WithLastName("Queen");
39+
3240
CreateStudent.ICreateStudent createStudent = CreateStudent.InitialStep();
33-
Student student2 = createStudent.WithFirstName("Bob").WithLastName("Bishop");
41+
Student student3 = createStudent.WithFirstName("Bob").WithLastName("Bishop");
3442
}
3543
}
3644
}

0 commit comments

Comments
 (0)