Skip to content

Commit 1ac38b9

Browse files
authored
feat: FluentSkippable (#19)
* feat(FluentSkippable): FluentSkippable attribute * feat(FluentSkippable): make SkippableMemberClass test work * refactor(BuilderStepMethodCreator): create static methods from BuilderStepMethods * refactor(BuilderStepMethodCreator): cleanup * feat(FluentSkippable): make SkippableFirstMemberClass test work * test: SkippableSeveralMembersClass * fix(SkippableSeveralMembersClass): remove blank line * feat(FluentSkippable): last step cannot be skipped diagnostic * test: add failing test SkippableLoopClass * Test(SkippableLoopClass): add expected code * test(SkippableLoopClass): add desired CreatedStudent.g.cs * feat(FluentSkippable): BuilderStepsGenerator new version * feat(FluentSkippable): loop handling classes * feat(FluentSkippable): make SkippableLoopClass test work * fix: remove obsolete EmptyInterfaceBuilderMethod class * refactor: rename BuilderStepsGeneration folder and namespace * fix: rename file * test: CanExecuteSkippableLoopClass * fix: rename FirstStepBuilderMethod and SingleStepBuilderMethod classes * test: TarjansSccAlgorithmTests * test: ContinueWithInForkClass * test: SkippableFirstTwoMembersClass * test: SkippableTwoLoopsClass * test: SkippableForkMembersClass * fix: address resharper warnings * feat(FluentSkippable): adjust examples and storybook * docs(Readme): add FluentSkippable * chore: increase nuget versions to 1.6.0
1 parent 86374ac commit 1ac38b9

File tree

170 files changed

+3824
-932
lines changed

Some content is hidden

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

170 files changed

+3824
-932
lines changed

Diff for: README.md

+38-8
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ PM> Install-Package M31.FluentApi
2525
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:
2626

2727
```xml
28-
<PackageReference Include="M31.FluentApi" Version="1.5.0" PrivateAssets="all"/>
28+
<PackageReference Include="M31.FluentApi" Version="1.6.0" PrivateAssets="all"/>
2929
```
3030

3131
If you would like to examine the generated code, you may emit it by adding the following lines to your `csproj` file:
@@ -111,7 +111,7 @@ The attributes `FluentPredicate`, `FluentCollection`, and `FluentLambda` can be
111111

112112
The `FluentMethod` attribute is used for custom builder method implementations.
113113

114-
The control attribute `FluentContinueWith` indicates a jump to the specified builder step, and `FluentBreak` stops the builder. `FluentReturn` allows returning arbitrary types and values within the generated API.
114+
The control attribute `FluentSkippable` allows builder methods to be optional, while `FluentContinueWith` indicates a jump to the specified builder step. `FluentBreak` stops the builder, and `FluentReturn` allows returning arbitrary types and values within the generated API.
115115

116116

117117
### FluentApi
@@ -300,23 +300,23 @@ private void BornOn(DateOnly dateOfBirth)
300300
```
301301

302302

303-
### FluentContinueWith
303+
### FluentSkippable
304304

305305
```cs
306-
FluentContinueWith(int builderStep)
306+
FluentSkippable()
307307
```
308308

309-
Can be used at all steps on fields, properties, and methods to jump to a specific builder step. Useful for skipping steps and branching. May be used to create optional builder methods:
309+
Can be used at all steps on fields, properties, and methods to create an optional builder method. The generated API will offer the method but it does not have to be called.
310310

311311
```cs
312312
[FluentMember(0)]
313313
public string FirstName { get; private set; }
314314

315315
[FluentMember(1)]
316-
[FluentContinueWith(1)]
316+
[FluentSkippable]
317317
public string? MiddleName { get; private set; }
318318

319-
[FluentMember(1)]
319+
[FluentMember(2)]
320320
public string LastName { get; private set; }
321321
```
322322

@@ -326,6 +326,36 @@ public string LastName { get; private set; }
326326
```
327327

328328

329+
### FluentContinueWith
330+
331+
```cs
332+
FluentContinueWith(int builderStep)
333+
```
334+
335+
Can be used at all steps on fields, properties, and methods to jump to a specific builder step. Useful for branching.
336+
337+
```cs
338+
[FluentMethod(3)]
339+
[FluentContinueWith(7)]
340+
private void WhoIsADigitalNomad()
341+
{
342+
IsDigitalNomad = true;
343+
}
344+
345+
// ...
346+
347+
[FluentMethod(7)]
348+
private void LivingInCity(string city)
349+
{
350+
City = city;
351+
}
352+
```
353+
354+
```cs
355+
...WhoIsADigitalNomad().LivingInCity("Berlin")...
356+
```
357+
358+
329359
### FluentBreak
330360

331361
```cs
@@ -335,7 +365,7 @@ FluentBreak()
335365
Can be used at all steps on fields, properties, and methods to stop the builder. Only relevant for non-linear APIs that make use of `FluentContinueWith`.
336366

337367
```cs
338-
[FluentMethod(1)]
368+
[FluentMethod(3)]
339369
[FluentBreak]
340370
private void WhoseAddressIsUnknown()
341371
{

Diff for: src/ExampleProject/Person.cs

+10-10
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ public class Person
1313
public string FirstName { get; private set; }
1414

1515
[FluentMember(1)]
16-
[FluentContinueWith(1)]
16+
[FluentSkippable]
1717
public string? MiddleName { get; private set; }
1818

19-
[FluentMember(1)]
19+
[FluentMember(2)]
2020
public string LastName { get; private set; }
2121

2222
public string? HouseNumber { get; private set; }
@@ -27,44 +27,44 @@ public class Person
2727

2828
public bool IsDigitalNomad { get; private set; }
2929

30-
[FluentMethod(2)]
30+
[FluentMethod(3)]
3131
[FluentBreak]
3232
private void WhoseAddressIsUnknown()
3333
{
3434
}
3535

36-
[FluentMethod(2)]
36+
[FluentMethod(3)]
3737
private void WhoLivesAtAddress()
3838
{
3939
}
4040

41-
[FluentMethod(3)]
41+
[FluentMethod(4)]
4242
private void WithHouseNumber(string houseNumber)
4343
{
4444
HouseNumber = houseNumber;
4545
}
4646

47-
[FluentMethod(4)]
47+
[FluentMethod(5)]
4848
private void WithStreet(string street)
4949
{
5050
Street = street;
5151
}
5252

53-
[FluentMethod(5)]
53+
[FluentMethod(6)]
5454
[FluentBreak]
5555
private void InCity(string city)
5656
{
5757
City = city;
5858
}
5959

60-
[FluentMethod(2)]
61-
[FluentContinueWith(6)]
60+
[FluentMethod(3)]
61+
[FluentContinueWith(7)]
6262
private void WhoIsADigitalNomad()
6363
{
6464
IsDigitalNomad = true;
6565
}
6666

67-
[FluentMethod(6)]
67+
[FluentMethod(7)]
6868
private void LivingInCity(string city)
6969
{
7070
City = city;

Diff for: src/M31.FluentApi.Generator/AnalyzerReleases.Shipped.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,13 @@ M31FA022 | M31.Usage | Error | Fluent lambda member without Fluent API
5959

6060
Rule ID | Category | Severity | Notes
6161
--------|----------|----------|-------
62-
M31FA007 | M31.Usage | Error | Partial types are not supported
62+
M31FA007 | M31.Usage | Error | Partial types are not supported
63+
64+
65+
## Release 1.6.0
66+
67+
### New Rules
68+
69+
Rule ID | Category | Severity | Notes
70+
--------|----------|----------|-------
71+
M31FA023 | M31.Usage | Error | Last builder step cannot be skipped

Diff for: src/M31.FluentApi.Generator/CodeBuilding/Interface.cs

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// ReSharper disable ParameterHidesMember
2+
13
namespace M31.FluentApi.Generator.CodeBuilding;
24

35
internal class Interface : ICode
@@ -33,6 +35,11 @@ internal void AddBaseInterface(string baseInterface)
3335
baseInterfaces.Add(baseInterface);
3436
}
3537

38+
internal void AddBaseInterfaces(IEnumerable<string> baseInterfaces)
39+
{
40+
this.baseInterfaces.AddRange(baseInterfaces);
41+
}
42+
3643
public CodeBuilder AppendCode(CodeBuilder codeBuilder)
3744
{
3845
return codeBuilder
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace M31.FluentApi.Generator.CodeGeneration.CodeBoardActors.BuilderMethodsGeneration;
2+
3+
internal class BaseInterface
4+
{
5+
public BaseInterface(string name, int step)
6+
{
7+
Name = name;
8+
Step = step;
9+
}
10+
11+
public string Name { get; }
12+
public int Step { get; }
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using M31.FluentApi.Generator.CodeBuilding;
2+
using M31.FluentApi.Generator.CodeGeneration.CodeBoardElements;
3+
4+
namespace M31.FluentApi.Generator.CodeGeneration.CodeBoardActors.BuilderMethodsGeneration;
5+
6+
internal class BuilderGenerator : ICodeBoardActor
7+
{
8+
public void Modify(CodeBoard codeBoard)
9+
{
10+
BuilderMethods builderMethods =
11+
BuilderMethodsCreator.CreateBuilderMethods(codeBoard.Forks, codeBoard.CancellationToken);
12+
13+
foreach (BuilderStepMethod staticMethod in builderMethods.StaticMethods)
14+
{
15+
if (codeBoard.CancellationToken.IsCancellationRequested)
16+
{
17+
break;
18+
}
19+
20+
Method method = CreateMethod(staticMethod, codeBoard);
21+
codeBoard.BuilderClass.AddMethod(method);
22+
}
23+
24+
List<Interface> interfaces = new List<Interface>(builderMethods.Interfaces.Count);
25+
interfaces.Add(CreateInitialStepInterface(builderMethods, codeBoard));
26+
27+
foreach (BuilderInterface builderInterface in builderMethods.Interfaces)
28+
{
29+
if (codeBoard.CancellationToken.IsCancellationRequested)
30+
{
31+
break;
32+
}
33+
34+
Interface @interface =
35+
new Interface(codeBoard.Info.DefaultAccessModifier, builderInterface.InterfaceName);
36+
37+
foreach (InterfaceBuilderMethod interfaceMethod in builderInterface.Methods)
38+
{
39+
Method method = CreateMethod(interfaceMethod, codeBoard);
40+
codeBoard.BuilderClass.AddMethod(method);
41+
@interface.AddMethodSignature(method.MethodSignature.ToSignatureForInterface());
42+
}
43+
44+
@interface.AddBaseInterfaces(builderInterface.BaseInterfaces);
45+
interfaces.Add(@interface);
46+
}
47+
48+
AddInterfacesToBuilderClass(
49+
interfaces,
50+
codeBoard.BuilderClass,
51+
codeBoard.Info.BuilderClassNameWithTypeParameters);
52+
AddInterfaceDefinitionsToBuilderClass(interfaces, codeBoard.BuilderClass);
53+
}
54+
55+
private Method CreateMethod(BuilderStepMethod builderStepMethod, CodeBoard codeBoard)
56+
{
57+
ReservedVariableNames reservedVariableNames = codeBoard.ReservedVariableNames.NewLocalScope();
58+
reservedVariableNames.ReserveLocalVariableNames(builderStepMethod.Parameters.Select(p => p.Name));
59+
60+
Method method = builderStepMethod.BuildMethodCode(
61+
codeBoard.Info,
62+
reservedVariableNames);
63+
64+
return method;
65+
}
66+
67+
private Interface CreateInitialStepInterface(BuilderMethods builderMethods, CodeBoard codeBoard)
68+
{
69+
string? firstInterfaceName = builderMethods.Interfaces.FirstOrDefault()?.InterfaceName;
70+
71+
Interface initialStepInterface =
72+
new Interface(codeBoard.Info.DefaultAccessModifier, codeBoard.Info.InitialStepInterfaceName);
73+
74+
if (firstInterfaceName != null)
75+
{
76+
initialStepInterface.AddBaseInterface(firstInterfaceName);
77+
}
78+
79+
return initialStepInterface;
80+
}
81+
82+
private void AddInterfacesToBuilderClass(List<Interface> interfaces, Class builderClass, string prefix)
83+
{
84+
foreach (Interface @interface in interfaces)
85+
{
86+
builderClass.AddInterface($"{prefix}.{@interface.Name}");
87+
}
88+
}
89+
90+
private void AddInterfaceDefinitionsToBuilderClass(List<Interface> interfaces, Class builderClass)
91+
{
92+
foreach (Interface @interface in interfaces)
93+
{
94+
builderClass.AddDefinition(@interface);
95+
}
96+
}
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace M31.FluentApi.Generator.CodeGeneration.CodeBoardActors.BuilderMethodsGeneration;
2+
3+
internal class BuilderInterface
4+
{
5+
internal BuilderInterface(
6+
string interfaceName,
7+
IReadOnlyCollection<string> baseInterfaces,
8+
IReadOnlyCollection<InterfaceBuilderMethod> methods)
9+
{
10+
InterfaceName = interfaceName;
11+
BaseInterfaces = baseInterfaces;
12+
Methods = methods;
13+
}
14+
15+
public string InterfaceName { get; }
16+
public IReadOnlyCollection<string> BaseInterfaces { get; }
17+
public IReadOnlyCollection<InterfaceBuilderMethod> Methods { get; }
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using M31.FluentApi.Generator.Commons;
2+
3+
namespace M31.FluentApi.Generator.CodeGeneration.CodeBoardActors.BuilderMethodsGeneration;
4+
5+
internal class BuilderMethods
6+
{
7+
public BuilderMethods(
8+
IReadOnlyCollection<BuilderStepMethod> staticMethods,
9+
IReadOnlyCollection<BuilderInterface> interfaces)
10+
{
11+
Interfaces = interfaces;
12+
StaticMethods = staticMethods;
13+
}
14+
15+
internal IReadOnlyCollection<BuilderInterface> Interfaces { get; }
16+
internal IReadOnlyCollection<BuilderStepMethod> StaticMethods { get; }
17+
18+
internal static IReadOnlyCollection<BuilderInterface> CreateInterfaces(
19+
IReadOnlyCollection<InterfaceBuilderMethod> interfaceMethods,
20+
CancellationToken cancellationToken)
21+
{
22+
List<BuilderInterface> interfaces = new List<BuilderInterface>();
23+
24+
IGrouping<string, InterfaceBuilderMethod>[] methodsGroupedByInterface =
25+
interfaceMethods.GroupBy(m => m.InterfaceName).ToArray();
26+
27+
foreach (IGrouping<string, InterfaceBuilderMethod> group in methodsGroupedByInterface)
28+
{
29+
if (cancellationToken.IsCancellationRequested)
30+
{
31+
break;
32+
}
33+
34+
string interfaceName = group.Key;
35+
36+
List<BaseInterface> baseInterfaces = new List<BaseInterface>();
37+
38+
foreach (InterfaceBuilderMethod method in group)
39+
{
40+
if (method.BaseInterface != null)
41+
{
42+
baseInterfaces.Add(method.BaseInterface);
43+
}
44+
}
45+
46+
string[] baseInterfaceNames = baseInterfaces
47+
.DistinctBy(i => i.Name)
48+
.OrderBy(i => i.Step)
49+
.Select(i => i.Name).ToArray();
50+
51+
interfaces.Add(new BuilderInterface(interfaceName, baseInterfaceNames, group.ToArray()));
52+
}
53+
54+
return interfaces;
55+
}
56+
}

0 commit comments

Comments
 (0)