diff --git a/src/SDK/Language.php b/src/SDK/Language.php index 1beeac2509..8be627b8a7 100644 --- a/src/SDK/Language.php +++ b/src/SDK/Language.php @@ -458,6 +458,17 @@ protected function getArraySchemaModel(Schema|Parameter $value): ?string return $items instanceof ReferenceSchema ? $this->normalizeSchemaReference($items->reference) : null; } + /** Whether an optional string example should leave the field unset. */ + public function shouldOmitExample(Schema|Parameter $value, ?bool $required = null): bool + { + $required ??= $value instanceof Parameter ? $value->required : true; + + return !$required + && $this->getSchemaType($value) === self::TYPE_STRING + && !$this->isStringEnum($value) + && \in_array($this->getSchemaExample($value), [null, ''], true); + } + protected function getSchemaExample(Schema|Parameter $value): mixed { $schema = $this->getSchema($value); diff --git a/src/SDK/Language/Dart.php b/src/SDK/Language/Dart.php index 3f1bcdb14a..61487bff1c 100644 --- a/src/SDK/Language/Dart.php +++ b/src/SDK/Language/Dart.php @@ -194,7 +194,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str $type = $this->getSchemaType($param); $example = $this->getSchemaExample($param); - $hasExample = !empty($example) || $example === 0 || $example === false; + $hasExample = $example !== null && $example !== ''; if (!$hasExample) { return match ($type) { @@ -203,7 +203,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str self::TYPE_BOOLEAN => 'false', self::TYPE_FILE => 'InputFile(path: \'./path-to-files/image.jpg\', filename: \'image.jpg\')', self::TYPE_INTEGER, self::TYPE_NUMBER => '0', - self::TYPE_STRING => "''", + self::TYPE_STRING => $this->shouldOmitExample($param) ? 'null' : "''", }; } diff --git a/src/SDK/Language/DotNet.php b/src/SDK/Language/DotNet.php index ccb6f555bb..1b64c12e75 100644 --- a/src/SDK/Language/DotNet.php +++ b/src/SDK/Language/DotNet.php @@ -225,7 +225,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str $output = ''; - if (empty($example) && $example !== 0 && $example !== false) { + if ($example === null || $example === '') { switch ($type) { case self::TYPE_FILE: $output .= 'InputFile.FromPath("./path-to-files/image.jpg")'; @@ -238,7 +238,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str $output .= 'false'; break; case self::TYPE_STRING: - $output .= '""'; + $output .= $this->shouldOmitExample($param) ? 'null' : '""'; break; case self::TYPE_OBJECT: $output .= '[object]'; diff --git a/src/SDK/Language/Go.php b/src/SDK/Language/Go.php index 0b01e65dc5..8ce6fbe13d 100644 --- a/src/SDK/Language/Go.php +++ b/src/SDK/Language/Go.php @@ -255,7 +255,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str $output = ''; - if (empty($example) && $example !== 0 && $example !== false) { + if ($example === null || $example === '') { switch ($type) { case self::TYPE_NUMBER: case self::TYPE_INTEGER: diff --git a/src/SDK/Language/GraphQL.php b/src/SDK/Language/GraphQL.php index d73c03fd0f..613d5dddff 100644 --- a/src/SDK/Language/GraphQL.php +++ b/src/SDK/Language/GraphQL.php @@ -50,7 +50,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str $type = $this->getSchemaType($param); $example = $this->getSchemaExample($param); - $hasExample = !empty($example) || $example === 0 || $example === false; + $hasExample = $example !== null && $example !== ''; if (!$hasExample) { return match ($type) { diff --git a/src/SDK/Language/Kotlin.php b/src/SDK/Language/Kotlin.php index 39179b0cc7..633dca57da 100644 --- a/src/SDK/Language/Kotlin.php +++ b/src/SDK/Language/Kotlin.php @@ -185,7 +185,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = 'kotlin' $output = ''; - if (empty($example) && $example !== 0 && $example !== false) { + if ($example === null || $example === '') { switch ($type) { case self::TYPE_FILE: $output .= 'InputFile.fromPath("file.png")'; @@ -198,7 +198,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = 'kotlin' $output .= 'false'; break; case self::TYPE_STRING: - $output .= "\"\""; + $output .= $this->shouldOmitExample($param) ? 'null' : "\"\""; break; case self::TYPE_OBJECT: $output .= $lang === 'java' diff --git a/src/SDK/Language/PHP.php b/src/SDK/Language/PHP.php index 8ea534c04d..70edfbdb7c 100644 --- a/src/SDK/Language/PHP.php +++ b/src/SDK/Language/PHP.php @@ -390,7 +390,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str $output = ''; - if (empty($example) && $example !== 0 && $example !== false) { + if ($example === null || $example === '') { switch ($type) { case self::TYPE_NUMBER: case self::TYPE_INTEGER: @@ -398,7 +398,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str $output .= 'null'; break; case self::TYPE_STRING: - $output .= "''"; + $output .= $this->shouldOmitExample($param) ? 'null' : "''"; break; case self::TYPE_ARRAY: case self::TYPE_OBJECT: diff --git a/src/SDK/Language/Python.php b/src/SDK/Language/Python.php index 13118ba601..481808b77e 100644 --- a/src/SDK/Language/Python.php +++ b/src/SDK/Language/Python.php @@ -399,7 +399,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str $type = $this->getSchemaType($param); $example = $this->getSchemaExample($param); - $hasExample = !empty($example) || $example === 0 || $example === false; + $hasExample = $example !== null && $example !== ''; if (!$hasExample) { return match ($type) { @@ -407,7 +407,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str self::TYPE_FILE => 'InputFile.from_path(\'file.png\')', self::TYPE_INTEGER, self::TYPE_NUMBER , self::TYPE_BOOLEAN => 'None', self::TYPE_OBJECT => '{}', - self::TYPE_STRING => "''", + self::TYPE_STRING => $this->shouldOmitExample($param) ? 'None' : "''", }; } @@ -546,7 +546,11 @@ protected function getRequestModelInstanceExample(string $modelName, Specificati } $arguments = []; - foreach ($model->properties as $property) { + foreach ($model->properties as $name => $property) { + if ($this->shouldOmitExample($property, \in_array($name, $model->required, true))) { + continue; + } + $arguments[] = $this->getModelFieldName($property, $model->properties) . ' = ' . $this->getRequestModelPropertyExample($property, $spec, $serviceName); } diff --git a/src/SDK/Language/REST.php b/src/SDK/Language/REST.php index 9df156abba..ecb7728048 100644 --- a/src/SDK/Language/REST.php +++ b/src/SDK/Language/REST.php @@ -32,7 +32,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str $type = $this->getSchemaType($param); $example = $this->getSchemaExample($param); - $hasExample = !empty($example) || $example === 0 || $example === false; + $hasExample = $example !== null && $example !== ''; if (!$hasExample) { return match ($type) { diff --git a/src/SDK/Language/Ruby.php b/src/SDK/Language/Ruby.php index d188659cdc..c8d5629855 100644 --- a/src/SDK/Language/Ruby.php +++ b/src/SDK/Language/Ruby.php @@ -255,7 +255,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str $output = ''; - if (empty($example) && $example !== 0 && $example !== false) { + if ($example === null || $example === '') { switch ($type) { case self::TYPE_NUMBER: case self::TYPE_INTEGER: @@ -263,7 +263,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str $output .= 'nil'; break; case self::TYPE_STRING: - $output .= "''"; + $output .= $this->shouldOmitExample($param) ? 'nil' : "''"; break; case self::TYPE_ARRAY: $output .= '[]'; diff --git a/src/SDK/Language/Rust.php b/src/SDK/Language/Rust.php index dd0b942153..45b7803819 100644 --- a/src/SDK/Language/Rust.php +++ b/src/SDK/Language/Rust.php @@ -333,7 +333,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str $output = ""; - if (empty($example) && $example !== 0 && $example !== false) { + if ($example === null || $example === '') { switch ($type) { case self::TYPE_NUMBER: case self::TYPE_INTEGER: @@ -540,6 +540,10 @@ function ($value, $indent = 0): string { new TwigFilter("returnType", fn(Operation $method, Specification $spec, string $namespace, string $generic = "serde_json::Value"): string => $this->getReturnType($method, $spec)), new TwigFilter("caseEnumKey", fn(string $value): string => $this->toPascalCase($value)), new TwigFilter("docsArgumentExample", function (Schema|Parameter $param, string $crateName): string { + if ($this->shouldOmitExample($param)) { + return 'None'; + } + if ($this->getSchemaType($param) === self::TYPE_FILE) { $value = $this->toCaseSnake($param instanceof Parameter ? $param->name : 'file'); if (isset($this->getIdentifierOverrides()[$value])) { diff --git a/src/SDK/Language/Swift.php b/src/SDK/Language/Swift.php index bf8f472d56..050721d496 100644 --- a/src/SDK/Language/Swift.php +++ b/src/SDK/Language/Swift.php @@ -375,7 +375,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str $output = ''; - if (empty($example) && $example !== 0 && $example !== false) { + if ($example === null || $example === '') { switch ($type) { case self::TYPE_FILE: $output .= 'InputFile.fromPath("file.png")'; @@ -388,7 +388,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str $output .= 'false'; break; case self::TYPE_STRING: - $output .= '""'; + $output .= $this->shouldOmitExample($param) ? 'nil' : '""'; break; case self::TYPE_ARRAY: $output .= '[]'; diff --git a/src/SDK/Language/Web.php b/src/SDK/Language/Web.php index cc410a0e91..4a06f94b92 100644 --- a/src/SDK/Language/Web.php +++ b/src/SDK/Language/Web.php @@ -327,7 +327,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str $type = $this->getSchemaType($param); $example = $this->getSchemaExample($param); - $hasExample = !empty($example) || $example === 0 || $example === false; + $hasExample = $example !== null && $example !== ''; if (!$hasExample) { return match ($type) { @@ -335,7 +335,7 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str self::TYPE_FILE => $this->getFileExample(), self::TYPE_INTEGER, self::TYPE_NUMBER, self::TYPE_BOOLEAN => $this->getScalarExampleFallback($param), self::TYPE_OBJECT => '{}', - self::TYPE_STRING => "''", + self::TYPE_STRING => $this->shouldOmitExample($param) ? 'undefined' : "''", }; } diff --git a/src/SDK/SDK.php b/src/SDK/SDK.php index b9b3cb024a..d7676f1a0e 100644 --- a/src/SDK/SDK.php +++ b/src/SDK/SDK.php @@ -150,6 +150,7 @@ public function __construct(protected Language $language, protected Specificatio $this->twig->addFilter(new TwigFilter('caseJson', fn($value) => (is_array($value)) ? json_encode($value) : $value, ['is_safe' => ['html']])); $this->twig->addFilter(new TwigFilter('typeName', fn(Schema|Parameter $value, ?Specification $spec = null): string => $this->language->getTypeName($value, $spec ?? $this->spec), ['is_safe' => ['html']])); $this->twig->addFilter(new TwigFilter('getValidResponseModels', fn(Operation $value): array => $this->getValidResponseModels($value))); + $this->twig->addFilter(new TwigFilter('omitExample', fn(Schema|Parameter $value): bool => $this->language->shouldOmitExample($value))); $this->twig->addFilter(new TwigFilter('paramExample', fn(Schema|Parameter $value): string => $this->language->isOpenStringEnum($value) ? $this->language->getSuggestedEnumExample($value) : $this->language->getParamExample($value), ['is_safe' => ['html']])); diff --git a/templates/go/docs/example.md.twig b/templates/go/docs/example.md.twig index 8bc9a78f4f..25789b7a95 100644 --- a/templates/go/docs/example.md.twig +++ b/templates/go/docs/example.md.twig @@ -1,5 +1,6 @@ +{% set exampleParameters = (method | parameters('all')) | filter(parameter => not (parameter | omitExample)) %} ```go -{% set requireFilesPkg = (method | parameters('all')) | filter(parameter => (parameter | schemaType) == 'file') | length > 0 -%} +{% set requireFilesPkg = exampleParameters | filter(parameter => (parameter | schemaType) == 'file') | length > 0 -%} {%- set packagePath = sdk | goPackagePath -%} {%- set imports = [packagePath ~ '/appwrite', packagePath ~ '/' ~ (service.name | caseLower)] -%} {%- if requireFilesPkg -%} @@ -29,11 +30,11 @@ func main() { service := {{ service.name | caseLower }}.New(client) -{% if (method | parameters('all')) | length == 0 %} +{% if exampleParameters | length == 0 %} response, err := service.{{ (method | methodName) | caseUcfirst }}() {% else %} response, err := service.{{ (method | methodName) | caseUcfirst }}( -{% for parameter in (method | parameters('all')) %} +{% for parameter in exampleParameters %} {% if parameter.required %} {{ parameter | paramExample }}, {% else %} diff --git a/templates/graphql/docs/example.md.twig b/templates/graphql/docs/example.md.twig index cb75ad3751..e5d4b8b80f 100644 --- a/templates/graphql/docs/example.md.twig +++ b/templates/graphql/docs/example.md.twig @@ -1,3 +1,4 @@ +{% set exampleParameters = (method | parameters('all')) | filter(parameter => not (parameter | omitExample)) %} ```graphql {% macro getProperty(definitions, responseModel, depth) %} {%~ for definitionName, definition in definitions %} @@ -36,16 +37,16 @@ Content-Length: *Length of your entity body in bytes* --{{boundary}} Content-Disposition: form-data; name="operations" -{ "query": "mutation { {{ service.name | caseCamel }}{{ (method | methodName) | caseCamel | caseUcfirst }}({% for parameter in (method | parameters('all')) %}{{ parameter.name | caseCamel }}: ${{ parameter.name | caseCamel }}{% if not loop.last %}, {% endif %}{% endfor %}) { id }" }, "variables": { {% for parameter in (method | parameters('all')) %}"{{ parameter.name | caseCamel }}": {{ parameter | paramExample }}{% if not loop.last %}, {% endif %}{% endfor %} } } +{ "query": "mutation { {{ service.name | caseCamel }}{{ (method | methodName) | caseCamel | caseUcfirst }}({% for parameter in exampleParameters %}{{ parameter.name | caseCamel }}: ${{ parameter.name | caseCamel }}{% if not loop.last %}, {% endif %}{% endfor %}) { id }" }, "variables": { {% for parameter in exampleParameters %}"{{ parameter.name | caseCamel }}": {{ parameter | paramExample }}{% if not loop.last %}, {% endif %}{% endfor %} } } --{{boundary}} Content-Disposition: form-data; name="map" {% set counter = 0 %} -{ {% for parameter in (method | parameters('all')) %}{% if (parameter | schemaType) == 'file' %}"{{ counter }}": ["variables.{{ parameter.name | caseCamel }}"]{% if not loop.last %}, {% endif %}{% set counter = counter + 1 %}{% endif %}{% endfor %} } +{ {% for parameter in exampleParameters %}{% if (parameter | schemaType) == 'file' %}"{{ counter }}": ["variables.{{ parameter.name | caseCamel }}"]{% if not loop.last %}, {% endif %}{% set counter = counter + 1 %}{% endif %}{% endfor %} } {% set counter = 0 %} -{% for parameter in (method | parameters('all')) %} +{% for parameter in exampleParameters %} {% if (parameter | schemaType) == 'file' %} --{{ boundary }} Content-Disposition: form-data; name="{{ counter }}"; filename="{{ parameter.name }}.ext" @@ -62,8 +63,8 @@ query { {%~ else %} mutation { {%~ endif %} - {{ service.name | caseCamel }}{{ (method | methodName) | caseCamel | caseUcfirst }}{% if (method | parameters('all')) | length == 0 %} {{ '{' }}{% else %}( - {%~ for parameter in (method | parameters('all')) %} + {{ service.name | caseCamel }}{{ (method | methodName) | caseCamel | caseUcfirst }}{% if exampleParameters | length == 0 %} {{ '{' }}{% else %}( + {%~ for parameter in exampleParameters %} {{ parameter.name | caseCamel }}: {{ parameter | paramExample }}{% if not loop.last %},{% endif %} {%~ endfor %} diff --git a/templates/rest/docs/example.md.twig b/templates/rest/docs/example.md.twig index 911a56d4b9..02b06dd915 100644 --- a/templates/rest/docs/example.md.twig +++ b/templates/rest/docs/example.md.twig @@ -16,7 +16,7 @@ Host: {{ ((spec | endpoint) | split('/'))[2] }} {% if (method | parameters('body')) %} { -{% for parameter in (method | parameters('body')) %} +{% for parameter in ((method | parameters('body')) | filter(parameter => not (parameter | omitExample))) %} "{{ parameter.name }}": {{ parameter | paramExample }}{% if not loop.last %},{% endif ~%} {% endfor %} } @@ -25,7 +25,7 @@ Host: {{ ((spec | endpoint) | split('/'))[2] }} {% if (key | caseLower) == 'content-type' and header == 'multipart/form-data' %} Content-Length: *Length of your entity body in bytes* -{% for parameter in (method | parameters('body')) %} +{% for parameter in ((method | parameters('body')) | filter(parameter => not (parameter | omitExample))) %} --{{boundary}} Content-Disposition: form-data; name="{{parameter.name}}{% if (parameter | schemaType) == "array" %}[]{% endif %}" diff --git a/tests/generation/GenerationTest.php b/tests/generation/GenerationTest.php index 3fa910e2e5..025e266d4f 100644 --- a/tests/generation/GenerationTest.php +++ b/tests/generation/GenerationTest.php @@ -244,6 +244,17 @@ private function generate(string $name, string $platform, string $unionVariant = $this->removeDirectory($dir); $document = \json_decode((string) \file_get_contents(self::FIXTURE), flags: JSON_THROW_ON_ERROR); + if ($variant === '-optional-examples') { + $properties = $document->components->schemas->player->properties; + $properties->zznestedmissing = (object) ['type' => 'string']; + $properties->zznestedempty = (object) ['type' => 'string', 'example' => '']; + $properties->zznestedzero = (object) ['type' => 'string', 'example' => '0']; + $properties->zznestedrequired = (object) ['type' => 'string']; + $document->components->schemas->player->required[] = 'zznestedrequired'; + $body = $document->paths->{'/mock/tests/general/zzexamples'}->post->requestBody->content->{'application/json'}->schema; + $body->required = []; + $body->properties = (object) ['zzmissing' => (object) ['type' => 'string']]; + } if ($unionVariant !== '') { $union = $document->paths->{'/mock/tests/compound'}->get->responses->{'200'}->content->{'application/json'}->schema; foreach ($union->anyOf as $branch) { @@ -646,6 +657,85 @@ public function testJavascriptDefaultsAndExamplesKeepTheirTypes(): void $this->assertNotEmpty($examples); } + /** + * Missing optional strings must stay unset; explicit examples, including + * the string "0", numeric zero and false, must remain usable arguments. + */ + public function testOptionalStringExamplesStayUnset(): void + { + $examples = [ + ['web', 'client', '', 'zzmissing: undefined', "zzzero: '0'", 'zzcount: 0', 'zzenabled: false'], + ['node', 'server', '', 'zzmissing: undefined', "zzzero: '0'", 'zzcount: 0', 'zzenabled: false'], + ['react-native', 'client', '', 'zzmissing: undefined', "zzzero: '0'", 'zzcount: 0', 'zzenabled: false'], + ['dart', 'server', '', 'zzmissing: null', "zzzero: '0'", 'zzcount: 0', 'zzenabled: false'], + ['flutter', 'client', '', 'zzmissing: null', "zzzero: '0'", 'zzcount: 0', 'zzenabled: false'], + ['php', 'server', '', 'zzmissing: null', "zzzero: '0'", 'zzcount: 0', 'zzenabled: false'], + ['python', 'server', '', 'zzmissing = none', "zzzero = '0'", 'zzcount = 0', 'zzenabled = false'], + ['ruby', 'server', '', 'zzmissing: nil', "zzzero: '0'", 'zzcount: 0', 'zzenabled: false'], + ['dotnet', 'server', '', 'zzmissing: null', 'zzzero: "0"', 'zzcount: 0', 'zzenabled: false'], + ['unity', 'client', '', 'zzmissing: null', 'zzzero: "0"', 'zzcount: 0', 'zzenabled: false'], + ['swift', 'server', '', 'zzmissing: nil', 'zzzero: "0"', 'zzcount: 0', 'zzenabled: false'], + ['apple', 'client', '', 'zzmissing: nil', 'zzzero: "0"', 'zzcount: 0', 'zzenabled: false'], + ['kotlin', 'server', 'kotlin/', 'zzmissing = null', 'zzzero = "0"', 'zzcount = 0', 'zzenabled = false'], + ['android', 'client', 'kotlin/', 'zzmissing = null', 'zzzero = "0"', 'zzcount = 0', 'zzenabled = false'], + ['kotlin', 'server', 'java/', 'null, // zzmissing', '"0", // zzzero', '0l, // zzcount', 'false, // zzenabled'], + ['android', 'client', 'java/', 'null, // zzmissing', '"0", // zzzero', '0l, // zzcount', 'false, // zzenabled'], + ['go', 'server', '', null, 'withzzexampleszzzero("0")', 'withzzexampleszzcount(0)', 'withzzexampleszzenabled(false)'], + ['rust', 'server', '', 'none, // optional', 'some("0")', 'some(0)', 'some(false)'], + ['rest', 'client', '', null, '"zzzero": "0"', '"zzcount": 0', '"zzenabled": false'], + ['graphql', 'client', '', null, 'zzzero: "0"', 'zzcount: 0', 'zzenabled: false'], + ]; + + foreach ($examples as [$name, $platform, $directory, $missing, $zero, $count, $enabled]) { + $files = $this->generate($name, $platform); + $root = $name === 'unity' ? 'docs~' : 'docs'; + $path = "{$root}/examples/{$directory}general/zzexamples.md"; + $this->assertArrayHasKey($path, $files, $name); + $example = $files[$path]; + foreach ([$zero, $count, $enabled, 'required-value', 'kept-value'] as $value) { + $this->assertStringContainsString($value, $example, $name); + } + if ($missing !== null) { + if ($name === 'rust') { + $this->assertSame(2, \substr_count($example, $missing)); + } + $this->assertStringContainsString($missing, $example, $name); + $this->assertStringContainsString(\str_replace('zzmissing', 'zzempty', $missing), $example, $name); + } else { + $this->assertStringNotContainsString('zzmissing', $example, $name); + $this->assertStringNotContainsString('zzempty', $example, $name); + } + $this->assertDoesNotMatchRegularExpression('/(?:zzmissing|zzempty)[^\n]*[\'\"][\'\"]/', $example, $name); + } + + $rest = $this->generate('rest', 'client')['docs/examples/general/zzmultipart.md']; + $this->assertStringNotContainsString('name="zzmissing"', $rest); + $this->assertStringNotContainsString('name="zzempty"', $rest); + $this->assertStringContainsString('name="zzvalue"', $rest); + + $cli = $this->generate('cli', 'console')['docs/examples/general/zzexamples.md']; + $this->assertStringContainsString('required-value', $cli); + $this->assertStringNotContainsString('--zzmissing', $cli); + $this->assertStringNotContainsString('--zzempty', $cli); + } + + public function testOptionalExamplesPreserveRequiredBodiesAndNestedModels(): void + { + $python = $this->generate('python', 'server', variant: '-optional-examples'); + foreach (['create-player', 'create-players'] as $method) { + $example = $python["docs/examples/general/{$method}.md"]; + $this->assertStringNotContainsString('zznestedmissing', $example); + $this->assertStringNotContainsString('zznestedempty', $example); + $this->assertStringContainsString("zznestedzero = '0'", $example); + $this->assertStringContainsString("id = 'player123'", $example); + $this->assertStringContainsString("zznestedrequired = ''", $example); + } + + $rest = $this->generate('rest', 'client', variant: '-optional-examples')['docs/examples/general/zzexamples.md']; + $this->assertStringNotContainsString('zzmissing', $rest); + $this->assertMatchesRegularExpression('/\{\s*\}\s*```/', $rest); + } + /** * A canonical document keys `x-appwrite.auth` by platform. The fixture's * platform-auth operation lists `Project` for client and `Project, Key` for diff --git a/tests/resources/spec-openapi3.json b/tests/resources/spec-openapi3.json index 789c22ba60..3fe142c8a5 100644 --- a/tests/resources/spec-openapi3.json +++ b/tests/resources/spec-openapi3.json @@ -24,6 +24,171 @@ } ], "paths": { + "/mock/tests/general/zzexamples": { + "post": { + "summary": "Optional string example fixture", + "description": "Exercise optional example omission and explicit values.", + "operationId": "generalZzexamples", + "tags": [ + "general" + ], + "x-appwrite": { + "platforms": [ + "client", + "server", + "console" + ] + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "zzrequired" + ], + "properties": { + "zzrequired": { + "description": "Example fixture value.", + "type": "string", + "example": "required-value" + }, + "zzmissing": { + "description": "Example fixture value.", + "type": "string", + "minLength": 1 + }, + "zzempty": { + "description": "Example fixture value.", + "type": "string", + "minLength": 1, + "example": "" + }, + "zzzero": { + "description": "Example fixture value.", + "type": "string", + "example": "0" + }, + "zzvalue": { + "description": "Example fixture value.", + "type": "string", + "example": "kept-value" + }, + "zzcount": { + "description": "Example fixture value.", + "type": "integer", + "example": 0 + }, + "zzenabled": { + "description": "Example fixture value.", + "type": "boolean", + "example": false + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Any", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/any" + } + } + } + } + } + } + }, + "/mock/tests/general/zzmultipart": { + "post": { + "summary": "Optional string example fixture", + "description": "Exercise optional example omission and explicit values.", + "operationId": "generalZzmultipart", + "tags": [ + "general" + ], + "x-appwrite": { + "platforms": [ + "client", + "server", + "console" + ] + }, + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "zzrequired" + ], + "properties": { + "zzfile": { + "type": "string", + "format": "binary", + "description": "File example." + }, + "zzrequired": { + "description": "Example fixture value.", + "type": "string", + "example": "required-value" + }, + "zzmissing": { + "description": "Example fixture value.", + "type": "string", + "minLength": 1 + }, + "zzempty": { + "description": "Example fixture value.", + "type": "string", + "minLength": 1, + "example": "" + }, + "zzzero": { + "description": "Example fixture value.", + "type": "string", + "example": "0" + }, + "zzvalue": { + "description": "Example fixture value.", + "type": "string", + "example": "kept-value" + }, + "zzcount": { + "description": "Example fixture value.", + "type": "integer", + "example": 0 + }, + "zzenabled": { + "description": "Example fixture value.", + "type": "boolean", + "example": false + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Any", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/any" + } + } + } + } + } + } + }, "\/ping": { "get": { "summary": "Ping",