Skip to content

Commit

Permalink
3.5.0: UpdateResoureKey command added
Browse files Browse the repository at this point in the history
  • Loading branch information
bdongus committed Apr 9, 2024
1 parent d63b898 commit 61c3b4f
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 9 deletions.
1 change: 1 addition & 0 deletions idee5.Globalization.Test/UnitTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public void MyTestInitialize() {
_connection = new SqliteConnection("DataSource=:memory:");
_connection.Open();
contextOptions.UseSqlite(_connection);
contextOptions.LogTo(message => System.Diagnostics.Debug.WriteLine(message));
contextOptions.EnableSensitiveDataLogging();
context = new GlobalizationDbContext(contextOptions.Options);
context.Database.EnsureDeleted();
Expand Down
77 changes: 77 additions & 0 deletions idee5.Globalization.Test/UpdateResourceKeyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using idee5.Globalization.Commands;
using idee5.Globalization.Models;

using MELT;

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace idee5.Globalization.Test;

[TestClass]
public class UpdateResourceKeyTests : UnitTestBase {
private const string _resourceSet = "UpdateResourceKey";

[TestInitialize]
public void TestInitialize() {
if (!context.Resources.Any(Specifications.InResourceSet(_resourceSet))) {
resourceUnitOfWork.ResourceRepository.Add(new Resource { Id = "DeRemove", ResourceSet = _resourceSet, BinFile = null, Textfile = null, Comment = null, Customer = "", Industry = "", Language = "de", Value = "xx" });
resourceUnitOfWork.ResourceRepository.Add(new Resource { Id = "DeRemove", ResourceSet = _resourceSet, BinFile = null, Textfile = null, Comment = null, Customer = "", Industry = "", Language = "en-GB", Value = "xxx" });
resourceUnitOfWork.ResourceRepository.Add(new Resource { Id = "LogTest", ResourceSet = _resourceSet, BinFile = null, Textfile = null, Comment = null, Customer = "", Industry = "", Language = "en-GB", Value = "xxx" });
context.SaveChanges();
}
}

[TestMethod]
public async Task CanCreateUpdateAndDeleteTranslation() {
// Arrange
var handler = new UpdateResourceKeyCommandHandler(resourceUnitOfWork, new NullLogger<UpdateResourceKeyCommandHandler>());
var rk = new ResourceKey() { ResourceSet = _resourceSet, Id ="DeRemove" };
var translations = new Dictionary<string, string> {
{ "en-GB", "lsmf" },
{ "it", "xyz" }
};
var command = new UpdateResourceKeyCommand(rk, translations.ToImmutableDictionary());

// Act
await handler.HandleAsync(command, CancellationToken.None).ConfigureAwait(false);

// Assert
var rsc = await resourceUnitOfWork.ResourceRepository.GetAsync(r => r.ResourceSet == _resourceSet).ConfigureAwait(false);
Assert.AreEqual(2, rsc.Count);
Assert.AreEqual("lsmf", rsc.SingleOrDefault(r => r.Language == "en-GB")?.Value);
Assert.AreEqual("xyz", rsc.SingleOrDefault(r => r.Language == "it")?.Value);
}

[TestMethod]
public async Task CanCreateLogs() {
// Arrange
var loggerFactory = TestLoggerFactory.Create();
var logger = loggerFactory.CreateLogger<UpdateResourceKeyCommandHandler>();
var handler = new UpdateResourceKeyCommandHandler(resourceUnitOfWork, logger);
var rk = new ResourceKey() { ResourceSet = _resourceSet, Id ="LogTest" };
var translations = new Dictionary<string, string> {
{ "en-GB", "lsmf" },
{ "it", "xyz" }
};
var command = new UpdateResourceKeyCommand(rk, translations.ToImmutableDictionary());

// Act
await handler.HandleAsync(command, CancellationToken.None).ConfigureAwait(false);


// Assert
Assert.AreEqual(4, loggerFactory.Sink.LogEntries.Count());
// 2 create/update events
Assert.AreEqual(2, loggerFactory.Sink.LogEntries.Count(le => le.EventId.Id == 4));
Assert.AreEqual(2, loggerFactory.Sink.LogEntries.Single(le =>le.EventId == 2).Properties.Single(p => p.Key == "Count").Value);
}
}
2 changes: 2 additions & 0 deletions idee5.Globalization.Test/idee5.Globalization.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

<ItemGroup>
<PackageReference Include="idee5.Common.Data" version="2.2.0" />
<PackageReference Include="MELT" Version="0.9.0" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
Expand All @@ -43,6 +44,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\idee5.Globalization.EFCore\idee5.Globalization.EFCore.csproj" />
<ProjectReference Include="..\idee5.Globalization\idee5.Globalization.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Resources\Icon1.ico">
Expand Down
22 changes: 22 additions & 0 deletions idee5.Globalization/Commands/UpdateResourceKeyCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using idee5.Globalization.Models;

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Text;

namespace idee5.Globalization.Commands;
/// <summary>
/// The update resource key command
/// </summary>
public record UpdateResourceKeyCommand : ResourceKey {
public UpdateResourceKeyCommand(ResourceKey original, ImmutableDictionary<string, string> translations) : base(original) {
Translations = translations ?? throw new ArgumentNullException(nameof(translations));
}

/// <summary>
/// Translations of the <see cref="ResourceKey">.
/// The dictionary key is the <see cref="Resource.Language"/>, the value is the <see cref="Resource.Value"/>
/// </summary>
public ImmutableDictionary<string, string> Translations { get; set; }
}
46 changes: 46 additions & 0 deletions idee5.Globalization/Commands/UpdateResourceKeyCommandHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using idee5.Common;
using idee5.Globalization.Models;
using idee5.Globalization.Repositories;

using Microsoft.Extensions.Logging;

using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace idee5.Globalization.Commands;

/// <summary>
/// The update resource key command handler. Removes translations NOT in the given list.
/// </summary>
public class UpdateResourceKeyCommandHandler : ICommandHandlerAsync<UpdateResourceKeyCommand> {
private readonly IResourceUnitOfWork _unitOfWork;
private readonly ILogger<UpdateResourceKeyCommandHandler> _logger;

public UpdateResourceKeyCommandHandler(IResourceUnitOfWork unitOfWork, ILogger<UpdateResourceKeyCommandHandler> logger) {
_unitOfWork = unitOfWork;
_logger = logger;
}

/// <inheritdoc/>
public async Task HandleAsync(UpdateResourceKeyCommand command, CancellationToken cancellationToken = default) {
_logger.TranslationsReceived(command.Translations.Count, command);
Resource baseResource = new() {
ResourceSet = command.ResourceSet,
Id = command.Id,
Customer = command.Customer,
Industry = command.Industry
};
// first remove all missing translations
_logger.RemovingTranslations();
await _unitOfWork.ResourceRepository.RemoveAsync(Specifications.OfResourceKey(baseResource) & !Specifications.TranslatedTo(command.Translations.Keys), cancellationToken).ConfigureAwait(false);

// then update or add the given translations
foreach (var translation in command.Translations) {
Resource rsc = baseResource with { Language = translation.Key, Value = translation.Value };
_logger.CreateOrUpdateResource(rsc);
await _unitOfWork.ResourceRepository.UpdateOrAddAsync(rsc, cancellationToken).ConfigureAwait(false);
}
await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
}
11 changes: 10 additions & 1 deletion idee5.Globalization/Log.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
using Microsoft.Extensions.Logging;
using idee5.Globalization.Models;

using Microsoft.Extensions.Logging;

namespace idee5.Globalization;
internal static partial class Log {
[LoggerMessage(1, LogLevel.Warning, "Resx file '{FilePath}' not found!")]
public static partial void ResxFileNotFound(this ILogger logger, string FilePath);
[LoggerMessage(2,LogLevel.Debug, "Received {Count} translations for {Resource}")]
public static partial void TranslationsReceived(this ILogger logger, int Count, ResourceKey Resource);

[LoggerMessage(3, LogLevel.Debug, "Removing translations ...")]
public static partial void RemovingTranslations(this ILogger logger);
[LoggerMessage(4, LogLevel.Debug, "Create or update Resource: {Resource}")]
public static partial void CreateOrUpdateResource(this ILogger logger, Resource Resource);
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public async Task<IEnumerable<Resource>> HandleAsync(GetAllParlanceResourcesQuer
ASpec<Resource> customerClause = String.IsNullOrEmpty(query.CustomerId) ? CustomerNeutral : CustomerParlance(query.CustomerId);
ASpec<Resource> whereClause = industryClause & customerClause;
if (query.LocalResources != null)
whereClause &= query.LocalResources == true ? LocalResources : !LocalResources;
whereClause &= query.LocalResources == true ? IsLocalResources : !IsLocalResources;
List<Resource> resList = await _repository.GetAsync(whereClause, cancellationToken).ConfigureAwait(false);
// TODO: Let the database do the ordering and grouping
return resList.OrderBy(r => r.ResourceSet).ThenBy(r => r.Language).ThenBy(r => r.Id).ThenByDescending(r => r.Industry).ThenByDescending(r => r.Customer)
Expand Down
3 changes: 2 additions & 1 deletion idee5.Globalization/Repositories/AResourceRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,12 @@ public override async Task UpdateOrAddAsync(Resource resource, CancellationToken
throw new ArgumentNullException(nameof(resource.Value));
}

// from the domain perspective the following properties can be NULL
// from the database perspective composite key columns cannot
resource.Language ??= String.Empty;
if (resource.Language.Length > 0)
_ = new CultureInfo(resource.Language); // throws an exception if not valid.

// default values
resource.Industry ??= String.Empty;
resource.Customer ??= String.Empty;

Expand Down
29 changes: 25 additions & 4 deletions idee5.Globalization/Specifications.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using idee5.Globalization.Models;
using idee5.Common;
using idee5.Globalization.Models;
using NSpecifications;
using System;
using System.Collections.Generic;
using System.Linq;

namespace idee5.Globalization;
/// <summary>
Expand Down Expand Up @@ -42,7 +45,7 @@ public static class Specifications {
/// <summary>
/// Check if <see cref="Resource.ResourceSet"/> contains a dot (".")
/// </summary>
public static readonly ASpec<Resource> LocalResources = new Spec<Resource>(r => r.ResourceSet.Contains("."));
public static readonly ASpec<Resource> IsLocalResources = new Spec<Resource>(r => r.ResourceSet.Contains("."));

#endregion Public Fields

Expand Down Expand Up @@ -124,8 +127,8 @@ public static class Specifications {
/// <summary>
/// Search the for a value in the resource set, id, value,industry, customer or comment
/// </summary>
/// <param name="resourceSet">Resource set to search in.</param>
/// <param name="searchValue">The search value.</param>
/// <param name="resourceSet">Resource set to search in</param>
/// <param name="searchValue">The search value</param>
/// <returns>An ASpec</returns>
public static ASpec<Resource> ContainsInResourceSet(string resourceSet, string searchValue) => new Spec<Resource>(r =>
r.ResourceSet == resourceSet
Expand All @@ -135,5 +138,23 @@ public static class Specifications {
|| (r.Customer != null && r.Customer.Contains(searchValue))
|| (r.Comment !=null && r.Comment.Contains(searchValue)))
);

/// <summary>
/// Select all resources of a specific <see cref="ResourceKey"/>
/// </summary>
/// <param name="resourceKey">The resource key</param>
/// <returns>An ASpec</returns>
public static ASpec<Resource> OfResourceKey(ResourceKey resourceKey) => new Spec<Resource>(r =>
r.ResourceSet == resourceKey.ResourceSet
&& r.Id == resourceKey.Id
&& (r.Industry == resourceKey.Industry || r.Industry == "" && resourceKey.Industry == null)
&& (r.Customer == resourceKey.Customer || r.Customer == "" && resourceKey.Customer == null));

/// <summary>
/// Check if <see cref="Resource.Language"/> is one of the given language codes
/// </summary>
/// <param name="translations">The language codes</param>
/// <returns>An ASpec</returns>
public static ASpec<Resource> TranslatedTo(IEnumerable<string> translations) => new Spec<Resource>(r => translations.Contains(r.Language ?? ""));
#endregion Public Methods
}
4 changes: 2 additions & 2 deletions idee5.Globalization/idee5.Globalization.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
<Description>Globalization extensions. Enables database support for localization resources and parlances for industries and customers..</Description>
<Company>idee5</Company>
<Copyright>© idee5 2016 - 2024</Copyright>
<Version>3.4.0</Version>
<Version>3.5.0</Version>
<PackageTags>idee5, Globalization, Localization</PackageTags>
<PackageReleaseNotes>Resx import updated to AsyncDataImporter</PackageReleaseNotes>
<PackageReleaseNotes>Update resource key added</PackageReleaseNotes>
<Nullable>enable</Nullable>
<Authors>Bernd Dongus</Authors>
<Title>Globalization tool for parlances for industries and customers</Title>
Expand Down

0 comments on commit 61c3b4f

Please sign in to comment.