<?php
/**
* @file tools/buildSwagger.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class buildSwagger
*
* @ingroup tools
*
* @brief CLI tool to compile a complete swagger.json file for hosting API
* documentation.
*/
use APP\core\Services;
use PKP\decision\DecisionType;
use PKP\file\FileManager;
define('APP_ROOT', dirname(__FILE__, 4));
require(APP_ROOT . '/tools/bootstrap.php');
class buildSwagger extends \PKP\cliTool\CommandLineTool
{
public $outputFile;
public $parameters;
/**
* Constructor.
*
* @param array $argv command-line arguments (see usage)
*/
public function __construct($argv = [])
{
parent::__construct($argv);
$this->outputFile = array_shift($this->argv);
}
/**
* Print command usage information.
*/
public function usage()
{
echo "Command-line tool to compile swagger.json API definitions\n"
. "Usage:\n"
. "\t{$this->scriptName} [outputFile]: Compile swagger file and save to [outputFile]\n"
. "\t{$this->scriptName} usage: Display usage information this tool\n";
}
/**
* Parse and execute the import/export task.
*/
public function execute()
{
if (empty($this->outputFile)) {
$this->usage();
exit;
} elseif ((file_exists($this->outputFile) && !is_writable($this->outputFile)) ||
(!is_writeable(dirname($this->outputFile)))) {
echo "You do not have permission to write to this file.\n";
exit;
} else {
$source = file_get_contents(APP_ROOT . '/docs/dev/swagger-source.json');
$decisions = file_get_contents(APP_ROOT . '/docs/dev/swagger-source-decision-examples.json');
if (!$source || !$decisions) {
echo 'Unable to find source files at ' . APP_ROOT . '/docs/dev/';
exit;
}
$locales = ['en', 'fr_CA'];
$apiSchema = json_decode($source);
foreach ($apiSchema->definitions as $definitionName => $definition) {
// We assume a definition that is not a string does not need to be compiled
// from the schema files. It has already been defined.
if (!is_string($definition)) {
continue;
}
$editDefinition = $summaryDefinition = $readDefinition = ['type' => 'object', 'properties' => []];
$entitySchema = Services::get('schema')->get($definition, true);
foreach ($entitySchema->properties as $propName => $propSchema) {
$editPropSchema = clone $propSchema;
$readPropSchema = clone $propSchema;
$summaryPropSchema = clone $propSchema;
// Special handling to catch readOnly, writeOnly and apiSummary props in objects
if (!empty($propSchema->{'$ref'})) {
if (empty($propSchema->readOnly) || empty($propSchema->writeDisabledInApi)) {
$editPropSchema->properties = $propSchema;
}
if (empty($propSchema->writeOnly)) {
$readPropSchema->properties = $propSchema;
}
if (!empty($propSchema->apiSummary)) {
$summaryPropSchema->properties = $propSchema;
}
} elseif ($propSchema->type === 'object') {
$subPropsEdit = $subPropsRead = $subPropsSummary = [];
foreach ($propSchema->properties as $subPropName => $subPropSchema) {
if (empty($subPropSchema->readOnly) && empty($propSchema->writeDisabledInApi)) {
$subPropsEdit[$subPropName] = $subPropSchema;
}
if (empty($subPropSchema->writeOnly)) {
$subPropsRead[$subPropName] = $subPropSchema;
}
if (!empty($subPropSchema->apiSummary)) {
$subPropsSummary[$subPropName] = $subPropSchema;
}
}
if (!empty($propSchema->multilingual)) {
$subPropsSchemaEdit = $subPropsSchemaRead = $subPropsSchemaSummary = [
'type' => 'object',
'properties' => [],
];
foreach ($locales as $localeKey) {
$subPropsSchemaEdit[$localeKey]['properties'] = $subPropsEdit;
$subPropsSchemaRead[$localeKey]['properties'] = $subPropsRead;
$subPropsSchemaSummary[$localeKey]['properties'] = $subPropsSummary;
}
} else {
$subPropsSchemaEdit = $subPropsEdit;
$subPropsSchemaRead = $subPropsRead;
$subPropsSchemaSummary = $subPropsSummary;
}
if (empty($propSchema->readOnly) && empty($propSchema->writeDisabledInApi)) {
$editPropSchema->properties = $subPropsSchemaEdit;
}
if (empty($propSchema->writeOnly)) {
$readPropSchema->properties = $subPropsSchemaRead;
}
if (!empty($propSchema->apiSummary)) {
$summaryPropSchema->properties = $subPropsSchemaSummary;
}
// All non-object props
} else {
if (!empty($propSchema->multilingual)) {
if ($propSchema->type === 'array') {
$subProperties = [];
foreach ($locales as $localeKey) {
$subProperties[$localeKey] = $propSchema->items;
}
if (empty($propSchema->readOnly) && empty($propSchema->writeDisabledInApi)) {
$editPropSchema->properties = $subProperties;
}
if (empty($propSchema->writeOnly)) {
$readPropSchema->properties = $subProperties;
}
if (!empty($propSchema->apiSummary)) {
$summaryPropSchema->properties = $subProperties;
}
} else {
if (empty($propSchema->readOnly) && empty($propSchema->writeDisabledInApi)) {
$editPropSchema = ['$ref' => '#/definitions/LocaleObject'];
}
if (empty($propSchema->writeOnly)) {
$readPropSchema = ['$ref' => '#/definitions/LocaleObject'];
}
if (!empty($propSchema->apiSummary)) {
$summaryPropSchema = ['$ref' => '#/definitions/LocaleObject'];
}
}
}
}
if (empty($propSchema->readOnly) && empty($propSchema->writeDisabledInApi)) {
$editDefinition['properties'][$propName] = $editPropSchema;
}
if (empty($propSchema->writeOnly)) {
$readDefinition['properties'][$propName] = $readPropSchema;
}
if (!empty($propSchema->apiSummary)) {
$summaryDefinition['properties'][$propName] = $summaryPropSchema;
}
}
if (!empty($editDefinition['properties'])) {
$definitionEditableName = $definitionName . 'Editable';
ksort($editDefinition['properties']);
$apiSchema->definitions->{$definitionEditableName} = $this->setEnum($editDefinition);
}
if (!empty($readDefinition['properties'])) {
ksort($readDefinition['properties']);
$apiSchema->definitions->{$definitionName} = $this->setEnum($readDefinition);
}
if (!empty($summaryDefinition['properties'])) {
$definitionSummaryName = $definitionName . 'Summary';
ksort($summaryDefinition['properties']);
$apiSchema->definitions->{$definitionSummaryName} = $this->setEnum($summaryDefinition);
}
}
$this->addDecisionExamples($apiSchema, json_decode($decisions));
file_put_contents($this->outputFile, json_encode($apiSchema, JSON_PRETTY_PRINT));
echo "Done\n";
}
}
/**
* Convert the `in:` validation rules to swagger's
* `enum` specification
*/
protected function setEnum(array $definition): array
{
foreach ($definition['properties'] as $propName => $schema) {
if (isset($schema->validation) && is_array($schema->validation)) {
foreach ($schema->validation as $rule) {
if (substr($rule, 0, 3) === 'in:') {
$enum = explode(',', substr($rule, 3));
if ($schema->type === 'integer') {
$enum = array_map('intval', $enum);
}
$definition['properties'][$propName]->enum = $enum;
}
}
}
}
return $definition;
}
/**
* Add the example request bodies for each decision
*/
protected function addDecisionExamples(stdClass $schema, stdClass $decisions): void
{
$examples = [];
foreach ($decisions as $class => $decision) {
/** @var DecisionType $object */
$object = new $class();
$value = [
'decision' => $object->getDecision(),
];
if ($this->isDecisionInReview($object)) {
$value['reviewRound'] = 123;
$value['round'] = 1;
}
if (!empty($decision->actions)) {
$value['actions'] = array_map(
function (stdClass $action) {
if ($action->type === 'form') {
return array_merge((array) $action->data, ['id' => $action->id]);
} elseif ($action->type === 'email') {
return [
'attachments' => [
[
'name' => 'example-upload.pdf',
'temporaryFileId' => 1,
'documentType' => FileManager::DOCUMENT_TYPE_PDF
],
[
'name' => 'example-submission-file.pdf',
'submissionFileId' => 1,
'documentType' => FileManager::DOCUMENT_TYPE_PDF
],
[
'name' => 'example-library-file.pdf',
'libraryFileId' => 1,
'documentType' => FileManager::DOCUMENT_TYPE_PDF
]
],
'bcc' => '[email protected]',
'cc' => '[email protected]',
'id' => $action->id,
'locale' => 'en',
'recipients' => $action->canChangeRecipients
? [1,2]
: [],
'subject' => 'Example email subject',
'body' => '<p>Example email body.</p>',
];
}
throw new Exception('Unrecognized decision action type. Can not compile example request body for decision ' . $class);
},
$decision->actions ?? []
);
}
$examples[$class] = [
'summary' => $object->getLabel(),
'value' => $value,
];
}
$schema
->paths
->{'/submissions/{submissionId}/decisions'}
->post
->requestBody
->content
->{'application/json'}
->examples = $examples;
}
/**
* Is the decision type in a review stage?
*/
protected function isDecisionInReview(DecisionType $decision): bool
{
return in_array($decision->getStageId(), [WORKFLOW_STAGE_ID_INTERNAL_REVIEW, WORKFLOW_STAGE_ID_EXTERNAL_REVIEW]);
}
}
$tool = new buildSwagger($argv ?? []);
$tool->execute();
|