diff --git a/example.php b/example.php index fe52a6a19..8ac62af87 100644 --- a/example.php +++ b/example.php @@ -21,6 +21,7 @@ use Appwrite\SDK\Language\Android; use Appwrite\SDK\Language\Kotlin; use Appwrite\SDK\Language\ReactNative; +use Appwrite\SDK\Language\Docs; try { @@ -266,6 +267,13 @@ function configureSDK($sdk, $overrides = []) { configureSDK($sdk); $sdk->generate(__DIR__ . '/examples/graphql'); } + + // Docs + if (!$requestedSdk || $requestedSdk === 'docs') { + $sdk = new SDK(new Docs(), new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/docs'); + } } catch (Exception $exception) { echo 'Error: ' . $exception->getMessage() . ' on ' . $exception->getFile() . ':' . $exception->getLine() . "\n"; diff --git a/src/SDK/Language/Docs.php b/src/SDK/Language/Docs.php new file mode 100644 index 000000000..3ce303197 --- /dev/null +++ b/src/SDK/Language/Docs.php @@ -0,0 +1,208 @@ + 'method', + 'destination' => 'typescript/{{ service.name | caseLower }}/{{ method.name | caseKebab }}.md', + 'template' => 'docs/typescript/method.md.twig', + ] + ]; + } + + /** + * @param array $parameter + * @return string + */ + public function getTypeName(array $parameter, array $spec = []): string + { + // For TypeScript/JavaScript-like languages + if (isset($parameter['enumName'])) { + return \ucfirst($parameter['enumName']); + } + if (!empty($parameter['enumValues'])) { + return \ucfirst($parameter['name']); + } + if (isset($parameter['items'])) { + $parameter['array'] = $parameter['items']; + } + switch ($parameter['type']) { + case self::TYPE_INTEGER: + case self::TYPE_NUMBER: + return 'number'; + case self::TYPE_ARRAY: + if (!empty(($parameter['array'] ?? [])['type']) && !\is_array($parameter['array']['type'])) { + return $this->getTypeName($parameter['array']) . '[]'; + } + return 'any[]'; + case self::TYPE_FILE: + return 'File'; + case self::TYPE_OBJECT: + return 'object'; + } + return $parameter['type']; + } + + /** + * @param array $param + * @return string + */ + public function getParamDefault(array $param): string + { + $type = $param['type'] ?? ''; + $default = $param['default'] ?? ''; + $required = $param['required'] ?? false; + + if ($required) { + return ''; + } + + if (!empty($default)) { + return ' = ' . $default; + } + + return match ($type) { + self::TYPE_ARRAY => ' = []', + self::TYPE_OBJECT => ' = {}', + default => ' = null', + }; + } + + /** + * @param array $param + * @param string $lang + * @return string + */ + public function getParamExample(array $param, string $lang = ''): string + { + $type = $param['type'] ?? ''; + $example = $param['example'] ?? ''; + + $hasExample = !empty($example) || $example === 0 || $example === false; + + if (!$hasExample) { + return match ($type) { + self::TYPE_ARRAY => '[]', + self::TYPE_FILE => 'file', + self::TYPE_INTEGER, self::TYPE_NUMBER => '0', + self::TYPE_BOOLEAN => 'false', + self::TYPE_OBJECT => '{}', + self::TYPE_STRING => "''", + }; + } + + return match ($type) { + self::TYPE_ARRAY => $this->isPermissionString($example) ? $this->getPermissionExample($example) : $example, + self::TYPE_INTEGER, self::TYPE_NUMBER => (string)$example, + self::TYPE_FILE => 'file', + self::TYPE_BOOLEAN => ($example) ? 'true' : 'false', + self::TYPE_OBJECT => ($example === '{}') + ? '{}' + : (($formatted = json_encode(json_decode($example, true), JSON_PRETTY_PRINT)) + ? $formatted + : $example), + self::TYPE_STRING => "'{$example}'", + }; + } + + public function getPermissionExample(string $example): string + { + $permissions = $this->extractPermissionParts($example); + $result = []; + + foreach ($permissions as $permission) { + $action = ucfirst($permission['action']); + $role = ucfirst($this->toCamelCase($permission['role'])); + + if ($permission['id'] !== null) { + if ($permission['innerRole'] !== null) { + $result[] = "Permission.{$action}(Role.{$role}('{$permission['id']}', '{$permission['innerRole']}'))"; + } else { + $result[] = "Permission.{$action}(Role.{$role}('{$permission['id']}'))"; + } + } else { + $result[] = "Permission.{$action}(Role.{$role}())"; + } + } + + return '[' . implode(', ', $result) . ']'; + } + + public function getFilters(): array + { + return [ + new TwigFilter('getPropertyType', function ($value, $method = []) { + return $this->getTypeName($value, $method); + }), + new TwigFilter('comment', function ($value) { + $value = explode("\n", $value); + foreach ($value as $key => $line) { + $value[$key] = wordwrap($line, 80, "\n"); + } + return implode("\n", $value); + }, ['is_safe' => ['html']]), + new TwigFilter('caseEnumKey', function (string $value) { + return $this->toPascalCase($value); + }), + new TwigFilter('getResponseModel', function (array $method) { + if (!empty($method['responseModel']) && $method['responseModel'] !== 'any') { + return 'Models.' . \ucfirst($method['responseModel']); + } + return null; + }), + ]; + } +} diff --git a/templates/docs/typescript/method.md.twig b/templates/docs/typescript/method.md.twig new file mode 100644 index 000000000..9a0ef29d9 --- /dev/null +++ b/templates/docs/typescript/method.md.twig @@ -0,0 +1,82 @@ +# {{ method.name | caseCamel }} +{% if method.deprecated %} + +⚠️ **DEPRECATED**{% if method.since %} since {{ method.since }}{% endif %}{% if method.replaceWith %} - Use `{{ method.replaceWith }}` instead{% endif %} + +{% endif %} + +Description: {{ method.description | raw | trim }} + +## Parameters +{% if method.parameters.all | length > 0 %} + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +{% for parameter in method.parameters.all -%} +| `{{ parameter.name | caseCamel }}` | `{{ parameter | getPropertyType }}` | {% if parameter.required %}✅{% else %}❌{% endif %} | {{ parameter.description | raw }}{% if parameter.default %} (Default: `{{ parameter.default }}`){% endif %}{% if parameter.enumValues | length > 0 %}
**Allowed:** {% for value in parameter.enumValues %}`{{ value }}`{% if not loop.last %}, {% endif %}{% endfor %}{% endif %} | +{% endfor %} +{%- else %} + +This method does not accept any parameters. +{% endif %} + +## Usage + +```typescript +import { Client, {{ service.name | caseUcfirst }}{% for parameter in method.parameters.all %}{% if parameter.enumValues | length > 0 %}, {{ parameter.enumName | caseUcfirst }}{% endif %}{% endfor %}{% if method | getResponseModel %}, Models{% endif %} } from '{{ spec.title | caseLower }}'; + +const client = new Client() + .setEndpoint('{{ spec.endpointDocs | raw }}') +{%- if method.auth|length > 0 %} +{% for node in method.auth -%} +{% for key,header in node|keys -%} + .set{{header}}('{{ node[header]['x-appwrite']['demo'] | raw }}') +{%- endfor %} +{%- endfor -%} +{% endif -%} +; + +const {{ service.name | caseCamel }} = new {{ service.name | caseUcfirst }}(client); +{% if method.type == 'location' %} +const result{% if method | getResponseModel %}: {{ method | getResponseModel }}{% endif %} = {{ service.name | caseCamel }}.{{ method.name | caseCamel }}({% if method.parameters.all | length == 0 %}); +{% else -%} +{ +{% for parameter in method.parameters.all %} + {{ parameter.name | caseCamel }}: {% if parameter.enumValues | length > 0 %}{{ parameter.enumName | caseUcfirst }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample }}{% endif %}, +{% endfor -%} +}); +{% endif -%} +{% elseif method.type != 'webAuth' %} +const result{% if method | getResponseModel %}: {{ method | getResponseModel }}{% endif %} = await {{ service.name | caseCamel }}.{{ method.name | caseCamel }}({% if method.parameters.all | length == 0 %}); +{% else -%} +{ +{% for parameter in method.parameters.all %} + {{ parameter.name | caseCamel }}: {% if parameter.enumValues | length > 0 %}{{ parameter.enumName | caseUcfirst }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample }}{% endif %}, +{% endfor -%} +}); +{% endif -%} +{% else -%} +{{ service.name | caseCamel }}.{{ method.name | caseCamel }}({% if method.parameters.all | length == 0 %}); +{% else -%} +{ +{% for parameter in method.parameters.all %} + {{ parameter.name | caseCamel }}: {% if parameter.enumValues | length > 0 %}{{ parameter.enumName | caseUcfirst }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample }}{% endif %}, +{% endfor -%} +}); +{% endif -%} +{% endif -%} +``` +{% if method | getResponseModel %} + +## Response Model + +Returns a `{{ method | getResponseModel }}` object with the following properties: +{% if method.responseModel and spec.definitions[method.responseModel] %} + +| Property | Type | Description | +|----------|------|-------------| +{% for property in spec.definitions[method.responseModel].properties -%} +| `{{ property.name | caseCamel }}` | `{{ property | getPropertyType }}` | {{ property.description | raw }} | +{% endfor %} +{%- endif %} +{% endif %}