fix resource class name generation -> 0.0.3

This commit is contained in:
2023-05-19 18:43:41 +00:00
parent 78a330b68b
commit 37a7c50e4d
9 changed files with 255 additions and 98 deletions

197
Readme.md
View File

@@ -3,8 +3,10 @@
## What is ResourceString.Net? ## What is ResourceString.Net?
ResourceString.Net is a powerful .NET library that allows you to work with string resources in a type-safe manner. ResourceString.Net is a powerful .NET library that allows you to work with string resources in a type-safe manner.
It leverages the `resx` file in your project and utilizes a [c# source code generator](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) to create a comprehensive API. It leverages the `resx` file in your project and utilizes a [c# source code generator](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) to create a comprehensive API. No [`Designer.cs`](https://learn.microsoft.com/en-us/dotnet/framework/tools/resgen-exe-resource-file-generator?source=recommendations) or [`T4 Text Templates`](https://learn.microsoft.com/en-us/visualstudio/modeling/code-generation-and-t4-text-templates?) filer for resources are required any more.
With ResourceString.Net, you can handle resource strings as "multi-language strings" (see [The ResourceString-Classes](#the-resourcestring-classes)) instead of built-in strings. With ResourceString.Net, you can handle resource strings as "multi-language strings" (see [The ResourceString-Classes](#the-resourcestring-classes)) instead of built-in strings.
This provides the ability to switch languages during runtime without the need to rerun string factory methods. This provides the ability to switch languages during runtime without the need to rerun string factory methods.
Additionally, ResourceString.Net ensures that formatted strings have methods with the correct number of expected parameters. Additionally, ResourceString.Net ensures that formatted strings have methods with the correct number of expected parameters.
@@ -39,7 +41,7 @@ echo "<?xml version='1.0' encoding='utf-8'?>
<value>Hello {0}</value> <value>Hello {0}</value>
<comment>0 = name</comment> <comment>0 = name</comment>
</data> </data>
<data name='World' type='System.String'> <data name='World'>
<value>World</value> <value>World</value>
</data> </data>
</root> </root>
@@ -81,53 +83,166 @@ namespace MyTestConsoleApp
{ {
internal static class Resources internal static class Resources
{ {
#region ResourceManager
#region ResourceManager private static readonly Type _Type = typeof(Resources);
private static readonly Lazy<ResourceManager> _ResourceManager = new Lazy<ResourceManager>(
() => new ResourceManager("MyTestConsoleApp.Resources" ?? string.Empty, _Type.Assembly),
LazyThreadSafetyMode.PublicationOnly
);
private static readonly Type _Type = typeof(Resources); public static ResourceManager ResourceManager => _ResourceManager.Value;
#endregion // ResourceManager
private static readonly Lazy<ResourceManager> _ResourceManager = new Lazy<ResourceManager>( #region Greetings
() => new ResourceManager(_Type.FullName ?? string.Empty, _Type.Assembly),
LazyThreadSafetyMode.PublicationOnly
);
public static ResourceManager ResourceManager => _ResourceManager.Value; internal static class Greetings
{
private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>(
() => new ResourceManagerString("Greetings", ResourceManager, CultureInfo.CurrentCulture),
LazyThreadSafetyMode.PublicationOnly
);
#endregion // ResourceManager public static IResourceString Format => LazyFormat.Value;
public static IResourceString From(IResourceString name) => new FormattedResourceString(
Format,
name
);
internal static class Greetings }
{
private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>(
() => new ResourceManagerString("Greetings", ResourceManager, CultureInfo.CurrentCulture),
LazyThreadSafetyMode.PublicationOnly
);
public static IResourceString Format => LazyFormat.Value; #endregion // Greetings
public static IResourceString From(IResourceString name) => new FormattedResourceString(
Format,
name
);
}
#region World
private static readonly Lazy<IResourceString> LazyWorld = new Lazy<IResourceString>( #region World
() => new ResourceManagerString("World", ResourceManager, CultureInfo.CurrentCulture),
LazyThreadSafetyMode.PublicationOnly
);
public static IResourceString World => LazyWorld.Value; private static readonly Lazy<IResourceString> LazyWorld = new Lazy<IResourceString>(
() => new ResourceManagerString("World", ResourceManager, CultureInfo.CurrentCulture),
LazyThreadSafetyMode.PublicationOnly
);
#endregion // World public static IResourceString World => LazyWorld.Value;
#endregion // World
} }
} }
``` ```
### Add Multi-Languages
Run the following script to add a language specific resource file and add a culture change during runtime:
```sh
echo "<?xml version='1.0' encoding='utf-8'?>
<root>
<data name='Greetings'>
<value>Hallo {0}</value>
<comment>0 = name</comment>
</data>
<data name='World'>
<value>Welt</value>
</data>
</root>
" > Resources.de.resx
echo "
Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo(\"de-DE\");
Console.WriteLine(message.Value);
" >> Program.cs
dotnet run
# Expected output: Hello World \n Hallo Welt
```
In the code from `Program.cs` you can see there is explicit rebuild of the format string required after the culture switch.
The `IResourceString` object themselves are rebuilding the culture specific string.
## How it works
The ResourceString.Net source code generator operates by reading all `AdditionalFiles` with the `.resx` file extension and generating a static class based on the specified `data` elements within the file.
For example, given the following XML element:
```xml
<data name='World'>
<value>Welt</value>
</data>
```
the source code generator transforms it into the following `C#` class members:
```cs
#region World
private static readonly Lazy<IResourceString> LazyWorld = new Lazy<IResourceString>(
() => new ResourceManagerString("World", ResourceManager, CultureInfo.CurrentCulture),
LazyThreadSafetyMode.PublicationOnly
);
public static IResourceString World => LazyWorld.Value;
#endregion // World
```
If an element contains a format string, such as:
```xml
<data name='Greetings'>
<value>Hello {0} and {1}</value>
</data>
```
the generator generates following code to support the formatted string:
```cs
#region Greetings
internal static class Greetings
{
private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>(
() => new ResourceManagerString("Greetings", ResourceManager, CultureInfo.CurrentCulture),
LazyThreadSafetyMode.PublicationOnly
);
public static IResourceString Format => LazyFormat.Value;
public static IResourceString From(IResourceString p1, IResourceString p2) => new FormattedResourceString(
Format,
p1,
p2
);
}
#endregion // Greetings
```
In cases where the element with the format string includes a comment element like:
```xml
<data name='Greetings'>
<value>Hello {0} and {1}</value>
<comment>0 = name, 1 = otherName </comment>
</data>
```
the source generator extracts the parameter names from the comment instead of using generic names:
```cs
public static IResourceString From(IResourceString name, IResourceString otherName) => new FormattedResourceString(
Format,
name,
otherName
);
```
This allows for more descriptive parameter names in the generated code.
## The ResourceString-Classes ## The ResourceString-Classes
### IResourceString ### IResourceString
@@ -212,13 +327,13 @@ You can redirect the output to a file if desired.
## Third party packages ## Third party packages
| Package | Version | | Package | Version |
| ------------------------------------- | ------- | | --------------------------------------------------------------------------------------------------- | ------- |
| Microsoft.CodeAnalysis.Analyzers | 3.3.4 | | [Microsoft.CodeAnalysis.Analyzers](https://www.nuget.org/packages/Microsoft.CodeAnalysis.Analyzers) | 3.3.4 |
| Microsoft.CodeAnalysis.CSharp | 4.3.0 | | [Microsoft.CodeAnalysis.CSharp](https://www.nuget.org/packages/Microsoft.CodeAnalysis.CSharp) | 4.3.0 |
| NETStandard.Library (Auto-referenced) | 2.0.3 | | [NETStandard.Library](https://www.nuget.org/packages/NETStandard.Library) | 2.0.3 |
| LanguageExt.Core | 4.4.3 | | [LanguageExt.Core](https://www.nuget.org/packages/LanguageExt.Core) | 4.4.3 |
| System.Resources.Extensions | 7.0.0 | | [System.Resources.Extensions](https://www.nuget.org/packages/System.Resources.Extensions) | 7.0.0 |
## Development Notes ## Development Notes

View File

@@ -3,14 +3,21 @@ using ResourceString.Net.Logic.Parsers.Resx;
var sourceFile = args.First(); var sourceFile = args.First();
var namespaceString = args.Skip(1).FirstOrDefault() ?? "Properties"; var namespaceString = args.Skip(1).FirstOrDefault() ?? "Properties";
var className = args.Skip(2).FirstOrDefault() ?? "Resources";
var resourceFileName = args.Skip(2).FirstOrDefault() ?? "Resources";
var className = CodeSnippetFactory.TransformToClassName(
resourceFileName
);
var result = Parser.TryParse(System.IO.File.ReadAllText(sourceFile)).Match( var result = Parser.TryParse(System.IO.File.ReadAllText(sourceFile)).Match(
Some: v => CodeSnippetFactory.CreateResourceClassCodeSnippet( Some: v => CodeSnippetFactory.CreateResourceClassCodeSnippet(
namespaceString, CodeSnippetFactory.TransformToNamespace(namespaceString),
className, className,
CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet(className), CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet(
v.Resources $"{namespaceString}.{className}",
className
),
v.Resources
), ),
None: () => throw new InvalidOperationException() None: () => throw new InvalidOperationException()
); );

View File

@@ -68,10 +68,12 @@ public class LogicResourcesTests
var rm = Properties.Resources.ResourceManager; var rm = Properties.Resources.ResourceManager;
Assert.AreEqual( Assert.AreEqual(
CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet( CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet(
"Resources",
"Resources" "Resources"
).Value, ).Value,
string.Format( string.Format(
rm.GetString("ResourceManagerMemberTemplate") ?? string.Empty, rm.GetString("ResourceManagerMemberTemplate") ?? string.Empty,
"Resources",
"Resources" "Resources"
) )
); );
@@ -83,6 +85,7 @@ public class LogicResourcesTests
var rm = Properties.Resources.ResourceManager; var rm = Properties.Resources.ResourceManager;
var rmSnippet = CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet( var rmSnippet = CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet(
"Resources",
"Resources" "Resources"
); );

View File

@@ -36,10 +36,10 @@ namespace TestNameSpace
#region ResourceManager #region ResourceManager
private static readonly Type _Type = typeof(ResourceManager); private static readonly Type _Type = typeof(TestResourceClass);
private static readonly Lazy<ResourceManager> _ResourceManager = new Lazy<ResourceManager>( private static readonly Lazy<ResourceManager> _ResourceManager = new Lazy<ResourceManager>(
() => new ResourceManager(_Type.FullName ?? string.Empty, _Type.Assembly), () => new ResourceManager(""ResourceManager"" ?? string.Empty, _Type.Assembly),
LazyThreadSafetyMode.PublicationOnly LazyThreadSafetyMode.PublicationOnly
); );
@@ -175,7 +175,7 @@ namespace TestNameSpace
Some: v => CodeSnippetFactory.CreateResourceClassCodeSnippet( Some: v => CodeSnippetFactory.CreateResourceClassCodeSnippet(
"TestNameSpace", "TestNameSpace",
"TestResourceClass", "TestResourceClass",
CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet("ResourceManager"), CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet("ResourceManager", "TestResourceClass"),
v.Resources v.Resources
), ),
None: () => throw new InvalidOperationException() None: () => throw new InvalidOperationException()

View File

@@ -2,15 +2,19 @@ using ResourceString.Net.Logic.DomainObjects.Resx;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
namespace ResourceString.Net.Logic.Factories; namespace ResourceString.Net.Logic.Factories;
internal static class CodeSnippetFactory internal static class CodeSnippetFactory
{ {
public static IResourceString CreateResourceMangerMemberCodeSnippet(string typeName) public static IResourceString CreateResourceMangerMemberCodeSnippet(
string resourceName,
string className)
{ {
return Properties.Resources.ResourceManagerMemberTemplate.From( return Properties.Resources.ResourceManagerMemberTemplate.From(
LiteralString.Factory(typeName) LiteralString.Factory(resourceName),
LiteralString.Factory(className)
); );
} }
@@ -24,8 +28,7 @@ internal static class CodeSnippetFactory
public static IEnumerable<IResourceString> CreateMemberCodeSnippets( public static IEnumerable<IResourceString> CreateMemberCodeSnippets(
IEnumerable<Resource> resources, IEnumerable<Resource> resources,
IResourceString resourceManagerName IResourceString resourceManagerName)
)
{ {
if (resources is null) if (resources is null)
{ {
@@ -95,8 +98,7 @@ internal static class CodeSnippetFactory
string namespaceString, string namespaceString,
string resourceClassName, string resourceClassName,
IResourceString resourceManagerSnippet, IResourceString resourceManagerSnippet,
IEnumerable<IResourceString> memberSnippets IEnumerable<IResourceString> memberSnippets)
)
{ {
return Properties.Resources.ResourcesClassTemplate.From( return Properties.Resources.ResourcesClassTemplate.From(
LiteralString.Factory(namespaceString), LiteralString.Factory(namespaceString),
@@ -123,4 +125,28 @@ internal static class CodeSnippetFactory
CreateMemberCodeSnippets(resources) CreateMemberCodeSnippets(resources)
); );
} }
public static string TransformToClassName(string input)
{
// Remove invalid characters and replace them with underscores
string sanitizedInput = Regex.Replace(
input?.Trim() ?? string.Empty,
@"[^a-zA-Z0-9_]", "_"
);
// Remove leading digits if the string starts with a digit
return Regex.Replace(sanitizedInput, @"^\d+", "");
}
public static string TransformToNamespace(string input)
{
// Remove invalid characters and replace them with dots
string sanitizedInput = Regex.Replace(
input?.Trim() ?? string.Empty,
@"[^a-zA-Z0-9_.]", "."
);
// Remove leading dots if present
return sanitizedInput.TrimStart('.');
}
} }

View File

@@ -12,18 +12,18 @@ namespace ResourceString.Net.Logic.Properties
#region ResourceManager #region ResourceManager
private static readonly Type _Type = typeof(Resources); private static readonly Type _Type = typeof(Resources);
private static readonly Lazy<ResourceManager> _ResourceManager = new Lazy<ResourceManager>( private static readonly Lazy<ResourceManager> _ResourceManager = new Lazy<ResourceManager>(
() => new ResourceManager(_Type.FullName ?? string.Empty, _Type.Assembly), () => new ResourceManager("ResourceString.Net.Logic.Properties.Resources" ?? string.Empty, _Type.Assembly),
LazyThreadSafetyMode.PublicationOnly LazyThreadSafetyMode.PublicationOnly
); );
public static ResourceManager ResourceManager => _ResourceManager.Value; public static ResourceManager ResourceManager => _ResourceManager.Value;
#endregion // ResourceManager #endregion // ResourceManager
internal static class ResourceStringMembers
internal static class ResourceStringMembers
{ {
private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>( private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>(
() => new ResourceManagerString("ResourceStringMembers", ResourceManager, CultureInfo.CurrentCulture), () => new ResourceManagerString("ResourceStringMembers", ResourceManager, CultureInfo.CurrentCulture),
@@ -31,16 +31,16 @@ namespace ResourceString.Net.Logic.Properties
); );
public static IResourceString Format => LazyFormat.Value; public static IResourceString Format => LazyFormat.Value;
public static IResourceString From(IResourceString resourceId, IResourceString resourceManagerPropertyName) => new FormattedResourceString( public static IResourceString From(IResourceString resourceId, IResourceString resourceManagerPropertyName) => new FormattedResourceString(
Format, Format,
resourceId, resourceId,
resourceManagerPropertyName resourceManagerPropertyName
); );
} }
internal static class ResourceFormatClassMembers internal static class ResourceFormatClassMembers
{ {
private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>( private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>(
() => new ResourceManagerString("ResourceFormatClassMembers", ResourceManager, CultureInfo.CurrentCulture), () => new ResourceManagerString("ResourceFormatClassMembers", ResourceManager, CultureInfo.CurrentCulture),
@@ -48,17 +48,17 @@ namespace ResourceString.Net.Logic.Properties
); );
public static IResourceString Format => LazyFormat.Value; public static IResourceString Format => LazyFormat.Value;
public static IResourceString From(IResourceString resourceId, IResourceString resourceManagerPropertyName, IResourceString fromMethodDefinition) => new FormattedResourceString( public static IResourceString From(IResourceString resourceId, IResourceString resourceManagerPropertyName, IResourceString fromMethodDefinition) => new FormattedResourceString(
Format, Format,
resourceId, resourceId,
resourceManagerPropertyName, resourceManagerPropertyName,
fromMethodDefinition fromMethodDefinition
); );
} }
internal static class ResourceFormatClassFromMethod internal static class ResourceFormatClassFromMethod
{ {
private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>( private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>(
() => new ResourceManagerString("ResourceFormatClassFromMethod", ResourceManager, CultureInfo.CurrentCulture), () => new ResourceManagerString("ResourceFormatClassFromMethod", ResourceManager, CultureInfo.CurrentCulture),
@@ -66,16 +66,16 @@ namespace ResourceString.Net.Logic.Properties
); );
public static IResourceString Format => LazyFormat.Value; public static IResourceString Format => LazyFormat.Value;
public static IResourceString From(IResourceString fromMethodSignature, IResourceString parameterNames) => new FormattedResourceString( public static IResourceString From(IResourceString fromMethodSignature, IResourceString parameterNames) => new FormattedResourceString(
Format, Format,
fromMethodSignature, fromMethodSignature,
parameterNames parameterNames
); );
} }
internal static class ResourcesClassTemplate internal static class ResourcesClassTemplate
{ {
private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>( private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>(
() => new ResourceManagerString("ResourcesClassTemplate", ResourceManager, CultureInfo.CurrentCulture), () => new ResourceManagerString("ResourcesClassTemplate", ResourceManager, CultureInfo.CurrentCulture),
@@ -83,18 +83,18 @@ namespace ResourceString.Net.Logic.Properties
); );
public static IResourceString Format => LazyFormat.Value; public static IResourceString Format => LazyFormat.Value;
public static IResourceString From(IResourceString ns, IResourceString className, IResourceString resourceManagerRegion, IResourceString resourceRegions) => new FormattedResourceString( public static IResourceString From(IResourceString ns, IResourceString className, IResourceString resourceManagerRegion, IResourceString resourceRegions) => new FormattedResourceString(
Format, Format,
ns, ns,
className, className,
resourceManagerRegion, resourceManagerRegion,
resourceRegions resourceRegions
); );
} }
internal static class ResourceManagerMemberTemplate internal static class ResourceManagerMemberTemplate
{ {
private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>( private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>(
() => new ResourceManagerString("ResourceManagerMemberTemplate", ResourceManager, CultureInfo.CurrentCulture), () => new ResourceManagerString("ResourceManagerMemberTemplate", ResourceManager, CultureInfo.CurrentCulture),
@@ -102,14 +102,15 @@ namespace ResourceString.Net.Logic.Properties
); );
public static IResourceString Format => LazyFormat.Value; public static IResourceString Format => LazyFormat.Value;
public static IResourceString From(IResourceString resourceManagerTypeName) => new FormattedResourceString( public static IResourceString From(IResourceString className, IResourceString resourceManagerTypeName) => new FormattedResourceString(
Format, Format,
className,
resourceManagerTypeName resourceManagerTypeName
); );
} }
#region DefaultPropertyName_ResourceManager #region DefaultPropertyName_ResourceManager
private static readonly Lazy<IResourceString> LazyDefaultPropertyName_ResourceManager = new Lazy<IResourceString>( private static readonly Lazy<IResourceString> LazyDefaultPropertyName_ResourceManager = new Lazy<IResourceString>(
@@ -120,6 +121,6 @@ namespace ResourceString.Net.Logic.Properties
public static IResourceString DefaultPropertyName_ResourceManager => LazyDefaultPropertyName_ResourceManager.Value; public static IResourceString DefaultPropertyName_ResourceManager => LazyDefaultPropertyName_ResourceManager.Value;
#endregion // DefaultPropertyName_ResourceManager #endregion // DefaultPropertyName_ResourceManager
} }
} }

View File

@@ -62,10 +62,10 @@ namespace {0}
<value> <value>
#region ResourceManager #region ResourceManager
private static readonly Type _Type = typeof({0}); private static readonly Type _Type = typeof({1});
private static readonly Lazy&lt;ResourceManager> _ResourceManager = new Lazy&lt;ResourceManager>( private static readonly Lazy&lt;ResourceManager> _ResourceManager = new Lazy&lt;ResourceManager>(
() => new ResourceManager(_Type.FullName ?? string.Empty, _Type.Assembly), () => new ResourceManager("{0}" ?? string.Empty, _Type.Assembly),
LazyThreadSafetyMode.PublicationOnly LazyThreadSafetyMode.PublicationOnly
); );
@@ -73,7 +73,7 @@ namespace {0}
#endregion // ResourceManager #endregion // ResourceManager
</value> </value>
<comment>0 = ResourceManagerTypeName</comment> <comment>0 = ResourceManagerTypeName, 1 = ClassName</comment>
</data> </data>
<data name="DefaultPropertyName_ResourceManager"> <data name="DefaultPropertyName_ResourceManager">
<value>ResourceManager</value> <value>ResourceManager</value>

View File

@@ -36,14 +36,19 @@ public class Generator : IIncrementalGenerator
? t.assembly.Name ? t.assembly.Name
: $"{t.assembly.Name}.{relativeNamespace}"; : $"{t.assembly.Name}.{relativeNamespace}";
var resourceFileName = t.name;
var className = CodeSnippetFactory.TransformToClassName(t.name);
var snippet = CodeSnippetFactory.CreateResourceClassCodeSnippet( var snippet = CodeSnippetFactory.CreateResourceClassCodeSnippet(
ns, CodeSnippetFactory.TransformToNamespace(ns),
t.name, className,
CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet(t.name), CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet(
$"{ns}.{resourceFileName}",
className
),
v.Resources v.Resources
); );
spc.AddSource(ns + t.name, snippet.Value); spc.AddSource($"{ns}.{className}", snippet.Value);
}); });
}); });
} }

View File

@@ -4,7 +4,7 @@
outputs = { self, nixpkgs }: outputs = { self, nixpkgs }:
let let
name = "ResourceString.Net"; name = "ResourceString.Net";
version = "0.0.2"; version = "0.0.3";
supportedSystems = supportedSystems =
[ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems; forAllSystems = nixpkgs.lib.genAttrs supportedSystems;