Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/Command/Api/ApiBaseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ private function castParamType(array $paramSpec, array|string|bool|int $value):
if (in_array('integer', $types, true) && ctype_digit($value)) {
return $this->doCastParamType('integer', $value);
}
} elseif ($paramSpec['type'] === 'array') {
} elseif ($this->getParamType($paramSpec) === 'array') {
if (is_array($value) && count($value) === 1) {
return $this->castParamToArray($paramSpec, $value[0]);
}
Expand Down
50 changes: 40 additions & 10 deletions src/Command/Api/ApiCommandHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ private function addApiCommandParameters(array $schema, array $acquiaCloudSpec,
$requestBodySchema = $this->getRequestBodyFromParameterSchema($schema, $acquiaCloudSpec);
/** @var \Symfony\Component\Console\Input\InputOption|InputArgument $parameterDefinition */
foreach ($bodyInputDefinition as $parameterDefinition) {
$parameterSpecification = $this->getPropertySpecFromRequestBodyParam($requestBodySchema, $parameterDefinition);
$parameterSpecification = $this->getPropertySpecFromRequestBodyParam($requestBodySchema, $parameterDefinition, $acquiaCloudSpec);
$command->addPostParameter($parameterDefinition->getName(), $parameterSpecification);
}
$usage .= $requestBodyParamUsageSuffix;
Expand Down Expand Up @@ -125,6 +125,12 @@ private function addApiCommandParametersForRequestBody(array $schema, array $acq
$requestBodySchema['properties'] = [];
}
foreach ($requestBodySchema['properties'] as $propKey => $paramDefinition) {
// Resolve $ref inside individual property definitions.
if (array_key_exists('$ref', $paramDefinition)) {
$parts = explode('/', $paramDefinition['$ref']);
$paramKey = end($parts);
$paramDefinition = $this->getParameterSchemaFromSpec($paramKey, $acquiaCloudSpec);
}
$isRequired = array_key_exists('required', $requestBodySchema) && in_array($propKey, $requestBodySchema['required'], true);
$propKey = self::renameParameter($propKey);

Expand All @@ -141,15 +147,15 @@ private function addApiCommandParametersForRequestBody(array $schema, array $acq
array_key_exists('type', $paramDefinition) && $paramDefinition['type'] === 'array' ? InputArgument::IS_ARRAY | InputArgument::REQUIRED : InputArgument::REQUIRED,
$description
);
$usage = $this->addPostArgumentUsageToExample($schema['requestBody'], $propKey, $paramDefinition, 'argument', $usage, $acquiaCloudSpec);
$usage = $this->addPostArgumentUsageToExample($schema['requestBody'], $propKey, $paramDefinition, 'argument', $usage ? $usage . ' ' : '', $acquiaCloudSpec);
} else {
$inputDefinition[] = new InputOption(
$propKey,
null,
array_key_exists('type', $paramDefinition) && $paramDefinition['type'] === 'array' ? InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED : InputOption::VALUE_REQUIRED,
array_key_exists('description', $paramDefinition) ? $paramDefinition['description'] : $propKey
);
$usage = $this->addPostArgumentUsageToExample($schema["requestBody"], $propKey, $paramDefinition, 'option', $usage, $acquiaCloudSpec);
$usage = $this->addPostArgumentUsageToExample($schema["requestBody"], $propKey, $paramDefinition, 'option', $usage ? $usage . ' ' : '', $acquiaCloudSpec);
// @todo Add validator for $param['enum'] values?
}
}
Expand All @@ -168,31 +174,42 @@ private function addApiCommandParametersForRequestBody(array $schema, array $acq
private function addPostArgumentUsageToExample(mixed $requestBody, mixed $propKey, mixed $paramDefinition, string $type, string $usage, array $acquiaCloudSpec): string
{
$requestBodyContent = $this->getRequestBodyContent($requestBody, $acquiaCloudSpec);

// Example may live directly on the content-type object (inline requestBody),
// or nested inside schema (e.g. $ref-resolved requestBodies).
if (array_key_exists('example', $requestBodyContent)) {
$example = $requestBodyContent['example'];
} elseif (array_key_exists('schema', $requestBodyContent) && array_key_exists('example', $requestBodyContent['schema'])) {
$example = $requestBodyContent['schema']['example'];
} else {
return $usage;
}

if ($example) {
$prefix = $type === 'argument' ? '' : "--$propKey=";
if (array_key_exists($propKey, $example)) {
if (!array_key_exists('type', $paramDefinition)) {
return $usage;
}
$parts = [];
switch ($paramDefinition['type']) {
case 'object':
$usage .= $prefix . '"' . json_encode($example[$propKey], JSON_THROW_ON_ERROR) . '"" ';
// Wrap JSON in single quotes so inner double quotes remain shell-safe.
$parts[] = sprintf("%s'%s'", $prefix, json_encode($example[$propKey], JSON_THROW_ON_ERROR));
break;

case 'array':
$isMultidimensional = count($example[$propKey]) !== count($example[$propKey], COUNT_RECURSIVE);
if (!$isMultidimensional) {
foreach ($example[$propKey] as $value) {
$usage .= $prefix . "\"$value\" ";
$parts[] = sprintf("%s'%s'", $prefix, $value);
}
} else {
// @todo Pretty sure prevents the user from using the arguments.
// Probably a bug. How can we allow users to specify a multidimensional array as an
// argument?
$value = json_encode($example[$propKey], JSON_THROW_ON_ERROR);
$usage .= $prefix . "\"$value\" ";
// Wrap JSON in single quotes so inner double quotes remain shell-safe.
$parts[] = sprintf("%s'%s'", $prefix, $value);
}
break;

Expand All @@ -204,9 +221,12 @@ private function addPostArgumentUsageToExample(mixed $requestBody, mixed $propKe
} else {
$value = $example[$propKey];
}
$usage .= $prefix . "\"$value\" ";
$parts[] = sprintf("%s'%s'", $prefix, $value);
break;
}
if ($parts !== []) {
return $usage . implode(' ', $parts);
}
}
}
return $usage;
Expand Down Expand Up @@ -482,10 +502,20 @@ private function getRequestBodyFromParameterSchema(array $schema, array $acquiaC
return $requestBodySchema;
}

private function getPropertySpecFromRequestBodyParam(array $requestBodySchema, mixed $parameterDefinition): mixed
private function getPropertySpecFromRequestBodyParam(array $requestBodySchema, mixed $parameterDefinition, array $acquiaCloudSpec = []): mixed
{
$name = self::restoreRenamedParameter($parameterDefinition->getName());
return $requestBodySchema['properties'][$name] ?? null;
$spec = $requestBodySchema['properties'][$name] ?? [];

// Resolve $ref in the property spec so downstream code (e.g. castParamType) always
// receives a fully resolved spec with a 'type' key rather than a bare $ref object.
if (array_key_exists('$ref', $spec)) {
$parts = explode('/', $spec['$ref']);
$paramKey = end($parts);
$spec = $this->getParameterSchemaFromSpec($paramKey, $acquiaCloudSpec);
}

return $spec;
}

/**
Expand Down
37 changes: 37 additions & 0 deletions tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,41 @@ public function testNamespaceWithAllHiddenCommandsDoesNotGetListCommand(): void
$listCommands = $this->generateApiListCommands($apiCommands);
$this->assertArrayNotHasKey('api:baz', $listCommands);
}

/**
* Calls private or protected method of ApiCommandHelper class via reflection.
*
* @throws \ReflectionException
*/
private function invokeApiCommandHelperMethod(string $methodName, array $args = []): mixed
{
$commandHelper = new ApiCommandHelper($this->logger);
$refClass = new ReflectionMethod($commandHelper::class, $methodName);
return $refClass->invokeArgs($commandHelper, $args);
}

/**
* Test that addPostArgumentUsageToExample correctly formats a flat array with a single item.
*/
public function testAddPostArgumentUsageToExampleFlatArraySingleItem(): void
{
$result = $this->invokeApiCommandHelperMethod(
'addPostArgumentUsageToExample',
[
[
'content' => [
'application/json' => [
'example' => ['tags' => ['drupal']],
],
],
],
'tags',
['type' => 'array'],
'option',
'',
[],
]
);
$this->assertSame("--tags='drupal'", $result);
}
}
55 changes: 50 additions & 5 deletions tests/phpunit/src/Commands/Api/ApiCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -543,12 +543,49 @@ public static function providerTestApiCommandDefinitionRequestBody(): array
[
'api:accounts:ssh-key-create',
'post',
'api:accounts:ssh-key-create "mykey" "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQChwPHzTTDKDpSbpa2+d22LcbQmsw92eLsUK3Fmei1fiGDkd34NsYCN8m7lsi3NbvdMS83CtPQPWiCveYPzFs1/hHc4PYj8opD2CNnr5iWVVbyaulCYHCgVv4aB/ojcexg8q483A4xJeF15TiCr/gu34rK6ucTvC/tn/rCwJBudczvEwt0klqYwv8Cl/ytaQboSuem5KgSjO3lMrb6CWtfSNhE43ZOw+UBFBqxIninN868vGMkIv9VY34Pwj54rPn/ItQd6Ef4B0KHHaGmzK0vfP+AK7FxNMoHnj3iYT33KZNqtDozdn5tYyH/bThPebEtgqUn+/w5l6wZIC/8zzvls/127ngHk+jNa0PlNyS2TxhPUK4NaPHIEnnrlp07JEYC4ImcBjaYCWAdcTcUkcJjwZQkN4bGmyO9cjICH98SdLD/HxqzTHeaYDbAX/Hu9HfaBb5dXLWsjw3Xc6hoVnUUZbMQyfgb0KgxDLh92eNGxJkpZiL0VDNOWCxDWsNpzwhLNkLqCvI6lyxiLaUzvJAk6dPaRhExmCbU1lDO2eR0FdSwC1TEhJOT9eDIK1r2hztZKs2oa5FNFfB/IFHVWasVFC9N2h/r/egB5zsRxC9MqBLRBq95NBxaRSFng6ML5WZSw41Qi4C/JWVm89rdj2WqScDHYyAdwyyppWU4T5c9Fmw== example@example.com"',
['api:accounts:ssh-key-create \'mykey\' \'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQChwPHzTTDKDpSbpa2+d22LcbQmsw92eLsUK3Fmei1fiGDkd34NsYCN8m7lsi3NbvdMS83CtPQPWiCveYPzFs1/hHc4PYj8opD2CNnr5iWVVbyaulCYHCgVv4aB/ojcexg8q483A4xJeF15TiCr/gu34rK6ucTvC/tn/rCwJBudczvEwt0klqYwv8Cl/ytaQboSuem5KgSjO3lMrb6CWtfSNhE43ZOw+UBFBqxIninN868vGMkIv9VY34Pwj54rPn/ItQd6Ef4B0KHHaGmzK0vfP+AK7FxNMoHnj3iYT33KZNqtDozdn5tYyH/bThPebEtgqUn+/w5l6wZIC/8zzvls/127ngHk+jNa0PlNyS2TxhPUK4NaPHIEnnrlp07JEYC4ImcBjaYCWAdcTcUkcJjwZQkN4bGmyO9cjICH98SdLD/HxqzTHeaYDbAX/Hu9HfaBb5dXLWsjw3Xc6hoVnUUZbMQyfgb0KgxDLh92eNGxJkpZiL0VDNOWCxDWsNpzwhLNkLqCvI6lyxiLaUzvJAk6dPaRhExmCbU1lDO2eR0FdSwC1TEhJOT9eDIK1r2hztZKs2oa5FNFfB/IFHVWasVFC9N2h/r/egB5zsRxC9MqBLRBq95NBxaRSFng6ML5WZSw41Qi4C/JWVm89rdj2WqScDHYyAdwyyppWU4T5c9Fmw== example@example.com\''],
],
[
'api:environments:file-copy',
'post',
'12-d314739e-296f-11e9-b210-d663bd873d93 --source="14-0c7e79ab-1c4a-424e-8446-76ae8be7e851"',
[
'api:environments:file-copy 12-d314739e-296f-11e9-b210-d663bd873d93 --source=\'14-0c7e79ab-1c4a-424e-8446-76ae8be7e851\'',
'api:environments:file-copy myapp.dev --source=\'14-0c7e79ab-1c4a-424e-8446-76ae8be7e851\'',
],
],
[
'api:private-networks:create',
'post',
['api:private-networks:create \'123e4567-e89b-12d3-a456-426614174000\' \'us-east-1\' \'customer-private-network\' --description=\'Private network for customer\' --label=\'anyLabel\' --isolation=\'{"dedicated_compute":false,"dedicated_network":false}\' --ingress=\'{"drupal_ssh":{"ingress_acls":["test-acls"]}}\' \'{"cidr":"114.7.55.1\/16","private_egress_access":{"drupal":true},"vpns":[{"name":"vpn1","gateway_ip":"10.10.10.10","routes":["127.0.0.1\/32","127.0.0.2\/32"],"tunnel1":{"shared_key":"sharedKey1","internal_cidr":"192.1.1.0\/24","ike_versions":"1","startup_action":"start","dpd_timeout_action":"stop"},"tunnel2":{"shared_key":"sharedKey2","internal_cidr":"192.1.1.0\/14","ike_versions":"1","startup_action":"start","dpd_timeout_action":"stop"}}],"vpc_peers":[{"name":"vpcPeer1","aws_account":"123456789012","vpc_id":"vpc-1234567890abcdef0","vpc_cidr":"120.24.16.1\/24"}]}\''],
],
[
'api:private-networks:update-isolation',
'put',
['api:private-networks:update-isolation --dedicated_compute=\'1\' --dedicated_network=\'1\''],
],
[
'api:environments:livedev-disable',
'post',
[
'api:environments:livedev-disable 12-d314739e-296f-11e9-b210-d663bd873d93 --discard=\'1\'',
'api:environments:livedev-disable myapp.dev --discard=\'1\'',
],
],
[
'api:environments:code-deploy',
'post',
[
'api:environments:code-deploy 12-d314739e-296f-11e9-b210-d663bd873d93 \'14-0c7e79ab-1c4a-424e-8446-76ae8be7e851\' --message=\'Optional commit message\'',
'api:environments:code-deploy myapp.dev \'14-0c7e79ab-1c4a-424e-8446-76ae8be7e851\' --message=\'Optional commit message\'',
],
],
[
'api:environments:code-switch',
'post',
[
'api:environments:code-switch 12-d314739e-296f-11e9-b210-d663bd873d93 \'my-feature-branch\'',
'api:environments:code-switch myapp.dev \'my-feature-branch\'',
],
],
];
}
Expand All @@ -559,19 +596,27 @@ public static function providerTestApiCommandDefinitionRequestBody(): array
* @param $method
* @param $usage
*/
public function testApiCommandDefinitionRequestBody(string $commandName, string $method, string $usage): void
public function testApiCommandDefinitionRequestBody(string $commandName, string $method, array $usage): void
{
$this->command = $this->getApiCommandByName($commandName);
$resource = self::getResourceFromSpec($this->command->getPath(), $method);
foreach ($resource['requestBody']['content']['application/hal+json']['example'] as $propKey => $value) {
if (array_key_exists('$ref', $resource['requestBody'])) {
$cloudApiSpec = self::getCloudApiSpec();
$parts = explode('/', $resource['requestBody']['$ref']);
$paramKey = end($parts);
$resource['requestBody'] = $cloudApiSpec['components']['requestBodies'][$paramKey];
}
$example = $resource['requestBody']['content']['application/hal+json']['example'] ?? $resource['requestBody']['content']['application/json']['schema']['example'] ?? $resource['requestBody']['content']['application/json']['example'] ?? null;
self::assertNotEmpty($example);
foreach ($example as $propKey => $value) {
$this->assertTrue(
$this->command->getDefinition()
->hasArgument($propKey) || $this->command->getDefinition()
->hasOption($propKey),
"Command {$this->command->getName()} does not have expected argument or option $propKey"
);
}
$this->assertStringContainsString($usage, $this->command->getUsages()[0]);
$this->assertSame($usage, $this->command->getUsages());
}

public function testGetApplicationUuidFromBltYml(): void
Expand Down
Loading