Skip to content

Commit

Permalink
Extract JSON schema generated by FileFieldFactory into an own class
Browse files Browse the repository at this point in the history
  • Loading branch information
Dominic Tubach committed Sep 6, 2024
1 parent 5d06e97 commit 72a7487
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 83 deletions.
90 changes: 7 additions & 83 deletions Civi/RemoteTools/JsonSchema/FormSpec/Factory/FileFieldFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,9 @@
use Civi\RemoteTools\Form\FormSpec\AbstractFormField;
use Civi\RemoteTools\Form\FormSpec\Field\FileField;
use Civi\RemoteTools\JsonSchema\JsonSchema;
use Civi\RemoteTools\JsonSchema\JsonSchemaNull;
use Civi\RemoteTools\JsonSchema\JsonSchemaObject;
use Civi\RemoteTools\JsonSchema\JsonSchemaString;
use Civi\RemoteTools\Util\ArrayUtil;
use Civi\RemoteTools\Util\FormatUtil;
use CRM_Remotetools_ExtensionUtil as E;
use Civi\RemoteTools\JsonSchema\JsonSchemaFile;
use Webmozart\Assert\Assert;

/**
* @phpstan-import-type TValue from JsonSchema
*/
final class FileFieldFactory extends AbstractFieldJsonSchemaFactory {

public static function getPriority(): int {
Expand All @@ -43,87 +35,19 @@ public function createSchema(AbstractFormField $field): JsonSchema {
Assert::isInstanceOf($field, FileField::class);
/** @var \Civi\RemoteTools\Form\FormSpec\Field\FileField $field */

$properties = [
'filename' => new JsonSchemaString(['minLength' => 1, 'maxLength' => 255]),
];
$keywords = [
'required' => ['filename'],
];

$contentKeywords = ['contentEncoding' => 'base64'];
if (NULL !== $field->getMaxFileSize() && $field->getMaxFileSize() > 0) {
$contentKeywords['$validations'] = [
JsonSchema::fromArray([
'keyword' => 'maxLength',
// The file might need up to 37 % more space through Base64 encoding.
'value' => (int) ceil($field->getMaxFileSize() * 1.37),
'message' => E::ts('The file must not be larger than %1.',
[1 => FormatUtil::toHumanReadableBytes($field->getMaxFileSize())]
),
]),
];
}

if ($field->hasDefaultValue() && NULL !== $field->getFilename() && NULL !== $field->getUrl()) {
$keywords['default'] = JsonSchema::fromArray([
$currentFile = [
'filename' => $field->getFilename(),
'url' => $field->getUrl(),
]);

[$urlWithoutQuery] = explode('?', $field->getUrl(), 2);

// Matches the default (current file).
$currentFileSchema = JsonSchema::fromArray([
'properties' => [
'filename' => new JsonSchemaString(['const' => $field->getFilename()]),
// We don't use the 'const' keyword because the URL might contain a
// hash that depends on the time. Thus, we exclude the query from
// the test.
'url' => new JsonSchemaString(['format' => 'uri', 'pattern' => '^' . $urlWithoutQuery]),
],
'required' => ['url'],
]);
$newFileSchema = JsonSchema::fromArray([
'properties' => [
'content' => new JsonSchemaString($contentKeywords),
],
'required' => ['content'],
]);

if ($field->isReadOnly()) {
$keywords['readOnly'] = TRUE;
// Allow only the current file.
$valueSchema = $currentFileSchema;
}
else {
// Allow either the current file or a new file.
$valueSchema = JsonSchema::fromArray([
// Test if property 'url' exists.
'if' => ['required' => ['url']],
'then' => $currentFileSchema,
'else' => $newFileSchema,
]);
}

if ($field->isNullable()) {
$keywords['if'] = JsonSchema::fromArray(['not' => new JsonSchemaNull()]);
$keywords['then'] = $valueSchema;
}
else {
/** @var array<string, TValue> $keywords */
$keywords = ArrayUtil::mergeRecursive($keywords, $valueSchema->getKeywords());
}
}
elseif ($field->isReadOnly()) {
$keywords['readOnly'] = TRUE;
$keywords['const'] = NULL;
];
}
else {
$properties['content'] = new JsonSchemaString($contentKeywords);
$keywords['required'][] = 'content';
$currentFile = NULL;
}

return new JsonSchemaObject($properties, $keywords, $field->isNullable());
return new JsonSchemaFile(
$currentFile, $field->getMaxFileSize(), ['readOnly' => $field->isReadOnly()], $field->isNullable()
);
}

public function supportsField(AbstractFormField $field): bool {
Expand Down
117 changes: 117 additions & 0 deletions Civi/RemoteTools/JsonSchema/JsonSchemaFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php
declare(strict_types = 1);

namespace Civi\RemoteTools\JsonSchema;

use Civi\RemoteTools\Util\ArrayUtil;
use Civi\RemoteTools\Util\FormatUtil;
use CRM_Remotetools_ExtensionUtil as E;

/**
* Custom schema for a CiviCRM file field.
*
* @phpstan-import-type TValue from JsonSchema
*
* @phpstan-type validFileSchemaDataT array{
* filename: string,
* url: string,
* }|array{
* filename: string,
* content: string,
* }
* Valid data. "url" is set, if the value is unchanged (existing file).
* "content" is set, if a new file is uploaded.
*/
final class JsonSchemaFile extends JsonSchemaObject {

/**
* @phpstan-param array{filename: string, url: string}|null $currentFile
* The current file, if any, is necessary to create the correct schema.
*/
public function __construct(
?array $currentFile = NULL,
?int $maxFileSize = NULL,
array $keywords = [],
bool $nullable = FALSE
) {
$properties = [
'filename' => new JsonSchemaString(['minLength' => 1, 'maxLength' => 255]),
];
$keywords['required'] = ['filename'];

$contentKeywords = ['contentEncoding' => 'base64'];
if (NULL !== $maxFileSize && $maxFileSize > 0) {
$contentKeywords['$validations'] = [
JsonSchema::fromArray([
'keyword' => 'maxLength',
// The file might need up to 37 % more space through Base64 encoding.
'value' => (int) ceil($maxFileSize * 1.37),
'message' => E::ts('The file must not be larger than %1.',
[1 => FormatUtil::toHumanReadableBytes($maxFileSize)]
),
]),
];
}

if (NULL !== $currentFile) {
$keywords['default'] = JsonSchema::fromArray([
'filename' => $currentFile['filename'],
'url' => $currentFile['url'],
]);

[$urlWithoutQuery] = explode('?', $currentFile['url'], 2);

// Matches the default (current file).
$currentFileSchema = JsonSchema::fromArray([
'properties' => [
'filename' => new JsonSchemaString(['const' => $currentFile['filename']]),
// We don't use the 'const' keyword because the URL might contain a
// hash that depends on the time. Thus, we exclude the query from
// the test.
'url' => new JsonSchemaString(['format' => 'uri', 'pattern' => '^' . $urlWithoutQuery]),
],
'required' => ['url'],
]);
$newFileSchema = JsonSchema::fromArray([
'properties' => [
'content' => new JsonSchemaString($contentKeywords),
],
'required' => ['content'],
]);

if (TRUE === ($keywords['readOnly'] ?? NULL)) {
// Allow only the current file.
$valueSchema = $currentFileSchema;
}
else {
// Allow either the current file or a new file.
$valueSchema = JsonSchema::fromArray([
// Test if property 'url' exists.
'if' => ['required' => ['url']],
'then' => $currentFileSchema,
'else' => $newFileSchema,
]);
}

if ($nullable) {
$keywords['if'] = JsonSchema::fromArray(['not' => new JsonSchemaNull()]);
$keywords['then'] = $valueSchema;
}
else {
/** @var array<string, TValue> $keywords */
$keywords = ArrayUtil::mergeRecursive($keywords, $valueSchema->getKeywords());
}
}
elseif (TRUE === ($keywords['readOnly'] ?? NULL)) {
$keywords['readOnly'] = TRUE;
$keywords['const'] = NULL;
}
else {
$properties['content'] = new JsonSchemaString($contentKeywords);
$keywords['required'][] = 'content';
}

parent::__construct($properties, $keywords);
}

}

0 comments on commit 72a7487

Please sign in to comment.