Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract JSON schema generated by FileFieldFactory into an own class #55

Merged
merged 2 commits into from
Oct 10, 2024
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
86 changes: 8 additions & 78 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,25 @@ 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())]
),
]),
];
$keywords = [];
if ($field->isReadOnly()) {
$keywords['readOnly'] = TRUE;
}

if ($field->hasDefaultValue() && NULL !== $field->getFilename() && NULL !== $field->getUrl()) {
$keywords['default'] = JsonSchema::fromArray([
'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());
}
// If the field is read only, we cannot use the const keyword, because
// the URL might contain a hash that depends on the time. This is at least
// true for the /civicrm/file path.
}
elseif ($field->isReadOnly()) {
$keywords['readOnly'] = TRUE;
$keywords['const'] = NULL;
}
else {
$properties['content'] = new JsonSchemaString($contentKeywords);
$keywords['required'][] = 'content';
}

return new JsonSchemaObject($properties, $keywords, $field->isNullable());
return new JsonSchemaFile($field->getMaxFileSize(), $keywords, $field->isNullable());
}

public function supportsField(AbstractFormField $field): bool {
Expand Down
91 changes: 91 additions & 0 deletions Civi/RemoteTools/JsonSchema/JsonSchemaFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?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 {

public function __construct(
?int $maxFileSize = NULL,
array $keywords = [],
bool $nullable = FALSE
) {
$properties = ($keywords['properties'] ?? []) + [
'filename' => new JsonSchemaString(['minLength' => 1, 'maxLength' => 255]),
];
$keywords['required'] = ['filename'];

// Matches a current file, i.e. 'url' is set.
$currentFileSchema = JsonSchema::fromArray([
'properties' => [
'url' => new JsonSchemaString(['format' => 'uri']),
],
'required' => ['url'],
]);

if (TRUE === ($keywords['readOnly'] ?? NULL)) {
// Allow only a current file.
$valueSchema = $currentFileSchema;
}
else {
// Allow either a current file or a new file.
$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)]
),
]),
];
}

$newFileSchema = JsonSchema::fromArray([
'properties' => [
'content' => new JsonSchemaString($contentKeywords),
],
'required' => ['content'],
]);
$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());
}

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

}
Loading