Skip to content

Commit

Permalink
feat: Lambda methods by default (#24)
Browse files Browse the repository at this point in the history
* feat: fluent lambda per default

* feat: lambda parameters for compounds

* fix: lambda method parameter names

* feat(LambdaByDefault): LambdaBuilderInfo for the FluentCollectionAttributeInfo

* fix(FluentApiInfoCreator): remove assumptions from TryGetLambdaBuilderInfoOfCollectionType

* feat: WithItem method with lambda parameter

* test(FluentLambdaCollectionClass): add expected result

* test: CanExecuteFluentLambdaCollectionClass

* feat: WithItems method with lambda parameters

* improve(Readme): feature list and acknowledgements

* chore: make FluentLambda obsolete

* fix(Readme)

* chore: replace FluentLambda with FluentMember in exmaples and tests

* chore(Storybook): NestedFluentApis example instead of FluentLambdaExample

* fix(ArrayCreator): remove semicolon in CreateCollectionFromEnumerable

* fix(ArrayCreator): add using only when needed

* test: FluentLambdaCollectionClass2

* test: FluentLambdaManyCollectionsClass and FluentLambdaManyPrivateCollectionsClass

* improve: property order in FluentLambdaManyPrivateCollectionsClass and FluentLambdaManyCollectionsClass

* fix: execution tests

* improve(CodeGenerationExecutionTests): blocks between different subtests

* test: add failing test TryBreakFluentApiClass3

* refactor(CollectionMethodCreator): cleanup

* fix: make TryBreakFluentApiClass3 test work

* chore: adjust storybook and readme

* chore: increase package version to 1.7.0

* fix: minor change
  • Loading branch information
m31coding authored Jun 16, 2024
1 parent 52c68e1 commit fe625e8
Show file tree
Hide file tree
Showing 65 changed files with 3,702 additions and 296 deletions.
49 changes: 34 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ The generated code follows the builder design pattern and allows you to construc

Accompanying blog post: [www.m31coding.com>blog>fluent-api](https://www.m31coding.com/blog/fluent-api.html)

## Features
- Builder code generation controlled by attributes
- Stepwise object construction
- Special handling for boolean, collection, and nullable members
- Nested fluent APIs via lambda methods
- Custom builder methods
- Optional (skippable) builder methods
- Forking and branching capabilities
- Support for returning arbitrary types
- Support for generics and partial classes

## Installing via NuGet

Install the latest version of the package `M31.FluentApi` via your IDE or use the package manager console:
Expand All @@ -25,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.6.0" PrivateAssets="all"/>
<PackageReference Include="M31.FluentApi" Version="1.7.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 @@ -106,7 +118,7 @@ You may have a look at the generated code for this example: [CreateStudent.g.cs]

The attributes `FluentApi` and `FluentMember` are all you need in order to get started.

The attributes `FluentPredicate`, `FluentCollection`, and `FluentLambda` can be used instead of the `FluentMember` attribute if the decorated member is a boolean, a collection, or has its own Fluent API, respectively.
The attributes `FluentPredicate` and `FluentCollection` can be used instead of the `FluentMember` attribute if the decorated member is a boolean or a collection, respectively.

`FluentDefault` and `FluentNullable` can be used in combination with these attributes to set a default value or null, respectively.

Expand Down Expand Up @@ -170,6 +182,17 @@ public string LastName { get; private set; }
```cs
...Named("Alice", "King")...
```
If the decorated member has its own Fluent API, an additional lambda method is generated, e.g.

```cs
[FluentMember(1)]
public Address Address { get; private set; }
```

```cs
...WithAddress(a => a.WithHouseNumber("108").WithStreet("5th Avenue").InCity("New York"))...
```


### FluentPredicate
Expand Down Expand Up @@ -217,25 +240,20 @@ public IReadOnlyCollection<string> Friends { get; private set; }
...WhoHasNoFriends()...
```


### FluentLambda
If the element type of the decorated member has its own Fluent API, additional lambda methods are generated, e.g.

```cs
FluentLambda(int builderStep, string method = "With{Name}")
[FluentCollection(1, "Address")]
public IReadOnlyCollection<Address> Addresses { get; private set; }
```

Can be used instead of the `FluentMember` attribute if the decorated member has its own Fluent API. Generates an additional builder method that accepts a lambda expression for creating the target field or property.

```cs
[FluentLambda(1)]
public Address Address { get; private set; }
...WithAddresses(
a => a.WithHouseNumber("108").WithStreet("5th Avenue").InCity("New York"),
a => a.WithHouseNumber("42").WithStreet("Maple Ave").InCity("Boston"))...
...WithAddress(a => a.WithHouseNumber("82").WithStreet("Friedrichstraße").InCity("Berlin"))...
```

```cs
...WithAddress(new Address("23", "Market Street", "San Francisco"))...
...WithAddress(a => a.WithHouseNumber("23").WithStreet("Market Street").InCity("San Francisco"))...
```


### FluentDefault

Expand Down Expand Up @@ -438,7 +456,7 @@ public void AddStudent(Func<CreateStudent.ICreateStudent, Student> createStudent
university.AddStudent(s => s.Named("Alice", "King").OfAge(22)...);
```

Note that if you want to set a single field or property on a Fluent API class, you can instead use the `FluentLambda` attribute.
Note that if you want to set a member of a Fluent API class, you can simply use `FluentMember` or `FluentCollection` instead of the pattern above.


## Problems with the IDE
Expand All @@ -451,6 +469,7 @@ In particular, if your IDE visually indicates that there are errors in your code
- Unload and reload the project
- Close and reopen the IDE
- Remove the .vs folder (Visual Studio) or the .idea folder (Rider)


## Support and Contribution

Expand Down
2 changes: 1 addition & 1 deletion src/ExampleProject/Order.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class Order
[FluentMember(1, "{Name}")]
public DateTime CreatedOn { get; private set; }

[FluentLambda(2, "ShippedTo")]
[FluentMember(2, "ShippedTo")]
public Address ShippingAddress { get; private set; }
}

Expand Down
2 changes: 1 addition & 1 deletion src/ExampleProject/OrderArbitrarySteps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class Order2
[FluentContinueWith(0)]
public DateTime? CreatedOn { get; private set; }

[FluentLambda(0, "ShippedTo")]
[FluentMember(0, "ShippedTo")]
[FluentContinueWith(0)]
public Address2? ShippingAddress { get; private set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ protected override string CreateCollectionFromArray(string genericTypeArgument,
return arrayParameter;
}

protected override string CreateCollectionFromEnumerable(string genericTypeArgument, string enumerableParameter)
{
RequiredUsings.Add("System.Linq");
return $"{enumerableParameter}.ToArray()";
}

protected override string CreateCollectionFromSingleItem(string genericTypeArgument, string itemParameter)
{
return $"new {genericTypeArgument}[1]{{ {itemParameter} }}";
Expand All @@ -27,6 +33,4 @@ protected override string CreateCollectionWithZeroItems(string genericTypeArgume
{
return $"new {genericTypeArgument}[0]";
}

internal override IReadOnlyCollection<string> RequiredUsings => Array.Empty<string>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ internal abstract class CollectionMethodCreator
private readonly FluentCollectionAttributeInfo collectionAttributeInfo;
private readonly string genericTypeArgument;
private readonly MemberSymbolInfo symbolInfo;
private readonly string questionMarkIfNullable;

internal CollectionMethodCreator(
FluentCollectionAttributeInfo collectionAttributeInfo,
Expand All @@ -19,20 +20,25 @@ internal CollectionMethodCreator(
this.collectionAttributeInfo = collectionAttributeInfo;
this.genericTypeArgument = genericTypeArgument;
this.symbolInfo = symbolInfo;
questionMarkIfNullable = symbolInfo.IsNullable ? "?" : string.Empty;
}

internal BuilderMethod? CreateWithItemsMethod(MethodCreator methodCreator)
{
return symbolInfo.TypeForCodeGeneration == $"{genericTypeArgument}[]" ||
symbolInfo.TypeForCodeGeneration == $"{genericTypeArgument}[]?"
? null
: methodCreator.CreateMethod(symbolInfo, collectionAttributeInfo.WithItems);
return !ShouldCreateWithItemsMethod() ? null :
methodCreator.CreateMethod(symbolInfo, collectionAttributeInfo.WithItems);
}

private bool ShouldCreateWithItemsMethod()
{
return symbolInfo.TypeForCodeGeneration != $"{genericTypeArgument}[]" &&
symbolInfo.TypeForCodeGeneration != $"{genericTypeArgument}[]?";
}

internal BuilderMethod CreateWithItemsParamsMethod(MethodCreator methodCreator)
{
Parameter parameter = new Parameter(
symbolInfo.IsNullable ? $"{genericTypeArgument}[]?" : $"{genericTypeArgument}[]",
$"{genericTypeArgument}[]{questionMarkIfNullable}",
symbolInfo.NameInCamelCase,
null,
null,
Expand All @@ -45,6 +51,44 @@ internal BuilderMethod CreateWithItemsParamsMethod(MethodCreator methodCreator)
p => CreateCollectionFromArray(genericTypeArgument, p));
}

internal BuilderMethod? CreateWithItemsLambdaParamsMethod(MethodCreator methodCreator)
{
if (collectionAttributeInfo.LambdaBuilderInfo == null)
{
return null;
}

ComputeValueCode lambdaCode = LambdaMethod.GetComputeValueCode(
genericTypeArgument,
collectionAttributeInfo.SingularNameInCamelCase,
symbolInfo.Name,
collectionAttributeInfo.LambdaBuilderInfo);

string parameterType = $"{lambdaCode.Parameter!.Type}[]{questionMarkIfNullable}";
string parameterName = LambdaMethod.GetFullParameterName(symbolInfo.NameInCamelCase);

Parameter parameter = new Parameter(
parameterType,
parameterName,
null,
null,
new ParameterAnnotations(ParameterKinds.Params));

ComputeValueCode computeValueCode = ComputeValueCode.Create(
lambdaCode.TargetMember,
parameter,
p => CreateCollectionFromEnumerable(
genericTypeArgument,
$"{p}{questionMarkIfNullable}.Select({lambdaCode.Parameter!.Name} => {lambdaCode.Code})"));

RequiredUsings.Add("System");
RequiredUsings.Add("System.Linq");

return methodCreator.BuilderMethodFactory.CreateBuilderMethod(
collectionAttributeInfo.WithItems,
computeValueCode);
}

internal BuilderMethod CreateWithItemMethod(MethodCreator methodCreator)
{
Parameter parameter = new Parameter(genericTypeArgument, collectionAttributeInfo.SingularNameInCamelCase);
Expand All @@ -55,6 +99,30 @@ internal BuilderMethod CreateWithItemMethod(MethodCreator methodCreator)
p => CreateCollectionFromSingleItem(genericTypeArgument, p));
}

internal BuilderMethod? CreateWithItemLambdaMethod(MethodCreator methodCreator)
{
if (collectionAttributeInfo.LambdaBuilderInfo == null)
{
return null;
}

ComputeValueCode lambdaCode = LambdaMethod.GetComputeValueCode(
genericTypeArgument,
collectionAttributeInfo.SingularNameInCamelCase,
symbolInfo.Name,
collectionAttributeInfo.LambdaBuilderInfo);

ComputeValueCode computeValueCode = ComputeValueCode.Create(
lambdaCode.TargetMember,
lambdaCode.Parameter!,
_ => CreateCollectionFromSingleItem(genericTypeArgument, lambdaCode.Code));

RequiredUsings.Add("System");

return methodCreator.BuilderMethodFactory.CreateBuilderMethod(collectionAttributeInfo.WithItem,
computeValueCode);
}

internal BuilderMethod CreateWithZeroItemsMethod(MethodCreator methodCreator)
{
string collectionWithZeroItemsCode = CreateCollectionWithZeroItems(genericTypeArgument);
Expand All @@ -65,7 +133,8 @@ internal BuilderMethod CreateWithZeroItemsMethod(MethodCreator methodCreator)
}

protected abstract string CreateCollectionFromArray(string genericTypeArgument, string arrayParameter);
protected abstract string CreateCollectionFromEnumerable(string genericTypeArgument, string enumerableParameter);
protected abstract string CreateCollectionFromSingleItem(string genericTypeArgument, string itemParameter);
protected abstract string CreateCollectionWithZeroItems(string genericTypeArgument);
internal abstract IReadOnlyCollection<string> RequiredUsings { get; }
internal HashSet<string> RequiredUsings { get; } = new HashSet<string>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,14 @@ public BuilderMethods CreateBuilderMethods(MethodCreator methodCreator)
{
collectionMethodCreator.CreateWithItemsMethod(methodCreator),
collectionMethodCreator.CreateWithItemsParamsMethod(methodCreator),
collectionMethodCreator.CreateWithItemsLambdaParamsMethod(methodCreator),
collectionMethodCreator.CreateWithItemMethod(methodCreator),
collectionMethodCreator.CreateWithItemLambdaMethod(methodCreator),
collectionMethodCreator.CreateWithZeroItemsMethod(methodCreator),
};

return new BuilderMethods(builderMethods.OfType<BuilderMethod>().ToList(),
new HashSet<string>(collectionMethodCreator.RequiredUsings));
return new BuilderMethods(
builderMethods.OfType<BuilderMethod>().ToList(),
collectionMethodCreator.RequiredUsings);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@ internal HashSetCreator(
MemberSymbolInfo symbolInfo)
: base(collectionAttributeInfo, genericTypeArgument, symbolInfo)
{
RequiredUsings.Add("System.Collections.Generic");
}

protected override string CreateCollectionFromArray(string genericTypeArgument, string arrayParameter)
{
return $"new HashSet<{genericTypeArgument}>({arrayParameter})";
return CreateCollectionFromEnumerable(genericTypeArgument, arrayParameter);
}

protected override string CreateCollectionFromEnumerable(string genericTypeArgument, string enumerableParameter)
{
return $"new HashSet<{genericTypeArgument}>({enumerableParameter})";
}

protected override string CreateCollectionFromSingleItem(string genericTypeArgument, string itemParameter)
Expand All @@ -27,6 +33,4 @@ protected override string CreateCollectionWithZeroItems(string genericTypeArgume
{
return $"new HashSet<{genericTypeArgument}>(0)";
}

internal override IReadOnlyCollection<string> RequiredUsings => new string[] { "System.Collections.Generic" };
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@ internal ListCreator(
MemberSymbolInfo symbolInfo)
: base(collectionAttributeInfo, genericTypeArgument, symbolInfo)
{
RequiredUsings.Add("System.Collections.Generic");
}

protected override string CreateCollectionFromArray(string genericTypeArgument, string arrayParameter)
{
return $"new List<{genericTypeArgument}>({arrayParameter})";
return CreateCollectionFromEnumerable(genericTypeArgument, arrayParameter);
}

protected override string CreateCollectionFromEnumerable(string genericTypeArgument, string enumerableParameter)
{
return $"new List<{genericTypeArgument}>({enumerableParameter})";
}

protected override string CreateCollectionFromSingleItem(string genericTypeArgument, string itemParameter)
Expand All @@ -27,6 +33,4 @@ protected override string CreateCollectionWithZeroItems(string genericTypeArgume
{
return $"new List<{genericTypeArgument}>(0)";
}

internal override IReadOnlyCollection<string> RequiredUsings => new string[] { "System.Collections.Generic" };
}
Loading

0 comments on commit fe625e8

Please sign in to comment.