TenantAtlas/app/Support/Verification/VerificationReportSchema.php
2026-02-04 00:57:26 +01:00

236 lines
5.7 KiB
PHP

<?php
namespace App\Support\Verification;
use DateTimeImmutable;
final class VerificationReportSchema
{
public const string CURRENT_SCHEMA_VERSION = '1.0.0';
/**
* @return array<string, mixed>|null
*/
public static function normalizeReport(mixed $report): ?array
{
if (! is_array($report)) {
return null;
}
if (! self::isValidReport($report)) {
return null;
}
return $report;
}
/**
* @param array<string, mixed> $report
*/
public static function isValidReport(array $report): bool
{
$schemaVersion = self::schemaVersion($report);
if ($schemaVersion === null || ! self::isSupportedSchemaVersion($schemaVersion)) {
return false;
}
if (! self::isNonEmptyString($report['flow'] ?? null)) {
return false;
}
if (! self::isIsoDateTimeString($report['generated_at'] ?? null)) {
return false;
}
if (array_key_exists('identity', $report) && ! is_array($report['identity'])) {
return false;
}
$summary = $report['summary'] ?? null;
if (! is_array($summary)) {
return false;
}
$overall = $summary['overall'] ?? null;
if (! is_string($overall) || ! in_array($overall, VerificationReportOverall::values(), true)) {
return false;
}
$counts = $summary['counts'] ?? null;
if (! is_array($counts)) {
return false;
}
foreach (['total', 'pass', 'fail', 'warn', 'skip', 'running'] as $key) {
if (! self::isNonNegativeInt($counts[$key] ?? null)) {
return false;
}
}
$checks = $report['checks'] ?? null;
if (! is_array($checks)) {
return false;
}
foreach ($checks as $check) {
if (! is_array($check) || ! self::isValidCheckResult($check)) {
return false;
}
}
return true;
}
/**
* @param array<string, mixed> $report
*/
public static function schemaVersion(array $report): ?string
{
$candidate = $report['schema_version'] ?? null;
if (! is_string($candidate)) {
return null;
}
$candidate = trim($candidate);
if ($candidate === '') {
return null;
}
if (! preg_match('/^\d+\.\d+\.\d+$/', $candidate)) {
return null;
}
return $candidate;
}
public static function isSupportedSchemaVersion(string $schemaVersion): bool
{
$parts = explode('.', $schemaVersion, 3);
if (count($parts) !== 3) {
return false;
}
$major = (int) $parts[0];
return $major === 1;
}
/**
* @param array<string, mixed> $check
*/
private static function isValidCheckResult(array $check): bool
{
if (! self::isNonEmptyString($check['key'] ?? null)) {
return false;
}
if (! self::isNonEmptyString($check['title'] ?? null)) {
return false;
}
$status = $check['status'] ?? null;
if (! is_string($status) || ! in_array($status, VerificationCheckStatus::values(), true)) {
return false;
}
$severity = $check['severity'] ?? null;
if (! is_string($severity) || ! in_array($severity, VerificationCheckSeverity::values(), true)) {
return false;
}
if (! is_bool($check['blocking'] ?? null)) {
return false;
}
if (! self::isNonEmptyString($check['reason_code'] ?? null)) {
return false;
}
if (! self::isNonEmptyString($check['message'] ?? null)) {
return false;
}
$evidence = $check['evidence'] ?? null;
if (! is_array($evidence)) {
return false;
}
foreach ($evidence as $pointer) {
if (! is_array($pointer) || ! self::isValidEvidencePointer($pointer)) {
return false;
}
}
$nextSteps = $check['next_steps'] ?? null;
if (! is_array($nextSteps)) {
return false;
}
foreach ($nextSteps as $step) {
if (! is_array($step) || ! self::isValidNextStep($step)) {
return false;
}
}
return true;
}
/**
* @param array<string, mixed> $pointer
*/
private static function isValidEvidencePointer(array $pointer): bool
{
if (! self::isNonEmptyString($pointer['kind'] ?? null)) {
return false;
}
$value = $pointer['value'] ?? null;
return is_int($value) || self::isNonEmptyString($value);
}
/**
* @param array<string, mixed> $step
*/
private static function isValidNextStep(array $step): bool
{
if (! self::isNonEmptyString($step['label'] ?? null)) {
return false;
}
if (! self::isNonEmptyString($step['url'] ?? null)) {
return false;
}
return true;
}
private static function isNonEmptyString(mixed $value): bool
{
return is_string($value) && trim($value) !== '';
}
private static function isNonNegativeInt(mixed $value): bool
{
return is_int($value) && $value >= 0;
}
private static function isIsoDateTimeString(mixed $value): bool
{
if (! self::isNonEmptyString($value)) {
return false;
}
try {
new DateTimeImmutable((string) $value);
return true;
} catch (\Throwable) {
return false;
}
}
}