From c7103b277336d9948fde12f1086895ae6d5e0b03 Mon Sep 17 00:00:00 2001 From: Paul Martins Date: Mon, 15 Jul 2024 23:21:35 +0100 Subject: [PATCH] ### Changed - The Builder delegate is now public, enabling generated extensions to be called from outside the scope of the current assembly. --- CHANGELOG.md | 6 ++ README.md | 57 ++++++++++++------- .../Simple/MovieTests/WhenMovieIsBuilt.cs | 41 +++++++++++++ .../WhenMyServiceIsBuilt.cs | 24 ++++++++ .../Record/Example/Simple/Actor.cs | 7 +++ .../Record/Example/Simple/Genre.cs | 7 +++ .../Record/Example/Simple/Movie.cs | 10 ++++ .../Record/Example/Simple/MyService.cs | 17 ++++++ .../Record/Example/Simple/MyServiceBuilder.cs | 14 +++++ src/Fluentify/BuilderDelegateGenerator.cs | 2 +- 10 files changed, 165 insertions(+), 20 deletions(-) create mode 100644 src/Fluentify.Console.Tests/Record/Example/Simple/MovieTests/WhenMovieIsBuilt.cs create mode 100644 src/Fluentify.Console.Tests/Record/Example/Simple/MyServiceBuilderTests/WhenMyServiceIsBuilt.cs create mode 100644 src/Fluentify.Console/Record/Example/Simple/Actor.cs create mode 100644 src/Fluentify.Console/Record/Example/Simple/Genre.cs create mode 100644 src/Fluentify.Console/Record/Example/Simple/Movie.cs create mode 100644 src/Fluentify.Console/Record/Example/Simple/MyService.cs create mode 100644 src/Fluentify.Console/Record/Example/Simple/MyServiceBuilder.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 692216e..ddc0922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. @@ -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 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 diff --git a/README.md b/README.md index 17bdb8c..e0c703a 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 @@ -189,12 +190,30 @@ 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 @@ -202,22 +221,22 @@ 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(); diff --git a/src/Fluentify.Console.Tests/Record/Example/Simple/MovieTests/WhenMovieIsBuilt.cs b/src/Fluentify.Console.Tests/Record/Example/Simple/MovieTests/WhenMovieIsBuilt.cs new file mode 100644 index 0000000..c3757e9 --- /dev/null +++ b/src/Fluentify.Console.Tests/Record/Example/Simple/MovieTests/WhenMovieIsBuilt.cs @@ -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); + } +} \ No newline at end of file diff --git a/src/Fluentify.Console.Tests/Record/Example/Simple/MyServiceBuilderTests/WhenMyServiceIsBuilt.cs b/src/Fluentify.Console.Tests/Record/Example/Simple/MyServiceBuilderTests/WhenMyServiceIsBuilt.cs new file mode 100644 index 0000000..ab8fd9b --- /dev/null +++ b/src/Fluentify.Console.Tests/Record/Example/Simple/MyServiceBuilderTests/WhenMyServiceIsBuilt.cs @@ -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); + } +} \ No newline at end of file diff --git a/src/Fluentify.Console/Record/Example/Simple/Actor.cs b/src/Fluentify.Console/Record/Example/Simple/Actor.cs new file mode 100644 index 0000000..911f095 --- /dev/null +++ b/src/Fluentify.Console/Record/Example/Simple/Actor.cs @@ -0,0 +1,7 @@ +namespace Fluentify.Console.Record.Example; + +[Fluentify] +public partial record Actor( + [Descriptor("BornIn")] int Birthday, + string FirstName, + string Surname); \ No newline at end of file diff --git a/src/Fluentify.Console/Record/Example/Simple/Genre.cs b/src/Fluentify.Console/Record/Example/Simple/Genre.cs new file mode 100644 index 0000000..0c4ca2c --- /dev/null +++ b/src/Fluentify.Console/Record/Example/Simple/Genre.cs @@ -0,0 +1,7 @@ +namespace Fluentify.Console.Record.Example.Simple; + +public enum Genre +{ + Horror, + SciFi, +} \ No newline at end of file diff --git a/src/Fluentify.Console/Record/Example/Simple/Movie.cs b/src/Fluentify.Console/Record/Example/Simple/Movie.cs new file mode 100644 index 0000000..9c789df --- /dev/null +++ b/src/Fluentify.Console/Record/Example/Simple/Movie.cs @@ -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); \ No newline at end of file diff --git a/src/Fluentify.Console/Record/Example/Simple/MyService.cs b/src/Fluentify.Console/Record/Example/Simple/MyService.cs new file mode 100644 index 0000000..137eb12 --- /dev/null +++ b/src/Fluentify.Console/Record/Example/Simple/MyService.cs @@ -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; } +} \ No newline at end of file diff --git a/src/Fluentify.Console/Record/Example/Simple/MyServiceBuilder.cs b/src/Fluentify.Console/Record/Example/Simple/MyServiceBuilder.cs new file mode 100644 index 0000000..9a25d2a --- /dev/null +++ b/src/Fluentify.Console/Record/Example/Simple/MyServiceBuilder.cs @@ -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)); + } +} \ No newline at end of file diff --git a/src/Fluentify/BuilderDelegateGenerator.cs b/src/Fluentify/BuilderDelegateGenerator.cs index b497114..0e3a801 100644 --- a/src/Fluentify/BuilderDelegateGenerator.cs +++ b/src/Fluentify/BuilderDelegateGenerator.cs @@ -17,7 +17,7 @@ public sealed class BuilderDelegateGenerator internal const string Source = $$""" namespace Fluentify { - internal delegate T Builder(T subject) + public delegate T Builder(T subject) where T : new(); } """;