diff --git a/.editorconfig b/.editorconfig
index efb12e903c..a7cd612838 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -210,7 +210,7 @@ indent_size = 2
# markdown
[*.{md,mdx}]
-indent_size = 2
+indent_size = unset
trim_trailing_whitespace = false
# Verify settings
diff --git a/docs/docs/configuration/mapper.mdx b/docs/docs/configuration/mapper.mdx
index 7fac0bfb6f..a03ada032b 100644
--- a/docs/docs/configuration/mapper.mdx
+++ b/docs/docs/configuration/mapper.mdx
@@ -229,7 +229,7 @@ To enforce strict enum mappings set `RMG037` and `RMG038` to error, see [strict
### String format
-The string format passed to `ToString` calls when converting to a string can be customized
+The string format passed to `ToString` calls when converting to a string (using `IFormattable`) can be customized
by using the `StringFormat` property of the `MapPropertyAttribute`.
```csharp
@@ -243,6 +243,43 @@ public partial class CarMapper
}
```
+### String format provider & culture
+
+To customize the format provider / culture to be used by Mapperly when calling `ToString` (using `IFormattable`)
+format providers can be used.
+A format provider can be provided to Mapperly by simply annotating a field or property within the Mapper with the `FormatProviderAttribute`.
+The field/property need to return a type implementing `System.IFormatProvider`.
+Format providers can be referenced by the name of the property / field in `MapPropertyAttribute.FormatProvider`.
+A format provider can be marked as default (set the default property of the `FormatProviderAttribute` to true).
+A default format provider is used for all `ToString` conversions when the source implements `System.IFormattable`.
+In a mapper only one format provider can be marked as default.
+
+```csharp
+[Mapper]
+public partial class CarMapper
+{
+ // highlight-start
+ [FormatProvider(Default = true)]
+ private IFormatProvider CurrentCulture => CultureInfo.CurrentCulture;
+ // highlight-end
+
+ // highlight-start
+ [FormatProvider]
+ private readonly IFormatProvider _enCulture = CultureInfo.GetCultureInfo("en-US");
+ // highlight-end
+
+ // highlight-start
+ [MapProperty(nameof(Car.LocalPrice), nameof(CarDto.LocalPrice), StringFormat = "C")]
+ [MapProperty(nameof(Car.ListPrice), nameof(CarDto.ListPrice), StringFormat = "C", FormatProvider = nameof(_enCulture)]
+ // highlight-end
+ public partial CarDto MapCar(Car car);
+
+ // generates
+ target.LocalPrice = source.LocalPrice.ToString("C", CurrentCulture);
+ target.ListPrice = source.ListPrice.ToString("C", _enCulture);
+}
+```
+
## Default Mapper configuration
The `MapperDefaultsAttribute` allows to set default configurations applied to all mappers on the assembly level.
diff --git a/docs/docs/contributing/architecture.md b/docs/docs/contributing/architecture.md
index 8416f61781..44f23bcf25 100644
--- a/docs/docs/contributing/architecture.md
+++ b/docs/docs/contributing/architecture.md
@@ -33,8 +33,9 @@ The `DescriptorBuilder` does this by following this process:
2. Extracting user implemented and user defined mapping methods.
It instantiates a `User*Mapping` (eg. `UserDefinedNewInstanceMethodMapping`) for each discovered mapping method and adds it to the queue of mappings to work on.
3. Extracting user implemented object factories
-4. Extracting external mappings
-5. For each mapping in the queue the `DescriptorBuilder` tries to build its implementation bodies.
+4. Extracting user implemented format providers
+5. Extracting external mappings
+6. For each mapping in the queue the `DescriptorBuilder` tries to build its implementation bodies.
This is done by a so called `*MappingBodyBuilder`.
A mapping body builder tries to map each property from the source to the target.
To do this, it asks the `DescriptorBuilder` to create mappings for the according types.
@@ -42,7 +43,7 @@ The `DescriptorBuilder` does this by following this process:
Each of the mapping builders try to create a mapping (an `ITypeMapping` implementation) for the asked type mapping by using
one approach on how to map types (eg. an explicit cast is implemented by the `ExplicitCastMappingBuilder`).
These mappings are queued in the queue of mappings which need the body to be built (currently body builders are only used for object to object (property-based) mappings).
-6. The `SourceEmitter` emits the code described by the `MapperDescriptor` and all its mappings.
+7. The `SourceEmitter` emits the code described by the `MapperDescriptor` and all its mappings.
The syntax objects are created by using `SyntaxFactory` and `SyntaxFactoryHelper`.
The `SyntaxFactoryHelper` tries to simplify creating formatted syntax trees.
If indentation is needed,
diff --git a/src/Riok.Mapperly.Abstractions/FormatProviderAttribute.cs b/src/Riok.Mapperly.Abstractions/FormatProviderAttribute.cs
new file mode 100644
index 0000000000..a6816ef07c
--- /dev/null
+++ b/src/Riok.Mapperly.Abstractions/FormatProviderAttribute.cs
@@ -0,0 +1,16 @@
+namespace Riok.Mapperly.Abstractions;
+
+///
+/// Marks a property or field as a format provider.
+/// A format provider needs to be of a type which implements and needs to have a getter.
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
+public sealed class FormatProviderAttribute : Attribute
+{
+ ///
+ /// If set to true, this format provider acts as a default format provider
+ /// and is used for all conversions without an explicit set.
+ /// Only one in a Mapper can be set to true.
+ ///
+ public bool Default { get; set; }
+}
diff --git a/src/Riok.Mapperly.Abstractions/MapPropertyAttribute.cs b/src/Riok.Mapperly.Abstractions/MapPropertyAttribute.cs
index 2887b8228f..11878e0507 100644
--- a/src/Riok.Mapperly.Abstractions/MapPropertyAttribute.cs
+++ b/src/Riok.Mapperly.Abstractions/MapPropertyAttribute.cs
@@ -44,10 +44,17 @@ public MapPropertyAttribute(string[] source, string[] target)
public IReadOnlyCollection Target { get; }
///
- /// Gets or sets the format of the ToString conversion.
+ /// Gets or sets the format of the ToString conversion (implementing ).
///
public string? StringFormat { get; set; }
+ ///
+ /// Gets or sets the name of a format provider field or property to be used for conversions accepting a format provider (implementing ).
+ /// If null the default format provider (annotated with and true)
+ /// or none (if no default format provider is provided) is used.
+ ///
+ public string? FormatProvider { get; set; }
+
///
/// Gets the full name of the target property path.
///
diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
index cb33fe160d..737d71121e 100644
--- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
+++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
@@ -131,3 +131,9 @@ Riok.Mapperly.Abstractions.MemberVisibility.Internal = 4 -> Riok.Mapperly.Abstra
Riok.Mapperly.Abstractions.MemberVisibility.Private = 16 -> Riok.Mapperly.Abstractions.MemberVisibility
Riok.Mapperly.Abstractions.MemberVisibility.Protected = 8 -> Riok.Mapperly.Abstractions.MemberVisibility
Riok.Mapperly.Abstractions.MemberVisibility.Public = 2 -> Riok.Mapperly.Abstractions.MemberVisibility
+Riok.Mapperly.Abstractions.FormatProviderAttribute
+Riok.Mapperly.Abstractions.FormatProviderAttribute.Default.get -> bool
+Riok.Mapperly.Abstractions.FormatProviderAttribute.Default.set -> void
+Riok.Mapperly.Abstractions.FormatProviderAttribute.FormatProviderAttribute() -> void
+Riok.Mapperly.Abstractions.MapPropertyAttribute.FormatProvider.get -> string?
+Riok.Mapperly.Abstractions.MapPropertyAttribute.FormatProvider.set -> void
diff --git a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md
index 04042b272b..c82bd0df87 100644
--- a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md
+++ b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md
@@ -127,7 +127,10 @@ RMG051 | Mapper | Warning | Invalid ignore source member found, nested ignor
RMG052 | Mapper | Warning | Invalid ignore target member found, nested ignores are not supported
RMG053 | Mapper | Error | The flag MemberVisibility.Accessible cannot be disabled, this feature requires .NET 8.0 or greater
RMG054 | Mapper | Error | Mapper class containing 'static partial' method must not have any instance methods
-RMG055 | Mapper | Error | The source type does not implement IFormattable, string format cannot be applied
+RMG055 | Mapper | Error | The source type does not implement IFormattable, string format and format provider cannot be applied
+RMG056 | Mapper | Error | Invalid format provider signature
+RMG057 | Mapper | Error | Format provider not found
+RMG058 | Mapper | Error | Multiple default format providers found, only one is allowed
### Removed Rules
Rule ID | Category | Severity | Notes
diff --git a/src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs b/src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs
index afcfd9064a..8fe4a1a538 100644
--- a/src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs
+++ b/src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs
@@ -13,6 +13,9 @@ public class AttributeDataAccessor(SymbolAccessor symbolAccessor)
private const string NameOfOperatorName = "nameof";
private const char FullNameOfPrefix = '@';
+ public TAttribute AccessSingle(ISymbol symbol)
+ where TAttribute : Attribute => AccessSingle(symbol);
+
public TData AccessSingle(ISymbol symbol)
where TAttribute : Attribute
where TData : notnull => Access(symbol).Single();
diff --git a/src/Riok.Mapperly/Configuration/PropertyMappingConfiguration.cs b/src/Riok.Mapperly/Configuration/PropertyMappingConfiguration.cs
index 5c8c802146..9341a08da4 100644
--- a/src/Riok.Mapperly/Configuration/PropertyMappingConfiguration.cs
+++ b/src/Riok.Mapperly/Configuration/PropertyMappingConfiguration.cs
@@ -6,5 +6,7 @@ public record PropertyMappingConfiguration(StringMemberPath Source, StringMember
{
public string? StringFormat { get; set; }
- public TypeMappingConfiguration ToTypeMappingConfiguration() => new(StringFormat);
+ public string? FormatProvider { get; set; }
+
+ public TypeMappingConfiguration ToTypeMappingConfiguration() => new(StringFormat, FormatProvider);
}
diff --git a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
index 3ae11aee56..357c9a7eb4 100644
--- a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
@@ -3,6 +3,7 @@
using Riok.Mapperly.Abstractions.ReferenceHandling;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Descriptors.ExternalMappings;
+using Riok.Mapperly.Descriptors.FormatProviders;
using Riok.Mapperly.Descriptors.MappingBodyBuilders;
using Riok.Mapperly.Descriptors.MappingBuilders;
using Riok.Mapperly.Descriptors.ObjectFactories;
@@ -67,7 +68,8 @@ MapperConfiguration defaultMapperConfiguration
// ExtractObjectFactories needs to be called after ExtractUserMappings due to configuring mapperDescriptor.Static
var objectFactories = ExtractObjectFactories();
- EnqueueUserMappings(objectFactories);
+ var formatProviders = ExtractFormatProviders();
+ EnqueueUserMappings(objectFactories, formatProviders);
ExtractExternalMappings();
_mappingBodyBuilder.BuildMappingBodies(cancellationToken);
BuildMappingMethodNames();
@@ -144,13 +146,14 @@ private ObjectFactoryCollection ExtractObjectFactories()
return ObjectFactoryBuilder.ExtractObjectFactories(_builderContext, _mapperDescriptor.Symbol);
}
- private void EnqueueUserMappings(ObjectFactoryCollection objectFactories)
+ private void EnqueueUserMappings(ObjectFactoryCollection objectFactories, FormatProviderCollection formatProviders)
{
foreach (var userMapping in _mappings.UserMappings)
{
var ctx = new MappingBuilderContext(
_builderContext,
objectFactories,
+ formatProviders,
userMapping.Method,
new TypeMappingKey(userMapping.SourceType, userMapping.TargetType)
);
@@ -167,6 +170,11 @@ private void ExtractExternalMappings()
}
}
+ private FormatProviderCollection ExtractFormatProviders()
+ {
+ return FormatProviderBuilder.ExtractFormatProviders(_builderContext, _mapperDescriptor.Symbol);
+ }
+
private void BuildMappingMethodNames()
{
foreach (var methodMapping in _mappings.MethodMappings)
diff --git a/src/Riok.Mapperly/Descriptors/FormatProviders/FormatProvider.cs b/src/Riok.Mapperly/Descriptors/FormatProviders/FormatProvider.cs
new file mode 100644
index 0000000000..91a9d0c7bb
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/FormatProviders/FormatProvider.cs
@@ -0,0 +1,5 @@
+using Microsoft.CodeAnalysis;
+
+namespace Riok.Mapperly.Descriptors.FormatProviders;
+
+public record FormatProvider(string Name, bool Default, ISymbol Symbol);
diff --git a/src/Riok.Mapperly/Descriptors/FormatProviders/FormatProviderBuilder.cs b/src/Riok.Mapperly/Descriptors/FormatProviders/FormatProviderBuilder.cs
new file mode 100644
index 0000000000..fcdebe50e4
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/FormatProviders/FormatProviderBuilder.cs
@@ -0,0 +1,45 @@
+using Microsoft.CodeAnalysis;
+using Riok.Mapperly.Abstractions;
+using Riok.Mapperly.Diagnostics;
+using Riok.Mapperly.Helpers;
+using Riok.Mapperly.Symbols;
+
+namespace Riok.Mapperly.Descriptors.FormatProviders;
+
+public static class FormatProviderBuilder
+{
+ public static FormatProviderCollection ExtractFormatProviders(SimpleMappingBuilderContext ctx, ITypeSymbol mapperSymbol)
+ {
+ var formatProviders = mapperSymbol
+ .GetMembers()
+ .Where(x => ctx.SymbolAccessor.HasAttribute(x))
+ .Select(x => BuildFormatProvider(ctx, x))
+ .WhereNotNull()
+ .ToList();
+
+ var defaultFormatProviderCandidates = formatProviders.Where(x => x.Default).Take(2).ToList();
+ if (defaultFormatProviderCandidates.Count > 1)
+ {
+ ctx.ReportDiagnostic(DiagnosticDescriptors.MultipleDefaultFormatProviders, defaultFormatProviderCandidates[1].Symbol);
+ }
+
+ var formatProvidersByName = formatProviders.GroupBy(x => x.Name).ToDictionary(x => x.Key, x => x.Single());
+ return new FormatProviderCollection(formatProvidersByName, defaultFormatProviderCandidates.FirstOrDefault());
+ }
+
+ private static FormatProvider? BuildFormatProvider(SimpleMappingBuilderContext ctx, ISymbol symbol)
+ {
+ var memberSymbol = MappableMember.Create(ctx.SymbolAccessor, symbol);
+ if (memberSymbol == null)
+ return null;
+
+ if (!memberSymbol.CanGet || symbol.IsStatic != ctx.Static || !memberSymbol.Type.Implements(ctx.Types.Get()))
+ {
+ ctx.ReportDiagnostic(DiagnosticDescriptors.InvalidFormatProviderSignature, symbol, symbol.Name);
+ return null;
+ }
+
+ var attribute = ctx.AttributeAccessor.AccessSingle(symbol);
+ return new FormatProvider(symbol.Name, attribute.Default, symbol);
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/FormatProviders/FormatProviderCollection.cs b/src/Riok.Mapperly/Descriptors/FormatProviders/FormatProviderCollection.cs
new file mode 100644
index 0000000000..851ef66657
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/FormatProviders/FormatProviderCollection.cs
@@ -0,0 +1,12 @@
+namespace Riok.Mapperly.Descriptors.FormatProviders;
+
+public class FormatProviderCollection(
+ IReadOnlyDictionary formatProvidersByName,
+ FormatProvider? defaultFormatProvider
+)
+{
+ public FormatProvider? Get(string? reference)
+ {
+ return reference == null ? defaultFormatProvider : formatProvidersByName.GetValueOrDefault(reference);
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
index 63eefd7d0d..8b842ad05f 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
@@ -2,6 +2,7 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Descriptors.Enumerables;
+using Riok.Mapperly.Descriptors.FormatProviders;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.ExistingTarget;
using Riok.Mapperly.Descriptors.Mappings.UserMappings;
@@ -19,19 +20,21 @@ public class MappingBuilderContext : SimpleMappingBuilderContext
public MappingBuilderContext(
SimpleMappingBuilderContext parentCtx,
ObjectFactoryCollection objectFactories,
+ FormatProviderCollection formatProviders,
IMethodSymbol? userSymbol,
TypeMappingKey mappingKey
)
: base(parentCtx)
{
ObjectFactories = objectFactories;
+ FormatProviders = formatProviders;
UserSymbol = userSymbol;
MappingKey = mappingKey;
Configuration = ReadConfiguration(new MappingConfigurationReference(UserSymbol, mappingKey.Source, mappingKey.Target));
}
protected MappingBuilderContext(MappingBuilderContext ctx, IMethodSymbol? userSymbol, TypeMappingKey mappingKey, bool clearDerivedTypes)
- : this(ctx, ctx.ObjectFactories, userSymbol, mappingKey)
+ : this(ctx, ctx.ObjectFactories, ctx.FormatProviders, userSymbol, mappingKey)
{
if (clearDerivedTypes)
{
@@ -57,6 +60,7 @@ protected MappingBuilderContext(MappingBuilderContext ctx, IMethodSymbol? userSy
public virtual bool IsExpression => false;
public ObjectFactoryCollection ObjectFactories { get; }
+ public FormatProviderCollection FormatProviders { get; }
///
public IReadOnlyCollection UserMappings => MappingBuilder.UserMappings;
@@ -212,6 +216,17 @@ public void ReportDiagnostic(DiagnosticDescriptor descriptor, params object[] me
public NullFallbackValue GetNullFallbackValue(ITypeSymbol? targetType = null) =>
GetNullFallbackValue(targetType ?? Target, MapperConfiguration.ThrowOnMappingNullMismatch);
+ public FormatProvider? GetFormatProvider(string? formatProviderName)
+ {
+ var formatProvider = FormatProviders.Get(formatProviderName);
+ if (formatProviderName != null && formatProvider == null)
+ {
+ ReportDiagnostic(DiagnosticDescriptors.FormatProviderNotFound, formatProviderName);
+ }
+
+ return formatProvider;
+ }
+
protected virtual NullFallbackValue GetNullFallbackValue(ITypeSymbol targetType, bool throwOnMappingNullMismatch)
{
if (targetType.IsNullable())
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/ToStringMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/ToStringMappingBuilder.cs
index cd8afa0b4a..ce37611fcb 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilders/ToStringMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/ToStringMappingBuilder.cs
@@ -16,7 +16,8 @@ public static class ToStringMappingBuilder
if (ctx.Target.SpecialType != SpecialType.System_String)
return null;
- if (ctx.MappingKey.Configuration.StringFormat == null)
+ var formatProvider = ctx.GetFormatProvider(ctx.MappingKey.Configuration.FormatProviderName);
+ if (ctx.MappingKey.Configuration.StringFormat == null && formatProvider == null)
return new ToStringMapping(ctx.Source, ctx.Target);
if (!ctx.Source.Implements(ctx.Types.Get()))
@@ -25,6 +26,6 @@ public static class ToStringMappingBuilder
return new ToStringMapping(ctx.Source, ctx.Target);
}
- return new ToStringMapping(ctx.Source, ctx.Target, ctx.MappingKey.Configuration.StringFormat);
+ return new ToStringMapping(ctx.Source, ctx.Target, ctx.MappingKey.Configuration.StringFormat, formatProvider?.Name);
}
}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/ToStringMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ToStringMapping.cs
index fe413383d0..0402d1baab 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/ToStringMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/ToStringMapping.cs
@@ -1,5 +1,6 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;
namespace Riok.Mapperly.Descriptors.Mappings;
@@ -11,15 +12,15 @@ namespace Riok.Mapperly.Descriptors.Mappings;
/// target = source.ToString();
///
///
-public class ToStringMapping(ITypeSymbol sourceType, ITypeSymbol targetType, string? stringFormat = null)
+public class ToStringMapping(ITypeSymbol sourceType, ITypeSymbol targetType, string? stringFormat = null, string? formatProviderName = null)
: SourceObjectMethodMapping(sourceType, targetType, nameof(ToString))
{
protected override IEnumerable BuildArguments(TypeMappingBuildContext ctx)
{
- if (stringFormat == null)
+ if (stringFormat == null && formatProviderName == null)
yield break;
- yield return StringLiteral(stringFormat);
- yield return NullLiteral();
+ yield return stringFormat == null ? NullLiteral() : StringLiteral(stringFormat);
+ yield return formatProviderName == null ? NullLiteral() : IdentifierName(formatProviderName);
}
}
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappingConfiguration.cs b/src/Riok.Mapperly/Descriptors/TypeMappingConfiguration.cs
index 5aaeced986..e9bcebde73 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappingConfiguration.cs
+++ b/src/Riok.Mapperly/Descriptors/TypeMappingConfiguration.cs
@@ -4,8 +4,9 @@ namespace Riok.Mapperly.Descriptors;
/// Configuration for a type mapping.
/// Eg. the format to apply to `ToString` calls.
///
-/// The format to apply to `ToString` calls.
-public record TypeMappingConfiguration(string? StringFormat = null)
+/// The format to apply to .
+/// The name of the format provider to apply to .
+public record TypeMappingConfiguration(string? StringFormat = null, string? FormatProviderName = null)
{
public static readonly TypeMappingConfiguration Default = new();
}
diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
index 02e01b609d..1796bb9210 100644
--- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
+++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
@@ -476,8 +476,35 @@ public static class DiagnosticDescriptors
public static readonly DiagnosticDescriptor SourceDoesNotImplementIFormattable = new DiagnosticDescriptor(
"RMG055",
- $"The source type does not implement {nameof(IFormattable)}, string format cannot be applied",
- $"The source type {{0}} does not implement {nameof(IFormattable)}, string format cannot be applied",
+ $"The source type does not implement {nameof(IFormattable)}, string format and format provider cannot be applied",
+ $"The source type {{0}} does not implement {nameof(IFormattable)}, string format and format provider cannot be applied",
+ DiagnosticCategories.Mapper,
+ DiagnosticSeverity.Error,
+ true
+ );
+
+ public static readonly DiagnosticDescriptor InvalidFormatProviderSignature = new DiagnosticDescriptor(
+ "RMG056",
+ "Invalid format provider signature",
+ "The format provider {0} has an invalid signature",
+ DiagnosticCategories.Mapper,
+ DiagnosticSeverity.Error,
+ true
+ );
+
+ public static readonly DiagnosticDescriptor FormatProviderNotFound = new DiagnosticDescriptor(
+ "RMG057",
+ "Format provider not found",
+ $"The format provider {{0}} could not be found, make sure it is annotated with {nameof(FormatProviderAttribute)}",
+ DiagnosticCategories.Mapper,
+ DiagnosticSeverity.Error,
+ true
+ );
+
+ public static readonly DiagnosticDescriptor MultipleDefaultFormatProviders = new DiagnosticDescriptor(
+ "RMG058",
+ "Multiple default format providers found, only one is allowed",
+ "Multiple default format providers found, only one is allowed",
DiagnosticCategories.Mapper,
DiagnosticSeverity.Error,
true
diff --git a/src/Riok.Mapperly/Symbols/FieldMember.cs b/src/Riok.Mapperly/Symbols/FieldMember.cs
index 538995c880..f136b36199 100644
--- a/src/Riok.Mapperly/Symbols/FieldMember.cs
+++ b/src/Riok.Mapperly/Symbols/FieldMember.cs
@@ -14,8 +14,8 @@ public class FieldMember(IFieldSymbol fieldSymbol) : IMappableMember
public ISymbol MemberSymbol => _fieldSymbol;
public bool IsNullable => _fieldSymbol.NullableAnnotation == NullableAnnotation.Annotated || Type.IsNullable();
public bool IsIndexer => false;
- public bool CanGet => !_fieldSymbol.IsReadOnly;
- public bool CanSet => true;
+ public bool CanGet => true;
+ public bool CanSet => !_fieldSymbol.IsReadOnly;
public bool CanSetDirectly => true;
public bool IsInitOnly => false;
diff --git a/test/Riok.Mapperly.IntegrationTests/BaseMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/BaseMapperTest.cs
index e371b3124a..96ddb174ce 100644
--- a/test/Riok.Mapperly.IntegrationTests/BaseMapperTest.cs
+++ b/test/Riok.Mapperly.IntegrationTests/BaseMapperTest.cs
@@ -15,8 +15,8 @@ namespace Riok.Mapperly.IntegrationTests
{
public abstract class BaseMapperTest
{
- private static string SolutionDirectory = GetSolutionDirectory();
- private static string ProjectDirectory = GetProjectDirectory();
+ private static readonly string _solutionDirectory = GetSolutionDirectory();
+ private static readonly string _projectDirectory = GetProjectDirectory();
static BaseMapperTest()
{
@@ -33,7 +33,7 @@ static BaseMapperTest()
Verifier.DerivePathInfo(
(_, _, type, method) =>
new PathInfo(
- Path.Combine(ProjectDirectory, "_snapshots"),
+ Path.Combine(_projectDirectory, "_snapshots"),
type.Name,
method.Name + GetSnapshotVersionSuffix(type, method)
)
@@ -43,7 +43,7 @@ static BaseMapperTest()
protected string GetGeneratedMapperFilePath(string name)
{
return Path.Combine(
- SolutionDirectory,
+ _solutionDirectory,
"artifacts",
"obj",
"Riok.Mapperly.IntegrationTests",
@@ -70,6 +70,7 @@ public static TestObject NewTestObj()
SubObject = new InheritanceSubObject { BaseIntValue = 1, SubIntValue = 2, },
EnumRawValue = TestEnum.Value20,
EnumStringValue = TestEnum.Value30,
+ DateTimeValue = new DateTime(2020, 1, 3, 15, 10, 5, DateTimeKind.Utc),
DateTimeValueTargetDateOnly = new DateTime(2020, 1, 3, 15, 10, 5, DateTimeKind.Utc),
DateTimeValueTargetTimeOnly = new DateTime(2020, 1, 3, 15, 10, 5, DateTimeKind.Utc),
IgnoredStringValue = "ignored",
diff --git a/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs b/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs
index b6e60d56a3..7a391aa5fc 100644
--- a/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs
+++ b/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs
@@ -123,6 +123,10 @@ public TestObjectDto(int ctorValue, int unknownValue = 10, int ctorValue2 = 100)
public TimeOnly DateTimeValueTargetTimeOnly { get; set; }
+ public string FormattedIntValue { get; set; } = string.Empty;
+
+ public string FormattedDateValue { get; set; } = string.Empty;
+
public int ExposePrivateValue => PrivateValue;
private int PrivateValue { get; set; }
diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs
index 9c3a3f2361..c860a9c05d 100644
--- a/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs
+++ b/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.IntegrationTests.Dto;
using Riok.Mapperly.IntegrationTests.Models;
@@ -17,6 +18,12 @@ namespace Riok.Mapperly.IntegrationTests.Mapper
#endif
public partial class TestMapper
{
+ [FormatProvider(Default = true)]
+ private readonly CultureInfo _formatDeCh = CultureInfo.GetCultureInfo("de-CH");
+
+ [FormatProvider]
+ private readonly CultureInfo _formatEnUs = CultureInfo.GetCultureInfo("en-US");
+
public partial int DirectInt(int value);
public partial long ImplicitCastInt(int value);
@@ -43,6 +50,13 @@ public TestObjectDto MapToDto(TestObject src)
[MapperIgnoreTarget(nameof(TestObjectDto.IgnoredStringValue))]
[MapperIgnoreTarget(nameof(TestObjectDto.IgnoredIntValue))]
[MapperIgnoreSource(nameof(TestObject.IgnoredIntValue))]
+ [MapProperty(nameof(TestObject.IntValue), nameof(TestObjectDto.FormattedIntValue), StringFormat = "C")]
+ [MapProperty(
+ nameof(TestObject.DateTimeValue),
+ nameof(TestObjectDto.FormattedDateValue),
+ StringFormat = "D",
+ FormatProvider = nameof(_formatEnUs)
+ )]
[MapProperty(nameof(TestObject.RenamedStringValue), nameof(TestObjectDto.RenamedStringValue2))]
[MapProperty(
new[] { nameof(TestObject.UnflatteningIdValue) },
diff --git a/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs b/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs
index bd797aa3c6..0c0af0ea83 100644
--- a/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs
+++ b/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs
@@ -116,6 +116,8 @@ public TestObject(int ctorValue, int unknownValue = 10, int ctorValue2 = 100)
public int IgnoredIntValue { get; set; }
+ public DateTime DateTimeValue { get; set; }
+
public DateTime DateTimeValueTargetDateOnly { get; set; }
public DateTime DateTimeValueTargetTimeOnly { get; set; }
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.RunMappingShouldWork_NET6_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.RunMappingShouldWork_NET6_0.verified.txt
index 4aa16dc034..d3bf52de8e 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.RunMappingShouldWork_NET6_0.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.RunMappingShouldWork_NET6_0.verified.txt
@@ -168,6 +168,7 @@
SubIntValue: 2,
BaseIntValue: 1
},
+ DateTimeValue: 2020-01-03 15:10:05 Utc,
DateTimeValueTargetDateOnly: 2020-01-03 15:10:05 Utc,
DateTimeValueTargetTimeOnly: 2020-01-03 15:10:05 Utc,
ExposePrivateValue: 18
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs
index 18bc0c942b..c23f6c8f9f 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs
@@ -95,6 +95,7 @@ public static partial class DeepCloningMapper
target.EnumRawValue = src.EnumRawValue;
target.EnumStringValue = src.EnumStringValue;
target.EnumReverseStringValue = src.EnumReverseStringValue;
+ target.DateTimeValue = src.DateTimeValue;
target.DateTimeValueTargetDateOnly = src.DateTimeValueTargetDateOnly;
target.DateTimeValueTargetTimeOnly = src.DateTimeValueTargetTimeOnly;
return target;
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork_NET6_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork_NET6_0.verified.txt
index 93274d630b..1adfb8a39b 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork_NET6_0.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork_NET6_0.verified.txt
@@ -48,6 +48,8 @@
EnumName: Value30,
EnumStringValue: 0,
EnumReverseStringValue: DtoValue3,
+ FormattedIntValue: CHF 0.00,
+ FormattedDateValue: Monday, January 1, 0001,
ExposePrivateValue: 16
},
SourceTargetSameObjectType: {
@@ -184,5 +186,7 @@
},
DateTimeValueTargetDateOnly: 2020-01-03,
DateTimeValueTargetTimeOnly: 3:10 PM,
+ FormattedIntValue: CHF 10.00,
+ FormattedDateValue: Friday, January 3, 2020,
ExposePrivateValue: 18
}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource_NET8_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource_NET8_0.verified.cs
index 1d345352d6..c56f5b81db 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource_NET8_0.verified.cs
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource_NET8_0.verified.cs
@@ -1,4 +1,4 @@
-//
+//
#nullable enable
namespace Riok.Mapperly.IntegrationTests.Mapper
{
@@ -137,6 +137,8 @@ public partial int ParseableInt(string value)
target.EnumReverseStringValue = MapToTestEnumDtoByValue(testObject.EnumReverseStringValue);
target.DateTimeValueTargetDateOnly = global::System.DateOnly.FromDateTime(testObject.DateTimeValueTargetDateOnly);
target.DateTimeValueTargetTimeOnly = global::System.TimeOnly.FromDateTime(testObject.DateTimeValueTargetTimeOnly);
+ target.FormattedIntValue = testObject.IntValue.ToString("C", _formatDeCh);
+ target.FormattedDateValue = testObject.DateTimeValue.ToString("D", _formatEnUs);
target.SetPrivateValue(DirectInt(testObject.GetPrivateValue()));
return target;
}
@@ -179,38 +181,38 @@ public partial int ParseableInt(string value)
target.StringNullableTargetNotNullable = dto.StringNullableTargetNotNullable;
target.SourceTargetSameObjectType = dto.SourceTargetSameObjectType;
target.MemoryValue = MapToStringArray(dto.MemoryValue.Span);
- target.StackValue = new global::System.Collections.Generic.Stack(global::System.Linq.Enumerable.Select(dto.StackValue, x => x.ToString()));
- target.QueueValue = new global::System.Collections.Generic.Queue(global::System.Linq.Enumerable.Select(dto.QueueValue, x => x.ToString()));
- target.ImmutableArrayValue = global::System.Collections.Immutable.ImmutableArray.ToImmutableArray(global::System.Linq.Enumerable.Select(dto.ImmutableArrayValue, x => x.ToString()));
- target.ImmutableListValue = global::System.Collections.Immutable.ImmutableList.ToImmutableList(global::System.Linq.Enumerable.Select(dto.ImmutableListValue, x => x.ToString()));
- target.ImmutableHashSetValue = global::System.Collections.Immutable.ImmutableHashSet.ToImmutableHashSet(global::System.Linq.Enumerable.Select(dto.ImmutableHashSetValue, x => x.ToString()));
- target.ImmutableQueueValue = global::System.Collections.Immutable.ImmutableQueue.CreateRange(global::System.Linq.Enumerable.Select(dto.ImmutableQueueValue, x => x.ToString()));
- target.ImmutableStackValue = global::System.Collections.Immutable.ImmutableStack.CreateRange(global::System.Linq.Enumerable.Select(dto.ImmutableStackValue, x => x.ToString()));
- target.ImmutableSortedSetValue = global::System.Collections.Immutable.ImmutableSortedSet.ToImmutableSortedSet(global::System.Linq.Enumerable.Select(dto.ImmutableSortedSetValue, x => x.ToString()));
- target.ImmutableDictionaryValue = global::System.Collections.Immutable.ImmutableDictionary.ToImmutableDictionary(dto.ImmutableDictionaryValue, x => x.Key.ToString(), x => x.Value.ToString());
- target.ImmutableSortedDictionaryValue = global::System.Collections.Immutable.ImmutableSortedDictionary.ToImmutableSortedDictionary(dto.ImmutableSortedDictionaryValue, x => x.Key.ToString(), x => x.Value.ToString());
+ target.StackValue = new global::System.Collections.Generic.Stack(global::System.Linq.Enumerable.Select(dto.StackValue, x => x.ToString(null, _formatDeCh)));
+ target.QueueValue = new global::System.Collections.Generic.Queue(global::System.Linq.Enumerable.Select(dto.QueueValue, x => x.ToString(null, _formatDeCh)));
+ target.ImmutableArrayValue = global::System.Collections.Immutable.ImmutableArray.ToImmutableArray(global::System.Linq.Enumerable.Select(dto.ImmutableArrayValue, x => x.ToString(null, _formatDeCh)));
+ target.ImmutableListValue = global::System.Collections.Immutable.ImmutableList.ToImmutableList(global::System.Linq.Enumerable.Select(dto.ImmutableListValue, x => x.ToString(null, _formatDeCh)));
+ target.ImmutableHashSetValue = global::System.Collections.Immutable.ImmutableHashSet.ToImmutableHashSet(global::System.Linq.Enumerable.Select(dto.ImmutableHashSetValue, x => x.ToString(null, _formatDeCh)));
+ target.ImmutableQueueValue = global::System.Collections.Immutable.ImmutableQueue.CreateRange(global::System.Linq.Enumerable.Select(dto.ImmutableQueueValue, x => x.ToString(null, _formatDeCh)));
+ target.ImmutableStackValue = global::System.Collections.Immutable.ImmutableStack.CreateRange(global::System.Linq.Enumerable.Select(dto.ImmutableStackValue, x => x.ToString(null, _formatDeCh)));
+ target.ImmutableSortedSetValue = global::System.Collections.Immutable.ImmutableSortedSet.ToImmutableSortedSet(global::System.Linq.Enumerable.Select(dto.ImmutableSortedSetValue, x => x.ToString(null, _formatDeCh)));
+ target.ImmutableDictionaryValue = global::System.Collections.Immutable.ImmutableDictionary.ToImmutableDictionary(dto.ImmutableDictionaryValue, x => x.Key.ToString(null, _formatDeCh), x => x.Value.ToString(null, _formatDeCh));
+ target.ImmutableSortedDictionaryValue = global::System.Collections.Immutable.ImmutableSortedDictionary.ToImmutableSortedDictionary(dto.ImmutableSortedDictionaryValue, x => x.Key.ToString(null, _formatDeCh), x => x.Value.ToString(null, _formatDeCh));
foreach (var item in dto.ExistingISet)
{
- target.ExistingISet.Add(item.ToString());
+ target.ExistingISet.Add(item.ToString(null, _formatDeCh));
}
target.ExistingHashSet.EnsureCapacity(dto.ExistingHashSet.Count + target.ExistingHashSet.Count);
foreach (var item1 in dto.ExistingHashSet)
{
- target.ExistingHashSet.Add(item1.ToString());
+ target.ExistingHashSet.Add(item1.ToString(null, _formatDeCh));
}
foreach (var item2 in dto.ExistingSortedSet)
{
- target.ExistingSortedSet.Add(item2.ToString());
+ target.ExistingSortedSet.Add(item2.ToString(null, _formatDeCh));
}
target.ExistingList.EnsureCapacity(dto.ExistingList.Count + target.ExistingList.Count);
foreach (var item3 in dto.ExistingList)
{
- target.ExistingList.Add(item3.ToString());
+ target.ExistingList.Add(item3.ToString(null, _formatDeCh));
}
- target.ISet = global::System.Linq.Enumerable.ToHashSet(global::System.Linq.Enumerable.Select(dto.ISet, x => x.ToString()));
- target.IReadOnlySet = global::System.Linq.Enumerable.ToHashSet(global::System.Linq.Enumerable.Select(dto.IReadOnlySet, x => x.ToString()));
- target.HashSet = global::System.Linq.Enumerable.ToHashSet(global::System.Linq.Enumerable.Select(dto.HashSet, x => x.ToString()));
- target.SortedSet = new global::System.Collections.Generic.SortedSet(global::System.Linq.Enumerable.Select(dto.SortedSet, x => x.ToString()));
+ target.ISet = global::System.Linq.Enumerable.ToHashSet(global::System.Linq.Enumerable.Select(dto.ISet, x => x.ToString(null, _formatDeCh)));
+ target.IReadOnlySet = global::System.Linq.Enumerable.ToHashSet(global::System.Linq.Enumerable.Select(dto.IReadOnlySet, x => x.ToString(null, _formatDeCh)));
+ target.HashSet = global::System.Linq.Enumerable.ToHashSet(global::System.Linq.Enumerable.Select(dto.HashSet, x => x.ToString(null, _formatDeCh)));
+ target.SortedSet = new global::System.Collections.Generic.SortedSet(global::System.Linq.Enumerable.Select(dto.SortedSet, x => x.ToString(null, _formatDeCh)));
target.EnumValue = (global::Riok.Mapperly.IntegrationTests.Models.TestEnum)dto.EnumValue;
target.FlagsEnumValue = (global::Riok.Mapperly.IntegrationTests.Models.TestFlagsEnum)dto.FlagsEnumValue;
target.EnumName = (global::Riok.Mapperly.IntegrationTests.Models.TestEnum)dto.EnumName;
@@ -325,7 +327,7 @@ private partial int PrivateDirectInt(int value)
public partial (string X, string Y) MapAliasedTuple((int X, int Y) source)
{
- var target = (X: source.X.ToString(), Y: source.Y.ToString());
+ var target = (X: source.X.ToString(null, _formatDeCh), Y: source.Y.ToString(null, _formatDeCh));
return target;
}
@@ -413,7 +415,7 @@ private string MapToString(global::Riok.Mapperly.IntegrationTests.Models.TestEnu
private (string A, string) MapToValueTuple1((int A, int) source)
{
- var target = (A: source.A.ToString(), source.Item2.ToString());
+ var target = (A: source.A.ToString(null, _formatDeCh), source.Item2.ToString(null, _formatDeCh));
return target;
}
@@ -432,7 +434,7 @@ private string[] MapToStringArray(global::System.ReadOnlySpan source)
var target = new string[source.Length];
for (var i = 0; i < source.Length; i++)
{
- target[i] = source[i].ToString();
+ target[i] = source[i].ToString(null, _formatDeCh);
}
return target;
}
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunExtensionMappingShouldWork_NET6_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunExtensionMappingShouldWork_NET6_0.verified.txt
index 9e396839a6..17153c34e0 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunExtensionMappingShouldWork_NET6_0.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunExtensionMappingShouldWork_NET6_0.verified.txt
@@ -43,6 +43,8 @@
EnumName: Value30,
EnumStringValue: 0,
EnumReverseStringValue: DtoValue3,
+ FormattedIntValue: ,
+ FormattedDateValue: ,
ExposePrivateValue: 26
},
SourceTargetSameObjectType: {
@@ -179,5 +181,7 @@
},
DateTimeValueTargetDateOnly: 2020-01-03,
DateTimeValueTargetTimeOnly: 3:10 PM,
+ FormattedIntValue: ,
+ FormattedDateValue: ,
ExposePrivateValue: 28
}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunMappingShouldWork_NET6_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunMappingShouldWork_NET6_0.verified.txt
index 0e81ca4261..4b1ab3647a 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunMappingShouldWork_NET6_0.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunMappingShouldWork_NET6_0.verified.txt
@@ -48,6 +48,8 @@
EnumName: Value30,
EnumStringValue: 0,
EnumReverseStringValue: DtoValue3,
+ FormattedIntValue: ,
+ FormattedDateValue: ,
ExposePrivateValue: 26
},
SourceTargetSameObjectType: {
@@ -184,5 +186,7 @@
},
DateTimeValueTargetDateOnly: 2020-01-03,
DateTimeValueTargetTimeOnly: 3:10 PM,
+ FormattedIntValue: ,
+ FormattedDateValue: ,
ExposePrivateValue: 28
}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/Mapping/ToStringFormattedTest.cs b/test/Riok.Mapperly.Tests/Mapping/ToStringFormattedTest.cs
index fe791fba1f..f6bc712845 100644
--- a/test/Riok.Mapperly.Tests/Mapping/ToStringFormattedTest.cs
+++ b/test/Riok.Mapperly.Tests/Mapping/ToStringFormattedTest.cs
@@ -54,7 +54,31 @@ public void RecordMultiplePropertiesToStringWithDifferentFormats()
}
[Fact]
- public void ClassToStringWithoutFormatParameterShouldDiagnostic()
+ public void ClassToStringWithIFormattable()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [MapProperty("Value", "Value", StringFormat = "C")]
+ partial B Map(A source);",
+ """,
+ "class A { public C Value { get; set; } }",
+ "class B { public string Value { get; set; } }",
+ "class C : IFormattable { public string ToString(string? format, IFormatProvider? formatProvider) => format; }"
+ );
+ TestHelper
+ .GenerateMapper(source)
+ .Should()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value.ToString("C", null);
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void ClassToStringWithoutIFormattableShouldDiagnostic()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""
@@ -70,7 +94,7 @@ public void ClassToStringWithoutFormatParameterShouldDiagnostic()
.Should()
.HaveDiagnostic(
DiagnosticDescriptors.SourceDoesNotImplementIFormattable,
- "The source type C does not implement IFormattable, string format cannot be applied"
+ "The source type C does not implement IFormattable, string format and format provider cannot be applied"
)
.HaveAssertedAllDiagnostics()
.HaveSingleMethodBody(
@@ -81,4 +105,344 @@ public void ClassToStringWithoutFormatParameterShouldDiagnostic()
"""
);
}
+
+ [Fact]
+ public void ClassToStringWithFormatProviderWithoutIFormattableShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [FormatProvider]
+ private readonly IFormatProvider _formatter = CultureInfo.GetCultureInfo("de-CH");
+
+ [MapProperty("Value", "Value", FormatProvider = "_formatter")]
+ partial B Map(A source);",
+ """,
+ "class A { public C Value { get; set; } }",
+ "class B { public string Value { get; set; } }",
+ "class C {}"
+ );
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(
+ DiagnosticDescriptors.SourceDoesNotImplementIFormattable,
+ "The source type C does not implement IFormattable, string format and format provider cannot be applied"
+ )
+ .HaveAssertedAllDiagnostics()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value.ToString();
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void ClassWithDefaultFormatProvider()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [FormatProvider(Default = true)]
+ private readonly IFormatProvider _formatter = CultureInfo.GetCultureInfo("de-CH");
+
+ partial B Map(A source);",
+ """,
+ "class A { public DateTime Value { get; set; } }",
+ "class B { public string Value { get; set; } }"
+ );
+ TestHelper
+ .GenerateMapper(source)
+ .Should()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value.ToString(null, _formatter);
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void ClassWithDefaultFormatProviderAndStringFormat()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [FormatProvider(Default = true)]
+ private readonly IFormatProvider _formatter = CultureInfo.GetCultureInfo("de-CH");
+
+ [MapProperty("Value", "Value", StringFormat = "dd.MM.yyyy")]
+ partial B Map(A source);",
+ """,
+ "class A { public DateTime Value { get; set; } }",
+ "class B { public string Value { get; set; } }"
+ );
+ TestHelper
+ .GenerateMapper(source)
+ .Should()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value.ToString("dd.MM.yyyy", _formatter);
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void ClassWithDefaultAndExplicitFormatProviderAndStringFormat()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [FormatProvider(Default = true)]
+ private readonly IFormatProvider _formatter = CultureInfo.GetCultureInfo("de-CH");
+
+ [FormatProvider]
+ private readonly IFormatProvider _formatterEN = CultureInfo.GetCultureInfo("en-US");
+
+ [MapProperty("Value", "Value", StringFormat = "yyyy-MM-dd", FormatProvider = nameof(_formatterEN))]
+ partial B Map(A source);",
+ """,
+ "class A { public DateTime Value { get; set; } }",
+ "class B { public string Value { get; set; } }"
+ );
+ TestHelper
+ .GenerateMapper(source)
+ .Should()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value.ToString("yyyy-MM-dd", _formatterEN);
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void ClassWithExplicitFormatProvider()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [FormatProvider]
+ private readonly IFormatProvider _formatterEN = CultureInfo.GetCultureInfo("en-US");
+
+ [MapProperty("Value", "Value", StringFormat = "yyyy-MM-dd", FormatProvider = nameof(_formatterEN))]
+ partial B Map(A source);",
+ """,
+ "class A { public DateTime Value { get; set; } public DateTime Value2 { get; set; } }",
+ "class B { public string Value { get; set; } public string Value2 { get; set; } }"
+ );
+ TestHelper
+ .GenerateMapper(source)
+ .Should()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value.ToString("yyyy-MM-dd", _formatterEN);
+ target.Value2 = source.Value2.ToString();
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void ClassWithDefaultFormatProviderDisabledNullable()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [FormatProvider(Default = true)]
+ private readonly IFormatProvider _formatter = CultureInfo.GetCultureInfo("de-CH");
+
+ partial B Map(A source);",
+ """,
+ "class A { public DateTime Value { get; set; } }",
+ "class B { public string Value { get; set; } }"
+ );
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.DisabledNullable)
+ .Should()
+ .HaveSingleMethodBody(
+ """
+ if (source == null)
+ return default;
+ var target = new global::B();
+ target.Value = source.Value.ToString(null, _formatter);
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void NullableFormatProvider()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [FormatProvider(Default = true)]
+ private readonly IFormatProvider? _formatter = CultureInfo.GetCultureInfo("de-CH");
+
+ [MapProperty("Value", "Value", StringFormat = "dd.MM.yyyy")]
+ partial B Map(A source);",
+ """,
+ "class A { public DateTime Value { get; set; } }",
+ "class B { public string Value { get; set; } }"
+ );
+ TestHelper
+ .GenerateMapper(source)
+ .Should()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value.ToString("dd.MM.yyyy", _formatter);
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void FormatProviderStaticInStaticMapper()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [FormatProvider(Default = true)]
+ private static readonly IFormatProvider _formatter = CultureInfo.GetCultureInfo("de-CH");
+ static partial B Map(A source);
+ """,
+ TestSourceBuilderOptions.AsStatic,
+ "class A { public DateTime Value { get; set; } }",
+ "class B { public string Value { get; set; } }"
+ );
+ TestHelper
+ .GenerateMapper(source)
+ .Should()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value.ToString(null, _formatter);
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void FormatProviderStaticInStaticMethodsMapper()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [FormatProvider(Default = true)]
+ private static readonly IFormatProvider _formatter = CultureInfo.GetCultureInfo("de-CH");
+ static partial B Map(A source);
+ """,
+ "class A { public DateTime Value { get; set; } }",
+ "class B { public string Value { get; set; } }"
+ );
+ TestHelper
+ .GenerateMapper(source)
+ .Should()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value.ToString(null, _formatter);
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void FormatProviderStaticInMapperShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [FormatProvider(Default = true)]
+ private static readonly IFormatProvider _formatter = CultureInfo.GetCultureInfo("de-CH");
+ partial B Map(A source);
+ """,
+ "class A { public DateTime Value { get; set; } }",
+ "class B { public string Value { get; set; } }"
+ );
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.InvalidFormatProviderSignature, "The format provider _formatter has an invalid signature")
+ .HaveAssertedAllDiagnostics();
+ }
+
+ [Fact]
+ public void FormatProviderStaticShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [FormatProvider(Default = true)]
+ private static readonly IFormatProvider _formatter = CultureInfo.GetCultureInfo("de-CH");
+
+ [MapProperty("Value", "Value", StringFormat = "dd.MM.yyyy")]
+ partial B Map(A source);",
+ """,
+ "class A { public DateTime Value { get; set; } }",
+ "class B { public string Value { get; set; } }"
+ );
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.InvalidFormatProviderSignature, "The format provider _formatter has an invalid signature")
+ .HaveAssertedAllDiagnostics()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value.ToString("dd.MM.yyyy", null);
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void UnknownFormatProviderShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [FormatProvider(Default = true)]
+ private readonly IFormatProvider _formatter = CultureInfo.GetCultureInfo("de-CH");
+
+ [MapProperty("Value", "Value", FormatProvider = "fooBar")]
+ partial B Map(A source);",
+ """,
+ "class A { public DateTime Value { get; set; } }",
+ "class B { public string Value { get; set; } }"
+ );
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(
+ DiagnosticDescriptors.FormatProviderNotFound,
+ "The format provider fooBar could not be found, make sure it is annotated with FormatProviderAttribute"
+ )
+ .HaveAssertedAllDiagnostics();
+ }
+
+ [Fact]
+ public void DuplicatedDefaultFormatProviderShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ [FormatProvider(Default = true)]
+ private readonly IFormatProvider _formatter = CultureInfo.GetCultureInfo("de-CH");
+
+ [FormatProvider(Default = true)]
+ private readonly IFormatProvider _formatterDe = CultureInfo.GetCultureInfo("de-DE");
+
+ partial B Map(A source);",
+ """,
+ "class A { public DateTime Value { get; set; } }",
+ "class B { public string Value { get; set; } }"
+ );
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(
+ DiagnosticDescriptors.MultipleDefaultFormatProviders,
+ "Multiple default format providers found, only one is allowed"
+ )
+ .HaveAssertedAllDiagnostics();
+ }
+
+ // TODO test diagnostics
}
diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs
index 62579524b3..26e715f305 100644
--- a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs
+++ b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs
@@ -48,7 +48,7 @@ public static string MapperWithBody([StringSyntax(StringSyntax.CSharp)] string b
{{(options.Namespace != null ? $"namespace {options.Namespace};" : string.Empty)}}
{{BuildAttribute(options)}}
- public partial class {{options.MapperClassName}}
+ public {{(options.Static ? "static " : "")}}partial class {{options.MapperClassName}}
{
{{body}}
}
diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
index 0060d5008d..737294f945 100644
--- a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
+++ b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
@@ -16,12 +16,14 @@ public record TestSourceBuilderOptions(
bool? EnumMappingIgnoreCase = null,
IgnoreObsoleteMembersStrategy? IgnoreObsoleteMembersStrategy = null,
RequiredMappingStrategy? RequiredMappingStrategy = null,
- MemberVisibility? IncludedMembers = null
+ MemberVisibility? IncludedMembers = null,
+ bool Static = false
)
{
public const string DefaultMapperClassName = "Mapper";
public static readonly TestSourceBuilderOptions Default = new();
+ public static readonly TestSourceBuilderOptions AsStatic = new(Static: true);
public static readonly TestSourceBuilderOptions WithDeepCloning = new(UseDeepCloning: true);
public static readonly TestSourceBuilderOptions WithReferenceHandling = new(UseReferenceHandling: true);