TenantAtlas/app/Support/Diff/RbacRoleDefinitionDiffBuilder.php
2026-03-14 21:08:32 +01:00

206 lines
6.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Diff;
final class RbacRoleDefinitionDiffBuilder
{
private const ROLE_FIELD_ORDER = [
'Role definition > Display name' => 100,
'Role definition > Description' => 200,
'Role definition > Role source' => 300,
'Role definition > Permission blocks' => 400,
'Role definition > Scope tag IDs' => 500,
];
private const PERMISSION_FIELD_ORDER = [
'Allowed actions' => 100,
'Denied actions' => 200,
'Conditions' => 300,
];
public function __construct(
private readonly DiffPresenter $presenter,
) {}
/**
* @param array<string, mixed> $payload
*/
public function build(array $payload): DiffPresentation
{
$baseline = $this->normalizeSide(
is_array($payload['baseline'] ?? null) ? $payload['baseline'] : [],
);
$current = $this->normalizeSide(
is_array($payload['current'] ?? null) ? $payload['current'] : [],
);
$changedKeys = $this->changedKeys($payload);
$presentation = $this->presenter->present(
baseline: $baseline,
current: $current,
changedKeys: $changedKeys,
labels: $this->labelsFor($baseline, $current, $changedKeys),
);
if ($presentation->rows === []) {
return $presentation;
}
$rows = array_map(
fn (DiffRow $row): DiffRow => $this->normalizeRow($row),
$presentation->rows,
);
usort($rows, fn (DiffRow $left, DiffRow $right): int => $this->sortKey($left->key) <=> $this->sortKey($right->key));
return new DiffPresentation(
summary: DiffSummary::fromRows($rows, $presentation->summary->message),
rows: $rows,
);
}
/**
* @param array<string, mixed> $payload
* @return list<string>
*/
private function changedKeys(array $payload): array
{
$changedKeys = is_array($payload['changed_keys'] ?? null) ? $payload['changed_keys'] : [];
return array_values(array_filter(
$changedKeys,
fn (mixed $key): bool => is_string($key) && trim($key) !== '',
));
}
/**
* @param array<string, mixed> $side
* @return array<string, mixed>
*/
private function normalizeSide(array $side): array
{
$normalized = [];
$rows = is_array($side['normalized'] ?? null) ? $side['normalized'] : [];
foreach ($rows as $key => $value) {
if (! is_string($key) || trim($key) === '') {
continue;
}
$normalized[trim($key)] = $value;
}
if (! array_key_exists('Role definition > Role source', $normalized)) {
$roleSource = $this->roleSourceLabel($side['is_built_in'] ?? null);
if ($roleSource !== null) {
$normalized['Role definition > Role source'] = $roleSource;
}
}
if (
! array_key_exists('Role definition > Permission blocks', $normalized)
&& is_numeric($side['role_permission_count'] ?? null)
) {
$normalized['Role definition > Permission blocks'] = (int) $side['role_permission_count'];
}
return $normalized;
}
private function roleSourceLabel(mixed $isBuiltIn): ?string
{
return match ($isBuiltIn) {
true => 'Built-in',
false => 'Custom',
default => null,
};
}
/**
* @param array<string, mixed> $baseline
* @param array<string, mixed> $current
* @param list<string> $changedKeys
* @return array<string, string>
*/
private function labelsFor(array $baseline, array $current, array $changedKeys): array
{
$labels = [];
foreach ([array_keys($baseline), array_keys($current), $changedKeys] as $sourceKeys) {
foreach ($sourceKeys as $key) {
if (! is_string($key) || trim($key) === '') {
continue;
}
$labels[$key] = $key;
}
}
return $labels;
}
private function normalizeRow(DiffRow $row): DiffRow
{
$shouldRenderAsList = $row->isListLike && $this->isDesignatedListField($row->key);
if ($row->isListLike === $shouldRenderAsList) {
return $row;
}
return new DiffRow(
key: $row->key,
label: $row->label,
status: $row->status,
oldValue: $row->oldValue,
newValue: $row->newValue,
isListLike: $shouldRenderAsList,
addedItems: $shouldRenderAsList ? $row->addedItems : [],
removedItems: $shouldRenderAsList ? $row->removedItems : [],
unchangedItems: $shouldRenderAsList ? $row->unchangedItems : [],
meta: $row->meta,
);
}
private function isDesignatedListField(string $key): bool
{
if ($key === 'Role definition > Scope tag IDs') {
return true;
}
return preg_match('/^Permission block \d+ > (Allowed actions|Denied actions|Conditions)$/', $key) === 1;
}
/**
* @return array{int, int, int, string}
*/
private function sortKey(string $key): array
{
if (array_key_exists($key, self::ROLE_FIELD_ORDER)) {
return [0, self::ROLE_FIELD_ORDER[$key], 0, mb_strtolower($key)];
}
if (preg_match('/^Role definition > (?P<label>.+)$/', $key, $matches) === 1) {
return [0, 900, 0, mb_strtolower((string) ($matches['label'] ?? $key))];
}
if (preg_match('/^Permission block (?P<block>\d+) > (?P<label>.+)$/', $key, $matches) === 1) {
return [
1,
(int) ($matches['block'] ?? 0),
$this->permissionFieldOrder((string) ($matches['label'] ?? '')),
mb_strtolower((string) ($matches['label'] ?? $key)),
];
}
return [2, 0, 0, mb_strtolower($key)];
}
private function permissionFieldOrder(string $label): int
{
return self::PERMISSION_FIELD_ORDER[$label] ?? 900;
}
}