Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
90a1606
feat: add enum support to response models in ts
ChiragAgg5k Sep 22, 2025
cec90d7
proper enum support
ChiragAgg5k Sep 23, 2025
7ee11c9
update react native
ChiragAgg5k Sep 23, 2025
01e3a97
update to use enums in android and kotlin
ChiragAgg5k Sep 23, 2025
d413ad8
fix: max length issue
ChiragAgg5k Sep 23, 2025
85db6f4
add swift support
ChiragAgg5k Sep 25, 2025
66adb5a
fix: dart
ChiragAgg5k Sep 25, 2025
ee724c1
fix: tests for dart
ChiragAgg5k Sep 25, 2025
8d781b0
fix: import
ChiragAgg5k Sep 25, 2025
d007352
fix: enum in apple
ChiragAgg5k Sep 25, 2025
8c5cf9f
fix: android
ChiragAgg5k Sep 25, 2025
62827f4
composer
ChiragAgg5k Sep 25, 2025
8bc6991
remove codeable
ChiragAgg5k Sep 25, 2025
9223f82
enum support for csharp
ChiragAgg5k Sep 25, 2025
c21260c
remove deno
ChiragAgg5k Sep 25, 2025
830aeb4
remove import
ChiragAgg5k Sep 25, 2025
09cbde1
fix: swift
ChiragAgg5k Sep 25, 2025
0a1d5e3
fix: format
ChiragAgg5k Sep 25, 2025
5341746
fix: reviews
ChiragAgg5k Sep 25, 2025
783144b
fix: imports
ChiragAgg5k Sep 26, 2025
ff36fa1
add mock status
ChiragAgg5k Sep 26, 2025
c7344e9
use requestenums name
ChiragAgg5k Sep 26, 2025
ca726d5
merge enums
ChiragAgg5k Sep 26, 2025
27e484a
fix: optional handling in swift
ChiragAgg5k Sep 26, 2025
f9ad8d8
fix: optional handling in dart
ChiragAgg5k Sep 26, 2025
0417b7b
fix: python, remove deno
ChiragAgg5k Sep 26, 2025
54cd185
fix: node
ChiragAgg5k Sep 26, 2025
80b76ff
Trigger Build
ChiragAgg5k Sep 26, 2025
c5884fb
Merge branch 'master' into feat-enum-support
ChiragAgg5k Sep 26, 2025
81e960a
add model support to ruby
ChiragAgg5k Sep 29, 2025
035ee8d
only validate if its not nil
ChiragAgg5k Sep 29, 2025
34e8577
trigger ci
ChiragAgg5k Sep 29, 2025
8b5b334
trigger ci
ChiragAgg5k Sep 29, 2025
be203d7
fix: optional issue with enums in android
ChiragAgg5k Sep 30, 2025
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: 0 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ jobs:
CLINode18,
DartBeta,
DartStable,
Deno1193,
Deno1303,
DotNet60,
DotNet80,
DotNet90,
Expand Down
25 changes: 0 additions & 25 deletions example.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
use Appwrite\SDK\Language\Ruby;
use Appwrite\SDK\Language\Dart;
use Appwrite\SDK\Language\Go;
use Appwrite\SDK\Language\Deno;
use Appwrite\SDK\Language\REST;
use Appwrite\SDK\Language\Swift;
use Appwrite\SDK\Language\Apple;
Expand Down Expand Up @@ -101,30 +100,6 @@ function getSSLPage($url) {

$sdk->generate(__DIR__ . '/examples/web');

// Deno
$sdk = new SDK(new Deno(), new Swagger2($spec));

$sdk
->setName('NAME')
->setDescription('Repo description goes here')
->setShortDescription('Repo short description goes here')
->setVersion('0.0.0')
->setURL('https://example.com')
->setLogo('https://appwrite.io/v1/images/console.png')
->setLicenseContent('test test test')
->setWarning('**WORK IN PROGRESS - NOT READY FOR USAGE**')
->setChangelog('**CHANGELOG**')
->setGitUserName('repoowner')
->setGitRepoName('reponame')
->setTwitter('appwrite_io')
->setDiscord('564160730845151244', 'https://appwrite.io/discord')
->setDefaultHeaders([
'X-Appwrite-Response-Format' => '1.6.0',
])
;

$sdk->generate(__DIR__ . '/examples/deno');

// Node
$sdk = new SDK(new Node(), new Swagger2($spec));

Expand Down
42 changes: 42 additions & 0 deletions src/SDK/Language/DotNet.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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

class DotNet extends Language
{
Expand Down Expand Up @@ -465,6 +466,47 @@ public function getFilters(): array
];
}

/**
* get sub_scheme and property_name functions
* @return TwigFunction[]
*/
public function getFunctions(): array
{
return [
new TwigFunction('sub_schema', function (array $property) {
$result = '';

if (isset($property['sub_schema']) && !empty($property['sub_schema'])) {
if ($property['type'] === 'array') {
$result = 'List<' . \ucfirst($property['sub_schema']) . '>';
} else {
$result = \ucfirst($property['sub_schema']);
}
} elseif (isset($property['enum']) && !empty($property['enum'])) {
$enumName = $property['enumName'] ?? $property['name'];
$result = \ucfirst($enumName);
} else {
$result = $this->getTypeName($property);
}

if (!($property['required'] ?? true)) {
$result .= '?';
}

return $result;
}),
new TwigFunction('property_name', function (array $definition, array $property) {
$name = $property['name'];
$name = \ucfirst($name);
$name = \str_replace('$', '', $name);
if (\in_array(\strtolower($name), $this->getKeywords())) {
$name = '@' . $name;
}
return $name;
}),
];
}

/**
* Format a PHP array as a C# anonymous object
*/
Expand Down
73 changes: 73 additions & 0 deletions src/SDK/Language/Kotlin.php
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,9 @@ public function getFilters(): array
}
return $this->toUpperSnakeCase($value);
}),
new TwigFilter('propertyAssignment', function (array $property, array $spec) {
return $this->getPropertyAssignment($property, $spec);
}),
];
}

Expand Down Expand Up @@ -518,6 +521,9 @@ protected function getPropertyType(array $property, array $spec, string $generic
if ($property['type'] === 'array') {
$type = 'List<' . $type . '>';
}
} elseif (isset($property['enum'])) {
$enumName = $property['enumName'] ?? $property['name'];
$type = \ucfirst($enumName);
} else {
$type = $this->getTypeName($property);
}
Expand Down Expand Up @@ -551,4 +557,71 @@ protected function hasGenericType(?string $model, array $spec): string

return false;
}

/**
* Generate property assignment logic for model deserialization
*
* @param array $property
* @param array $spec
* @return string
*/
protected function getPropertyAssignment(array $property, array $spec): string
Comment on lines +561 to +568
Copy link
Member

Choose a reason for hiding this comment

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

I really like this shift of the complex templating logic into PHP, can we do the same for the other languages changed so far? To keep consistency, and improve readability of their templates too

{
$propertyName = $property['name'];
$escapedPropertyName = str_replace('$', '\$', $propertyName);
$mapKey = "map[\"$escapedPropertyName\"]";

// Handle sub-schema (nested objects)
if (isset($property['sub_schema']) && !empty($property['sub_schema'])) {
$subSchemaClass = $this->toPascalCase($property['sub_schema']);
$hasGenericType = $this->hasGenericType($property['sub_schema'], $spec);
$nestedTypeParam = $hasGenericType ? ', nestedType' : '';

if ($property['type'] === 'array') {
return "($mapKey as List<Map<String, Any>>).map { " .
"$subSchemaClass.from(map = it$nestedTypeParam) }";
} else {
return "$subSchemaClass.from(" .
"map = $mapKey as Map<String, Any>$nestedTypeParam" .
")";
}
}

// Handle enum properties
if (isset($property['enum']) && !empty($property['enum'])) {
$enumName = $property['enumName'] ?? $property['name'];
$enumClass = $this->toPascalCase($enumName);
$nullCheck = $property['required'] ? '!!' : ' ?: null';

if ($property['required']) {
return "$enumClass.values().find { " .
"it.value == $mapKey as String " .
"}$nullCheck";
}

return "$enumClass.values().find { " .
"it.value == ($mapKey as? String) " .
"}$nullCheck";
}

// Handle primitive types
$nullableModifier = $property['required'] ? '' : '?';

if ($property['type'] === 'integer') {
return "($mapKey as$nullableModifier Number)" .
($nullableModifier ? '?' : '') . '.toLong()';
}

if ($property['type'] === 'number') {
return "($mapKey as$nullableModifier Number)" .
($nullableModifier ? '?' : '') . '.toDouble()';
}

// Handle other types (string, boolean, etc.)
$kotlinType = $this->getPropertyType($property, $spec);
// Remove nullable modifier from type since we handle it in the cast
$kotlinType = str_replace('?', '', $kotlinType);

return "$mapKey as$nullableModifier $kotlinType";
}
}
2 changes: 1 addition & 1 deletion src/SDK/Language/Swift.php
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ protected function getPropertyType(array $property, array $spec, string $generic
$type = '[' . $type . ']';
}
} else {
$type = $this->getTypeName($property, isProperty: true);
$type = $this->getTypeName($property, $spec, true);
}

return $type;
Expand Down
14 changes: 11 additions & 3 deletions src/SDK/Language/Web.php
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ public function getReturn(array $method, array $spec): string
return 'Promise<{}>';
}

public function getSubSchema(array $property, array $spec): string
public function getSubSchema(array $property, array $spec, string $methodName = ''): string
{
if (array_key_exists('sub_schema', $property)) {
$ret = '';
Expand All @@ -336,6 +336,14 @@ public function getSubSchema(array $property, array $spec): string
return $ret;
}

if (array_key_exists('enum', $property) && !empty($methodName)) {
if (isset($property['enumName'])) {
return $this->toPascalCase($property['enumName']);
}

return $this->toPascalCase($methodName) . $this->toPascalCase($property['name']);
}

return $this->getTypeName($property);
}

Expand All @@ -348,8 +356,8 @@ public function getFilters(): array
new TwigFilter('getReadOnlyProperties', function ($value, $responseModel, $spec = []) {
return $this->getReadOnlyProperties($value, $responseModel, $spec);
}),
new TwigFilter('getSubSchema', function (array $property, array $spec) {
return $this->getSubSchema($property, $spec);
new TwigFilter('getSubSchema', function (array $property, array $spec, string $methodName = '') {
return $this->getSubSchema($property, $spec, $methodName);
}),
new TwigFilter('getGenerics', function (string $model, array $spec, bool $skipAdditional = false) {
return $this->getGenerics($model, $spec, $skipAdditional);
Expand Down
6 changes: 4 additions & 2 deletions src/SDK/SDK.php
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,9 @@ public function generate(string $target): void
'contactURL' => $this->spec->getContactURL(),
'contactEmail' => $this->spec->getContactEmail(),
'services' => $this->getFilteredServices(),
'enums' => $this->spec->getEnums(),
'requestEnums' => $this->spec->getRequestEnums(),
'responseEnums' => $this->spec->getResponseEnums(),
'allEnums' => $this->spec->getAllEnums(),
'definitions' => $this->spec->getDefinitions(),
'global' => [
'headers' => $this->spec->getGlobalHeaders(),
Expand Down Expand Up @@ -724,7 +726,7 @@ public function generate(string $target): void
}
break;
case 'enum':
foreach ($this->spec->getEnums() as $key => $enum) {
foreach ($this->spec->getAllEnums() as $key => $enum) {
$params['enum'] = $enum;

$this->render($template, $destination, $block, $params, $minify);
Expand Down
11 changes: 9 additions & 2 deletions src/Spec/Spec.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,16 @@ public function setAttribute($key, $value, $type = self::SET_TYPE_ASSIGN)
}

/**
* Get Enums
* Get Request Enums
*
* @return array
*/
abstract public function getEnums();
abstract public function getRequestEnums();

/**
* Get Response Enums
*
* @return array
*/
abstract public function getResponseEnums();
}
67 changes: 66 additions & 1 deletion src/Spec/Swagger2.php
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,13 @@ public function getDefinitions()
//nested model
$sch['properties'][$name]['sub_schemas'] = \array_map(fn($schema) => str_replace('#/definitions/', '', $schema['$ref']), $def['items']['x-oneOf']);
}

if (isset($def['enum'])) {
// enum property
$sch['properties'][$name]['enum'] = $def['enum'];
$sch['properties'][$name]['enumName'] = $def['x-enum-name'] ?? ucfirst($key) . ucfirst($name);
$sch['properties'][$name]['enumKeys'] = $def['x-enum-keys'] ?? [];
}
}
}
$list[$key] = $sch;
Expand All @@ -499,7 +506,7 @@ public function getDefinitions()
/**
* @return array
*/
public function getEnums(): array
public function getRequestEnums(): array
{
$list = [];

Expand All @@ -523,4 +530,62 @@ public function getEnums(): array

return \array_values($list);
}

/**
* @return array
*/
public function getResponseEnums(): array
{
$list = [];
$definitions = $this->getDefinitions();

foreach ($definitions as $modelName => $model) {
if (isset($model['properties']) && is_array($model['properties'])) {
foreach ($model['properties'] as $propertyName => $property) {
if (isset($property['enum'])) {
$enumName = $property['x-enum-name'] ?? ucfirst($modelName) . ucfirst($propertyName);

if (!isset($list[$enumName])) {
$list[$enumName] = [
'name' => $enumName,
'enum' => $property['enum'],
'keys' => $property['x-enum-keys'] ?? [],
];
}
}

// array of enums
if ((($property['type'] ?? null) === 'array') && isset($property['items']['enum'])) {
$enumName = $property['x-enum-name'] ?? ucfirst($modelName) . ucfirst($propertyName);

if (!isset($list[$enumName])) {
$list[$enumName] = [
'name' => $enumName,
'enum' => $property['items']['enum'],
'keys' => $property['items']['x-enum-keys'] ?? [],
];
}
}
}
}
}

return \array_values($list);
}

/**
* @return array
*/
public function getAllEnums(): array
{
$list = [];
foreach ($this->getRequestEnums() as $enum) {
$list[$enum['name']] = $enum;
}
foreach ($this->getResponseEnums() as $enum) {
$list[$enum['name']] = $enum;
}

return \array_values($list);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ package {{ sdk.namespace | caseDot }}.models

import com.google.gson.annotations.SerializedName
import io.appwrite.extensions.jsonCast
{%~ for property in definition.properties %}
{%~ if property.enum %}
import {{ sdk.namespace | caseDot }}.enums.{{ property.enumName | caseUcfirst }}
{%~ endif %}
{%~ endfor %}

/**
* {{ definition.description | replace({"\n": "\n * "}) | raw }}
Expand All @@ -27,7 +32,7 @@ import io.appwrite.extensions.jsonCast
) {
fun toMap(): Map<String, Any> = mapOf(
{%~ for property in definition.properties %}
"{{ property.name | escapeDollarSign }}" to {% if property.sub_schema %}{% if property.type == 'array' %}{{property.name | escapeKeyword | removeDollarSign}}.map { it.toMap() }{% else %}{{property.name | escapeKeyword | removeDollarSign}}.toMap(){% endif %}{% else %}{{property.name | escapeKeyword | removeDollarSign}}{% endif %} as Any,
"{{ property.name | escapeDollarSign }}" to {% if property.sub_schema %}{% if property.type == 'array' %}{{property.name | escapeKeyword | removeDollarSign}}.map { it.toMap() }{% else %}{{property.name | escapeKeyword | removeDollarSign}}.toMap(){% endif %}{% elseif property.enum %}{{property.name | escapeKeyword | removeDollarSign}}{% if not property.required %}?{% endif %}.value{% else %}{{property.name | escapeKeyword | removeDollarSign}}{% endif %} as Any,
{%~ endfor %}
{%~ if definition.additionalProperties %}
"data" to data!!.jsonCast(to = Map::class.java)
Expand Down Expand Up @@ -61,7 +66,7 @@ import io.appwrite.extensions.jsonCast
{%~ endif %}
) = {{ definition | modelType(spec) | raw }}(
{%~ for property in definition.properties %}
{{ property.name | escapeKeyword | removeDollarSign }} = {% if property.sub_schema %}{% if property.type == 'array' %}(map["{{ property.name | escapeDollarSign }}"] as List<Map<String, Any>>).map { {{ property.sub_schema | caseUcfirst }}.from(map = it{% if property.sub_schema | hasGenericType(spec) %}, nestedType{% endif %}) }{% else %}{{ property.sub_schema | caseUcfirst }}.from(map = map["{{property.name | escapeDollarSign }}"] as Map<String, Any>{% if property.sub_schema | hasGenericType(spec) %}, nestedType{% endif %}){% endif %}{% else %}{% if property.type == "integer" or property.type == "number" %}({% endif %}map["{{ property.name | escapeDollarSign }}"]{% if property.type == "integer" or property.type == "number" %} as{% if not property.required %}?{% endif %} Number){% endif %}{% if property.type == "integer" %}{% if not property.required %}?{% endif %}.toLong(){% elseif property.type == "number" %}{% if not property.required %}?{% endif %}.toDouble(){% else %} as{% if not property.required %}?{% endif %} {{ property | propertyType(spec) | raw }}{% endif %}{% endif %},
{{ property.name | escapeKeyword | removeDollarSign }} = {{ property | propertyAssignment(spec) | raw }},
{%~ endfor %}
{%~ if definition.additionalProperties %}
data = map.jsonCast(to = nestedType)
Expand Down
Loading