Skip to content

Commit 7208d92

Browse files
Merge pull request #21 from altasoft/feature/transformation
Feature/transformation
2 parents ccf9654 + e1d7488 commit 7208d92

File tree

49 files changed

+762
-142
lines changed

Some content is hidden

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

49 files changed

+762
-142
lines changed

AltaSoft.DomainPrimitives.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHubActions", "GitHubActi
3131
EndProject
3232
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AltaSoft.DomainPrimitives.SwaggerExtensions", "src\AltaSoft.DomainPrimitives.SwaggerExtensions\AltaSoft.DomainPrimitives.SwaggerExtensions.csproj", "{289BC781-8B67-4A15-87A9-198EEA255160}"
3333
EndProject
34+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AltaSoft.DomainPrimitives.UnitTests", "tests\AltaSoft.DomainPrimitives.UnitTests\AltaSoft.DomainPrimitives.UnitTests.csproj", "{253F7819-7F69-9B49-8257-65A17B56DF55}"
35+
EndProject
3436
Global
3537
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3638
Debug|Any CPU = Debug|Any CPU
@@ -61,6 +63,10 @@ Global
6163
{289BC781-8B67-4A15-87A9-198EEA255160}.Debug|Any CPU.Build.0 = Debug|Any CPU
6264
{289BC781-8B67-4A15-87A9-198EEA255160}.Release|Any CPU.ActiveCfg = Release|Any CPU
6365
{289BC781-8B67-4A15-87A9-198EEA255160}.Release|Any CPU.Build.0 = Release|Any CPU
66+
{253F7819-7F69-9B49-8257-65A17B56DF55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
67+
{253F7819-7F69-9B49-8257-65A17B56DF55}.Debug|Any CPU.Build.0 = Debug|Any CPU
68+
{253F7819-7F69-9B49-8257-65A17B56DF55}.Release|Any CPU.ActiveCfg = Release|Any CPU
69+
{253F7819-7F69-9B49-8257-65A17B56DF55}.Release|Any CPU.Build.0 = Release|Any CPU
6470
EndGlobalSection
6571
GlobalSection(SolutionProperties) = preSolution
6672
HideSolutionNode = FALSE
@@ -69,6 +75,7 @@ Global
6975
{A3D3536F-993E-4606-B9DF-A4C4B3E1AF69} = {09A03F75-1F18-45BC-9D0A-289AC607DAD0}
7076
{E49201A0-DCC3-4ED2-A5A0-25DDD5E3D017} = {09A03F75-1F18-45BC-9D0A-289AC607DAD0}
7177
{A7BFF96E-3DFF-4132-AD2B-7C529C14A3E8} = {BF9447CC-2D00-495A-A8C3-D4890EADB01B}
78+
{253F7819-7F69-9B49-8257-65A17B56DF55} = {09A03F75-1F18-45BC-9D0A-289AC607DAD0}
7279
EndGlobalSection
7380
GlobalSection(ExtensibilityGlobals) = postSolution
7481
SolutionGuid = {84804F7F-F00C-454C-A90E-4013B6EA6263}

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<Product>Domain Primitives</Product>
1010
<Company>ALTA Software llc.</Company>
1111
<Copyright>Copyright © 2024 ALTA Software llc.</Copyright>
12-
<Version>5.1.3</Version>
12+
<Version>6.0.0</Version>
1313
</PropertyGroup>
1414

1515
<PropertyGroup>

README.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# DomainPrimitives for C#
1+
# DomainPrimitives for C#
22

33
[![Version](https://img.shields.io/nuget/v/AltaSoft.DomainPrimitives?label=Version&color=0c3c60&style=for-the-badge&logo=nuget)](https://www.nuget.org/profiles/AltaSoft)
44
[![Dot NET 7+](https://img.shields.io/static/v1?label=DOTNET&message=7%2B&color=0c3c60&style=for-the-badge)](https://dotnet.microsoft.com)
@@ -14,6 +14,7 @@
1414
- [Installation](#installation)
1515
- [Creating your Domain type](#creating-your-domain-type)
1616
- [Json Conversion](#json-conversion)
17+
- [Transform Method](#transform-method)
1718
- [Contributions](#contributions)
1819
- [Contact](#contact)
1920
- [License](#license)
@@ -875,6 +876,51 @@ public static void JsonSerializationAndDeserialization()
875876
}
876877
```
877878

879+
880+
# 🔄 Transform Method
881+
882+
In `AltaSoft.DomainPrimitives`, you can optionally define a static method named `Transform` inside your domain primitive to automatically preprocess input values before validation or instantiation.
883+
884+
## ✅ Signature
885+
```csharp
886+
public static T Transform(T value) //
887+
```
888+
- `T` must match the value type of your domain primitive (e.g., `string`, `int`, etc.).
889+
- The method can be `private`, `internal`, or `public`.
890+
- It **must be static** and **accept a single parameter** of type `T`.
891+
892+
## 🎯 When It's Invoked
893+
894+
If present, the `Transform` method is automatically called before:
895+
896+
- Running `Validate(value)`
897+
- Invoking the constructor or `TryCreate(...)` method
898+
899+
This ensures the input is normalized consistently at the boundary of your domain object.
900+
901+
## 📌 Example: `ToUpperString`
902+
903+
```csharp
904+
public sealed partial class ToUpperString : IDomainValue<string>
905+
{
906+
static PrimitiveValidationResult Validate(string value) =>
907+
value.All(char.IsUpper) ? PrimitiveValidationResult.Ok : "Value must be all uppercase.";
908+
909+
// This method is automatically invoked before validation and construction.
910+
static string Transform(string value) => value.ToUpperInvariant();
911+
912+
public ToUpperString(string value) : this(Transform(value), true) { }
913+
}
914+
```
915+
916+
### What happens:
917+
918+
- A user provides `"hello"` to `TryCreate("hello", out var result, out var error)`
919+
- `Transform("hello")` runs and returns `"HELLO"`
920+
- `"HELLO"` is then validated with `Validate(...)`
921+
---
922+
923+
878924
# Contributions
879925
Contributions to AltaSoft.DomainPrimitives are welcome! Whether you have suggestions or wish to contribute code, feel free to submit a pull request or open an issue.
880926

src/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>netstandard2.0</TargetFramework>
@@ -17,8 +17,8 @@
1717
</PropertyGroup>
1818

1919
<ItemGroup>
20-
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
21-
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" PrivateAssets="all" />
20+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0" PrivateAssets="all" />
21+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" PrivateAssets="all" />
2222
</ItemGroup>
2323

2424
<ItemGroup>

src/AltaSoft.DomainPrimitives.Generator/Executor.cs

Lines changed: 21 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ internal static void Execute(
118118

119119
var hasOverridenHashCode = typeSymbol.GetMembersOfType<IMethodSymbol>().Any(x => string.Equals(x.OverriddenMethod?.Name, "GetHashCode", StringComparison.Ordinal));
120120

121+
var hasTransformMethod = typeSymbol.GetMembersOfType<IMethodSymbol>()
122+
.Any(x =>
123+
x is { Name: "Transform", IsStatic: true, Parameters.Length: 1 } &&
124+
x.ReturnType.Equals(underlyingTypeSymbol, SymbolEqualityComparer.Default) &&
125+
x.Parameters[0].Type.Equals(underlyingTypeSymbol, SymbolEqualityComparer.Default));
126+
121127
var generatorData = new GeneratorData
122128
{
123129
FieldName = "_valueOrThrow",
@@ -129,7 +135,8 @@ internal static void Execute(
129135
Namespace = typeSymbol.ContainingNamespace.ToDisplayString(),
130136
GenerateImplicitOperators = true,
131137
ParentSymbols = parentSymbols,
132-
GenerateConvertibles = underlyingType.IsIConvertible()
138+
GenerateConvertibles = underlyingType.IsIConvertible(),
139+
UseTransformMethod = hasTransformMethod
133140
};
134141

135142
var attributes = typeSymbol.GetAttributes();
@@ -145,6 +152,17 @@ internal static void Execute(
145152
return null;
146153
}
147154

155+
if (underlyingType == DomainPrimitiveUnderlyingType.String && typeSymbol.TypeKind != TypeKind.Class)
156+
{
157+
context.ReportDiagnostic(DiagnosticHelper.InvalidClassTypeSpecified(typeSymbol.Locations.FirstOrDefault(), typeSymbol.Name));
158+
return null;
159+
}
160+
if (underlyingType != DomainPrimitiveUnderlyingType.String && typeSymbol.TypeKind != TypeKind.Struct)
161+
{
162+
context.ReportDiagnostic(DiagnosticHelper.InvalidStructTypeSpecified(typeSymbol.Locations.FirstOrDefault(), underlyingType.ToString(), typeSymbol.Name));
163+
return null;
164+
}
165+
148166
if (!isDateOrTime && serializationAttribute is not null)
149167
{
150168
context.ReportDiagnostic(DiagnosticHelper.TypeMustBeDateType(serializationAttribute.GetAttributeLocation(), typeSymbol.Name));
@@ -185,78 +203,6 @@ internal static void Execute(
185203
return generatorData;
186204
}
187205

188-
//private static bool DefaultPropertyReturnsDefaultValue(IPropertySymbol property, DomainPrimitiveUnderlyingType underlyingType)
189-
//{
190-
// var syntaxRefs = property.GetMethod?.DeclaringSyntaxReferences;
191-
// if (syntaxRefs is null)
192-
// {
193-
// // If there are no syntax references, the property doesn't have a getter
194-
// return false;
195-
// }
196-
197-
// ExpressionSyntax? returnExpression = null;
198-
199-
// foreach (var syntaxRef in syntaxRefs)
200-
// {
201-
// var syntaxNode = syntaxRef.GetSyntax();
202-
203-
// // Handle expression-bodied properties
204-
// if (syntaxNode is ArrowExpressionClauseSyntax arrowExpressionClauseSyntax)
205-
// {
206-
// returnExpression = arrowExpressionClauseSyntax.Expression;
207-
// break;
208-
// }
209-
210-
// // Handle expression-bodied properties
211-
// if (syntaxNode is PropertyDeclarationSyntax { ExpressionBody: { } expressionBody })
212-
// {
213-
// returnExpression = expressionBody.Expression;
214-
// break;
215-
// }
216-
217-
// // Handle properties with getters that have a body
218-
// if (syntaxNode is AccessorDeclarationSyntax { Body: not null } accessorDeclaration)
219-
// {
220-
// var returnExpressions = accessorDeclaration.Body.DescendantNodes()
221-
// .OfType<ReturnStatementSyntax>()
222-
// .Select(r => r.Expression)
223-
// .ToArray();
224-
225-
// if (returnExpressions.Length != 1)
226-
// return false;
227-
228-
// returnExpression = returnExpressions[0];
229-
// break;
230-
// }
231-
// }
232-
233-
// // Check if the return expression is a default value for the type
234-
// switch (returnExpression)
235-
// {
236-
// case null:
237-
// return false;
238-
239-
// case DefaultExpressionSyntax:
240-
// return true;
241-
242-
// // Simplified check for literal or default expressions
243-
// case LiteralExpressionSyntax literal:
244-
// if (literal.IsKind(SyntaxKind.DefaultLiteralExpression))
245-
// return true;
246-
247-
// // Determine the default value for the property's type
248-
// var defaultValue = underlyingType.GetDefaultValue();
249-
// if (defaultValue is null)
250-
// return literal.Token.Value is null;
251-
252-
// return defaultValue.Equals(literal.Token.Value);
253-
254-
// // For more complex expressions, additional analysis is required
255-
// default:
256-
// return false;
257-
// }
258-
//}
259-
260206
/// <summary>
261207
/// Retrieves the SupportedOperationsAttributeData for a specified class, considering inheritance.
262208
/// </summary>
@@ -784,7 +730,8 @@ private static bool ProcessConstructor(GeneratorData data, SourceCodeBuilder bui
784730
builder.AppendSummary($"Initializes a new instance of the <see cref=\"{type.Name}\"/> class by validating the specified <see cref=\"{underlyingTypeName}\"/> value using <see cref=\"Validate\"/> static method.");
785731
builder.AppendParamDescription("value", "The value to be validated.");
786732

787-
builder.AppendLine($"public {type.Name}({underlyingTypeName} value) : this(value, true)")
733+
var ctorCall = data.UseTransformMethod ? "Transform(value)" : "value";
734+
builder.AppendLine($"public {type.Name}({underlyingTypeName} value) : this({ctorCall}, true)")
788735
.OpenBracket()
789736
.CloseBracket()
790737
.NewLine();

src/AltaSoft.DomainPrimitives.Generator/Helpers/DiagnosticHelper.cs

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
using Microsoft.CodeAnalysis;
2-
using System;
1+
using System;
2+
using Microsoft.CodeAnalysis;
33

44
namespace AltaSoft.DomainPrimitives.Generator.Helpers;
55

@@ -10,23 +10,6 @@ internal static class DiagnosticHelper
1010
{
1111
private const string Category = "AltaSoft.DomainPrimitives.Generator";
1212

13-
///// <summary>
14-
///// Creates a diagnostic indicating that the AltaSoft.DomainPrimitives.Generator has started.
15-
///// </summary>
16-
///// <returns>
17-
///// The created diagnostic.
18-
///// </returns>
19-
//internal static Diagnostic GeneratorStarted()
20-
//{
21-
// return Diagnostic.Create(new DiagnosticDescriptor(
22-
// "AL0000",
23-
// "AltaSoft.DomainPrimitives.Generator started",
24-
// "AltaSoft.DomainPrimitives.Generator started",
25-
// Category,
26-
// DiagnosticSeverity.Info,
27-
// isEnabledByDefault: true), null);
28-
//}
29-
3013
/// <summary>
3114
/// Creates a diagnostic for general error
3215
/// </summary>
@@ -206,4 +189,46 @@ internal static Diagnostic ClassHasDefaultConstructor(string className, Location
206189
DiagnosticSeverity.Error,
207190
isEnabledByDefault: true), location, className);
208191
}
192+
193+
/// <summary>
194+
/// Creates a diagnostic indicating that the specified type must be a class
195+
/// to support domain primitive generation.
196+
/// </summary>
197+
/// <param name="location">The source location where the diagnostic should appear.</param>
198+
/// <param name="className">The name of the type that is incorrectly used.</param>
199+
/// <returns>
200+
/// A diagnostic indicating that a domain primitive based on a string type must be a class.
201+
/// </returns>
202+
internal static Diagnostic InvalidClassTypeSpecified(Location? location, string className)
203+
{
204+
return Diagnostic.Create(
205+
new DiagnosticDescriptor(
206+
"AL1052",
207+
"Domain primitive types based on string must be declared as classes",
208+
"Type '{0}' must be declared as a class when using 'string' as the underlying domain primitive type.",
209+
Category,
210+
DiagnosticSeverity.Error,
211+
isEnabledByDefault: true), location, className);
212+
}
213+
/// <summary>
214+
/// Creates a diagnostic indicating that the specified type must be a struct
215+
/// to support domain primitive generation.
216+
/// </summary>
217+
/// <param name="location">The source location where the diagnostic should appear.</param>
218+
/// <param name="typeName">The name of the underlying type used in the domain primitive.</param>
219+
/// <param name="className">The name of the type that is incorrectly used.</param>
220+
/// <returns>
221+
/// A diagnostic indicating that a domain primitive based on <paramref name="typeName"/> must be a struct.
222+
/// </returns>
223+
internal static Diagnostic InvalidStructTypeSpecified(Location? location, string typeName, string className)
224+
{
225+
return Diagnostic.Create(
226+
new DiagnosticDescriptor(
227+
"AL1053",
228+
$"Domain primitive types based on '{typeName}' must be declared as structs",
229+
"Type '{0}' must be declared as a struct when using '{1}' as the underlying domain primitive type.",
230+
Category,
231+
DiagnosticSeverity.Error,
232+
isEnabledByDefault: true), location, className, typeName);
233+
}
209234
}

src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,9 +406,15 @@ internal static void GenerateMandatoryMethods(GeneratorData data, SourceCodeBuil
406406
.AppendParamDescription("errorMessage", "When this method returns, contains the error message if the conversion failed; otherwise, null.")
407407
.AppendReturnsDescription("true if the conversion succeeded; otherwise, false.");
408408

409-
builder.Append("public static bool TryCreate(").Append(primitiveType).Append(" value,[NotNullWhen(true)] out ").Append(data.ClassName)
409+
builder.Append("public static bool TryCreate(").Append(primitiveType).Append(" value, [NotNullWhen(true)] out ").Append(data.ClassName)
410410
.AppendLine("? result, [NotNullWhen(false)] out string? errorMessage)")
411411
.OpenBracket();
412+
413+
if (data.UseTransformMethod)
414+
{
415+
builder.AppendLine("value = Transform(value);");
416+
}
417+
412418
AddStringLengthValidation(data, builder);
413419

414420
builder.AppendLine("var validationResult = Validate(value);")

src/AltaSoft.DomainPrimitives.Generator/Models/GeneratorData.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,9 @@ internal sealed class GeneratorData
122122
/// if StringLengthAttribute validation is applied to a domain Primitive this will be used to determine the values and use them before calling validation method.
123123
/// </summary>
124124
public (int minLength, int maxLength)? StringLengthAttributeValidation { get; set; }
125+
126+
/// <summary>
127+
/// Indicates whether the `Transform` method should be invoked before validation and instantiation.
128+
/// </summary>
129+
public bool UseTransformMethod { get; set; }
125130
}

src/AltaSoft.DomainPrimitives.XmlDataTypes/AltaSoft.DomainPrimitives.XmlDataTypes.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
</ItemGroup>
2626

2727
<ItemGroup>
28-
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="7.0.0" />
28+
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="8.1.1" />
2929
</ItemGroup>
3030

3131

0 commit comments

Comments
 (0)