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:
71
GeradoresService/Core/Checksums.cs
Normal file
71
GeradoresService/Core/Checksums.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace GeradoresService.Core;
|
||||
|
||||
public static class Checksums
|
||||
{
|
||||
/// <summary>
|
||||
/// Dígito de controlo módulo 11 com pesos decrescentes (n+1 .. 2), usado no NIF e no NIC do CC.
|
||||
/// Resto 0 ou 1 dá dígito 0.
|
||||
/// </summary>
|
||||
public static int Mod11(ReadOnlySpan<char> digits)
|
||||
{
|
||||
var sum = 0;
|
||||
for (var i = 0; i < digits.Length; i++)
|
||||
sum += (digits[i] - '0') * (digits.Length + 1 - i);
|
||||
var remainder = sum % 11;
|
||||
return remainder < 2 ? 0 : 11 - remainder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Soma Luhn a partir da direita, duplicando posições alternadas.
|
||||
/// Aceita letras (A=10 .. Z=35), como no número de documento do Cartão de Cidadão.
|
||||
/// </summary>
|
||||
public static int LuhnSum(ReadOnlySpan<char> value)
|
||||
{
|
||||
var sum = 0;
|
||||
var doubleIt = false;
|
||||
for (var i = value.Length - 1; i >= 0; i--)
|
||||
{
|
||||
var v = CharValue(value[i]);
|
||||
if (doubleIt)
|
||||
{
|
||||
v *= 2;
|
||||
if (v > 9) v -= 9;
|
||||
}
|
||||
sum += v;
|
||||
doubleIt = !doubleIt;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
public static bool LuhnValid(ReadOnlySpan<char> value) => LuhnSum(value) % 10 == 0;
|
||||
|
||||
/// <summary>Calcula o dígito Luhn a acrescentar a <paramref name="payload"/>.</summary>
|
||||
public static int LuhnCheckDigit(string payload)
|
||||
{
|
||||
var sum = LuhnSum(payload + "0");
|
||||
return (10 - sum % 10) % 10;
|
||||
}
|
||||
|
||||
/// <summary>ISO 7064 MOD 97-10 sobre uma string de dígitos.</summary>
|
||||
public static int Mod97(string digits) => (int)(BigInteger.Parse(digits) % 97);
|
||||
|
||||
/// <summary>Converte letras em números (A=10 .. Z=35) para cálculo de IBAN.</summary>
|
||||
public static string LettersToDigits(string value) =>
|
||||
string.Concat(value.Select(c => char.IsLetter(c) ? (char.ToUpperInvariant(c) - 'A' + 10).ToString() : c.ToString()));
|
||||
|
||||
private static int CharValue(char c) => c switch
|
||||
{
|
||||
>= '0' and <= '9' => c - '0',
|
||||
>= 'A' and <= 'Z' => c - 'A' + 10,
|
||||
>= 'a' and <= 'z' => c - 'a' + 10,
|
||||
_ => throw new ArgumentException($"Carácter inválido '{c}'."),
|
||||
};
|
||||
|
||||
/// <summary>Remove espaços, hífens, pontos e barras.</summary>
|
||||
public static string Clean(string value) =>
|
||||
new(value.Where(c => !char.IsWhiteSpace(c) && c is not '-' and not '.' and not '/').ToArray());
|
||||
|
||||
public static bool AllDigits(string value) => value.Length > 0 && value.All(char.IsAsciiDigit);
|
||||
}
|
||||
18
GeradoresService/Core/GeneratorBase.cs
Normal file
18
GeradoresService/Core/GeneratorBase.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace GeradoresService.Core;
|
||||
|
||||
public abstract class GeneratorBase : IGenerator
|
||||
{
|
||||
public abstract string Id { get; }
|
||||
public abstract string Name { get; }
|
||||
public abstract string Category { get; }
|
||||
public abstract string Description { get; }
|
||||
public virtual IReadOnlyList<GeneratorOption> Options => [];
|
||||
public virtual bool HasChecksum => true;
|
||||
public virtual bool CanValidate => true;
|
||||
|
||||
public abstract GeneratedItem Generate(GeneratorContext context);
|
||||
public abstract ValidationResult Validate(string value);
|
||||
|
||||
protected static GeneratorOption Option(string key, string label, string defaultValue, params (string Value, string Label)[] choices) =>
|
||||
new(key, label, choices.Select(c => new OptionChoice(c.Value, c.Label)).ToArray(), defaultValue);
|
||||
}
|
||||
39
GeradoresService/Core/IGenerator.cs
Normal file
39
GeradoresService/Core/IGenerator.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
namespace GeradoresService.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Um gerador/validador de um tipo de identificador.
|
||||
/// Os metadados (Id, Name, Options, ...) são expostos pela API para o frontend construir a UI.
|
||||
/// </summary>
|
||||
public interface IGenerator
|
||||
{
|
||||
/// <summary>Identificador estável usado nos URLs (ex.: "nif").</summary>
|
||||
string Id { get; }
|
||||
|
||||
string Name { get; }
|
||||
|
||||
string Category { get; }
|
||||
|
||||
string Description { get; }
|
||||
|
||||
IReadOnlyList<GeneratorOption> Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indica se a validação verifica um dígito de controlo (true) ou apenas o formato (false).
|
||||
/// </summary>
|
||||
bool HasChecksum { get; }
|
||||
|
||||
/// <summary>False para geradores compostos que não têm validação (ex.: pessoa fictícia).</summary>
|
||||
bool CanValidate { get; }
|
||||
|
||||
GeneratedItem Generate(GeneratorContext context);
|
||||
|
||||
ValidationResult Validate(string value);
|
||||
}
|
||||
|
||||
public static class Categories
|
||||
{
|
||||
public const string Identificacao = "Identificação";
|
||||
public const string Financeiro = "Financeiro";
|
||||
public const string Contactos = "Contactos e Morada";
|
||||
public const string Outros = "Outros";
|
||||
}
|
||||
72
GeradoresService/Core/Models.cs
Normal file
72
GeradoresService/Core/Models.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
namespace GeradoresService.Core;
|
||||
|
||||
public sealed record GeneratorOption(
|
||||
string Key,
|
||||
string Label,
|
||||
IReadOnlyList<OptionChoice> Choices,
|
||||
string DefaultValue);
|
||||
|
||||
public sealed record OptionChoice(string Value, string Label);
|
||||
|
||||
/// <summary>
|
||||
/// Valor gerado. <see cref="Fields"/> é usado por geradores compostos (ex.: pessoa fictícia).
|
||||
/// </summary>
|
||||
public sealed record GeneratedItem(string Value, IReadOnlyDictionary<string, string>? Fields = null);
|
||||
|
||||
/// <param name="IsValid">O valor é válido.</param>
|
||||
/// <param name="FormatMatch">O valor tem o formato deste tipo (mesmo que o dígito de controlo falhe).</param>
|
||||
/// <param name="Message">Resumo legível.</param>
|
||||
/// <param name="Normalized">Valor normalizado/formatado, quando válido.</param>
|
||||
/// <param name="Details">Passos do cálculo / informação extra.</param>
|
||||
public sealed record ValidationResult(
|
||||
bool IsValid,
|
||||
bool FormatMatch,
|
||||
string Message,
|
||||
string? Normalized = null,
|
||||
IReadOnlyList<string>? Details = null)
|
||||
{
|
||||
public static ValidationResult Valid(string normalized, string message, IReadOnlyList<string>? details = null) =>
|
||||
new(true, true, message, normalized, details);
|
||||
|
||||
public static ValidationResult WrongFormat(string message) =>
|
||||
new(false, false, message);
|
||||
|
||||
public static ValidationResult BadChecksum(string message, IReadOnlyList<string>? details = null) =>
|
||||
new(false, true, message, null, details);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contexto de geração: fonte de aleatoriedade e opções escolhidas pelo utilizador.
|
||||
/// </summary>
|
||||
public sealed class GeneratorContext(Random random, IReadOnlyDictionary<string, string?> options)
|
||||
{
|
||||
public Random Random { get; } = random;
|
||||
|
||||
public static GeneratorContext Default(Random? random = null) =>
|
||||
new(random ?? Random.Shared, new Dictionary<string, string?>());
|
||||
|
||||
/// <summary>
|
||||
/// Devolve o valor da opção ou o default. O valor tem de ser uma das escolhas válidas.
|
||||
/// </summary>
|
||||
public string Option(GeneratorOption option)
|
||||
{
|
||||
if (options.TryGetValue(option.Key, out var value) && !string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
var match = option.Choices.FirstOrDefault(c => string.Equals(c.Value, value, StringComparison.OrdinalIgnoreCase));
|
||||
if (match is null)
|
||||
throw new ArgumentException($"Valor '{value}' inválido para a opção '{option.Key}'.");
|
||||
return match.Value;
|
||||
}
|
||||
return option.DefaultValue;
|
||||
}
|
||||
|
||||
public string Digits(int length)
|
||||
{
|
||||
Span<char> buffer = stackalloc char[length];
|
||||
for (var i = 0; i < length; i++)
|
||||
buffer[i] = (char)('0' + Random.Next(10));
|
||||
return new string(buffer);
|
||||
}
|
||||
|
||||
public T Pick<T>(IReadOnlyList<T> items) => items[Random.Next(items.Count)];
|
||||
}
|
||||
Reference in New Issue
Block a user