Skip to content

Commit

Permalink
### Changed
Browse files Browse the repository at this point in the history
- The Builder<T> delegate is now public, enabling generated extensions to be called from outside the scope of the current assembly.
  • Loading branch information
MooVC committed Jul 15, 2024
1 parent 00fe4c9 commit c7103b2
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 20 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.0.0-alpha0007] - 2024-07-15

### Added

- Analyzer FLTFY01 that issues a Warning whenever the Fluentify attribute is attached to a class that does not have an accessible default constructor.
Expand All @@ -17,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- README.md to Nuget package.
- Security Policy.

### Changed

- The Builder<T> delegate is now public, enabling generated extensions to be called from outside the scope of the current assembly.

## [1.0.0-alpha0006] - 2024-06-20

### Changed
Expand Down
57 changes: 38 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@



# Fluentify [![NuGet](https://img.shields.io/nuget/v/Fluentify?logo=nuget)](https://www.nuget.org/packages/Fluentify/) [![GitHub](https://img.shields.io/github/license/MooVC/Fluentify)](LICENSE.md)

Fluentify is a .NET Roslyn Source Generator designed to automate the creation of Fluent APIs. This tool enables engineers to rapidly develop rich, expressive, and maintainable APIs with ease. Utilizing Fluentify allows for cleaner code, easier maintenance, and more expressive interactions within your C# .NET applications.
Expand Down Expand Up @@ -174,12 +175,12 @@ Fluentify includes several analyzers to assist engineers with its usage. These a

Rule ID | Category | Severity | Notes
---------------------------------------------------------------------------------------|----------|----------|-------
[FLTFY01](https://github.com/MooVC/Fluentify/blob/release/1.0.0/docs/rules/FLTFY01.md) | Design | Warning | Class must have an accessible parameterless constructor to use Fluentify
[FLTFY02](https://github.com/MooVC/Fluentify/blob/release/1.0.0/docs/rules/FLTFY02.md) | Usage | Info | Descriptor is disregarded from consideration by Fluentify
[FLTFY03](https://github.com/MooVC/Fluentify/blob/release/1.0.0/docs/rules/FLTFY03.md) | Usage | Info | Type does not utilize Fluentify
[FLTFY04](https://github.com/MooVC/Fluentify/blob/release/1.0.0/docs/rules/FLTFY04.md) | Naming | Warning | Descriptor must adhere to the naming conventions for Methods
[FLTFY05](https://github.com/MooVC/Fluentify/blob/release/1.0.0/docs/rules/FLTFY05.md) | Usage | Info | Type does not utilize Fluentify
[FLTFY06](https://github.com/MooVC/Fluentify/blob/release/1.0.0/docs/rules/FLTFY06.md) | Usage | Info | Property is already disregarded from consideration by Fluentify
[FLTFY01](docs/rules/FLTFY01.md) | Design | Warning | Class must have an accessible parameterless constructor to use Fluentify
[FLTFY02](docs/rules/FLTFY02.md) | Usage | Info | Descriptor is disregarded from consideration by Fluentify
[FLTFY03](docs/rules/FLTFY03.md) | Usage | Info | Type does not utilize Fluentify
[FLTFY04](docs/rules/FLTFY04.md) | Naming | Warning | Descriptor must adhere to the naming conventions for Methods
[FLTFY05](docs/rules/FLTFY05.md) | Usage | Info | Type does not utilize Fluentify
[FLTFY06](docs/rules/FLTFY06.md) | Usage | Info | Property is already disregarded from consideration by Fluentify

## How to Apply in Practice

Expand All @@ -189,35 +190,53 @@ If we take the example of the `MovieBuilder` and apply Fluentify, it may look li

```csharp
[Fluentify]
public partial record Actor(string FirstName, string Surname, [Descriptor("BornIn")] int Birthday);
public partial record Actor(
[Descriptor("BornIn")] int Birthday,
string FirstName,
string Surname);

[Fluentify]
public partial record Movie(Actor[] Actors, string Title, DateOnly ReleasedOn, Genre Genre);
public partial record Movie(
Actor[] Actors,
[Descriptor("OfGenre")] Genre Genre,
[Descriptor("ReleasedOn")] DateOnly ReleasedOn,
string Title);
```
In this example, we did not need to create the various `With` methods, nor did we need to explicitly create the `Build` method, significantly reducing the effort required by the engineer to support the highly expressive Fluent approach to building the `Movie` instance, demonstrated as follows:

```csharp
var actual = new Movie()
.OfGenre(Genre.SciFi)
.WithTitle("Star Trek: First Contact")
.ReleasedOn(new DateOnly(1996, 12, 13))
.WithActors(actor => actor
.WithFirstName("Patrick")
.WithSurname("Stewart")
.BornIn(1940));
```
In this example, we did not need to create the various `With` methods, nor did we need to explicitly create the `Build` method, significantly reducing the effort required by the engineer to support the highly expressive Fluent approach to building the `Movie` instance.

Naturally, using Fluentify does not preclude engineers from adding additional methods to support building, and this will often be required if you choose to adopt the guided builder approach, or if specific validations or conversions are required before the final instance can be build. For example:
```csharp
public class MyService
{
public MyService(string connectionString, TimeSpan timeout)
{
ConnectionString = Guard.Against.NullOrWhitespace(connectionString, message: "A connection string must be provided.");

Timeout = Guard.Against.OutOfRange(
timeout,
TimeSpan.FromSeconds(1),
TimeSpan.MaxValue,
message: "Timeout must be greater than 1 second." );
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
ArgumentOutOfRangeException.ThrowIfLessThan(timeout.TotalSeconds, 1);

ConnectionString = connectionString;
Timeout = timeout;
}

public string ConnectionString { get; }

public TimeSpan Timeout { get; }
}

[Fluentify]
public partial record MyServiceBuilder([Descriptor("ConnectsTo")]string ConnectionString, [Descriptor("Waits")] int Timeout)
public partial record MyServiceBuilder(
[Descriptor("ConnectsTo")]string ConnectionString,
[Descriptor("Waits")] int Timeout)
{
public static MyServiceBuilder Empty => new();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace Fluentify.Console.Record.Example.Simple.MovieTests;

public sealed class WhenMovieIsBuilt
{
[Fact]
public void GivenAMovieThenTheInstanceIsCreated()
{
// Arrange
var original = new Movie();

var expected = new Movie
{
Actors =
[
new Actor
{
Birthday = 1940,
FirstName = "Patrick",
Surname = "Stewart",
},
],
Genre = Genre.SciFi,
ReleasedOn = new DateOnly(1996, 12, 13),
Title = "Star Trek: First Contact",
};

// Act
Movie actual = original
.OfGenre(Genre.SciFi)
.WithTitle("Star Trek: First Contact")
.ReleasedOn(new DateOnly(1996, 12, 13))
.WithActors(actor => actor
.WithFirstName("Patrick")
.WithSurname("Stewart")
.BornIn(1940));

// Assert
_ = actual.Should().NotBe(original);
_ = actual.Should().BeEquivalentTo(expected);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Fluentify.Console.Record.Example.Simple.MyServiceBuilderTests;

public sealed class WhenMyServiceIsBuilt
{
[Fact]
public void GivenRequiredValuesThenTheInstanceIsBuilt()
{
// Arrange
const string connectionString = "The String";
const int timeout = 30;

// Act
MyService service = MyServiceBuilder
.Empty
.ConnectsTo(connectionString)
.Waits(timeout)
.Build();

// Assert
_ = service.Should().NotBeNull();
_ = service.ConnectionString.Should().Be(connectionString);
_ = service.Timeout.TotalSeconds.Should().Be(timeout);
}
}
7 changes: 7 additions & 0 deletions src/Fluentify.Console/Record/Example/Simple/Actor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Fluentify.Console.Record.Example;

[Fluentify]
public partial record Actor(
[Descriptor("BornIn")] int Birthday,
string FirstName,
string Surname);
7 changes: 7 additions & 0 deletions src/Fluentify.Console/Record/Example/Simple/Genre.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Fluentify.Console.Record.Example.Simple;

public enum Genre
{
Horror,
SciFi,
}
10 changes: 10 additions & 0 deletions src/Fluentify.Console/Record/Example/Simple/Movie.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Fluentify.Console.Record.Example;

using Fluentify.Console.Record.Example.Simple;

[Fluentify]
public partial record Movie(
Actor[] Actors,
[Descriptor("OfGenre")] Genre Genre,
[Descriptor("ReleasedOn")] DateOnly ReleasedOn,
string Title);
17 changes: 17 additions & 0 deletions src/Fluentify.Console/Record/Example/Simple/MyService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Fluentify.Console.Record.Example.Simple;

public class MyService
{
public MyService(string connectionString, TimeSpan timeout)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
ArgumentOutOfRangeException.ThrowIfLessThan(timeout.TotalSeconds, 1);

ConnectionString = connectionString;
Timeout = timeout;
}

public string ConnectionString { get; }

public TimeSpan Timeout { get; }
}
14 changes: 14 additions & 0 deletions src/Fluentify.Console/Record/Example/Simple/MyServiceBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Fluentify.Console.Record.Example.Simple;

[Fluentify]
public partial record MyServiceBuilder(
[Descriptor("ConnectsTo")] string ConnectionString,
[Descriptor("Waits")] int Timeout)
{
public static MyServiceBuilder Empty => new();

public MyService Build()
{
return new MyService(ConnectionString, TimeSpan.FromSeconds(Timeout));
}
}
2 changes: 1 addition & 1 deletion src/Fluentify/BuilderDelegateGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public sealed class BuilderDelegateGenerator
internal const string Source = $$"""
namespace Fluentify
{
internal delegate T Builder<T>(T subject)
public delegate T Builder<T>(T subject)
where T : new();
}
""";
Expand Down

0 comments on commit c7103b2

Please sign in to comment.