Skip to content

Commit

Permalink
feat: FluentSkippable (#19)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
m31coding authored Jun 9, 2024
1 parent 86374ac commit 1ac38b9
Show file tree
Hide file tree
Showing 170 changed files with 3,824 additions and 932 deletions.
46 changes: 38 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,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.5.0" PrivateAssets="all"/>
<PackageReference Include="M31.FluentApi" Version="1.6.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 Expand Up @@ -111,7 +111,7 @@ The attributes `FluentPredicate`, `FluentCollection`, and `FluentLambda` can be

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

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.
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.


### FluentApi
Expand Down Expand Up @@ -300,23 +300,23 @@ private void BornOn(DateOnly dateOfBirth)
```


### FluentContinueWith
### FluentSkippable

```cs
FluentContinueWith(int builderStep)
FluentSkippable()
```

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:
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.

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

[FluentMember(1)]
[FluentContinueWith(1)]
[FluentSkippable]
public string? MiddleName { get; private set; }

[FluentMember(1)]
[FluentMember(2)]
public string LastName { get; private set; }
```

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


### FluentContinueWith

```cs
FluentContinueWith(int builderStep)
```

Can be used at all steps on fields, properties, and methods to jump to a specific builder step. Useful for branching.

```cs
[FluentMethod(3)]
[FluentContinueWith(7)]
private void WhoIsADigitalNomad()
{
IsDigitalNomad = true;
}

// ...
[FluentMethod(7)]
private void LivingInCity(string city)
{
City = city;
}
```

```cs
...WhoIsADigitalNomad().LivingInCity("Berlin")...
```


### FluentBreak

```cs
Expand All @@ -335,7 +365,7 @@ FluentBreak()
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`.

```cs
[FluentMethod(1)]
[FluentMethod(3)]
[FluentBreak]
private void WhoseAddressIsUnknown()
{
Expand Down
20 changes: 10 additions & 10 deletions src/ExampleProject/Person.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ public class Person
public string FirstName { get; private set; }

[FluentMember(1)]
[FluentContinueWith(1)]
[FluentSkippable]
public string? MiddleName { get; private set; }

[FluentMember(1)]
[FluentMember(2)]
public string LastName { get; private set; }

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

public bool IsDigitalNomad { get; private set; }

[FluentMethod(2)]
[FluentMethod(3)]
[FluentBreak]
private void WhoseAddressIsUnknown()
{
}

[FluentMethod(2)]
[FluentMethod(3)]
private void WhoLivesAtAddress()
{
}

[FluentMethod(3)]
[FluentMethod(4)]
private void WithHouseNumber(string houseNumber)
{
HouseNumber = houseNumber;
}

[FluentMethod(4)]
[FluentMethod(5)]
private void WithStreet(string street)
{
Street = street;
}

[FluentMethod(5)]
[FluentMethod(6)]
[FluentBreak]
private void InCity(string city)
{
City = city;
}

[FluentMethod(2)]
[FluentContinueWith(6)]
[FluentMethod(3)]
[FluentContinueWith(7)]
private void WhoIsADigitalNomad()
{
IsDigitalNomad = true;
}

[FluentMethod(6)]
[FluentMethod(7)]
private void LivingInCity(string city)
{
City = city;
Expand Down
11 changes: 10 additions & 1 deletion src/M31.FluentApi.Generator/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,13 @@ M31FA022 | M31.Usage | Error | Fluent lambda member without Fluent API

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
M31FA007 | M31.Usage | Error | Partial types are not supported
M31FA007 | M31.Usage | Error | Partial types are not supported


## Release 1.6.0

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
M31FA023 | M31.Usage | Error | Last builder step cannot be skipped
7 changes: 7 additions & 0 deletions src/M31.FluentApi.Generator/CodeBuilding/Interface.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// ReSharper disable ParameterHidesMember

namespace M31.FluentApi.Generator.CodeBuilding;

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

internal void AddBaseInterfaces(IEnumerable<string> baseInterfaces)
{
this.baseInterfaces.AddRange(baseInterfaces);
}

public CodeBuilder AppendCode(CodeBuilder codeBuilder)
{
return codeBuilder
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace M31.FluentApi.Generator.CodeGeneration.CodeBoardActors.BuilderMethodsGeneration;

internal class BaseInterface
{
public BaseInterface(string name, int step)
{
Name = name;
Step = step;
}

public string Name { get; }
public int Step { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using M31.FluentApi.Generator.CodeBuilding;
using M31.FluentApi.Generator.CodeGeneration.CodeBoardElements;

namespace M31.FluentApi.Generator.CodeGeneration.CodeBoardActors.BuilderMethodsGeneration;

internal class BuilderGenerator : ICodeBoardActor
{
public void Modify(CodeBoard codeBoard)
{
BuilderMethods builderMethods =
BuilderMethodsCreator.CreateBuilderMethods(codeBoard.Forks, codeBoard.CancellationToken);

foreach (BuilderStepMethod staticMethod in builderMethods.StaticMethods)
{
if (codeBoard.CancellationToken.IsCancellationRequested)
{
break;
}

Method method = CreateMethod(staticMethod, codeBoard);
codeBoard.BuilderClass.AddMethod(method);
}

List<Interface> interfaces = new List<Interface>(builderMethods.Interfaces.Count);
interfaces.Add(CreateInitialStepInterface(builderMethods, codeBoard));

foreach (BuilderInterface builderInterface in builderMethods.Interfaces)
{
if (codeBoard.CancellationToken.IsCancellationRequested)
{
break;
}

Interface @interface =
new Interface(codeBoard.Info.DefaultAccessModifier, builderInterface.InterfaceName);

foreach (InterfaceBuilderMethod interfaceMethod in builderInterface.Methods)
{
Method method = CreateMethod(interfaceMethod, codeBoard);
codeBoard.BuilderClass.AddMethod(method);
@interface.AddMethodSignature(method.MethodSignature.ToSignatureForInterface());
}

@interface.AddBaseInterfaces(builderInterface.BaseInterfaces);
interfaces.Add(@interface);
}

AddInterfacesToBuilderClass(
interfaces,
codeBoard.BuilderClass,
codeBoard.Info.BuilderClassNameWithTypeParameters);
AddInterfaceDefinitionsToBuilderClass(interfaces, codeBoard.BuilderClass);
}

private Method CreateMethod(BuilderStepMethod builderStepMethod, CodeBoard codeBoard)
{
ReservedVariableNames reservedVariableNames = codeBoard.ReservedVariableNames.NewLocalScope();
reservedVariableNames.ReserveLocalVariableNames(builderStepMethod.Parameters.Select(p => p.Name));

Method method = builderStepMethod.BuildMethodCode(
codeBoard.Info,
reservedVariableNames);

return method;
}

private Interface CreateInitialStepInterface(BuilderMethods builderMethods, CodeBoard codeBoard)
{
string? firstInterfaceName = builderMethods.Interfaces.FirstOrDefault()?.InterfaceName;

Interface initialStepInterface =
new Interface(codeBoard.Info.DefaultAccessModifier, codeBoard.Info.InitialStepInterfaceName);

if (firstInterfaceName != null)
{
initialStepInterface.AddBaseInterface(firstInterfaceName);
}

return initialStepInterface;
}

private void AddInterfacesToBuilderClass(List<Interface> interfaces, Class builderClass, string prefix)
{
foreach (Interface @interface in interfaces)
{
builderClass.AddInterface($"{prefix}.{@interface.Name}");
}
}

private void AddInterfaceDefinitionsToBuilderClass(List<Interface> interfaces, Class builderClass)
{
foreach (Interface @interface in interfaces)
{
builderClass.AddDefinition(@interface);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace M31.FluentApi.Generator.CodeGeneration.CodeBoardActors.BuilderMethodsGeneration;

internal class BuilderInterface
{
internal BuilderInterface(
string interfaceName,
IReadOnlyCollection<string> baseInterfaces,
IReadOnlyCollection<InterfaceBuilderMethod> methods)
{
InterfaceName = interfaceName;
BaseInterfaces = baseInterfaces;
Methods = methods;
}

public string InterfaceName { get; }
public IReadOnlyCollection<string> BaseInterfaces { get; }
public IReadOnlyCollection<InterfaceBuilderMethod> Methods { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using M31.FluentApi.Generator.Commons;

namespace M31.FluentApi.Generator.CodeGeneration.CodeBoardActors.BuilderMethodsGeneration;

internal class BuilderMethods
{
public BuilderMethods(
IReadOnlyCollection<BuilderStepMethod> staticMethods,
IReadOnlyCollection<BuilderInterface> interfaces)
{
Interfaces = interfaces;
StaticMethods = staticMethods;
}

internal IReadOnlyCollection<BuilderInterface> Interfaces { get; }
internal IReadOnlyCollection<BuilderStepMethod> StaticMethods { get; }

internal static IReadOnlyCollection<BuilderInterface> CreateInterfaces(
IReadOnlyCollection<InterfaceBuilderMethod> interfaceMethods,
CancellationToken cancellationToken)
{
List<BuilderInterface> interfaces = new List<BuilderInterface>();

IGrouping<string, InterfaceBuilderMethod>[] methodsGroupedByInterface =
interfaceMethods.GroupBy(m => m.InterfaceName).ToArray();

foreach (IGrouping<string, InterfaceBuilderMethod> group in methodsGroupedByInterface)
{
if (cancellationToken.IsCancellationRequested)
{
break;
}

string interfaceName = group.Key;

List<BaseInterface> baseInterfaces = new List<BaseInterface>();

foreach (InterfaceBuilderMethod method in group)
{
if (method.BaseInterface != null)
{
baseInterfaces.Add(method.BaseInterface);
}
}

string[] baseInterfaceNames = baseInterfaces
.DistinctBy(i => i.Name)
.OrderBy(i => i.Step)
.Select(i => i.Name).ToArray();

interfaces.Add(new BuilderInterface(interfaceName, baseInterfaceNames, group.ToArray()));
}

return interfaces;
}
}
Loading

0 comments on commit 1ac38b9

Please sign in to comment.