Skip to content

Commit fe625e8

Browse files
authored
feat: Lambda methods by default (#24)
* 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
1 parent 52c68e1 commit fe625e8

File tree

65 files changed

+3702
-296
lines changed

Some content is hidden

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

65 files changed

+3702
-296
lines changed

README.md

+34-15
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@ The generated code follows the builder design pattern and allows you to construc
1414

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

17+
## Features
18+
19+
- Builder code generation controlled by attributes
20+
- Stepwise object construction
21+
- Special handling for boolean, collection, and nullable members
22+
- Nested fluent APIs via lambda methods
23+
- Custom builder methods
24+
- Optional (skippable) builder methods
25+
- Forking and branching capabilities
26+
- Support for returning arbitrary types
27+
- Support for generics and partial classes
28+
1729
## Installing via NuGet
1830

1931
Install the latest version of the package `M31.FluentApi` via your IDE or use the package manager console:
@@ -25,7 +37,7 @@ PM> Install-Package M31.FluentApi
2537
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:
2638

2739
```xml
28-
<PackageReference Include="M31.FluentApi" Version="1.6.0" PrivateAssets="all"/>
40+
<PackageReference Include="M31.FluentApi" Version="1.7.0" PrivateAssets="all"/>
2941
```
3042

3143
If you would like to examine the generated code, you may emit it by adding the following lines to your `csproj` file:
@@ -106,7 +118,7 @@ You may have a look at the generated code for this example: [CreateStudent.g.cs]
106118

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

109-
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.
121+
The attributes `FluentPredicate` and `FluentCollection` can be used instead of the `FluentMember` attribute if the decorated member is a boolean or a collection, respectively.
110122

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

@@ -170,6 +182,17 @@ public string LastName { get; private set; }
170182
```cs
171183
...Named("Alice", "King")...
172184
```
185+
186+
If the decorated member has its own Fluent API, an additional lambda method is generated, e.g.
187+
188+
```cs
189+
[FluentMember(1)]
190+
public Address Address { get; private set; }
191+
```
192+
193+
```cs
194+
...WithAddress(a => a.WithHouseNumber("108").WithStreet("5th Avenue").InCity("New York"))...
195+
```
173196

174197

175198
### FluentPredicate
@@ -217,25 +240,20 @@ public IReadOnlyCollection<string> Friends { get; private set; }
217240
...WhoHasNoFriends()...
218241
```
219242

220-
221-
### FluentLambda
243+
If the element type of the decorated member has its own Fluent API, additional lambda methods are generated, e.g.
222244

223245
```cs
224-
FluentLambda(int builderStep, string method = "With{Name}")
246+
[FluentCollection(1, "Address")]
247+
public IReadOnlyCollection<Address> Addresses { get; private set; }
225248
```
226249

227-
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.
228-
229250
```cs
230-
[FluentLambda(1)]
231-
public Address Address { get; private set; }
251+
...WithAddresses(
252+
a => a.WithHouseNumber("108").WithStreet("5th Avenue").InCity("New York"),
253+
a => a.WithHouseNumber("42").WithStreet("Maple Ave").InCity("Boston"))...
254+
...WithAddress(a => a.WithHouseNumber("82").WithStreet("Friedrichstraße").InCity("Berlin"))...
232255
```
233256

234-
```cs
235-
...WithAddress(new Address("23", "Market Street", "San Francisco"))...
236-
...WithAddress(a => a.WithHouseNumber("23").WithStreet("Market Street").InCity("San Francisco"))...
237-
```
238-
239257

240258
### FluentDefault
241259

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

441-
Note that if you want to set a single field or property on a Fluent API class, you can instead use the `FluentLambda` attribute.
459+
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.
442460

443461

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

455474
## Support and Contribution
456475

src/ExampleProject/Order.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class Order
1818
[FluentMember(1, "{Name}")]
1919
public DateTime CreatedOn { get; private set; }
2020

21-
[FluentLambda(2, "ShippedTo")]
21+
[FluentMember(2, "ShippedTo")]
2222
public Address ShippingAddress { get; private set; }
2323
}
2424

src/ExampleProject/OrderArbitrarySteps.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public class Order2
2020
[FluentContinueWith(0)]
2121
public DateTime? CreatedOn { get; private set; }
2222

23-
[FluentLambda(0, "ShippedTo")]
23+
[FluentMember(0, "ShippedTo")]
2424
[FluentContinueWith(0)]
2525
public Address2? ShippingAddress { get; private set; }
2626

src/M31.FluentApi.Generator/CodeGeneration/CodeBoardActors/MethodCreation/Collections/ArrayCreator.cs

+6-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ protected override string CreateCollectionFromArray(string genericTypeArgument,
1818
return arrayParameter;
1919
}
2020

21+
protected override string CreateCollectionFromEnumerable(string genericTypeArgument, string enumerableParameter)
22+
{
23+
RequiredUsings.Add("System.Linq");
24+
return $"{enumerableParameter}.ToArray()";
25+
}
26+
2127
protected override string CreateCollectionFromSingleItem(string genericTypeArgument, string itemParameter)
2228
{
2329
return $"new {genericTypeArgument}[1]{{ {itemParameter} }}";
@@ -27,6 +33,4 @@ protected override string CreateCollectionWithZeroItems(string genericTypeArgume
2733
{
2834
return $"new {genericTypeArgument}[0]";
2935
}
30-
31-
internal override IReadOnlyCollection<string> RequiredUsings => Array.Empty<string>();
3236
}

src/M31.FluentApi.Generator/CodeGeneration/CodeBoardActors/MethodCreation/Collections/CollectionMethodCreator.cs

+75-6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ internal abstract class CollectionMethodCreator
1010
private readonly FluentCollectionAttributeInfo collectionAttributeInfo;
1111
private readonly string genericTypeArgument;
1212
private readonly MemberSymbolInfo symbolInfo;
13+
private readonly string questionMarkIfNullable;
1314

1415
internal CollectionMethodCreator(
1516
FluentCollectionAttributeInfo collectionAttributeInfo,
@@ -19,20 +20,25 @@ internal CollectionMethodCreator(
1920
this.collectionAttributeInfo = collectionAttributeInfo;
2021
this.genericTypeArgument = genericTypeArgument;
2122
this.symbolInfo = symbolInfo;
23+
questionMarkIfNullable = symbolInfo.IsNullable ? "?" : string.Empty;
2224
}
2325

2426
internal BuilderMethod? CreateWithItemsMethod(MethodCreator methodCreator)
2527
{
26-
return symbolInfo.TypeForCodeGeneration == $"{genericTypeArgument}[]" ||
27-
symbolInfo.TypeForCodeGeneration == $"{genericTypeArgument}[]?"
28-
? null
29-
: methodCreator.CreateMethod(symbolInfo, collectionAttributeInfo.WithItems);
28+
return !ShouldCreateWithItemsMethod() ? null :
29+
methodCreator.CreateMethod(symbolInfo, collectionAttributeInfo.WithItems);
30+
}
31+
32+
private bool ShouldCreateWithItemsMethod()
33+
{
34+
return symbolInfo.TypeForCodeGeneration != $"{genericTypeArgument}[]" &&
35+
symbolInfo.TypeForCodeGeneration != $"{genericTypeArgument}[]?";
3036
}
3137

3238
internal BuilderMethod CreateWithItemsParamsMethod(MethodCreator methodCreator)
3339
{
3440
Parameter parameter = new Parameter(
35-
symbolInfo.IsNullable ? $"{genericTypeArgument}[]?" : $"{genericTypeArgument}[]",
41+
$"{genericTypeArgument}[]{questionMarkIfNullable}",
3642
symbolInfo.NameInCamelCase,
3743
null,
3844
null,
@@ -45,6 +51,44 @@ internal BuilderMethod CreateWithItemsParamsMethod(MethodCreator methodCreator)
4551
p => CreateCollectionFromArray(genericTypeArgument, p));
4652
}
4753

54+
internal BuilderMethod? CreateWithItemsLambdaParamsMethod(MethodCreator methodCreator)
55+
{
56+
if (collectionAttributeInfo.LambdaBuilderInfo == null)
57+
{
58+
return null;
59+
}
60+
61+
ComputeValueCode lambdaCode = LambdaMethod.GetComputeValueCode(
62+
genericTypeArgument,
63+
collectionAttributeInfo.SingularNameInCamelCase,
64+
symbolInfo.Name,
65+
collectionAttributeInfo.LambdaBuilderInfo);
66+
67+
string parameterType = $"{lambdaCode.Parameter!.Type}[]{questionMarkIfNullable}";
68+
string parameterName = LambdaMethod.GetFullParameterName(symbolInfo.NameInCamelCase);
69+
70+
Parameter parameter = new Parameter(
71+
parameterType,
72+
parameterName,
73+
null,
74+
null,
75+
new ParameterAnnotations(ParameterKinds.Params));
76+
77+
ComputeValueCode computeValueCode = ComputeValueCode.Create(
78+
lambdaCode.TargetMember,
79+
parameter,
80+
p => CreateCollectionFromEnumerable(
81+
genericTypeArgument,
82+
$"{p}{questionMarkIfNullable}.Select({lambdaCode.Parameter!.Name} => {lambdaCode.Code})"));
83+
84+
RequiredUsings.Add("System");
85+
RequiredUsings.Add("System.Linq");
86+
87+
return methodCreator.BuilderMethodFactory.CreateBuilderMethod(
88+
collectionAttributeInfo.WithItems,
89+
computeValueCode);
90+
}
91+
4892
internal BuilderMethod CreateWithItemMethod(MethodCreator methodCreator)
4993
{
5094
Parameter parameter = new Parameter(genericTypeArgument, collectionAttributeInfo.SingularNameInCamelCase);
@@ -55,6 +99,30 @@ internal BuilderMethod CreateWithItemMethod(MethodCreator methodCreator)
5599
p => CreateCollectionFromSingleItem(genericTypeArgument, p));
56100
}
57101

102+
internal BuilderMethod? CreateWithItemLambdaMethod(MethodCreator methodCreator)
103+
{
104+
if (collectionAttributeInfo.LambdaBuilderInfo == null)
105+
{
106+
return null;
107+
}
108+
109+
ComputeValueCode lambdaCode = LambdaMethod.GetComputeValueCode(
110+
genericTypeArgument,
111+
collectionAttributeInfo.SingularNameInCamelCase,
112+
symbolInfo.Name,
113+
collectionAttributeInfo.LambdaBuilderInfo);
114+
115+
ComputeValueCode computeValueCode = ComputeValueCode.Create(
116+
lambdaCode.TargetMember,
117+
lambdaCode.Parameter!,
118+
_ => CreateCollectionFromSingleItem(genericTypeArgument, lambdaCode.Code));
119+
120+
RequiredUsings.Add("System");
121+
122+
return methodCreator.BuilderMethodFactory.CreateBuilderMethod(collectionAttributeInfo.WithItem,
123+
computeValueCode);
124+
}
125+
58126
internal BuilderMethod CreateWithZeroItemsMethod(MethodCreator methodCreator)
59127
{
60128
string collectionWithZeroItemsCode = CreateCollectionWithZeroItems(genericTypeArgument);
@@ -65,7 +133,8 @@ internal BuilderMethod CreateWithZeroItemsMethod(MethodCreator methodCreator)
65133
}
66134

67135
protected abstract string CreateCollectionFromArray(string genericTypeArgument, string arrayParameter);
136+
protected abstract string CreateCollectionFromEnumerable(string genericTypeArgument, string enumerableParameter);
68137
protected abstract string CreateCollectionFromSingleItem(string genericTypeArgument, string itemParameter);
69138
protected abstract string CreateCollectionWithZeroItems(string genericTypeArgument);
70-
internal abstract IReadOnlyCollection<string> RequiredUsings { get; }
139+
internal HashSet<string> RequiredUsings { get; } = new HashSet<string>();
71140
}

src/M31.FluentApi.Generator/CodeGeneration/CodeBoardActors/MethodCreation/Collections/CollectionMethods.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,14 @@ public BuilderMethods CreateBuilderMethods(MethodCreator methodCreator)
4343
{
4444
collectionMethodCreator.CreateWithItemsMethod(methodCreator),
4545
collectionMethodCreator.CreateWithItemsParamsMethod(methodCreator),
46+
collectionMethodCreator.CreateWithItemsLambdaParamsMethod(methodCreator),
4647
collectionMethodCreator.CreateWithItemMethod(methodCreator),
48+
collectionMethodCreator.CreateWithItemLambdaMethod(methodCreator),
4749
collectionMethodCreator.CreateWithZeroItemsMethod(methodCreator),
4850
};
4951

50-
return new BuilderMethods(builderMethods.OfType<BuilderMethod>().ToList(),
51-
new HashSet<string>(collectionMethodCreator.RequiredUsings));
52+
return new BuilderMethods(
53+
builderMethods.OfType<BuilderMethod>().ToList(),
54+
collectionMethodCreator.RequiredUsings);
5255
}
5356
}

src/M31.FluentApi.Generator/CodeGeneration/CodeBoardActors/MethodCreation/Collections/HashSetCreator.cs

+7-3
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,17 @@ internal HashSetCreator(
1111
MemberSymbolInfo symbolInfo)
1212
: base(collectionAttributeInfo, genericTypeArgument, symbolInfo)
1313
{
14+
RequiredUsings.Add("System.Collections.Generic");
1415
}
1516

1617
protected override string CreateCollectionFromArray(string genericTypeArgument, string arrayParameter)
1718
{
18-
return $"new HashSet<{genericTypeArgument}>({arrayParameter})";
19+
return CreateCollectionFromEnumerable(genericTypeArgument, arrayParameter);
20+
}
21+
22+
protected override string CreateCollectionFromEnumerable(string genericTypeArgument, string enumerableParameter)
23+
{
24+
return $"new HashSet<{genericTypeArgument}>({enumerableParameter})";
1925
}
2026

2127
protected override string CreateCollectionFromSingleItem(string genericTypeArgument, string itemParameter)
@@ -27,6 +33,4 @@ protected override string CreateCollectionWithZeroItems(string genericTypeArgume
2733
{
2834
return $"new HashSet<{genericTypeArgument}>(0)";
2935
}
30-
31-
internal override IReadOnlyCollection<string> RequiredUsings => new string[] { "System.Collections.Generic" };
3236
}

src/M31.FluentApi.Generator/CodeGeneration/CodeBoardActors/MethodCreation/Collections/ListCreator.cs

+7-3
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,17 @@ internal ListCreator(
1111
MemberSymbolInfo symbolInfo)
1212
: base(collectionAttributeInfo, genericTypeArgument, symbolInfo)
1313
{
14+
RequiredUsings.Add("System.Collections.Generic");
1415
}
1516

1617
protected override string CreateCollectionFromArray(string genericTypeArgument, string arrayParameter)
1718
{
18-
return $"new List<{genericTypeArgument}>({arrayParameter})";
19+
return CreateCollectionFromEnumerable(genericTypeArgument, arrayParameter);
20+
}
21+
22+
protected override string CreateCollectionFromEnumerable(string genericTypeArgument, string enumerableParameter)
23+
{
24+
return $"new List<{genericTypeArgument}>({enumerableParameter})";
1925
}
2026

2127
protected override string CreateCollectionFromSingleItem(string genericTypeArgument, string itemParameter)
@@ -27,6 +33,4 @@ protected override string CreateCollectionWithZeroItems(string genericTypeArgume
2733
{
2834
return $"new List<{genericTypeArgument}>(0)";
2935
}
30-
31-
internal override IReadOnlyCollection<string> RequiredUsings => new string[] { "System.Collections.Generic" };
3236
}

0 commit comments

Comments
 (0)