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
8 changes: 8 additions & 0 deletions example.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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";
Expand Down
208 changes: 208 additions & 0 deletions src/SDK/Language/Docs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
<?php

namespace Appwrite\SDK\Language;

use Appwrite\SDK\Language;
use Twig\TwigFilter;

class Docs extends Language
{
protected $params = [];

/**
* @return string
*/
public function getName(): string
{
return 'Docs';
}

/**
* @return array
*/
public function getKeywords(): array
{
// Docs don't need keyword escaping
return [];
}

/**
* @return array
*/
public function getIdentifierOverrides(): array
{
return [];
}

public function getStaticAccessOperator(): string
{
return '.';
}

public function getStringQuote(): string
{
return "'";
}

public function getArrayOf(string $elements): string
{
return '[' . $elements . ']';
}

/**
* @return array
*/
public function getFiles(): array
{
return [
[
'scope' => '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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Find the Docs.php file
find . -name "Docs.php" -type f 2>/dev/null | head -20

# Also look for Language class
find . -name "Language.php" -type f 2>/dev/null | head -20

# List the SDK directory structure if it exists
if [ -d "src/SDK" ]; then
  find src/SDK -type f -name "*.php" | head -30
fi

Repository: appwrite/sdk-generator

Length of output: 716


🏁 Script executed:

#!/bin/bash

# Read the Docs.php file to check the specific lines mentioned in the review
echo "=== DOCS.PHP (lines 50-200) ==="
head -200 src/SDK/Language/Docs.php | tail -150

echo -e "\n=== LANGUAGE.PHP (to see parent class definition) ==="
# Look for method definitions in Language.php
grep -n "public function getTypeName\|public function getParamExample" src/SDK/Language.php

# Check if they're abstract methods
grep -n "abstract.*function\|interface" src/SDK/Language.php | head -20

Repository: appwrite/sdk-generator

Length of output: 5968


Add default cases to match expressions to prevent UnhandledMatchError.

Both match expressions in getParamExample() (lines ~145-150 and ~152-163) lack default cases. If $type contains an unexpected value, an UnhandledMatchError is thrown. Add default => $example, or an appropriate fallback to handle unknown types. The unused parameters $spec and $lang are required by the parent abstract method signatures and should be kept.

🧰 Tools
🪛 PHPMD (2.15.0)

75-75: Avoid unused parameters such as '$spec'. (undefined)

(UnusedFormalParameter)

🤖 Prompt for AI Agents
In src/SDK/Language/Docs.php around lines 75 (and specifically fix the match
expressions in getParamExample around lines ~145-150 and ~152-163), the two
match expressions lack default cases which can throw UnhandledMatchError for
unexpected $type values; update each match to include a fallback arm such as
default => $example, (or another appropriate fallback) to safely handle unknown
types while keeping the unused $spec and $lang parameters intact as required by
the parent signatures.

{
// 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 => "''",
};
Comment on lines +137 to +144
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing default case in match expression.

If $type is an unexpected value (e.g., a custom type not handled), this match expression will throw an UnhandledMatchError. Consider adding a default fallback.

         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 => "''",
+            default => "''",
         };
🤖 Prompt for AI Agents
In src/SDK/Language/Docs.php around lines 142 to 149, the match expression lacks
a default arm so unexpected $type values can raise an UnhandledMatchError; add a
fallback arm (e.g., default => 'null' or another appropriate example string) to
the match to return a safe default value for unrecognized types.

}

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;
}),
];
}
}
82 changes: 82 additions & 0 deletions templates/docs/typescript/method.md.twig
Original file line number Diff line number Diff line change
@@ -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 %}<br>**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 %}
Loading