Files
git.stella-ops.org/src/Platform/StellaOps.Platform.WebService/Endpoints/FunctionMapEndpoints.cs
2026-02-23 23:44:50 +02:00

259 lines
9.2 KiB
C#

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_039_Scanner_runtime_linkage_verification
// Task: RLV-009 - Platform API: Function Map Endpoints
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using StellaOps.Auth.ServerIntegration.Tenancy;
using StellaOps.Platform.WebService.Constants;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
namespace StellaOps.Platform.WebService.Endpoints;
/// <summary>
/// Function map management API endpoints.
/// </summary>
public static class FunctionMapEndpoints
{
/// <summary>
/// Maps function-map-related endpoints.
/// </summary>
public static IEndpointRouteBuilder MapFunctionMapEndpoints(this IEndpointRouteBuilder app)
{
var maps = app.MapGroup("/api/v1/function-maps")
.WithTags("Function Maps")
.RequireAuthorization(PlatformPolicies.FunctionMapRead)
.RequireTenant();
MapCrudEndpoints(maps);
MapVerifyEndpoints(maps);
return app;
}
private static void MapCrudEndpoints(IEndpointRouteBuilder maps)
{
// POST /api/v1/function-maps - Create function map
maps.MapPost("/", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IFunctionMapService service,
[FromBody] CreateFunctionMapRequest request,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.CreateAsync(
requestContext!,
request,
cancellationToken).ConfigureAwait(false);
return Results.Created(
$"/api/v1/function-maps/{result.Value.Id}",
new PlatformItemResponse<FunctionMapDetail>(
requestContext!.TenantId,
requestContext.ActorId,
result.DataAsOf,
result.Cached,
result.CacheTtlSeconds,
result.Value));
})
.WithName("CreateFunctionMap")
.WithSummary("Create function map")
.WithDescription("Creates a new function map from an SBOM reference and hot function patterns.")
.RequireAuthorization(PlatformPolicies.FunctionMapWrite);
// GET /api/v1/function-maps - List function maps
maps.MapGet("/", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IFunctionMapService service,
[FromQuery] int? limit,
[FromQuery] int? offset,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.ListAsync(
requestContext!,
limit ?? 100,
offset ?? 0,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<FunctionMapSummary>(
requestContext!.TenantId,
requestContext.ActorId,
result.DataAsOf,
result.Cached,
result.CacheTtlSeconds,
result.Value,
result.Value.Count,
limit ?? 100,
offset ?? 0));
})
.WithName("ListFunctionMaps")
.WithSummary("List function maps")
.WithDescription("Lists all function maps for the current tenant.")
.RequireAuthorization(PlatformPolicies.FunctionMapRead);
// GET /api/v1/function-maps/{id} - Get function map by ID
maps.MapGet("/{id}", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IFunctionMapService service,
string id,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.GetByIdAsync(
requestContext!,
id,
cancellationToken).ConfigureAwait(false);
if (result.Value is null)
{
return Results.NotFound(new { error = "Function map not found", id });
}
return Results.Ok(new PlatformItemResponse<FunctionMapDetail>(
requestContext!.TenantId,
requestContext.ActorId,
result.DataAsOf,
result.Cached,
result.CacheTtlSeconds,
result.Value));
})
.WithName("GetFunctionMap")
.WithSummary("Get function map")
.WithDescription("Retrieves a function map by its unique identifier.")
.RequireAuthorization(PlatformPolicies.FunctionMapRead);
// DELETE /api/v1/function-maps/{id} - Delete function map
maps.MapDelete("/{id}", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IFunctionMapService service,
string id,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.DeleteAsync(
requestContext!,
id,
cancellationToken).ConfigureAwait(false);
if (!result.Value)
{
return Results.NotFound(new { error = "Function map not found", id });
}
return Results.NoContent();
})
.WithName("DeleteFunctionMap")
.WithSummary("Delete function map")
.WithDescription("Deletes a function map by its unique identifier.")
.RequireAuthorization(PlatformPolicies.FunctionMapWrite);
}
private static void MapVerifyEndpoints(IEndpointRouteBuilder maps)
{
// POST /api/v1/function-maps/{id}/verify - Verify observations against map
maps.MapPost("/{id}/verify", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IFunctionMapService service,
string id,
[FromBody] VerifyFunctionMapRequest request,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.VerifyAsync(
requestContext!,
id,
request,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformItemResponse<FunctionMapVerifyResponse>(
requestContext!.TenantId,
requestContext.ActorId,
result.DataAsOf,
result.Cached,
result.CacheTtlSeconds,
result.Value));
})
.WithName("VerifyFunctionMap")
.WithSummary("Verify function map")
.WithDescription("Verifies runtime observations against a declared function map.")
.RequireAuthorization(PlatformPolicies.FunctionMapVerify);
// GET /api/v1/function-maps/{id}/coverage - Get coverage statistics
maps.MapGet("/{id}/coverage", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
IFunctionMapService service,
string id,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.GetCoverageAsync(
requestContext!,
id,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformItemResponse<FunctionMapCoverageResponse>(
requestContext!.TenantId,
requestContext.ActorId,
result.DataAsOf,
result.Cached,
result.CacheTtlSeconds,
result.Value));
})
.WithName("GetFunctionMapCoverage")
.WithSummary("Get function map coverage")
.WithDescription("Returns current coverage statistics for a function map.")
.RequireAuthorization(PlatformPolicies.FunctionMapRead);
}
private static bool TryResolveContext(
HttpContext context,
PlatformRequestContextResolver resolver,
out PlatformRequestContext? requestContext,
out IResult? failure)
{
if (resolver.TryResolve(context, out requestContext, out var error))
{
failure = null;
return true;
}
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
return false;
}
}