Reescrita dos geradores: biblioteca IGenerator, minimal API e novo frontend

- GeradoresService: cada tipo passa a ser um IGenerator (NIF, NIPC, NISS, CC,
  passaporte, IBAN, NIB, cartões, referência MB, pessoa, telefone, código postal,
  email, matrícula, EAN, UUID) registado em GeneratorRegistry; remove DAL/EF
- GeradoresWS: minimal APIs em /api/v1 com OpenAPI + Scalar e health check
- UnitTest: testes de ida e volta, valores conhecidos, detecção e API
- geradoresfe: UI construída a partir dos metadados da API, validador livre,
  exportação CSV/JSON/TXT, nginx com API_UPSTREAM configurável
- Docker, docker-compose e Jenkinsfile actualizados

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 23:51:39 +01:00
parent 0f764b1239
commit d7ea8a9b9e
119 changed files with 7764 additions and 2502 deletions

View File

@@ -0,0 +1,101 @@
using System.ComponentModel;
using GeradoresService;
using GeradoresService.Core;
using Microsoft.AspNetCore.Http.HttpResults;
namespace GeradoresWS;
public static class GeneratorEndpoints
{
private static readonly HashSet<string> ReservedQueryKeys = new(StringComparer.OrdinalIgnoreCase) { "count", "seed" };
public static void MapGeneratorEndpoints(this IEndpointRouteBuilder app)
{
var api = app.MapGroup("/api/v1").WithTags("Geradores");
api.MapGet("/generators", (GeneratorRegistry registry) =>
registry.All.Select(GeneratorDto.From).ToList())
.WithName("ListGenerators")
.WithSummary("Lista todos os geradores e as respectivas opções.");
api.MapGet("/generators/{id}", Results<Ok<GeneratorDto>, NotFound> (string id, GeneratorRegistry registry) =>
registry.Find(id) is { } g ? TypedResults.Ok(GeneratorDto.From(g)) : TypedResults.NotFound())
.WithName("GetGenerator")
.WithSummary("Detalhe de um gerador.");
api.MapGet("/generators/{id}/generate", Generate)
.WithName("Generate")
.WithSummary("Gera um ou mais valores.")
.WithDescription("As opções do gerador passam-se na query string, ex.: /generators/nif/generate?count=10&tipo=5");
api.MapGet("/generators/{id}/validate",
Results<Ok<ValidationResult>, NotFound> (string id, [Description("Valor a validar")] string value, GeneratorRegistry registry) =>
registry.Find(id) is { CanValidate: true } g
? TypedResults.Ok(g.Validate(value))
: TypedResults.NotFound())
.WithName("Validate")
.WithSummary("Valida um valor de um tipo específico.");
api.MapGet("/validate", (string value, GeneratorRegistry registry) =>
new DetectResponse(value, registry.Detect(value)
.Select(m => new DetectMatchDto(m.Generator.Id, m.Generator.Name, m.Result))
.ToList()))
.WithName("Detect")
.WithSummary("Validador livre: identifica o tipo do valor e diz se é válido.");
}
private static Results<Ok<GenerateResponse>, NotFound, ValidationProblem> Generate(
string id,
HttpRequest request,
GeneratorRegistry registry,
[Description("Quantidade (1–1000)")] int count = 1,
[Description("Seed para resultados reprodutíveis")] int? seed = null)
{
var generator = registry.Find(id);
if (generator is null)
return TypedResults.NotFound();
var options = request.Query
.Where(q => !ReservedQueryKeys.Contains(q.Key))
.ToDictionary(q => q.Key, q => (string?)q.Value.ToString(), StringComparer.OrdinalIgnoreCase);
var unknown = options.Keys.Where(k => generator.Options.All(o => !string.Equals(o.Key, k, StringComparison.OrdinalIgnoreCase))).ToList();
if (unknown.Count > 0)
return Problem(unknown[0], $"Opção desconhecida. Opções válidas: {string.Join(", ", generator.Options.Select(o => o.Key))}.");
if (count is < 1 or > GeneratorRegistry.MaxBatch)
return Problem("count", $"A quantidade deve estar entre 1 e {GeneratorRegistry.MaxBatch}.");
try
{
var items = registry.Generate(generator, count, options, seed);
return TypedResults.Ok(new GenerateResponse(generator.Id, items));
}
catch (ArgumentException ex)
{
return Problem("options", ex.Message);
}
}
private static ValidationProblem Problem(string key, string message) =>
TypedResults.ValidationProblem(new Dictionary<string, string[]> { [key] = [message] });
}
public sealed record GeneratorDto(
string Id,
string Name,
string Category,
string Description,
bool HasChecksum,
bool CanValidate,
IReadOnlyList<GeneratorOption> Options)
{
public static GeneratorDto From(IGenerator g) =>
new(g.Id, g.Name, g.Category, g.Description, g.HasChecksum, g.CanValidate, g.Options);
}
public sealed record GenerateResponse(string Generator, IReadOnlyList<GeneratedItem> Items);
public sealed record DetectMatchDto(string GeneratorId, string GeneratorName, ValidationResult Result);
public sealed record DetectResponse(string Value, IReadOnlyList<DetectMatchDto> Matches);