diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..bed90d0 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,36 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet +{ + "name": "C# (.NET)", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/dotnet:0-6.0", + "features": { + "ghcr.io/devcontainers/features/nix:1": {} + }, + "customizations": { + "vscode": { + "extensions": [ + "formulahendry.dotnet-test-explorer", + "jnoortheen.nix-ide", + "eamodio.gitlens" + ] + } + }, + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [5000, 5001], + // "portsAttributes": { + // "5001": { + // "protocol": "https" + // } + // } + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "nix-env -i nixpkgs-fmt" + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..70f8ac5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,478 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk +result \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..25a5e11 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,40 @@ +{ + "version": "0.2.0", + "configurations": [ + + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": "Launch Example App", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/example/Example.App/bin/Debug/net6.0/Example.App.dll", + "args": [], + "cwd": "${workspaceFolder}/example/Example.App", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": "Update Resources.cs", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/ResourceString.Net.App.Console/bin/Debug/net6.0/ResourceString.Net.App.Console.dll", + "args": [ + "${workspaceFolder}/ResourceString.Net.Logic/Properties/Resources.resx" + ], + "cwd": "${workspaceFolder}/ResourceString.Net.App.Console", + "console": "integratedTerminal", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..40df522 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet-test-explorer.testProjectPath": "**/*Tests.csproj" +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..c09ae0f --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "nix", + "type": "process", + "args": [ + "--experimental-features", + "nix-command flakes", + "run", + ".#devTasks.buildAllAssemblies" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/example/Example.App/Example.App.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/example/Example.App/Example.App.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a56c50e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 stubbfel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Readme.md b/Readme.md index e69de29..06e0613 100644 --- a/Readme.md +++ b/Readme.md @@ -0,0 +1,240 @@ +# 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. +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. +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. +Additionally, ResourceString.Net ensures that formatted strings have methods with the correct number of expected parameters. + +## Installation and Setup Instructions + +To incorporate ResourceString.Net into your .NET application, follow these simple steps: + +1. Install the NuGet package by executing the following command in the NuGet package manager or the dotnet CLI: + +```sh +dotnet add package ResourceString.Net +``` + +2. Once the package is installed, you can start using the APIs in your application. + +## Getting Started + +To quickly get started with ResourceString.Net, follow the steps below: + +1. Run the following script to create a new project and a resource file: + +```sh +dotnet new console --name MyTestConsoleApp +cd MyTestConsoleApp + +dotnet add package "System.Resources.Extensions" +dotnet add package "ResourceString.Net" + +echo " + + + Hello {0} + 0 = name + + + World + + +" > Resources.resx + +echo "var message = MyTestConsoleApp.Resources.Greetings.From( + MyTestConsoleApp.Resources.World +); + +Console.WriteLine(message.Value); +" > Program.cs +``` + +2. Add the following `PropertyGroup` to the `MyTestConsoleApp.csproj` file to enable ResourceString.Net to handle the project's resource file: + +```xml + + $(AdditionalFileItemNames);EmbeddedResource + +``` + +3. Run the project using the following command: + +```sh +dotnet run +# Expected output: Hello World +``` + +During compile time, the ResourceString.Net source code generator will automatically add the following code to the `MyTestConsoleApp.csproj` project: + +```cs +using ResourceString.Net.Contract; +using System; +using System.Globalization; +using System.Resources; +using System.Threading; + +namespace MyTestConsoleApp +{ + internal static class Resources + { + + #region ResourceManager + + private static readonly Type _Type = typeof(Resources); + + private static readonly Lazy _ResourceManager = new Lazy( + () => new ResourceManager(_Type.FullName ?? string.Empty, _Type.Assembly), + LazyThreadSafetyMode.PublicationOnly + ); + + public static ResourceManager ResourceManager => _ResourceManager.Value; + + #endregion // ResourceManager + + + internal static class Greetings + { + private static readonly Lazy LazyFormat = new Lazy( + () => new ResourceManagerString("Greetings", ResourceManager, CultureInfo.CurrentCulture), + LazyThreadSafetyMode.PublicationOnly + ); + + public static IResourceString Format => LazyFormat.Value; + + public static IResourceString From(IResourceString name) => new FormattedResourceString( + Format, + name + ); + + } + + #region World + + private static readonly Lazy LazyWorld = new Lazy( + () => new ResourceManagerString("World", ResourceManager, CultureInfo.CurrentCulture), + LazyThreadSafetyMode.PublicationOnly + ); + + public static IResourceString World => LazyWorld.Value; + + #endregion // World + + } +} + +``` + +## The ResourceString-Classes + +### IResourceString + +- Interface that defines the contract for resource strings. +- Contains properties: `Value`, representing the string value, and `GetValue(CultureInfo cultureInfo)`, for retrieving the value for a specific `CultureInfo`. + +### FormattedResourceString + +- Implements the `IResourceString` interface. +- Represents a resource string with placeholders for parameters. +- Allows for dynamic formatting of the string by providing a format and an array of parameters. +- Formats the string by replacing the placeholders with the parameter values. + +### JoinedResourceString + +- Implements the `IResourceString` interface. +- Represents a resource string that joins multiple elements with a separator. +- Useful for constructing strings that involve concatenating multiple resource strings or literal strings together. +- Allows customization of the separator between the elements. + +### LiteralString + +- Implements the `IResourceString` interface. +- Represents a literal string resource. +- Provides the actual string value as-is without any formatting or localization. +- Can be used for static, non-localized strings. + +### ResourceManagerString + +- Implements the `IResourceString` interface. +- Represents a resource string retrieved from a `ResourceManager`. +- Provides access to resource strings stored in `resx` files or other resource sources. +- Handles retrieving the localized value for the specified `CultureInfo`. + +Great! Based on the provided code for the console app `ResourceString.Net.App.Console`, let's enhance the Readme.md file to include instructions on how to use the console app and generate a C# class based on a given resource file. + +## Console App: ResourceString.Net.App.Console + +The ResourceString.Net project also provide a console app, `ResourceString.Net.App.Console`, which allows you to generate a C# class based on a given resource file. This can be useful for automating the creation of resource classes in your projects without usage of the `source generator`. + +### Usage + +To use the console app, follow these steps: + +1. Build the console app from this source repository +2. Open a command prompt or terminal. +3. Navigate to the directory where the `ResourceString.Net.App.Console` executable is located. + +#### Syntax + +``` +ResourceString.Net.App.Console [namespaceString] [className] +``` + +#### Parameters + +- ``: Required. The path to the resource file (e.g., `Resources.resx`) that you want to generate the C# class from. +- `[namespaceString]` (optional): The namespace for the generated C# class. If not provided, the default value is `"Properties"`. +- `[className]` (optional): The name of the generated C# class. If not provided, the default value is `"Resources"`. + +#### Examples + +Here are some examples of how to use the console app: + +- Generate a C# class named `Resources.cs` in the `"Properties"` namespace based on the `Resources.resx` file: + + ``` + ResourceString.Net.App.Console Resources.resx + ``` + +- Generate a C# class named `MyResources.cs` in the `"MyNamespace"` namespace based on the `MyResources.resx` file: + + ``` + ResourceString.Net.App.Console MyResources.resx MyNamespace MyResources + ``` + +### Output + +The console app will generate the C# class code based on the provided resource file and output it to the console. +You can redirect the output to a file if desired. + +## Third party packages + +| Package | Version | +| ------------------------------------- | ------- | +| Fody | 6.7.0 | +| ILMerge.Fody | 1.24.0 | +| Microsoft.CodeAnalysis.Analyzers | 3.3.4 | +| Microsoft.CodeAnalysis.CSharp | 4.3.0 | +| NETStandard.Library (Auto-referenced) | 2.0.3 | +| LanguageExt.Core | 4.4.3 | +| System.Resources.Extensions | 7.0.0 | + +## Development Notes + +Here are some useful aliases for running Nix commands with experimental features: + +- `nixe`: This alias runs the `nix` command with the experimental feature flag `nix-command flakes`. +- `nulock`: This alias runs the `nixe` command with the argument `run .#devTasks.updateNugetLock`, which updates the NuGet lock file. +- `fllock`: This alias runs the `nixe` command with the argument `run .#devTasks.updateFlakeLock`, which updates the Flake lock file. +- `ulock`: This alias combines the `nulock` and `fllock` aliases to update both the NuGet and Flake lock files. + +To load the `alias.sh` source file from the current folder, use the following command: + +```sh +source ./alias.sh +``` + +By executing the command above, you'll make the aliases available for use in the current terminal session. diff --git a/ResourceString.Net.App.Console/Program.cs b/ResourceString.Net.App.Console/Program.cs new file mode 100644 index 0000000..54add19 --- /dev/null +++ b/ResourceString.Net.App.Console/Program.cs @@ -0,0 +1,18 @@ +using ResourceString.Net.Logic.Factories; +using ResourceString.Net.Logic.Parsers.Resx; + +var sourceFile = args.First(); +var namespaceString = args.Skip(1).FirstOrDefault() ?? "Properties"; +var className = args.Skip(2).FirstOrDefault() ?? "Resources"; + +var result = Parser.TryParse(System.IO.File.ReadAllText(sourceFile)).Match( + Some: v => CodeSnippetFactory.CreateResourceClassCodeSnippet( + namespaceString, + className, + CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet(className), + v.Resources + ), + None: () => throw new InvalidOperationException() +); + +Console.WriteLine(result.Value.Trim()); diff --git a/ResourceString.Net.App.Console/ResourceString.Net.App.Console.csproj b/ResourceString.Net.App.Console/ResourceString.Net.App.Console.csproj new file mode 100644 index 0000000..9360a2a --- /dev/null +++ b/ResourceString.Net.App.Console/ResourceString.Net.App.Console.csproj @@ -0,0 +1,16 @@ + + + + Exe + net6.0 + enable + enable + latest + true + + + + + + + diff --git a/ResourceString.Net.Contract/FormattedResourceString.cs b/ResourceString.Net.Contract/FormattedResourceString.cs new file mode 100644 index 0000000..1e1f00d --- /dev/null +++ b/ResourceString.Net.Contract/FormattedResourceString.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace ResourceString.Net.Contract +{ + public class FormattedResourceString : IResourceString + { + public IResourceString Format { get; } + + public IEnumerable Parameters { get; } + + public string Value => GetValue(CultureInfo.CurrentCulture); + + public FormattedResourceString( + IResourceString format, + params IResourceString[] parameters) + { + Format = format ?? new JoinedResourceString( + LiteralString.Factory(","), + parameters.Select(((p, idx) => LiteralString.Factory("{" + idx + "}"))).ToArray() + ); + + Parameters = parameters; + } + + public string GetValue(CultureInfo cultureInfo) + { + var format = Format.GetValue(cultureInfo); + var parameterStrings = Parameters.Select(p => p.GetValue(cultureInfo)); + return string.Format(format, parameterStrings.ToArray()); + } + } +} + diff --git a/ResourceString.Net.Contract/IResourceString.cs b/ResourceString.Net.Contract/IResourceString.cs new file mode 100644 index 0000000..67089e1 --- /dev/null +++ b/ResourceString.Net.Contract/IResourceString.cs @@ -0,0 +1,13 @@ +using System.Globalization; + +namespace ResourceString.Net.Contract +{ + public interface IResourceString + { + + string Value { get; } + + string GetValue(CultureInfo cultureInfo); + } +} + diff --git a/ResourceString.Net.Contract/JoinedResourceString.cs b/ResourceString.Net.Contract/JoinedResourceString.cs new file mode 100644 index 0000000..7f6298a --- /dev/null +++ b/ResourceString.Net.Contract/JoinedResourceString.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace ResourceString.Net.Contract +{ + + public class JoinedResourceString : IResourceString + { + public IResourceString Separator { get; } + + public IEnumerable Elements { get; } + + public string Value => GetValue(CultureInfo.CurrentCulture); + + public JoinedResourceString( + IResourceString separator, + params IResourceString[] elements) + { + Separator = separator ?? new LiteralString( + () => "," + ); + + Elements = elements; + } + + public string GetValue(CultureInfo cultureInfo) + { + var separator = Separator.GetValue(cultureInfo); + var elementsStrings = Elements.Select(p => p.GetValue(cultureInfo)); + return string.Join(separator, elementsStrings); + } + } +} + diff --git a/ResourceString.Net.Contract/LiteralString.cs b/ResourceString.Net.Contract/LiteralString.cs new file mode 100644 index 0000000..2352e47 --- /dev/null +++ b/ResourceString.Net.Contract/LiteralString.cs @@ -0,0 +1,30 @@ +using System; +using System.Globalization; +using System.Threading; + +namespace ResourceString.Net.Contract +{ + public class LiteralString : IResourceString + { + private Lazy m_Value; + + public string Value => m_Value.Value; + + public static IResourceString Empty { get; } = Factory(string.Empty); + public static IResourceString Factory(string source) + { + return new LiteralString(() => source); + } + + public LiteralString(Func factory) + { + m_Value = new Lazy( + () => factory?.Invoke() ?? string.Empty, + LazyThreadSafetyMode.PublicationOnly + ); + } + + public string GetValue(CultureInfo _) => Value; + } +} + diff --git a/ResourceString.Net.Contract/ResourceManagerString.cs b/ResourceString.Net.Contract/ResourceManagerString.cs new file mode 100644 index 0000000..039b028 --- /dev/null +++ b/ResourceString.Net.Contract/ResourceManagerString.cs @@ -0,0 +1,33 @@ +using System; +using System.Globalization; +using System.Resources; + +namespace ResourceString.Net.Contract +{ + public class ResourceManagerString : IResourceString + { + private readonly CultureInfo m_Culture; + + public string Id { get; } + + public ResourceManager Manager { get; } + + public string Value => GetValue(m_Culture); + + public ResourceManagerString( + string id, + ResourceManager manager, + CultureInfo cultureInfo) + { + Id = id ?? string.Empty; + Manager = manager ?? throw new ArgumentNullException(nameof(manager)); + m_Culture = cultureInfo ?? CultureInfo.CurrentCulture; + } + + public string GetValue(CultureInfo cultureInfo) + { + return Manager.GetString(Id, cultureInfo); + } + } +} + diff --git a/ResourceString.Net.Contract/ResourceString.Net.Contract.csproj b/ResourceString.Net.Contract/ResourceString.Net.Contract.csproj new file mode 100644 index 0000000..ddd5277 --- /dev/null +++ b/ResourceString.Net.Contract/ResourceString.Net.Contract.csproj @@ -0,0 +1,9 @@ + + + + netstandard2.0 + latest + enable + + + diff --git a/ResourceString.Net.Logic.Tests/LogicResourcesTests.cs b/ResourceString.Net.Logic.Tests/LogicResourcesTests.cs new file mode 100644 index 0000000..15970e0 --- /dev/null +++ b/ResourceString.Net.Logic.Tests/LogicResourcesTests.cs @@ -0,0 +1,106 @@ +using ResourceString.Net.Logic.Factories; + +namespace ResourceString.Net.Logic.Tests; + +[TestClass] +public class LogicResourcesTests +{ + + [TestMethod] + public void Test_Get_ResourceStringMembers() + { + var rm = Properties.Resources.ResourceManager; + Assert.AreEqual( + new FormattedResourceString( + Properties.Resources.ResourceStringMembers.Format, + LiteralString.Factory("ResourceStringMembers"), + LiteralString.Factory("ResourceManager") + ).Value, + string.Format( + rm.GetString("ResourceStringMembers") ?? string.Empty, + "ResourceStringMembers", + "ResourceManager" + ) + ); + } + + [TestMethod] + public void Test_Get_ResourceFormatClassMembers() + { + var rm = Properties.Resources.ResourceManager; + Assert.AreEqual( + new FormattedResourceString( + Properties.Resources.ResourceFormatClassMembers.Format, + LiteralString.Factory("ResourceFormatClassMembers"), + LiteralString.Factory("ResourceManager"), + LiteralString.Factory("Format") + ).Value, + string.Format( + rm.GetString("ResourceFormatClassMembers") ?? string.Empty, + "ResourceFormatClassMembers", + "ResourceManager", + "Format" + ) + ); + } + + [TestMethod] + public void Test_Get_ResourceFormatClassFromMethod() + { + var rm = Properties.Resources.ResourceManager; + Assert.AreEqual( + new FormattedResourceString( + Properties.Resources.ResourceFormatClassFromMethod.Format, + LiteralString.Factory("IResourceString name"), + LiteralString.Factory("name") + ).Value, + string.Format( + rm.GetString("ResourceFormatClassFromMethod") ?? string.Empty, + "IResourceString name", + "name" + ) + ); + } + + [TestMethod] + public void Test_Get_ResourceManagerMemberTemplate() + { + var rm = Properties.Resources.ResourceManager; + Assert.AreEqual( + CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet( + "Resources" + ).Value, + string.Format( + rm.GetString("ResourceManagerMemberTemplate") ?? string.Empty, + "Resources" + ) + ); + } + + [TestMethod] + public void Test_Get_ResourcesClassTemplate() + { + var rm = Properties.Resources.ResourceManager; + + var rmSnippet = CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet( + "Resources" + ); + + Assert.AreEqual( + CodeSnippetFactory.CreateResourceClassCodeSnippet( + "ResourceString.Net.Logic.Properties", + "Resources", + rmSnippet, + Enumerable.Empty() + ).Value, + string.Format( + rm.GetString("ResourcesClassTemplate") ?? string.Empty, + "ResourceString.Net.Logic.Properties", + "Resources", + rmSnippet.Value, + string.Empty + ) + ); + } +} + diff --git a/ResourceString.Net.Logic.Tests/ResourceString.Net.Logic.Tests.csproj b/ResourceString.Net.Logic.Tests/ResourceString.Net.Logic.Tests.csproj new file mode 100644 index 0000000..b3ef7bf --- /dev/null +++ b/ResourceString.Net.Logic.Tests/ResourceString.Net.Logic.Tests.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + enable + latest + false + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/ResourceString.Net.Logic.Tests/ResxFileTests.cs b/ResourceString.Net.Logic.Tests/ResxFileTests.cs new file mode 100644 index 0000000..0a1ea55 --- /dev/null +++ b/ResourceString.Net.Logic.Tests/ResxFileTests.cs @@ -0,0 +1,192 @@ +using ResourceString.Net.Logic.DomainObjects.Resx; +using ResourceString.Net.Logic.Factories; +using ResourceString.Net.Logic.Parsers.Resx; + +namespace ResourceString.Net.Logic.Tests; + +[TestClass] +public class ResxFileTests +{ + private readonly string validXml = @$" + + + Value1 + This is a greeting message. + + + {{2}}Value2{{0}}{{{{1}}}} + 2 = prefix + + + 0xDEADBEEF + +"; + + private const string expectedClass = @" +using ResourceString.Net.Contract; +using System; +using System.Globalization; +using System.Resources; +using System.Threading; + +namespace TestNameSpace +{ + internal static class TestResourceClass + { + + #region ResourceManager + + private static readonly Type _Type = typeof(ResourceManager); + + private static readonly Lazy _ResourceManager = new Lazy( + () => new ResourceManager(_Type.FullName ?? string.Empty, _Type.Assembly), + LazyThreadSafetyMode.PublicationOnly + ); + + public static ResourceManager ResourceManager => _ResourceManager.Value; + + #endregion // ResourceManager + + + #region Test1 + + private static readonly Lazy LazyTest1 = new Lazy( + () => new ResourceManagerString(""Test1"", ResourceManager, CultureInfo.CurrentCulture), + LazyThreadSafetyMode.PublicationOnly + ); + + public static IResourceString Test1 => LazyTest1.Value; + + #endregion // Test1 + + internal static class Test2 + { + private static readonly Lazy LazyFormat = new Lazy( + () => new ResourceManagerString(""Test2"", ResourceManager, CultureInfo.CurrentCulture), + LazyThreadSafetyMode.PublicationOnly + ); + + public static IResourceString Format => LazyFormat.Value; + + public static IResourceString From(IResourceString prefix, IResourceString p1) => new FormattedResourceString( + Format, + prefix, + p1 + ); + + } + + } +}"; + + private readonly string emptyXml = @""; + + private readonly string invalidXml = @""; + + [TestMethod] + public void Test_TryParse_ValidXml() + { + // Arrange + var expectedData = new[] { + ("Test1", "Value1", string.Empty, "This is a greeting message."), + ("Test2", "{2}Value2{0}{{1}}", typeof(string).FullName, "2 = prefix"), + ("Test3", "0xDEADBEEF", typeof(byte[]).FullName, string.Empty) + }; + + // Act + var result = Parser.TryParse(validXml).Match( + Some: v => v.Resources, + None: () => throw new InvalidOperationException() + ); + + // Assert + CollectionAssert.AreEqual( + expectedData, + result.Select(i => ( + i.Name, + i.Value, + i.Type.IfNone(string.Empty), + i.Comment.IfNone(string.Empty) + )).ToList() + ); + } + + [TestMethod] + public void Test_TryParse_EmptyXml() + { + // Arrange + var expectedData = Enumerable.Empty(); + + // Act + var isSome = Parser.TryParse(emptyXml).Match( + Some: _ => true, + None: () => false + ); + + // Assert + Assert.IsFalse(isSome); + } + + [TestMethod] + public void Test_TryParse_InValidXml() + { + // Arrange + var expectedData = Enumerable.Empty(); + + // Act + var isSome = Parser.TryParse(invalidXml).Match( + Some: _ => true, + None: () => false + ); + + // Assert + Assert.IsFalse(isSome); + } + + [TestMethod] + public void Test_ToMemberString() + { + // Act + var result = Parser.TryParse(validXml).Match( + Some: v => CodeSnippetFactory.CreateMemberCodeSnippets(v.Resources), + None: () => throw new InvalidOperationException() + ); + + // Assert + Assert.AreEqual(2, result.Count()); + + Assert.AreEqual( + "#region Test1", + result.Select(i => i.Value.Substring(0, 25).Trim()).ToList()[0] + ); + + Assert.AreEqual( + "internal static class Test2", + result.Select(i => i.Value.Substring(0, 45).Trim()).ToList()[1] + ); + + } + + [TestMethod] + public void Test_ToClassString() + { + // Act + var result = Parser.TryParse(validXml).Match( + Some: v => CodeSnippetFactory.CreateResourceClassCodeSnippet( + "TestNameSpace", + "TestResourceClass", + CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet("ResourceManager"), + v.Resources + ), + None: () => throw new InvalidOperationException() + ); + + // Assert + Assert.AreEqual( + expectedClass.Trim(), + result.Value.Trim() + ); + } +} + + diff --git a/ResourceString.Net.Logic.Tests/Usings.cs b/ResourceString.Net.Logic.Tests/Usings.cs new file mode 100644 index 0000000..9fc20fb --- /dev/null +++ b/ResourceString.Net.Logic.Tests/Usings.cs @@ -0,0 +1,4 @@ +global using LanguageExt; +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using ResourceString.Net.Contract; +global using ResourceString.Net.Logic; diff --git a/ResourceString.Net.Logic/DomainObjects/Resx/File.cs b/ResourceString.Net.Logic/DomainObjects/Resx/File.cs new file mode 100644 index 0000000..c724563 --- /dev/null +++ b/ResourceString.Net.Logic/DomainObjects/Resx/File.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Linq; + +namespace ResourceString.Net.Logic.DomainObjects.Resx; + +internal record File +{ + public File(IEnumerable resources) + { + Resources = new Seq( + resources?.Where(o => o != null) ?? Enumerable.Empty() + ); + } + + public IEnumerable Resources { get; } +} diff --git a/ResourceString.Net.Logic/DomainObjects/Resx/Resource.cs b/ResourceString.Net.Logic/DomainObjects/Resx/Resource.cs new file mode 100644 index 0000000..03c6870 --- /dev/null +++ b/ResourceString.Net.Logic/DomainObjects/Resx/Resource.cs @@ -0,0 +1,18 @@ +namespace ResourceString.Net.Logic.DomainObjects.Resx; + +internal record Resource +{ + public Resource(string name, string value) + { + Name = name ?? string.Empty; + Value = value ?? string.Empty; + } + + public string Name { get;} + + public Option Type { get; init;} + + public Option Comment { get; init;} + + public string Value { get; } +} diff --git a/ResourceString.Net.Logic/Factories/CodeSnippetFactory.cs b/ResourceString.Net.Logic/Factories/CodeSnippetFactory.cs new file mode 100644 index 0000000..dcdcbf2 --- /dev/null +++ b/ResourceString.Net.Logic/Factories/CodeSnippetFactory.cs @@ -0,0 +1,126 @@ +using ResourceString.Net.Logic.DomainObjects.Resx; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ResourceString.Net.Logic.Factories; + +internal static class CodeSnippetFactory +{ + public static IResourceString CreateResourceMangerMemberCodeSnippet(string typeName) + { + return Properties.Resources.ResourceManagerMemberTemplate.From( + LiteralString.Factory(typeName) + ); + } + + public static IEnumerable CreateMemberCodeSnippets(IEnumerable resources) + { + return CreateMemberCodeSnippets( + resources, + Properties.Resources.DefaultPropertyName_ResourceManager + ); + } + + public static IEnumerable CreateMemberCodeSnippets( + IEnumerable resources, + IResourceString resourceManagerName + ) + { + if (resources is null) + { + return Enumerable.Empty(); + } + + var stringResourses = resources.Where(r => r.Type.Match( + v => typeof(string).IsAssignableFrom(Type.GetType(v.Trim(), false, true)), + () => true + )); + + return stringResourses.Select(r => + { + var openBraces = r.Value + .Replace("{{", string.Empty) + .Replace("{{", string.Empty) + .Split('{') + .Skip(1) + .ToArray(); + + if (openBraces.Any()) + { + var formatNames = new System.Collections.Generic.HashSet( + openBraces.Select(b => b.Split('}').First()) + ); + + var resolveParameterNames = r.Comment.Match( + c => c.Split(',') + .Select(str => str.Split('=')) + .Where(parts => parts.Count() == 2) + .Select(parts => ( + parts.First().Trim().Trim('{', '}').Split(' ').First(), + parts.Last().Trim().Split(' ').First() + )).Where(t => uint.TryParse(t.Item1, out var _)) + .GroupBy(t => t.Item1) + .ToDictionary(k => k.Key, v => v.First().Item2), + () => new Dictionary() + ); + + var parameterNames = formatNames.Select( + n => resolveParameterNames.TryGetValue(n.Trim(), out var paramName) + ? paramName.Substring(0, 1).ToLower() + paramName.Substring(1) + : uint.TryParse(n, out var value) ? $"p{value + 1}" : n + ).ToArray(); + + var from = Properties.Resources.ResourceFormatClassFromMethod.From( + new JoinedResourceString( + LiteralString.Factory(", "), + parameterNames.Select(n => LiteralString.Factory($"{nameof(IResourceString)} {n}")).ToArray() + ), + new JoinedResourceString( + LiteralString.Factory($",{System.Environment.NewLine} "), + parameterNames.Select(n => LiteralString.Factory($"{n}")).ToArray() + ) + ); + return Properties.Resources.ResourceFormatClassMembers.From( + LiteralString.Factory(r.Name), resourceManagerName, from + ); + } + return Properties.Resources.ResourceStringMembers.From( + LiteralString.Factory(r.Name), resourceManagerName + ); + }); + } + + public static IResourceString CreateResourceClassCodeSnippet( + string namespaceString, + string resourceClassName, + IResourceString resourceManagerSnippet, + IEnumerable memberSnippets + ) + { + return Properties.Resources.ResourcesClassTemplate.From( + LiteralString.Factory(namespaceString), + LiteralString.Factory(resourceClassName), + resourceManagerSnippet, + new JoinedResourceString( + LiteralString.Empty, + memberSnippets?.ToArray() ?? Array.Empty() + ) + ); + } + + public static IResourceString CreateResourceClassCodeSnippet( + string namespaceString, + string resourceClassName, + IResourceString resourceManagerSnippet, + IEnumerable resources + ) + { + return CreateResourceClassCodeSnippet( + namespaceString, + resourceClassName, + resourceManagerSnippet, + CreateMemberCodeSnippets(resources) + ); + } +} \ No newline at end of file diff --git a/ResourceString.Net.Logic/IsExternalInit.cs b/ResourceString.Net.Logic/IsExternalInit.cs new file mode 100644 index 0000000..ed1a03e --- /dev/null +++ b/ResourceString.Net.Logic/IsExternalInit.cs @@ -0,0 +1,3 @@ +namespace System.Runtime.CompilerServices; + +internal static class IsExternalInit { } diff --git a/ResourceString.Net.Logic/Parsers/Resx/Parser.cs b/ResourceString.Net.Logic/Parsers/Resx/Parser.cs new file mode 100644 index 0000000..5c9df5b --- /dev/null +++ b/ResourceString.Net.Logic/Parsers/Resx/Parser.cs @@ -0,0 +1,44 @@ +using ResourceString.Net.Logic.DomainObjects.Resx; +using System.Linq; +using System.Xml; + +namespace ResourceString.Net.Logic.Parsers.Resx; + +internal static class Parser +{ + public static Option TryParse(string xmlString) + { + var doc = new XmlDocument(); + + try + { + doc.LoadXml(xmlString); + } + catch + { + return Option.None; + } + + var resources = doc.SelectNodes("descendant::data").OfType().Select((i, _) => + { + var name = i.GetAttribute("name"); + var type = i.GetAttribute("type"); + var comment = i.SelectSingleNode("descendant::comment")?.InnerXml ?? string.Empty; + var value = i.SelectSingleNode("descendant::value")?.InnerXml ?? string.Empty; + + return new Resource(name, value) + { + Type = string.IsNullOrWhiteSpace(type) + ? Option.None + : Option.Some(type), + Comment = string.IsNullOrWhiteSpace(comment) + ? Option.None + : Option.Some(comment) + }; + }).ToArray(); + + return resources.Any() + ? Option.Some(new File(resources)) + : Option.None; + } +} diff --git a/ResourceString.Net.Logic/Properties/Resources.cs b/ResourceString.Net.Logic/Properties/Resources.cs new file mode 100644 index 0000000..a901733 --- /dev/null +++ b/ResourceString.Net.Logic/Properties/Resources.cs @@ -0,0 +1,125 @@ +using ResourceString.Net.Contract; +using System; +using System.Globalization; +using System.Resources; +using System.Threading; + +namespace ResourceString.Net.Logic.Properties +{ + internal static class Resources + { + + #region ResourceManager + + private static readonly Type _Type = typeof(Resources); + + private static readonly Lazy _ResourceManager = new Lazy( + () => new ResourceManager(_Type.FullName ?? string.Empty, _Type.Assembly), + LazyThreadSafetyMode.PublicationOnly + ); + + public static ResourceManager ResourceManager => _ResourceManager.Value; + + #endregion // ResourceManager + + + internal static class ResourceStringMembers + { + private static readonly Lazy LazyFormat = new Lazy( + () => new ResourceManagerString("ResourceStringMembers", ResourceManager, CultureInfo.CurrentCulture), + LazyThreadSafetyMode.PublicationOnly + ); + + public static IResourceString Format => LazyFormat.Value; + + public static IResourceString From(IResourceString resourceId, IResourceString resourceManagerPropertyName) => new FormattedResourceString( + Format, + resourceId, + resourceManagerPropertyName + ); + + } + + internal static class ResourceFormatClassMembers + { + private static readonly Lazy LazyFormat = new Lazy( + () => new ResourceManagerString("ResourceFormatClassMembers", ResourceManager, CultureInfo.CurrentCulture), + LazyThreadSafetyMode.PublicationOnly + ); + + public static IResourceString Format => LazyFormat.Value; + + public static IResourceString From(IResourceString resourceId, IResourceString resourceManagerPropertyName, IResourceString fromMethodDefinition) => new FormattedResourceString( + Format, + resourceId, + resourceManagerPropertyName, + fromMethodDefinition + ); + + } + + internal static class ResourceFormatClassFromMethod + { + private static readonly Lazy LazyFormat = new Lazy( + () => new ResourceManagerString("ResourceFormatClassFromMethod", ResourceManager, CultureInfo.CurrentCulture), + LazyThreadSafetyMode.PublicationOnly + ); + + public static IResourceString Format => LazyFormat.Value; + + public static IResourceString From(IResourceString fromMethodSignature, IResourceString parameterNames) => new FormattedResourceString( + Format, + fromMethodSignature, + parameterNames + ); + + } + + internal static class ResourcesClassTemplate + { + private static readonly Lazy LazyFormat = new Lazy( + () => new ResourceManagerString("ResourcesClassTemplate", ResourceManager, CultureInfo.CurrentCulture), + LazyThreadSafetyMode.PublicationOnly + ); + + public static IResourceString Format => LazyFormat.Value; + + public static IResourceString From(IResourceString ns, IResourceString className, IResourceString resourceManagerRegion, IResourceString resourceRegions) => new FormattedResourceString( + Format, + ns, + className, + resourceManagerRegion, + resourceRegions + ); + + } + + internal static class ResourceManagerMemberTemplate + { + private static readonly Lazy LazyFormat = new Lazy( + () => new ResourceManagerString("ResourceManagerMemberTemplate", ResourceManager, CultureInfo.CurrentCulture), + LazyThreadSafetyMode.PublicationOnly + ); + + public static IResourceString Format => LazyFormat.Value; + + public static IResourceString From(IResourceString resourceManagerTypeName) => new FormattedResourceString( + Format, + resourceManagerTypeName + ); + + } + + #region DefaultPropertyName_ResourceManager + + private static readonly Lazy LazyDefaultPropertyName_ResourceManager = new Lazy( + () => new ResourceManagerString("DefaultPropertyName_ResourceManager", ResourceManager, CultureInfo.CurrentCulture), + LazyThreadSafetyMode.PublicationOnly + ); + + public static IResourceString DefaultPropertyName_ResourceManager => LazyDefaultPropertyName_ResourceManager.Value; + + #endregion // DefaultPropertyName_ResourceManager + + } +} diff --git a/ResourceString.Net.Logic/Properties/Resources.resx b/ResourceString.Net.Logic/Properties/Resources.resx new file mode 100644 index 0000000..9fd20fe --- /dev/null +++ b/ResourceString.Net.Logic/Properties/Resources.resx @@ -0,0 +1,81 @@ + + + + + #region {0} + + private static readonly Lazy<IResourceString> Lazy{0} = new Lazy<IResourceString>( + () => new ResourceManagerString("{0}", {1}, CultureInfo.CurrentCulture), + LazyThreadSafetyMode.PublicationOnly + ); + + public static IResourceString {0} => Lazy{0}.Value; + + #endregion // {0} + + 0 = ResourceId, 1 = ResourceManagerPropertyName + + + + internal static class {0} + {{ + private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>( + () => new ResourceManagerString("{0}", {1}, CultureInfo.CurrentCulture), + LazyThreadSafetyMode.PublicationOnly + ); + + public static IResourceString Format => LazyFormat.Value; + {2} + }} + + 0 = ResourceId, 1 = ResourceManagerPropertyName, 2 = FromMethodDefinition + + + + public static IResourceString From({0}) => new FormattedResourceString( + Format, + {1} + ); + + 0 = FromMethodSignature, 1 = ParameterNames + + + +using ResourceString.Net.Contract; +using System; +using System.Globalization; +using System.Resources; +using System.Threading; + +namespace {0} +{{ + internal static class {1} + {{ +{2} +{3} + }} +}} + + 0 = ns, 1 = ClassName, 2 = ResourceManagerRegion, 3 = ResourceRegions + + + + #region ResourceManager + + private static readonly Type _Type = typeof({0}); + + private static readonly Lazy<ResourceManager> _ResourceManager = new Lazy<ResourceManager>( + () => new ResourceManager(_Type.FullName ?? string.Empty, _Type.Assembly), + LazyThreadSafetyMode.PublicationOnly + ); + + public static ResourceManager ResourceManager => _ResourceManager.Value; + + #endregion // ResourceManager + + 0 = ResourceManagerTypeName + + + ResourceManager + + diff --git a/ResourceString.Net.Logic/ResourceString.Net.Logic.csproj b/ResourceString.Net.Logic/ResourceString.Net.Logic.csproj new file mode 100644 index 0000000..1746496 --- /dev/null +++ b/ResourceString.Net.Logic/ResourceString.Net.Logic.csproj @@ -0,0 +1,29 @@ + + + + netstandard2.0 + latest + enable + + + + + + + + + + + + + + <_Parameter1>$(MSBuildProjectName).Tests + + + <_Parameter1>ResourceString.Net.App.Console + + + <_Parameter1>ResourceString.Net + + + diff --git a/ResourceString.Net.Logic/Usings.cs b/ResourceString.Net.Logic/Usings.cs new file mode 100644 index 0000000..dc739e8 --- /dev/null +++ b/ResourceString.Net.Logic/Usings.cs @@ -0,0 +1,4 @@ +global using LanguageExt; + +global using ResourceString.Net.Contract; + diff --git a/ResourceString.Net/FodyWeavers.xml b/ResourceString.Net/FodyWeavers.xml new file mode 100644 index 0000000..f529bfe --- /dev/null +++ b/ResourceString.Net/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ResourceString.Net/Generator.cs b/ResourceString.Net/Generator.cs new file mode 100644 index 0000000..b7a39f6 --- /dev/null +++ b/ResourceString.Net/Generator.cs @@ -0,0 +1,50 @@ +using Microsoft.CodeAnalysis; +using ResourceString.Net.Logic.Factories; +using ResourceString.Net.Logic.Parsers.Resx; + +namespace ResourceString.Net; + +[Generator] +public class Generator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext initContext) + { + // find all additional files that end with .txt + var resxFiles = initContext.AdditionalTextsProvider.Where(static file => file.Path.EndsWith(".resx")); + var assemblies = initContext.CompilationProvider.Select(static (c, _) => c.Assembly); + + // read their contents and save their name + var config = resxFiles.Combine(assemblies).Select( + (t, cancellationToken) => ( + name: System.IO.Path.GetFileNameWithoutExtension(t.Left.Path), + content: t.Left.GetText(cancellationToken)!.ToString(), + assembly: t.Right, + path: t.Left.Path + ) + ); + + // generate a class that contains their values as const strings + initContext.RegisterSourceOutput(config, (spc, t) => + { + Parser.TryParse(t.content).IfSome(v => + { + var relativeNamespace = System.IO.Path.GetDirectoryName(t.path).Substring( + System.IO.Path.GetDirectoryName(t.assembly.Locations[0].GetLineSpan().Path).Length + ).Trim(System.IO.Path.DirectorySeparatorChar).Replace(System.IO.Path.DirectorySeparatorChar, '.'); + + var ns = string.IsNullOrWhiteSpace(relativeNamespace) + ? t.assembly.Name + : $"{t.assembly.Name}.{relativeNamespace}"; + + var snippet = CodeSnippetFactory.CreateResourceClassCodeSnippet( + ns, + t.name, + CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet(t.name), + v.Resources + ); + + spc.AddSource(ns + t.name, snippet.Value); + }); + }); + } +} diff --git a/ResourceString.Net/ResourceString.Net.csproj b/ResourceString.Net/ResourceString.Net.csproj new file mode 100644 index 0000000..8d42a7c --- /dev/null +++ b/ResourceString.Net/ResourceString.Net.csproj @@ -0,0 +1,41 @@ + + + + netstandard2.0 + latest + enable + true + true + stubbfel + false + generator + true + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + + + all + + + all + + + + + + all + + + \ No newline at end of file diff --git a/ResourceString.Net/deps.nix b/ResourceString.Net/deps.nix new file mode 100644 index 0000000..99a33c4 --- /dev/null +++ b/ResourceString.Net/deps.nix @@ -0,0 +1,48 @@ +{ fetchNuGet }: [ + (fetchNuGet { pname = "Fody"; version = "6.7.0"; sha256 = "0fv0zrffa296qjyi11yk31vfqh6gm1nxsx8g5zz380jcsrilnp3h"; }) + (fetchNuGet { pname = "ILMerge.Fody"; version = "1.24.0"; sha256 = "1gibwcl8ngbvwlcqzd9clysrhsjb8g4gwry7n8ifw1mrw7sjjk6x"; }) + (fetchNuGet { pname = "LanguageExt.Core"; version = "4.4.3"; sha256 = "1pd7wx4c21v56y6i75sxbs990mjrs6bp9h8c48a5w79s1zpbinw5"; }) + (fetchNuGet { pname = "Microsoft.Bcl.AsyncInterfaces"; version = "7.0.0"; sha256 = "1waiggh3g1cclc81gmjrqbh128kwfjky3z79ma4bd2ms9pa3gvfm"; }) + (fetchNuGet { pname = "Microsoft.CodeAnalysis.Analyzers"; version = "3.3.4"; sha256 = "0wd6v57p53ahz5z9zg4iyzmy3src7rlsncyqpcag02jjj1yx6g58"; }) + (fetchNuGet { pname = "Microsoft.CodeAnalysis.Common"; version = "4.3.0"; sha256 = "0qpxygiq53v2d2wl6hccnkjf1lhlxjh4q3w5b6d23aq9pw5qj626"; }) + (fetchNuGet { pname = "Microsoft.CodeAnalysis.CSharp"; version = "4.3.0"; sha256 = "0m9qqn391ayfi1ffkzvhpij790hs96q6dbhzfkj2ahvw6qx47b30"; }) + (fetchNuGet { pname = "Microsoft.CSharp"; version = "4.7.0"; sha256 = "0gd67zlw554j098kabg887b5a6pq9kzavpa3jjy5w53ccjzjfy8j"; }) + (fetchNuGet { pname = "Microsoft.NETCore.Platforms"; version = "1.1.0"; sha256 = "08vh1r12g6ykjygq5d3vq09zylgb84l63k49jc4v8faw9g93iqqm"; }) + (fetchNuGet { pname = "Microsoft.NETCore.Targets"; version = "1.1.0"; sha256 = "193xwf33fbm0ni3idxzbr5fdq3i2dlfgihsac9jj7whj0gd902nh"; }) + (fetchNuGet { pname = "NETStandard.Library"; version = "2.0.3"; sha256 = "1fn9fxppfcg4jgypp2pmrpr6awl3qz1xmnri0cygpkwvyx27df1y"; }) + (fetchNuGet { pname = "System.Buffers"; version = "4.5.1"; sha256 = "04kb1mdrlcixj9zh1xdi5as0k0qi8byr5mi3p3jcxx72qz93s2y3"; }) + (fetchNuGet { pname = "System.Collections"; version = "4.3.0"; sha256 = "19r4y64dqyrq6k4706dnyhhw7fs24kpp3awak7whzss39dakpxk9"; }) + (fetchNuGet { pname = "System.Collections.Immutable"; version = "6.0.0"; sha256 = "1js98kmjn47ivcvkjqdmyipzknb9xbndssczm8gq224pbaj1p88c"; }) + (fetchNuGet { pname = "System.Diagnostics.Contracts"; version = "4.3.0"; sha256 = "1gxawcr4d2y5jmc6y7iv8c1q83hm22f6savcvspvhmpl974jigib"; }) + (fetchNuGet { pname = "System.Diagnostics.Debug"; version = "4.3.0"; sha256 = "00yjlf19wjydyr6cfviaph3vsjzg3d5nvnya26i2fvfg53sknh3y"; }) + (fetchNuGet { pname = "System.Globalization"; version = "4.3.0"; sha256 = "1cp68vv683n6ic2zqh2s1fn4c2sd87g5hpp6l4d4nj4536jz98ki"; }) + (fetchNuGet { pname = "System.IO"; version = "4.3.0"; sha256 = "05l9qdrzhm4s5dixmx68kxwif4l99ll5gqmh7rqgw554fx0agv5f"; }) + (fetchNuGet { pname = "System.Linq"; version = "4.3.0"; sha256 = "1w0gmba695rbr80l1k2h4mrwzbzsyfl2z4klmpbsvsg5pm4a56s7"; }) + (fetchNuGet { pname = "System.Linq.Expressions"; version = "4.3.0"; sha256 = "0ky2nrcvh70rqq88m9a5yqabsl4fyd17bpr63iy2mbivjs2nyypv"; }) + (fetchNuGet { pname = "System.Linq.Queryable"; version = "4.3.0"; sha256 = "0vidv9cjwy8scabxd33mm4zl5vql695rz56ydc42m9b731xi2ahj"; }) + (fetchNuGet { pname = "System.Memory"; version = "4.5.4"; sha256 = "14gbbs22mcxwggn0fcfs1b062521azb9fbb7c113x0mq6dzq9h6y"; }) + (fetchNuGet { pname = "System.Memory"; version = "4.5.5"; sha256 = "08jsfwimcarfzrhlyvjjid61j02irx6xsklf32rv57x2aaikvx0h"; }) + (fetchNuGet { pname = "System.Numerics.Vectors"; version = "4.4.0"; sha256 = "0rdvma399070b0i46c4qq1h2yvjj3k013sqzkilz4bz5cwmx1rba"; }) + (fetchNuGet { pname = "System.ObjectModel"; version = "4.3.0"; sha256 = "191p63zy5rpqx7dnrb3h7prvgixmk168fhvvkkvhlazncf8r3nc2"; }) + (fetchNuGet { pname = "System.Reflection"; version = "4.3.0"; sha256 = "0xl55k0mw8cd8ra6dxzh974nxif58s3k1rjv1vbd7gjbjr39j11m"; }) + (fetchNuGet { pname = "System.Reflection.Emit"; version = "4.7.0"; sha256 = "121l1z2ypwg02yz84dy6gr82phpys0njk7yask3sihgy214w43qp"; }) + (fetchNuGet { pname = "System.Reflection.Emit.ILGeneration"; version = "4.3.0"; sha256 = "0w1n67glpv8241vnpz1kl14sy7zlnw414aqwj4hcx5nd86f6994q"; }) + (fetchNuGet { pname = "System.Reflection.Emit.ILGeneration"; version = "4.7.0"; sha256 = "0l8jpxhpgjlf1nkz5lvp61r4kfdbhr29qi8aapcxn3izd9wd0j8r"; }) + (fetchNuGet { pname = "System.Reflection.Emit.Lightweight"; version = "4.7.0"; sha256 = "0mbjfajmafkca47zr8v36brvknzks5a7pgb49kfq2d188pyv6iap"; }) + (fetchNuGet { pname = "System.Reflection.Extensions"; version = "4.3.0"; sha256 = "02bly8bdc98gs22lqsfx9xicblszr2yan7v2mmw3g7hy6miq5hwq"; }) + (fetchNuGet { pname = "System.Reflection.Metadata"; version = "5.0.0"; sha256 = "17qsl5nanlqk9iz0l5wijdn6ka632fs1m1fvx18dfgswm258r3ss"; }) + (fetchNuGet { pname = "System.Reflection.Primitives"; version = "4.3.0"; sha256 = "04xqa33bld78yv5r93a8n76shvc8wwcdgr1qvvjh959g3rc31276"; }) + (fetchNuGet { pname = "System.Reflection.TypeExtensions"; version = "4.3.0"; sha256 = "0y2ssg08d817p0vdag98vn238gyrrynjdj4181hdg780sif3ykp1"; }) + (fetchNuGet { pname = "System.Resources.Extensions"; version = "7.0.0"; sha256 = "0d5gk5g5qqkwa728jwx9yabgjvgywsy6k8r5vgqv2dmlvjrqflb4"; }) + (fetchNuGet { pname = "System.Resources.ResourceManager"; version = "4.3.0"; sha256 = "0sjqlzsryb0mg4y4xzf35xi523s4is4hz9q4qgdvlvgivl7qxn49"; }) + (fetchNuGet { pname = "System.Runtime"; version = "4.3.0"; sha256 = "066ixvgbf2c929kgknshcxqj6539ax7b9m570cp8n179cpfkapz7"; }) + (fetchNuGet { pname = "System.Runtime.CompilerServices.Unsafe"; version = "4.5.3"; sha256 = "1afi6s2r1mh1kygbjmfba6l4f87pi5sg13p4a48idqafli94qxln"; }) + (fetchNuGet { pname = "System.Runtime.CompilerServices.Unsafe"; version = "6.0.0"; sha256 = "0qm741kh4rh57wky16sq4m0v05fxmkjjr87krycf5vp9f0zbahbc"; }) + (fetchNuGet { pname = "System.Runtime.Extensions"; version = "4.3.0"; sha256 = "1ykp3dnhwvm48nap8q23893hagf665k0kn3cbgsqpwzbijdcgc60"; }) + (fetchNuGet { pname = "System.Text.Encoding"; version = "4.3.0"; sha256 = "1f04lkir4iladpp51sdgmis9dj4y8v08cka0mbmsy0frc9a4gjqr"; }) + (fetchNuGet { pname = "System.Text.Encoding.CodePages"; version = "6.0.0"; sha256 = "0gm2kiz2ndm9xyzxgi0jhazgwslcs427waxgfa30m7yqll1kcrww"; }) + (fetchNuGet { pname = "System.Threading"; version = "4.3.0"; sha256 = "0rw9wfamvhayp5zh3j7p1yfmx9b5khbf4q50d8k5rk993rskfd34"; }) + (fetchNuGet { pname = "System.Threading.Tasks"; version = "4.3.0"; sha256 = "134z3v9abw3a6jsw17xl3f6hqjpak5l682k2vz39spj4kmydg6k7"; }) + (fetchNuGet { pname = "System.Threading.Tasks.Extensions"; version = "4.5.4"; sha256 = "0y6ncasgfcgnjrhynaf0lwpkpkmv4a07sswwkwbwb5h7riisj153"; }) + (fetchNuGet { pname = "System.ValueTuple"; version = "4.5.0"; sha256 = "00k8ja51d0f9wrq4vv5z2jhq8hy31kac2rg0rv06prylcybzl8cy"; }) +] diff --git a/alias.sh b/alias.sh new file mode 100644 index 0000000..2edc66a --- /dev/null +++ b/alias.sh @@ -0,0 +1,6 @@ +#!/bin/env sh + +alias nixe='nix --experimental-features "nix-command flakes"' +alias nulock='nixe run .#devTasks.updateNugetLock' +alias fllock='nixe run .#devTasks.updateFlakeLock' +alias ulock='nulock && fllock' \ No newline at end of file diff --git a/example/Example.App/Example.App.csproj b/example/Example.App/Example.App.csproj new file mode 100644 index 0000000..24a4f0f --- /dev/null +++ b/example/Example.App/Example.App.csproj @@ -0,0 +1,23 @@ + + + + + + + + Exe + net6.0 + enable + enable + latest + false + + + + $(AdditionalFileItemNames);EmbeddedResource + + + + + + diff --git a/example/Example.App/Program.cs b/example/Example.App/Program.cs new file mode 100644 index 0000000..7b3bc36 --- /dev/null +++ b/example/Example.App/Program.cs @@ -0,0 +1,22 @@ +namespace Test +{ + + internal class Program + { + private static void Main(string[] args) + { + Console.WriteLine("Hello"); + + var foo = Example.App.Resources.Test1; + Console.WriteLine(foo.Value); + + var bar = Example.App.Properties.SubProperties.Properties.Test2.From( + ResourceString.Net.Contract.LiteralString.Factory("1"), + ResourceString.Net.Contract.LiteralString.Factory("2") + ); + + Console.WriteLine(bar.Value); + } + } + +} \ No newline at end of file diff --git a/example/Example.App/Properties/SubProperties/Properties.resx b/example/Example.App/Properties/SubProperties/Properties.resx new file mode 100644 index 0000000..514f90a --- /dev/null +++ b/example/Example.App/Properties/SubProperties/Properties.resx @@ -0,0 +1,14 @@ + + + + Value1 + This is a greeting message. + + + {1}Value2{0}{{2}} + 2 = prefix + + + 0xDEADBEEF + + \ No newline at end of file diff --git a/example/Example.App/Resources.resx b/example/Example.App/Resources.resx new file mode 100644 index 0000000..514f90a --- /dev/null +++ b/example/Example.App/Resources.resx @@ -0,0 +1,14 @@ + + + + Value1 + This is a greeting message. + + + {1}Value2{0}{{2}} + 2 = prefix + + + 0xDEADBEEF + + \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..bc6a680 --- /dev/null +++ b/flake.lock @@ -0,0 +1,26 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1684364070, + "narHash": "sha256-+bmqPSEQBePWwmfwxUX8kvJLyg8OM9mRKnDi5qB+m1s=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "2e6eb88c9ab70147e6087d37c833833fd4a907e5", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-22.11-small", + "type": "indirect" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..c72218c --- /dev/null +++ b/flake.nix @@ -0,0 +1,158 @@ +{ + description = ""; + inputs.nixpkgs.url = "nixpkgs/nixos-22.11-small"; + outputs = { self, nixpkgs }: + let + name = "ResourceString.Net"; + version = "0.0.1"; + supportedSystems = + [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; }); + scripts = forAllSystems (system: { + updateFlakeLockFile = nixpkgsFor.${system}.writeScript "update_flake_lock.sh" '' + nix --experimental-features 'nix-command flakes' flake lock --update-input nixpkgs + nix --experimental-features 'nix-command flakes' build + ''; + + updateNugetLockFile = nixpkgsFor.${system}.writeScript "update_nuget_lock.sh" '' + temp_package_folder=$(mktemp -d) + dotnet restore ResourceString.Net --packages "$temp_package_folder" + ${nixpkgsFor.${system}.nuget-to-nix}/bin/nuget-to-nix "$temp_package_folder" > ResourceString.Net/deps.nix + rm -rf "$temp_package_folder" + ''; + + autoTag = nixpkgsFor.${system}.writeScript "auto_tag.sh" '' + git tag --force v${version} + git push origin v${version} + ''; + updateLogicResourceFile = nixpkgsFor.${system}.writeScript "update_logic_resource_file.sh" '' + dotnet run --project ResourceString.Net.App.Console ResourceString.Net.Logic/Properties/Resources.resx ResourceString.Net.Logic.Properties > ResourceString.Net.Logic/Properties/Resources.cs.tmp + mv ResourceString.Net.Logic/Properties/Resources.cs.tmp ResourceString.Net.Logic/Properties/Resources.cs + ''; + runAllTests = nixpkgsFor.${system}.writeScript "run_all_tests.sh" '' + dotnet test *.Tests + ''; + buildAllAssemblies = nixpkgsFor.${system}.writeScript "build_all_assemblies.sh" '' + # Find all .csproj files below the current folder + csproj_files=$(find . -name '*.csproj') + + # Create a temporary file to store the number of dependencies for each project + temp_file=$(mktemp) + + # Loop through each .csproj file and count its number of ResourceString dependencies + for file in $csproj_files + do + count=$(cat "$file" | grep -c "> "$temp_file" + done + + # Sort the projects by their count of dependencies and path depth, in ascending order + sorted_projects=$(sort -nk1 -nk2 -nk3 -t' ' "$temp_file" | awk '{print $4}') + + # Loop through the sorted projects and build them + while read -r file + do + echo "Building project: $file" + dotnet build "$file" + done << EOF + $sorted_projects + EOF + + # Remove the temporary file + rm "$temp_file" + + ''; + + cleanAllAssemblies = nixpkgsFor.${system}.writeScript "clean_all_assemblies.sh" '' + csproj_files=$(find . -name '*.csproj') + for file in $csproj_files + do + dotnet clean "$file" + done + ''; + runDefaultApp = nixpkgsFor.${system}.writeScript "run_default_app.sh" '' + dotnet run --project ResourceString.Net.App.Console $@ + ''; + createNugetPackage = nixpkgsFor.${system}.writeScript "run_default_app.sh" '' + dotnet run --project ResourceString.Net.App.Console $@ + ''; + }); + + in + rec { + packages = forAllSystems (system: { + default = nixpkgsFor.${system}.buildDotnetModule { + pname = name; + version = version; + + src = self; + + projectFile = [ + "ResourceString.Net/ResourceString.Net.csproj" + "ResourceString.Net.App.Console/ResourceString.Net.App.Console.csproj" + "example/Example.App/Example.App.csproj" + ]; + + nugetDeps = ./ResourceString.Net/deps.nix; + projectReferences = [ ]; + + dotnet-sdk = nixpkgsFor.${system}.dotnet-sdk; + dotnet-runtime = nixpkgsFor.${system}.dotnet-runtime; + executables = [ "ResourceString.Net.App.Console" "Example.App" ]; + packNupkg = true; + runtimeDeps = [ ]; + }; + }); + + apps = forAllSystems (system: { + default = { + type = "app"; + program = "${scripts.${system}.runDefaultApp}"; + }; + devTasks = { + updateFlakeLock = { + type = "app"; + program = "${scripts.${system}.updateFlakeLockFile}"; + }; + updateNugetLock = { + type = "app"; + program = "${scripts.${system}.updateNugetLockFile}"; + }; + autoTag = { + type = "app"; + program = "${scripts.${system}.autoTag}"; + }; + updateLogicResourceFile = { + type = "app"; + program = "${scripts.${system}.updateLogicResourceFile}"; + }; + runAllTests = { + type = "app"; + program = "${scripts.${system}.runAllTests}"; + }; + buildAllAssemblies = { + type = "app"; + program = "${scripts.${system}.buildAllAssemblies}"; + }; + cleanAllAssemblies = { + type = "app"; + program = "${scripts.${system}.cleanAllAssemblies}"; + }; + }; + }); + + devShells = forAllSystems (system: { + default = nixpkgsFor.${system}.mkShell { + name = "dev-shell"; + packages = + [ nixpkgsFor.${system}.dotnet-sdk nixpkgsFor.${system}.nixfmt ]; + shellHook = '' + alias nixe="nix --experimental-features 'nix-command flakes'" + ''; + }; + }); + }; +}