Skip to content

Commit e7f5e7f

Browse files
authored
Merge pull request #1940 from appwrite/fix/cli-generated-examples
fix(cli): generate shell-safe command examples
2 parents 7c82582 + 736774a commit e7f5e7f

2 files changed

Lines changed: 81 additions & 43 deletions

File tree

‎src/SDK/Language/Concern/CliCommandSurface.php‎

Lines changed: 80 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Utopia\OpenAPI\Model\Operation;
66
use Utopia\OpenAPI\Model\Parameter;
77
use Utopia\OpenAPI\Model\Schema;
8+
use Utopia\OpenAPI\Model\StringSchema;
89
use Utopia\OpenAPI\Model\Tag;
910
use Utopia\OpenAPI\Specification;
1011
use Override;
@@ -514,6 +515,7 @@ protected function getCliHelpOptionOrder(): array
514515
protected function getCliHelpFunctions(): array
515516
{
516517
return [
518+
new TwigFunction('cliExample', fn(Operation $method, Tag $service): array => $this->getCliExample($method, $service)),
517519
new TwigFunction('cliHelpGroups', fn (): array => $this->getCliHelpGroups()),
518520
new TwigFunction('cliHelpSummaries', fn (string $title): array => $this->getCliHelpSummaries($title)),
519521
new TwigFunction('cliHelpOptionOrder', fn (): array => $this->getCliHelpOptionOrder()),
@@ -524,43 +526,92 @@ protected function getCliHelpFunctions(): array
524526
}
525527

526528
/**
527-
* How a value is spelled on the command line, for the docs examples. This
528-
* is a shell invocation, not source in either CLI's implementation
529-
* language, so it belongs to the shared surface: the target language of the
530-
* generated CLI cannot change how a user types an array of roles.
529+
* Render required spec parameters using the registered CLI option metadata.
531530
*
532-
* A trait method wins over the inherited Go implementation, which renders
533-
* Go literals; `--roles []string{}` is not a command anybody can run.
531+
* @return list<string>
532+
*/
533+
protected function getCliExample(Operation $method, Tag $service): array
534+
{
535+
$arguments = [];
536+
foreach ($this->getOperationParameters($method) as $parameter) {
537+
if (!$parameter->required) {
538+
continue;
539+
}
540+
541+
$option = $this->getCliOption($parameter, $service);
542+
$flag = '--' . $option['flag'];
543+
544+
if ($option['register'] === 'StringArray') {
545+
$itemSchema = $this->getArraySchema($parameter) ?? new StringSchema();
546+
$example = $this->getSchema($parameter)->example;
547+
if (is_string($example)) {
548+
$decoded = json_decode($example);
549+
if (json_last_error() === JSON_ERROR_NONE) {
550+
$example = $decoded;
551+
}
552+
}
553+
554+
$items = is_array($example) && $example !== [] ? $example : [$itemSchema->example];
555+
foreach ($items as $item) {
556+
$arguments[] = $flag . ' ' . $this->getParamExample($itemSchema, example: $item);
557+
}
558+
continue;
559+
}
560+
561+
$separator = $option['register'] === 'Bool' ? '=' : ' ';
562+
$arguments[] = $flag . $separator . $this->getParamExample($parameter);
563+
}
564+
565+
$query = $this->getCliQueryConfig($method);
566+
if ($query['hasPagination']) {
567+
$arguments[] = '--limit 25';
568+
}
569+
if ($this->getMethodType($method) === 'location') {
570+
$arguments[] = '--destination ./download';
571+
}
572+
573+
return $arguments;
574+
}
575+
576+
protected function quoteCliExample(mixed $value): string
577+
{
578+
$text = is_string($value)
579+
? $value
580+
: json_encode($value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
581+
582+
if ($text !== '' && !preg_match('/[^A-Za-z0-9_@%+=:,\.\/-]/', $text)) {
583+
return $text;
584+
}
585+
586+
return "'" . str_replace("'", "'\"'\"'", $text) . "'";
587+
}
588+
589+
/**
590+
* Render a schema example as a shell-safe CLI value.
534591
*/
535592
#[Override]
536-
public function getParamExample(Schema|Parameter $param, string $lang = ''): string
593+
public function getParamExample(Schema|Parameter $param, string $lang = '', mixed $example = null): string
537594
{
538595
$type = $this->getSchemaType($param);
539-
$example = $this->getSchemaExample($param);
540-
541-
if (empty($example) && $example !== 0 && $example !== false) {
542-
return match ($type) {
543-
self::TYPE_NUMBER, self::TYPE_INTEGER, self::TYPE_BOOLEAN => 'null',
544-
self::TYPE_STRING => "''",
545-
self::TYPE_ARRAY => 'one two three',
546-
self::TYPE_OBJECT => '\'{ "key": "value" }\'',
547-
self::TYPE_FILE => "'path/to/file.png'",
548-
default => '',
549-
};
596+
$example ??= $this->getSchema($param)->example;
597+
if (in_array($type, [self::TYPE_ARRAY, self::TYPE_OBJECT], true) && is_string($example)) {
598+
$decoded = json_decode($example);
599+
if (json_last_error() === JSON_ERROR_NONE) {
600+
$example = $decoded;
601+
}
602+
}
603+
604+
if ($type === self::TYPE_OBJECT && is_array($example)) {
605+
$example = (object) $example;
550606
}
551607

552608
return match ($type) {
553-
self::TYPE_ARRAY => (\str_contains((string) $example, '[') && \str_contains((string) $example, ']'))
554-
? \implode(' ', \explode(',', \substr((string) $example, 1, -1)))
555-
: (string) $example,
556-
self::TYPE_OBJECT => '\'{ "key": "value" }\'',
557-
self::TYPE_NUMBER, self::TYPE_INTEGER => (string) $example,
558-
self::TYPE_BOOLEAN => $example ? 'true' : 'false',
559-
self::TYPE_STRING => \preg_match('/[^A-Za-z0-9_@%+=:,\.\/-]/', (string) $example)
560-
? "'" . \str_replace("'", "'\"'\"'", (string) $example) . "'"
561-
: (string) $example,
562-
self::TYPE_FILE => "'path/to/file.png'",
563-
default => '',
609+
self::TYPE_ARRAY => $this->quoteCliExample($example ?? []),
610+
self::TYPE_OBJECT => $this->quoteCliExample($example ?? (object) ['key' => 'value']),
611+
self::TYPE_NUMBER, self::TYPE_INTEGER => $this->quoteCliExample($example ?? 1),
612+
self::TYPE_BOOLEAN => $this->quoteCliExample($example ?? false),
613+
self::TYPE_FILE => $this->quoteCliExample('path/to/file.png'),
614+
default => $this->quoteCliExample($example ?? $this->getEnumSchema($param)->enum[0] ?? '<VALUE>'),
564615
};
565616
}
566617
}

‎templates/cli/docs/example.md.twig‎

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,5 @@
11
```bash
2-
{% set requiredParams = [] %}
3-
{% set query = method | cliQueryConfig %}
4-
{% for parameter in (method | parameters('all')) %}
5-
{% if parameter.required %}
6-
{% set requiredParams = requiredParams|merge([parameter]) %}
7-
{% endif %}
8-
{% endfor %}
9-
{% set exampleLines = [] %}
10-
{% for parameter in requiredParams %}
11-
{% set exampleLines = exampleLines|merge(['--' ~ (parameter.name | caseKebab) ~ ' ' ~ (parameter | paramExample)]) %}
12-
{% endfor %}
13-
{% if query.hasPagination %}
14-
{% set exampleLines = exampleLines|merge(['--limit 25']) %}
15-
{% endif %}
2+
{% set exampleLines = cliExample(method, service) %}
163
{% if method | cliIsTopLevelAlias(service) %}
174
{{ language.params.executableName }} {{ (method | methodName) | caseKebab }}{% if exampleLines | length > 0 %} \{% endif %}
185
{% else %}

0 commit comments

Comments
 (0)