mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-27 06:21:14 +00:00
68 lines
3.3 KiB
C#
68 lines
3.3 KiB
C#
using FluentValidation;
|
|
using Microsoft.Extensions.Options;
|
|
using SatelliteProvider.Common.Configs;
|
|
using SatelliteProvider.Common.DTO;
|
|
|
|
namespace SatelliteProvider.Api.Validators;
|
|
|
|
// AZ-810: per-item metadata validator for the UAV upload endpoint. Runs as
|
|
// a `RuleForEach.SetValidator(...)` chain child of `UavTileBatchMetadataPayloadValidator`,
|
|
// so error keys come out as `errors.metadata.items[i].latitude`, `…tileZoom`,
|
|
// `…capturedAt`, etc. once the `UavUploadValidationFilter` prefixes the result.
|
|
//
|
|
// CapturedAt freshness (rule 11) is the same window that
|
|
// `IUavTileQualityGate.Validate` enforces; running the same check at the API
|
|
// boundary lets us short-circuit before any file bytes are inspected. The
|
|
// gate remains as a defence-in-depth backstop for unit tests of the gate
|
|
// itself and for the unlikely path of a caller invoking
|
|
// `IUavTileUploadHandler` directly (bypassing the filter).
|
|
public sealed class UavTileMetadataValidator : AbstractValidator<UavTileMetadata>
|
|
{
|
|
private const double MinLat = -90.0;
|
|
private const double MaxLat = 90.0;
|
|
private const double MinLon = -180.0;
|
|
private const double MaxLon = 180.0;
|
|
private const int MinZoom = 0;
|
|
private const int MaxZoom = 22;
|
|
|
|
public UavTileMetadataValidator(IOptions<UavQualityConfig> qualityConfig, TimeProvider? timeProvider = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(qualityConfig);
|
|
var cfg = qualityConfig.Value;
|
|
var tp = timeProvider ?? TimeProvider.System;
|
|
var maxAgeDays = cfg.MaxAgeDays;
|
|
var futureSkewSeconds = cfg.CapturedAtFutureSkewSeconds;
|
|
|
|
RuleFor(m => m.Latitude)
|
|
.InclusiveBetween(MinLat, MaxLat)
|
|
.WithMessage($"`latitude` must be between {MinLat} and {MaxLat}.");
|
|
|
|
RuleFor(m => m.Longitude)
|
|
.InclusiveBetween(MinLon, MaxLon)
|
|
.WithMessage($"`longitude` must be between {MinLon} and {MaxLon}.");
|
|
|
|
RuleFor(m => m.TileZoom)
|
|
.InclusiveBetween(MinZoom, MaxZoom)
|
|
.WithMessage($"`tileZoom` must be between {MinZoom} and {MaxZoom} (slippy-map range).");
|
|
|
|
RuleFor(m => m.TileSizeMeters)
|
|
.GreaterThan(0.0)
|
|
.WithMessage("`tileSizeMeters` must be greater than 0.");
|
|
|
|
// Freshness window: capturedAt ∈ [now - MaxAgeDays, now + CapturedAtFutureSkewSeconds].
|
|
// `Must` lambdas close over `tp` so the comparison fetches fresh
|
|
// time per call (rule executes at validation time, not constructor
|
|
// time). Equivalent to AZ-488 Rule 4 in UavTileQualityGate.
|
|
RuleFor(m => m.CapturedAt)
|
|
.Must(capturedAt => capturedAt.UtcDateTime <= tp.GetUtcNow().UtcDateTime.AddSeconds(futureSkewSeconds))
|
|
.WithMessage($"`capturedAt` must be within {futureSkewSeconds}s of the current time (no future-dated tiles).")
|
|
.Must(capturedAt => capturedAt.UtcDateTime >= tp.GetUtcNow().UtcDateTime.AddDays(-maxAgeDays))
|
|
.WithMessage($"`capturedAt` must be within the last {maxAgeDays} days.");
|
|
|
|
// `FlightId` is intentionally not validated beyond JSON shape — AZ-503
|
|
// anonymous-flight semantics require null/missing to be a valid case.
|
|
// System.Text.Json already rejects malformed UUID strings at the
|
|
// deserializer with `JsonException` → 400 via GlobalExceptionHandler.
|
|
}
|
|
}
|