Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace App\Support\Converters\Exceptions;

use DomainException;

final class InvalidOptionsSchemaException extends DomainException
{
public static function becauseFieldIsInvalid(): self
{
return new self('Options schema field must be an array.');
}

public static function becauseKeyIsMissing(): self
{
return new self('Options schema field is missing the "key" attribute.');
}

public static function becauseKeyIsDuplicated(string $key): self
{
return new self("Options schema contains duplicate field key: {$key}.");
}

public static function becauseTypeIsUnsupported(string $type): self
{
return new self("Options schema field type is not supported: {$type}.");
}

public static function becauseLabelIsMissing(string $key): self
{
return new self("Options schema field [{$key}] is missing the \"label\" attribute.");
}

public static function becauseOptionsAreRequired(string $key): self
{
return new self("Options schema field [{$key}] requires a non-empty \"options\" list.");
}

public static function becauseOptionItemIsInvalid(string $key): self
{
return new self("Options schema field [{$key}] contains an invalid option entry.");
}
}
79 changes: 79 additions & 0 deletions app/Support/Converters/OptionsSchemaValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace App\Support\Converters;

use App\Support\Converters\Exceptions\InvalidOptionsSchemaException;

final class OptionsSchemaValidator
{
private const SUPPORTED_TYPES = [
'select',
'segmented',
'toggle',
'color',
'number',
'range',
];

private const TYPES_REQUIRING_OPTIONS = [
'select',
'segmented',
];

public function validate(array $schema): void
{
$keys = [];

foreach ($schema as $field) {
if (! is_array($field)) {
throw InvalidOptionsSchemaException::becauseFieldIsInvalid();
}

$key = $field['key'] ?? null;
$type = $field['type'] ?? null;
$label = $field['label'] ?? null;

if (! is_string($key) || $key === '') {
throw InvalidOptionsSchemaException::becauseKeyIsMissing();
}

if (in_array($key, $keys, true)) {
throw InvalidOptionsSchemaException::becauseKeyIsDuplicated($key);
}

$keys[] = $key;

if (! is_string($type) || ! in_array($type, self::SUPPORTED_TYPES, true)) {
throw InvalidOptionsSchemaException::becauseTypeIsUnsupported((string) $type);
}

if (! is_string($label) || $label === '') {
throw InvalidOptionsSchemaException::becauseLabelIsMissing($key);
}

if (in_array($type, self::TYPES_REQUIRING_OPTIONS, true)) {
$this->validateChoiceOptions($key, $field['options'] ?? null);
}
}
}

private function validateChoiceOptions(string $key, mixed $options): void
{
if (! is_array($options) || $options === []) {
throw InvalidOptionsSchemaException::becauseOptionsAreRequired($key);
}

foreach ($options as $option) {
if (! is_array($option)) {
throw InvalidOptionsSchemaException::becauseOptionItemIsInvalid($key);
}

$value = $option['value'] ?? null;
$label = $option['label'] ?? null;

if ($value === null || ! is_string($label) || $label === '') {
throw InvalidOptionsSchemaException::becauseOptionItemIsInvalid($key);
}
}
}
}