diff --git a/.ci/appsettings.override.postgres.docker.json b/.ci/appsettings.override.postgres.docker.json index 061b69368f..2705554bee 100644 --- a/.ci/appsettings.override.postgres.docker.json +++ b/.ci/appsettings.override.postgres.docker.json @@ -95,6 +95,76 @@ } } }, + "Tags": { + "Application": { + "SupportedLanguages": [ + "de", + "en" + ], + "TagsForAttributeValueTypes": { + "IdentityFileReference": { + "schulabschluss": { + "displayNames": { + "de": "Abschluss", + "en": "Degree" + }, + "children": { + "realschule": { + "displayNames": { + "de": "Realschule", + "en": "Secondary School" + }, + "children": { + "zeugnis": { + "displayNames": { + "de": "Zeugnis", + "en": "Diploma" + } + } + } + }, + "gymnasium": { + "displayNames": { + "de": "Gymnasium", + "en": "High School" + }, + "children": { + "zeugnis": { + "displayNames": { + "de": "Zeugnis", + "en": "Diploma" + } + } + } + } + } + } + }, + "PhoneNumber": { + "notfall": { + "displayNames": { + "de": "Notfallkontakt", + "en": "Emergency Contact" + } + } + }, + "StreetAddress": { + "lieferung": { + "displayNames": { + "de": "Lieferadresse", + "en": "Deliver Address" + } + }, + "heimat": { + "displayNames": { + "de": "Heimatadresse", + "en": "Home Address" + } + } + } + } + } + }, "Tokens": { "Infrastructure": { "SqlDatabase": { diff --git a/.ci/appsettings.override.postgres.local.json b/.ci/appsettings.override.postgres.local.json index d8f49ffaf7..7321d4ae9c 100644 --- a/.ci/appsettings.override.postgres.local.json +++ b/.ci/appsettings.override.postgres.local.json @@ -95,6 +95,76 @@ } } }, + "Tags": { + "Application": { + "SupportedLanguages": [ + "de", + "en" + ], + "TagsForAttributeValueTypes": { + "IdentityFileReference": { + "schulabschluss": { + "displayNames": { + "de": "Abschluss", + "en": "Degree" + }, + "children": { + "realschule": { + "displayNames": { + "de": "Realschule", + "en": "Secondary School" + }, + "children": { + "zeugnis": { + "displayNames": { + "de": "Zeugnis", + "en": "Diploma" + } + } + } + }, + "gymnasium": { + "displayNames": { + "de": "Gymnasium", + "en": "High School" + }, + "children": { + "zeugnis": { + "displayNames": { + "de": "Zeugnis", + "en": "Diploma" + } + } + } + } + } + } + }, + "PhoneNumber": { + "notfall": { + "displayNames": { + "de": "Notfallkontakt", + "en": "Emergency Contact" + } + } + }, + "StreetAddress": { + "lieferung": { + "displayNames": { + "de": "Lieferadresse", + "en": "Deliver Address" + } + }, + "heimat": { + "displayNames": { + "de": "Heimatadresse", + "en": "Home Address" + } + } + } + } + } + }, "Tokens": { "Infrastructure": { "SqlDatabase": { diff --git a/.ci/appsettings.override.sqlserver.docker.json b/.ci/appsettings.override.sqlserver.docker.json index 7b8f8880b5..a6102ed26e 100644 --- a/.ci/appsettings.override.sqlserver.docker.json +++ b/.ci/appsettings.override.sqlserver.docker.json @@ -95,6 +95,76 @@ } } }, + "Tags": { + "Application": { + "SupportedLanguages": [ + "de", + "en" + ], + "TagsForAttributeValueTypes": { + "IdentityFileReference": { + "schulabschluss": { + "displayNames": { + "de": "Abschluss", + "en": "Degree" + }, + "children": { + "realschule": { + "displayNames": { + "de": "Realschule", + "en": "Secondary School" + }, + "children": { + "zeugnis": { + "displayNames": { + "de": "Zeugnis", + "en": "Diploma" + } + } + } + }, + "gymnasium": { + "displayNames": { + "de": "Gymnasium", + "en": "High School" + }, + "children": { + "zeugnis": { + "displayNames": { + "de": "Zeugnis", + "en": "Diploma" + } + } + } + } + } + } + }, + "PhoneNumber": { + "notfall": { + "displayNames": { + "de": "Notfallkontakt", + "en": "Emergency Contact" + } + } + }, + "StreetAddress": { + "lieferung": { + "displayNames": { + "de": "Lieferadresse", + "en": "Deliver Address" + } + }, + "heimat": { + "displayNames": { + "de": "Heimatadresse", + "en": "Home Address" + } + } + } + } + } + }, "Tokens": { "Infrastructure": { "SqlDatabase": { diff --git a/.ci/appsettings.override.sqlserver.local.json b/.ci/appsettings.override.sqlserver.local.json index 45bb2811be..74b61bb4e9 100644 --- a/.ci/appsettings.override.sqlserver.local.json +++ b/.ci/appsettings.override.sqlserver.local.json @@ -95,6 +95,76 @@ } } }, + "Tags": { + "Application": { + "SupportedLanguages": [ + "de", + "en" + ], + "TagsForAttributeValueTypes": { + "IdentityFileReference": { + "schulabschluss": { + "displayNames": { + "de": "Abschluss", + "en": "Degree" + }, + "children": { + "realschule": { + "displayNames": { + "de": "Realschule", + "en": "Secondary School" + }, + "children": { + "zeugnis": { + "displayNames": { + "de": "Zeugnis", + "en": "Diploma" + } + } + } + }, + "gymnasium": { + "displayNames": { + "de": "Gymnasium", + "en": "High School" + }, + "children": { + "zeugnis": { + "displayNames": { + "de": "Zeugnis", + "en": "Diploma" + } + } + } + } + } + } + }, + "PhoneNumber": { + "notfall": { + "displayNames": { + "de": "Notfallkontakt", + "en": "Emergency Contact" + } + } + }, + "StreetAddress": { + "lieferung": { + "displayNames": { + "de": "Lieferadresse", + "en": "Deliver Address" + } + }, + "heimat": { + "displayNames": { + "de": "Heimatadresse", + "en": "Home Address" + } + } + } + } + } + }, "Tokens": { "Infrastructure": { "SqlDatabase": { diff --git a/Applications/ConsumerApi/src/ConsumerApi.csproj b/Applications/ConsumerApi/src/ConsumerApi.csproj index 3c0a17c437..c214c725d9 100644 --- a/Applications/ConsumerApi/src/ConsumerApi.csproj +++ b/Applications/ConsumerApi/src/ConsumerApi.csproj @@ -37,6 +37,7 @@ + diff --git a/Applications/ConsumerApi/src/Program.cs b/Applications/ConsumerApi/src/Program.cs index a6dc80cf4f..0f2df63a56 100644 --- a/Applications/ConsumerApi/src/Program.cs +++ b/Applications/ConsumerApi/src/Program.cs @@ -26,6 +26,7 @@ using Backbone.Modules.Relationships.Infrastructure.Persistence.Database; using Backbone.Modules.Synchronization.ConsumerApi; using Backbone.Modules.Synchronization.Infrastructure.Persistence.Database; +using Backbone.Modules.Tags.ConsumerApi; using Backbone.Modules.Tokens.ConsumerApi; using Backbone.Modules.Tokens.Infrastructure.Persistence.Database; using Backbone.Tooling.Extensions; @@ -147,6 +148,7 @@ static void ConfigureServices(IServiceCollection services, IConfiguration config .AddModule(configuration) .AddModule(configuration) .AddModule(configuration) + .AddModule(configuration) .AddModule(configuration); var quotasSqlDatabaseConfiguration = parsedConfiguration.Modules.Quotas.Infrastructure.SqlDatabase; diff --git a/Applications/ConsumerApi/src/http/Tags/ListTags.bru b/Applications/ConsumerApi/src/http/Tags/ListTags.bru new file mode 100644 index 0000000000..d1ac3fc138 --- /dev/null +++ b/Applications/ConsumerApi/src/http/Tags/ListTags.bru @@ -0,0 +1,11 @@ +meta { + name: /Tags + type: http + seq: 1 +} + +get { + url: {{baseUrl}}/Tags + body: none + auth: none +} diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Tags/GET.feature b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Tags/GET.feature new file mode 100644 index 0000000000..79faf4f3c8 --- /dev/null +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Tags/GET.feature @@ -0,0 +1,10 @@ +@Integration +Feature: GET /Tags + +User requests available Tags + + Scenario: Requesting the available Tags + When A GET request to the /Tags endpoint gets sent + Then the response status code is 200 (OK) + And the response supports the English language + And the response attributes contain tags diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/TagsStepDefinitions.cs b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/TagsStepDefinitions.cs new file mode 100644 index 0000000000..1c2cfe05f5 --- /dev/null +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/TagsStepDefinitions.cs @@ -0,0 +1,43 @@ +using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types; +using Backbone.ConsumerApi.Sdk.Endpoints.Tags.Types.Responses; +using Backbone.ConsumerApi.Tests.Integration.Contexts; +using Backbone.ConsumerApi.Tests.Integration.Helpers; + +namespace Backbone.ConsumerApi.Tests.Integration.StepDefinitions; + +[Binding] +public class TagsStepDefinitions +{ + private readonly ResponseContext _responseContext; + private readonly ClientPool _clientPool; + + private ApiResponse? _listTagsResponse; + + public TagsStepDefinitions(ResponseContext responseContext, ClientPool clientPool) + { + _responseContext = responseContext; + _clientPool = clientPool; + } + + [When("A GET request to the /Tags endpoint gets sent")] + public async Task WhenAGETRequestToTheTagsEndpointGetsSent() + { + var client = _clientPool.Anonymous; + _responseContext.WhenResponse = _listTagsResponse = await client.Tags.ListTags(); + } + + [Then("the response supports the English language")] + public void AndTheResponseSupportsTheEnglishLanguage() + { + _listTagsResponse!.Result!.SupportedLanguages.Should().Contain("en"); + } + + [Then("the response attributes contain tags")] + public void AndTheResponseAttributesContainTags() + { + foreach (var attr in _listTagsResponse!.Result!.TagsForAttributeValueTypes.Values) + { + attr.Should().NotBeEmpty(); + } + } +} diff --git a/Backbone.sln b/Backbone.sln index f4fb2411bb..502f7b2fc6 100644 --- a/Backbone.sln +++ b/Backbone.sln @@ -370,6 +370,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{1BFEE2F6-D EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DatabaseMigrator.Tests", "Applications\DatabaseMigrator\test\DatabaseMigrator.Tests\DatabaseMigrator.Tests.csproj", "{553F1C8B-E099-4856-9416-67C103D3D194}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tags", "Tags", "{2E8887F1-55BD-4751-A522-0377C80DC70B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2A9D40E4-AB8F-49B5-878B-A8C7763EE609}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tags.Application", "Modules\Tags\src\Tags.Application\Tags.Application.csproj", "{04A11F4A-6D3E-484E-89EC-041D223CED21}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tags.ConsumerApi", "Modules\Tags\src\Tags.ConsumerApi\Tags.ConsumerApi.csproj", "{2BE30D30-CCFF-453E-AEAC-9CB3EF677058}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tags.Domain", "Modules\Tags\src\Tags.Domain\Tags.Domain.csproj", "{3A107CCD-A7E9-448B-82DF-0FD87D1ABA9E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tags.Infrastructure", "Modules\Tags\src\Tags.Infrastructure\Tags.Infrastructure.csproj", "{98C16B16-7ECE-4E23-8D6C-2CA372EC310C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -808,6 +820,22 @@ Global {553F1C8B-E099-4856-9416-67C103D3D194}.Debug|Any CPU.Build.0 = Debug|Any CPU {553F1C8B-E099-4856-9416-67C103D3D194}.Release|Any CPU.ActiveCfg = Release|Any CPU {553F1C8B-E099-4856-9416-67C103D3D194}.Release|Any CPU.Build.0 = Release|Any CPU + {04A11F4A-6D3E-484E-89EC-041D223CED21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04A11F4A-6D3E-484E-89EC-041D223CED21}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04A11F4A-6D3E-484E-89EC-041D223CED21}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04A11F4A-6D3E-484E-89EC-041D223CED21}.Release|Any CPU.Build.0 = Release|Any CPU + {2BE30D30-CCFF-453E-AEAC-9CB3EF677058}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2BE30D30-CCFF-453E-AEAC-9CB3EF677058}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2BE30D30-CCFF-453E-AEAC-9CB3EF677058}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2BE30D30-CCFF-453E-AEAC-9CB3EF677058}.Release|Any CPU.Build.0 = Release|Any CPU + {3A107CCD-A7E9-448B-82DF-0FD87D1ABA9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A107CCD-A7E9-448B-82DF-0FD87D1ABA9E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A107CCD-A7E9-448B-82DF-0FD87D1ABA9E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A107CCD-A7E9-448B-82DF-0FD87D1ABA9E}.Release|Any CPU.Build.0 = Release|Any CPU + {98C16B16-7ECE-4E23-8D6C-2CA372EC310C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98C16B16-7ECE-4E23-8D6C-2CA372EC310C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98C16B16-7ECE-4E23-8D6C-2CA372EC310C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98C16B16-7ECE-4E23-8D6C-2CA372EC310C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -977,6 +1005,12 @@ Global {D28F10DA-33BD-4F08-B736-1B7AB53026BD} = {CA03350A-2013-4D18-B68D-8C0F680DBA39} {1BFEE2F6-D514-4458-994A-3DD106405990} = {A82D9F44-8B80-4803-BA97-7D737DEF6A98} {553F1C8B-E099-4856-9416-67C103D3D194} = {1BFEE2F6-D514-4458-994A-3DD106405990} + {2E8887F1-55BD-4751-A522-0377C80DC70B} = {0EAF57B8-E97C-469E-A74B-596D78C978B2} + {2A9D40E4-AB8F-49B5-878B-A8C7763EE609} = {2E8887F1-55BD-4751-A522-0377C80DC70B} + {04A11F4A-6D3E-484E-89EC-041D223CED21} = {2A9D40E4-AB8F-49B5-878B-A8C7763EE609} + {2BE30D30-CCFF-453E-AEAC-9CB3EF677058} = {2A9D40E4-AB8F-49B5-878B-A8C7763EE609} + {3A107CCD-A7E9-448B-82DF-0FD87D1ABA9E} = {2A9D40E4-AB8F-49B5-878B-A8C7763EE609} + {98C16B16-7ECE-4E23-8D6C-2CA372EC310C} = {2A9D40E4-AB8F-49B5-878B-A8C7763EE609} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1F3BD2C6-7CB3-450F-A21A-23EA520D5B7A} diff --git a/Modules/Tags/src/Tags.Application/ApplicationOptions.cs b/Modules/Tags/src/Tags.Application/ApplicationOptions.cs new file mode 100644 index 0000000000..e718810507 --- /dev/null +++ b/Modules/Tags/src/Tags.Application/ApplicationOptions.cs @@ -0,0 +1,75 @@ +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using Backbone.Modules.Tags.Domain; + +namespace Backbone.Modules.Tags.Application; + +[ContainsValidTags] +public class ApplicationOptions +{ + [Required] + public List SupportedLanguages { get; set; } = []; + + public Dictionary> TagsForAttributeValueTypes { get; set; } = []; +} + +[AttributeUsage(AttributeTargets.Class)] +public class ContainsValidTagsAttribute : ValidationAttribute +{ + private static readonly CultureInfo[] CULTURES = CultureInfo.GetCultures(CultureTypes.AllCultures & ~CultureTypes.NeutralCultures); + + private List _supportedLanguages = []; + + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (value is not ApplicationOptions applicationOptions) return new ValidationResult($"The attribute can only be used for {nameof(ApplicationOptions)}."); + + _supportedLanguages = applicationOptions.SupportedLanguages; + var result = ValidateLanguages(); + if (result != ValidationResult.Success) return result; + + foreach (var (attributeName, tags) in applicationOptions.TagsForAttributeValueTypes) + { + foreach (var (tagName, tag) in tags) + { + result = ValidateTag([attributeName, tagName], tag); + if (result != ValidationResult.Success) return result; + } + } + + return ValidationResult.Success; + } + + private ValidationResult? ValidateLanguages() + { + if (!_supportedLanguages.Contains("en")) return new ValidationResult("The tags have to support the English language.", [nameof(ApplicationOptions.SupportedLanguages)]); + + var invalidLanguageEntries = _supportedLanguages.Where(value => CULTURES.All(c => c.TwoLetterISOLanguageName != value)).ToList(); + if (invalidLanguageEntries.Count != 0) + return new ValidationResult($"The language entries \"{Enumerate(invalidLanguageEntries)}\" are not valid language codes.", [nameof(ApplicationOptions.SupportedLanguages)]); + + return ValidationResult.Success; + } + + private ValidationResult? ValidateTag(IEnumerable nameParts, TagInfo tag) + { + var notSupportedLanguages = tag.DisplayNames.Keys.Except(_supportedLanguages).ToList(); + var notImplementedLanguages = _supportedLanguages.Except(tag.DisplayNames.Keys).ToList(); + + if (notSupportedLanguages.Count != 0) + return new ValidationResult($"The languages \"{Enumerate(notSupportedLanguages)}\" are unsupported", [GetPathOfProperty(nameParts)]); + if (notImplementedLanguages.Count != 0) + return new ValidationResult($"A display name for the language(s) \"{Enumerate(notImplementedLanguages)}\" is required.", [GetPathOfProperty(nameParts)]); + + foreach (var (childName, child) in tag.Children) + { + var result = ValidateTag(nameParts.Append(childName), child); + if (result != ValidationResult.Success) return result; + } + + return ValidationResult.Success; + } + + private static string GetPathOfProperty(IEnumerable nameParts) => string.Join('.', nameParts); + private static string Enumerate(IEnumerable names) => string.Join(',', names); +} diff --git a/Modules/Tags/src/Tags.Application/Extensions/IServiceCollectionExtensions.cs b/Modules/Tags/src/Tags.Application/Extensions/IServiceCollectionExtensions.cs new file mode 100644 index 0000000000..467c502c4c --- /dev/null +++ b/Modules/Tags/src/Tags.Application/Extensions/IServiceCollectionExtensions.cs @@ -0,0 +1,14 @@ +using Backbone.Modules.Tags.Application.Tags.Queries.ListTags; +using Microsoft.Extensions.DependencyInjection; + +namespace Backbone.Modules.Tags.Application.Extensions; + +public static class IServiceCollectionExtensions +{ + public static void AddApplication(this IServiceCollection services) + { + services.AddMediatR(c => c + .RegisterServicesFromAssemblyContaining() + ); + } +} diff --git a/Modules/Tags/src/Tags.Application/Infrastructure/Persistence/Repository/ITagsRepository.cs b/Modules/Tags/src/Tags.Application/Infrastructure/Persistence/Repository/ITagsRepository.cs new file mode 100644 index 0000000000..9815619c40 --- /dev/null +++ b/Modules/Tags/src/Tags.Application/Infrastructure/Persistence/Repository/ITagsRepository.cs @@ -0,0 +1,9 @@ +using Backbone.Modules.Tags.Domain; + +namespace Backbone.Modules.Tags.Application.Infrastructure.Persistence.Repository; + +public interface ITagsRepository +{ + public IEnumerable GetSupportedLanguages(); + public Dictionary> GetAttributes(); +} diff --git a/Modules/Tags/src/Tags.Application/Tags.Application.csproj b/Modules/Tags/src/Tags.Application/Tags.Application.csproj new file mode 100644 index 0000000000..688eca0f04 --- /dev/null +++ b/Modules/Tags/src/Tags.Application/Tags.Application.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Modules/Tags/src/Tags.Application/Tags/Queries/ListTags/Handler.cs b/Modules/Tags/src/Tags.Application/Tags/Queries/ListTags/Handler.cs new file mode 100644 index 0000000000..49cc334725 --- /dev/null +++ b/Modules/Tags/src/Tags.Application/Tags/Queries/ListTags/Handler.cs @@ -0,0 +1,25 @@ +using Backbone.Modules.Tags.Application.Infrastructure.Persistence.Repository; +using MediatR; + +namespace Backbone.Modules.Tags.Application.Tags.Queries.ListTags; + +public class Handler : IRequestHandler +{ + private readonly ITagsRepository _tagsRepository; + + public Handler(ITagsRepository tagsRepository) + { + _tagsRepository = tagsRepository; + } + + public Task Handle(ListTagsQuery request, CancellationToken cancellationToken) + { + var response = new ListTagsResponse + { + SupportedLanguages = _tagsRepository.GetSupportedLanguages(), + TagsForAttributeValueTypes = _tagsRepository.GetAttributes() + }; + + return Task.FromResult(response); + } +} diff --git a/Modules/Tags/src/Tags.Application/Tags/Queries/ListTags/ListTagsQuery.cs b/Modules/Tags/src/Tags.Application/Tags/Queries/ListTags/ListTagsQuery.cs new file mode 100644 index 0000000000..d6004cb1bf --- /dev/null +++ b/Modules/Tags/src/Tags.Application/Tags/Queries/ListTags/ListTagsQuery.cs @@ -0,0 +1,5 @@ +using MediatR; + +namespace Backbone.Modules.Tags.Application.Tags.Queries.ListTags; + +public class ListTagsQuery : IRequest; diff --git a/Modules/Tags/src/Tags.Application/Tags/Queries/ListTags/ListTagsResponse.cs b/Modules/Tags/src/Tags.Application/Tags/Queries/ListTags/ListTagsResponse.cs new file mode 100644 index 0000000000..e838121db1 --- /dev/null +++ b/Modules/Tags/src/Tags.Application/Tags/Queries/ListTags/ListTagsResponse.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; +using Backbone.Modules.Tags.Domain; + +namespace Backbone.Modules.Tags.Application.Tags.Queries.ListTags; + +public class ListTagsResponse +{ + public required IEnumerable SupportedLanguages { get; set; } + + [JsonConverter(typeof(PascalCaseDictionaryConverter>))] + public required Dictionary> TagsForAttributeValueTypes { get; set; } +} diff --git a/Modules/Tags/src/Tags.ConsumerApi/Controllers/TagsController.cs b/Modules/Tags/src/Tags.ConsumerApi/Controllers/TagsController.cs new file mode 100644 index 0000000000..ee28cbe550 --- /dev/null +++ b/Modules/Tags/src/Tags.ConsumerApi/Controllers/TagsController.cs @@ -0,0 +1,24 @@ +using Backbone.BuildingBlocks.API.Mvc; +using Backbone.Modules.Tags.Application.Tags.Queries.ListTags; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Backbone.Modules.Tags.ConsumerApi.Controllers; + +[Route("api/v1/[controller]")] +[Authorize("OpenIddict.Validation.AspNetCore")] +public class TagsController : ApiControllerBase +{ + public TagsController(IMediator mediator) : base(mediator) + { + } + + [HttpGet] + [AllowAnonymous] + public async Task ListTags(CancellationToken cancellationToken) + { + var response = await _mediator.Send(new ListTagsQuery(), cancellationToken); + return Ok(response); + } +} diff --git a/Modules/Tags/src/Tags.ConsumerApi/Tags.ConsumerApi.csproj b/Modules/Tags/src/Tags.ConsumerApi/Tags.ConsumerApi.csproj new file mode 100644 index 0000000000..70e3ad5aab --- /dev/null +++ b/Modules/Tags/src/Tags.ConsumerApi/Tags.ConsumerApi.csproj @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Modules/Tags/src/Tags.ConsumerApi/TagsModule.cs b/Modules/Tags/src/Tags.ConsumerApi/TagsModule.cs new file mode 100644 index 0000000000..9dacc76eb4 --- /dev/null +++ b/Modules/Tags/src/Tags.ConsumerApi/TagsModule.cs @@ -0,0 +1,27 @@ +using Backbone.BuildingBlocks.API; +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; +using Backbone.Modules.Tags.Application; +using Backbone.Modules.Tags.Application.Extensions; +using Backbone.Modules.Tags.Infrastructure.Persistence; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Backbone.Modules.Tags.ConsumerApi; + +public class TagsModule : AbstractModule +{ + public override string Name => "Tags"; + + public override void ConfigureServices(IServiceCollection services, IConfigurationSection configuration) + { + services.ConfigureAndValidate(options => configuration.GetSection("Application").Bind(options)); + + services.AddApplication(); + services.AddPersistence(); + } + + public override void ConfigureEventBus(IEventBus eventBus) + { + // No Event bus needed here + } +} diff --git a/Modules/Tags/src/Tags.Domain/TagInfo.cs b/Modules/Tags/src/Tags.Domain/TagInfo.cs new file mode 100644 index 0000000000..6b963d58a5 --- /dev/null +++ b/Modules/Tags/src/Tags.Domain/TagInfo.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Backbone.Modules.Tags.Domain; + +public class TagInfo +{ + [Required] + public Dictionary DisplayNames { get; set; } = []; + + [JsonConverter(typeof(PascalCaseDictionaryConverter))] + public Dictionary Children { get; set; } = []; +} + +public class PascalCaseDictionaryConverter : JsonConverter> +{ + public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotSupportedException("Deserialization is not supported by this converter."); + } + + public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + foreach (var key in value.Keys) + { + var value2 = value[key]; + writer.WritePropertyName(key); + JsonSerializer.Serialize(writer, value2, options); + } + + writer.WriteEndObject(); + } +} diff --git a/Modules/Tags/src/Tags.Domain/Tags.Domain.csproj b/Modules/Tags/src/Tags.Domain/Tags.Domain.csproj new file mode 100644 index 0000000000..3a63532952 --- /dev/null +++ b/Modules/Tags/src/Tags.Domain/Tags.Domain.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Modules/Tags/src/Tags.Infrastructure/Persistence/IServiceCollectionExtensions.cs b/Modules/Tags/src/Tags.Infrastructure/Persistence/IServiceCollectionExtensions.cs new file mode 100644 index 0000000000..eb5ab7da5f --- /dev/null +++ b/Modules/Tags/src/Tags.Infrastructure/Persistence/IServiceCollectionExtensions.cs @@ -0,0 +1,12 @@ +using Backbone.Modules.Tags.Infrastructure.Persistence.Repository; +using Microsoft.Extensions.DependencyInjection; + +namespace Backbone.Modules.Tags.Infrastructure.Persistence; + +public static class IServiceCollectionExtensions +{ + public static void AddPersistence(this IServiceCollection services) + { + services.AddRepositories(); + } +} diff --git a/Modules/Tags/src/Tags.Infrastructure/Persistence/Repository/IServiceCollectionExtensions.cs b/Modules/Tags/src/Tags.Infrastructure/Persistence/Repository/IServiceCollectionExtensions.cs new file mode 100644 index 0000000000..974a02f6f2 --- /dev/null +++ b/Modules/Tags/src/Tags.Infrastructure/Persistence/Repository/IServiceCollectionExtensions.cs @@ -0,0 +1,12 @@ +using Backbone.Modules.Tags.Application.Infrastructure.Persistence.Repository; +using Microsoft.Extensions.DependencyInjection; + +namespace Backbone.Modules.Tags.Infrastructure.Persistence.Repository; + +public static class IServiceCollectionExtensions +{ + public static void AddRepositories(this IServiceCollection services) + { + services.AddTransient(); + } +} diff --git a/Modules/Tags/src/Tags.Infrastructure/Persistence/Repository/TagsRepository.cs b/Modules/Tags/src/Tags.Infrastructure/Persistence/Repository/TagsRepository.cs new file mode 100644 index 0000000000..77bf236827 --- /dev/null +++ b/Modules/Tags/src/Tags.Infrastructure/Persistence/Repository/TagsRepository.cs @@ -0,0 +1,21 @@ +using Backbone.Modules.Tags.Application; +using Backbone.Modules.Tags.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Tags.Domain; +using Microsoft.Extensions.Options; + +namespace Backbone.Modules.Tags.Infrastructure.Persistence.Repository; + +public class TagsRepository : ITagsRepository +{ + private readonly List _supportedLanguages; + private readonly Dictionary> _attributes; + + public TagsRepository(IOptions options) + { + _supportedLanguages = options.Value.SupportedLanguages; + _attributes = options.Value.TagsForAttributeValueTypes; + } + + public IEnumerable GetSupportedLanguages() => _supportedLanguages; + public Dictionary> GetAttributes() => _attributes; +} diff --git a/Modules/Tags/src/Tags.Infrastructure/Tags.Infrastructure.csproj b/Modules/Tags/src/Tags.Infrastructure/Tags.Infrastructure.csproj new file mode 100644 index 0000000000..a027b534c3 --- /dev/null +++ b/Modules/Tags/src/Tags.Infrastructure/Tags.Infrastructure.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/Sdks/ConsumerApi.Sdk/src/Client.cs b/Sdks/ConsumerApi.Sdk/src/Client.cs index 36fceaf0a7..3a3b9de046 100644 --- a/Sdks/ConsumerApi.Sdk/src/Client.cs +++ b/Sdks/ConsumerApi.Sdk/src/Client.cs @@ -17,6 +17,7 @@ using Backbone.ConsumerApi.Sdk.Endpoints.RelationshipTemplates; using Backbone.ConsumerApi.Sdk.Endpoints.SyncRuns; using Backbone.ConsumerApi.Sdk.Endpoints.SyncRuns.Types.Requests; +using Backbone.ConsumerApi.Sdk.Endpoints.Tags; using Backbone.ConsumerApi.Sdk.Endpoints.Tokens; using Backbone.Crypto; using Backbone.Crypto.Implementations; @@ -49,6 +50,7 @@ private Client(HttpClient httpClient, Configuration configuration, DeviceData? d Relationships = new RelationshipsEndpoint(endpointClient); RelationshipTemplates = new RelationshipTemplatesEndpoint(endpointClient); SyncRuns = new SyncRunsEndpoint(endpointClient); + Tags = new TagsEndpoint(endpointClient); Tokens = new TokensEndpoint(endpointClient); } @@ -67,6 +69,7 @@ private Client(HttpClient httpClient, Configuration configuration, DeviceData? d public RelationshipsEndpoint Relationships { get; } public RelationshipTemplatesEndpoint RelationshipTemplates { get; } public SyncRunsEndpoint SyncRuns { get; } + public TagsEndpoint Tags { get; } public TokensEndpoint Tokens { get; } // ReSharper restore UnusedAutoPropertyAccessor.Global diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Tags/TagsEndpoint.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Tags/TagsEndpoint.cs new file mode 100644 index 0000000000..917410cc7f --- /dev/null +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Tags/TagsEndpoint.cs @@ -0,0 +1,13 @@ +using Backbone.BuildingBlocks.SDK.Endpoints.Common; +using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types; +using Backbone.ConsumerApi.Sdk.Endpoints.Tags.Types.Responses; + +namespace Backbone.ConsumerApi.Sdk.Endpoints.Tags; + +public class TagsEndpoint(EndpointClient client) : ConsumerApiEndpoint(client) +{ + public async Task> ListTags() + { + return await _client.GetUnauthenticated($"api/{API_VERSION}/Tags"); + } +} diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Tags/Types/Responses/ListTagsResponse.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Tags/Types/Responses/ListTagsResponse.cs new file mode 100644 index 0000000000..a1129e43e3 --- /dev/null +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Tags/Types/Responses/ListTagsResponse.cs @@ -0,0 +1,7 @@ +namespace Backbone.ConsumerApi.Sdk.Endpoints.Tags.Types.Responses; + +public class ListTagsResponse +{ + public required List SupportedLanguages { get; set; } + public required Dictionary TagsForAttributeValueTypes { get; set; } +} diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Tags/Types/Tag.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Tags/Types/Tag.cs new file mode 100644 index 0000000000..e12d9460ef --- /dev/null +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Tags/Types/Tag.cs @@ -0,0 +1,9 @@ +namespace Backbone.ConsumerApi.Sdk.Endpoints.Tags.Types; + +public class TagDefinition : Dictionary; + +public class Tag +{ + public required Dictionary DisplayNames { get; set; } + public TagDefinition Children { get; set; } = []; +} diff --git a/appsettings.override.json b/appsettings.override.json index 8e82844419..69c0bcbf7a 100644 --- a/appsettings.override.json +++ b/appsettings.override.json @@ -122,6 +122,76 @@ } } }, + "Tags": { + "Application": { + "SupportedLanguages": [ + "de", + "en" + ], + "TagsForAttributeValueTypes": { + "IdentityFileReference": { + "schulabschluss": { + "displayNames": { + "de": "Abschluss", + "en": "Degree" + }, + "children": { + "realschule": { + "displayNames": { + "de": "Realschule", + "en": "Secondary School" + }, + "children": { + "zeugnis": { + "displayNames": { + "de": "Zeugnis", + "en": "Diploma" + } + } + } + }, + "gymnasium": { + "displayNames": { + "de": "Gymnasium", + "en": "High School" + }, + "children": { + "zeugnis": { + "displayNames": { + "de": "Zeugnis", + "en": "Diploma" + } + } + } + } + } + } + }, + "PhoneNumber": { + "notfall": { + "displayNames": { + "de": "Notfallkontakt", + "en": "Emergency Contact" + } + } + }, + "StreetAddress": { + "lieferung": { + "displayNames": { + "de": "Lieferadresse", + "en": "Deliver Address" + } + }, + "heimat": { + "displayNames": { + "de": "Heimatadresse", + "en": "Home Address" + } + } + } + } + } + }, "Tokens": { "Infrastructure": { "SqlDatabase": {