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:
@@ -1,55 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using GeradoresService;
|
||||
using GeradoresService.DAL;
|
||||
|
||||
namespace GeradoresWS.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class GenerateController : Controller
|
||||
{
|
||||
GeradoresContext _context;
|
||||
public GenerateController(GeradoresContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
#region NIF
|
||||
[HttpGet("GenerateNIF")]
|
||||
public string GenerateNIF(NIF.NIFType type)
|
||||
{
|
||||
var nif = new NIF(_context);
|
||||
return nif.Generate(type);
|
||||
}
|
||||
|
||||
[HttpGet("ValidateNIF")]
|
||||
public bool ValidateNIF(string nif)
|
||||
{
|
||||
var teste = new NIF(_context);
|
||||
return teste.Validate(nif);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region NISS
|
||||
[HttpGet("GenerateNISS")]
|
||||
public string GenerateNISS()
|
||||
{
|
||||
return NISS.Generate();
|
||||
}
|
||||
|
||||
[HttpGet("ValidateNISS")]
|
||||
public bool ValidateNISS(string nif)
|
||||
{
|
||||
return NISS.Validate(nif);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region CC
|
||||
[HttpGet("GenerateCC")]
|
||||
public string GenerateCC()
|
||||
{
|
||||
return CartaoCidadao.Generate();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace GeradoresWS.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class WeatherForecastController : ControllerBase
|
||||
{
|
||||
private static readonly string[] Summaries = new[]
|
||||
{
|
||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||
};
|
||||
|
||||
private readonly ILogger<WeatherForecastController> _logger;
|
||||
|
||||
public WeatherForecastController(ILogger<WeatherForecastController> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet(Name = "GetWeatherForecast")]
|
||||
public IEnumerable<WeatherForecast> Get()
|
||||
{
|
||||
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||
{
|
||||
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
||||
TemperatureC = Random.Shared.Next(-20, 55),
|
||||
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,26 @@
|
||||
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 443
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
# Usado pelo Visual Studio (perfil "Docker"). Igual ao Dockerfile da raiz, com o stage "base" que o VS espera.
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
COPY ["GeradoresWS/GeradoresWS.csproj", "GeradoresWS/"]
|
||||
COPY ["GeradoresService/GeradoresService.csproj", "GeradoresService/"]
|
||||
|
||||
RUN dotnet restore "GeradoresWS/GeradoresWS.csproj"
|
||||
COPY . .
|
||||
|
||||
WORKDIR "/src/GeradoresWS"
|
||||
RUN dotnet build "GeradoresWS.csproj" -c debug -o /app/build
|
||||
COPY GeradoresService/ GeradoresService/
|
||||
COPY GeradoresWS/ GeradoresWS/
|
||||
RUN dotnet publish "GeradoresWS/GeradoresWS.csproj" -c Release -o /app/publish --no-restore /p:UseAppHost=false
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "GeradoresWS.csproj" -c debug -o /app/publish /p:UseAppHost=false
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
# Set environment variables
|
||||
ENV ASPNETCORE_URLS=http://+:8080
|
||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||
ENV ASPNETCORE_HTTP_PORTS=8080 \
|
||||
ASPNETCORE_ENVIRONMENT=Production
|
||||
EXPOSE 8080
|
||||
USER $APP_UID
|
||||
|
||||
ENTRYPOINT ["dotnet", "GeradoresWS.dll"]
|
||||
ENTRYPOINT ["dotnet", "GeradoresWS.dll"]
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Primus.ProfitabilityModel.WebAPI.Filters
|
||||
{
|
||||
public class EnumDescriptionFilter : ISchemaFilter
|
||||
{
|
||||
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
|
||||
{
|
||||
if (context.Type.IsEnum)
|
||||
{
|
||||
// Obter os valores do enum
|
||||
var enumValues = Enum.GetValues(context.Type)
|
||||
.Cast<Enum>()
|
||||
.ToList();
|
||||
|
||||
// Obter os nomes dos enums
|
||||
var enumNames = enumValues.Select(e => e.ToString()).ToList();
|
||||
|
||||
// Obter as descrições dos enums
|
||||
var enumDescriptions = enumValues.Select(e => GetEnumDescription(e)).ToList();
|
||||
|
||||
// Adicionar as extensões com os nomes e descrições
|
||||
var enumNamesArray = new OpenApiArray();
|
||||
foreach (var name in enumNames)
|
||||
{
|
||||
enumNamesArray.Add(new OpenApiString(name));
|
||||
}
|
||||
|
||||
var enumDescriptionsArray = new OpenApiArray();
|
||||
foreach (var description in enumDescriptions)
|
||||
{
|
||||
enumDescriptionsArray.Add(new OpenApiString(description));
|
||||
}
|
||||
|
||||
schema.Extensions.Add("x-enumNames", enumDescriptionsArray);
|
||||
schema.Extensions.Add("x-enumDescriptions", enumDescriptionsArray);
|
||||
|
||||
// Adicionar os valores do enum ao campo Enum
|
||||
schema.Enum = enumNames
|
||||
.Select(name => new OpenApiString(name))
|
||||
.Cast<IOpenApiAny>()
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetEnumDescription(Enum value)
|
||||
{
|
||||
// Verificar se o campo é nulo antes de chamar GetCustomAttribute
|
||||
var field = value.GetType().GetField(value.ToString());
|
||||
if (field == null)
|
||||
{
|
||||
return value.ToString(); // Se o campo for nulo, retorna o nome do valor do enum
|
||||
}
|
||||
|
||||
// Acessa o atributo Description, caso exista
|
||||
var attribute = (DescriptionAttribute)Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute));
|
||||
|
||||
// Retorna a descrição ou o nome do valor do enum, se a descrição não estiver presente
|
||||
return attribute?.Description ?? value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
101
GeradoresWS/GeneratorEndpoints.cs
Normal file
101
GeradoresWS/GeneratorEndpoints.cs
Normal 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);
|
||||
@@ -1,17 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>df357c84-88b1-4100-86d1-889166b333f7</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.18.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.17.14" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,56 +1,40 @@
|
||||
using GeradoresService.DAL;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Primus.ProfitabilityModel.WebAPI.Filters;
|
||||
using System.Text.Json.Serialization;
|
||||
using GeradoresService;
|
||||
using GeradoresWS;
|
||||
using Scalar.AspNetCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||
builder.Services.AddSingleton<GeneratorRegistry>();
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddHealthChecks();
|
||||
builder.Services.AddOpenApi(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
});
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(options =>
|
||||
{
|
||||
options.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
|
||||
options.SwaggerDoc("v1", new OpenApiInfo
|
||||
options.AddDocumentTransformer((document, _, _) =>
|
||||
{
|
||||
|
||||
Version = "v1",
|
||||
Title = "FactoryId",
|
||||
Contact = new OpenApiContact
|
||||
{
|
||||
Name = "Marco Santos"
|
||||
},
|
||||
document.Info.Title = "FactoryId API";
|
||||
document.Info.Description = "Geradores e validadores de identificadores portugueses (NIF, NISS, CC, IBAN, …) para testes.";
|
||||
document.Info.Contact = new() { Name = "Marco Santos" };
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
// Registra o filtro de esquema que aplica descrições para todos os enums
|
||||
options.SchemaFilter<EnumDescriptionFilter>();
|
||||
});
|
||||
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("GeradoresConnection");
|
||||
builder.Services.AddDbContext<GeradoresContext>(options => options.UseSqlServer(connectionString));
|
||||
// API pública, só de leitura e sem credenciais: qualquer origem pode fazer GET.
|
||||
builder.Services.AddCors(options =>
|
||||
options.AddDefaultPolicy(policy => policy.AllowAnyOrigin().WithMethods("GET").AllowAnyHeader()));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
app.UseHttpsRedirection();
|
||||
app.UseExceptionHandler();
|
||||
app.UseStatusCodePages();
|
||||
app.UseCors();
|
||||
|
||||
app.UseAuthorization();
|
||||
app.MapOpenApi();
|
||||
app.MapScalarApiReference("/docs", options => options.WithTitle("FactoryId API"));
|
||||
app.MapGet("/", () => Results.Redirect("/docs")).ExcludeFromDescription();
|
||||
app.MapHealthChecks("/health");
|
||||
|
||||
app.UseCors(options =>
|
||||
{
|
||||
options.AllowAnyOrigin();
|
||||
options.AllowAnyMethod();
|
||||
options.AllowAnyHeader();
|
||||
});
|
||||
|
||||
app.MapControllers();
|
||||
app.MapGeneratorEndpoints();
|
||||
|
||||
app.Run();
|
||||
|
||||
public partial class Program;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"launchUrl": "docs",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
@@ -13,7 +13,7 @@
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"launchUrl": "docs",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
@@ -23,7 +23,7 @@
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"launchUrl": "docs",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
"Docker": {
|
||||
"commandName": "Docker",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
|
||||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/docs",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_URLS": "http://+:8080",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace GeradoresWS
|
||||
{
|
||||
public class WeatherForecast
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
|
||||
public int TemperatureC { get; set; }
|
||||
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
|
||||
public string? Summary { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,5 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"GeradoresConnection": "server=127.0.0.1;uid=sa;pwd=mssqlAdmin!1;database=Geradores;TrustServerCertificate=True;"
|
||||
}
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user