Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IParsable and Parse/TryParse hoisting #570

Merged
merged 4 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
6 changes: 6 additions & 0 deletions Consumers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_files", "_files", "{0F2E18
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsumerTests", "tests\ConsumerTests\ConsumerTests.csproj", "{B9FAC951-7F77-49B5-9DCD-C8278B45EE65}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApplication", "samples\WebApplication\WebApplication.csproj", "{DDCC7F24-6C4B-4F6D-A216-CEB01E634BD1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -31,6 +33,10 @@ Global
{B9FAC951-7F77-49B5-9DCD-C8278B45EE65}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B9FAC951-7F77-49B5-9DCD-C8278B45EE65}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B9FAC951-7F77-49B5-9DCD-C8278B45EE65}.Release|Any CPU.Build.0 = Release|Any CPU
{DDCC7F24-6C4B-4F6D-A216-CEB01E634BD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DDCC7F24-6C4B-4F6D-A216-CEB01E634BD1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DDCC7F24-6C4B-4F6D-A216-CEB01E634BD1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DDCC7F24-6C4B-4F6D-A216-CEB01E634BD1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
2 changes: 2 additions & 0 deletions Consumers.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CheckNamespace/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypesAndNamespaces/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=a0b4bc4d_002Dd13b_002D4a37_002Db37e_002Dc9c6864e4302/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"&gt;&lt;ElementKinds&gt;&lt;Kind Name="NAMESPACE" /&gt;&lt;Kind Name="CLASS" /&gt;&lt;Kind Name="STRUCT" /&gt;&lt;Kind Name="ENUM" /&gt;&lt;Kind Name="DELEGATE" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CustomTools/CustomToolsData/@EntryValue"></s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Vogen/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
8 changes: 8 additions & 0 deletions Consumers.v3.ncrunchsolution
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<SolutionConfiguration>
<Settings>
<AllowParallelTestExecution>True</AllowParallelTestExecution>
<EnableRDI>True</EnableRDI>
<RdiConfigured>True</RdiConfigured>
<SolutionConfigured>True</SolutionConfigured>
</Settings>
</SolutionConfiguration>
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,7 @@ platform and tooling. Those are [described here](https://github.com/VerifyTests/
**NOTE: If the change to the source generators expectedly changes the majority of the snapshot tests, then you can tell the
snapshot runner to overwrite the expected files with the actual files that are generated.**

To do this, run `.\Build.ps1 -v "Minimal" -resetSnapshots $true`. This deletes all `snaphsot` folders under the `tests` folder
To do this, run `.\RunSnapshots.ps1 -v "Minimal" -reset $true`. This deletes all `snaphsot` folders under the `tests` folder
and treats everything that's generated as the new baseline for future comparisons.

This will mean that there are potentially **thousands** of changed files that will end up in the commit, but it's expected and unavoidable.
Expand All @@ -784,7 +784,7 @@ is a separate solution for this. It's called `Consumers.sln`. What happens is th
the tests, and creates the NuGet package _in a private local folder_. The package is version `999.9.xxx` and the consumer
references the latest version. The consumer can then really use the source generator, just like anything else.

> Note: if you don't want to run the lengthy snapshot tests when building the local nuget package, run `.\Build.ps1 -v "minimal" -skiptests $true`
> Note: if you want to run the lengthy snapshot tests, run `.\RunSnapshots.ps1 -v "minimal"`

### Can I get it to throw my own exception?

Expand Down
3 changes: 3 additions & 0 deletions Vogen.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_ARGUMENTS_STYLE/@EntryValue">CHOP_IF_LONG</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateConstants/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=15b5b1f1_002D457c_002D4ca6_002Db278_002D5615aedc07d3/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="READONLY_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=236f7aa5_002D7b06_002D43ca_002Dbf2a_002D9b31bfcff09a/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="CONSTANT_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CustomTools/CustomToolsData/@EntryValue"></s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Int64 x:Key="/Default/Environment/UnitTesting/ParallelProcessesCount/@EntryValue">16</s:Int64>

<s:Boolean x:Key="/Default/PatternsAndTemplates/Todo/TodoPatterns/=50FDC07863E169428BD0187F9012A372/@KeyIndexDefined">True</s:Boolean>
Expand Down
2 changes: 2 additions & 0 deletions docs/site/Writerside/hi.tree
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
<toc-element topic="Terms-Used.md"/>
<toc-element topic="Performance.md"/>
<toc-element topic="Testing.md"/>
<toc-element topic="Hoisting.md"/>
<toc-element topic="Parsing.md"/>
</toc-element>
<toc-element topic="FAQ.md">
<toc-element topic="How-to-identify-a-type-that-is-generated-by-Vogen.md"/>
Expand Down
16 changes: 16 additions & 0 deletions docs/site/Writerside/topics/how-to/Hoisting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Hoisting

Vogen 'hoists' functionality from the underlying primitive. For instance, if the underlying type implements `IComparable<>`, then the code that Vogen generates will also implement `IComparable<>` with the generic argument being the type of wrapper.

Here is what is hoisted:

## Parsing

Any method named `Parse` or `TryParse` from the underlying primitive is hoisted. Also, the `IParsable` family of interfaces (including `ISpanParsable` and `IUtf8SpanParsable`) that are **implemented publicly** by the primitive, are hoisted.

Please see the [Parsing](Parsing.md) documentation for more information.

## IComparable

If the underlying primitive implements this, and the configuration for `ComparisonGeneration` is `UseUnderlying`, then the generated wrapper will implement `IComparable<>` and `IComparable`, with the generic argument being the type of wrapped primitive.
The method generated is `public into CompareTo([primitive] other)...`
24 changes: 24 additions & 0 deletions docs/site/Writerside/topics/how-to/Parsing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Parsing

Vogen 'hoists' (copies up to the wrapper) certain functionality from the underlying primitive. For instance, any method named `Parse` or `TryParse` from the underlying primitive is hoisted.

The `IParsable` family of interfaces (including `ISpanParsable` and `IUtf8SpanParsable`) that are **implemented
publicly** by the primitive are hoisted to the wrapper and the generic parameter is changed to that of the wrapper. The methods that are generated delegate back to the underlying implementation of primitive.

Some primitive types, such as `bool`, explicitly implement `ISpanParsable<bool>` **privately**, so the _interface_ is not
hoisted to wrapper, but the _non-explicit_ methods *are* hoisted.

`TryParse` calls the underlying's `TryParse`. It then sees if the parsed value passes the `Validate` method. If it
doesn't, then it will return `false`, and the `out` will have a default instance; for classes, a `null`
value, or, for structs, a `default` value object that will throw if you try to access its value.

`string`s are a special case. It is useful to have a Parse/TryParse methods on these, for instance, when value objects represent parameters in ASP.NET Core endpoints.

If `IParsable<>` is not available, e.g. in versions before .NET 7, then the interfaces are not generated for the wrapper. For `strings`, the `Parse` and `TryParse` methods are still generated though.

<note>
Beginning with V4.0, the behaviour of `TryParse` has changed. In previous versions, `TryParse` would throw a `ValueObjectValidationException`.
With hindsight, this doesn't make sense and doesn't fit in with the idiomatic use of the `TryParse` pattern.
Also changed is that by default, a value object wrapping a string will automatically generate `IParsable`, `ISpanParsable`, and `IUtf8SpanParsable`.
</note>

Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# Differences with records

TL;DR: there are differences, and it's best to stick to a `class` or `struct` rather than records as the benefits of records don't really apply to Vogen where a single primitive value is being wrapped and protected.
TL;DR: there are significant differences, and it is best to stick to a `class` or `struct` rather than records as the benefits of records don't apply to Vogen where a single primitive value is being wrapped and protected.

For classes and structs, Vogen generates a lot of boilerplate code. But for records, some of this boilerplate code is
already generated. This page lists the differences between records (classes and structs) and non-record classes and structs.

* the generated code for records have an `init` accessibility on the `Value` property in order to support `with`,
* the generated code for records have an `init` accessibility on the `Value` property to support `with`,
e.g. `var vo2 = vo1 with { Value = 42 }` - but initializing via this doesn't set the object as being initialized as this
would promote the use of public constructor (even though the analyzer will still cause a compilation error)
would promote using a public constructor (even though the analyzer will still cause a compilation error)
* the generated code for records still overrides `ToString` as the default enumerates fields, which we don't want

Something to consider in the forthcoming C# 12, is primary constructors for classes, and how they will fit in with Vogen.
Something not yet implemented is primary constructor analysis for classes in C#12, and how they will fit in with Vogen.
This is covered in [this issue](https://github.com/SteveDunn/Vogen/issues/563).
7 changes: 3 additions & 4 deletions docs/site/Writerside/topics/reference/ValueObjectAttribute.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# ValueObjectAttribute

This has the following parameters

## param 1
## param 2
<note>
This topic is incomplete and is currently being improved.
</note>
68 changes: 68 additions & 0 deletions samples/WebApplication/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Vogen;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)],
City.From("London")
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

app.MapGet("/weatherforecast/{city}", (City city) =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)],
city
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecastByCity")
.WithOpenApi();

app.Run();

record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary, City City)
{
public int TemperatureF => 32 + (int) (TemperatureC / 0.5556);
}

[ValueObject<string>(parsableForStrings: ParsableForStrings.GenerateMethods)]
public partial class City
{
}
41 changes: 41 additions & 0 deletions samples/WebApplication/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:45231",
"sslPort": 44303
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5125",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7033;http://localhost:5125",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
24 changes: 24 additions & 0 deletions samples/WebApplication/WebApplication.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseLocallyBuiltPackage>true</UseLocallyBuiltPackage>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.2"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
</ItemGroup>

<ItemGroup Condition=" '$(UseLocallyBuiltPackage)' != ''">
<PackageReference Include="Vogen" Version="999.9.*" />
</ItemGroup>

<ItemGroup Condition=" '$(UseLocallyBuiltPackage)' == ''">
<PackageReference Include="Vogen" Version="999.9.10219943" />
</ItemGroup>


</Project>
6 changes: 6 additions & 0 deletions samples/WebApplication/WebApplication.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@WebApplication_HostAddress = http://localhost:5125

GET {{WebApplication_HostAddress}}/weatherforecast/
Accept: application/json

###
8 changes: 8 additions & 0 deletions samples/WebApplication/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions samples/WebApplication/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
23 changes: 23 additions & 0 deletions src/Vogen.SharedTypes/ParsableGeneration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.ComponentModel;

namespace Vogen;

public enum ParsableForStrings
{
[EditorBrowsable(EditorBrowsableState.Never)]
Unspecified = -1,

GenerateNothing = 0,
GenerateMethods = 1,
GenerateMethodsAndInterface = 2
}

public enum ParsableForPrimitives
{
[EditorBrowsable(EditorBrowsableState.Never)]
Unspecified = -1,

GenerateNothing = 0,
HoistMethods = 1,
HoistMethodsAndInterfaces = 2
}
22 changes: 19 additions & 3 deletions src/Vogen.SharedTypes/ValueObjectAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,22 @@ public ValueObjectAttribute(
ComparisonGeneration comparison = ComparisonGeneration.Default,
StringComparersGeneration stringComparers = StringComparersGeneration.Unspecified,
CastOperator toPrimitiveCasting = CastOperator.Unspecified,
CastOperator fromPrimitiveCasting = CastOperator.Unspecified)
: base(typeof(T), conversions, throws, customizations, deserializationStrictness, debuggerAttributes, comparison, stringComparers, toPrimitiveCasting, fromPrimitiveCasting)
CastOperator fromPrimitiveCasting = CastOperator.Unspecified,
ParsableForStrings parsableForStrings = ParsableForStrings.Unspecified,
ParsableForPrimitives parsableForPrimitives = ParsableForPrimitives.Unspecified)
: base(
typeof(T),
conversions,
throws,
customizations,
deserializationStrictness,
debuggerAttributes,
comparison,
stringComparers,
toPrimitiveCasting,
fromPrimitiveCasting,
parsableForStrings,
parsableForPrimitives)
{
}
}
Expand All @@ -52,7 +66,9 @@ public ValueObjectAttribute(
ComparisonGeneration comparison = ComparisonGeneration.Default,
StringComparersGeneration stringComparers = StringComparersGeneration.Unspecified,
CastOperator toPrimitiveCasting = CastOperator.Unspecified,
CastOperator fromPrimitiveCasting = CastOperator.Unspecified)
CastOperator fromPrimitiveCasting = CastOperator.Unspecified,
ParsableForStrings parsableForStrings = ParsableForStrings.Unspecified,
ParsableForPrimitives parsableForPrimitives = ParsableForPrimitives.Unspecified)
{
}
}
Expand Down
Loading