From 26c72415a0cc39f1a1935b23a72601cf27002f9d Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Mon, 27 Apr 2026 15:07:00 +0530 Subject: [PATCH 01/58] feat(godot): adding gdscript and godot game engine --- composer.lock | 217 ++++++++++++++-------------- example.php | 45 +++--- src/SDK/Language/GDScript.php | 256 ++++++++++++++++++++++++++++++++++ src/SDK/Language/Godot.php | 14 ++ 4 files changed, 404 insertions(+), 128 deletions(-) create mode 100644 src/SDK/Language/GDScript.php create mode 100644 src/SDK/Language/Godot.php diff --git a/composer.lock b/composer.lock index 2383c7398d..e6c4c33e33 100644 --- a/composer.lock +++ b/composer.lock @@ -198,16 +198,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -257,7 +257,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -277,20 +277,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", "shasum": "" }, "require": { @@ -342,7 +342,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" }, "funding": [ { @@ -362,11 +362,11 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-04-10T17:25:58+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -422,7 +422,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.37.0" }, "funding": [ { @@ -527,16 +527,16 @@ "packages-dev": [ { "name": "brianium/paratest", - "version": "v7.8.4", + "version": "v7.8.5", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "130a9bf0e269ee5f5b320108f794ad03e275cad4" + "reference": "9b324c8fc319cf9728b581c7a90e1c8f6361c5e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/130a9bf0e269ee5f5b320108f794ad03e275cad4", - "reference": "130a9bf0e269ee5f5b320108f794ad03e275cad4", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/9b324c8fc319cf9728b581c7a90e1c8f6361c5e5", + "reference": "9b324c8fc319cf9728b581c7a90e1c8f6361c5e5", "shasum": "" }, "require": { @@ -544,27 +544,27 @@ "ext-pcre": "*", "ext-reflection": "*", "ext-simplexml": "*", - "fidry/cpu-core-counter": "^1.2.0", + "fidry/cpu-core-counter": "^1.3.0", "jean85/pretty-package-versions": "^2.1.1", - "php": "~8.2.0 || ~8.3.0 || ~8.4.0", - "phpunit/php-code-coverage": "^11.0.10", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "phpunit/php-code-coverage": "^11.0.12", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-timer": "^7.0.1", - "phpunit/phpunit": "^11.5.24", + "phpunit/phpunit": "^11.5.46", "sebastian/environment": "^7.2.1", - "symfony/console": "^6.4.22 || ^7.3.0", - "symfony/process": "^6.4.20 || ^7.3.0" + "symfony/console": "^6.4.22 || ^7.3.4 || ^8.0.3", + "symfony/process": "^6.4.20 || ^7.3.4 || ^8.0.3" }, "require-dev": { "doctrine/coding-standard": "^12.0.0", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^2.1.17", + "phpstan/phpstan": "^2.1.33", "phpstan/phpstan-deprecation-rules": "^2.0.3", - "phpstan/phpstan-phpunit": "^2.0.6", - "phpstan/phpstan-strict-rules": "^2.0.4", - "squizlabs/php_codesniffer": "^3.13.2", - "symfony/filesystem": "^6.4.13 || ^7.3.0" + "phpstan/phpstan-phpunit": "^2.0.11", + "phpstan/phpstan-strict-rules": "^2.0.7", + "squizlabs/php_codesniffer": "^3.13.5", + "symfony/filesystem": "^6.4.13 || ^7.3.2 || ^8.0.1" }, "bin": [ "bin/paratest", @@ -604,7 +604,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.8.4" + "source": "https://github.com/paratestphp/paratest/tree/v7.8.5" }, "funding": [ { @@ -616,7 +616,7 @@ "type": "paypal" } ], - "time": "2025-06-23T06:07:21+00:00" + "time": "2026-01-08T08:02:38+00:00" }, { "name": "fidry/cpu-core-counter", @@ -1067,28 +1067,28 @@ }, { "name": "phpunit/php-file-iterator", - "version": "5.1.0", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -1116,15 +1116,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" } ], - "time": "2024-08-27T05:02:59+00:00" + "time": "2026-02-02T13:52:54+00:00" }, { "name": "phpunit/php-invoker", @@ -1312,16 +1324,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.50", + "version": "11.5.55", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3" + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fdfc727f0fcacfeb8fcb30c7e5da173125b58be3", - "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", "shasum": "" }, "require": { @@ -1336,7 +1348,7 @@ "phar-io/version": "^3.2.1", "php": ">=8.2", "phpunit/php-code-coverage": "^11.0.12", - "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-file-iterator": "^5.1.1", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", @@ -1348,6 +1360,7 @@ "sebastian/exporter": "^6.3.2", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", "sebastian/type": "^5.1.3", "sebastian/version": "^5.0.2", "staabm/side-effects-detector": "^1.0.5" @@ -1393,7 +1406,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.50" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" }, "funding": [ { @@ -1417,7 +1430,7 @@ "type": "tidelift" } ], - "time": "2026-01-27T05:59:18+00:00" + "time": "2026-02-18T12:37:06+00:00" }, { "name": "psr/container", @@ -2591,47 +2604,39 @@ }, { "name": "symfony/console", - "version": "v7.3.6", + "version": "v8.0.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a" + "reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "url": "https://api.github.com/repos/symfony/console/zipball/5b66d385dc58f69652e56f78a4184615e3f2b7f7", + "reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" - }, - "conflict": { - "symfony/dependency-injection": "<6.4", - "symfony/dotenv": "<6.4", - "symfony/event-dispatcher": "<6.4", - "symfony/lock": "<6.4", - "symfony/process": "<6.4" + "symfony/string": "^7.4|^8.0" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -2665,7 +2670,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.6" + "source": "https://github.com/symfony/console/tree/v8.0.8" }, "funding": [ { @@ -2685,20 +2690,20 @@ "type": "tidelift" } ], - "time": "2025-11-04T01:21:42+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", "shasum": "" }, "require": { @@ -2747,7 +2752,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" }, "funding": [ { @@ -2767,11 +2772,11 @@ "type": "tidelift" } ], - "time": "2025-06-27T09:58:17+00:00" + "time": "2026-04-26T13:13:48+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -2832,7 +2837,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" }, "funding": [ { @@ -2856,20 +2861,20 @@ }, { "name": "symfony/process", - "version": "v7.4.5", + "version": "v8.0.8", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "608476f4604102976d687c483ac63a79ba18cc97" + "reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", - "reference": "608476f4604102976d687c483ac63a79ba18cc97", + "url": "https://api.github.com/repos/symfony/process/zipball/cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc", + "reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "type": "library", "autoload": { @@ -2897,7 +2902,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.5" + "source": "https://github.com/symfony/process/tree/v8.0.8" }, "funding": [ { @@ -2917,7 +2922,7 @@ "type": "tidelift" } ], - "time": "2026-01-26T15:07:59+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { "name": "symfony/service-contracts", @@ -3008,34 +3013,34 @@ }, { "name": "symfony/string", - "version": "v7.3.4", + "version": "v8.0.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" + "reference": "ae9488f874d7603f9d2dfbf120203882b645d963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", + "url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963", + "reference": "ae9488f874d7603f9d2dfbf120203882b645d963", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3074,7 +3079,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" + "source": "https://github.com/symfony/string/tree/v8.0.8" }, "funding": [ { @@ -3094,7 +3099,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:36:48+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { "name": "theseer/tokenizer", @@ -3149,7 +3154,7 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -3158,6 +3163,6 @@ "ext-mbstring": "*", "ext-json": "*" }, - "platform-dev": {}, - "plugin-api-version": "2.6.0" + "platform-dev": [], + "plugin-api-version": "2.2.0" } diff --git a/example.php b/example.php index 01a7cb8140..0baae0e6d4 100644 --- a/example.php +++ b/example.php @@ -27,10 +27,12 @@ use Appwrite\SDK\Language\ClaudePlugin; use Appwrite\SDK\Language\CursorPlugin; use Appwrite\SDK\Language\Rust; +use Appwrite\SDK\Language\Godot; try { - function getSSLPage($url) { + function getSSLPage($url) + { $ch = curl_init(); curl_setopt($ch, CURLOPT_HEADER, false); curl_setopt($ch, CURLOPT_URL, $url); @@ -41,7 +43,8 @@ function getSSLPage($url) { return $result; } - function configureSDK($sdk, $overrides = []) { + function configureSDK($sdk, $overrides = []) + { $defaults = [ 'name' => 'NAME', 'version' => '0.0.0', @@ -121,7 +124,7 @@ function configureSDK($sdk, $overrides = []) { if ($needsSpec) { $spec = getSSLPage("https://raw.githubusercontent.com/appwrite/specs/main/specs/{$version}/swagger2-{$version}-{$platform}.json"); - if(empty($spec)) { + if (empty($spec)) { throw new Exception('Failed to fetch spec from Appwrite server'); } } @@ -136,28 +139,28 @@ function configureSDK($sdk, $overrides = []) { $php ->setComposerVendor('appwrite') ->setComposerPackage('appwrite'); - $sdk = new SDK($php, new Swagger2($spec)); + $sdk = new SDK($php, new Swagger2($spec)); configureSDK($sdk); $sdk->generate(__DIR__ . '/examples/php'); } // Web if (!$requestedSdk || $requestedSdk === 'web') { - $sdk = new SDK(new Web(), new Swagger2($spec)); + $sdk = new SDK(new Web(), new Swagger2($spec)); configureSDK($sdk, ['platform' => $platform]); $sdk->generate(__DIR__ . '/examples/web'); } // Node if (!$requestedSdk || $requestedSdk === 'node') { - $sdk = new SDK(new Node(), new Swagger2($spec)); + $sdk = new SDK(new Node(), new Swagger2($spec)); configureSDK($sdk); $sdk->generate(__DIR__ . '/examples/node'); } // CLI if (!$requestedSdk || $requestedSdk === 'cli') { - $language = new CLI(); + $language = new CLI(); $language->setNPMPackage('appwrite-cli'); $language->setExecutableName('appwrite'); $language->setLogo(json_encode(" @@ -177,7 +180,7 @@ function configureSDK($sdk, $overrides = []) { \_/ \_/ .__/| .__/ \_/\_/ |_| |_|\__\___| \____/\____/\____/ |_| |_| "); - $sdk = new SDK($language, new Swagger2($spec)); + $sdk = new SDK($language, new Swagger2($spec)); $sdk->setTest(false); configureSDK($sdk, [ 'exclude' => [ @@ -193,14 +196,14 @@ function configureSDK($sdk, $overrides = []) { // Ruby if (!$requestedSdk || $requestedSdk === 'ruby') { - $sdk = new SDK(new Ruby(), new Swagger2($spec)); + $sdk = new SDK(new Ruby(), new Swagger2($spec)); configureSDK($sdk); $sdk->generate(__DIR__ . '/examples/ruby'); } // Python if (!$requestedSdk || $requestedSdk === 'python') { - $sdk = new SDK(new Python(), new Swagger2($spec)); + $sdk = new SDK(new Python(), new Swagger2($spec)); configureSDK($sdk); $sdk->generate(__DIR__ . '/examples/python'); } @@ -209,7 +212,7 @@ function configureSDK($sdk, $overrides = []) { if (!$requestedSdk || $requestedSdk === 'dart') { $dart = new Dart(); $dart->setPackageName('dart_appwrite'); - $sdk = new SDK($dart, new Swagger2($spec)); + $sdk = new SDK($dart, new Swagger2($spec)); configureSDK($sdk); $sdk->generate(__DIR__ . '/examples/dart'); } @@ -218,7 +221,7 @@ function configureSDK($sdk, $overrides = []) { if (!$requestedSdk || $requestedSdk === 'flutter') { $flutter = new Flutter(); $flutter->setPackageName('appwrite'); - $sdk = new SDK($flutter, new Swagger2($spec)); + $sdk = new SDK($flutter, new Swagger2($spec)); configureSDK($sdk); $sdk->generate(__DIR__ . '/examples/flutter'); } @@ -227,42 +230,42 @@ function configureSDK($sdk, $overrides = []) { if (!$requestedSdk || $requestedSdk === 'react-native') { $reactNative = new ReactNative(); $reactNative->setNPMPackage('react-native-appwrite'); - $sdk = new SDK($reactNative, new Swagger2($spec)); + $sdk = new SDK($reactNative, new Swagger2($spec)); configureSDK($sdk); $sdk->generate(__DIR__ . '/examples/react-native'); } // GO if (!$requestedSdk || $requestedSdk === 'go') { - $sdk = new SDK(new Go(), new Swagger2($spec)); + $sdk = new SDK(new Go(), new Swagger2($spec)); configureSDK($sdk); $sdk->generate(__DIR__ . '/examples/go'); } // Swift if (!$requestedSdk || $requestedSdk === 'swift') { - $sdk = new SDK(new Swift(), new Swagger2($spec)); + $sdk = new SDK(new Swift(), new Swagger2($spec)); configureSDK($sdk); $sdk->generate(__DIR__ . '/examples/swift'); } // Apple if (!$requestedSdk || $requestedSdk === 'apple') { - $sdk = new SDK(new Apple(), new Swagger2($spec)); + $sdk = new SDK(new Apple(), new Swagger2($spec)); configureSDK($sdk); $sdk->generate(__DIR__ . '/examples/apple'); } // DotNet if (!$requestedSdk || $requestedSdk === 'dotnet') { - $sdk = new SDK(new DotNet(), new Swagger2($spec)); + $sdk = new SDK(new DotNet(), new Swagger2($spec)); configureSDK($sdk); $sdk->generate(__DIR__ . '/examples/dotnet'); } // REST if (!$requestedSdk || $requestedSdk === 'rest') { - $sdk = new SDK(new REST(), new Swagger2($spec)); + $sdk = new SDK(new REST(), new Swagger2($spec)); configureSDK($sdk); $sdk->generate(__DIR__ . '/examples/REST'); } @@ -345,11 +348,9 @@ function configureSDK($sdk, $overrides = []) { configureSDK($sdk); $sdk->generate(__DIR__ . '/examples/rust'); } -} -catch (Exception $exception) { +} catch (Exception $exception) { echo 'Error: ' . $exception->getMessage() . ' on ' . $exception->getFile() . ':' . $exception->getLine() . "\n"; -} -catch (Throwable $exception) { +} catch (Throwable $exception) { echo 'Error: ' . $exception->getMessage() . ' on ' . $exception->getFile() . ':' . $exception->getLine() . "\n"; } diff --git a/src/SDK/Language/GDScript.php b/src/SDK/Language/GDScript.php new file mode 100644 index 0000000000..8e8f432e7d --- /dev/null +++ b/src/SDK/Language/GDScript.php @@ -0,0 +1,256 @@ +toPascalCase($parameter['model']); + } + return match ($parameter['type']) { + self::TYPE_INTEGER => 'int', + self::TYPE_NUMBER => 'float', + self::TYPE_STRING => 'String', + self::TYPE_FILE => 'FileAccess', // or PackedByteArray depending on usage + self::TYPE_BOOLEAN => 'bool', + self::TYPE_ARRAY => 'Array', + self::TYPE_OBJECT => 'Dictionary', + default => 'Variant', + }; + } + + /** + * @param array $param + * @return string + */ + public function getParamDefault(array $param): string + { + $type = $param['type'] ?? ''; + $default = $param['default'] ?? ''; + $required = $param['required'] ?? ''; + + if ($required) { + return ''; + } + + $output = ' = '; + + if (empty($default) && $default !== 0 && $default !== false) { + switch ($type) { + case self::TYPE_NUMBER: + $output .= '0.0'; + break; + case self::TYPE_INTEGER: + $output .= '0'; + break; + case self::TYPE_BOOLEAN: + $output .= 'false'; + break; + case self::TYPE_STRING: + $output .= '""'; + break; + case self::TYPE_ARRAY: + $output .= '[]'; + break; + case self::TYPE_OBJECT: + $output .= '{}'; + break; + default: + $output .= 'null'; + break; + } + } else { + switch ($type) { + case self::TYPE_NUMBER: + case self::TYPE_INTEGER: + case self::TYPE_ARRAY: + $output .= $default; + break; + case self::TYPE_BOOLEAN: + $output .= ($default) ? 'true' : 'false'; + break; + case self::TYPE_STRING: + $output .= "\"{$default}\""; + break; + case self::TYPE_OBJECT: + $output .= $default; // Should be formatted as GDScript dictionary + break; + default: + $output .= 'null'; + break; + } + } + + return $output; + } + + /** + * @param array $param + * @param string $lang + * @return string + */ + public function getParamExample(array $param, string $lang = ''): string + { + $type = $param['type'] ?? ''; + $example = $param['example'] ?? ''; + + $output = ''; + + if (empty($example) && $example !== 0 && $example !== false) { + switch ($type) { + case self::TYPE_NUMBER: + case self::TYPE_INTEGER: + $output .= '0'; + break; + case self::TYPE_BOOLEAN: + $output .= 'false'; + break; + case self::TYPE_STRING: + $output .= '""'; + break; + case self::TYPE_ARRAY: + $output .= '[]'; + break; + case self::TYPE_OBJECT: + $output .= '{}'; + break; + case self::TYPE_FILE: + $output .= 'FileAccess.open("file.png", FileAccess.READ)'; + break; + } + } else { + switch ($type) { + case self::TYPE_NUMBER: + case self::TYPE_INTEGER: + case self::TYPE_ARRAY: + case self::TYPE_OBJECT: + $output .= $example; + break; + case self::TYPE_BOOLEAN: + $output .= ($example) ? 'true' : 'false'; + break; + case self::TYPE_STRING: + $output .= "\"{$example}\""; + break; + case self::TYPE_FILE: + $output .= 'FileAccess.open("file.png", FileAccess.READ)'; + break; + } + } + + return $output; + } +} diff --git a/src/SDK/Language/Godot.php b/src/SDK/Language/Godot.php new file mode 100644 index 0000000000..ce6fa81b4d --- /dev/null +++ b/src/SDK/Language/Godot.php @@ -0,0 +1,14 @@ + Date: Thu, 30 Apr 2026 22:02:27 +0530 Subject: [PATCH 02/58] feat(godot): bootstrap Godot SDK via Appwrite generator Initial implementation of Godot support: - template-based generation for services and client - preliminary docs example template - mirrors structure of existing SDKs (services/, enums/, models/) - not fully functional yet (runtime + DX pending) --- example.php | 8 + src/SDK/Language/Godot.php | 93 +++++++++ templates/godot/CHANGELOG.md.twig | 1 + templates/godot/LICENSE.twig | 1 + templates/godot/README.md.twig | 54 +++++ templates/godot/docs/example.md.twig | 15 ++ templates/godot/src/client.gd.twig | 129 ++++++++++++ templates/godot/src/enums/enum.gd.twig | 6 + templates/godot/src/exception.gd.twig | 17 ++ templates/godot/src/id.gd.twig | 21 ++ templates/godot/src/input_file.gd.twig | 18 ++ templates/godot/src/models/model.gd.twig | 20 ++ templates/godot/src/permission.gd.twig | 16 ++ templates/godot/src/query.gd.twig | 197 +++++++++++++++++++ templates/godot/src/role.gd.twig | 28 +++ templates/godot/src/service.gd.twig | 7 + templates/godot/src/services/service.gd.twig | 30 +++ 17 files changed, 661 insertions(+) create mode 100644 templates/godot/CHANGELOG.md.twig create mode 100644 templates/godot/LICENSE.twig create mode 100644 templates/godot/README.md.twig create mode 100644 templates/godot/docs/example.md.twig create mode 100644 templates/godot/src/client.gd.twig create mode 100644 templates/godot/src/enums/enum.gd.twig create mode 100644 templates/godot/src/exception.gd.twig create mode 100644 templates/godot/src/id.gd.twig create mode 100644 templates/godot/src/input_file.gd.twig create mode 100644 templates/godot/src/models/model.gd.twig create mode 100644 templates/godot/src/permission.gd.twig create mode 100644 templates/godot/src/query.gd.twig create mode 100644 templates/godot/src/role.gd.twig create mode 100644 templates/godot/src/service.gd.twig create mode 100644 templates/godot/src/services/service.gd.twig diff --git a/example.php b/example.php index 0baae0e6d4..acf59d6f5e 100644 --- a/example.php +++ b/example.php @@ -27,6 +27,7 @@ use Appwrite\SDK\Language\ClaudePlugin; use Appwrite\SDK\Language\CursorPlugin; use Appwrite\SDK\Language\Rust; +use Appwrite\SDK\Language\GDScript; use Appwrite\SDK\Language\Godot; try { @@ -348,6 +349,13 @@ function configureSDK($sdk, $overrides = []) configureSDK($sdk); $sdk->generate(__DIR__ . '/examples/rust'); } + + // Godot + if (!$requestedSdk || $requestedSdk === 'godot') { + $sdk = new SDK(new Godot(), new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/godot'); + } } catch (Exception $exception) { echo 'Error: ' . $exception->getMessage() . ' on ' . $exception->getFile() . ':' . $exception->getLine() . "\n"; } catch (Throwable $exception) { diff --git a/src/SDK/Language/Godot.php b/src/SDK/Language/Godot.php index ce6fa81b4d..db5a957cad 100644 --- a/src/SDK/Language/Godot.php +++ b/src/SDK/Language/Godot.php @@ -2,6 +2,8 @@ namespace Appwrite\SDK\Language; +use Appwrite\SDK\Language\GDScript; + class Godot extends GDScript { /** @@ -11,4 +13,95 @@ public function getName(): string { return 'Godot'; } + + public function getVersion(): string + { + return '4.0'; + } + + public function getFiles(): array + { + return [ + [ + 'scope' => 'default', + 'destination' => 'CHANGELOG.md', + 'template' => 'godot/CHANGELOG.md.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'LICENSE', + 'template' => 'godot/LICENSE.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'README.md', + 'template' => 'godot/README.md.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'src/addons/{{ sdk.name | caseLower }}/client.gd', + 'template' => 'godot/src/client.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'src/addons/{{ sdk.name | caseLower }}/service.gd', + 'template' => 'godot/src/service.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'src/addons/{{ sdk.name | caseLower }}/exception.gd', + 'template' => 'godot/src/exception.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'src/addons/{{ sdk.name | caseLower }}/id.gd', + 'template' => 'godot/src/id.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'src/addons/{{ sdk.name | caseLower }}/permission.gd', + 'template' => 'godot/src/permission.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'src/addons/{{ sdk.name | caseLower }}/role.gd', + 'template' => 'godot/src/role.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'src/addons/{{ sdk.name | caseLower }}/query.gd', + 'template' => 'godot/src/query.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'src/addons/{{ sdk.name | caseLower }}/input_file.gd', + 'template' => 'godot/src/input_file.gd.twig', + ], + [ + 'scope' => 'enum', + 'destination' => 'src/addons/{{ sdk.name | caseLower }}/enums/{{ enum.name | caseSnake }}.gd', + 'template' => 'godot/src/enums/enum.gd.twig', + ], + [ + 'scope' => 'service', + 'destination' => 'src/addons/{{ sdk.name | caseLower }}/services/{{ service.name | caseSnake }}.gd', + 'template' => 'godot/src/services/service.gd.twig', + ], + [ + 'scope' => 'definition', + 'destination' => 'src/addons/{{ sdk.name | caseLower }}/models/{{ definition.name | caseSnake }}.gd', + 'template' => 'godot/src/models/model.gd.twig', + ], + [ + 'scope' => 'method', + 'destination' => 'src/addons/{{sdk.name | caseLower}}/docs/{{service.name | caseSnake}}/{{method.name | caseSnake}}.md', + 'template' => 'godot/docs/example.md.twig', + ], + ]; + } + + public function getDocsUrl(): string + { + return 'https://appwrite.io/docs/sdks/godot'; + } } \ No newline at end of file diff --git a/templates/godot/CHANGELOG.md.twig b/templates/godot/CHANGELOG.md.twig new file mode 100644 index 0000000000..c176de4fcc --- /dev/null +++ b/templates/godot/CHANGELOG.md.twig @@ -0,0 +1 @@ +{{ sdk.changelog }} diff --git a/templates/godot/LICENSE.twig b/templates/godot/LICENSE.twig new file mode 100644 index 0000000000..418c652f66 --- /dev/null +++ b/templates/godot/LICENSE.twig @@ -0,0 +1 @@ +{{ sdk.licenseContent }} diff --git a/templates/godot/README.md.twig b/templates/godot/README.md.twig new file mode 100644 index 0000000000..30488fc70a --- /dev/null +++ b/templates/godot/README.md.twig @@ -0,0 +1,54 @@ +# {{ spec.title }} {{ sdk.name }} SDK + + +![License](https://img.shields.io/github/license/{{ sdk.gitUserName|url_encode }}/{{ sdk.gitRepoName|url_encode }}.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-{{ spec.version|url_encode }}-blue.svg?style=flat-square) +{% if sdk.twitterHandle %} +[![Twitter Account](https://img.shields.io/twitter/follow/{{ sdk.twitterHandle }}?color=00acee&label=twitter&style=flat-square)](https://twitter.com/{{ sdk.twitterHandle }}) +{% endif %} + +**{{ sdk.description }}** + +{% if sdk.logo %} +![{{ spec.title }}]({{ sdk.logo }}) +{% endif %} + +## Installation + +1. Download the generated SDK source files. +2. Place the `src` folder into your Godot project (e.g., `res://addons/appwrite/`). +3. Add the files to your Godot project. + +## Getting Started + +In Godot 4, the SDK uses asynchronous coroutines via the `await` keyword. You can initialize the `Client` and then use any of the available services. + +```gdscript +extends Node + +var client = AppwriteClient.new() + +func _ready(): + # Initialize the Appwrite client + client.set_endpoint('https://cloud.appwrite.io/v1') + client.set_project('YOUR_PROJECT_ID') + + # Initialize a service, e.g. Account + var account = AppwriteAccount.new(client) + + # Call an API method asynchronously + var response = await account.get() + + if response.has("error"): + print("Error: ", response["error"]) + else: + print("Logged in as: ", response["name"]) +``` + +## Contribution + +This library is auto-generated by Appwrite custom [SDK Generator](https://github.com/appwrite/sdk-generator). To learn more about how you can help us improve this SDK, please check the [contribution guide](https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md) before sending a pull-request. + +## License + +Please see the [{{spec.licenseName}} license]({{spec.licenseURL}}) file for more information. \ No newline at end of file diff --git a/templates/godot/docs/example.md.twig b/templates/godot/docs/example.md.twig new file mode 100644 index 0000000000..f7fd6473f7 --- /dev/null +++ b/templates/godot/docs/example.md.twig @@ -0,0 +1,15 @@ +extends Node + +func _ready(): + {% if method.type != 'webAuth' %} + var result = await Appwrite.{{ service.name | caseCamel }}.{{ method.name | caseCamel }}( + {% else %} + var result = Appwrite.{{ service.name | caseCamel }}.{{ method.name | caseCamel }}( + {% endif %} + + {% for parameter in method.parameters.all %} + {{ parameter.name | caseSnake }} = {{ parameter | paramExample }}{% if not loop.last %},{% endif %}{% if not parameter.required %} # optional{% endif %} + {% endfor %} + ) + + print(result) \ No newline at end of file diff --git a/templates/godot/src/client.gd.twig b/templates/godot/src/client.gd.twig new file mode 100644 index 0000000000..dd2de31103 --- /dev/null +++ b/templates/godot/src/client.gd.twig @@ -0,0 +1,129 @@ +class_name AppwriteClient +extends RefCounted + +var _chunk_size = 5 * 1024 * 1024 +var _self_signed = false +var _endpoint = '{{spec.endpoint}}' +var _global_headers = { + 'content-type': '', + 'user-agent': '{{spec.title | caseUcfirst}}{{ language.name | caseUcfirst }}SDK/{{ sdk.version }} (Godot/' + Engine.get_version_info().string + '; ' + OS.get_name() + ')', + 'x-sdk-name': '{{ sdk.name }}', + 'x-sdk-platform': '{{ sdk.platform }}', + 'x-sdk-language': '{{ language.name | caseLower }}', + 'x-sdk-version': '{{ sdk.version }}', +{% for key,header in spec.global.defaultHeaders %} + '{{key}}': '{{header}}', +{% endfor %} +} + +func set_self_signed(status: bool = true) -> AppwriteClient: + _self_signed = status + return self + +func set_endpoint(endpoint: String) -> AppwriteClient: + _endpoint = endpoint + return self + +func add_header(key: String, value: String) -> AppwriteClient: + _global_headers[key.to_lower()] = value + return self + +func get_headers() -> Dictionary: + return _global_headers.duplicate() + +{% for header in spec.global.headers %} +func set_{{header.key | caseSnake}}(value: String) -> AppwriteClient: +{% if header.description %} + # {{header.description}} +{% endif %} + _global_headers['{{header.name|lower}}'] = value + return self + +{% endfor %} + +func call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: + var http := HTTPClient.new() + + # Parse host, port, and TLS from _endpoint + var uri := _endpoint.trim_suffix("/") + var use_tls := uri.begins_with("https://") + var host := uri.replace("https://", "").replace("http://", "") + var port := 443 if use_tls else 80 + if ":" in host: + var parts := host.split(":") + host = parts[0] + port = int(parts[1]) + + var tls_options := TLSOptions.client_unsafe() if _self_signed else TLSOptions.client() + var err := http.connect_to_host(host, port, tls_options) + if err != OK: + return {"statusCode": 0, "error": "Failed to connect: " + error_string(err)} + + while http.get_status() in [HTTPClient.STATUS_CONNECTING, HTTPClient.STATUS_RESOLVING]: + http.poll() + await Engine.get_main_loop().process_frame + + if http.get_status() != HTTPClient.STATUS_CONNECTED: + return {"statusCode": 0, "error": "Could not connect, status: " + str(http.get_status())} + + # Merge headers + var combined_headers := _global_headers.duplicate() + for key in headers: + combined_headers[key.to_lower()] = headers[key] + + # Choose HTTP method constant + var http_method: int + match method.to_upper(): + "POST": http_method = HTTPClient.METHOD_POST + "PUT": http_method = HTTPClient.METHOD_PUT + "PATCH": http_method = HTTPClient.METHOD_PATCH + "DELETE": http_method = HTTPClient.METHOD_DELETE + _: http_method = HTTPClient.METHOD_GET + + # Build request path and body + var request_path := path + var body := PackedByteArray() + + if http_method in [HTTPClient.METHOD_GET, HTTPClient.METHOD_DELETE]: + if not params.is_empty(): + request_path += "?" + http.query_string_from_dict(params) + else: + var body_str := JSON.stringify(params) + body = body_str.to_utf8_buffer() + combined_headers["content-type"] = "application/json" + combined_headers["content-length"] = str(body.size()) + + # Build header array + var header_list := PackedStringArray() + for key in combined_headers: + header_list.append(key + ": " + str(combined_headers[key])) + + err = http.request_raw(http_method, request_path, header_list, body) + if err != OK: + return {"statusCode": 0, "error": "Request failed: " + error_string(err)} + + while http.get_status() == HTTPClient.STATUS_REQUESTING: + http.poll() + await Engine.get_main_loop().process_frame + + if http.get_status() not in [HTTPClient.STATUS_BODY, HTTPClient.STATUS_CONNECTED]: + return {"statusCode": 0, "error": "Unexpected status after request: " + str(http.get_status())} + + var response_code := http.get_response_code() + + # Read body chunks + var response_body := PackedByteArray() + while http.get_status() == HTTPClient.STATUS_BODY: + http.poll() + var chunk := http.read_response_body_chunk() + if chunk.size() > 0: + response_body.append_array(chunk) + else: + await Engine.get_main_loop().process_frame + + var response_text := response_body.get_string_from_utf8() + var json := JSON.new() + if json.parse(response_text) == OK: + return {"statusCode": response_code, "body": json.data} + else: + return {"statusCode": response_code, "body": response_text} diff --git a/templates/godot/src/enums/enum.gd.twig b/templates/godot/src/enums/enum.gd.twig new file mode 100644 index 0000000000..ad1ce970c9 --- /dev/null +++ b/templates/godot/src/enums/enum.gd.twig @@ -0,0 +1,6 @@ +class_name {{ enum.name | caseUcfirst }} + +{% for value in enum.enum %} +{% set key = enum.keys is empty ? value : enum.keys[loop.index0] %} +const {{ key | caseSnake | upper | escapeKeyword }} = "{{ value }}" +{% endfor %} diff --git a/templates/godot/src/exception.gd.twig b/templates/godot/src/exception.gd.twig new file mode 100644 index 0000000000..6432711c14 --- /dev/null +++ b/templates/godot/src/exception.gd.twig @@ -0,0 +1,17 @@ +class_name {{ spec.title | caseUcfirst }}Exception extends RefCounted + +var message: String +var code: int +var type: String +var response: String + +func _init(p_message: String = "", p_code: int = 0, p_type: String = "", p_response: String = "") -> void: + self.message = p_message + self.code = p_code + self.type = p_type + self.response = p_response + +func _to_string() -> String: + if message == "": + return "{{ spec.title | caseUcfirst }}Exception" + return "{{ spec.title | caseUcfirst }}Exception: " + type + ", " + message + " (" + str(code) + ")" diff --git a/templates/godot/src/id.gd.twig b/templates/godot/src/id.gd.twig new file mode 100644 index 0000000000..c566d3496e --- /dev/null +++ b/templates/godot/src/id.gd.twig @@ -0,0 +1,21 @@ +class_name ID extends RefCounted + +static func _hex_timestamp() -> String: + var now := Time.get_unix_time_from_system() + var sec := int(floor(now)) + var usec := int((now - sec) * 1000000) + var sec_hex := "%x" % sec + var usec_hex := "%05x" % usec + return sec_hex + usec_hex + +static func unique(padding: int = 7) -> String: + var id := _hex_timestamp() + if padding > 0: + var sb := "" + for i in range(padding): + sb += "%x" % (randi() % 16) + id += sb + return id + +static func custom(id: String) -> String: + return id diff --git a/templates/godot/src/input_file.gd.twig b/templates/godot/src/input_file.gd.twig new file mode 100644 index 0000000000..8185e8e2c3 --- /dev/null +++ b/templates/godot/src/input_file.gd.twig @@ -0,0 +1,18 @@ +class_name InputFile extends RefCounted + +var path: String +var bytes: PackedByteArray +var filename: String +var content_type: String + +func _init(p_path: String = "", p_bytes: PackedByteArray = PackedByteArray(), p_filename: String = "", p_content_type: String = "") -> void: + self.path = p_path + self.bytes = p_bytes + self.filename = p_filename + self.content_type = p_content_type + +static func from_path(p_path: String, p_filename: String = "", p_content_type: String = "") -> InputFile: + return InputFile.new(p_path, PackedByteArray(), p_filename, p_content_type) + +static func from_bytes(p_bytes: PackedByteArray, p_filename: String, p_content_type: String = "") -> InputFile: + return InputFile.new("", p_bytes, p_filename, p_content_type) diff --git a/templates/godot/src/models/model.gd.twig b/templates/godot/src/models/model.gd.twig new file mode 100644 index 0000000000..4091436cd6 --- /dev/null +++ b/templates/godot/src/models/model.gd.twig @@ -0,0 +1,20 @@ +class_name {{ definition.name | caseUcfirst }} +extends RefCounted + +{% for property in definition.properties %} +var {{ property.name | caseSnake }}: {{ property | typeName }} +{% endfor %} + +func from_dictionary(dict: Dictionary) -> {{ definition.name | caseUcfirst }}: +{% for property in definition.properties %} + if dict.has('{{ property.name }}'): + {{ property.name | caseSnake }} = dict['{{ property.name }}'] +{% endfor %} + return self + +func to_dictionary() -> Dictionary: + var dict = {} +{% for property in definition.properties %} + dict['{{ property.name }}'] = {{ property.name | caseSnake }} +{% endfor %} + return dict diff --git a/templates/godot/src/permission.gd.twig b/templates/godot/src/permission.gd.twig new file mode 100644 index 0000000000..c0bfaf0da9 --- /dev/null +++ b/templates/godot/src/permission.gd.twig @@ -0,0 +1,16 @@ +class_name Permission extends RefCounted + +static func read(role: String) -> String: + return 'read("%s")' % role + +static func write(role: String) -> String: + return 'write("%s")' % role + +static func create(role: String) -> String: + return 'create("%s")' % role + +static func update(role: String) -> String: + return 'update("%s")' % role + +static func delete(role: String) -> String: + return 'delete("%s")' % role diff --git a/templates/godot/src/query.gd.twig b/templates/godot/src/query.gd.twig new file mode 100644 index 0000000000..21e1db0214 --- /dev/null +++ b/templates/godot/src/query.gd.twig @@ -0,0 +1,197 @@ +class_name Query extends RefCounted + +var method: String +var attribute: Variant +var values: Variant + +func _init(p_method: String, p_attribute: Variant = null, p_values: Variant = null) -> void: + self.method = p_method + self.attribute = p_attribute + self.values = p_values + +func to_dict() -> Dictionary: + var result := { + "method": method + } + if attribute != null: + result["attribute"] = attribute + if values != null: + if values is Array: + result["values"] = values + else: + result["values"] = [values] + return result + +func _to_string() -> String: + return JSON.stringify(to_dict()) + +static func equal(attribute: String, value: Variant) -> String: + return Query.new("equal", attribute, value)._to_string() + +static func notEqual(attribute: String, value: Variant) -> String: + return Query.new("notEqual", attribute, value)._to_string() + +static func regex(attribute: String, pattern: String) -> String: + return Query.new("regex", attribute, pattern)._to_string() + +static func lessThan(attribute: String, value: Variant) -> String: + return Query.new("lessThan", attribute, value)._to_string() + +static func lessThanEqual(attribute: String, value: Variant) -> String: + return Query.new("lessThanEqual", attribute, value)._to_string() + +static func greaterThan(attribute: String, value: Variant) -> String: + return Query.new("greaterThan", attribute, value)._to_string() + +static func greaterThanEqual(attribute: String, value: Variant) -> String: + return Query.new("greaterThanEqual", attribute, value)._to_string() + +static func search(attribute: String, value: String) -> String: + return Query.new("search", attribute, value)._to_string() + +static func isNull(attribute: String) -> String: + return Query.new("isNull", attribute)._to_string() + +static func isNotNull(attribute: String) -> String: + return Query.new("isNotNull", attribute)._to_string() + +static func exists(attributes: Array) -> String: + return Query.new("exists", null, attributes)._to_string() + +static func notExists(attributes: Array) -> String: + return Query.new("notExists", null, attributes)._to_string() + +static func between(attribute: String, start: Variant, end: Variant) -> String: + return Query.new("between", attribute, [start, end])._to_string() + +static func startsWith(attribute: String, value: String) -> String: + return Query.new("startsWith", attribute, value)._to_string() + +static func endsWith(attribute: String, value: String) -> String: + return Query.new("endsWith", attribute, value)._to_string() + +static func contains(attribute: String, value: Variant) -> String: + return Query.new("contains", attribute, value)._to_string() + +static func containsAny(attribute: String, value: Array) -> String: + return Query.new("containsAny", attribute, value)._to_string() + +static func containsAll(attribute: String, value: Array) -> String: + return Query.new("containsAll", attribute, value)._to_string() + +static func notContains(attribute: String, value: Variant) -> String: + return Query.new("notContains", attribute, value)._to_string() + +static func notSearch(attribute: String, value: String) -> String: + return Query.new("notSearch", attribute, value)._to_string() + +static func notBetween(attribute: String, start: Variant, end: Variant) -> String: + return Query.new("notBetween", attribute, [start, end])._to_string() + +static func notStartsWith(attribute: String, value: String) -> String: + return Query.new("notStartsWith", attribute, value)._to_string() + +static func notEndsWith(attribute: String, value: String) -> String: + return Query.new("notEndsWith", attribute, value)._to_string() + +static func createdBefore(value: String) -> String: + return lessThan("$createdAt", value) + +static func createdAfter(value: String) -> String: + return greaterThan("$createdAt", value) + +static func createdBetween(start: String, end: String) -> String: + return between("$createdAt", start, end) + +static func updatedBefore(value: String) -> String: + return lessThan("$updatedAt", value) + +static func updatedAfter(value: String) -> String: + return greaterThan("$updatedAt", value) + +static func updatedBetween(start: String, end: String) -> String: + return between("$updatedAt", start, end) + +static func or_query(queries: Array) -> String: + var parsed_queries := [] + for q in queries: + var parsed = JSON.parse_string(q) + if parsed != null: + parsed_queries.append(parsed) + return Query.new("or", null, parsed_queries)._to_string() + +static func and_query(queries: Array) -> String: + var parsed_queries := [] + for q in queries: + var parsed = JSON.parse_string(q) + if parsed != null: + parsed_queries.append(parsed) + return Query.new("and", null, parsed_queries)._to_string() + +static func elemMatch(attribute: String, queries: Array) -> String: + var parsed_queries := [] + for q in queries: + var parsed = JSON.parse_string(q) + if parsed != null: + parsed_queries.append(parsed) + return Query.new("elemMatch", attribute, parsed_queries)._to_string() + +static func select(attributes: Array) -> String: + return Query.new("select", null, attributes)._to_string() + +static func orderAsc(attribute: String) -> String: + return Query.new("orderAsc", attribute)._to_string() + +static func orderDesc(attribute: String) -> String: + return Query.new("orderDesc", attribute)._to_string() + +static func orderRandom() -> String: + return Query.new("orderRandom")._to_string() + +static func cursorBefore(id: String) -> String: + return Query.new("cursorBefore", null, id)._to_string() + +static func cursorAfter(id: String) -> String: + return Query.new("cursorAfter", null, id)._to_string() + +static func limit(p_limit: int) -> String: + return Query.new("limit", null, p_limit)._to_string() + +static func offset(p_offset: int) -> String: + return Query.new("offset", null, p_offset)._to_string() + +static func distanceEqual(attribute: String, values: Array, distance: float, meters: bool = true) -> String: + return Query.new("distanceEqual", attribute, [[values, distance, meters]])._to_string() + +static func distanceNotEqual(attribute: String, values: Array, distance: float, meters: bool = true) -> String: + return Query.new("distanceNotEqual", attribute, [[values, distance, meters]])._to_string() + +static func distanceGreaterThan(attribute: String, values: Array, distance: float, meters: bool = true) -> String: + return Query.new("distanceGreaterThan", attribute, [[values, distance, meters]])._to_string() + +static func distanceLessThan(attribute: String, values: Array, distance: float, meters: bool = true) -> String: + return Query.new("distanceLessThan", attribute, [[values, distance, meters]])._to_string() + +static func intersects(attribute: String, values: Array) -> String: + return Query.new("intersects", attribute, [values])._to_string() + +static func notIntersects(attribute: String, values: Array) -> String: + return Query.new("notIntersects", attribute, [values])._to_string() + +static func crosses(attribute: String, values: Array) -> String: + return Query.new("crosses", attribute, [values])._to_string() + +static func notCrosses(attribute: String, values: Array) -> String: + return Query.new("notCrosses", attribute, [values])._to_string() + +static func overlaps(attribute: String, values: Array) -> String: + return Query.new("overlaps", attribute, [values])._to_string() + +static func notOverlaps(attribute: String, values: Array) -> String: + return Query.new("notOverlaps", attribute, [values])._to_string() + +static func touches(attribute: String, values: Array) -> String: + return Query.new("touches", attribute, [values])._to_string() + +static func notTouches(attribute: String, values: Array) -> String: + return Query.new("notTouches", attribute, [values])._to_string() diff --git a/templates/godot/src/role.gd.twig b/templates/godot/src/role.gd.twig new file mode 100644 index 0000000000..d68bf2596d --- /dev/null +++ b/templates/godot/src/role.gd.twig @@ -0,0 +1,28 @@ +class_name Role extends RefCounted + +static func any() -> String: + return 'any' + +static func user(id: String, status: String = '') -> String: + if status == '': + return 'user:%s' % id + return 'user:%s/%s' % [id, status] + +static func users(status: String = '') -> String: + if status == '': + return 'users' + return 'users/%s' % status + +static func guests() -> String: + return 'guests' + +static func team(id: String, role: String = '') -> String: + if role == '': + return 'team:%s' % id + return 'team:%s/%s' % [id, role] + +static func member(id: String) -> String: + return 'member:%s' % id + +static func label(name: String) -> String: + return 'label:%s' % name diff --git a/templates/godot/src/service.gd.twig b/templates/godot/src/service.gd.twig new file mode 100644 index 0000000000..7c1d59ccd9 --- /dev/null +++ b/templates/godot/src/service.gd.twig @@ -0,0 +1,7 @@ +class_name AppwriteService +extends RefCounted + +var client: AppwriteClient + +func _init(client_ptr: AppwriteClient): + client = client_ptr diff --git a/templates/godot/src/services/service.gd.twig b/templates/godot/src/services/service.gd.twig new file mode 100644 index 0000000000..e78ad7ebf4 --- /dev/null +++ b/templates/godot/src/services/service.gd.twig @@ -0,0 +1,30 @@ +class_name {{ service.name | caseUcfirst }} +extends AppwriteService + +{% for method in service.methods %} +func {{ method.name | caseSnake }}({% for parameter in method.parameters.all %}{{ parameter.name | caseSnake }}: {{ parameter | typeName }}{{ parameter | paramDefault }}{% if not loop.last %}, {% endif %}{% endfor %}) -> Dictionary: + var path = '{{ method.path }}' +{% for parameter in method.parameters.path %} + path = path.replace('{{ '{' }}{{ parameter.name }}{{ '}' }}', {{ parameter.name | caseSnake }}) +{% endfor %} + var params = { +{% for parameter in method.parameters.query %} + '{{ parameter.name }}': {{ parameter.name | caseSnake }}, +{% endfor %} +{% for parameter in method.parameters.body %} + '{{ parameter.name }}': {{ parameter.name | caseSnake }}, +{% endfor %} +{% for parameter in method.parameters.formData %} + '{{ parameter.name }}': {{ parameter.name | caseSnake }}, +{% endfor %} + } + + var headers = { +{% for key, value in method.headers %} + '{{ key }}': '{{ value }}', +{% endfor %} + } + + return await client.call_api('{{ method.method }}', path, headers, params) + +{% endfor %} From 740831ff3a85a5baa054a73aaa6e52ba1a7d5e5b Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Fri, 1 May 2026 00:57:09 +0530 Subject: [PATCH 03/58] wip(godot): refactor Godot SDK templates for native integration - Refactored Godot SDK output directory structure to follow the addons/appwrite convention for plugins. - Migrated call_api in client.gd from an HTTPClient polling loop to a native, async HTTPRequest node architecture. - Added appwrite.gd plugin singleton and updated plugin.cfg / plugin.gd generation logic. - Implemented robust error handling by parsing HTTP responses into typed AppwriteExceptions. - Fixed docs/example.md.twig template for correct GDScript method invocation and autoload reference. - Enhanced global state management by defaulting endpoint and project settings directly from ProjectSettings overrides. --- src/SDK/Language/GDScript.php | 13 +- src/SDK/Language/Godot.php | 44 +++++-- templates/godot/README.md.twig | 42 ++++--- templates/godot/docs/example.md.twig | 27 ++-- templates/godot/src/appwrite.gd.twig | 31 +++++ templates/godot/src/client.gd.twig | 125 +++++++++++-------- templates/godot/src/enums/enum.gd.twig | 2 +- templates/godot/src/exception.gd.twig | 2 +- templates/godot/src/icon.svg | 1 + templates/godot/src/id.gd.twig | 2 +- templates/godot/src/input_file.gd.twig | 20 ++- templates/godot/src/models/model.gd.twig | 32 ++++- templates/godot/src/permission.gd.twig | 2 +- templates/godot/src/plugin.cfg.twig | 7 ++ templates/godot/src/plugin.gd.twig | 8 ++ templates/godot/src/query.gd.twig | 95 +++++++------- templates/godot/src/role.gd.twig | 2 +- templates/godot/src/service.gd.twig | 9 +- templates/godot/src/services/service.gd.twig | 46 +++++-- 19 files changed, 339 insertions(+), 171 deletions(-) create mode 100644 templates/godot/src/appwrite.gd.twig create mode 100644 templates/godot/src/icon.svg create mode 100644 templates/godot/src/plugin.cfg.twig create mode 100644 templates/godot/src/plugin.gd.twig diff --git a/src/SDK/Language/GDScript.php b/src/SDK/Language/GDScript.php index 8e8f432e7d..69a5dafbb7 100644 --- a/src/SDK/Language/GDScript.php +++ b/src/SDK/Language/GDScript.php @@ -108,23 +108,26 @@ public function getFiles(): array */ public function getTypeName(array $parameter, array $spec = []): string { + $prefix = $this->toPascalCase($spec['title'] ?? 'Appwrite'); + if (isset($parameter['enumName'])) { - return \ucfirst($parameter['enumName']); + return $prefix . \ucfirst($parameter['enumName']); } if (!empty($parameter['enumValues'])) { - return \ucfirst($parameter['name']); + return $prefix . \ucfirst($parameter['name']); } if (!empty($parameter['array']['model'])) { - return 'Array'; + return 'Array[' . $prefix . $this->toPascalCase($parameter['array']['model']) . ']'; } if (!empty($parameter['model'])) { - return $parameter['type'] === self::TYPE_ARRAY ? 'Array' : $this->toPascalCase($parameter['model']); + $modelType = $prefix . $this->toPascalCase($parameter['model']); + return $parameter['type'] === self::TYPE_ARRAY ? 'Array[' . $modelType . ']' : $modelType; } return match ($parameter['type']) { self::TYPE_INTEGER => 'int', self::TYPE_NUMBER => 'float', self::TYPE_STRING => 'String', - self::TYPE_FILE => 'FileAccess', // or PackedByteArray depending on usage + self::TYPE_FILE => $prefix . 'InputFile', self::TYPE_BOOLEAN => 'bool', self::TYPE_ARRAY => 'Array', self::TYPE_OBJECT => 'Dictionary', diff --git a/src/SDK/Language/Godot.php b/src/SDK/Language/Godot.php index db5a957cad..1dd43fc77d 100644 --- a/src/SDK/Language/Godot.php +++ b/src/SDK/Language/Godot.php @@ -39,62 +39,82 @@ public function getFiles(): array ], [ 'scope' => 'default', - 'destination' => 'src/addons/{{ sdk.name | caseLower }}/client.gd', + 'destination' => 'addons/{{ sdk.name | caseLower }}/plugin.cfg', + 'template' => 'godot/src/plugin.cfg.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ sdk.name | caseLower }}/plugin.gd', + 'template' => 'godot/src/plugin.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ sdk.name | caseLower }}/{{ sdk.name | caseLower }}.gd', + 'template' => 'godot/src/appwrite.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ sdk.name | caseLower }}/icon.svg', + 'template' => 'godot/src/icon.svg', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ sdk.name | caseLower }}/client.gd', 'template' => 'godot/src/client.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'src/addons/{{ sdk.name | caseLower }}/service.gd', + 'destination' => 'addons/{{ sdk.name | caseLower }}/service.gd', 'template' => 'godot/src/service.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'src/addons/{{ sdk.name | caseLower }}/exception.gd', + 'destination' => 'addons/{{ sdk.name | caseLower }}/exception.gd', 'template' => 'godot/src/exception.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'src/addons/{{ sdk.name | caseLower }}/id.gd', + 'destination' => 'addons/{{ sdk.name | caseLower }}/id.gd', 'template' => 'godot/src/id.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'src/addons/{{ sdk.name | caseLower }}/permission.gd', + 'destination' => 'addons/{{ sdk.name | caseLower }}/permission.gd', 'template' => 'godot/src/permission.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'src/addons/{{ sdk.name | caseLower }}/role.gd', + 'destination' => 'addons/{{ sdk.name | caseLower }}/role.gd', 'template' => 'godot/src/role.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'src/addons/{{ sdk.name | caseLower }}/query.gd', + 'destination' => 'addons/{{ sdk.name | caseLower }}/query.gd', 'template' => 'godot/src/query.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'src/addons/{{ sdk.name | caseLower }}/input_file.gd', + 'destination' => 'addons/{{ sdk.name | caseLower }}/input_file.gd', 'template' => 'godot/src/input_file.gd.twig', ], [ 'scope' => 'enum', - 'destination' => 'src/addons/{{ sdk.name | caseLower }}/enums/{{ enum.name | caseSnake }}.gd', + 'destination' => 'addons/{{ sdk.name | caseLower }}/enums/{{ enum.name | caseSnake }}.gd', 'template' => 'godot/src/enums/enum.gd.twig', ], [ 'scope' => 'service', - 'destination' => 'src/addons/{{ sdk.name | caseLower }}/services/{{ service.name | caseSnake }}.gd', + 'destination' => 'addons/{{ sdk.name | caseLower }}/services/{{ service.name | caseSnake }}.gd', 'template' => 'godot/src/services/service.gd.twig', ], [ 'scope' => 'definition', - 'destination' => 'src/addons/{{ sdk.name | caseLower }}/models/{{ definition.name | caseSnake }}.gd', + 'destination' => 'addons/{{ sdk.name | caseLower }}/models/{{ definition.name | caseSnake }}.gd', 'template' => 'godot/src/models/model.gd.twig', ], [ 'scope' => 'method', - 'destination' => 'src/addons/{{sdk.name | caseLower}}/docs/{{service.name | caseSnake}}/{{method.name | caseSnake}}.md', + 'destination' => 'docs/examples/{{service.name | caseSnake}}/{{method.name | caseSnake}}.md', 'template' => 'godot/docs/example.md.twig', ], ]; diff --git a/templates/godot/README.md.twig b/templates/godot/README.md.twig index 30488fc70a..21f0da2bc6 100644 --- a/templates/godot/README.md.twig +++ b/templates/godot/README.md.twig @@ -1,6 +1,5 @@ # {{ spec.title }} {{ sdk.name }} SDK - ![License](https://img.shields.io/github/license/{{ sdk.gitUserName|url_encode }}/{{ sdk.gitRepoName|url_encode }}.svg?style=flat-square) ![Version](https://img.shields.io/badge/api%20version-{{ spec.version|url_encode }}-blue.svg?style=flat-square) {% if sdk.twitterHandle %} @@ -15,34 +14,45 @@ ## Installation -1. Download the generated SDK source files. -2. Place the `src` folder into your Godot project (e.g., `res://addons/appwrite/`). -3. Add the files to your Godot project. +1. Copy the `addons/{{sdk.name | caseLower}}` folder into your Godot project's `res://addons/` directory. +2. Enable the plugin in **Project Settings** > **Plugins**. +3. The SDK is now available as a global singleton named `{{sdk.name}}`. ## Getting Started -In Godot 4, the SDK uses asynchronous coroutines via the `await` keyword. You can initialize the `Client` and then use any of the available services. +In Godot 4, the SDK uses asynchronous coroutines via the `await` keyword. You can use the global `{{sdk.name}}` singleton or initialize the `{{sdk.name}}Client` manually. + +### Using the Autoload Singleton ```gdscript extends Node -var client = AppwriteClient.new() +func _ready(): + # Initialize the global Appwrite singleton + {{sdk.name}}.set_endpoint('https://cloud.appwrite.io/v1') + {{sdk.name}}.set_project('YOUR_PROJECT_ID') + + # Call an API method asynchronously + var response = await {{sdk.name}}.account.get() + + if response == null: + print("An error occurred") + else: + print("Logged in as: ", response.name) +``` + +### Manual Initialization + +```gdscript +extends Node func _ready(): - # Initialize the Appwrite client + var client = {{sdk.name}}Client.new() client.set_endpoint('https://cloud.appwrite.io/v1') client.set_project('YOUR_PROJECT_ID') - # Initialize a service, e.g. Account - var account = AppwriteAccount.new(client) - - # Call an API method asynchronously + var account = {{sdk.name}}Account.new(client) var response = await account.get() - - if response.has("error"): - print("Error: ", response["error"]) - else: - print("Logged in as: ", response["name"]) ``` ## Contribution diff --git a/templates/godot/docs/example.md.twig b/templates/godot/docs/example.md.twig index f7fd6473f7..d5143ecff4 100644 --- a/templates/godot/docs/example.md.twig +++ b/templates/godot/docs/example.md.twig @@ -1,15 +1,22 @@ extends Node func _ready(): - {% if method.type != 'webAuth' %} - var result = await Appwrite.{{ service.name | caseCamel }}.{{ method.name | caseCamel }}( - {% else %} - var result = Appwrite.{{ service.name | caseCamel }}.{{ method.name | caseCamel }}( - {% endif %} + # If the Appwrite Autoload is configured, you can directly use it + Appwrite.set_endpoint("https://cloud.appwrite.io/v1") + Appwrite.set_project("YOUR_PROJECT_ID") + +{% if method.type != 'webAuth' %} + var result = await Appwrite.{{ service.name | caseCamel }}.{{ method.name | caseSnake }}( +{% else %} + var result = Appwrite.{{ service.name | caseCamel }}.{{ method.name | caseSnake }}( +{% endif %} +{% for parameter in method.parameters.all %} + {{ parameter | paramExample }}{% if not loop.last %},{% endif %}{% if not parameter.required %} # optional{% endif %} - {% for parameter in method.parameters.all %} - {{ parameter.name | caseSnake }} = {{ parameter | paramExample }}{% if not loop.last %},{% endif %}{% if not parameter.required %} # optional{% endif %} - {% endfor %} - ) +{% endfor %} + ) - print(result) \ No newline at end of file + if result is {{ spec.title | caseUcfirst }}Exception: + print("Error: ", result.message) + else: + print(result) \ No newline at end of file diff --git a/templates/godot/src/appwrite.gd.twig b/templates/godot/src/appwrite.gd.twig new file mode 100644 index 0000000000..1b923eb3c4 --- /dev/null +++ b/templates/godot/src/appwrite.gd.twig @@ -0,0 +1,31 @@ +{% set prefix = spec.title | caseUcfirst %} +extends Node + +# Autoload singleton for {{spec.title}} + +var client: {{prefix}}Client + +{% for service in spec.services %} +var {{ service.name | caseCamel }}: {{prefix}}{{ service.name | caseUcfirst }} +{% endfor %} + +func _ready() -> void: + client = {{prefix}}Client.new() + + # Attempt to load configuration from ProjectSettings + if ProjectSettings.has_setting("appwrite/config/endpoint"): + client.set_endpoint(ProjectSettings.get_setting("appwrite/config/endpoint")) + if ProjectSettings.has_setting("appwrite/config/project"): + client.set_project(ProjectSettings.get_setting("appwrite/config/project")) + +{% for service in spec.services %} + {{ service.name | caseCamel }} = {{prefix}}{{ service.name | caseUcfirst }}.new(client) +{% endfor %} + +func set_endpoint(endpoint: String) -> void: + client.set_endpoint(endpoint) + +func set_project(project: String) -> void: + client.set_project(project) + +# Add other global setters if needed diff --git a/templates/godot/src/client.gd.twig b/templates/godot/src/client.gd.twig index dd2de31103..0c40d19aab 100644 --- a/templates/godot/src/client.gd.twig +++ b/templates/godot/src/client.gd.twig @@ -1,4 +1,5 @@ -class_name AppwriteClient +{% set prefix = spec.title | caseUcfirst %} +class_name {{prefix}}Client extends RefCounted var _chunk_size = 5 * 1024 * 1024 @@ -16,15 +17,15 @@ var _global_headers = { {% endfor %} } -func set_self_signed(status: bool = true) -> AppwriteClient: +func set_self_signed(status: bool = true) -> {{prefix}}Client: _self_signed = status return self -func set_endpoint(endpoint: String) -> AppwriteClient: +func set_endpoint(endpoint: String) -> {{prefix}}Client: _endpoint = endpoint return self -func add_header(key: String, value: String) -> AppwriteClient: +func add_header(key: String, value: String) -> {{prefix}}Client: _global_headers[key.to_lower()] = value return self @@ -32,7 +33,7 @@ func get_headers() -> Dictionary: return _global_headers.duplicate() {% for header in spec.global.headers %} -func set_{{header.key | caseSnake}}(value: String) -> AppwriteClient: +func set_{{header.key | caseSnake}}(value: String) -> {{prefix}}Client: {% if header.description %} # {{header.description}} {% endif %} @@ -42,29 +43,12 @@ func set_{{header.key | caseSnake}}(value: String) -> AppwriteClient: {% endfor %} func call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: - var http := HTTPClient.new() - - # Parse host, port, and TLS from _endpoint - var uri := _endpoint.trim_suffix("/") - var use_tls := uri.begins_with("https://") - var host := uri.replace("https://", "").replace("http://", "") - var port := 443 if use_tls else 80 - if ":" in host: - var parts := host.split(":") - host = parts[0] - port = int(parts[1]) - + var http := HTTPRequest.new() + var tls_options := TLSOptions.client_unsafe() if _self_signed else TLSOptions.client() - var err := http.connect_to_host(host, port, tls_options) - if err != OK: - return {"statusCode": 0, "error": "Failed to connect: " + error_string(err)} - - while http.get_status() in [HTTPClient.STATUS_CONNECTING, HTTPClient.STATUS_RESOLVING]: - http.poll() - await Engine.get_main_loop().process_frame - - if http.get_status() != HTTPClient.STATUS_CONNECTED: - return {"statusCode": 0, "error": "Could not connect, status: " + str(http.get_status())} + http.set_tls_options(tls_options) + + Engine.get_main_loop().root.add_child(http) # Merge headers var combined_headers := _global_headers.duplicate() @@ -80,46 +64,52 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param "DELETE": http_method = HTTPClient.METHOD_DELETE _: http_method = HTTPClient.METHOD_GET - # Build request path and body + var uri := _endpoint.trim_suffix("/") var request_path := path var body := PackedByteArray() - + if http_method in [HTTPClient.METHOD_GET, HTTPClient.METHOD_DELETE]: if not params.is_empty(): - request_path += "?" + http.query_string_from_dict(params) + var query_http := HTTPClient.new() + request_path += "?" + query_http.query_string_from_dict(params) else: - var body_str := JSON.stringify(params) - body = body_str.to_utf8_buffer() - combined_headers["content-type"] = "application/json" + var has_files := false + for key in params: + if params[key] is {{prefix}}InputFile: + has_files = true + break + + if has_files: + var boundary := "Boundary-%x" % Time.get_ticks_msec() + combined_headers["content-type"] = "multipart/form-data; boundary=" + boundary + body = _build_multipart(params, boundary) + else: + var body_str := JSON.stringify(params) + body = body_str.to_utf8_buffer() + combined_headers["content-type"] = "application/json" + combined_headers["content-length"] = str(body.size()) # Build header array var header_list := PackedStringArray() for key in combined_headers: - header_list.append(key + ": " + str(combined_headers[key])) + if combined_headers[key] != "": + header_list.append(key + ": " + str(combined_headers[key])) - err = http.request_raw(http_method, request_path, header_list, body) + var err = http.request_raw(uri + request_path, header_list, http_method, body) if err != OK: + http.queue_free() return {"statusCode": 0, "error": "Request failed: " + error_string(err)} - while http.get_status() == HTTPClient.STATUS_REQUESTING: - http.poll() - await Engine.get_main_loop().process_frame - - if http.get_status() not in [HTTPClient.STATUS_BODY, HTTPClient.STATUS_CONNECTED]: - return {"statusCode": 0, "error": "Unexpected status after request: " + str(http.get_status())} - - var response_code := http.get_response_code() - - # Read body chunks - var response_body := PackedByteArray() - while http.get_status() == HTTPClient.STATUS_BODY: - http.poll() - var chunk := http.read_response_body_chunk() - if chunk.size() > 0: - response_body.append_array(chunk) - else: - await Engine.get_main_loop().process_frame + var response = await http.request_completed + http.queue_free() + + var request_result: int = response[0] + var response_code: int = response[1] + var response_body: PackedByteArray = response[3] + + if request_result != HTTPRequest.RESULT_SUCCESS: + return {"statusCode": 0, "error": "HTTP Request Error: " + str(request_result)} var response_text := response_body.get_string_from_utf8() var json := JSON.new() @@ -127,3 +117,32 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param return {"statusCode": response_code, "body": json.data} else: return {"statusCode": response_code, "body": response_text} + + +func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: + var body := PackedByteArray() + for key in params: + var value = params[key] + if value == null: continue + + body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) + + if value is {{prefix}}InputFile: + body.append_array(("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" % [key, value.filename]).to_utf8_buffer()) + var c_type = value.content_type if value.content_type != "" else "application/octet-stream" + body.append_array(("Content-Type: %s\r\n\r\n" % c_type).to_utf8_buffer()) + body.append_array(value.get_data()) + elif value is Array: + for i in range(value.size()): + if i > 0: + body.append_array(("\r\n--" + boundary + "\r\n").to_utf8_buffer()) + body.append_array(("Content-Disposition: form-data; name=\"%s[]\"\r\n\r\n" % key).to_utf8_buffer()) + body.append_array((str(value[i])).to_utf8_buffer()) + else: + body.append_array(("Content-Disposition: form-data; name=\"%s\"\r\n\r\n" % key).to_utf8_buffer()) + body.append_array((str(value)).to_utf8_buffer()) + + body.append_array(("\r\n").to_utf8_buffer()) + + body.append_array(("--" + boundary + "--\r\n").to_utf8_buffer()) + return body diff --git a/templates/godot/src/enums/enum.gd.twig b/templates/godot/src/enums/enum.gd.twig index ad1ce970c9..df24555460 100644 --- a/templates/godot/src/enums/enum.gd.twig +++ b/templates/godot/src/enums/enum.gd.twig @@ -1,4 +1,4 @@ -class_name {{ enum.name | caseUcfirst }} +class_name {{spec.title | caseUcfirst}}{{ enum.name | caseUcfirst }} {% for value in enum.enum %} {% set key = enum.keys is empty ? value : enum.keys[loop.index0] %} diff --git a/templates/godot/src/exception.gd.twig b/templates/godot/src/exception.gd.twig index 6432711c14..da0826b20c 100644 --- a/templates/godot/src/exception.gd.twig +++ b/templates/godot/src/exception.gd.twig @@ -1,4 +1,4 @@ -class_name {{ spec.title | caseUcfirst }}Exception extends RefCounted +class_name {{spec.title | caseUcfirst}}Exception extends RefCounted var message: String var code: int diff --git a/templates/godot/src/icon.svg b/templates/godot/src/icon.svg new file mode 100644 index 0000000000..1e37b46dea --- /dev/null +++ b/templates/godot/src/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/godot/src/id.gd.twig b/templates/godot/src/id.gd.twig index c566d3496e..ae6c68cdd8 100644 --- a/templates/godot/src/id.gd.twig +++ b/templates/godot/src/id.gd.twig @@ -1,4 +1,4 @@ -class_name ID extends RefCounted +class_name {{spec.title | caseUcfirst}}ID extends RefCounted static func _hex_timestamp() -> String: var now := Time.get_unix_time_from_system() diff --git a/templates/godot/src/input_file.gd.twig b/templates/godot/src/input_file.gd.twig index 8185e8e2c3..79e188b4c4 100644 --- a/templates/godot/src/input_file.gd.twig +++ b/templates/godot/src/input_file.gd.twig @@ -1,4 +1,4 @@ -class_name InputFile extends RefCounted +class_name {{spec.title | caseUcfirst}}InputFile extends RefCounted var path: String var bytes: PackedByteArray @@ -10,9 +10,19 @@ func _init(p_path: String = "", p_bytes: PackedByteArray = PackedByteArray(), p_ self.bytes = p_bytes self.filename = p_filename self.content_type = p_content_type + + if self.filename == "" and self.path != "": + self.filename = self.path.get_file() -static func from_path(p_path: String, p_filename: String = "", p_content_type: String = "") -> InputFile: - return InputFile.new(p_path, PackedByteArray(), p_filename, p_content_type) +static func from_path(p_path: String, p_filename: String = "", p_content_type: String = "") -> {{spec.title | caseUcfirst}}InputFile: + return {{spec.title | caseUcfirst}}InputFile.new(p_path, PackedByteArray(), p_filename, p_content_type) -static func from_bytes(p_bytes: PackedByteArray, p_filename: String, p_content_type: String = "") -> InputFile: - return InputFile.new("", p_bytes, p_filename, p_content_type) +static func from_bytes(p_bytes: PackedByteArray, p_filename: String, p_content_type: String = "") -> {{spec.title | caseUcfirst}}InputFile: + return {{spec.title | caseUcfirst}}InputFile.new("", p_bytes, p_filename, p_content_type) + +func get_data() -> PackedByteArray: + if not bytes.is_empty(): + return bytes + if not path.is_empty(): + return FileAccess.get_file_as_bytes(path) + return PackedByteArray() diff --git a/templates/godot/src/models/model.gd.twig b/templates/godot/src/models/model.gd.twig index 4091436cd6..d99a068ae0 100644 --- a/templates/godot/src/models/model.gd.twig +++ b/templates/godot/src/models/model.gd.twig @@ -1,20 +1,40 @@ -class_name {{ definition.name | caseUcfirst }} +{% set prefix = spec.title | caseUcfirst %} +class_name {{prefix}}{{ definition.name | caseUcfirst }} extends RefCounted {% for property in definition.properties %} -var {{ property.name | caseSnake }}: {{ property | typeName }} +var {{ property.name | caseSnake }}: {{ property | typeName(spec) }} {% endfor %} -func from_dictionary(dict: Dictionary) -> {{ definition.name | caseUcfirst }}: +static func from_dict(dict: Dictionary) -> {{prefix}}{{ definition.name | caseUcfirst }}: + var model = {{prefix}}{{ definition.name | caseUcfirst }}.new() {% for property in definition.properties %} if dict.has('{{ property.name }}'): - {{ property.name | caseSnake }} = dict['{{ property.name }}'] +{% if property.model %} + model.{{ property.name | caseSnake }} = {{prefix}}{{ property.model | caseUcfirst }}.from_dict(dict['{{ property.name }}']) +{% elseif property.array.model %} + var {{ property.name | caseSnake }}_list: {{ property | typeName(spec) }} = [] + for item in dict['{{ property.name }}']: + {{ property.name | caseSnake }}_list.append({{prefix}}{{ property.array.model | caseUcfirst }}.from_dict(item)) + model.{{ property.name | caseSnake }} = {{ property.name | caseSnake }}_list +{% else %} + model.{{ property.name | caseSnake }} = dict['{{ property.name }}'] +{% endif %} {% endfor %} - return self + return model -func to_dictionary() -> Dictionary: +func to_dict() -> Dictionary: var dict = {} {% for property in definition.properties %} +{% if property.model %} + dict['{{ property.name }}'] = {{ property.name | caseSnake }}.to_dict() if {{ property.name | caseSnake }} else null +{% elseif property.array.model %} + var {{ property.name | caseSnake }}_list = [] + for item in {{ property.name | caseSnake }}: + {{ property.name | caseSnake }}_list.append(item.to_dict()) + dict['{{ property.name }}'] = {{ property.name | caseSnake }}_list +{% else %} dict['{{ property.name }}'] = {{ property.name | caseSnake }} +{% endif %} {% endfor %} return dict diff --git a/templates/godot/src/permission.gd.twig b/templates/godot/src/permission.gd.twig index c0bfaf0da9..1dee6e9cf9 100644 --- a/templates/godot/src/permission.gd.twig +++ b/templates/godot/src/permission.gd.twig @@ -1,4 +1,4 @@ -class_name Permission extends RefCounted +class_name {{spec.title | caseUcfirst}}Permission extends RefCounted static func read(role: String) -> String: return 'read("%s")' % role diff --git a/templates/godot/src/plugin.cfg.twig b/templates/godot/src/plugin.cfg.twig new file mode 100644 index 0000000000..cc5ea0095f --- /dev/null +++ b/templates/godot/src/plugin.cfg.twig @@ -0,0 +1,7 @@ +[plugin] + +name="{{sdk.name}}" +description="{{sdk.description}}" +author="Appwrite Team" +version="{{sdk.version}}" +script="plugin.gd" diff --git a/templates/godot/src/plugin.gd.twig b/templates/godot/src/plugin.gd.twig new file mode 100644 index 0000000000..f6c5ca3fc6 --- /dev/null +++ b/templates/godot/src/plugin.gd.twig @@ -0,0 +1,8 @@ +@tool +extends EditorPlugin + +func _enter_tree(): + add_autoload_singleton("{{sdk.name}}", "res://addons/{{sdk.name | caseLower}}/{{sdk.name | caseLower}}.gd") + +func _exit_tree(): + remove_autoload_singleton("{{sdk.name}}") diff --git a/templates/godot/src/query.gd.twig b/templates/godot/src/query.gd.twig index 21e1db0214..4d6fdb8a29 100644 --- a/templates/godot/src/query.gd.twig +++ b/templates/godot/src/query.gd.twig @@ -1,4 +1,4 @@ -class_name Query extends RefCounted +class_name {{spec.title | caseUcfirst}}Query extends RefCounted var method: String var attribute: Variant @@ -26,73 +26,73 @@ func _to_string() -> String: return JSON.stringify(to_dict()) static func equal(attribute: String, value: Variant) -> String: - return Query.new("equal", attribute, value)._to_string() + return {{spec.title | caseUcfirst}}Query.new("equal", attribute, value)._to_string() static func notEqual(attribute: String, value: Variant) -> String: - return Query.new("notEqual", attribute, value)._to_string() + return {{spec.title | caseUcfirst}}Query.new("notEqual", attribute, value)._to_string() static func regex(attribute: String, pattern: String) -> String: - return Query.new("regex", attribute, pattern)._to_string() + return {{spec.title | caseUcfirst}}Query.new("regex", attribute, pattern)._to_string() static func lessThan(attribute: String, value: Variant) -> String: - return Query.new("lessThan", attribute, value)._to_string() + return {{spec.title | caseUcfirst}}Query.new("lessThan", attribute, value)._to_string() static func lessThanEqual(attribute: String, value: Variant) -> String: - return Query.new("lessThanEqual", attribute, value)._to_string() + return {{spec.title | caseUcfirst}}Query.new("lessThanEqual", attribute, value)._to_string() static func greaterThan(attribute: String, value: Variant) -> String: - return Query.new("greaterThan", attribute, value)._to_string() + return {{spec.title | caseUcfirst}}Query.new("greaterThan", attribute, value)._to_string() static func greaterThanEqual(attribute: String, value: Variant) -> String: - return Query.new("greaterThanEqual", attribute, value)._to_string() + return {{spec.title | caseUcfirst}}Query.new("greaterThanEqual", attribute, value)._to_string() static func search(attribute: String, value: String) -> String: - return Query.new("search", attribute, value)._to_string() + return {{spec.title | caseUcfirst}}Query.new("search", attribute, value)._to_string() static func isNull(attribute: String) -> String: - return Query.new("isNull", attribute)._to_string() + return {{spec.title | caseUcfirst}}Query.new("isNull", attribute)._to_string() static func isNotNull(attribute: String) -> String: - return Query.new("isNotNull", attribute)._to_string() + return {{spec.title | caseUcfirst}}Query.new("isNotNull", attribute)._to_string() static func exists(attributes: Array) -> String: - return Query.new("exists", null, attributes)._to_string() + return {{spec.title | caseUcfirst}}Query.new("exists", null, attributes)._to_string() static func notExists(attributes: Array) -> String: - return Query.new("notExists", null, attributes)._to_string() + return {{spec.title | caseUcfirst}}Query.new("notExists", null, attributes)._to_string() static func between(attribute: String, start: Variant, end: Variant) -> String: - return Query.new("between", attribute, [start, end])._to_string() + return {{spec.title | caseUcfirst}}Query.new("between", attribute, [start, end])._to_string() static func startsWith(attribute: String, value: String) -> String: - return Query.new("startsWith", attribute, value)._to_string() + return {{spec.title | caseUcfirst}}Query.new("startsWith", attribute, value)._to_string() static func endsWith(attribute: String, value: String) -> String: - return Query.new("endsWith", attribute, value)._to_string() + return {{spec.title | caseUcfirst}}Query.new("endsWith", attribute, value)._to_string() static func contains(attribute: String, value: Variant) -> String: - return Query.new("contains", attribute, value)._to_string() + return {{spec.title | caseUcfirst}}Query.new("contains", attribute, value)._to_string() static func containsAny(attribute: String, value: Array) -> String: - return Query.new("containsAny", attribute, value)._to_string() + return {{spec.title | caseUcfirst}}Query.new("containsAny", attribute, value)._to_string() static func containsAll(attribute: String, value: Array) -> String: - return Query.new("containsAll", attribute, value)._to_string() + return {{spec.title | caseUcfirst}}Query.new("containsAll", attribute, value)._to_string() static func notContains(attribute: String, value: Variant) -> String: - return Query.new("notContains", attribute, value)._to_string() + return {{spec.title | caseUcfirst}}Query.new("notContains", attribute, value)._to_string() static func notSearch(attribute: String, value: String) -> String: - return Query.new("notSearch", attribute, value)._to_string() + return {{spec.title | caseUcfirst}}Query.new("notSearch", attribute, value)._to_string() static func notBetween(attribute: String, start: Variant, end: Variant) -> String: - return Query.new("notBetween", attribute, [start, end])._to_string() + return {{spec.title | caseUcfirst}}Query.new("notBetween", attribute, [start, end])._to_string() static func notStartsWith(attribute: String, value: String) -> String: - return Query.new("notStartsWith", attribute, value)._to_string() + return {{spec.title | caseUcfirst}}Query.new("notStartsWith", attribute, value)._to_string() static func notEndsWith(attribute: String, value: String) -> String: - return Query.new("notEndsWith", attribute, value)._to_string() + return {{spec.title | caseUcfirst}}Query.new("notEndsWith", attribute, value)._to_string() static func createdBefore(value: String) -> String: return lessThan("$createdAt", value) @@ -118,7 +118,7 @@ static func or_query(queries: Array) -> String: var parsed = JSON.parse_string(q) if parsed != null: parsed_queries.append(parsed) - return Query.new("or", null, parsed_queries)._to_string() + return {{spec.title | caseUcfirst}}Query.new("or", null, parsed_queries)._to_string() static func and_query(queries: Array) -> String: var parsed_queries := [] @@ -126,7 +126,7 @@ static func and_query(queries: Array) -> String: var parsed = JSON.parse_string(q) if parsed != null: parsed_queries.append(parsed) - return Query.new("and", null, parsed_queries)._to_string() + return {{spec.title | caseUcfirst}}Query.new("and", null, parsed_queries)._to_string() static func elemMatch(attribute: String, queries: Array) -> String: var parsed_queries := [] @@ -134,64 +134,65 @@ static func elemMatch(attribute: String, queries: Array) -> String: var parsed = JSON.parse_string(q) if parsed != null: parsed_queries.append(parsed) - return Query.new("elemMatch", attribute, parsed_queries)._to_string() + return {{spec.title | caseUcfirst}}Query.new("elemMatch", attribute, parsed_queries)._to_string() static func select(attributes: Array) -> String: - return Query.new("select", null, attributes)._to_string() + return {{spec.title | caseUcfirst}}Query.new("select", null, attributes)._to_string() static func orderAsc(attribute: String) -> String: - return Query.new("orderAsc", attribute)._to_string() + return {{spec.title | caseUcfirst}}Query.new("orderAsc", attribute)._to_string() static func orderDesc(attribute: String) -> String: - return Query.new("orderDesc", attribute)._to_string() + return {{spec.title | caseUcfirst}}Query.new("orderDesc", attribute)._to_string() static func orderRandom() -> String: - return Query.new("orderRandom")._to_string() + return {{spec.title | caseUcfirst}}Query.new("orderRandom")._to_string() static func cursorBefore(id: String) -> String: - return Query.new("cursorBefore", null, id)._to_string() + return {{spec.title | caseUcfirst}}Query.new("cursorBefore", null, id)._to_string() static func cursorAfter(id: String) -> String: - return Query.new("cursorAfter", null, id)._to_string() + return {{spec.title | caseUcfirst}}Query.new("cursorAfter", null, id)._to_string() static func limit(p_limit: int) -> String: - return Query.new("limit", null, p_limit)._to_string() + return {{spec.title | caseUcfirst}}Query.new("limit", null, p_limit)._to_string() static func offset(p_offset: int) -> String: - return Query.new("offset", null, p_offset)._to_string() + return {{spec.title | caseUcfirst}}Query.new("offset", null, p_offset)._to_string() static func distanceEqual(attribute: String, values: Array, distance: float, meters: bool = true) -> String: - return Query.new("distanceEqual", attribute, [[values, distance, meters]])._to_string() + return {{spec.title | caseUcfirst}}Query.new("distanceEqual", attribute, [[values, distance, meters]])._to_string() static func distanceNotEqual(attribute: String, values: Array, distance: float, meters: bool = true) -> String: - return Query.new("distanceNotEqual", attribute, [[values, distance, meters]])._to_string() + return {{spec.title | caseUcfirst}}Query.new("distanceNotEqual", attribute, [[values, distance, meters]])._to_string() static func distanceGreaterThan(attribute: String, values: Array, distance: float, meters: bool = true) -> String: - return Query.new("distanceGreaterThan", attribute, [[values, distance, meters]])._to_string() + return {{spec.title | caseUcfirst}}Query.new("distanceGreaterThan", attribute, [[values, distance, meters]])._to_string() static func distanceLessThan(attribute: String, values: Array, distance: float, meters: bool = true) -> String: - return Query.new("distanceLessThan", attribute, [[values, distance, meters]])._to_string() + return {{spec.title | caseUcfirst}}Query.new("distanceLessThan", attribute, [[values, distance, meters]])._to_string() static func intersects(attribute: String, values: Array) -> String: - return Query.new("intersects", attribute, [values])._to_string() + return {{spec.title | caseUcfirst}}Query.new("intersects", attribute, [values])._to_string() static func notIntersects(attribute: String, values: Array) -> String: - return Query.new("notIntersects", attribute, [values])._to_string() + return {{spec.title | caseUcfirst}}Query.new("notIntersects", attribute, [values])._to_string() static func crosses(attribute: String, values: Array) -> String: - return Query.new("crosses", attribute, [values])._to_string() + return {{spec.title | caseUcfirst}}Query.new("crosses", attribute, [values])._to_string() static func notCrosses(attribute: String, values: Array) -> String: - return Query.new("notCrosses", attribute, [values])._to_string() + return {{spec.title | caseUcfirst}}Query.new("notCrosses", attribute, [values])._to_string() static func overlaps(attribute: String, values: Array) -> String: - return Query.new("overlaps", attribute, [values])._to_string() + return {{spec.title | caseUcfirst}}Query.new("overlaps", attribute, [values])._to_string() static func notOverlaps(attribute: String, values: Array) -> String: - return Query.new("notOverlaps", attribute, [values])._to_string() + return {{spec.title | caseUcfirst}}Query.new("notOverlaps", attribute, [values])._to_string() static func touches(attribute: String, values: Array) -> String: - return Query.new("touches", attribute, [values])._to_string() + return {{spec.title | caseUcfirst}}Query.new("touches", attribute, [values])._to_string() static func notTouches(attribute: String, values: Array) -> String: - return Query.new("notTouches", attribute, [values])._to_string() + return {{spec.title | caseUcfirst}}Query.new("notTouches", attribute, [values])._to_string() + diff --git a/templates/godot/src/role.gd.twig b/templates/godot/src/role.gd.twig index d68bf2596d..194c879883 100644 --- a/templates/godot/src/role.gd.twig +++ b/templates/godot/src/role.gd.twig @@ -1,4 +1,4 @@ -class_name Role extends RefCounted +class_name {{spec.title | caseUcfirst}}Role extends RefCounted static func any() -> String: return 'any' diff --git a/templates/godot/src/service.gd.twig b/templates/godot/src/service.gd.twig index 7c1d59ccd9..da2c941cf4 100644 --- a/templates/godot/src/service.gd.twig +++ b/templates/godot/src/service.gd.twig @@ -1,7 +1,8 @@ -class_name AppwriteService +{% set prefix = spec.title | caseUcfirst %} +class_name {{prefix}}Service extends RefCounted -var client: AppwriteClient +var client: {{prefix}}Client -func _init(client_ptr: AppwriteClient): - client = client_ptr +func _init(p_client: {{prefix}}Client) -> void: + client = p_client diff --git a/templates/godot/src/services/service.gd.twig b/templates/godot/src/services/service.gd.twig index e78ad7ebf4..542cc49d3f 100644 --- a/templates/godot/src/services/service.gd.twig +++ b/templates/godot/src/services/service.gd.twig @@ -1,13 +1,15 @@ -class_name {{ service.name | caseUcfirst }} -extends AppwriteService +{% set prefix = spec.title | caseUcfirst %} +class_name {{prefix}}{{ service.name | caseUcfirst }} +extends {{prefix}}Service {% for method in service.methods %} -func {{ method.name | caseSnake }}({% for parameter in method.parameters.all %}{{ parameter.name | caseSnake }}: {{ parameter | typeName }}{{ parameter | paramDefault }}{% if not loop.last %}, {% endif %}{% endfor %}) -> Dictionary: - var path = '{{ method.path }}' +{% set return_type = method.responseModel | caseUcfirst %} +func {{ method.name | caseSnake }}({% for parameter in method.parameters.all %}{{ parameter.name | caseSnake }}: {{ parameter | typeName(spec) }}{{ parameter | paramDefault }}{% if not loop.last %}, {% endif %}{% endfor %}) -> Variant: + var path := '{{ method.path }}' {% for parameter in method.parameters.path %} - path = path.replace('{{ '{' }}{{ parameter.name }}{{ '}' }}', {{ parameter.name | caseSnake }}) + path = path.replace('{{ '{' }}{{ parameter.name }}{{ '}' }}', str({{ parameter.name | caseSnake }})) {% endfor %} - var params = { + var params := { {% for parameter in method.parameters.query %} '{{ parameter.name }}': {{ parameter.name | caseSnake }}, {% endfor %} @@ -19,12 +21,40 @@ func {{ method.name | caseSnake }}({% for parameter in method.parameters.all %}{ {% endfor %} } - var headers = { + var headers := { {% for key, value in method.headers %} '{{ key }}': '{{ value }}', {% endfor %} } - return await client.call_api('{{ method.method }}', path, headers, params) + var result = await client.call_api('{{ method.method }}', path, headers, params) + + if result.statusCode >= 400: + var message = "" + var code = result.statusCode + var type = "" + var response = "" + if result.body is Dictionary: + message = result.body.get("message", "") + code = result.body.get("code", result.statusCode) + type = result.body.get("type", "") + response = str(result.body) + else: + message = str(result.body) + response = str(result.body) + + push_error("{{prefix}} Error (%s): %s" % [type, message]) + return {{prefix}}Exception.new(message, code, type, response) + +{% if method.responseModel and method.responseModel != 'any' %} + if result.body is Array: + var list: Array[{{prefix}}{{return_type}}] = [] + for item in result.body: + list.append({{prefix}}{{return_type}}.from_dict(item)) + return list + return {{prefix}}{{ return_type }}.from_dict(result.body) +{% else %} + return result.body +{% endif %} {% endfor %} From 4e1dfe02af1b775ca588c5358164a8bbbe6c58dc Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Sat, 2 May 2026 01:07:17 +0530 Subject: [PATCH 04/58] wip(godot): improve codegen, enum handling, and SDK structure - corrected type resolution for models, enums, arrays of models, and arrays of enums - added .env support for loading client configuration - reduced global namespace pollution by removing class_name from enums - improved enum system to support string-based values (Appwrite-compatible vs Godot int enums) - refactored models to use FIELD_MAP for better readability and mapping - added proper import handling for enums and nested models - updated plugin to load only when enabled (not immediately after installation) - centralized network calls and exception handling for better debugging - removed deprecated and duplicate methods from generated services - updated services to use centralized network caller --- src/SDK/Language/GDScript.php | 75 +++++++++-- src/SDK/Language/Godot.php | 35 +++--- templates/godot/README.md.twig | 18 +-- templates/godot/docs/example.md.twig | 3 +- templates/godot/example.env.twig | 5 + templates/godot/src/appwrite.gd.twig | 69 +++++++++-- templates/godot/src/client.gd.twig | 25 ++-- templates/godot/src/enums/enum.gd.twig | 15 ++- templates/godot/src/exception.gd.twig | 2 +- templates/godot/src/id.gd.twig | 2 +- templates/godot/src/input_file.gd.twig | 2 +- templates/godot/src/models/model.gd.twig | 124 +++++++++++++++---- templates/godot/src/permission.gd.twig | 2 +- templates/godot/src/plugin.cfg.twig | 6 +- templates/godot/src/plugin.gd.twig | 13 +- templates/godot/src/query.gd.twig | 2 +- templates/godot/src/role.gd.twig | 2 +- templates/godot/src/service.gd.twig | 45 ++++++- templates/godot/src/services/service.gd.twig | 97 +++++++-------- 19 files changed, 392 insertions(+), 150 deletions(-) create mode 100644 templates/godot/example.env.twig diff --git a/src/SDK/Language/GDScript.php b/src/SDK/Language/GDScript.php index 69a5dafbb7..ecf04aaf20 100644 --- a/src/SDK/Language/GDScript.php +++ b/src/SDK/Language/GDScript.php @@ -3,6 +3,7 @@ namespace Appwrite\SDK\Language; use Appwrite\SDK\Language; +use Twig\TwigFilter; class GDScript extends Language { @@ -67,6 +68,17 @@ public function getKeywords(): array 'TAU', 'INF', 'NAN', + // NODE specific keywords + '_ready', + 'name', + 'type', + '_process', + '_physics_process', + '_input', + '_unhandled_input', + '_exit_tree', + 'get', + 'set', ]; } @@ -108,29 +120,49 @@ public function getFiles(): array */ public function getTypeName(array $parameter, array $spec = []): string { - $prefix = $this->toPascalCase($spec['title'] ?? 'Appwrite'); + // ARRAY TYPES + if (($parameter['type'] ?? null) === self::TYPE_ARRAY) { - if (isset($parameter['enumName'])) { - return $prefix . \ucfirst($parameter['enumName']); + // Array of models + if (!empty($parameter['array']['model'])) { + return 'Array[' . $this->toPascalCase($parameter['array']['model']) . ']'; + } + + // Array of enums + if (isset($parameter['enumName'])) { + return 'Array[' . $this->toPascalCase($parameter['enumName']) . ']'; + } + + if (!empty($parameter['enumValues'])) { + return 'Array[' . $this->toPascalCase($parameter['name']) . ']'; + } + + return 'Array'; } - if (!empty($parameter['enumValues'])) { - return $prefix . \ucfirst($parameter['name']); + + // MODEL TYPE + if (!empty($parameter['model'])) { + return $this->toPascalCase($parameter['model']); } - if (!empty($parameter['array']['model'])) { - return 'Array[' . $prefix . $this->toPascalCase($parameter['array']['model']) . ']'; + + // ENUM TYPE + if (isset($parameter['enumName'])) { + return $this->toPascalCase($parameter['enumName']); } - if (!empty($parameter['model'])) { - $modelType = $prefix . $this->toPascalCase($parameter['model']); - return $parameter['type'] === self::TYPE_ARRAY ? 'Array[' . $modelType . ']' : $modelType; + + if (!empty($parameter['enumValues'])) { + return $this->toPascalCase($parameter['name']); } - return match ($parameter['type']) { + + // PRIMITIVES + return match ($parameter['type'] ?? '') { self::TYPE_INTEGER => 'int', self::TYPE_NUMBER => 'float', self::TYPE_STRING => 'String', - self::TYPE_FILE => $prefix . 'InputFile', self::TYPE_BOOLEAN => 'bool', self::TYPE_ARRAY => 'Array', self::TYPE_OBJECT => 'Dictionary', + self::TYPE_FILE => 'RefCounted', default => 'Variant', }; } @@ -256,4 +288,23 @@ public function getParamExample(array $param, string $lang = ''): string return $output; } + + public function getFilters(): array + { + return array_merge([ + new TwigFilter('caseEnumKey', function (string $value) { + return $this->toUpperSnakeCase($value); + }), + new TwigFilter('uniqueSnake', function (string $value) { + return $this->toUniqueSnake($value); + }), + ], parent::getFilters()); + } + + private function toUniqueSnake(string $value): string + { + $base = $this->toSnakeCase($value); + $name = $this->escapeKeyword($base); + return $name; + } } diff --git a/src/SDK/Language/Godot.php b/src/SDK/Language/Godot.php index 1dd43fc77d..4cb8e346a6 100644 --- a/src/SDK/Language/Godot.php +++ b/src/SDK/Language/Godot.php @@ -39,77 +39,82 @@ public function getFiles(): array ], [ 'scope' => 'default', - 'destination' => 'addons/{{ sdk.name | caseLower }}/plugin.cfg', + 'destination' => 'addons/{{ spec.title | caseSnake }}/plugin.cfg', 'template' => 'godot/src/plugin.cfg.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ sdk.name | caseLower }}/plugin.gd', + 'destination' => 'addons/{{ spec.title | caseSnake }}/plugin.gd', 'template' => 'godot/src/plugin.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ sdk.name | caseLower }}/{{ sdk.name | caseLower }}.gd', + 'destination' => 'addons/{{ spec.title | caseSnake }}/{{ spec.title | caseSnake }}.gd', 'template' => 'godot/src/appwrite.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ sdk.name | caseLower }}/icon.svg', + 'destination' => 'addons/{{ spec.title | caseSnake }}/icon.svg', 'template' => 'godot/src/icon.svg', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ sdk.name | caseLower }}/client.gd', + 'destination' => 'addons/{{ spec.title | caseSnake }}/client.gd', 'template' => 'godot/src/client.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ sdk.name | caseLower }}/service.gd', + 'destination' => 'addons/{{ spec.title | caseSnake }}/service.gd', 'template' => 'godot/src/service.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ sdk.name | caseLower }}/exception.gd', + 'destination' => 'addons/{{ spec.title | caseSnake }}/exception.gd', 'template' => 'godot/src/exception.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ sdk.name | caseLower }}/id.gd', + 'destination' => 'addons/{{ spec.title | caseSnake }}/id.gd', 'template' => 'godot/src/id.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ sdk.name | caseLower }}/permission.gd', + 'destination' => 'addons/{{ spec.title | caseSnake }}/permission.gd', 'template' => 'godot/src/permission.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ sdk.name | caseLower }}/role.gd', + 'destination' => 'addons/{{ spec.title | caseSnake }}/role.gd', 'template' => 'godot/src/role.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ sdk.name | caseLower }}/query.gd', + 'destination' => 'addons/{{ spec.title | caseSnake }}/query.gd', 'template' => 'godot/src/query.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ sdk.name | caseLower }}/input_file.gd', + 'destination' => 'addons/{{ spec.title | caseSnake }}/input_file.gd', 'template' => 'godot/src/input_file.gd.twig', ], + [ + 'scope' => 'default', + 'destination' => 'example.env', + 'template' => 'godot/example.env.twig' + ], [ 'scope' => 'enum', - 'destination' => 'addons/{{ sdk.name | caseLower }}/enums/{{ enum.name | caseSnake }}.gd', + 'destination' => 'addons/{{ spec.title | caseSnake }}/enums/{{ enum.name | caseSnake }}.gd', 'template' => 'godot/src/enums/enum.gd.twig', ], [ 'scope' => 'service', - 'destination' => 'addons/{{ sdk.name | caseLower }}/services/{{ service.name | caseSnake }}.gd', + 'destination' => 'addons/{{ spec.title | caseSnake }}/services/{{ service.name | caseSnake }}.gd', 'template' => 'godot/src/services/service.gd.twig', ], [ 'scope' => 'definition', - 'destination' => 'addons/{{ sdk.name | caseLower }}/models/{{ definition.name | caseSnake }}.gd', + 'destination' => 'addons/{{ spec.title | caseSnake }}/models/{{ definition.name | caseSnake }}.gd', 'template' => 'godot/src/models/model.gd.twig', ], [ diff --git a/templates/godot/README.md.twig b/templates/godot/README.md.twig index 21f0da2bc6..d0366f3af8 100644 --- a/templates/godot/README.md.twig +++ b/templates/godot/README.md.twig @@ -14,13 +14,13 @@ ## Installation -1. Copy the `addons/{{sdk.name | caseLower}}` folder into your Godot project's `res://addons/` directory. +1. Copy the `addons/{{spec.title | caseSnake}}` folder into your Godot project's `res://addons/` directory. 2. Enable the plugin in **Project Settings** > **Plugins**. -3. The SDK is now available as a global singleton named `{{sdk.name}}`. +3. The SDK is now available as a global singleton named `{{spec.title | caseUcfirst}}`. ## Getting Started -In Godot 4, the SDK uses asynchronous coroutines via the `await` keyword. You can use the global `{{sdk.name}}` singleton or initialize the `{{sdk.name}}Client` manually. +In Godot 4, the SDK uses asynchronous coroutines via the `await` keyword. You can use the global `{{spec.title | caseUcfirst}}` singleton or initialize the `{{spec.title | caseUcfirst}}Client` manually. ### Using the Autoload Singleton @@ -28,12 +28,12 @@ In Godot 4, the SDK uses asynchronous coroutines via the `await` keyword. You ca extends Node func _ready(): - # Initialize the global Appwrite singleton - {{sdk.name}}.set_endpoint('https://cloud.appwrite.io/v1') - {{sdk.name}}.set_project('YOUR_PROJECT_ID') + # Initialize the global {{spec.title | caseUcfirst}} singleton + {{spec.title | caseUcfirst}}.set_endpoint('https://cloud.appwrite.io/v1') + {{spec.title | caseUcfirst}}.set_project('YOUR_PROJECT_ID') # Call an API method asynchronously - var response = await {{sdk.name}}.account.get() + var response = await {{spec.title | caseUcfirst}}.account.get() if response == null: print("An error occurred") @@ -47,11 +47,11 @@ func _ready(): extends Node func _ready(): - var client = {{sdk.name}}Client.new() + var client = {{spec.title | caseUcfirst}}Client.new() client.set_endpoint('https://cloud.appwrite.io/v1') client.set_project('YOUR_PROJECT_ID') - var account = {{sdk.name}}Account.new(client) + var account = {{spec.title | caseUcfirst}}Account.new(client) var response = await account.get() ``` diff --git a/templates/godot/docs/example.md.twig b/templates/godot/docs/example.md.twig index d5143ecff4..3556176c17 100644 --- a/templates/godot/docs/example.md.twig +++ b/templates/godot/docs/example.md.twig @@ -4,6 +4,7 @@ func _ready(): # If the Appwrite Autoload is configured, you can directly use it Appwrite.set_endpoint("https://cloud.appwrite.io/v1") Appwrite.set_project("YOUR_PROJECT_ID") + Appwrite.set_key("YOUR_API_KEY") {% if method.type != 'webAuth' %} var result = await Appwrite.{{ service.name | caseCamel }}.{{ method.name | caseSnake }}( @@ -16,7 +17,7 @@ func _ready(): {% endfor %} ) - if result is {{ spec.title | caseUcfirst }}Exception: + if result is Appwrite.Exception: print("Error: ", result.message) else: print(result) \ No newline at end of file diff --git a/templates/godot/example.env.twig b/templates/godot/example.env.twig new file mode 100644 index 0000000000..18f22f56b4 --- /dev/null +++ b/templates/godot/example.env.twig @@ -0,0 +1,5 @@ +# {{ spec.title | caseUcfirst }} Configuration + +{{ spec.title | caseUpper }}_ENDPOINT=https://cloud.appwrite.io/v1 +{{ spec.title | caseUpper }}_PROJECT=your_project_id_here +{{ spec.title | caseUpper }}_KEY=your_api_key_here \ No newline at end of file diff --git a/templates/godot/src/appwrite.gd.twig b/templates/godot/src/appwrite.gd.twig index 1b923eb3c4..ab5ecb8b66 100644 --- a/templates/godot/src/appwrite.gd.twig +++ b/templates/godot/src/appwrite.gd.twig @@ -1,31 +1,76 @@ {% set prefix = spec.title | caseUcfirst %} extends Node -# Autoload singleton for {{spec.title}} +const ClientScript = preload("client.gd") +const IDScript = preload("id.gd") +const PermissionScript = preload("permission.gd") +const RoleScript = preload("role.gd") +const QueryScript = preload("query.gd") +const InputFileScript = preload("input_file.gd") +const ID = preload("id.gd") +const Permission = preload("permission.gd") +const Role = preload("role.gd") +const Query = preload("query.gd") +const InputFile = preload("input_file.gd") +const Exception = preload("exception.gd") -var client: {{prefix}}Client +{% for service in spec.services %} +const {{ service.name | caseUcfirst }}Script = preload("services/{{ service.name | caseSnake }}.gd") +{% endfor %} +var client: RefCounted {% for service in spec.services %} -var {{ service.name | caseCamel }}: {{prefix}}{{ service.name | caseUcfirst }} +var {{ service.name | caseCamel }}: RefCounted {% endfor %} func _ready() -> void: - client = {{prefix}}Client.new() - - # Attempt to load configuration from ProjectSettings - if ProjectSettings.has_setting("appwrite/config/endpoint"): - client.set_endpoint(ProjectSettings.get_setting("appwrite/config/endpoint")) - if ProjectSettings.has_setting("appwrite/config/project"): - client.set_project(ProjectSettings.get_setting("appwrite/config/project")) + client = ClientScript.new() + _load_env() {% for service in spec.services %} - {{ service.name | caseCamel }} = {{prefix}}{{ service.name | caseUcfirst }}.new(client) + {{ service.name | caseCamel }} = {{ service.name | caseUcfirst }}Script.new(client) {% endfor %} + +func _load_env() -> void: + var path = "res://.env" + if not FileAccess.file_exists(path): + return + + var file = FileAccess.open(path, FileAccess.READ) + while not file.eof_reached(): + var line = file.get_line().strip_edges() + if line.is_empty() or line.begins_with("#"): + continue + + var parts = line.split("=", true, 1) + if parts.size() != 2: + continue + + var key = parts[0].strip_edges() + var value = parts[1].strip_edges() + + # Remove quotes if present + if (value.begins_with("\"") and value.ends_with("\"")) or (value.begins_with("'") and value.ends_with("'")): + value = value.substr(1, value.length() - 2) + + match key: + "APPWRITE_ENDPOINT": + client.set_endpoint(value) + "APPWRITE_PROJECT": + client.set_project(value) + "APPWRITE_KEY": + client.set_key(value) + + func set_endpoint(endpoint: String) -> void: client.set_endpoint(endpoint) + func set_project(project: String) -> void: client.set_project(project) -# Add other global setters if needed + +func set_key(key: String) -> void: + client.set_key(key) + diff --git a/templates/godot/src/client.gd.twig b/templates/godot/src/client.gd.twig index 0c40d19aab..a040330740 100644 --- a/templates/godot/src/client.gd.twig +++ b/templates/godot/src/client.gd.twig @@ -1,7 +1,8 @@ -{% set prefix = spec.title | caseUcfirst %} -class_name {{prefix}}Client +# {{prefix}}Client extends RefCounted +const InputFile := preload("input_file.gd") + var _chunk_size = 5 * 1024 * 1024 var _self_signed = false var _endpoint = '{{spec.endpoint}}' @@ -17,31 +18,37 @@ var _global_headers = { {% endfor %} } -func set_self_signed(status: bool = true) -> {{prefix}}Client: +func set_self_signed(status: bool = true) -> RefCounted: _self_signed = status return self -func set_endpoint(endpoint: String) -> {{prefix}}Client: + +func set_endpoint(endpoint: String) -> RefCounted: _endpoint = endpoint return self -func add_header(key: String, value: String) -> {{prefix}}Client: + +func add_header(key: String, value: String) -> RefCounted: _global_headers[key.to_lower()] = value return self + func get_headers() -> Dictionary: return _global_headers.duplicate() + {% for header in spec.global.headers %} -func set_{{header.key | caseSnake}}(value: String) -> {{prefix}}Client: +func set_{{header.key | caseSnake}}(value: String) -> RefCounted: {% if header.description %} # {{header.description}} {% endif %} _global_headers['{{header.name|lower}}'] = value return self + {% endfor %} + func call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: var http := HTTPRequest.new() @@ -119,6 +126,9 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param return {"statusCode": response_code, "body": response_text} +const InputFileScript = preload("input_file.gd") + + func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: var body := PackedByteArray() for key in params: @@ -127,7 +137,7 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) - if value is {{prefix}}InputFile: + if value is InputFileScript: body.append_array(("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" % [key, value.filename]).to_utf8_buffer()) var c_type = value.content_type if value.content_type != "" else "application/octet-stream" body.append_array(("Content-Type: %s\r\n\r\n" % c_type).to_utf8_buffer()) @@ -146,3 +156,4 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: body.append_array(("--" + boundary + "--\r\n").to_utf8_buffer()) return body + diff --git a/templates/godot/src/enums/enum.gd.twig b/templates/godot/src/enums/enum.gd.twig index df24555460..2b537f61d5 100644 --- a/templates/godot/src/enums/enum.gd.twig +++ b/templates/godot/src/enums/enum.gd.twig @@ -1,6 +1,17 @@ -class_name {{spec.title | caseUcfirst}}{{ enum.name | caseUcfirst }} +# Enum: {{ enum.name }} {% for value in enum.enum %} {% set key = enum.keys is empty ? value : enum.keys[loop.index0] %} -const {{ key | caseSnake | upper | escapeKeyword }} = "{{ value }}" +const {{ key | caseEnumKey }} = "{{ value }}" {% endfor %} + + +static func is_valid(value: String) -> bool: + return value in values() + +static func values() -> Array: + return [ + {% for value in enum.enum %} + "{{ value }}", + {% endfor %} + ] \ No newline at end of file diff --git a/templates/godot/src/exception.gd.twig b/templates/godot/src/exception.gd.twig index da0826b20c..999be43a84 100644 --- a/templates/godot/src/exception.gd.twig +++ b/templates/godot/src/exception.gd.twig @@ -1,4 +1,4 @@ -class_name {{spec.title | caseUcfirst}}Exception extends RefCounted +# {{spec.title | caseUcfirst}}Exception var message: String var code: int diff --git a/templates/godot/src/id.gd.twig b/templates/godot/src/id.gd.twig index ae6c68cdd8..f8cb3d9f5a 100644 --- a/templates/godot/src/id.gd.twig +++ b/templates/godot/src/id.gd.twig @@ -1,4 +1,4 @@ -class_name {{spec.title | caseUcfirst}}ID extends RefCounted +# {{spec.title | caseUcfirst}}ID static func _hex_timestamp() -> String: var now := Time.get_unix_time_from_system() diff --git a/templates/godot/src/input_file.gd.twig b/templates/godot/src/input_file.gd.twig index 79e188b4c4..e8ba1e9b08 100644 --- a/templates/godot/src/input_file.gd.twig +++ b/templates/godot/src/input_file.gd.twig @@ -1,4 +1,4 @@ -class_name {{spec.title | caseUcfirst}}InputFile extends RefCounted +class_name {{spec.title | caseUcfirst}}InputFile var path: String var bytes: PackedByteArray diff --git a/templates/godot/src/models/model.gd.twig b/templates/godot/src/models/model.gd.twig index d99a068ae0..d44b44fbfe 100644 --- a/templates/godot/src/models/model.gd.twig +++ b/templates/godot/src/models/model.gd.twig @@ -1,40 +1,116 @@ {% set prefix = spec.title | caseUcfirst %} -class_name {{prefix}}{{ definition.name | caseUcfirst }} +class_name {{ prefix }}{{ definition.name | caseUcfirst }} extends RefCounted +{% set imports = [] %} +{%~ for property in definition.properties %} +{%~ set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (definition.name ~ (property.name | caseUcfirst)) %} +{%~ if property.enum or property.items.enum is defined %} +{%~ if enumName not in imports %} +const {{ enumName | caseUcfirst }} := preload("res://addons/{{ spec.title | caseSnake }}/enums/{{ enumName | caseSnake }}.gd") +{% set imports = imports|merge([enumName]) %} +{%~ endif %} +{%~ endif %} +{%~ if property.model and property.model not in imports %} +const {{ property.model | caseUcfirst }} := preload("res://addons/{{ spec.title | caseSnake }}/models/{{ property.model | caseSnake }}.gd") +{% set imports = imports|merge([property.model]) %} +{%~ endif %} +{%~ if property.array.model and property.array.model not in imports %} +const {{ property.array.model | caseUcfirst }} := preload("res://addons/{{ spec.title | caseSnake }}/models/{{ property.array.model | caseSnake }}.gd") +{% set imports = imports|merge([property.array.model]) %} +{%~ endif %} +{%~ endfor %} + +const FIELD_MAP := { {% for property in definition.properties %} -var {{ property.name | caseSnake }}: {{ property | typeName(spec) }} + "{{ property.name | uniqueSnake }}": "{{ property.name }}", {% endfor %} +} -static func from_dict(dict: Dictionary) -> {{prefix}}{{ definition.name | caseUcfirst }}: - var model = {{prefix}}{{ definition.name | caseUcfirst }}.new() {% for property in definition.properties %} - if dict.has('{{ property.name }}'): +{% set baseType = property | typeName(spec) %} +var {{ property.name | uniqueSnake }}: {{ baseType }} +{% endfor %} + +static func from_dict(dict: Dictionary): + var m := {{ prefix }}{{ definition.name | caseUcfirst }}.new() + + for key in FIELD_MAP: + var raw_key = FIELD_MAP[key] + var value = dict.get(raw_key) + +{% for property in definition.properties %} +{% set field = property.name | uniqueSnake %} +{% set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (definition.name ~ (property.name | caseUcfirst)) %} {% if property.model %} - model.{{ property.name | caseSnake }} = {{prefix}}{{ property.model | caseUcfirst }}.from_dict(dict['{{ property.name }}']) -{% elseif property.array.model %} - var {{ property.name | caseSnake }}_list: {{ property | typeName(spec) }} = [] - for item in dict['{{ property.name }}']: - {{ property.name | caseSnake }}_list.append({{prefix}}{{ property.array.model | caseUcfirst }}.from_dict(item)) - model.{{ property.name | caseSnake }} = {{ property.name | caseSnake }}_list -{% else %} - model.{{ property.name | caseSnake }} = dict['{{ property.name }}'] + if key == "{{ field }}" and value is Dictionary: + m.set(key, {{ property.model | caseUcfirst }}.from_dict(value)) + continue +{% endif %} +{% if property.array.model %} + if key == "{{ field }}" and value is Array: + var list := [] + for item in value: + if item is Dictionary: + list.append({{ property.array.model | caseUcfirst }}.from_dict(item)) + else: + list.append(item) + m.set(key, list) + continue +{% endif %} +{% if property.enum %} + if key == "{{ field }}" and value != null: + if not {{ enumName | caseUcfirst }}.is_valid(value): + push_error("Invalid enum value for {{ field }}: %s" % value) + m.set(key, value) + continue +{% endif %} +{% if property.type == 'array' and property.items.enum is defined %} + if key == "{{ field }}" and value is Array: + var list := [] + for item in value: + if not {{ enumName | caseUcfirst }}.is_valid(item): + push_error("Invalid enum value: %s" % item) + list.append(item) + m.set(key, list) + continue +{% endif %} +{% if property.type == 'array' and not property.array.model %} + if key == "{{ field }}" and value is Array: + m.set(key, value) + continue {% endif %} {% endfor %} - return model + + m.set(key, value) + + return m + func to_dict() -> Dictionary: - var dict = {} + var dict := {} + + for key in FIELD_MAP: + var value = get(key) + {% for property in definition.properties %} +{% set field = property.name | uniqueSnake %} {% if property.model %} - dict['{{ property.name }}'] = {{ property.name | caseSnake }}.to_dict() if {{ property.name | caseSnake }} else null -{% elseif property.array.model %} - var {{ property.name | caseSnake }}_list = [] - for item in {{ property.name | caseSnake }}: - {{ property.name | caseSnake }}_list.append(item.to_dict()) - dict['{{ property.name }}'] = {{ property.name | caseSnake }}_list -{% else %} - dict['{{ property.name }}'] = {{ property.name | caseSnake }} + if key == "{{ field }}" and value != null: + dict[FIELD_MAP[key]] = value.to_dict() + continue +{% endif %} +{% if property.array.model %} + if key == "{{ field }}" and value is Array: + var list := [] + for item in value: + if item != null: + list.append(item.to_dict()) + dict[FIELD_MAP[key]] = list + continue {% endif %} {% endfor %} - return dict + + dict[FIELD_MAP[key]] = value + + return dict \ No newline at end of file diff --git a/templates/godot/src/permission.gd.twig b/templates/godot/src/permission.gd.twig index 1dee6e9cf9..50f6b8d0f9 100644 --- a/templates/godot/src/permission.gd.twig +++ b/templates/godot/src/permission.gd.twig @@ -1,4 +1,4 @@ -class_name {{spec.title | caseUcfirst}}Permission extends RefCounted +# {{spec.title | caseUcfirst}}Permission static func read(role: String) -> String: return 'read("%s")' % role diff --git a/templates/godot/src/plugin.cfg.twig b/templates/godot/src/plugin.cfg.twig index cc5ea0095f..b5fc3fd1bf 100644 --- a/templates/godot/src/plugin.cfg.twig +++ b/templates/godot/src/plugin.cfg.twig @@ -1,7 +1,7 @@ [plugin] -name="{{sdk.name}}" -description="{{sdk.description}}" +name="{{ spec.title | caseUcfirst }}" +description="{{ spec.shortDescription }}" author="Appwrite Team" -version="{{sdk.version}}" +version="{{ spec.version }}" script="plugin.gd" diff --git a/templates/godot/src/plugin.gd.twig b/templates/godot/src/plugin.gd.twig index f6c5ca3fc6..0f8b33bf97 100644 --- a/templates/godot/src/plugin.gd.twig +++ b/templates/godot/src/plugin.gd.twig @@ -1,8 +1,13 @@ @tool extends EditorPlugin -func _enter_tree(): - add_autoload_singleton("{{sdk.name}}", "res://addons/{{sdk.name | caseLower}}/{{sdk.name | caseLower}}.gd") +const AUTOLOAD_NAME : String = "{{spec.title | caseUcfirst}}" +const AUTOLOAD_PATH : String = "res://addons/{{spec.title | caseSnake}}/{{spec.title | caseLower}}.gd" + +func _enable_plugin() -> void: + add_autoload_singleton(AUTOLOAD_NAME, AUTOLOAD_PATH) + + +func _disable_plugin() -> void: + remove_autoload_singleton(AUTOLOAD_NAME) -func _exit_tree(): - remove_autoload_singleton("{{sdk.name}}") diff --git a/templates/godot/src/query.gd.twig b/templates/godot/src/query.gd.twig index 4d6fdb8a29..d50a9133f2 100644 --- a/templates/godot/src/query.gd.twig +++ b/templates/godot/src/query.gd.twig @@ -1,4 +1,4 @@ -class_name {{spec.title | caseUcfirst}}Query extends RefCounted +class_name {{spec.title | caseUcfirst}}Query var method: String var attribute: Variant diff --git a/templates/godot/src/role.gd.twig b/templates/godot/src/role.gd.twig index 194c879883..3a15624cc3 100644 --- a/templates/godot/src/role.gd.twig +++ b/templates/godot/src/role.gd.twig @@ -1,4 +1,4 @@ -class_name {{spec.title | caseUcfirst}}Role extends RefCounted +# {{spec.title | caseUcfirst}}Role static func any() -> String: return 'any' diff --git a/templates/godot/src/service.gd.twig b/templates/godot/src/service.gd.twig index da2c941cf4..456ad70360 100644 --- a/templates/godot/src/service.gd.twig +++ b/templates/godot/src/service.gd.twig @@ -1,8 +1,47 @@ {% set prefix = spec.title | caseUcfirst %} -class_name {{prefix}}Service +# {{prefix}}Service + extends RefCounted -var client: {{prefix}}Client +const ExceptionScript = preload("exception.gd") + +var client: RefCounted -func _init(p_client: {{prefix}}Client) -> void: +func _init(p_client: RefCounted) -> void: client = p_client + +func _call(method: String, path: String, headers: Dictionary, params: Dictionary, model_script: Script = null) -> Variant: + var result = await client.call_api(method, path, headers, params) + + if result.statusCode == 0: + var error_msg = result.get("error", "Unknown error") + push_error("{{prefix}} Network Error: %s" % error_msg) + return ExceptionScript.new(error_msg, 0, "network_error", "") + + if result.statusCode >= 400: + var message = "" + var code = result.statusCode + var type = "" + var response = "" + if result.get("body") is Dictionary: + message = result.body.get("message", "") + code = result.body.get("code", result.statusCode) + type = result.body.get("type", "") + response = str(result.body) + else: + message = str(result.get("body", "")) + response = str(result.get("body", "")) + + push_error("{{prefix}} Error (%s): %s" % [type, message]) + return ExceptionScript.new(message, code, type, response) + + if model_script == null: + return result.get("body") + + if result.get("body") is Array: + var list: Array[RefCounted] = [] + for item in result.body: + list.append(model_script.from_dict(item)) + return list + + return model_script.from_dict(result.get("body", {})) diff --git a/templates/godot/src/services/service.gd.twig b/templates/godot/src/services/service.gd.twig index 542cc49d3f..6f9c4632be 100644 --- a/templates/godot/src/services/service.gd.twig +++ b/templates/godot/src/services/service.gd.twig @@ -1,60 +1,53 @@ {% set prefix = spec.title | caseUcfirst %} -class_name {{prefix}}{{ service.name | caseUcfirst }} -extends {{prefix}}Service +extends "../service.gd" + +// TODO: Import enums +const InputFile := preload("../input_file.gd") {% for method in service.methods %} -{% set return_type = method.responseModel | caseUcfirst %} -func {{ method.name | caseSnake }}({% for parameter in method.parameters.all %}{{ parameter.name | caseSnake }}: {{ parameter | typeName(spec) }}{{ parameter | paramDefault }}{% if not loop.last %}, {% endif %}{% endfor %}) -> Variant: - var path := '{{ method.path }}' -{% for parameter in method.parameters.path %} - path = path.replace('{{ '{' }}{{ parameter.name }}{{ '}' }}', str({{ parameter.name | caseSnake }})) -{% endfor %} - var params := { -{% for parameter in method.parameters.query %} - '{{ parameter.name }}': {{ parameter.name | caseSnake }}, -{% endfor %} -{% for parameter in method.parameters.body %} - '{{ parameter.name }}': {{ parameter.name | caseSnake }}, -{% endfor %} -{% for parameter in method.parameters.formData %} - '{{ parameter.name }}': {{ parameter.name | caseSnake }}, -{% endfor %} +{%~ set methodNameSnake = method.name | caseSnake %} +{%~ set shouldSkip = false %} +{%~ if method.deprecated %} +{%~ for otherMethod in service.methods %} +{%~ if not otherMethod.deprecated and (otherMethod.name | caseSnake) == methodNameSnake %} +{%~ set shouldSkip = true %} +{%~ endif %} +{%~ endfor %} +{%~ endif %} +{%~ if not shouldSkip %} +{%~ set return_type = method.responseModel | caseUcfirst %} + +func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters.all %}{{ parameter.name | caseSnake | escapeKeyword }}: {{ parameter | typeName(spec) }}{{ parameter | paramDefault }}{% if not loop.last %}, {% endif %}{% endfor %}) -> Variant: + var _path := '{{ method.path }}' + {%~ for parameter in method.parameters.path %} + _path = _path.replace('{{ '{' }}{{ parameter.name }}{{ '}' }}', str({{ parameter.name | caseSnake | escapeKeyword }})) + {%~ endfor %} + + var _params := { + {%~ for parameter in method.parameters.query %} + '{{ parameter.name }}': {{ parameter.name | caseSnake | escapeKeyword }}, + {%~ endfor %} + {%~ for parameter in method.parameters.body %} + '{{ parameter.name }}': {{ parameter.name | caseSnake | escapeKeyword }}, + {%~ endfor %} + {%~ for parameter in method.parameters.formData %} + '{{ parameter.name }}': {{ parameter.name | caseSnake | escapeKeyword }}, + {%~ endfor %} } - var headers := { -{% for key, value in method.headers %} + var _headers := { + {%~ for key, value in method.headers %} '{{ key }}': '{{ value }}', -{% endfor %} + {%~ endfor %} } - var result = await client.call_api('{{ method.method }}', path, headers, params) - - if result.statusCode >= 400: - var message = "" - var code = result.statusCode - var type = "" - var response = "" - if result.body is Dictionary: - message = result.body.get("message", "") - code = result.body.get("code", result.statusCode) - type = result.body.get("type", "") - response = str(result.body) - else: - message = str(result.body) - response = str(result.body) - - push_error("{{prefix}} Error (%s): %s" % [type, message]) - return {{prefix}}Exception.new(message, code, type, response) - -{% if method.responseModel and method.responseModel != 'any' %} - if result.body is Array: - var list: Array[{{prefix}}{{return_type}}] = [] - for item in result.body: - list.append({{prefix}}{{return_type}}.from_dict(item)) - return list - return {{prefix}}{{ return_type }}.from_dict(result.body) -{% else %} - return result.body -{% endif %} - -{% endfor %} + {%~ if method.responseModel and method.responseModel != 'any' %} + var model_script = {{prefix}}{{method.responseModel | caseUcfirst}} + {%~ else %} + var model_script = null + {%~ endif %} + + return await _call('{{ method.method }}', _path, _headers, _params, model_script) + +{%~ endif %} +{%~ endfor %} From 14ab4779eeab4eed06830374b007b83fb5c19b8e Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Sat, 2 May 2026 17:38:51 +0530 Subject: [PATCH 05/58] wip(godot): Correct SDK generated and Godot ping test pass - Fix enum handling to use String-based representation instead of invalid typed enums(Godot internal use int representation) - Resolve incorrect default value mapping for integer parameters (e.g. [] assigned to int in storage) - Improve parameter type inference in codegen for arrays, enums, and primitives - Fix Godot HTTPRequest lifecycle issue caused by calling request before node enters scene tree - Replace deferred node attachment pattern with proper add_child timing - Improve robustness of generated GDScript service methods and parameter defaults --- src/SDK/Language/GDScript.php | 136 ++++++++++++++----- templates/godot/src/client.gd.twig | 8 +- templates/godot/src/enums/enum.gd.twig | 3 +- templates/godot/src/models/model.gd.twig | 2 +- templates/godot/src/service.gd.twig | 2 +- templates/godot/src/services/service.gd.twig | 25 +++- 6 files changed, 134 insertions(+), 42 deletions(-) diff --git a/src/SDK/Language/GDScript.php b/src/SDK/Language/GDScript.php index ecf04aaf20..80bf7ee196 100644 --- a/src/SDK/Language/GDScript.php +++ b/src/SDK/Language/GDScript.php @@ -24,51 +24,50 @@ public function getKeywords(): array { return [ 'if', - 'else', 'elif', + 'else', 'for', - 'not', - 'and', - 'or', 'while', 'match', - 'master', + 'when', 'break', 'continue', + 'pass', 'return', - 'load', - 'func', 'class', 'class_name', 'extends', 'is', - 'in', 'as', + 'in', 'self', 'super', 'signal', - 'await', - 'var', + 'func', + 'static', 'const', 'enum', - 'static', + 'var', 'breakpoint', + 'preload', + 'await', + 'yield', 'assert', + 'void', + 'PI', + 'TAU', + 'INF', + 'NAN', + 'and', + 'or', + 'not', + 'master', '@export', '@onready', - 'pass', - 'preload', 'puppet', 'remote', 'remotesync', - 'static', '@tool', - 'void', - 'PI', - 'TAU', - 'INF', - 'NAN', - // NODE specific keywords '_ready', 'name', 'type', @@ -79,6 +78,67 @@ public function getKeywords(): array '_exit_tree', 'get', 'set', + 'Theme', + 'Button', + 'LineEdit', + 'TextureRect', + 'Label', + 'Control', + 'print', + 'prints', + 'printerr', + 'push_error', + 'push_warning', + 'len', + 'range', + 'load', + 'type_exists', + 'is_instance_valid', + 'randf', + 'randi', + 'randomize', + 'clamp', + 'lerp', + 'free', + 'call', + 'callv', + 'call_deferred', + '@export_range', + 'connect', + 'disconnect', + 'emit_signal', + 'has_method', + 'has_signal', + 'get_meta', + 'set_meta', + 'has_meta', + 'to_string', + 'is_inside_tree', + '_enter_tree', + '_unhandled_key_input', + '_notification', + 'add_child', + 'remove_child', + 'get_node', + 'get_parent', + 'queue_free', + 'duplicate', + 'get_tree', + 'Object', + 'RefCounted', + 'Node', + 'Node2D', + 'Node3D', + 'Callable', + 'Vector2', + 'Vector3', + 'Vector4', + 'Color', + 'Transform2D', + 'Transform3D', + 'RID', + '@on_ready' + // TODO: add more ]; } @@ -130,11 +190,11 @@ public function getTypeName(array $parameter, array $spec = []): string // Array of enums if (isset($parameter['enumName'])) { - return 'Array[' . $this->toPascalCase($parameter['enumName']) . ']'; + return 'Array[String]'; } if (!empty($parameter['enumValues'])) { - return 'Array[' . $this->toPascalCase($parameter['name']) . ']'; + return 'Array[String]'; } return 'Array'; @@ -147,11 +207,11 @@ public function getTypeName(array $parameter, array $spec = []): string // ENUM TYPE if (isset($parameter['enumName'])) { - return $this->toPascalCase($parameter['enumName']); + return 'String'; } if (!empty($parameter['enumValues'])) { - return $this->toPascalCase($parameter['name']); + return 'String'; } // PRIMITIVES @@ -186,21 +246,20 @@ public function getParamDefault(array $param): string if (empty($default) && $default !== 0 && $default !== false) { switch ($type) { case self::TYPE_NUMBER: - $output .= '0.0'; - break; case self::TYPE_INTEGER: - $output .= '0'; + $output .= '0.0'; break; case self::TYPE_BOOLEAN: $output .= 'false'; break; case self::TYPE_STRING: - $output .= '""'; + $output .= "''"; break; case self::TYPE_ARRAY: $output .= '[]'; break; case self::TYPE_OBJECT: + case self::TYPE_FILE: $output .= '{}'; break; default: @@ -210,18 +269,29 @@ public function getParamDefault(array $param): string } else { switch ($type) { case self::TYPE_NUMBER: + if (is_array($default) || $default === '[]') { + $default = '0.0'; + } + $output .= $default; + break; case self::TYPE_INTEGER: + if (is_array($default) || $default === '[]') { + $default = '0'; + } + $output .= $default; + break; case self::TYPE_ARRAY: + case self::TYPE_OBJECT: $output .= $default; break; + case self::TYPE_FILE: + $output .= '{}'; + break; case self::TYPE_BOOLEAN: $output .= ($default) ? 'true' : 'false'; break; case self::TYPE_STRING: - $output .= "\"{$default}\""; - break; - case self::TYPE_OBJECT: - $output .= $default; // Should be formatted as GDScript dictionary + $output .= "'{$default}'"; break; default: $output .= 'null'; @@ -278,7 +348,7 @@ public function getParamExample(array $param, string $lang = ''): string $output .= ($example) ? 'true' : 'false'; break; case self::TYPE_STRING: - $output .= "\"{$example}\""; + $output .= "'{$example}'"; break; case self::TYPE_FILE: $output .= 'FileAccess.open("file.png", FileAccess.READ)'; diff --git a/templates/godot/src/client.gd.twig b/templates/godot/src/client.gd.twig index a040330740..b4819cf258 100644 --- a/templates/godot/src/client.gd.twig +++ b/templates/godot/src/client.gd.twig @@ -55,7 +55,8 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param var tls_options := TLSOptions.client_unsafe() if _self_signed else TLSOptions.client() http.set_tls_options(tls_options) - Engine.get_main_loop().root.add_child(http) + Engine.get_main_loop().root.add_child.call_deferred(http) + await http.tree_entered # Merge headers var combined_headers := _global_headers.duplicate() @@ -157,3 +158,8 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: body.append_array(("--" + boundary + "--\r\n").to_utf8_buffer()) return body + +func ping() -> String: + var response = await call_api('GET', '/ping') + return str(response.get('body')) + diff --git a/templates/godot/src/enums/enum.gd.twig b/templates/godot/src/enums/enum.gd.twig index 2b537f61d5..a1e29b7ed7 100644 --- a/templates/godot/src/enums/enum.gd.twig +++ b/templates/godot/src/enums/enum.gd.twig @@ -9,7 +9,8 @@ const {{ key | caseEnumKey }} = "{{ value }}" static func is_valid(value: String) -> bool: return value in values() -static func values() -> Array: + +static func values() -> Array[String]: return [ {% for value in enum.enum %} "{{ value }}", diff --git a/templates/godot/src/models/model.gd.twig b/templates/godot/src/models/model.gd.twig index d44b44fbfe..ced60e0e99 100644 --- a/templates/godot/src/models/model.gd.twig +++ b/templates/godot/src/models/model.gd.twig @@ -7,7 +7,7 @@ extends RefCounted {%~ set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (definition.name ~ (property.name | caseUcfirst)) %} {%~ if property.enum or property.items.enum is defined %} {%~ if enumName not in imports %} -const {{ enumName | caseUcfirst }} := preload("res://addons/{{ spec.title | caseSnake }}/enums/{{ enumName | caseSnake }}.gd") +const {{ enumName | caseUcfirst | escapeKeyword }} := preload("res://addons/{{ spec.title | caseSnake }}/enums/{{ enumName | caseSnake }}.gd") {% set imports = imports|merge([enumName]) %} {%~ endif %} {%~ endif %} diff --git a/templates/godot/src/service.gd.twig b/templates/godot/src/service.gd.twig index 456ad70360..ea0b9db038 100644 --- a/templates/godot/src/service.gd.twig +++ b/templates/godot/src/service.gd.twig @@ -10,7 +10,7 @@ var client: RefCounted func _init(p_client: RefCounted) -> void: client = p_client -func _call(method: String, path: String, headers: Dictionary, params: Dictionary, model_script: Script = null) -> Variant: +func _call(method: String, path: String, headers: Dictionary, params: Dictionary, model_script: Variant = null) -> Variant: var result = await client.call_api(method, path, headers, params) if result.statusCode == 0: diff --git a/templates/godot/src/services/service.gd.twig b/templates/godot/src/services/service.gd.twig index 6f9c4632be..0ca2eb4bac 100644 --- a/templates/godot/src/services/service.gd.twig +++ b/templates/godot/src/services/service.gd.twig @@ -1,7 +1,18 @@ {% set prefix = spec.title | caseUcfirst %} extends "../service.gd" -// TODO: Import enums +{% set imports = [] %} +{% for method in service.methods %} +{% for parameter in method.parameters.all %} +{% if parameter.enumValues is not empty %} +{% if parameter.enumName not in imports %} +const {{ parameter.enumName | caseUcfirst | escapeKeyword }} := preload("../enums/{{ parameter.enumName | caseSnake }}.gd") +{% set imports = imports|merge([parameter.enumName]) %} +{% endif %} +{% endif %} +{% endfor %} +{% endfor %} + const InputFile := preload("../input_file.gd") {% for method in service.methods %} @@ -15,9 +26,13 @@ const InputFile := preload("../input_file.gd") {%~ endfor %} {%~ endif %} {%~ if not shouldSkip %} -{%~ set return_type = method.responseModel | caseUcfirst %} +{%~ if method.responseModel and method.responseModel != 'any' %} +{%~ set return_type = (spec.title | caseUcfirst) ~ (method.responseModel | caseUcfirst) %} +{%~ else %} +{%~ set return_type = 'Variant' %} +{%~ endif %} -func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters.all %}{{ parameter.name | caseSnake | escapeKeyword }}: {{ parameter | typeName(spec) }}{{ parameter | paramDefault }}{% if not loop.last %}, {% endif %}{% endfor %}) -> Variant: +func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters.all %}{{ parameter.name | caseSnake | escapeKeyword }}: {{ parameter | typeName(spec) }}{{ parameter | paramDefault }}{% if not loop.last %}, {% endif %}{% endfor %}) -> {{ return_type }}: var _path := '{{ method.path }}' {%~ for parameter in method.parameters.path %} _path = _path.replace('{{ '{' }}{{ parameter.name }}{{ '}' }}', str({{ parameter.name | caseSnake | escapeKeyword }})) @@ -41,8 +56,8 @@ func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters. {%~ endfor %} } - {%~ if method.responseModel and method.responseModel != 'any' %} - var model_script = {{prefix}}{{method.responseModel | caseUcfirst}} + {%~ if return_type != 'Variant' %} + var model_script = {{ return_type }} {%~ else %} var model_script = null {%~ endif %} From 5089686057c86dfb9807d4521e651d247bc21f1a Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Sat, 2 May 2026 23:05:19 +0530 Subject: [PATCH 06/58] feat(client): improve client abstraction, env config, and singleton API - refactor Appwrite singleton to delegate all config to client - centralize environment variable handling with _apply_env() - add dynamic header setters using spec.global.headers - support additional env configs (JWT, session, locale, mode, self-signed) - enforce single client instance via autoload (singleton pattern) - cleared Appwrite singleton namespace by removing raw classes --- templates/godot/src/appwrite.gd.twig | 62 +++++++++++++++++----------- templates/godot/src/client.gd.twig | 20 ++------- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/templates/godot/src/appwrite.gd.twig b/templates/godot/src/appwrite.gd.twig index ab5ecb8b66..05c17e89fc 100644 --- a/templates/godot/src/appwrite.gd.twig +++ b/templates/godot/src/appwrite.gd.twig @@ -1,12 +1,6 @@ {% set prefix = spec.title | caseUcfirst %} extends Node -const ClientScript = preload("client.gd") -const IDScript = preload("id.gd") -const PermissionScript = preload("permission.gd") -const RoleScript = preload("role.gd") -const QueryScript = preload("query.gd") -const InputFileScript = preload("input_file.gd") const ID = preload("id.gd") const Permission = preload("permission.gd") const Role = preload("role.gd") @@ -14,21 +8,17 @@ const Query = preload("query.gd") const InputFile = preload("input_file.gd") const Exception = preload("exception.gd") -{% for service in spec.services %} -const {{ service.name | caseUcfirst }}Script = preload("services/{{ service.name | caseSnake }}.gd") -{% endfor %} - var client: RefCounted {% for service in spec.services %} var {{ service.name | caseCamel }}: RefCounted {% endfor %} func _ready() -> void: - client = ClientScript.new() + client = preload("client.gd").new() _load_env() - + {% for service in spec.services %} - {{ service.name | caseCamel }} = {{ service.name | caseUcfirst }}Script.new(client) + {{ service.name | caseCamel }} = preload("services/{{ service.name | caseSnake }}.gd").new(client) {% endfor %} @@ -54,23 +44,47 @@ func _load_env() -> void: if (value.begins_with("\"") and value.ends_with("\"")) or (value.begins_with("'") and value.ends_with("'")): value = value.substr(1, value.length() - 2) - match key: - "APPWRITE_ENDPOINT": - client.set_endpoint(value) - "APPWRITE_PROJECT": - client.set_project(value) - "APPWRITE_KEY": - client.set_key(value) + _apply_env(key, value) + + +{% for header in spec.global.headers %} +{% if header.description %} +# {{header.description}} +{% endif %} +func set_{{header.key | caseSnake}}(value: String) -> void: + client.set_{{header.key | caseSnake}}(value) + + +{% endfor %} +func set_self_signed(status: bool = true) -> void: + client.set_self_signed(status) func set_endpoint(endpoint: String) -> void: client.set_endpoint(endpoint) -func set_project(project: String) -> void: - client.set_project(project) +func add_header(key: String, value: String) -> void: + client.add_header(key, value) + +func get_headers() -> Dictionary: + return client.get_headers() -func set_key(key: String) -> void: - client.set_key(key) +func _apply_env(key: String, value: String) -> void: + match key: + "{{ prefix | caseUpper }}_ENDPOINT": + client.set_endpoint(value) + "{{ prefix | caseUpper }}_SELF_SIGNED": + client.set_self_signed(value.to_lower() == "true") +{% for header in spec.global.headers %} + "{{ prefix | caseUpper }}_{{ header.key | caseUpper }}": + client.set_{{ header.key | caseSnake }}(value) +{% endfor %} + + +# Used to register the platform on Appwrite +func ping() -> String: + var response = await client.call_api('GET', '/ping') + return str(response.get('body')) \ No newline at end of file diff --git a/templates/godot/src/client.gd.twig b/templates/godot/src/client.gd.twig index b4819cf258..2e25d5fdd2 100644 --- a/templates/godot/src/client.gd.twig +++ b/templates/godot/src/client.gd.twig @@ -39,9 +39,6 @@ func get_headers() -> Dictionary: {% for header in spec.global.headers %} func set_{{header.key | caseSnake}}(value: String) -> RefCounted: -{% if header.description %} - # {{header.description}} -{% endif %} _global_headers['{{header.name|lower}}'] = value return self @@ -56,7 +53,7 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param http.set_tls_options(tls_options) Engine.get_main_loop().root.add_child.call_deferred(http) - await http.tree_entered + await http.tree_entered # Merge headers var combined_headers := _global_headers.duplicate() @@ -83,7 +80,7 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param else: var has_files := false for key in params: - if params[key] is {{prefix}}InputFile: + if params[key] is InputFile: has_files = true break @@ -127,9 +124,6 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param return {"statusCode": response_code, "body": response_text} -const InputFileScript = preload("input_file.gd") - - func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: var body := PackedByteArray() for key in params: @@ -138,7 +132,7 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) - if value is InputFileScript: + if value is InputFile: body.append_array(("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" % [key, value.filename]).to_utf8_buffer()) var c_type = value.content_type if value.content_type != "" else "application/octet-stream" body.append_array(("Content-Type: %s\r\n\r\n" % c_type).to_utf8_buffer()) @@ -156,10 +150,4 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: body.append_array(("\r\n").to_utf8_buffer()) body.append_array(("--" + boundary + "--\r\n").to_utf8_buffer()) - return body - - -func ping() -> String: - var response = await call_api('GET', '/ping') - return str(response.get('body')) - + return body \ No newline at end of file From de754be52434b477cf1af63b457a447d37bbd796 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Sun, 3 May 2026 00:08:14 +0530 Subject: [PATCH 07/58] docs(example): add type example and proper function calling --- templates/godot/docs/example.md.twig | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/templates/godot/docs/example.md.twig b/templates/godot/docs/example.md.twig index 3556176c17..186d38e22b 100644 --- a/templates/godot/docs/example.md.twig +++ b/templates/godot/docs/example.md.twig @@ -1,15 +1,17 @@ +{% set prefix = spec.title | caseUcfirst %} extends Node +{% if method.responseModel and method.responseModel != 'any' %} +{% set return_type = prefix ~ (method.responseModel | caseUcfirst) %} +{% else %} +{% set return_type = 'Variant' %} +{% endif -%} + func _ready(): - # If the Appwrite Autoload is configured, you can directly use it - Appwrite.set_endpoint("https://cloud.appwrite.io/v1") - Appwrite.set_project("YOUR_PROJECT_ID") - Appwrite.set_key("YOUR_API_KEY") - {% if method.type != 'webAuth' %} - var result = await Appwrite.{{ service.name | caseCamel }}.{{ method.name | caseSnake }}( + var result = await Appwrite.{{ service.name | caseCamel }}.{{ method.name | caseSnake | escapeKeyword }}( {% else %} - var result = Appwrite.{{ service.name | caseCamel }}.{{ method.name | caseSnake }}( + var result = Appwrite.{{ service.name | caseCamel }}.{{ method.name | caseSnake | escapeKeyword }}( {% endif %} {% for parameter in method.parameters.all %} {{ parameter | paramExample }}{% if not loop.last %},{% endif %}{% if not parameter.required %} # optional{% endif %} @@ -18,6 +20,7 @@ func _ready(): ) if result is Appwrite.Exception: - print("Error: ", result.message) - else: - print(result) \ No newline at end of file + push_error(result.message) + + if result is {{return_type}}: + print(result.to_dict()) \ No newline at end of file From 1a5ba1bb16d227aa8058dd3a2459b51cb391259f Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Sun, 3 May 2026 16:09:28 +0530 Subject: [PATCH 08/58] refactor(service): return Variant from API calls to handle errors safely Changed typed return values to Variant so errors can be returned to the SDK user instead of causing runtime failures in services. --- templates/godot/src/services/service.gd.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/godot/src/services/service.gd.twig b/templates/godot/src/services/service.gd.twig index 0ca2eb4bac..cc6db4ab47 100644 --- a/templates/godot/src/services/service.gd.twig +++ b/templates/godot/src/services/service.gd.twig @@ -32,7 +32,7 @@ const InputFile := preload("../input_file.gd") {%~ set return_type = 'Variant' %} {%~ endif %} -func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters.all %}{{ parameter.name | caseSnake | escapeKeyword }}: {{ parameter | typeName(spec) }}{{ parameter | paramDefault }}{% if not loop.last %}, {% endif %}{% endfor %}) -> {{ return_type }}: +func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters.all %}{{ parameter.name | caseSnake | escapeKeyword }}: {{ parameter | typeName(spec) }}{{ parameter | paramDefault }}{% if not loop.last %}, {% endif %}{% endfor %}) -> Variant : var _path := '{{ method.path }}' {%~ for parameter in method.parameters.path %} _path = _path.replace('{{ '{' }}{{ parameter.name }}{{ '}' }}', str({{ parameter.name | caseSnake | escapeKeyword }})) From 824aaf48d0453d490cef15ceed4a8a3233638dac Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Sun, 3 May 2026 17:12:48 +0530 Subject: [PATCH 09/58] docs(service): add godot-specific method documentation - Generate inline documentation for all service methods - Add parameter description using [param] annotation - Include return type details helping in better type casting - Automatically annotate deprecated APIs with @deprecated - Format docs using Godot BBCode for editor integration --- templates/godot/src/services/service.gd.twig | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/templates/godot/src/services/service.gd.twig b/templates/godot/src/services/service.gd.twig index cc6db4ab47..b3d9b07480 100644 --- a/templates/godot/src/services/service.gd.twig +++ b/templates/godot/src/services/service.gd.twig @@ -32,6 +32,24 @@ const InputFile := preload("../input_file.gd") {%~ set return_type = 'Variant' %} {%~ endif %} +## {{ method.summary ?? method.description | replace({"\n": "[br]\n##"}) }}[br] +##[br] +{% if method.deprecated %} +## @deprecated{% if method.since %}: Since {{ method.since }}{% endif %}{% if method.replaceWith %} Use [method {{ method.replaceWith | caseSnake | escapeKeyword }}] instead.{% endif %}[br] +##[br] +{% endif %} +{% if method.parameters.all|length > 0 %} +## Parameters:[br] +{% for parameter in method.parameters.all %} +## - [param {{ parameter.name | caseSnake | escapeKeyword }}]: {{ parameter.description | default("No description provided.") | replace({"\n": "\n## "}) }}[br] +{% endfor %} +##[br] +{% endif %} +## Returns:[br] +## - [{{return_type}}] on success.[br] +##[br] +## Errors:[br] +## - Returns error data as [member {{spec.title | caseUcfirst}}.Exception]. func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters.all %}{{ parameter.name | caseSnake | escapeKeyword }}: {{ parameter | typeName(spec) }}{{ parameter | paramDefault }}{% if not loop.last %}, {% endif %}{% endfor %}) -> Variant : var _path := '{{ method.path }}' {%~ for parameter in method.parameters.path %} From 7969fed6477ab7bbfb55676686844ac9e71dcb31 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Sun, 3 May 2026 17:51:23 +0530 Subject: [PATCH 10/58] fix(model): hide the internal FIELD_MAP from godot generated documentation --- templates/godot/src/models/model.gd.twig | 28 +++++++++--------------- 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/templates/godot/src/models/model.gd.twig b/templates/godot/src/models/model.gd.twig index ced60e0e99..5e3a8657be 100644 --- a/templates/godot/src/models/model.gd.twig +++ b/templates/godot/src/models/model.gd.twig @@ -7,21 +7,13 @@ extends RefCounted {%~ set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (definition.name ~ (property.name | caseUcfirst)) %} {%~ if property.enum or property.items.enum is defined %} {%~ if enumName not in imports %} -const {{ enumName | caseUcfirst | escapeKeyword }} := preload("res://addons/{{ spec.title | caseSnake }}/enums/{{ enumName | caseSnake }}.gd") +const _{{ enumName | caseUcfirst }} := preload("res://addons/{{ spec.title | caseSnake }}/enums/{{ enumName | caseSnake }}.gd") {% set imports = imports|merge([enumName]) %} {%~ endif %} {%~ endif %} -{%~ if property.model and property.model not in imports %} -const {{ property.model | caseUcfirst }} := preload("res://addons/{{ spec.title | caseSnake }}/models/{{ property.model | caseSnake }}.gd") -{% set imports = imports|merge([property.model]) %} -{%~ endif %} -{%~ if property.array.model and property.array.model not in imports %} -const {{ property.array.model | caseUcfirst }} := preload("res://addons/{{ spec.title | caseSnake }}/models/{{ property.array.model | caseSnake }}.gd") -{% set imports = imports|merge([property.array.model]) %} -{%~ endif %} {%~ endfor %} -const FIELD_MAP := { +const _FIELD_MAP := { {% for property in definition.properties %} "{{ property.name | uniqueSnake }}": "{{ property.name }}", {% endfor %} @@ -35,8 +27,8 @@ var {{ property.name | uniqueSnake }}: {{ baseType }} static func from_dict(dict: Dictionary): var m := {{ prefix }}{{ definition.name | caseUcfirst }}.new() - for key in FIELD_MAP: - var raw_key = FIELD_MAP[key] + for key in _FIELD_MAP: + var raw_key = _FIELD_MAP[key] var value = dict.get(raw_key) {% for property in definition.properties %} @@ -60,7 +52,7 @@ static func from_dict(dict: Dictionary): {% endif %} {% if property.enum %} if key == "{{ field }}" and value != null: - if not {{ enumName | caseUcfirst }}.is_valid(value): + if not _{{ enumName | caseUcfirst }}.is_valid(value): push_error("Invalid enum value for {{ field }}: %s" % value) m.set(key, value) continue @@ -69,7 +61,7 @@ static func from_dict(dict: Dictionary): if key == "{{ field }}" and value is Array: var list := [] for item in value: - if not {{ enumName | caseUcfirst }}.is_valid(item): + if not _{{ enumName | caseUcfirst }}.is_valid(item): push_error("Invalid enum value: %s" % item) list.append(item) m.set(key, list) @@ -90,14 +82,14 @@ static func from_dict(dict: Dictionary): func to_dict() -> Dictionary: var dict := {} - for key in FIELD_MAP: + for key in _FIELD_MAP: var value = get(key) {% for property in definition.properties %} {% set field = property.name | uniqueSnake %} {% if property.model %} if key == "{{ field }}" and value != null: - dict[FIELD_MAP[key]] = value.to_dict() + dict[_FIELD_MAP[key]] = value.to_dict() continue {% endif %} {% if property.array.model %} @@ -106,11 +98,11 @@ func to_dict() -> Dictionary: for item in value: if item != null: list.append(item.to_dict()) - dict[FIELD_MAP[key]] = list + dict[_FIELD_MAP[key]] = list continue {% endif %} {% endfor %} - dict[FIELD_MAP[key]] = value + dict[_FIELD_MAP[key]] = value return dict \ No newline at end of file From 2291cb2ae69e0f9afc2bc0d10ebcdde6b457f0a8 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Sun, 3 May 2026 20:31:05 +0530 Subject: [PATCH 11/58] docs: minor documentation changes across SDK --- templates/godot/docs/example.md.twig | 6 +++++- templates/godot/example.env.twig | 3 +-- templates/godot/src/appwrite.gd.twig | 10 +++++++--- templates/godot/src/client.gd.twig | 4 ++-- templates/godot/src/enums/enum.gd.twig | 3 ++- templates/godot/src/exception.gd.twig | 8 ++++---- templates/godot/src/id.gd.twig | 3 +++ templates/godot/src/input_file.gd.twig | 15 +++++++++++---- templates/godot/src/models/model.gd.twig | 5 ++++- templates/godot/src/permission.gd.twig | 5 +++++ templates/godot/src/role.gd.twig | 12 ++++++++++++ templates/godot/src/service.gd.twig | 2 +- 12 files changed, 57 insertions(+), 19 deletions(-) diff --git a/templates/godot/docs/example.md.twig b/templates/godot/docs/example.md.twig index 186d38e22b..7866656644 100644 --- a/templates/godot/docs/example.md.twig +++ b/templates/godot/docs/example.md.twig @@ -1,3 +1,6 @@ +{% if method.description %}{{ method.description }}{% endif %} + +```gdscript {% set prefix = spec.title | caseUcfirst %} extends Node @@ -23,4 +26,5 @@ func _ready(): push_error(result.message) if result is {{return_type}}: - print(result.to_dict()) \ No newline at end of file + print(result.to_dict()) +``` \ No newline at end of file diff --git a/templates/godot/example.env.twig b/templates/godot/example.env.twig index 18f22f56b4..041f2672d3 100644 --- a/templates/godot/example.env.twig +++ b/templates/godot/example.env.twig @@ -1,5 +1,4 @@ # {{ spec.title | caseUcfirst }} Configuration {{ spec.title | caseUpper }}_ENDPOINT=https://cloud.appwrite.io/v1 -{{ spec.title | caseUpper }}_PROJECT=your_project_id_here -{{ spec.title | caseUpper }}_KEY=your_api_key_here \ No newline at end of file +{{ spec.title | caseUpper }}_PROJECT=your_project_id_here \ No newline at end of file diff --git a/templates/godot/src/appwrite.gd.twig b/templates/godot/src/appwrite.gd.twig index 05c17e89fc..10c23e9e9e 100644 --- a/templates/godot/src/appwrite.gd.twig +++ b/templates/godot/src/appwrite.gd.twig @@ -49,25 +49,29 @@ func _load_env() -> void: {% for header in spec.global.headers %} {% if header.description %} -# {{header.description}} +## {{header.description}} {% endif %} func set_{{header.key | caseSnake}}(value: String) -> void: client.set_{{header.key | caseSnake}}(value) {% endfor %} +## Set self signed status func set_self_signed(status: bool = true) -> void: client.set_self_signed(status) +## Set the endpoint func set_endpoint(endpoint: String) -> void: client.set_endpoint(endpoint) +## Add a header func add_header(key: String, value: String) -> void: client.add_header(key, value) +## Get all headers func get_headers() -> Dictionary: return client.get_headers() @@ -84,7 +88,7 @@ func _apply_env(key: String, value: String) -> void: {% endfor %} -# Used to register the platform on Appwrite +## Ping {{prefix}} Server for testing connection func ping() -> String: - var response = await client.call_api('GET', '/ping') + var response = await client._call_api('GET', '/ping') return str(response.get('body')) \ No newline at end of file diff --git a/templates/godot/src/client.gd.twig b/templates/godot/src/client.gd.twig index 2e25d5fdd2..c29899bce5 100644 --- a/templates/godot/src/client.gd.twig +++ b/templates/godot/src/client.gd.twig @@ -1,4 +1,4 @@ -# {{prefix}}Client +# {{prefix | caseUcfirst}}Client extends RefCounted const InputFile := preload("input_file.gd") @@ -46,7 +46,7 @@ func set_{{header.key | caseSnake}}(value: String) -> RefCounted: {% endfor %} -func call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: +func _call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: var http := HTTPRequest.new() var tls_options := TLSOptions.client_unsafe() if _self_signed else TLSOptions.client() diff --git a/templates/godot/src/enums/enum.gd.twig b/templates/godot/src/enums/enum.gd.twig index a1e29b7ed7..d5be49bbc6 100644 --- a/templates/godot/src/enums/enum.gd.twig +++ b/templates/godot/src/enums/enum.gd.twig @@ -5,11 +5,12 @@ const {{ key | caseEnumKey }} = "{{ value }}" {% endfor %} - +## Validate if value is in enum static func is_valid(value: String) -> bool: return value in values() +## Get all values of enum static func values() -> Array[String]: return [ {% for value in enum.enum %} diff --git a/templates/godot/src/exception.gd.twig b/templates/godot/src/exception.gd.twig index 999be43a84..30c623518e 100644 --- a/templates/godot/src/exception.gd.twig +++ b/templates/godot/src/exception.gd.twig @@ -1,9 +1,9 @@ # {{spec.title | caseUcfirst}}Exception -var message: String -var code: int -var type: String -var response: String +var message: String ## Error message +var code: int ## Error code +var type: String ## Error type +var response: String ## Full error response func _init(p_message: String = "", p_code: int = 0, p_type: String = "", p_response: String = "") -> void: self.message = p_message diff --git a/templates/godot/src/id.gd.twig b/templates/godot/src/id.gd.twig index f8cb3d9f5a..61a5118162 100644 --- a/templates/godot/src/id.gd.twig +++ b/templates/godot/src/id.gd.twig @@ -8,6 +8,8 @@ static func _hex_timestamp() -> String: var usec_hex := "%05x" % usec return sec_hex + usec_hex + +## Generate a unique ID with padding to have a longer ID static func unique(padding: int = 7) -> String: var id := _hex_timestamp() if padding > 0: @@ -17,5 +19,6 @@ static func unique(padding: int = 7) -> String: id += sb return id + static func custom(id: String) -> String: return id diff --git a/templates/godot/src/input_file.gd.twig b/templates/godot/src/input_file.gd.twig index e8ba1e9b08..ad3dfdf461 100644 --- a/templates/godot/src/input_file.gd.twig +++ b/templates/godot/src/input_file.gd.twig @@ -1,9 +1,10 @@ +## Input File class used to pass files to the SDK class_name {{spec.title | caseUcfirst}}InputFile -var path: String -var bytes: PackedByteArray -var filename: String -var content_type: String +var path: String ## Path to the file +var bytes: PackedByteArray ## File bytes +var filename: String ## File name +var content_type: String ## File content type func _init(p_path: String = "", p_bytes: PackedByteArray = PackedByteArray(), p_filename: String = "", p_content_type: String = "") -> void: self.path = p_path @@ -14,12 +15,18 @@ func _init(p_path: String = "", p_bytes: PackedByteArray = PackedByteArray(), p_ if self.filename == "" and self.path != "": self.filename = self.path.get_file() + +## Creates a new InputFile from a file path static func from_path(p_path: String, p_filename: String = "", p_content_type: String = "") -> {{spec.title | caseUcfirst}}InputFile: return {{spec.title | caseUcfirst}}InputFile.new(p_path, PackedByteArray(), p_filename, p_content_type) + +## Creates a new InputFile from bytes static func from_bytes(p_bytes: PackedByteArray, p_filename: String, p_content_type: String = "") -> {{spec.title | caseUcfirst}}InputFile: return {{spec.title | caseUcfirst}}InputFile.new("", p_bytes, p_filename, p_content_type) + +## Return the PackedByteArray of the file func get_data() -> PackedByteArray: if not bytes.is_empty(): return bytes diff --git a/templates/godot/src/models/model.gd.twig b/templates/godot/src/models/model.gd.twig index 5e3a8657be..f0d15d18d0 100644 --- a/templates/godot/src/models/model.gd.twig +++ b/templates/godot/src/models/model.gd.twig @@ -1,6 +1,7 @@ {% set prefix = spec.title | caseUcfirst %} class_name {{ prefix }}{{ definition.name | caseUcfirst }} extends RefCounted +## {{ definition.description | default("Model class.") | replace({"\n": "\n## "}) }}[br] {% set imports = [] %} {%~ for property in definition.properties %} @@ -21,9 +22,10 @@ const _FIELD_MAP := { {% for property in definition.properties %} {% set baseType = property | typeName(spec) %} -var {{ property.name | uniqueSnake }}: {{ baseType }} +var {{ property.name | uniqueSnake }}: {{ baseType }} ## {{ property.description | default("No description provided.") }} {% endfor %} +## Convert dictionary to model static func from_dict(dict: Dictionary): var m := {{ prefix }}{{ definition.name | caseUcfirst }}.new() @@ -79,6 +81,7 @@ static func from_dict(dict: Dictionary): return m +## Convert to Dictionary func to_dict() -> Dictionary: var dict := {} diff --git a/templates/godot/src/permission.gd.twig b/templates/godot/src/permission.gd.twig index 50f6b8d0f9..75bb7ae3a5 100644 --- a/templates/godot/src/permission.gd.twig +++ b/templates/godot/src/permission.gd.twig @@ -1,16 +1,21 @@ # {{spec.title | caseUcfirst}}Permission +## Read permission static func read(role: String) -> String: return 'read("%s")' % role +## Write permission static func write(role: String) -> String: return 'write("%s")' % role +## Create permission static func create(role: String) -> String: return 'create("%s")' % role +## Update permission static func update(role: String) -> String: return 'update("%s")' % role +## Delete permission static func delete(role: String) -> String: return 'delete("%s")' % role diff --git a/templates/godot/src/role.gd.twig b/templates/godot/src/role.gd.twig index 3a15624cc3..87d6248c3c 100644 --- a/templates/godot/src/role.gd.twig +++ b/templates/godot/src/role.gd.twig @@ -1,28 +1,40 @@ # {{spec.title | caseUcfirst}}Role + +## Any role static func any() -> String: return 'any' + +## User role static func user(id: String, status: String = '') -> String: if status == '': return 'user:%s' % id return 'user:%s/%s' % [id, status] + +## Users role static func users(status: String = '') -> String: if status == '': return 'users' return 'users/%s' % status + +## Guests role static func guests() -> String: return 'guests' + +## Team role static func team(id: String, role: String = '') -> String: if role == '': return 'team:%s' % id return 'team:%s/%s' % [id, role] +## Team member role static func member(id: String) -> String: return 'member:%s' % id + static func label(name: String) -> String: return 'label:%s' % name diff --git a/templates/godot/src/service.gd.twig b/templates/godot/src/service.gd.twig index ea0b9db038..7346e2301a 100644 --- a/templates/godot/src/service.gd.twig +++ b/templates/godot/src/service.gd.twig @@ -11,7 +11,7 @@ func _init(p_client: RefCounted) -> void: client = p_client func _call(method: String, path: String, headers: Dictionary, params: Dictionary, model_script: Variant = null) -> Variant: - var result = await client.call_api(method, path, headers, params) + var result = await client._call_api(method, path, headers, params) if result.statusCode == 0: var error_msg = result.get("error", "Unknown error") From 3ebd0b2cfc981065dbe0cb7c7c407d648103485b Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Sun, 3 May 2026 23:35:34 +0530 Subject: [PATCH 12/58] feat(sdk): improve typing and documentation for Godot SDK - Add explicit typing for services to enable autocomplete and navigation - Replace inline preload().new() with typed constants and instances - Introduce class_name to enable doc indexing - Fix missing method hints for chained calls (e.g., Appwrite.account.*) - Add description for services and examples - Exposed Enums through Appwrite autoload - Ensure documentation appears in editor tooltips along with method hints - Added Appwrite icon to Godot generated docs --- templates/godot/docs/example.md.twig | 6 +-- templates/godot/src/appwrite.gd.twig | 44 +++++++++----------- templates/godot/src/client.gd.twig | 20 ++++----- templates/godot/src/enums/enum.gd.twig | 2 +- templates/godot/src/exception.gd.twig | 2 +- templates/godot/src/id.gd.twig | 2 +- templates/godot/src/permission.gd.twig | 2 +- templates/godot/src/plugin.cfg.twig | 2 +- templates/godot/src/role.gd.twig | 2 +- templates/godot/src/service.gd.twig | 5 +-- templates/godot/src/services/service.gd.twig | 17 +------- 11 files changed, 42 insertions(+), 62 deletions(-) diff --git a/templates/godot/docs/example.md.twig b/templates/godot/docs/example.md.twig index 7866656644..7c9cbaccd2 100644 --- a/templates/godot/docs/example.md.twig +++ b/templates/godot/docs/example.md.twig @@ -12,9 +12,9 @@ extends Node func _ready(): {% if method.type != 'webAuth' %} - var result = await Appwrite.{{ service.name | caseCamel }}.{{ method.name | caseSnake | escapeKeyword }}( + var result = await {{prefix}}.{{ service.name | caseCamel }}.{{ method.name | caseSnake | escapeKeyword }}( {% else %} - var result = Appwrite.{{ service.name | caseCamel }}.{{ method.name | caseSnake | escapeKeyword }}( + var result = {{prefix}}.{{ service.name | caseCamel }}.{{ method.name | caseSnake | escapeKeyword }}( {% endif %} {% for parameter in method.parameters.all %} {{ parameter | paramExample }}{% if not loop.last %},{% endif %}{% if not parameter.required %} # optional{% endif %} @@ -22,7 +22,7 @@ func _ready(): {% endfor %} ) - if result is Appwrite.Exception: + if result is {{prefix}}Exception: push_error(result.message) if result is {{return_type}}: diff --git a/templates/godot/src/appwrite.gd.twig b/templates/godot/src/appwrite.gd.twig index 10c23e9e9e..cbdc67af01 100644 --- a/templates/godot/src/appwrite.gd.twig +++ b/templates/godot/src/appwrite.gd.twig @@ -1,28 +1,24 @@ {% set prefix = spec.title | caseUcfirst %} extends Node +## [img width=32]res://addons/{{spec.title | caseSnake}}/icon.svg[/img] {{ spec.description | default("Appwrite _client.") | replace({"\n": "[br]\n## "}) }} -const ID = preload("id.gd") -const Permission = preload("permission.gd") -const Role = preload("role.gd") -const Query = preload("query.gd") -const InputFile = preload("input_file.gd") -const Exception = preload("exception.gd") - -var client: RefCounted +# Internal classes for type hinting {% for service in spec.services %} -var {{ service.name | caseCamel }}: RefCounted +const _{{ service.name | caseUpper }} = preload("services/{{ service.name | caseSnake }}.gd") {% endfor %} -func _ready() -> void: - client = preload("client.gd").new() - _load_env() - +var _client := preload("client.gd").new() {% for service in spec.services %} - {{ service.name | caseCamel }} = preload("services/{{ service.name | caseSnake }}.gd").new(client) +var {{ service.name | caseUcfirst }} : _{{ service.name | caseUpper }} = _{{ service.name | caseUpper }}.new(_client) ## {{ service.description | default("No description is provided") }} {% endfor %} +# Exposing the Enums +{% for enum in spec.allEnums %} +{%~ set enumName = enum.name %} +const {{ enumName | caseUpper }} = preload("enums/{{ enumName | caseSnake }}.gd") +{% endfor %} -func _load_env() -> void: +func _ready() -> void: var path = "res://.env" if not FileAccess.file_exists(path): return @@ -52,43 +48,43 @@ func _load_env() -> void: ## {{header.description}} {% endif %} func set_{{header.key | caseSnake}}(value: String) -> void: - client.set_{{header.key | caseSnake}}(value) + _client._set_{{header.key | caseSnake}}(value) {% endfor %} ## Set self signed status func set_self_signed(status: bool = true) -> void: - client.set_self_signed(status) + _client._set_self_signed(status) ## Set the endpoint func set_endpoint(endpoint: String) -> void: - client.set_endpoint(endpoint) + _client._set_endpoint(endpoint) ## Add a header func add_header(key: String, value: String) -> void: - client.add_header(key, value) + _client._add_header(key, value) ## Get all headers func get_headers() -> Dictionary: - return client.get_headers() + return _client._get_headers() func _apply_env(key: String, value: String) -> void: match key: "{{ prefix | caseUpper }}_ENDPOINT": - client.set_endpoint(value) + _client._set_endpoint(value) "{{ prefix | caseUpper }}_SELF_SIGNED": - client.set_self_signed(value.to_lower() == "true") + _client._set_self_signed(value.to_lower() == "true") {% for header in spec.global.headers %} "{{ prefix | caseUpper }}_{{ header.key | caseUpper }}": - client.set_{{ header.key | caseSnake }}(value) + _client._set_{{ header.key | caseSnake }}(value) {% endfor %} ## Ping {{prefix}} Server for testing connection func ping() -> String: - var response = await client._call_api('GET', '/ping') + var response = await _client._call_api('GET', '/ping') return str(response.get('body')) \ No newline at end of file diff --git a/templates/godot/src/client.gd.twig b/templates/godot/src/client.gd.twig index c29899bce5..f63e0b1d3b 100644 --- a/templates/godot/src/client.gd.twig +++ b/templates/godot/src/client.gd.twig @@ -1,14 +1,12 @@ -# {{prefix | caseUcfirst}}Client +# {% set prefix = spec.title | caseUcfirst %}Client extends RefCounted -const InputFile := preload("input_file.gd") - var _chunk_size = 5 * 1024 * 1024 var _self_signed = false var _endpoint = '{{spec.endpoint}}' var _global_headers = { 'content-type': '', - 'user-agent': '{{spec.title | caseUcfirst}}{{ language.name | caseUcfirst }}SDK/{{ sdk.version }} (Godot/' + Engine.get_version_info().string + '; ' + OS.get_name() + ')', + 'user-agent': '{{prefix}}{{ language.name | caseUcfirst }}SDK/{{ sdk.version }} (Godot/' + Engine.get_version_info().string + '; ' + OS.get_name() + ')', 'x-sdk-name': '{{ sdk.name }}', 'x-sdk-platform': '{{ sdk.platform }}', 'x-sdk-language': '{{ language.name | caseLower }}', @@ -18,27 +16,27 @@ var _global_headers = { {% endfor %} } -func set_self_signed(status: bool = true) -> RefCounted: +func _set_self_signed(status: bool = true) -> RefCounted: _self_signed = status return self -func set_endpoint(endpoint: String) -> RefCounted: +func _set_endpoint(endpoint: String) -> RefCounted: _endpoint = endpoint return self -func add_header(key: String, value: String) -> RefCounted: +func _add_header(key: String, value: String) -> RefCounted: _global_headers[key.to_lower()] = value return self -func get_headers() -> Dictionary: +func _get_headers() -> Dictionary: return _global_headers.duplicate() {% for header in spec.global.headers %} -func set_{{header.key | caseSnake}}(value: String) -> RefCounted: +func _set_{{header.key | caseSnake}}(value: String) -> RefCounted: _global_headers['{{header.name|lower}}'] = value return self @@ -80,7 +78,7 @@ func _call_api(method: String, path: String = "", headers: Dictionary = {}, para else: var has_files := false for key in params: - if params[key] is InputFile: + if params[key] is {{prefix}}InputFile: has_files = true break @@ -132,7 +130,7 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) - if value is InputFile: + if value is {{prefix}}InputFile: body.append_array(("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" % [key, value.filename]).to_utf8_buffer()) var c_type = value.content_type if value.content_type != "" else "application/octet-stream" body.append_array(("Content-Type: %s\r\n\r\n" % c_type).to_utf8_buffer()) diff --git a/templates/godot/src/enums/enum.gd.twig b/templates/godot/src/enums/enum.gd.twig index d5be49bbc6..040e0bc00a 100644 --- a/templates/godot/src/enums/enum.gd.twig +++ b/templates/godot/src/enums/enum.gd.twig @@ -1,4 +1,4 @@ -# Enum: {{ enum.name }} +## Enum: {{ enum.name | caseUpper }} {% for value in enum.enum %} {% set key = enum.keys is empty ? value : enum.keys[loop.index0] %} diff --git a/templates/godot/src/exception.gd.twig b/templates/godot/src/exception.gd.twig index 30c623518e..dba708dd68 100644 --- a/templates/godot/src/exception.gd.twig +++ b/templates/godot/src/exception.gd.twig @@ -1,4 +1,4 @@ -# {{spec.title | caseUcfirst}}Exception +class_name {{spec.title | caseUcfirst}}Exception var message: String ## Error message var code: int ## Error code diff --git a/templates/godot/src/id.gd.twig b/templates/godot/src/id.gd.twig index 61a5118162..9adef4794d 100644 --- a/templates/godot/src/id.gd.twig +++ b/templates/godot/src/id.gd.twig @@ -1,4 +1,4 @@ -# {{spec.title | caseUcfirst}}ID +class_name {{spec.title | caseUcfirst}}ID static func _hex_timestamp() -> String: var now := Time.get_unix_time_from_system() diff --git a/templates/godot/src/permission.gd.twig b/templates/godot/src/permission.gd.twig index 75bb7ae3a5..6b5dc53dae 100644 --- a/templates/godot/src/permission.gd.twig +++ b/templates/godot/src/permission.gd.twig @@ -1,4 +1,4 @@ -# {{spec.title | caseUcfirst}}Permission +class_name {{spec.title | caseUcfirst}}Permission ## Read permission static func read(role: String) -> String: diff --git a/templates/godot/src/plugin.cfg.twig b/templates/godot/src/plugin.cfg.twig index b5fc3fd1bf..0f22c61214 100644 --- a/templates/godot/src/plugin.cfg.twig +++ b/templates/godot/src/plugin.cfg.twig @@ -2,6 +2,6 @@ name="{{ spec.title | caseUcfirst }}" description="{{ spec.shortDescription }}" -author="Appwrite Team" +author="{{ spec.contactName }}" version="{{ spec.version }}" script="plugin.gd" diff --git a/templates/godot/src/role.gd.twig b/templates/godot/src/role.gd.twig index 87d6248c3c..d73b2e3829 100644 --- a/templates/godot/src/role.gd.twig +++ b/templates/godot/src/role.gd.twig @@ -1,4 +1,4 @@ -# {{spec.title | caseUcfirst}}Role +class_name {{spec.title | caseUcfirst}}Role ## Any role diff --git a/templates/godot/src/service.gd.twig b/templates/godot/src/service.gd.twig index 7346e2301a..05f2ff5166 100644 --- a/templates/godot/src/service.gd.twig +++ b/templates/godot/src/service.gd.twig @@ -3,7 +3,6 @@ extends RefCounted -const ExceptionScript = preload("exception.gd") var client: RefCounted @@ -16,7 +15,7 @@ func _call(method: String, path: String, headers: Dictionary, params: Dictionary if result.statusCode == 0: var error_msg = result.get("error", "Unknown error") push_error("{{prefix}} Network Error: %s" % error_msg) - return ExceptionScript.new(error_msg, 0, "network_error", "") + return {{prefix}}Exception.new(error_msg, 0, "network_error", "") if result.statusCode >= 400: var message = "" @@ -33,7 +32,7 @@ func _call(method: String, path: String, headers: Dictionary, params: Dictionary response = str(result.get("body", "")) push_error("{{prefix}} Error (%s): %s" % [type, message]) - return ExceptionScript.new(message, code, type, response) + return {{prefix}}Exception.new(message, code, type, response) if model_script == null: return result.get("body") diff --git a/templates/godot/src/services/service.gd.twig b/templates/godot/src/services/service.gd.twig index b3d9b07480..f09ff042fc 100644 --- a/templates/godot/src/services/service.gd.twig +++ b/templates/godot/src/services/service.gd.twig @@ -1,19 +1,6 @@ {% set prefix = spec.title | caseUcfirst %} extends "../service.gd" - -{% set imports = [] %} -{% for method in service.methods %} -{% for parameter in method.parameters.all %} -{% if parameter.enumValues is not empty %} -{% if parameter.enumName not in imports %} -const {{ parameter.enumName | caseUcfirst | escapeKeyword }} := preload("../enums/{{ parameter.enumName | caseSnake }}.gd") -{% set imports = imports|merge([parameter.enumName]) %} -{% endif %} -{% endif %} -{% endfor %} -{% endfor %} - -const InputFile := preload("../input_file.gd") +## {{ service.description | default("Service class.") | replace({"\n": "\n## "}) }} {% for method in service.methods %} {%~ set methodNameSnake = method.name | caseSnake %} @@ -49,7 +36,7 @@ const InputFile := preload("../input_file.gd") ## - [{{return_type}}] on success.[br] ##[br] ## Errors:[br] -## - Returns error data as [member {{spec.title | caseUcfirst}}.Exception]. +## - Returns error data as [member {{ prefix }}Exception]. func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters.all %}{{ parameter.name | caseSnake | escapeKeyword }}: {{ parameter | typeName(spec) }}{{ parameter | paramDefault }}{% if not loop.last %}, {% endif %}{% endfor %}) -> Variant : var _path := '{{ method.path }}' {%~ for parameter in method.parameters.path %} From d25dfd57c5afab55cd6cea7433d76387349cf67e Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Thu, 7 May 2026 00:29:40 +0530 Subject: [PATCH 13/58] test: adding test using GUT library --- src/SDK/Language/Godot.php | 40 ++++ templates/godot/src/models/model.gd.twig | 2 + templates/godot/src/operator.gd.twig | 208 ++++++++++++++++++ templates/godot/src/tests/test_id.gd.twig | 19 ++ .../godot/src/tests/test_input_files.gd.twig | 16 ++ .../godot/src/tests/test_operator.gd.twig | 145 ++++++++++++ .../godot/src/tests/test_permission.gd.twig | 12 + templates/godot/src/tests/test_query.gd.twig | 96 ++++++++ templates/godot/src/tests/test_roles.gd.twig | 19 ++ 9 files changed, 557 insertions(+) create mode 100644 templates/godot/src/operator.gd.twig create mode 100644 templates/godot/src/tests/test_id.gd.twig create mode 100644 templates/godot/src/tests/test_input_files.gd.twig create mode 100644 templates/godot/src/tests/test_operator.gd.twig create mode 100644 templates/godot/src/tests/test_permission.gd.twig create mode 100644 templates/godot/src/tests/test_query.gd.twig create mode 100644 templates/godot/src/tests/test_roles.gd.twig diff --git a/src/SDK/Language/Godot.php b/src/SDK/Language/Godot.php index 4cb8e346a6..e812b4ac60 100644 --- a/src/SDK/Language/Godot.php +++ b/src/SDK/Language/Godot.php @@ -102,6 +102,46 @@ public function getFiles(): array 'destination' => 'example.env', 'template' => 'godot/example.env.twig' ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{spec.title | caseSnake}}/operator.gd', + 'template' => 'godot/src/operator.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/tests/test_query.gd', + 'template' => 'godot/src/tests/test_query.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/tests/test_roles.gd', + 'template' => 'godot/src/tests/test_roles.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/tests/test_query.gd', + 'template' => 'godot/src/tests/test_query.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/tests/test_id.gd', + 'template' => 'godot/src/tests/test_id.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/tests/test_permission.gd', + 'template' => 'godot/src/tests/test_permission.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/tests/test_input_files.gd', + 'template' => 'godot/src/tests/test_input_files.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/tests/test_operator.gd', + 'template' => 'godot/src/tests/test_operator.gd.twig', + ], [ 'scope' => 'enum', 'destination' => 'addons/{{ spec.title | caseSnake }}/enums/{{ enum.name | caseSnake }}.gd', diff --git a/templates/godot/src/models/model.gd.twig b/templates/godot/src/models/model.gd.twig index f0d15d18d0..b1d8e1ec61 100644 --- a/templates/godot/src/models/model.gd.twig +++ b/templates/godot/src/models/model.gd.twig @@ -56,6 +56,7 @@ static func from_dict(dict: Dictionary): if key == "{{ field }}" and value != null: if not _{{ enumName | caseUcfirst }}.is_valid(value): push_error("Invalid enum value for {{ field }}: %s" % value) + return {{prefix}}Exception.new("Invalid enum value for {{ field }}: %s" % value, 0, "invalid_enum_value", str(dict)) m.set(key, value) continue {% endif %} @@ -65,6 +66,7 @@ static func from_dict(dict: Dictionary): for item in value: if not _{{ enumName | caseUcfirst }}.is_valid(item): push_error("Invalid enum value: %s" % item) + return {{prefix}}Exception.new("Invalid enum value for {{ field }}: %s" % value, 0, "invalid_enum_value", str(dict)) list.append(item) m.set(key, list) continue diff --git a/templates/godot/src/operator.gd.twig b/templates/godot/src/operator.gd.twig new file mode 100644 index 0000000000..85534b7fa3 --- /dev/null +++ b/templates/godot/src/operator.gd.twig @@ -0,0 +1,208 @@ +class_name {{spec.title | caseUcfirst}}Operator +extends RefCounted + +const EQUAL := "equal" +const NOT_EQUAL := "notEqual" +const GREATER_THAN := "greaterThan" +const GREATER_THAN_EQUAL := "greaterThanEqual" +const LESS_THAN := "lessThan" +const LESS_THAN_EQUAL := "lessThanEqual" +const CONTAINS := "contains" +const IS_NULL := "isNull" +const IS_NOT_NULL := "isNotNull" + + +var method: String +var values: Array + + +## Validate condition value +static func is_valid(value: String) -> bool: + return value in [ + EQUAL, + NOT_EQUAL, + GREATER_THAN, + GREATER_THAN_EQUAL, + LESS_THAN, + LESS_THAN_EQUAL, + CONTAINS, + IS_NULL, + IS_NOT_NULL + ] + + +## Constructor +func _init(_method: String, _values: Variant = null) -> void: + method = _method + + if _values != null: + if _values is Array: + values = _values + else: + values = [_values] + else: + values = [] + + +## Convert operator object to JSON string +func _to_string() -> String: + return JSON.stringify({ + "method": method, + "values": values + }) + + +static func increment(value: float = 1, max: Variant = null) -> String: + if not is_finite(value): + push_error("Value cannot be NaN or Infinity") + return "" + + if max != null and not is_finite(max): + push_error("Max cannot be NaN or Infinity") + return "" + + var vals := [value] + if max != null: + vals.append(max) + + return AppwriteOperator.new("increment", vals).to_string() + + +static func decrement(value: float = 1, min: Variant = null) -> String: + if not is_finite(value): + push_error("Value cannot be NaN or Infinity") + return "" + + if min != null and not is_finite(min): + push_error("Min cannot be NaN or Infinity") + return "" + + var vals := [value] + if min != null: + vals.append(min) + + return AppwriteOperator.new("decrement", vals).to_string() + + +static func multiply(factor: float, max: Variant = null) -> String: + if not is_finite(factor): + push_error("Factor cannot be NaN or Infinity") + return "" + + if max != null and not is_finite(max): + push_error("Max cannot be NaN or Infinity") + return "" + + var vals := [factor] + if max != null: + vals.append(max) + + return AppwriteOperator.new("multiply", vals).to_string() + + +static func divide(divisor: float, min: Variant = null) -> String: + if not is_finite(divisor): + push_error("Divisor cannot be NaN or Infinity") + return "" + + if divisor == 0: + push_error("Divisor cannot be zero") + return "" + + if min != null and not is_finite(min): + push_error("Min cannot be NaN or Infinity") + return "" + + var vals := [divisor] + if min != null: + vals.append(min) + + return AppwriteOperator.new("divide", vals).to_string() + + +static func modulo(divisor: float) -> String: + if not is_finite(divisor): + push_error("Divisor cannot be NaN or Infinity") + return "" + + if divisor == 0: + push_error("Divisor cannot be zero") + return "" + + return AppwriteOperator.new("modulo", [divisor]).to_string() + + +static func power(exponent: float, max: Variant = null) -> String: + if not is_finite(exponent): + push_error("Exponent cannot be NaN or Infinity") + return "" + + if max != null and not is_finite(max): + push_error("Max cannot be NaN or Infinity") + return "" + + var vals := [exponent] + if max != null: + vals.append(max) + + return AppwriteOperator.new("power", vals).to_string() + + +static func array_append(values: Array) -> String: + return AppwriteOperator.new("arrayAppend", values).to_string() + + +static func array_prepend(values: Array) -> String: + return AppwriteOperator.new("arrayPrepend", values).to_string() + + +static func array_insert(index: int, value: Variant) -> String: + return AppwriteOperator.new("arrayInsert", [index, value]).to_string() + + +static func array_remove(value: Variant) -> String: + return AppwriteOperator.new("arrayRemove", [value]).to_string() + + +static func array_unique() -> String: + return AppwriteOperator.new("arrayUnique", []).to_string() + + +static func array_intersect(values: Array) -> String: + return AppwriteOperator.new("arrayIntersect", values).to_string() + + +static func array_diff(values: Array) -> String: + return AppwriteOperator.new("arrayDiff", values).to_string() + + +static func array_filter(condition: String, value: Variant = null) -> String: + if not is_valid(condition): + push_error("Invalid condition: %s" % condition) + return "" + + return AppwriteOperator.new("arrayFilter", [condition, value]).to_string() + + +static func string_concat(value: Variant) -> String: + return AppwriteOperator.new("stringConcat", [value]).to_string() + + +static func string_replace(search: String, replace: String) -> String: + return AppwriteOperator.new("stringReplace", [search, replace]).to_string() + + +static func toggle() -> String: + return AppwriteOperator.new("toggle", []).to_string() + + +static func date_add_days(days: int) -> String: + return AppwriteOperator.new("dateAddDays", [days]).to_string() + + +static func date_sub_days(days: int) -> String: + return AppwriteOperator.new("dateSubDays", [days]).to_string() + + +static func date_set_now() -> String: + return AppwriteOperator.new("dateSetNow", []).to_string() \ No newline at end of file diff --git a/templates/godot/src/tests/test_id.gd.twig b/templates/godot/src/tests/test_id.gd.twig new file mode 100644 index 0000000000..042215ceb0 --- /dev/null +++ b/templates/godot/src/tests/test_id.gd.twig @@ -0,0 +1,19 @@ +extends "res://addons/gut/test.gd" + +func test_id_unique_length(): + var id = {{spec.title | caseUcfirst}}ID.unique() + assert_true(id.length() > 10) + +func test_id_unique_padding(): + var id1 = {{spec.title | caseUcfirst}}ID.unique(0) + var id2 = {{spec.title | caseUcfirst}}ID.unique(10) + assert_true(id2.length() > id1.length()) + +func test_id_unique_randomness(): + var id1 = {{spec.title | caseUcfirst}}ID.unique() + var id2 = {{spec.title | caseUcfirst}}ID.unique() + assert_ne(id1, id2) + +func test_id_custom(): + var val = "custom_id" + assert_eq({{spec.title | caseUcfirst}}ID.custom(val), val) \ No newline at end of file diff --git a/templates/godot/src/tests/test_input_files.gd.twig b/templates/godot/src/tests/test_input_files.gd.twig new file mode 100644 index 0000000000..fb8ac42ada --- /dev/null +++ b/templates/godot/src/tests/test_input_files.gd.twig @@ -0,0 +1,16 @@ +extends "res://addons/gut/test.gd" + +func test_input_file_from_bytes(): + var bytes = PackedByteArray([1,2,3]) + var file = {{spec.title | caseUcfirst}}InputFile.from_bytes(bytes, "test.txt") + + assert_eq(file.filename, "test.txt") + assert_eq(file.get_data(), bytes) + +func test_input_file_from_path_sets_filename(): + var file = {{spec.title | caseUcfirst}}InputFile.from_path("res://test.txt") + assert_eq(file.filename, "test.txt") + +func test_input_file_empty(): + var file = {{spec.title | caseUcfirst}}InputFile.new() + assert_eq(file.get_data(), PackedByteArray()) \ No newline at end of file diff --git a/templates/godot/src/tests/test_operator.gd.twig b/templates/godot/src/tests/test_operator.gd.twig new file mode 100644 index 0000000000..3032b2ffe9 --- /dev/null +++ b/templates/godot/src/tests/test_operator.gd.twig @@ -0,0 +1,145 @@ +{% set prefix = spec.title | caseUcfirst %} +extends "res://addons/gut/test.gd" + +func test_operator_to_string(): + var op = {{prefix}}Operator.new("test", [1, 2]) + var parsed = JSON.parse_string(op.to_string()) + + assert_eq(parsed["method"], "test") + assert_eq(parsed["values"].size(), 2) + + +func test_operator_value_wrapping(): + var op = {{prefix}}Operator.new("test", 5) + var parsed = JSON.parse_string(op.to_string()) + + assert_true(parsed["values"] is Array) + assert_eq(parsed["values"][0], 5) + + +func test_operator_null_values(): + var op = {{prefix}}Operator.new("test", null) + var parsed = JSON.parse_string(op.to_string()) + + assert_eq(parsed["values"].size(), 0) + + +func test_is_valid_condition(): + assert_true({{prefix}}Operator.is_valid({{prefix}}Operator.EQUAL)) + assert_true({{prefix}}Operator.is_valid({{prefix}}Operator.NOT_EQUAL)) + assert_false({{prefix}}Operator.is_valid("invalid")) + + +func test_increment_basic(): + var q = {{prefix}}Operator.increment(5) + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "increment") + assert_eq(parsed["values"][0], 5) + + +func test_increment_with_max(): + var q = {{prefix}}Operator.increment(5, 10) + var parsed = JSON.parse_string(q) + + assert_eq(parsed["values"].size(), 2) + assert_eq(parsed["values"][1], 10) + + +func test_increment_invalid_number(): + var q = {{prefix}}Operator.increment(INF) + assert_eq(q, "") # should fail gracefully + assert_push_error("Value cannot be NaN or Infinity") + + + +func test_divide_by_zero(): + var q = {{prefix}}Operator.divide(0) + assert_eq(q, "") + assert_push_error("Divisor cannot be zero") + + +func test_divide_valid(): + var q = {{prefix}}Operator.divide(10) + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "divide") + assert_eq(parsed["values"][0], 10) + + +func test_array_append(): + var q = {{prefix}}Operator.array_append([1, 2]) + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "arrayAppend") + assert_eq(parsed["values"].size(), 2) + + +func test_array_insert(): + var q = {{prefix}}Operator.array_insert(1, "x") + var parsed = JSON.parse_string(q) + + assert_eq(parsed["values"][0], 1) + assert_eq(parsed["values"][1], "x") + + +func test_array_unique(): + var q = {{prefix}}Operator.array_unique() + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "arrayUnique") + assert_eq(parsed["values"].size(), 0) + + +func test_array_filter_valid(): + var q = {{prefix}}Operator.array_filter({{prefix}}Operator.EQUAL, 10) + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "arrayFilter") + assert_eq(parsed["values"][0], "equal") + + +func test_array_filter_invalid_condition(): + var q = {{prefix}}Operator.array_filter("INVALID", 10) + assert_eq(q, "") + assert_push_error("Invalid condition: INVALID") + + +func test_string_concat(): + var q = {{prefix}}Operator.string_concat("abc") + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "stringConcat") + assert_eq(parsed["values"][0], "abc") + + +func test_string_replace(): + var q = {{prefix}}Operator.string_replace("a", "b") + var parsed = JSON.parse_string(q) + + assert_eq(parsed["values"][0], "a") + assert_eq(parsed["values"][1], "b") + + +func test_toggle(): + var q = {{prefix}}Operator.toggle() + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "toggle") + assert_eq(parsed["values"].size(), 0) + + +func test_date_add_days(): + var q = {{prefix}}Operator.date_add_days(5) + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "dateAddDays") + assert_eq(parsed["values"][0], 5) + + +func test_date_set_now(): + var q = {{prefix}}Operator.date_set_now() + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "dateSetNow") + assert_eq(parsed["values"].size(), 0) \ No newline at end of file diff --git a/templates/godot/src/tests/test_permission.gd.twig b/templates/godot/src/tests/test_permission.gd.twig new file mode 100644 index 0000000000..a229ffbf4f --- /dev/null +++ b/templates/godot/src/tests/test_permission.gd.twig @@ -0,0 +1,12 @@ +extends "res://addons/gut/test.gd" + +func test_permission_read(): + assert_eq({{spec.title | caseUcfirst}}Permission.read("user:1"), 'read("user:1")') + +func test_permission_write(): + assert_eq({{spec.title | caseUcfirst}}Permission.write("role"), 'write("role")') + +func test_permission_all(): + assert_eq({{spec.title | caseUcfirst}}Permission.create("x"), 'create("x")') + assert_eq({{spec.title | caseUcfirst}}Permission.update("x"), 'update("x")') + assert_eq({{spec.title | caseUcfirst}}Permission.delete("x"), 'delete("x")') \ No newline at end of file diff --git a/templates/godot/src/tests/test_query.gd.twig b/templates/godot/src/tests/test_query.gd.twig new file mode 100644 index 0000000000..bf8946d3eb --- /dev/null +++ b/templates/godot/src/tests/test_query.gd.twig @@ -0,0 +1,96 @@ +extends "res://addons/gut/test.gd" + +func test_query_equal(): + var q = {{spec.title | caseUcfirst}}Query.equal("age", 10) + var parsed = JSON.parse_string(q) + + assert_not_null(parsed) + assert_eq(parsed["method"], "equal") + assert_eq(parsed["attribute"], "age") + assert_eq(parsed["values"][0], 10) + + +func test_query_value_wrapping(): + var q = {{spec.title | caseUcfirst}}Query.equal("age", 5) + var parsed = JSON.parse_string(q) + + assert_true(parsed["values"] is Array) + assert_eq(parsed["values"].size(), 1) + + +func test_query_is_null(): + var q = {{spec.title | caseUcfirst}}Query.isNull("field") + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "isNull") + assert_false(parsed.has("values")) + + +func test_query_between(): + var q = {{spec.title | caseUcfirst}}Query.between("age", 10, 20) + var parsed = JSON.parse_string(q) + + assert_eq(parsed["values"].size(), 2) + assert_eq(parsed["values"][0], 10) + assert_eq(parsed["values"][1], 20) + + +func test_query_and(): + var q1 = {{spec.title | caseUcfirst}}Query.equal("age", 10) + var q2 = {{spec.title | caseUcfirst}}Query.equal("name", "abc") + + var combined = {{spec.title | caseUcfirst}}Query.and_query([q1, q2]) + var parsed = JSON.parse_string(combined) + + assert_eq(parsed["method"], "and") + assert_eq(parsed["values"].size(), 2) + + +func test_query_or(): + var q1 = {{spec.title | caseUcfirst}}Query.equal("age", 10) + var q2 = {{spec.title | caseUcfirst}}Query.isNull("email") + + var combined = {{spec.title | caseUcfirst}}Query.or_query([q1, q2]) + var parsed = JSON.parse_string(combined) + + assert_eq(parsed["method"], "or") + assert_eq(parsed["values"].size(), 2) + + +func test_query_limit(): + var q = {{spec.title | caseUcfirst}}Query.limit(10) + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "limit") + assert_eq(parsed["values"][0], 10) + + +func test_query_offset(): + var q = {{spec.title | caseUcfirst}}Query.offset(5) + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "offset") + assert_eq(parsed["values"][0], 5) + + +func test_query_order_asc(): + var q = {{spec.title | caseUcfirst}}Query.orderAsc("age") + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "orderAsc") + assert_eq(parsed["attribute"], "age") + + +func test_query_order_desc(): + var q = {{spec.title | caseUcfirst}}Query.orderDesc("age") + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "orderDesc") + assert_eq(parsed["attribute"], "age") + + +func test_query_invalid_json_skipped(): + var combined = {{spec.title | caseUcfirst}}Query.and_query(["INVALID"]) + var parsed = JSON.parse_string(combined) + + assert_eq(parsed["values"].size(), 0) \ No newline at end of file diff --git a/templates/godot/src/tests/test_roles.gd.twig b/templates/godot/src/tests/test_roles.gd.twig new file mode 100644 index 0000000000..6666b1ea72 --- /dev/null +++ b/templates/godot/src/tests/test_roles.gd.twig @@ -0,0 +1,19 @@ +extends "res://addons/gut/test.gd" + +func test_role_any(): + assert_eq({{spec.title | caseUcfirst}}Role.any(), "any") + +func test_role_user(): + assert_eq({{spec.title | caseUcfirst}}Role.user("123"), "user:123") + +func test_role_user_with_status(): + assert_eq({{spec.title | caseUcfirst}}Role.user("123", "verified"), "user:123/verified") + +func test_role_team(): + assert_eq({{spec.title | caseUcfirst}}Role.team("team1"), "team:team1") + +func test_role_team_with_role(): + assert_eq({{spec.title | caseUcfirst}}Role.team("team1", "admin"), "team:team1/admin") + +func test_role_label(): + assert_eq({{spec.title | caseUcfirst}}Role.label("vip"), "label:vip") \ No newline at end of file From 0e9418bd36b0f7e6245dfb7a95f6f04e4ee35813 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Thu, 7 May 2026 21:53:39 +0530 Subject: [PATCH 14/58] fix(core): fixed silent SDK failure and minor issue in singleton --- src/SDK/Language/Godot.php | 29 +++++++++++----------------- templates/godot/docs/example.md.twig | 10 ++++++++++ templates/godot/example.env.twig | 6 +++++- templates/godot/src/appwrite.gd.twig | 2 +- templates/godot/src/client.gd.twig | 14 +++++++++++++- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/SDK/Language/Godot.php b/src/SDK/Language/Godot.php index e812b4ac60..c78988cae3 100644 --- a/src/SDK/Language/Godot.php +++ b/src/SDK/Language/Godot.php @@ -14,11 +14,9 @@ public function getName(): string return 'Godot'; } - public function getVersion(): string - { - return '4.0'; - } - + /** + * @return array + */ public function getFiles(): array { return [ @@ -99,7 +97,7 @@ public function getFiles(): array ], [ 'scope' => 'default', - 'destination' => 'example.env', + 'destination' => 'addons/{{ spec.title | caseSnake }}/.env.example', 'template' => 'godot/example.env.twig' ], [ @@ -109,37 +107,37 @@ public function getFiles(): array ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/tests/test_query.gd', + 'destination' => 'tests/test_query.gd', 'template' => 'godot/src/tests/test_query.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/tests/test_roles.gd', + 'destination' => 'tests/test_roles.gd', 'template' => 'godot/src/tests/test_roles.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/tests/test_query.gd', + 'destination' => 'tests/test_query.gd', 'template' => 'godot/src/tests/test_query.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/tests/test_id.gd', + 'destination' => 'tests/test_id.gd', 'template' => 'godot/src/tests/test_id.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/tests/test_permission.gd', + 'destination' => 'tests/test_permission.gd', 'template' => 'godot/src/tests/test_permission.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/tests/test_input_files.gd', + 'destination' => 'tests/test_input_files.gd', 'template' => 'godot/src/tests/test_input_files.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/tests/test_operator.gd', + 'destination' => 'tests/test_operator.gd', 'template' => 'godot/src/tests/test_operator.gd.twig', ], [ @@ -164,9 +162,4 @@ public function getFiles(): array ], ]; } - - public function getDocsUrl(): string - { - return 'https://appwrite.io/docs/sdks/godot'; - } } \ No newline at end of file diff --git a/templates/godot/docs/example.md.twig b/templates/godot/docs/example.md.twig index 7c9cbaccd2..d0969e683b 100644 --- a/templates/godot/docs/example.md.twig +++ b/templates/godot/docs/example.md.twig @@ -11,6 +11,16 @@ extends Node {% endif -%} func _ready(): + # You can skip setup if you have .env +{% if method.auth|length > 0 %} + {{prefix}}.set_endpoint('{{ spec.endpointDocs | raw }}') # Your API Endpoint +{% for node in method.auth %} +{% for key,header in node|keys %} + {{prefix}}.set_{{header | caseSnake}}('{{node[header]['x-appwrite']['demo'] | raw }}') # {{node[header].description}} +{% endfor %} +{% endfor %} +{% endif %} + {% if method.type != 'webAuth' %} var result = await {{prefix}}.{{ service.name | caseCamel }}.{{ method.name | caseSnake | escapeKeyword }}( {% else %} diff --git a/templates/godot/example.env.twig b/templates/godot/example.env.twig index 041f2672d3..d2f3b4656b 100644 --- a/templates/godot/example.env.twig +++ b/templates/godot/example.env.twig @@ -1,4 +1,8 @@ # {{ spec.title | caseUcfirst }} Configuration {{ spec.title | caseUpper }}_ENDPOINT=https://cloud.appwrite.io/v1 -{{ spec.title | caseUpper }}_PROJECT=your_project_id_here \ No newline at end of file +{{ spec.title | caseUpper }}_SELF_SIGNED=true +{% for header in spec.global.headers %} +{{ spec.title | caseUpper }}_{{ header.key | caseUpper }}={% if header.description %}{{header.description}} +{% endif %} +{% endfor %} \ No newline at end of file diff --git a/templates/godot/src/appwrite.gd.twig b/templates/godot/src/appwrite.gd.twig index cbdc67af01..bcb183780a 100644 --- a/templates/godot/src/appwrite.gd.twig +++ b/templates/godot/src/appwrite.gd.twig @@ -9,7 +9,7 @@ const _{{ service.name | caseUpper }} = preload("services/{{ service.name | case var _client := preload("client.gd").new() {% for service in spec.services %} -var {{ service.name | caseUcfirst }} : _{{ service.name | caseUpper }} = _{{ service.name | caseUpper }}.new(_client) ## {{ service.description | default("No description is provided") }} +var {{ service.name | caseCamel }} : _{{ service.name | caseUpper }} = _{{ service.name | caseUpper }}.new(_client) ## {{ service.description | default("No description is provided") }} {% endfor %} # Exposing the Enums diff --git a/templates/godot/src/client.gd.twig b/templates/godot/src/client.gd.twig index f63e0b1d3b..fec9456094 100644 --- a/templates/godot/src/client.gd.twig +++ b/templates/godot/src/client.gd.twig @@ -45,6 +45,8 @@ func _set_{{header.key | caseSnake}}(value: String) -> RefCounted: func _call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: + if not _ensure_configured(): + return {"statusCode": 400, "body": "Missing/wrong configuration. Please check logs for more info"} var http := HTTPRequest.new() var tls_options := TLSOptions.client_unsafe() if _self_signed else TLSOptions.client() @@ -148,4 +150,14 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: body.append_array(("\r\n").to_utf8_buffer()) body.append_array(("--" + boundary + "--\r\n").to_utf8_buffer()) - return body \ No newline at end of file + return body + + +func _ensure_configured() -> bool: + if _endpoint == "": + push_warning("{{prefix}}: endpoint is missing. Use set_endpoint or .env.") + return false + if not _global_headers.has("x-appwrite-project"): + push_warning("{{prefix}}: project id is missing. Use set_project or .env.") + return false + return true \ No newline at end of file From e4c96ed2f47b38d33431d4a067dd16c51a1de8a2 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Fri, 8 May 2026 00:55:40 +0530 Subject: [PATCH 15/58] feat: godot basic project for ping test --- example.php | 7 + src/SDK/Language/GDScript.php | 138 +++++++++++- src/SDK/Language/Godot.php | 83 ++++--- templates/gdscript/CHANGELOG.md.twig | 1 + templates/gdscript/LICENSE.twig | 1 + templates/gdscript/README.md.twig | 49 +++++ .../src => gdscript/addons}/appwrite.gd.twig | 0 .../src => gdscript/addons}/client.gd.twig | 2 +- .../addons}/enums/enum.gd.twig | 0 .../src => gdscript/addons}/exception.gd.twig | 0 .../{godot/src => gdscript/addons}/icon.svg | 0 .../{godot/src => gdscript/addons}/id.gd.twig | 0 .../addons}/input_file.gd.twig | 0 .../addons}/models/model.gd.twig | 0 .../src => gdscript/addons}/operator.gd.twig | 0 .../addons}/permission.gd.twig | 0 .../src => gdscript/addons}/plugin.cfg.twig | 0 .../src => gdscript/addons}/plugin.gd.twig | 0 .../src => gdscript/addons}/query.gd.twig | 0 .../src => gdscript/addons}/role.gd.twig | 0 .../src => gdscript/addons}/service.gd.twig | 0 .../addons}/services/service.gd.twig | 0 .../addons}/tests/test_id.gd.twig | 0 .../addons}/tests/test_input_files.gd.twig | 0 .../addons}/tests/test_operator.gd.twig | 0 .../addons}/tests/test_permission.gd.twig | 0 .../addons}/tests/test_query.gd.twig | 0 .../addons}/tests/test_roles.gd.twig | 0 templates/gdscript/docs/example.md.twig | 40 ++++ templates/godot/.editorconfig | 4 + templates/godot/.env.twig | 4 + templates/godot/Menu.tscn | 61 +++++ templates/godot/README.md.twig | 47 ++-- templates/godot/addons/appwrite.gd.twig | 90 ++++++++ templates/godot/addons/client.gd.twig | 163 ++++++++++++++ templates/godot/addons/enums/enum.gd.twig | 19 ++ templates/godot/addons/exception.gd.twig | 17 ++ templates/godot/addons/icon.svg | 1 + templates/godot/addons/id.gd.twig | 24 ++ templates/godot/addons/input_file.gd.twig | 35 +++ templates/godot/addons/models/model.gd.twig | 113 ++++++++++ templates/godot/addons/operator.gd.twig | 208 ++++++++++++++++++ templates/godot/addons/permission.gd.twig | 21 ++ templates/godot/addons/plugin.cfg.twig | 7 + templates/godot/addons/plugin.gd.twig | 13 ++ templates/godot/addons/query.gd.twig | 198 +++++++++++++++++ templates/godot/addons/role.gd.twig | 40 ++++ templates/godot/addons/service.gd.twig | 46 ++++ .../godot/addons/services/service.gd.twig | 73 ++++++ templates/godot/addons/tests/test_id.gd.twig | 19 ++ .../addons/tests/test_input_files.gd.twig | 16 ++ .../godot/addons/tests/test_operator.gd.twig | 145 ++++++++++++ .../addons/tests/test_permission.gd.twig | 12 + .../godot/addons/tests/test_query.gd.twig | 96 ++++++++ .../godot/addons/tests/test_roles.gd.twig | 19 ++ templates/godot/example.env.twig | 8 - templates/godot/menu.gd | 4 + templates/godot/project.godot.twig | 34 +++ 58 files changed, 1788 insertions(+), 70 deletions(-) create mode 100644 templates/gdscript/CHANGELOG.md.twig create mode 100644 templates/gdscript/LICENSE.twig create mode 100644 templates/gdscript/README.md.twig rename templates/{godot/src => gdscript/addons}/appwrite.gd.twig (100%) rename templates/{godot/src => gdscript/addons}/client.gd.twig (98%) rename templates/{godot/src => gdscript/addons}/enums/enum.gd.twig (100%) rename templates/{godot/src => gdscript/addons}/exception.gd.twig (100%) rename templates/{godot/src => gdscript/addons}/icon.svg (100%) rename templates/{godot/src => gdscript/addons}/id.gd.twig (100%) rename templates/{godot/src => gdscript/addons}/input_file.gd.twig (100%) rename templates/{godot/src => gdscript/addons}/models/model.gd.twig (100%) rename templates/{godot/src => gdscript/addons}/operator.gd.twig (100%) rename templates/{godot/src => gdscript/addons}/permission.gd.twig (100%) rename templates/{godot/src => gdscript/addons}/plugin.cfg.twig (100%) rename templates/{godot/src => gdscript/addons}/plugin.gd.twig (100%) rename templates/{godot/src => gdscript/addons}/query.gd.twig (100%) rename templates/{godot/src => gdscript/addons}/role.gd.twig (100%) rename templates/{godot/src => gdscript/addons}/service.gd.twig (100%) rename templates/{godot/src => gdscript/addons}/services/service.gd.twig (100%) rename templates/{godot/src => gdscript/addons}/tests/test_id.gd.twig (100%) rename templates/{godot/src => gdscript/addons}/tests/test_input_files.gd.twig (100%) rename templates/{godot/src => gdscript/addons}/tests/test_operator.gd.twig (100%) rename templates/{godot/src => gdscript/addons}/tests/test_permission.gd.twig (100%) rename templates/{godot/src => gdscript/addons}/tests/test_query.gd.twig (100%) rename templates/{godot/src => gdscript/addons}/tests/test_roles.gd.twig (100%) create mode 100644 templates/gdscript/docs/example.md.twig create mode 100755 templates/godot/.editorconfig create mode 100644 templates/godot/.env.twig create mode 100755 templates/godot/Menu.tscn create mode 100644 templates/godot/addons/appwrite.gd.twig create mode 100644 templates/godot/addons/client.gd.twig create mode 100644 templates/godot/addons/enums/enum.gd.twig create mode 100644 templates/godot/addons/exception.gd.twig create mode 100644 templates/godot/addons/icon.svg create mode 100644 templates/godot/addons/id.gd.twig create mode 100644 templates/godot/addons/input_file.gd.twig create mode 100644 templates/godot/addons/models/model.gd.twig create mode 100644 templates/godot/addons/operator.gd.twig create mode 100644 templates/godot/addons/permission.gd.twig create mode 100644 templates/godot/addons/plugin.cfg.twig create mode 100644 templates/godot/addons/plugin.gd.twig create mode 100644 templates/godot/addons/query.gd.twig create mode 100644 templates/godot/addons/role.gd.twig create mode 100644 templates/godot/addons/service.gd.twig create mode 100644 templates/godot/addons/services/service.gd.twig create mode 100644 templates/godot/addons/tests/test_id.gd.twig create mode 100644 templates/godot/addons/tests/test_input_files.gd.twig create mode 100644 templates/godot/addons/tests/test_operator.gd.twig create mode 100644 templates/godot/addons/tests/test_permission.gd.twig create mode 100644 templates/godot/addons/tests/test_query.gd.twig create mode 100644 templates/godot/addons/tests/test_roles.gd.twig delete mode 100644 templates/godot/example.env.twig create mode 100755 templates/godot/menu.gd create mode 100755 templates/godot/project.godot.twig diff --git a/example.php b/example.php index acf59d6f5e..90bf7c00e4 100644 --- a/example.php +++ b/example.php @@ -350,6 +350,13 @@ function configureSDK($sdk, $overrides = []) $sdk->generate(__DIR__ . '/examples/rust'); } + // GDScript + if (!$requestedSdk || $requestedSdk === 'gdscript') { + $sdk = new SDK(new GDScript(), new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/gdscript'); + } + // Godot if (!$requestedSdk || $requestedSdk === 'godot') { $sdk = new SDK(new Godot(), new Swagger2($spec)); diff --git a/src/SDK/Language/GDScript.php b/src/SDK/Language/GDScript.php index 80bf7ee196..15b3f1e0fb 100644 --- a/src/SDK/Language/GDScript.php +++ b/src/SDK/Language/GDScript.php @@ -170,7 +170,143 @@ public function getArrayOf(string $elements): string */ public function getFiles(): array { - return []; + return [ + [ + 'scope' => 'default', + 'destination' => 'CHANGELOG.md', + 'template' => 'gdscript/CHANGELOG.md.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'LICENSE', + 'template' => 'gdscript/LICENSE.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'README.md', + 'template' => 'gdscript/README.md.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/plugin.cfg', + 'template' => 'gdscript/addons/plugin.cfg.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/plugin.gd', + 'template' => 'gdscript/addons/plugin.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/{{ spec.title | caseSnake }}.gd', + 'template' => 'gdscript/addons/appwrite.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/icon.svg', + 'template' => 'gdscript/addons/icon.svg', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/client.gd', + 'template' => 'gdscript/addons/client.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/service.gd', + 'template' => 'gdscript/addons/service.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/exception.gd', + 'template' => 'gdscript/addons/exception.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/id.gd', + 'template' => 'gdscript/addons/id.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/permission.gd', + 'template' => 'gdscript/addons/permission.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/role.gd', + 'template' => 'gdscript/addons/role.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/query.gd', + 'template' => 'gdscript/addons/query.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/input_file.gd', + 'template' => 'gdscript/addons/input_file.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{spec.title | caseSnake}}/operator.gd', + 'template' => 'gdscript/addons/operator.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'tests/test_query.gd', + 'template' => 'gdscript/addons/tests/test_query.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'tests/test_roles.gd', + 'template' => 'gdscript/addons/tests/test_roles.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'tests/test_query.gd', + 'template' => 'gdscript/addons/tests/test_query.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'tests/test_id.gd', + 'template' => 'gdscript/addons/tests/test_id.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'tests/test_permission.gd', + 'template' => 'gdscript/addons/tests/test_permission.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'tests/test_input_files.gd', + 'template' => 'gdscript/addons/tests/test_input_files.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'tests/test_operator.gd', + 'template' => 'gdscript/addons/tests/test_operator.gd.twig', + ], + [ + 'scope' => 'enum', + 'destination' => 'addons/{{ spec.title | caseSnake }}/enums/{{ enum.name | caseSnake }}.gd', + 'template' => 'gdscript/addons/enums/enum.gd.twig', + ], + [ + 'scope' => 'service', + 'destination' => 'addons/{{ spec.title | caseSnake }}/services/{{ service.name | caseSnake }}.gd', + 'template' => 'gdscript/addons/services/service.gd.twig', + ], + [ + 'scope' => 'definition', + 'destination' => 'addons/{{ spec.title | caseSnake }}/models/{{ definition.name | caseSnake }}.gd', + 'template' => 'gdscript/addons/models/model.gd.twig', + ], + [ + 'scope' => 'method', + 'destination' => 'docs/examples/{{service.name | caseSnake}}/{{method.name | caseSnake}}.md', + 'template' => 'gdscript/docs/example.md.twig', + ], + ]; } /** diff --git a/src/SDK/Language/Godot.php b/src/SDK/Language/Godot.php index c78988cae3..9597608217 100644 --- a/src/SDK/Language/Godot.php +++ b/src/SDK/Language/Godot.php @@ -38,128 +38,153 @@ public function getFiles(): array [ 'scope' => 'default', 'destination' => 'addons/{{ spec.title | caseSnake }}/plugin.cfg', - 'template' => 'godot/src/plugin.cfg.twig', + 'template' => 'godot/addons/plugin.cfg.twig', ], [ 'scope' => 'default', 'destination' => 'addons/{{ spec.title | caseSnake }}/plugin.gd', - 'template' => 'godot/src/plugin.gd.twig', + 'template' => 'godot/addons/plugin.gd.twig', ], [ 'scope' => 'default', 'destination' => 'addons/{{ spec.title | caseSnake }}/{{ spec.title | caseSnake }}.gd', - 'template' => 'godot/src/appwrite.gd.twig', + 'template' => 'godot/addons/appwrite.gd.twig', ], [ - 'scope' => 'default', + 'scope' => 'copy', 'destination' => 'addons/{{ spec.title | caseSnake }}/icon.svg', - 'template' => 'godot/src/icon.svg', + 'template' => 'godot/addons/icon.svg', ], [ 'scope' => 'default', 'destination' => 'addons/{{ spec.title | caseSnake }}/client.gd', - 'template' => 'godot/src/client.gd.twig', + 'template' => 'godot/addons/client.gd.twig', ], [ 'scope' => 'default', 'destination' => 'addons/{{ spec.title | caseSnake }}/service.gd', - 'template' => 'godot/src/service.gd.twig', + 'template' => 'godot/addons/service.gd.twig', ], [ 'scope' => 'default', 'destination' => 'addons/{{ spec.title | caseSnake }}/exception.gd', - 'template' => 'godot/src/exception.gd.twig', + 'template' => 'godot/addons/exception.gd.twig', ], [ 'scope' => 'default', 'destination' => 'addons/{{ spec.title | caseSnake }}/id.gd', - 'template' => 'godot/src/id.gd.twig', + 'template' => 'godot/addons/id.gd.twig', ], [ 'scope' => 'default', 'destination' => 'addons/{{ spec.title | caseSnake }}/permission.gd', - 'template' => 'godot/src/permission.gd.twig', + 'template' => 'godot/addons/permission.gd.twig', ], [ 'scope' => 'default', 'destination' => 'addons/{{ spec.title | caseSnake }}/role.gd', - 'template' => 'godot/src/role.gd.twig', + 'template' => 'godot/addons/role.gd.twig', ], [ 'scope' => 'default', 'destination' => 'addons/{{ spec.title | caseSnake }}/query.gd', - 'template' => 'godot/src/query.gd.twig', + 'template' => 'godot/addons/query.gd.twig', ], [ 'scope' => 'default', 'destination' => 'addons/{{ spec.title | caseSnake }}/input_file.gd', - 'template' => 'godot/src/input_file.gd.twig', - ], - [ - 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/.env.example', - 'template' => 'godot/example.env.twig' + 'template' => 'godot/addons/input_file.gd.twig', ], [ 'scope' => 'default', 'destination' => 'addons/{{spec.title | caseSnake}}/operator.gd', - 'template' => 'godot/src/operator.gd.twig', + 'template' => 'godot/addons/operator.gd.twig', ], [ 'scope' => 'default', 'destination' => 'tests/test_query.gd', - 'template' => 'godot/src/tests/test_query.gd.twig', + 'template' => 'godot/addons/tests/test_query.gd.twig', ], [ 'scope' => 'default', 'destination' => 'tests/test_roles.gd', - 'template' => 'godot/src/tests/test_roles.gd.twig', + 'template' => 'godot/addons/tests/test_roles.gd.twig', ], [ 'scope' => 'default', 'destination' => 'tests/test_query.gd', - 'template' => 'godot/src/tests/test_query.gd.twig', + 'template' => 'godot/addons/tests/test_query.gd.twig', ], [ 'scope' => 'default', 'destination' => 'tests/test_id.gd', - 'template' => 'godot/src/tests/test_id.gd.twig', + 'template' => 'godot/addons/tests/test_id.gd.twig', ], [ 'scope' => 'default', 'destination' => 'tests/test_permission.gd', - 'template' => 'godot/src/tests/test_permission.gd.twig', + 'template' => 'godot/addons/tests/test_permission.gd.twig', ], [ 'scope' => 'default', 'destination' => 'tests/test_input_files.gd', - 'template' => 'godot/src/tests/test_input_files.gd.twig', + 'template' => 'godot/addons/tests/test_input_files.gd.twig', ], [ 'scope' => 'default', 'destination' => 'tests/test_operator.gd', - 'template' => 'godot/src/tests/test_operator.gd.twig', + 'template' => 'godot/addons/tests/test_operator.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => '.env', + 'template' => 'godot/.env.twig', ], [ 'scope' => 'enum', 'destination' => 'addons/{{ spec.title | caseSnake }}/enums/{{ enum.name | caseSnake }}.gd', - 'template' => 'godot/src/enums/enum.gd.twig', + 'template' => 'godot/addons/enums/enum.gd.twig', ], [ 'scope' => 'service', 'destination' => 'addons/{{ spec.title | caseSnake }}/services/{{ service.name | caseSnake }}.gd', - 'template' => 'godot/src/services/service.gd.twig', + 'template' => 'godot/addons/services/service.gd.twig', ], [ 'scope' => 'definition', 'destination' => 'addons/{{ spec.title | caseSnake }}/models/{{ definition.name | caseSnake }}.gd', - 'template' => 'godot/src/models/model.gd.twig', + 'template' => 'godot/addons/models/model.gd.twig', ], [ 'scope' => 'method', 'destination' => 'docs/examples/{{service.name | caseSnake}}/{{method.name | caseSnake}}.md', 'template' => 'godot/docs/example.md.twig', ], + [ + 'scope' => 'copy', + 'destination' => '.editorconfig', + 'template' => 'godot/.editorconfig', + ], + [ + 'scope' => 'copy', + 'destination' => 'menu.gd', + 'template' => 'godot/menu.gd', + ], + [ + 'scope' => 'copy', + 'destination' => 'Menu.tscn', + 'template' => 'godot/Menu.tscn', + ], + [ + 'scope' => 'default', + 'destination' => 'project.godot', + 'template' => 'godot/project.godot.twig', + ], + [ + 'scope' => 'copy', + 'destination' => 'icon.svg', + 'template' => 'godot/addons/icon.svg', + ] ]; } } \ No newline at end of file diff --git a/templates/gdscript/CHANGELOG.md.twig b/templates/gdscript/CHANGELOG.md.twig new file mode 100644 index 0000000000..c176de4fcc --- /dev/null +++ b/templates/gdscript/CHANGELOG.md.twig @@ -0,0 +1 @@ +{{ sdk.changelog }} diff --git a/templates/gdscript/LICENSE.twig b/templates/gdscript/LICENSE.twig new file mode 100644 index 0000000000..418c652f66 --- /dev/null +++ b/templates/gdscript/LICENSE.twig @@ -0,0 +1 @@ +{{ sdk.licenseContent }} diff --git a/templates/gdscript/README.md.twig b/templates/gdscript/README.md.twig new file mode 100644 index 0000000000..eb9c271ff0 --- /dev/null +++ b/templates/gdscript/README.md.twig @@ -0,0 +1,49 @@ +# {{ spec.title }} {{ sdk.name }} SDK + +![License](https://img.shields.io/github/license/{{ sdk.gitUserName|url_encode }}/{{ sdk.gitRepoName|url_encode }}.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-{{ spec.version|url_encode }}-blue.svg?style=flat-square) +[![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) +{% if sdk.twitterHandle %} +[![Twitter Account](https://img.shields.io/twitter/follow/{{ sdk.twitterHandle }}?color=00acee&label=twitter&style=flat-square)](https://twitter.com/{{ sdk.twitterHandle }}) +{% endif %} +{% if sdk.discordChannel %} +[![Discord](https://img.shields.io/discord/{{ sdk.discordChannel }}?label=discord&style=flat-square)]({{ sdk.discordUrl }}) +{% endif %} +{% if sdk.warning %} + +{{ sdk.warning|raw }} +{% endif %} + +{{ sdk.description }} + +{% if sdk.logo %} +![{{ spec.title }}]({{ sdk.logo }}) +{% endif %} + +## Installation + +1. Open the AssetLib tab inside the Godot 4 editor. +2. Search for {{ spec.title }} SDK. +3. Download and install plugin. +4. Enable the plugin from Project -> Project Settings -> Plugins. + +## Getting Started + +The plugin automatically intializes itself using values from .env. You can also set them at runtime using the `set_endpoint()`, `set_project()`, `set_project_name()` and `set_self_signed()` methods. + +### Quick Test connection to Appwrite server + +```gdscript +extends Node + +func _ready(): + {{spec.title | caseUcfirst}}.ping() +``` + +## Contribution + +This library is auto-generated by Appwrite custom [SDK Generator](https://github.com/appwrite/sdk-generator). To learn more about how you can help us improve this SDK, please check the [contribution guide](https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md) before sending a pull-request. + +## License + +Please see the [{{spec.licenseName}} license]({{spec.licenseURL}}) file for more information. \ No newline at end of file diff --git a/templates/godot/src/appwrite.gd.twig b/templates/gdscript/addons/appwrite.gd.twig similarity index 100% rename from templates/godot/src/appwrite.gd.twig rename to templates/gdscript/addons/appwrite.gd.twig diff --git a/templates/godot/src/client.gd.twig b/templates/gdscript/addons/client.gd.twig similarity index 98% rename from templates/godot/src/client.gd.twig rename to templates/gdscript/addons/client.gd.twig index fec9456094..7dea2ac3ae 100644 --- a/templates/godot/src/client.gd.twig +++ b/templates/gdscript/addons/client.gd.twig @@ -6,7 +6,7 @@ var _self_signed = false var _endpoint = '{{spec.endpoint}}' var _global_headers = { 'content-type': '', - 'user-agent': '{{prefix}}{{ language.name | caseUcfirst }}SDK/{{ sdk.version }} (Godot/' + Engine.get_version_info().string + '; ' + OS.get_name() + ')', + 'user-agent': '{{prefix}}{{ language.name | caseUcfirst }}SDK/{{ sdk.version }} (Server:' + Engine.get_version_info().string + '; ' + OS.get_name() + ')', 'x-sdk-name': '{{ sdk.name }}', 'x-sdk-platform': '{{ sdk.platform }}', 'x-sdk-language': '{{ language.name | caseLower }}', diff --git a/templates/godot/src/enums/enum.gd.twig b/templates/gdscript/addons/enums/enum.gd.twig similarity index 100% rename from templates/godot/src/enums/enum.gd.twig rename to templates/gdscript/addons/enums/enum.gd.twig diff --git a/templates/godot/src/exception.gd.twig b/templates/gdscript/addons/exception.gd.twig similarity index 100% rename from templates/godot/src/exception.gd.twig rename to templates/gdscript/addons/exception.gd.twig diff --git a/templates/godot/src/icon.svg b/templates/gdscript/addons/icon.svg similarity index 100% rename from templates/godot/src/icon.svg rename to templates/gdscript/addons/icon.svg diff --git a/templates/godot/src/id.gd.twig b/templates/gdscript/addons/id.gd.twig similarity index 100% rename from templates/godot/src/id.gd.twig rename to templates/gdscript/addons/id.gd.twig diff --git a/templates/godot/src/input_file.gd.twig b/templates/gdscript/addons/input_file.gd.twig similarity index 100% rename from templates/godot/src/input_file.gd.twig rename to templates/gdscript/addons/input_file.gd.twig diff --git a/templates/godot/src/models/model.gd.twig b/templates/gdscript/addons/models/model.gd.twig similarity index 100% rename from templates/godot/src/models/model.gd.twig rename to templates/gdscript/addons/models/model.gd.twig diff --git a/templates/godot/src/operator.gd.twig b/templates/gdscript/addons/operator.gd.twig similarity index 100% rename from templates/godot/src/operator.gd.twig rename to templates/gdscript/addons/operator.gd.twig diff --git a/templates/godot/src/permission.gd.twig b/templates/gdscript/addons/permission.gd.twig similarity index 100% rename from templates/godot/src/permission.gd.twig rename to templates/gdscript/addons/permission.gd.twig diff --git a/templates/godot/src/plugin.cfg.twig b/templates/gdscript/addons/plugin.cfg.twig similarity index 100% rename from templates/godot/src/plugin.cfg.twig rename to templates/gdscript/addons/plugin.cfg.twig diff --git a/templates/godot/src/plugin.gd.twig b/templates/gdscript/addons/plugin.gd.twig similarity index 100% rename from templates/godot/src/plugin.gd.twig rename to templates/gdscript/addons/plugin.gd.twig diff --git a/templates/godot/src/query.gd.twig b/templates/gdscript/addons/query.gd.twig similarity index 100% rename from templates/godot/src/query.gd.twig rename to templates/gdscript/addons/query.gd.twig diff --git a/templates/godot/src/role.gd.twig b/templates/gdscript/addons/role.gd.twig similarity index 100% rename from templates/godot/src/role.gd.twig rename to templates/gdscript/addons/role.gd.twig diff --git a/templates/godot/src/service.gd.twig b/templates/gdscript/addons/service.gd.twig similarity index 100% rename from templates/godot/src/service.gd.twig rename to templates/gdscript/addons/service.gd.twig diff --git a/templates/godot/src/services/service.gd.twig b/templates/gdscript/addons/services/service.gd.twig similarity index 100% rename from templates/godot/src/services/service.gd.twig rename to templates/gdscript/addons/services/service.gd.twig diff --git a/templates/godot/src/tests/test_id.gd.twig b/templates/gdscript/addons/tests/test_id.gd.twig similarity index 100% rename from templates/godot/src/tests/test_id.gd.twig rename to templates/gdscript/addons/tests/test_id.gd.twig diff --git a/templates/godot/src/tests/test_input_files.gd.twig b/templates/gdscript/addons/tests/test_input_files.gd.twig similarity index 100% rename from templates/godot/src/tests/test_input_files.gd.twig rename to templates/gdscript/addons/tests/test_input_files.gd.twig diff --git a/templates/godot/src/tests/test_operator.gd.twig b/templates/gdscript/addons/tests/test_operator.gd.twig similarity index 100% rename from templates/godot/src/tests/test_operator.gd.twig rename to templates/gdscript/addons/tests/test_operator.gd.twig diff --git a/templates/godot/src/tests/test_permission.gd.twig b/templates/gdscript/addons/tests/test_permission.gd.twig similarity index 100% rename from templates/godot/src/tests/test_permission.gd.twig rename to templates/gdscript/addons/tests/test_permission.gd.twig diff --git a/templates/godot/src/tests/test_query.gd.twig b/templates/gdscript/addons/tests/test_query.gd.twig similarity index 100% rename from templates/godot/src/tests/test_query.gd.twig rename to templates/gdscript/addons/tests/test_query.gd.twig diff --git a/templates/godot/src/tests/test_roles.gd.twig b/templates/gdscript/addons/tests/test_roles.gd.twig similarity index 100% rename from templates/godot/src/tests/test_roles.gd.twig rename to templates/gdscript/addons/tests/test_roles.gd.twig diff --git a/templates/gdscript/docs/example.md.twig b/templates/gdscript/docs/example.md.twig new file mode 100644 index 0000000000..d0969e683b --- /dev/null +++ b/templates/gdscript/docs/example.md.twig @@ -0,0 +1,40 @@ +{% if method.description %}{{ method.description }}{% endif %} + +```gdscript +{% set prefix = spec.title | caseUcfirst %} +extends Node + +{% if method.responseModel and method.responseModel != 'any' %} +{% set return_type = prefix ~ (method.responseModel | caseUcfirst) %} +{% else %} +{% set return_type = 'Variant' %} +{% endif -%} + +func _ready(): + # You can skip setup if you have .env +{% if method.auth|length > 0 %} + {{prefix}}.set_endpoint('{{ spec.endpointDocs | raw }}') # Your API Endpoint +{% for node in method.auth %} +{% for key,header in node|keys %} + {{prefix}}.set_{{header | caseSnake}}('{{node[header]['x-appwrite']['demo'] | raw }}') # {{node[header].description}} +{% endfor %} +{% endfor %} +{% endif %} + +{% if method.type != 'webAuth' %} + var result = await {{prefix}}.{{ service.name | caseCamel }}.{{ method.name | caseSnake | escapeKeyword }}( +{% else %} + var result = {{prefix}}.{{ service.name | caseCamel }}.{{ method.name | caseSnake | escapeKeyword }}( +{% endif %} +{% for parameter in method.parameters.all %} + {{ parameter | paramExample }}{% if not loop.last %},{% endif %}{% if not parameter.required %} # optional{% endif %} + +{% endfor %} + ) + + if result is {{prefix}}Exception: + push_error(result.message) + + if result is {{return_type}}: + print(result.to_dict()) +``` \ No newline at end of file diff --git a/templates/godot/.editorconfig b/templates/godot/.editorconfig new file mode 100755 index 0000000000..f28239ba52 --- /dev/null +++ b/templates/godot/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +charset = utf-8 diff --git a/templates/godot/.env.twig b/templates/godot/.env.twig new file mode 100644 index 0000000000..11c92a8591 --- /dev/null +++ b/templates/godot/.env.twig @@ -0,0 +1,4 @@ +{% set prefix = spec.title | caseUpper %} +APPWRITE_ENDPOINT={{ spec.endpointDocs | raw }} +APPWRITE_PROJECT={{ spec.project | raw }} +APPWRITE_SELF_SIGNED=true \ No newline at end of file diff --git a/templates/godot/Menu.tscn b/templates/godot/Menu.tscn new file mode 100755 index 0000000000..20d7bbf2e1 --- /dev/null +++ b/templates/godot/Menu.tscn @@ -0,0 +1,61 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://menu.gd" id="1_5yleq"] +[ext_resource type="Texture2D" path="res://icon.svg" id="1_uubjt"] + +[node name="Control" type="Control" unique_id=1627331841] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 4 +script = ExtResource("1_5yleq") +metadata/_edit_use_anchors_ = true + +[node name="TextureRect" type="TextureRect" parent="." unique_id=1629265256] +layout_mode = 1 +anchors_preset = 5 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -64.0 +offset_top = 32.0 +offset_right = 64.0 +offset_bottom = 160.0 +grow_horizontal = 2 +texture = ExtResource("1_uubjt") + +[node name="Status" type="Label" parent="." unique_id=244730041] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -47.5 +offset_top = -101.5 +offset_right = 47.5 +offset_bottom = -78.5 +grow_horizontal = 2 +grow_vertical = 2 +text = "Ping Status: " +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="Ping" type="Button" parent="." unique_id=52935639] +layout_mode = 1 +anchors_preset = -1 +anchor_left = 0.45000002 +anchor_top = 0.476 +anchor_right = 0.55 +anchor_bottom = 0.52400005 +offset_left = -28.400024 +offset_top = -9.947998 +offset_right = 22.399963 +offset_bottom = 5.9479675 +grow_horizontal = 2 +grow_vertical = 2 +text = "Ping Appwrite" + +[connection signal="pressed" from="Ping" to="." method="_on_ping_click"] diff --git a/templates/godot/README.md.twig b/templates/godot/README.md.twig index d0366f3af8..eb9c271ff0 100644 --- a/templates/godot/README.md.twig +++ b/templates/godot/README.md.twig @@ -2,11 +2,19 @@ ![License](https://img.shields.io/github/license/{{ sdk.gitUserName|url_encode }}/{{ sdk.gitRepoName|url_encode }}.svg?style=flat-square) ![Version](https://img.shields.io/badge/api%20version-{{ spec.version|url_encode }}-blue.svg?style=flat-square) +[![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) {% if sdk.twitterHandle %} [![Twitter Account](https://img.shields.io/twitter/follow/{{ sdk.twitterHandle }}?color=00acee&label=twitter&style=flat-square)](https://twitter.com/{{ sdk.twitterHandle }}) {% endif %} +{% if sdk.discordChannel %} +[![Discord](https://img.shields.io/discord/{{ sdk.discordChannel }}?label=discord&style=flat-square)]({{ sdk.discordUrl }}) +{% endif %} +{% if sdk.warning %} + +{{ sdk.warning|raw }} +{% endif %} -**{{ sdk.description }}** +{{ sdk.description }} {% if sdk.logo %} ![{{ spec.title }}]({{ sdk.logo }}) @@ -14,45 +22,22 @@ ## Installation -1. Copy the `addons/{{spec.title | caseSnake}}` folder into your Godot project's `res://addons/` directory. -2. Enable the plugin in **Project Settings** > **Plugins**. -3. The SDK is now available as a global singleton named `{{spec.title | caseUcfirst}}`. +1. Open the AssetLib tab inside the Godot 4 editor. +2. Search for {{ spec.title }} SDK. +3. Download and install plugin. +4. Enable the plugin from Project -> Project Settings -> Plugins. ## Getting Started -In Godot 4, the SDK uses asynchronous coroutines via the `await` keyword. You can use the global `{{spec.title | caseUcfirst}}` singleton or initialize the `{{spec.title | caseUcfirst}}Client` manually. - -### Using the Autoload Singleton - -```gdscript -extends Node - -func _ready(): - # Initialize the global {{spec.title | caseUcfirst}} singleton - {{spec.title | caseUcfirst}}.set_endpoint('https://cloud.appwrite.io/v1') - {{spec.title | caseUcfirst}}.set_project('YOUR_PROJECT_ID') - - # Call an API method asynchronously - var response = await {{spec.title | caseUcfirst}}.account.get() - - if response == null: - print("An error occurred") - else: - print("Logged in as: ", response.name) -``` +The plugin automatically intializes itself using values from .env. You can also set them at runtime using the `set_endpoint()`, `set_project()`, `set_project_name()` and `set_self_signed()` methods. -### Manual Initialization +### Quick Test connection to Appwrite server ```gdscript extends Node func _ready(): - var client = {{spec.title | caseUcfirst}}Client.new() - client.set_endpoint('https://cloud.appwrite.io/v1') - client.set_project('YOUR_PROJECT_ID') - - var account = {{spec.title | caseUcfirst}}Account.new(client) - var response = await account.get() + {{spec.title | caseUcfirst}}.ping() ``` ## Contribution diff --git a/templates/godot/addons/appwrite.gd.twig b/templates/godot/addons/appwrite.gd.twig new file mode 100644 index 0000000000..bcb183780a --- /dev/null +++ b/templates/godot/addons/appwrite.gd.twig @@ -0,0 +1,90 @@ +{% set prefix = spec.title | caseUcfirst %} +extends Node +## [img width=32]res://addons/{{spec.title | caseSnake}}/icon.svg[/img] {{ spec.description | default("Appwrite _client.") | replace({"\n": "[br]\n## "}) }} + +# Internal classes for type hinting +{% for service in spec.services %} +const _{{ service.name | caseUpper }} = preload("services/{{ service.name | caseSnake }}.gd") +{% endfor %} + +var _client := preload("client.gd").new() +{% for service in spec.services %} +var {{ service.name | caseCamel }} : _{{ service.name | caseUpper }} = _{{ service.name | caseUpper }}.new(_client) ## {{ service.description | default("No description is provided") }} +{% endfor %} + +# Exposing the Enums +{% for enum in spec.allEnums %} +{%~ set enumName = enum.name %} +const {{ enumName | caseUpper }} = preload("enums/{{ enumName | caseSnake }}.gd") +{% endfor %} + +func _ready() -> void: + var path = "res://.env" + if not FileAccess.file_exists(path): + return + + var file = FileAccess.open(path, FileAccess.READ) + while not file.eof_reached(): + var line = file.get_line().strip_edges() + if line.is_empty() or line.begins_with("#"): + continue + + var parts = line.split("=", true, 1) + if parts.size() != 2: + continue + + var key = parts[0].strip_edges() + var value = parts[1].strip_edges() + + # Remove quotes if present + if (value.begins_with("\"") and value.ends_with("\"")) or (value.begins_with("'") and value.ends_with("'")): + value = value.substr(1, value.length() - 2) + + _apply_env(key, value) + + +{% for header in spec.global.headers %} +{% if header.description %} +## {{header.description}} +{% endif %} +func set_{{header.key | caseSnake}}(value: String) -> void: + _client._set_{{header.key | caseSnake}}(value) + + +{% endfor %} +## Set self signed status +func set_self_signed(status: bool = true) -> void: + _client._set_self_signed(status) + + +## Set the endpoint +func set_endpoint(endpoint: String) -> void: + _client._set_endpoint(endpoint) + + +## Add a header +func add_header(key: String, value: String) -> void: + _client._add_header(key, value) + + +## Get all headers +func get_headers() -> Dictionary: + return _client._get_headers() + + +func _apply_env(key: String, value: String) -> void: + match key: + "{{ prefix | caseUpper }}_ENDPOINT": + _client._set_endpoint(value) + "{{ prefix | caseUpper }}_SELF_SIGNED": + _client._set_self_signed(value.to_lower() == "true") +{% for header in spec.global.headers %} + "{{ prefix | caseUpper }}_{{ header.key | caseUpper }}": + _client._set_{{ header.key | caseSnake }}(value) +{% endfor %} + + +## Ping {{prefix}} Server for testing connection +func ping() -> String: + var response = await _client._call_api('GET', '/ping') + return str(response.get('body')) \ No newline at end of file diff --git a/templates/godot/addons/client.gd.twig b/templates/godot/addons/client.gd.twig new file mode 100644 index 0000000000..5ee28dc88d --- /dev/null +++ b/templates/godot/addons/client.gd.twig @@ -0,0 +1,163 @@ +# {% set prefix = spec.title | caseUcfirst %}Client +extends RefCounted + +var _chunk_size = 5 * 1024 * 1024 +var _self_signed = false +var _endpoint = '{{spec.endpoint}}' +var _global_headers = { + 'content-type': '', + 'user-agent': '{{prefix}}{{ language.name | caseUcfirst }}SDK/{{ sdk.version }} (Client:' + Engine.get_version_info().string + '; ' + OS.get_name() + ' ' + OS.get_model_name() + ')', + 'x-sdk-name': '{{ sdk.name }}', + 'x-sdk-platform': '{{ sdk.platform }}', + 'x-sdk-language': '{{ language.name | caseLower }}', + 'x-sdk-version': '{{ sdk.version }}', +{% for key,header in spec.global.defaultHeaders %} + '{{key}}': '{{header}}', +{% endfor %} +} + +func _set_self_signed(status: bool = true) -> RefCounted: + _self_signed = status + return self + + +func _set_endpoint(endpoint: String) -> RefCounted: + _endpoint = endpoint + return self + + +func _add_header(key: String, value: String) -> RefCounted: + _global_headers[key.to_lower()] = value + return self + + +func _get_headers() -> Dictionary: + return _global_headers.duplicate() + + +{% for header in spec.global.headers %} +func _set_{{header.key | caseSnake}}(value: String) -> RefCounted: + _global_headers['{{header.name|lower}}'] = value + return self + + +{% endfor %} + + +func _call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: + if not _ensure_configured(): + return {"statusCode": 400, "body": "Missing/wrong configuration. Please check logs for more info"} + var http := HTTPRequest.new() + + var tls_options := TLSOptions.client_unsafe() if _self_signed else TLSOptions.client() + http.set_tls_options(tls_options) + + Engine.get_main_loop().root.add_child.call_deferred(http) + await http.tree_entered + + # Merge headers + var combined_headers := _global_headers.duplicate() + for key in headers: + combined_headers[key.to_lower()] = headers[key] + + # Choose HTTP method constant + var http_method: int + match method.to_upper(): + "POST": http_method = HTTPClient.METHOD_POST + "PUT": http_method = HTTPClient.METHOD_PUT + "PATCH": http_method = HTTPClient.METHOD_PATCH + "DELETE": http_method = HTTPClient.METHOD_DELETE + _: http_method = HTTPClient.METHOD_GET + + var uri := _endpoint.trim_suffix("/") + var request_path := path + var body := PackedByteArray() + + if http_method in [HTTPClient.METHOD_GET, HTTPClient.METHOD_DELETE]: + if not params.is_empty(): + var query_http := HTTPClient.new() + request_path += "?" + query_http.query_string_from_dict(params) + else: + var has_files := false + for key in params: + if params[key] is {{prefix}}InputFile: + has_files = true + break + + if has_files: + var boundary := "Boundary-%x" % Time.get_ticks_msec() + combined_headers["content-type"] = "multipart/form-data; boundary=" + boundary + body = _build_multipart(params, boundary) + else: + var body_str := JSON.stringify(params) + body = body_str.to_utf8_buffer() + combined_headers["content-type"] = "application/json" + + combined_headers["content-length"] = str(body.size()) + + # Build header array + var header_list := PackedStringArray() + for key in combined_headers: + if combined_headers[key] != "": + header_list.append(key + ": " + str(combined_headers[key])) + + var err = http.request_raw(uri + request_path, header_list, http_method, body) + if err != OK: + http.queue_free() + return {"statusCode": 0, "error": "Request failed: " + error_string(err)} + + var response = await http.request_completed + http.queue_free() + + var request_result: int = response[0] + var response_code: int = response[1] + var response_body: PackedByteArray = response[3] + + if request_result != HTTPRequest.RESULT_SUCCESS: + return {"statusCode": 0, "error": "HTTP Request Error: " + str(request_result)} + + var response_text := response_body.get_string_from_utf8() + var json := JSON.new() + if json.parse(response_text) == OK: + return {"statusCode": response_code, "body": json.data} + else: + return {"statusCode": response_code, "body": response_text} + + +func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: + var body := PackedByteArray() + for key in params: + var value = params[key] + if value == null: continue + + body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) + + if value is {{prefix}}InputFile: + body.append_array(("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" % [key, value.filename]).to_utf8_buffer()) + var c_type = value.content_type if value.content_type != "" else "application/octet-stream" + body.append_array(("Content-Type: %s\r\n\r\n" % c_type).to_utf8_buffer()) + body.append_array(value.get_data()) + elif value is Array: + for i in range(value.size()): + if i > 0: + body.append_array(("\r\n--" + boundary + "\r\n").to_utf8_buffer()) + body.append_array(("Content-Disposition: form-data; name=\"%s[]\"\r\n\r\n" % key).to_utf8_buffer()) + body.append_array((str(value[i])).to_utf8_buffer()) + else: + body.append_array(("Content-Disposition: form-data; name=\"%s\"\r\n\r\n" % key).to_utf8_buffer()) + body.append_array((str(value)).to_utf8_buffer()) + + body.append_array(("\r\n").to_utf8_buffer()) + + body.append_array(("--" + boundary + "--\r\n").to_utf8_buffer()) + return body + + +func _ensure_configured() -> bool: + if _endpoint == "": + push_warning("{{prefix}}: endpoint is missing. Use set_endpoint or .env.") + return false + if not _global_headers.has("x-appwrite-project"): + push_warning("{{prefix}}: project id is missing. Use set_project or .env.") + return false + return true \ No newline at end of file diff --git a/templates/godot/addons/enums/enum.gd.twig b/templates/godot/addons/enums/enum.gd.twig new file mode 100644 index 0000000000..040e0bc00a --- /dev/null +++ b/templates/godot/addons/enums/enum.gd.twig @@ -0,0 +1,19 @@ +## Enum: {{ enum.name | caseUpper }} + +{% for value in enum.enum %} +{% set key = enum.keys is empty ? value : enum.keys[loop.index0] %} +const {{ key | caseEnumKey }} = "{{ value }}" +{% endfor %} + +## Validate if value is in enum +static func is_valid(value: String) -> bool: + return value in values() + + +## Get all values of enum +static func values() -> Array[String]: + return [ + {% for value in enum.enum %} + "{{ value }}", + {% endfor %} + ] \ No newline at end of file diff --git a/templates/godot/addons/exception.gd.twig b/templates/godot/addons/exception.gd.twig new file mode 100644 index 0000000000..dba708dd68 --- /dev/null +++ b/templates/godot/addons/exception.gd.twig @@ -0,0 +1,17 @@ +class_name {{spec.title | caseUcfirst}}Exception + +var message: String ## Error message +var code: int ## Error code +var type: String ## Error type +var response: String ## Full error response + +func _init(p_message: String = "", p_code: int = 0, p_type: String = "", p_response: String = "") -> void: + self.message = p_message + self.code = p_code + self.type = p_type + self.response = p_response + +func _to_string() -> String: + if message == "": + return "{{ spec.title | caseUcfirst }}Exception" + return "{{ spec.title | caseUcfirst }}Exception: " + type + ", " + message + " (" + str(code) + ")" diff --git a/templates/godot/addons/icon.svg b/templates/godot/addons/icon.svg new file mode 100644 index 0000000000..1e37b46dea --- /dev/null +++ b/templates/godot/addons/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/godot/addons/id.gd.twig b/templates/godot/addons/id.gd.twig new file mode 100644 index 0000000000..9adef4794d --- /dev/null +++ b/templates/godot/addons/id.gd.twig @@ -0,0 +1,24 @@ +class_name {{spec.title | caseUcfirst}}ID + +static func _hex_timestamp() -> String: + var now := Time.get_unix_time_from_system() + var sec := int(floor(now)) + var usec := int((now - sec) * 1000000) + var sec_hex := "%x" % sec + var usec_hex := "%05x" % usec + return sec_hex + usec_hex + + +## Generate a unique ID with padding to have a longer ID +static func unique(padding: int = 7) -> String: + var id := _hex_timestamp() + if padding > 0: + var sb := "" + for i in range(padding): + sb += "%x" % (randi() % 16) + id += sb + return id + + +static func custom(id: String) -> String: + return id diff --git a/templates/godot/addons/input_file.gd.twig b/templates/godot/addons/input_file.gd.twig new file mode 100644 index 0000000000..ad3dfdf461 --- /dev/null +++ b/templates/godot/addons/input_file.gd.twig @@ -0,0 +1,35 @@ +## Input File class used to pass files to the SDK +class_name {{spec.title | caseUcfirst}}InputFile + +var path: String ## Path to the file +var bytes: PackedByteArray ## File bytes +var filename: String ## File name +var content_type: String ## File content type + +func _init(p_path: String = "", p_bytes: PackedByteArray = PackedByteArray(), p_filename: String = "", p_content_type: String = "") -> void: + self.path = p_path + self.bytes = p_bytes + self.filename = p_filename + self.content_type = p_content_type + + if self.filename == "" and self.path != "": + self.filename = self.path.get_file() + + +## Creates a new InputFile from a file path +static func from_path(p_path: String, p_filename: String = "", p_content_type: String = "") -> {{spec.title | caseUcfirst}}InputFile: + return {{spec.title | caseUcfirst}}InputFile.new(p_path, PackedByteArray(), p_filename, p_content_type) + + +## Creates a new InputFile from bytes +static func from_bytes(p_bytes: PackedByteArray, p_filename: String, p_content_type: String = "") -> {{spec.title | caseUcfirst}}InputFile: + return {{spec.title | caseUcfirst}}InputFile.new("", p_bytes, p_filename, p_content_type) + + +## Return the PackedByteArray of the file +func get_data() -> PackedByteArray: + if not bytes.is_empty(): + return bytes + if not path.is_empty(): + return FileAccess.get_file_as_bytes(path) + return PackedByteArray() diff --git a/templates/godot/addons/models/model.gd.twig b/templates/godot/addons/models/model.gd.twig new file mode 100644 index 0000000000..b1d8e1ec61 --- /dev/null +++ b/templates/godot/addons/models/model.gd.twig @@ -0,0 +1,113 @@ +{% set prefix = spec.title | caseUcfirst %} +class_name {{ prefix }}{{ definition.name | caseUcfirst }} +extends RefCounted +## {{ definition.description | default("Model class.") | replace({"\n": "\n## "}) }}[br] + +{% set imports = [] %} +{%~ for property in definition.properties %} +{%~ set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (definition.name ~ (property.name | caseUcfirst)) %} +{%~ if property.enum or property.items.enum is defined %} +{%~ if enumName not in imports %} +const _{{ enumName | caseUcfirst }} := preload("res://addons/{{ spec.title | caseSnake }}/enums/{{ enumName | caseSnake }}.gd") +{% set imports = imports|merge([enumName]) %} +{%~ endif %} +{%~ endif %} +{%~ endfor %} + +const _FIELD_MAP := { +{% for property in definition.properties %} + "{{ property.name | uniqueSnake }}": "{{ property.name }}", +{% endfor %} +} + +{% for property in definition.properties %} +{% set baseType = property | typeName(spec) %} +var {{ property.name | uniqueSnake }}: {{ baseType }} ## {{ property.description | default("No description provided.") }} +{% endfor %} + +## Convert dictionary to model +static func from_dict(dict: Dictionary): + var m := {{ prefix }}{{ definition.name | caseUcfirst }}.new() + + for key in _FIELD_MAP: + var raw_key = _FIELD_MAP[key] + var value = dict.get(raw_key) + +{% for property in definition.properties %} +{% set field = property.name | uniqueSnake %} +{% set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (definition.name ~ (property.name | caseUcfirst)) %} +{% if property.model %} + if key == "{{ field }}" and value is Dictionary: + m.set(key, {{ property.model | caseUcfirst }}.from_dict(value)) + continue +{% endif %} +{% if property.array.model %} + if key == "{{ field }}" and value is Array: + var list := [] + for item in value: + if item is Dictionary: + list.append({{ property.array.model | caseUcfirst }}.from_dict(item)) + else: + list.append(item) + m.set(key, list) + continue +{% endif %} +{% if property.enum %} + if key == "{{ field }}" and value != null: + if not _{{ enumName | caseUcfirst }}.is_valid(value): + push_error("Invalid enum value for {{ field }}: %s" % value) + return {{prefix}}Exception.new("Invalid enum value for {{ field }}: %s" % value, 0, "invalid_enum_value", str(dict)) + m.set(key, value) + continue +{% endif %} +{% if property.type == 'array' and property.items.enum is defined %} + if key == "{{ field }}" and value is Array: + var list := [] + for item in value: + if not _{{ enumName | caseUcfirst }}.is_valid(item): + push_error("Invalid enum value: %s" % item) + return {{prefix}}Exception.new("Invalid enum value for {{ field }}: %s" % value, 0, "invalid_enum_value", str(dict)) + list.append(item) + m.set(key, list) + continue +{% endif %} +{% if property.type == 'array' and not property.array.model %} + if key == "{{ field }}" and value is Array: + m.set(key, value) + continue +{% endif %} +{% endfor %} + + m.set(key, value) + + return m + + +## Convert to Dictionary +func to_dict() -> Dictionary: + var dict := {} + + for key in _FIELD_MAP: + var value = get(key) + +{% for property in definition.properties %} +{% set field = property.name | uniqueSnake %} +{% if property.model %} + if key == "{{ field }}" and value != null: + dict[_FIELD_MAP[key]] = value.to_dict() + continue +{% endif %} +{% if property.array.model %} + if key == "{{ field }}" and value is Array: + var list := [] + for item in value: + if item != null: + list.append(item.to_dict()) + dict[_FIELD_MAP[key]] = list + continue +{% endif %} +{% endfor %} + + dict[_FIELD_MAP[key]] = value + + return dict \ No newline at end of file diff --git a/templates/godot/addons/operator.gd.twig b/templates/godot/addons/operator.gd.twig new file mode 100644 index 0000000000..85534b7fa3 --- /dev/null +++ b/templates/godot/addons/operator.gd.twig @@ -0,0 +1,208 @@ +class_name {{spec.title | caseUcfirst}}Operator +extends RefCounted + +const EQUAL := "equal" +const NOT_EQUAL := "notEqual" +const GREATER_THAN := "greaterThan" +const GREATER_THAN_EQUAL := "greaterThanEqual" +const LESS_THAN := "lessThan" +const LESS_THAN_EQUAL := "lessThanEqual" +const CONTAINS := "contains" +const IS_NULL := "isNull" +const IS_NOT_NULL := "isNotNull" + + +var method: String +var values: Array + + +## Validate condition value +static func is_valid(value: String) -> bool: + return value in [ + EQUAL, + NOT_EQUAL, + GREATER_THAN, + GREATER_THAN_EQUAL, + LESS_THAN, + LESS_THAN_EQUAL, + CONTAINS, + IS_NULL, + IS_NOT_NULL + ] + + +## Constructor +func _init(_method: String, _values: Variant = null) -> void: + method = _method + + if _values != null: + if _values is Array: + values = _values + else: + values = [_values] + else: + values = [] + + +## Convert operator object to JSON string +func _to_string() -> String: + return JSON.stringify({ + "method": method, + "values": values + }) + + +static func increment(value: float = 1, max: Variant = null) -> String: + if not is_finite(value): + push_error("Value cannot be NaN or Infinity") + return "" + + if max != null and not is_finite(max): + push_error("Max cannot be NaN or Infinity") + return "" + + var vals := [value] + if max != null: + vals.append(max) + + return AppwriteOperator.new("increment", vals).to_string() + + +static func decrement(value: float = 1, min: Variant = null) -> String: + if not is_finite(value): + push_error("Value cannot be NaN or Infinity") + return "" + + if min != null and not is_finite(min): + push_error("Min cannot be NaN or Infinity") + return "" + + var vals := [value] + if min != null: + vals.append(min) + + return AppwriteOperator.new("decrement", vals).to_string() + + +static func multiply(factor: float, max: Variant = null) -> String: + if not is_finite(factor): + push_error("Factor cannot be NaN or Infinity") + return "" + + if max != null and not is_finite(max): + push_error("Max cannot be NaN or Infinity") + return "" + + var vals := [factor] + if max != null: + vals.append(max) + + return AppwriteOperator.new("multiply", vals).to_string() + + +static func divide(divisor: float, min: Variant = null) -> String: + if not is_finite(divisor): + push_error("Divisor cannot be NaN or Infinity") + return "" + + if divisor == 0: + push_error("Divisor cannot be zero") + return "" + + if min != null and not is_finite(min): + push_error("Min cannot be NaN or Infinity") + return "" + + var vals := [divisor] + if min != null: + vals.append(min) + + return AppwriteOperator.new("divide", vals).to_string() + + +static func modulo(divisor: float) -> String: + if not is_finite(divisor): + push_error("Divisor cannot be NaN or Infinity") + return "" + + if divisor == 0: + push_error("Divisor cannot be zero") + return "" + + return AppwriteOperator.new("modulo", [divisor]).to_string() + + +static func power(exponent: float, max: Variant = null) -> String: + if not is_finite(exponent): + push_error("Exponent cannot be NaN or Infinity") + return "" + + if max != null and not is_finite(max): + push_error("Max cannot be NaN or Infinity") + return "" + + var vals := [exponent] + if max != null: + vals.append(max) + + return AppwriteOperator.new("power", vals).to_string() + + +static func array_append(values: Array) -> String: + return AppwriteOperator.new("arrayAppend", values).to_string() + + +static func array_prepend(values: Array) -> String: + return AppwriteOperator.new("arrayPrepend", values).to_string() + + +static func array_insert(index: int, value: Variant) -> String: + return AppwriteOperator.new("arrayInsert", [index, value]).to_string() + + +static func array_remove(value: Variant) -> String: + return AppwriteOperator.new("arrayRemove", [value]).to_string() + + +static func array_unique() -> String: + return AppwriteOperator.new("arrayUnique", []).to_string() + + +static func array_intersect(values: Array) -> String: + return AppwriteOperator.new("arrayIntersect", values).to_string() + + +static func array_diff(values: Array) -> String: + return AppwriteOperator.new("arrayDiff", values).to_string() + + +static func array_filter(condition: String, value: Variant = null) -> String: + if not is_valid(condition): + push_error("Invalid condition: %s" % condition) + return "" + + return AppwriteOperator.new("arrayFilter", [condition, value]).to_string() + + +static func string_concat(value: Variant) -> String: + return AppwriteOperator.new("stringConcat", [value]).to_string() + + +static func string_replace(search: String, replace: String) -> String: + return AppwriteOperator.new("stringReplace", [search, replace]).to_string() + + +static func toggle() -> String: + return AppwriteOperator.new("toggle", []).to_string() + + +static func date_add_days(days: int) -> String: + return AppwriteOperator.new("dateAddDays", [days]).to_string() + + +static func date_sub_days(days: int) -> String: + return AppwriteOperator.new("dateSubDays", [days]).to_string() + + +static func date_set_now() -> String: + return AppwriteOperator.new("dateSetNow", []).to_string() \ No newline at end of file diff --git a/templates/godot/addons/permission.gd.twig b/templates/godot/addons/permission.gd.twig new file mode 100644 index 0000000000..6b5dc53dae --- /dev/null +++ b/templates/godot/addons/permission.gd.twig @@ -0,0 +1,21 @@ +class_name {{spec.title | caseUcfirst}}Permission + +## Read permission +static func read(role: String) -> String: + return 'read("%s")' % role + +## Write permission +static func write(role: String) -> String: + return 'write("%s")' % role + +## Create permission +static func create(role: String) -> String: + return 'create("%s")' % role + +## Update permission +static func update(role: String) -> String: + return 'update("%s")' % role + +## Delete permission +static func delete(role: String) -> String: + return 'delete("%s")' % role diff --git a/templates/godot/addons/plugin.cfg.twig b/templates/godot/addons/plugin.cfg.twig new file mode 100644 index 0000000000..0f22c61214 --- /dev/null +++ b/templates/godot/addons/plugin.cfg.twig @@ -0,0 +1,7 @@ +[plugin] + +name="{{ spec.title | caseUcfirst }}" +description="{{ spec.shortDescription }}" +author="{{ spec.contactName }}" +version="{{ spec.version }}" +script="plugin.gd" diff --git a/templates/godot/addons/plugin.gd.twig b/templates/godot/addons/plugin.gd.twig new file mode 100644 index 0000000000..0f8b33bf97 --- /dev/null +++ b/templates/godot/addons/plugin.gd.twig @@ -0,0 +1,13 @@ +@tool +extends EditorPlugin + +const AUTOLOAD_NAME : String = "{{spec.title | caseUcfirst}}" +const AUTOLOAD_PATH : String = "res://addons/{{spec.title | caseSnake}}/{{spec.title | caseLower}}.gd" + +func _enable_plugin() -> void: + add_autoload_singleton(AUTOLOAD_NAME, AUTOLOAD_PATH) + + +func _disable_plugin() -> void: + remove_autoload_singleton(AUTOLOAD_NAME) + diff --git a/templates/godot/addons/query.gd.twig b/templates/godot/addons/query.gd.twig new file mode 100644 index 0000000000..d50a9133f2 --- /dev/null +++ b/templates/godot/addons/query.gd.twig @@ -0,0 +1,198 @@ +class_name {{spec.title | caseUcfirst}}Query + +var method: String +var attribute: Variant +var values: Variant + +func _init(p_method: String, p_attribute: Variant = null, p_values: Variant = null) -> void: + self.method = p_method + self.attribute = p_attribute + self.values = p_values + +func to_dict() -> Dictionary: + var result := { + "method": method + } + if attribute != null: + result["attribute"] = attribute + if values != null: + if values is Array: + result["values"] = values + else: + result["values"] = [values] + return result + +func _to_string() -> String: + return JSON.stringify(to_dict()) + +static func equal(attribute: String, value: Variant) -> String: + return {{spec.title | caseUcfirst}}Query.new("equal", attribute, value)._to_string() + +static func notEqual(attribute: String, value: Variant) -> String: + return {{spec.title | caseUcfirst}}Query.new("notEqual", attribute, value)._to_string() + +static func regex(attribute: String, pattern: String) -> String: + return {{spec.title | caseUcfirst}}Query.new("regex", attribute, pattern)._to_string() + +static func lessThan(attribute: String, value: Variant) -> String: + return {{spec.title | caseUcfirst}}Query.new("lessThan", attribute, value)._to_string() + +static func lessThanEqual(attribute: String, value: Variant) -> String: + return {{spec.title | caseUcfirst}}Query.new("lessThanEqual", attribute, value)._to_string() + +static func greaterThan(attribute: String, value: Variant) -> String: + return {{spec.title | caseUcfirst}}Query.new("greaterThan", attribute, value)._to_string() + +static func greaterThanEqual(attribute: String, value: Variant) -> String: + return {{spec.title | caseUcfirst}}Query.new("greaterThanEqual", attribute, value)._to_string() + +static func search(attribute: String, value: String) -> String: + return {{spec.title | caseUcfirst}}Query.new("search", attribute, value)._to_string() + +static func isNull(attribute: String) -> String: + return {{spec.title | caseUcfirst}}Query.new("isNull", attribute)._to_string() + +static func isNotNull(attribute: String) -> String: + return {{spec.title | caseUcfirst}}Query.new("isNotNull", attribute)._to_string() + +static func exists(attributes: Array) -> String: + return {{spec.title | caseUcfirst}}Query.new("exists", null, attributes)._to_string() + +static func notExists(attributes: Array) -> String: + return {{spec.title | caseUcfirst}}Query.new("notExists", null, attributes)._to_string() + +static func between(attribute: String, start: Variant, end: Variant) -> String: + return {{spec.title | caseUcfirst}}Query.new("between", attribute, [start, end])._to_string() + +static func startsWith(attribute: String, value: String) -> String: + return {{spec.title | caseUcfirst}}Query.new("startsWith", attribute, value)._to_string() + +static func endsWith(attribute: String, value: String) -> String: + return {{spec.title | caseUcfirst}}Query.new("endsWith", attribute, value)._to_string() + +static func contains(attribute: String, value: Variant) -> String: + return {{spec.title | caseUcfirst}}Query.new("contains", attribute, value)._to_string() + +static func containsAny(attribute: String, value: Array) -> String: + return {{spec.title | caseUcfirst}}Query.new("containsAny", attribute, value)._to_string() + +static func containsAll(attribute: String, value: Array) -> String: + return {{spec.title | caseUcfirst}}Query.new("containsAll", attribute, value)._to_string() + +static func notContains(attribute: String, value: Variant) -> String: + return {{spec.title | caseUcfirst}}Query.new("notContains", attribute, value)._to_string() + +static func notSearch(attribute: String, value: String) -> String: + return {{spec.title | caseUcfirst}}Query.new("notSearch", attribute, value)._to_string() + +static func notBetween(attribute: String, start: Variant, end: Variant) -> String: + return {{spec.title | caseUcfirst}}Query.new("notBetween", attribute, [start, end])._to_string() + +static func notStartsWith(attribute: String, value: String) -> String: + return {{spec.title | caseUcfirst}}Query.new("notStartsWith", attribute, value)._to_string() + +static func notEndsWith(attribute: String, value: String) -> String: + return {{spec.title | caseUcfirst}}Query.new("notEndsWith", attribute, value)._to_string() + +static func createdBefore(value: String) -> String: + return lessThan("$createdAt", value) + +static func createdAfter(value: String) -> String: + return greaterThan("$createdAt", value) + +static func createdBetween(start: String, end: String) -> String: + return between("$createdAt", start, end) + +static func updatedBefore(value: String) -> String: + return lessThan("$updatedAt", value) + +static func updatedAfter(value: String) -> String: + return greaterThan("$updatedAt", value) + +static func updatedBetween(start: String, end: String) -> String: + return between("$updatedAt", start, end) + +static func or_query(queries: Array) -> String: + var parsed_queries := [] + for q in queries: + var parsed = JSON.parse_string(q) + if parsed != null: + parsed_queries.append(parsed) + return {{spec.title | caseUcfirst}}Query.new("or", null, parsed_queries)._to_string() + +static func and_query(queries: Array) -> String: + var parsed_queries := [] + for q in queries: + var parsed = JSON.parse_string(q) + if parsed != null: + parsed_queries.append(parsed) + return {{spec.title | caseUcfirst}}Query.new("and", null, parsed_queries)._to_string() + +static func elemMatch(attribute: String, queries: Array) -> String: + var parsed_queries := [] + for q in queries: + var parsed = JSON.parse_string(q) + if parsed != null: + parsed_queries.append(parsed) + return {{spec.title | caseUcfirst}}Query.new("elemMatch", attribute, parsed_queries)._to_string() + +static func select(attributes: Array) -> String: + return {{spec.title | caseUcfirst}}Query.new("select", null, attributes)._to_string() + +static func orderAsc(attribute: String) -> String: + return {{spec.title | caseUcfirst}}Query.new("orderAsc", attribute)._to_string() + +static func orderDesc(attribute: String) -> String: + return {{spec.title | caseUcfirst}}Query.new("orderDesc", attribute)._to_string() + +static func orderRandom() -> String: + return {{spec.title | caseUcfirst}}Query.new("orderRandom")._to_string() + +static func cursorBefore(id: String) -> String: + return {{spec.title | caseUcfirst}}Query.new("cursorBefore", null, id)._to_string() + +static func cursorAfter(id: String) -> String: + return {{spec.title | caseUcfirst}}Query.new("cursorAfter", null, id)._to_string() + +static func limit(p_limit: int) -> String: + return {{spec.title | caseUcfirst}}Query.new("limit", null, p_limit)._to_string() + +static func offset(p_offset: int) -> String: + return {{spec.title | caseUcfirst}}Query.new("offset", null, p_offset)._to_string() + +static func distanceEqual(attribute: String, values: Array, distance: float, meters: bool = true) -> String: + return {{spec.title | caseUcfirst}}Query.new("distanceEqual", attribute, [[values, distance, meters]])._to_string() + +static func distanceNotEqual(attribute: String, values: Array, distance: float, meters: bool = true) -> String: + return {{spec.title | caseUcfirst}}Query.new("distanceNotEqual", attribute, [[values, distance, meters]])._to_string() + +static func distanceGreaterThan(attribute: String, values: Array, distance: float, meters: bool = true) -> String: + return {{spec.title | caseUcfirst}}Query.new("distanceGreaterThan", attribute, [[values, distance, meters]])._to_string() + +static func distanceLessThan(attribute: String, values: Array, distance: float, meters: bool = true) -> String: + return {{spec.title | caseUcfirst}}Query.new("distanceLessThan", attribute, [[values, distance, meters]])._to_string() + +static func intersects(attribute: String, values: Array) -> String: + return {{spec.title | caseUcfirst}}Query.new("intersects", attribute, [values])._to_string() + +static func notIntersects(attribute: String, values: Array) -> String: + return {{spec.title | caseUcfirst}}Query.new("notIntersects", attribute, [values])._to_string() + +static func crosses(attribute: String, values: Array) -> String: + return {{spec.title | caseUcfirst}}Query.new("crosses", attribute, [values])._to_string() + +static func notCrosses(attribute: String, values: Array) -> String: + return {{spec.title | caseUcfirst}}Query.new("notCrosses", attribute, [values])._to_string() + +static func overlaps(attribute: String, values: Array) -> String: + return {{spec.title | caseUcfirst}}Query.new("overlaps", attribute, [values])._to_string() + +static func notOverlaps(attribute: String, values: Array) -> String: + return {{spec.title | caseUcfirst}}Query.new("notOverlaps", attribute, [values])._to_string() + +static func touches(attribute: String, values: Array) -> String: + return {{spec.title | caseUcfirst}}Query.new("touches", attribute, [values])._to_string() + +static func notTouches(attribute: String, values: Array) -> String: + return {{spec.title | caseUcfirst}}Query.new("notTouches", attribute, [values])._to_string() + diff --git a/templates/godot/addons/role.gd.twig b/templates/godot/addons/role.gd.twig new file mode 100644 index 0000000000..d73b2e3829 --- /dev/null +++ b/templates/godot/addons/role.gd.twig @@ -0,0 +1,40 @@ +class_name {{spec.title | caseUcfirst}}Role + + +## Any role +static func any() -> String: + return 'any' + + +## User role +static func user(id: String, status: String = '') -> String: + if status == '': + return 'user:%s' % id + return 'user:%s/%s' % [id, status] + + +## Users role +static func users(status: String = '') -> String: + if status == '': + return 'users' + return 'users/%s' % status + + +## Guests role +static func guests() -> String: + return 'guests' + + +## Team role +static func team(id: String, role: String = '') -> String: + if role == '': + return 'team:%s' % id + return 'team:%s/%s' % [id, role] + +## Team member role +static func member(id: String) -> String: + return 'member:%s' % id + + +static func label(name: String) -> String: + return 'label:%s' % name diff --git a/templates/godot/addons/service.gd.twig b/templates/godot/addons/service.gd.twig new file mode 100644 index 0000000000..05f2ff5166 --- /dev/null +++ b/templates/godot/addons/service.gd.twig @@ -0,0 +1,46 @@ +{% set prefix = spec.title | caseUcfirst %} +# {{prefix}}Service + +extends RefCounted + + +var client: RefCounted + +func _init(p_client: RefCounted) -> void: + client = p_client + +func _call(method: String, path: String, headers: Dictionary, params: Dictionary, model_script: Variant = null) -> Variant: + var result = await client._call_api(method, path, headers, params) + + if result.statusCode == 0: + var error_msg = result.get("error", "Unknown error") + push_error("{{prefix}} Network Error: %s" % error_msg) + return {{prefix}}Exception.new(error_msg, 0, "network_error", "") + + if result.statusCode >= 400: + var message = "" + var code = result.statusCode + var type = "" + var response = "" + if result.get("body") is Dictionary: + message = result.body.get("message", "") + code = result.body.get("code", result.statusCode) + type = result.body.get("type", "") + response = str(result.body) + else: + message = str(result.get("body", "")) + response = str(result.get("body", "")) + + push_error("{{prefix}} Error (%s): %s" % [type, message]) + return {{prefix}}Exception.new(message, code, type, response) + + if model_script == null: + return result.get("body") + + if result.get("body") is Array: + var list: Array[RefCounted] = [] + for item in result.body: + list.append(model_script.from_dict(item)) + return list + + return model_script.from_dict(result.get("body", {})) diff --git a/templates/godot/addons/services/service.gd.twig b/templates/godot/addons/services/service.gd.twig new file mode 100644 index 0000000000..f09ff042fc --- /dev/null +++ b/templates/godot/addons/services/service.gd.twig @@ -0,0 +1,73 @@ +{% set prefix = spec.title | caseUcfirst %} +extends "../service.gd" +## {{ service.description | default("Service class.") | replace({"\n": "\n## "}) }} + +{% for method in service.methods %} +{%~ set methodNameSnake = method.name | caseSnake %} +{%~ set shouldSkip = false %} +{%~ if method.deprecated %} +{%~ for otherMethod in service.methods %} +{%~ if not otherMethod.deprecated and (otherMethod.name | caseSnake) == methodNameSnake %} +{%~ set shouldSkip = true %} +{%~ endif %} +{%~ endfor %} +{%~ endif %} +{%~ if not shouldSkip %} +{%~ if method.responseModel and method.responseModel != 'any' %} +{%~ set return_type = (spec.title | caseUcfirst) ~ (method.responseModel | caseUcfirst) %} +{%~ else %} +{%~ set return_type = 'Variant' %} +{%~ endif %} + +## {{ method.summary ?? method.description | replace({"\n": "[br]\n##"}) }}[br] +##[br] +{% if method.deprecated %} +## @deprecated{% if method.since %}: Since {{ method.since }}{% endif %}{% if method.replaceWith %} Use [method {{ method.replaceWith | caseSnake | escapeKeyword }}] instead.{% endif %}[br] +##[br] +{% endif %} +{% if method.parameters.all|length > 0 %} +## Parameters:[br] +{% for parameter in method.parameters.all %} +## - [param {{ parameter.name | caseSnake | escapeKeyword }}]: {{ parameter.description | default("No description provided.") | replace({"\n": "\n## "}) }}[br] +{% endfor %} +##[br] +{% endif %} +## Returns:[br] +## - [{{return_type}}] on success.[br] +##[br] +## Errors:[br] +## - Returns error data as [member {{ prefix }}Exception]. +func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters.all %}{{ parameter.name | caseSnake | escapeKeyword }}: {{ parameter | typeName(spec) }}{{ parameter | paramDefault }}{% if not loop.last %}, {% endif %}{% endfor %}) -> Variant : + var _path := '{{ method.path }}' + {%~ for parameter in method.parameters.path %} + _path = _path.replace('{{ '{' }}{{ parameter.name }}{{ '}' }}', str({{ parameter.name | caseSnake | escapeKeyword }})) + {%~ endfor %} + + var _params := { + {%~ for parameter in method.parameters.query %} + '{{ parameter.name }}': {{ parameter.name | caseSnake | escapeKeyword }}, + {%~ endfor %} + {%~ for parameter in method.parameters.body %} + '{{ parameter.name }}': {{ parameter.name | caseSnake | escapeKeyword }}, + {%~ endfor %} + {%~ for parameter in method.parameters.formData %} + '{{ parameter.name }}': {{ parameter.name | caseSnake | escapeKeyword }}, + {%~ endfor %} + } + + var _headers := { + {%~ for key, value in method.headers %} + '{{ key }}': '{{ value }}', + {%~ endfor %} + } + + {%~ if return_type != 'Variant' %} + var model_script = {{ return_type }} + {%~ else %} + var model_script = null + {%~ endif %} + + return await _call('{{ method.method }}', _path, _headers, _params, model_script) + +{%~ endif %} +{%~ endfor %} diff --git a/templates/godot/addons/tests/test_id.gd.twig b/templates/godot/addons/tests/test_id.gd.twig new file mode 100644 index 0000000000..042215ceb0 --- /dev/null +++ b/templates/godot/addons/tests/test_id.gd.twig @@ -0,0 +1,19 @@ +extends "res://addons/gut/test.gd" + +func test_id_unique_length(): + var id = {{spec.title | caseUcfirst}}ID.unique() + assert_true(id.length() > 10) + +func test_id_unique_padding(): + var id1 = {{spec.title | caseUcfirst}}ID.unique(0) + var id2 = {{spec.title | caseUcfirst}}ID.unique(10) + assert_true(id2.length() > id1.length()) + +func test_id_unique_randomness(): + var id1 = {{spec.title | caseUcfirst}}ID.unique() + var id2 = {{spec.title | caseUcfirst}}ID.unique() + assert_ne(id1, id2) + +func test_id_custom(): + var val = "custom_id" + assert_eq({{spec.title | caseUcfirst}}ID.custom(val), val) \ No newline at end of file diff --git a/templates/godot/addons/tests/test_input_files.gd.twig b/templates/godot/addons/tests/test_input_files.gd.twig new file mode 100644 index 0000000000..fb8ac42ada --- /dev/null +++ b/templates/godot/addons/tests/test_input_files.gd.twig @@ -0,0 +1,16 @@ +extends "res://addons/gut/test.gd" + +func test_input_file_from_bytes(): + var bytes = PackedByteArray([1,2,3]) + var file = {{spec.title | caseUcfirst}}InputFile.from_bytes(bytes, "test.txt") + + assert_eq(file.filename, "test.txt") + assert_eq(file.get_data(), bytes) + +func test_input_file_from_path_sets_filename(): + var file = {{spec.title | caseUcfirst}}InputFile.from_path("res://test.txt") + assert_eq(file.filename, "test.txt") + +func test_input_file_empty(): + var file = {{spec.title | caseUcfirst}}InputFile.new() + assert_eq(file.get_data(), PackedByteArray()) \ No newline at end of file diff --git a/templates/godot/addons/tests/test_operator.gd.twig b/templates/godot/addons/tests/test_operator.gd.twig new file mode 100644 index 0000000000..3032b2ffe9 --- /dev/null +++ b/templates/godot/addons/tests/test_operator.gd.twig @@ -0,0 +1,145 @@ +{% set prefix = spec.title | caseUcfirst %} +extends "res://addons/gut/test.gd" + +func test_operator_to_string(): + var op = {{prefix}}Operator.new("test", [1, 2]) + var parsed = JSON.parse_string(op.to_string()) + + assert_eq(parsed["method"], "test") + assert_eq(parsed["values"].size(), 2) + + +func test_operator_value_wrapping(): + var op = {{prefix}}Operator.new("test", 5) + var parsed = JSON.parse_string(op.to_string()) + + assert_true(parsed["values"] is Array) + assert_eq(parsed["values"][0], 5) + + +func test_operator_null_values(): + var op = {{prefix}}Operator.new("test", null) + var parsed = JSON.parse_string(op.to_string()) + + assert_eq(parsed["values"].size(), 0) + + +func test_is_valid_condition(): + assert_true({{prefix}}Operator.is_valid({{prefix}}Operator.EQUAL)) + assert_true({{prefix}}Operator.is_valid({{prefix}}Operator.NOT_EQUAL)) + assert_false({{prefix}}Operator.is_valid("invalid")) + + +func test_increment_basic(): + var q = {{prefix}}Operator.increment(5) + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "increment") + assert_eq(parsed["values"][0], 5) + + +func test_increment_with_max(): + var q = {{prefix}}Operator.increment(5, 10) + var parsed = JSON.parse_string(q) + + assert_eq(parsed["values"].size(), 2) + assert_eq(parsed["values"][1], 10) + + +func test_increment_invalid_number(): + var q = {{prefix}}Operator.increment(INF) + assert_eq(q, "") # should fail gracefully + assert_push_error("Value cannot be NaN or Infinity") + + + +func test_divide_by_zero(): + var q = {{prefix}}Operator.divide(0) + assert_eq(q, "") + assert_push_error("Divisor cannot be zero") + + +func test_divide_valid(): + var q = {{prefix}}Operator.divide(10) + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "divide") + assert_eq(parsed["values"][0], 10) + + +func test_array_append(): + var q = {{prefix}}Operator.array_append([1, 2]) + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "arrayAppend") + assert_eq(parsed["values"].size(), 2) + + +func test_array_insert(): + var q = {{prefix}}Operator.array_insert(1, "x") + var parsed = JSON.parse_string(q) + + assert_eq(parsed["values"][0], 1) + assert_eq(parsed["values"][1], "x") + + +func test_array_unique(): + var q = {{prefix}}Operator.array_unique() + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "arrayUnique") + assert_eq(parsed["values"].size(), 0) + + +func test_array_filter_valid(): + var q = {{prefix}}Operator.array_filter({{prefix}}Operator.EQUAL, 10) + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "arrayFilter") + assert_eq(parsed["values"][0], "equal") + + +func test_array_filter_invalid_condition(): + var q = {{prefix}}Operator.array_filter("INVALID", 10) + assert_eq(q, "") + assert_push_error("Invalid condition: INVALID") + + +func test_string_concat(): + var q = {{prefix}}Operator.string_concat("abc") + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "stringConcat") + assert_eq(parsed["values"][0], "abc") + + +func test_string_replace(): + var q = {{prefix}}Operator.string_replace("a", "b") + var parsed = JSON.parse_string(q) + + assert_eq(parsed["values"][0], "a") + assert_eq(parsed["values"][1], "b") + + +func test_toggle(): + var q = {{prefix}}Operator.toggle() + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "toggle") + assert_eq(parsed["values"].size(), 0) + + +func test_date_add_days(): + var q = {{prefix}}Operator.date_add_days(5) + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "dateAddDays") + assert_eq(parsed["values"][0], 5) + + +func test_date_set_now(): + var q = {{prefix}}Operator.date_set_now() + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "dateSetNow") + assert_eq(parsed["values"].size(), 0) \ No newline at end of file diff --git a/templates/godot/addons/tests/test_permission.gd.twig b/templates/godot/addons/tests/test_permission.gd.twig new file mode 100644 index 0000000000..a229ffbf4f --- /dev/null +++ b/templates/godot/addons/tests/test_permission.gd.twig @@ -0,0 +1,12 @@ +extends "res://addons/gut/test.gd" + +func test_permission_read(): + assert_eq({{spec.title | caseUcfirst}}Permission.read("user:1"), 'read("user:1")') + +func test_permission_write(): + assert_eq({{spec.title | caseUcfirst}}Permission.write("role"), 'write("role")') + +func test_permission_all(): + assert_eq({{spec.title | caseUcfirst}}Permission.create("x"), 'create("x")') + assert_eq({{spec.title | caseUcfirst}}Permission.update("x"), 'update("x")') + assert_eq({{spec.title | caseUcfirst}}Permission.delete("x"), 'delete("x")') \ No newline at end of file diff --git a/templates/godot/addons/tests/test_query.gd.twig b/templates/godot/addons/tests/test_query.gd.twig new file mode 100644 index 0000000000..bf8946d3eb --- /dev/null +++ b/templates/godot/addons/tests/test_query.gd.twig @@ -0,0 +1,96 @@ +extends "res://addons/gut/test.gd" + +func test_query_equal(): + var q = {{spec.title | caseUcfirst}}Query.equal("age", 10) + var parsed = JSON.parse_string(q) + + assert_not_null(parsed) + assert_eq(parsed["method"], "equal") + assert_eq(parsed["attribute"], "age") + assert_eq(parsed["values"][0], 10) + + +func test_query_value_wrapping(): + var q = {{spec.title | caseUcfirst}}Query.equal("age", 5) + var parsed = JSON.parse_string(q) + + assert_true(parsed["values"] is Array) + assert_eq(parsed["values"].size(), 1) + + +func test_query_is_null(): + var q = {{spec.title | caseUcfirst}}Query.isNull("field") + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "isNull") + assert_false(parsed.has("values")) + + +func test_query_between(): + var q = {{spec.title | caseUcfirst}}Query.between("age", 10, 20) + var parsed = JSON.parse_string(q) + + assert_eq(parsed["values"].size(), 2) + assert_eq(parsed["values"][0], 10) + assert_eq(parsed["values"][1], 20) + + +func test_query_and(): + var q1 = {{spec.title | caseUcfirst}}Query.equal("age", 10) + var q2 = {{spec.title | caseUcfirst}}Query.equal("name", "abc") + + var combined = {{spec.title | caseUcfirst}}Query.and_query([q1, q2]) + var parsed = JSON.parse_string(combined) + + assert_eq(parsed["method"], "and") + assert_eq(parsed["values"].size(), 2) + + +func test_query_or(): + var q1 = {{spec.title | caseUcfirst}}Query.equal("age", 10) + var q2 = {{spec.title | caseUcfirst}}Query.isNull("email") + + var combined = {{spec.title | caseUcfirst}}Query.or_query([q1, q2]) + var parsed = JSON.parse_string(combined) + + assert_eq(parsed["method"], "or") + assert_eq(parsed["values"].size(), 2) + + +func test_query_limit(): + var q = {{spec.title | caseUcfirst}}Query.limit(10) + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "limit") + assert_eq(parsed["values"][0], 10) + + +func test_query_offset(): + var q = {{spec.title | caseUcfirst}}Query.offset(5) + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "offset") + assert_eq(parsed["values"][0], 5) + + +func test_query_order_asc(): + var q = {{spec.title | caseUcfirst}}Query.orderAsc("age") + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "orderAsc") + assert_eq(parsed["attribute"], "age") + + +func test_query_order_desc(): + var q = {{spec.title | caseUcfirst}}Query.orderDesc("age") + var parsed = JSON.parse_string(q) + + assert_eq(parsed["method"], "orderDesc") + assert_eq(parsed["attribute"], "age") + + +func test_query_invalid_json_skipped(): + var combined = {{spec.title | caseUcfirst}}Query.and_query(["INVALID"]) + var parsed = JSON.parse_string(combined) + + assert_eq(parsed["values"].size(), 0) \ No newline at end of file diff --git a/templates/godot/addons/tests/test_roles.gd.twig b/templates/godot/addons/tests/test_roles.gd.twig new file mode 100644 index 0000000000..6666b1ea72 --- /dev/null +++ b/templates/godot/addons/tests/test_roles.gd.twig @@ -0,0 +1,19 @@ +extends "res://addons/gut/test.gd" + +func test_role_any(): + assert_eq({{spec.title | caseUcfirst}}Role.any(), "any") + +func test_role_user(): + assert_eq({{spec.title | caseUcfirst}}Role.user("123"), "user:123") + +func test_role_user_with_status(): + assert_eq({{spec.title | caseUcfirst}}Role.user("123", "verified"), "user:123/verified") + +func test_role_team(): + assert_eq({{spec.title | caseUcfirst}}Role.team("team1"), "team:team1") + +func test_role_team_with_role(): + assert_eq({{spec.title | caseUcfirst}}Role.team("team1", "admin"), "team:team1/admin") + +func test_role_label(): + assert_eq({{spec.title | caseUcfirst}}Role.label("vip"), "label:vip") \ No newline at end of file diff --git a/templates/godot/example.env.twig b/templates/godot/example.env.twig deleted file mode 100644 index d2f3b4656b..0000000000 --- a/templates/godot/example.env.twig +++ /dev/null @@ -1,8 +0,0 @@ -# {{ spec.title | caseUcfirst }} Configuration - -{{ spec.title | caseUpper }}_ENDPOINT=https://cloud.appwrite.io/v1 -{{ spec.title | caseUpper }}_SELF_SIGNED=true -{% for header in spec.global.headers %} -{{ spec.title | caseUpper }}_{{ header.key | caseUpper }}={% if header.description %}{{header.description}} -{% endif %} -{% endfor %} \ No newline at end of file diff --git a/templates/godot/menu.gd b/templates/godot/menu.gd new file mode 100755 index 0000000000..57569738cd --- /dev/null +++ b/templates/godot/menu.gd @@ -0,0 +1,4 @@ +extends Control + +func _on_ping_click(): + $Status.text += await Appwrite.ping() diff --git a/templates/godot/project.godot.twig b/templates/godot/project.godot.twig new file mode 100755 index 0000000000..c6693cc4ad --- /dev/null +++ b/templates/godot/project.godot.twig @@ -0,0 +1,34 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Appwrite test" +run/main_scene="res://Menu.tscn" +config/features=PackedStringArray("4.6", "GL Compatibility") +config/icon="res://icon.svg" + +[autoload] + +Appwrite="*res://addons/{{spec.title | caseSnake }}/appwrite.gd" + +[editor_plugins] + +enabled=PackedStringArray("res://addons/{{spec.title | caseSnake }}/plugin.cfg") + +[physics] + +3d/physics_engine="Jolt Physics" + +[rendering] + +rendering_device/driver.windows="d3d12" +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" From 43c677826f4b2f7a2a3a11cb3f3e7c26756fc80f Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Fri, 8 May 2026 15:11:39 +0530 Subject: [PATCH 16/58] feat: SDK ready for review --- src/SDK/Language/GDScript.php | 5 +++++ src/SDK/Language/Godot.php | 7 ++++++- templates/gdscript/.gitignore | 12 ++++++++++++ templates/gdscript/addons/appwrite.gd.twig | 18 +++++++++--------- templates/gdscript/addons/client.gd.twig | 12 ++++++------ templates/gdscript/addons/service.gd.twig | 2 +- templates/godot/.env.twig | 3 +-- templates/godot/.gitignore | 12 ++++++++++++ templates/godot/addons/appwrite.gd.twig | 18 +++++++++--------- templates/godot/addons/client.gd.twig | 12 ++++++------ templates/godot/addons/service.gd.twig | 2 +- 11 files changed, 68 insertions(+), 35 deletions(-) create mode 100644 templates/gdscript/.gitignore create mode 100644 templates/godot/.gitignore diff --git a/src/SDK/Language/GDScript.php b/src/SDK/Language/GDScript.php index 15b3f1e0fb..29d3136bae 100644 --- a/src/SDK/Language/GDScript.php +++ b/src/SDK/Language/GDScript.php @@ -206,6 +206,11 @@ public function getFiles(): array 'destination' => 'addons/{{ spec.title | caseSnake }}/icon.svg', 'template' => 'gdscript/addons/icon.svg', ], + [ + 'scopoe' => 'copy', + 'destination' => '.gitignore', + 'template' => 'gdscript/.gitignore', + ], [ 'scope' => 'default', 'destination' => 'addons/{{ spec.title | caseSnake }}/client.gd', diff --git a/src/SDK/Language/Godot.php b/src/SDK/Language/Godot.php index 9597608217..8f0700186e 100644 --- a/src/SDK/Language/Godot.php +++ b/src/SDK/Language/Godot.php @@ -51,10 +51,15 @@ public function getFiles(): array 'template' => 'godot/addons/appwrite.gd.twig', ], [ - 'scope' => 'copy', + 'scope' => 'default', 'destination' => 'addons/{{ spec.title | caseSnake }}/icon.svg', 'template' => 'godot/addons/icon.svg', ], + [ + 'scopoe' => 'copy', + 'destination' => '.gitignore', + 'template' => 'godot/.gitignore', + ], [ 'scope' => 'default', 'destination' => 'addons/{{ spec.title | caseSnake }}/client.gd', diff --git a/templates/gdscript/.gitignore b/templates/gdscript/.gitignore new file mode 100644 index 0000000000..a457ab1b9a --- /dev/null +++ b/templates/gdscript/.gitignore @@ -0,0 +1,12 @@ +# Godot 4+ specific ignores +.godot/ +.import/ +export.cfg +export_presets.cfg +android/ +testing/ +EngineConfig.build +# Mono-specific ignores +.mono/ +data_*/ +.env \ No newline at end of file diff --git a/templates/gdscript/addons/appwrite.gd.twig b/templates/gdscript/addons/appwrite.gd.twig index bcb183780a..d53600d2e9 100644 --- a/templates/gdscript/addons/appwrite.gd.twig +++ b/templates/gdscript/addons/appwrite.gd.twig @@ -48,43 +48,43 @@ func _ready() -> void: ## {{header.description}} {% endif %} func set_{{header.key | caseSnake}}(value: String) -> void: - _client._set_{{header.key | caseSnake}}(value) + _client.set_{{header.key | caseSnake}}(value) {% endfor %} ## Set self signed status func set_self_signed(status: bool = true) -> void: - _client._set_self_signed(status) + _client.set_self_signed(status) ## Set the endpoint func set_endpoint(endpoint: String) -> void: - _client._set_endpoint(endpoint) + _client.set_endpoint(endpoint) ## Add a header func add_header(key: String, value: String) -> void: - _client._add_header(key, value) + _client.add_header(key, value) ## Get all headers func get_headers() -> Dictionary: - return _client._get_headers() + return _client.get_headers() func _apply_env(key: String, value: String) -> void: match key: "{{ prefix | caseUpper }}_ENDPOINT": - _client._set_endpoint(value) + _client.set_endpoint(value) "{{ prefix | caseUpper }}_SELF_SIGNED": - _client._set_self_signed(value.to_lower() == "true") + _client.set_self_signed(value.to_lower() == "true") {% for header in spec.global.headers %} "{{ prefix | caseUpper }}_{{ header.key | caseUpper }}": - _client._set_{{ header.key | caseSnake }}(value) + _client.set_{{ header.key | caseSnake }}(value) {% endfor %} ## Ping {{prefix}} Server for testing connection func ping() -> String: - var response = await _client._call_api('GET', '/ping') + var response = await _client.call_api('GET', '/ping') return str(response.get('body')) \ No newline at end of file diff --git a/templates/gdscript/addons/client.gd.twig b/templates/gdscript/addons/client.gd.twig index 7dea2ac3ae..71235661df 100644 --- a/templates/gdscript/addons/client.gd.twig +++ b/templates/gdscript/addons/client.gd.twig @@ -16,27 +16,27 @@ var _global_headers = { {% endfor %} } -func _set_self_signed(status: bool = true) -> RefCounted: +func set_self_signed(status: bool = true) -> RefCounted: _self_signed = status return self -func _set_endpoint(endpoint: String) -> RefCounted: +func set_endpoint(endpoint: String) -> RefCounted: _endpoint = endpoint return self -func _add_header(key: String, value: String) -> RefCounted: +func add_header(key: String, value: String) -> RefCounted: _global_headers[key.to_lower()] = value return self -func _get_headers() -> Dictionary: +func get_headers() -> Dictionary: return _global_headers.duplicate() {% for header in spec.global.headers %} -func _set_{{header.key | caseSnake}}(value: String) -> RefCounted: +func set_{{header.key | caseSnake}}(value: String) -> RefCounted: _global_headers['{{header.name|lower}}'] = value return self @@ -44,7 +44,7 @@ func _set_{{header.key | caseSnake}}(value: String) -> RefCounted: {% endfor %} -func _call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: +func call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: if not _ensure_configured(): return {"statusCode": 400, "body": "Missing/wrong configuration. Please check logs for more info"} var http := HTTPRequest.new() diff --git a/templates/gdscript/addons/service.gd.twig b/templates/gdscript/addons/service.gd.twig index 05f2ff5166..76b6839cb9 100644 --- a/templates/gdscript/addons/service.gd.twig +++ b/templates/gdscript/addons/service.gd.twig @@ -10,7 +10,7 @@ func _init(p_client: RefCounted) -> void: client = p_client func _call(method: String, path: String, headers: Dictionary, params: Dictionary, model_script: Variant = null) -> Variant: - var result = await client._call_api(method, path, headers, params) + var result = await client.call_api(method, path, headers, params) if result.statusCode == 0: var error_msg = result.get("error", "Unknown error") diff --git a/templates/godot/.env.twig b/templates/godot/.env.twig index 11c92a8591..f5f7d75bc5 100644 --- a/templates/godot/.env.twig +++ b/templates/godot/.env.twig @@ -1,4 +1,3 @@ {% set prefix = spec.title | caseUpper %} APPWRITE_ENDPOINT={{ spec.endpointDocs | raw }} -APPWRITE_PROJECT={{ spec.project | raw }} -APPWRITE_SELF_SIGNED=true \ No newline at end of file +APPWRITE_PROJECT={{ spec.project | raw }} \ No newline at end of file diff --git a/templates/godot/.gitignore b/templates/godot/.gitignore new file mode 100644 index 0000000000..a457ab1b9a --- /dev/null +++ b/templates/godot/.gitignore @@ -0,0 +1,12 @@ +# Godot 4+ specific ignores +.godot/ +.import/ +export.cfg +export_presets.cfg +android/ +testing/ +EngineConfig.build +# Mono-specific ignores +.mono/ +data_*/ +.env \ No newline at end of file diff --git a/templates/godot/addons/appwrite.gd.twig b/templates/godot/addons/appwrite.gd.twig index bcb183780a..d53600d2e9 100644 --- a/templates/godot/addons/appwrite.gd.twig +++ b/templates/godot/addons/appwrite.gd.twig @@ -48,43 +48,43 @@ func _ready() -> void: ## {{header.description}} {% endif %} func set_{{header.key | caseSnake}}(value: String) -> void: - _client._set_{{header.key | caseSnake}}(value) + _client.set_{{header.key | caseSnake}}(value) {% endfor %} ## Set self signed status func set_self_signed(status: bool = true) -> void: - _client._set_self_signed(status) + _client.set_self_signed(status) ## Set the endpoint func set_endpoint(endpoint: String) -> void: - _client._set_endpoint(endpoint) + _client.set_endpoint(endpoint) ## Add a header func add_header(key: String, value: String) -> void: - _client._add_header(key, value) + _client.add_header(key, value) ## Get all headers func get_headers() -> Dictionary: - return _client._get_headers() + return _client.get_headers() func _apply_env(key: String, value: String) -> void: match key: "{{ prefix | caseUpper }}_ENDPOINT": - _client._set_endpoint(value) + _client.set_endpoint(value) "{{ prefix | caseUpper }}_SELF_SIGNED": - _client._set_self_signed(value.to_lower() == "true") + _client.set_self_signed(value.to_lower() == "true") {% for header in spec.global.headers %} "{{ prefix | caseUpper }}_{{ header.key | caseUpper }}": - _client._set_{{ header.key | caseSnake }}(value) + _client.set_{{ header.key | caseSnake }}(value) {% endfor %} ## Ping {{prefix}} Server for testing connection func ping() -> String: - var response = await _client._call_api('GET', '/ping') + var response = await _client.call_api('GET', '/ping') return str(response.get('body')) \ No newline at end of file diff --git a/templates/godot/addons/client.gd.twig b/templates/godot/addons/client.gd.twig index 5ee28dc88d..9b74ce94ba 100644 --- a/templates/godot/addons/client.gd.twig +++ b/templates/godot/addons/client.gd.twig @@ -16,27 +16,27 @@ var _global_headers = { {% endfor %} } -func _set_self_signed(status: bool = true) -> RefCounted: +func set_self_signed(status: bool = true) -> RefCounted: _self_signed = status return self -func _set_endpoint(endpoint: String) -> RefCounted: +func set_endpoint(endpoint: String) -> RefCounted: _endpoint = endpoint return self -func _add_header(key: String, value: String) -> RefCounted: +func add_header(key: String, value: String) -> RefCounted: _global_headers[key.to_lower()] = value return self -func _get_headers() -> Dictionary: +func get_headers() -> Dictionary: return _global_headers.duplicate() {% for header in spec.global.headers %} -func _set_{{header.key | caseSnake}}(value: String) -> RefCounted: +func set_{{header.key | caseSnake}}(value: String) -> RefCounted: _global_headers['{{header.name|lower}}'] = value return self @@ -44,7 +44,7 @@ func _set_{{header.key | caseSnake}}(value: String) -> RefCounted: {% endfor %} -func _call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: +func call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: if not _ensure_configured(): return {"statusCode": 400, "body": "Missing/wrong configuration. Please check logs for more info"} var http := HTTPRequest.new() diff --git a/templates/godot/addons/service.gd.twig b/templates/godot/addons/service.gd.twig index 05f2ff5166..76b6839cb9 100644 --- a/templates/godot/addons/service.gd.twig +++ b/templates/godot/addons/service.gd.twig @@ -10,7 +10,7 @@ func _init(p_client: RefCounted) -> void: client = p_client func _call(method: String, path: String, headers: Dictionary, params: Dictionary, model_script: Variant = null) -> Variant: - var result = await client._call_api(method, path, headers, params) + var result = await client.call_api(method, path, headers, params) if result.statusCode == 0: var error_msg = result.get("error", "Unknown error") From d317ebc8c01beed305005bf1606f49f355b3978f Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Sun, 10 May 2026 22:04:00 +0530 Subject: [PATCH 17/58] fix(upload): AppwriteInputFile now work as intended --- src/SDK/Language/GDScript.php | 4 +- templates/gdscript/addons/client.gd.twig | 4 ++ templates/gdscript/addons/input_file.gd.twig | 45 +++++++++++++++----- templates/godot/addons/client.gd.twig | 4 ++ templates/godot/addons/input_file.gd.twig | 45 +++++++++++++++----- 5 files changed, 78 insertions(+), 24 deletions(-) diff --git a/src/SDK/Language/GDScript.php b/src/SDK/Language/GDScript.php index 29d3136bae..194f970a4c 100644 --- a/src/SDK/Language/GDScript.php +++ b/src/SDK/Language/GDScript.php @@ -474,7 +474,7 @@ public function getParamExample(array $param, string $lang = ''): string $output .= '{}'; break; case self::TYPE_FILE: - $output .= 'FileAccess.open("file.png", FileAccess.READ)'; + $output .= 'AppwriteInputFile.from_path("file.png")'; break; } } else { @@ -492,7 +492,7 @@ public function getParamExample(array $param, string $lang = ''): string $output .= "'{$example}'"; break; case self::TYPE_FILE: - $output .= 'FileAccess.open("file.png", FileAccess.READ)'; + $output .= 'AppwriteInputFile.from_path("file.png")'; break; } } diff --git a/templates/gdscript/addons/client.gd.twig b/templates/gdscript/addons/client.gd.twig index 71235661df..1f382a9f53 100644 --- a/templates/gdscript/addons/client.gd.twig +++ b/templates/gdscript/addons/client.gd.twig @@ -133,6 +133,10 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) if value is {{prefix}}InputFile: + var data = value.get_data() + if data.is_empty(): + push_warning("{{prefix}}InputFile contains no data") + continue body.append_array(("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" % [key, value.filename]).to_utf8_buffer()) var c_type = value.content_type if value.content_type != "" else "application/octet-stream" body.append_array(("Content-Type: %s\r\n\r\n" % c_type).to_utf8_buffer()) diff --git a/templates/gdscript/addons/input_file.gd.twig b/templates/gdscript/addons/input_file.gd.twig index ad3dfdf461..4ecc9f53a0 100644 --- a/templates/gdscript/addons/input_file.gd.twig +++ b/templates/gdscript/addons/input_file.gd.twig @@ -1,29 +1,34 @@ -## Input File class used to pass files to the SDK -class_name {{spec.title | caseUcfirst}}InputFile +{% set prefix = spec.title | caseUcfirst %} +class_name {{prefix}}InputFile var path: String ## Path to the file var bytes: PackedByteArray ## File bytes var filename: String ## File name var content_type: String ## File content type -func _init(p_path: String = "", p_bytes: PackedByteArray = PackedByteArray(), p_filename: String = "", p_content_type: String = "") -> void: - self.path = p_path - self.bytes = p_bytes - self.filename = p_filename - self.content_type = p_content_type +func _init(file_path: String = "", bytes_data: PackedByteArray = PackedByteArray(), custom_filename: String = "") -> void: + self.path = file_path + self.bytes = bytes_data + self.filename = custom_filename if self.filename == "" and self.path != "": self.filename = self.path.get_file() + + self.content_type = _guess_mime_type(self.path.get_file()) ## Creates a new InputFile from a file path -static func from_path(p_path: String, p_filename: String = "", p_content_type: String = "") -> {{spec.title | caseUcfirst}}InputFile: - return {{spec.title | caseUcfirst}}InputFile.new(p_path, PackedByteArray(), p_filename, p_content_type) +static func from_path(file_path: String, custom_filename: String = "") -> {{spec.title | caseUcfirst}}InputFile: + return {{spec.title | caseUcfirst}}InputFile.new(file_path, PackedByteArray(), custom_filename) ## Creates a new InputFile from bytes -static func from_bytes(p_bytes: PackedByteArray, p_filename: String, p_content_type: String = "") -> {{spec.title | caseUcfirst}}InputFile: - return {{spec.title | caseUcfirst}}InputFile.new("", p_bytes, p_filename, p_content_type) +static func from_bytes(bytes_data: PackedByteArray, custom_filename: String = "") -> {{spec.title | caseUcfirst}}InputFile: + return {{spec.title | caseUcfirst}}InputFile.new("", bytes_data, custom_filename) + + +static func from_text(text: String, custom_filename: String = "") -> {{spec.title | caseUcfirst}}InputFile: + return from_bytes(text.to_utf8_buffer(), custom_filename) ## Return the PackedByteArray of the file @@ -33,3 +38,21 @@ func get_data() -> PackedByteArray: if not path.is_empty(): return FileAccess.get_file_as_bytes(path) return PackedByteArray() + + +static func _guess_mime_type(filename: String) -> String: + var ext := filename.get_extension().to_lower() + + match ext: + "png": + return "image/png" + "jpg", "jpeg": + return "image/jpeg" + "json": + return "application/json" + "txt": + return "text/plain" + "pdf": + return "application/pdf" + _: + return "application/octet-stream" \ No newline at end of file diff --git a/templates/godot/addons/client.gd.twig b/templates/godot/addons/client.gd.twig index 9b74ce94ba..2aa703cd5e 100644 --- a/templates/godot/addons/client.gd.twig +++ b/templates/godot/addons/client.gd.twig @@ -133,6 +133,10 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) if value is {{prefix}}InputFile: + var data = value.get_data() + if data.is_empty(): + push_warning("{{prefix}}InputFile contains no data") + continue body.append_array(("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" % [key, value.filename]).to_utf8_buffer()) var c_type = value.content_type if value.content_type != "" else "application/octet-stream" body.append_array(("Content-Type: %s\r\n\r\n" % c_type).to_utf8_buffer()) diff --git a/templates/godot/addons/input_file.gd.twig b/templates/godot/addons/input_file.gd.twig index ad3dfdf461..4ecc9f53a0 100644 --- a/templates/godot/addons/input_file.gd.twig +++ b/templates/godot/addons/input_file.gd.twig @@ -1,29 +1,34 @@ -## Input File class used to pass files to the SDK -class_name {{spec.title | caseUcfirst}}InputFile +{% set prefix = spec.title | caseUcfirst %} +class_name {{prefix}}InputFile var path: String ## Path to the file var bytes: PackedByteArray ## File bytes var filename: String ## File name var content_type: String ## File content type -func _init(p_path: String = "", p_bytes: PackedByteArray = PackedByteArray(), p_filename: String = "", p_content_type: String = "") -> void: - self.path = p_path - self.bytes = p_bytes - self.filename = p_filename - self.content_type = p_content_type +func _init(file_path: String = "", bytes_data: PackedByteArray = PackedByteArray(), custom_filename: String = "") -> void: + self.path = file_path + self.bytes = bytes_data + self.filename = custom_filename if self.filename == "" and self.path != "": self.filename = self.path.get_file() + + self.content_type = _guess_mime_type(self.path.get_file()) ## Creates a new InputFile from a file path -static func from_path(p_path: String, p_filename: String = "", p_content_type: String = "") -> {{spec.title | caseUcfirst}}InputFile: - return {{spec.title | caseUcfirst}}InputFile.new(p_path, PackedByteArray(), p_filename, p_content_type) +static func from_path(file_path: String, custom_filename: String = "") -> {{spec.title | caseUcfirst}}InputFile: + return {{spec.title | caseUcfirst}}InputFile.new(file_path, PackedByteArray(), custom_filename) ## Creates a new InputFile from bytes -static func from_bytes(p_bytes: PackedByteArray, p_filename: String, p_content_type: String = "") -> {{spec.title | caseUcfirst}}InputFile: - return {{spec.title | caseUcfirst}}InputFile.new("", p_bytes, p_filename, p_content_type) +static func from_bytes(bytes_data: PackedByteArray, custom_filename: String = "") -> {{spec.title | caseUcfirst}}InputFile: + return {{spec.title | caseUcfirst}}InputFile.new("", bytes_data, custom_filename) + + +static func from_text(text: String, custom_filename: String = "") -> {{spec.title | caseUcfirst}}InputFile: + return from_bytes(text.to_utf8_buffer(), custom_filename) ## Return the PackedByteArray of the file @@ -33,3 +38,21 @@ func get_data() -> PackedByteArray: if not path.is_empty(): return FileAccess.get_file_as_bytes(path) return PackedByteArray() + + +static func _guess_mime_type(filename: String) -> String: + var ext := filename.get_extension().to_lower() + + match ext: + "png": + return "image/png" + "jpg", "jpeg": + return "image/jpeg" + "json": + return "application/json" + "txt": + return "text/plain" + "pdf": + return "application/pdf" + _: + return "application/octet-stream" \ No newline at end of file From 854563470efcbd0f8e95da216db9b42b0b280d4a Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Sun, 10 May 2026 23:40:54 +0530 Subject: [PATCH 18/58] fix: optional params --- .../gdscript/addons/services/service.gd.twig | 36 +++++++++++++------ .../godot/addons/services/service.gd.twig | 36 +++++++++++++------ 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/templates/gdscript/addons/services/service.gd.twig b/templates/gdscript/addons/services/service.gd.twig index f09ff042fc..28d0f19c52 100644 --- a/templates/gdscript/addons/services/service.gd.twig +++ b/templates/gdscript/addons/services/service.gd.twig @@ -43,17 +43,31 @@ func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters. _path = _path.replace('{{ '{' }}{{ parameter.name }}{{ '}' }}', str({{ parameter.name | caseSnake | escapeKeyword }})) {%~ endfor %} - var _params := { - {%~ for parameter in method.parameters.query %} - '{{ parameter.name }}': {{ parameter.name | caseSnake | escapeKeyword }}, - {%~ endfor %} - {%~ for parameter in method.parameters.body %} - '{{ parameter.name }}': {{ parameter.name | caseSnake | escapeKeyword }}, - {%~ endfor %} - {%~ for parameter in method.parameters.formData %} - '{{ parameter.name }}': {{ parameter.name | caseSnake | escapeKeyword }}, - {%~ endfor %} - } + var _params := {} + {%~ for parameter in method.parameters.query %} + {%~ if parameter.required %} + _params['{{ parameter.name }}'] = {{ parameter.name | caseSnake | escapeKeyword }} + {%~ else %} + if {{ parameter.name | caseSnake | escapeKeyword }} != null: + _params['{{ parameter.name }}'] = {{ parameter.name | caseSnake | escapeKeyword }} + {%~ endif %} + {%~ endfor %} + {%~ for parameter in method.parameters.body %} + {%~ if parameter.required %} + _params['{{ parameter.name }}'] = {{ parameter.name | caseSnake | escapeKeyword }} + {%~ else %} + if {{ parameter.name | caseSnake | escapeKeyword }} != null: + _params['{{ parameter.name }}'] = {{ parameter.name | caseSnake | escapeKeyword }} + {%~ endif %} + {%~ endfor %} + {%~ for parameter in method.parameters.formData %} + {%~ if parameter.required %} + _params['{{ parameter.name }}'] = {{ parameter.name | caseSnake | escapeKeyword }} + {%~ else %} + if {{ parameter.name | caseSnake | escapeKeyword }} != null: + _params['{{ parameter.name }}'] = {{ parameter.name | caseSnake | escapeKeyword }} + {%~ endif %} + {%~ endfor %} var _headers := { {%~ for key, value in method.headers %} diff --git a/templates/godot/addons/services/service.gd.twig b/templates/godot/addons/services/service.gd.twig index f09ff042fc..e55fec3666 100644 --- a/templates/godot/addons/services/service.gd.twig +++ b/templates/godot/addons/services/service.gd.twig @@ -43,17 +43,31 @@ func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters. _path = _path.replace('{{ '{' }}{{ parameter.name }}{{ '}' }}', str({{ parameter.name | caseSnake | escapeKeyword }})) {%~ endfor %} - var _params := { - {%~ for parameter in method.parameters.query %} - '{{ parameter.name }}': {{ parameter.name | caseSnake | escapeKeyword }}, - {%~ endfor %} - {%~ for parameter in method.parameters.body %} - '{{ parameter.name }}': {{ parameter.name | caseSnake | escapeKeyword }}, - {%~ endfor %} - {%~ for parameter in method.parameters.formData %} - '{{ parameter.name }}': {{ parameter.name | caseSnake | escapeKeyword }}, - {%~ endfor %} - } + var _params := {} + {%~ for parameter in method.parameters.query %} + {%~ if parameter.required %} + _params['{{ parameter.name }}'] = {{ parameter.name | caseSnake | escapeKeyword }} + {%~ else %} + if {{ parameter.name | caseSnake | escapeKeyword }} != null: + _params['{{ parameter.name }}'] = {{ parameter.name | caseSnake | escapeKeyword }} + {%~ endif %} + {%~ endfor %} + {%~ for parameter in method.parameters.body %} + {%~ if parameter.required %} + _params['{{ parameter.name }}'] = {{ parameter.name | caseSnake | escapeKeyword }} + {%~ else %} + if {{ parameter.name | caseSnake | escapeKeyword }} != null: + _params['{{ parameter.name }}'] = {{ parameter.name | caseSnake | escapeKeyword }} + {%~ endif %} + {%~ endfor %} + {%~ for parameter in method.parameters.formData %} + {%~ if parameter.required %} + _params['{{ parameter.name }}'] = {{ parameter.name | caseSnake | escapeKeyword }} + {%~ else %} + if {{ parameter.name | caseSnake | escapeKeyword }} != null: + _params['{{ parameter.name }}'] = {{ parameter.name | caseSnake | escapeKeyword }} + {%~ endif %} + {%~ endfor %} var _headers := { {%~ for key, value in method.headers %} From a05d5748169c953aa02a678efe2d2b4e312b388a Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Mon, 11 May 2026 11:33:07 +0530 Subject: [PATCH 19/58] fix: sessions are now stored and recovered --- src/SDK/Language/Godot.php | 12 +++- templates/godot/addons/client.gd.twig | 11 +++ .../godot/addons/persistence/cookie.gd.twig | 61 +++++++++++++++++ .../addons/persistence/cookie_store.gd.twig | 68 +++++++++++++++++++ 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 templates/godot/addons/persistence/cookie.gd.twig create mode 100644 templates/godot/addons/persistence/cookie_store.gd.twig diff --git a/src/SDK/Language/Godot.php b/src/SDK/Language/Godot.php index 8f0700186e..910929af8c 100644 --- a/src/SDK/Language/Godot.php +++ b/src/SDK/Language/Godot.php @@ -56,7 +56,7 @@ public function getFiles(): array 'template' => 'godot/addons/icon.svg', ], [ - 'scopoe' => 'copy', + 'scope' => 'copy', 'destination' => '.gitignore', 'template' => 'godot/.gitignore', ], @@ -145,6 +145,16 @@ public function getFiles(): array 'destination' => '.env', 'template' => 'godot/.env.twig', ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/persistence/cookie.gd', + 'template' => 'godot/addons/persistence/cookie.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/persistence/cookie_store.gd', + 'template' => 'godot/addons/persistence/cookie_store.gd.twig', + ], [ 'scope' => 'enum', 'destination' => 'addons/{{ spec.title | caseSnake }}/enums/{{ enum.name | caseSnake }}.gd', diff --git a/templates/godot/addons/client.gd.twig b/templates/godot/addons/client.gd.twig index 2aa703cd5e..ec03d3f5e6 100644 --- a/templates/godot/addons/client.gd.twig +++ b/templates/godot/addons/client.gd.twig @@ -16,6 +16,9 @@ var _global_headers = { {% endfor %} } +const _cookie_store_class = preload("persistence/cookie_store.gd") +var _cookie_store = _cookie_store_class.new() + func set_self_signed(status: bool = true) -> RefCounted: _self_signed = status return self @@ -101,6 +104,10 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param if combined_headers[key] != "": header_list.append(key + ": " + str(combined_headers[key])) + var cookie_header = _cookie_store.get_cookie_header() + if cookie_header != "": + header_list.append("Cookie: " + cookie_header) + var err = http.request_raw(uri + request_path, header_list, http_method, body) if err != OK: http.queue_free() @@ -116,6 +123,10 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param if request_result != HTTPRequest.RESULT_SUCCESS: return {"statusCode": 0, "error": "HTTP Request Error: " + str(request_result)} + for header in response[2]: + if header.to_lower().begins_with("set-cookie:"): + _cookie_store.add_cookie(header.substr(11).strip_edges()) + var response_text := response_body.get_string_from_utf8() var json := JSON.new() if json.parse(response_text) == OK: diff --git a/templates/godot/addons/persistence/cookie.gd.twig b/templates/godot/addons/persistence/cookie.gd.twig new file mode 100644 index 0000000000..50b131c770 --- /dev/null +++ b/templates/godot/addons/persistence/cookie.gd.twig @@ -0,0 +1,61 @@ +extends RefCounted + +var name: String = "" +var value: String = "" +var expires_at: int = -1 # -1 means session cookie (no expiration), 0 means already expired +var path: String = "/" +var domain: String = "" + +func parse(header: String) -> void: + var parts = header.split(";") + if parts.size() == 0: + return + + var key_value = parts[0].split("=", true, 1) + if key_value.size() >= 2: + name = key_value[0].strip_edges() + value = key_value[1].strip_edges() + else: + name = key_value[0].strip_edges() + + for i in range(1, parts.size()): + var attr = parts[i].strip_edges() + var attr_kv = attr.split("=", true, 1) + var attr_name = attr_kv[0].to_lower() + var attr_value = attr_kv[1] if attr_kv.size() > 1 else "" + + match attr_name: + "max-age": + var max_age = attr_value.to_int() + if max_age <= 0: + expires_at = 0 + else: + expires_at = int(Time.get_unix_time_from_system()) + max_age + "path": + path = attr_value + "domain": + domain = attr_value + + if value == "" or value.to_lower() == "deleted": + expires_at = 0 + +func is_expired() -> bool: + if expires_at == -1: + return false + return int(Time.get_unix_time_from_system()) >= expires_at + +func to_dict() -> Dictionary: + return { + "name": name, + "value": value, + "expires_at": expires_at, + "path": path, + "domain": domain + } + +func from_dict(dict: Dictionary) -> void: + name = dict.get("name", "") + value = dict.get("value", "") + expires_at = int(dict.get("expires_at", -1)) + path = dict.get("path", "/") + domain = dict.get("domain", "") diff --git a/templates/godot/addons/persistence/cookie_store.gd.twig b/templates/godot/addons/persistence/cookie_store.gd.twig new file mode 100644 index 0000000000..099fb626cc --- /dev/null +++ b/templates/godot/addons/persistence/cookie_store.gd.twig @@ -0,0 +1,68 @@ +extends RefCounted + +const _cookie_class = preload("cookie.gd") + +var _cookies: Dictionary = {} +var _cookie_path: String = "user://{{spec.title|caseLower}}_cookies.json" + +func _init() -> void: + load_cookies() + +func add_cookie(cookie_str: String) -> void: + var cookie = _cookie_class.new() + cookie.parse(cookie_str) + + if cookie.name == "": + return + + if cookie.is_expired(): + if _cookies.has(cookie.name): + _cookies.erase(cookie.name) + else: + _cookies[cookie.name] = cookie + + save_cookies() + +func get_cookie_header() -> String: + var header = "" + var to_remove = [] + + for name in _cookies: + var cookie = _cookies[name] + if cookie.is_expired(): + to_remove.append(name) + else: + header += "%s=%s; " % [cookie.name, cookie.value] + + for name in to_remove: + _cookies.erase(name) + + if to_remove.size() > 0: + save_cookies() + + return header.trim_suffix("; ") + +func save_cookies() -> void: + var file = FileAccess.open(_cookie_path, FileAccess.WRITE) + if file: + var dict = {} + for name in _cookies: + dict[name] = _cookies[name].to_dict() + file.store_string(JSON.stringify(dict)) + file.close() + +func load_cookies() -> void: + if FileAccess.file_exists(_cookie_path): + var file = FileAccess.open(_cookie_path, FileAccess.READ) + if file: + var text = file.get_as_text() + var json = JSON.new() + if json.parse(text) == OK and typeof(json.data) == TYPE_DICTIONARY: + for name in json.data: + var data = json.data[name] + if typeof(data) == TYPE_DICTIONARY: + var cookie = _cookie_class.new() + cookie.from_dict(data) + if not cookie.is_expired(): + _cookies[name] = cookie + file.close() From 0cc97e83bf7e18f0ceffd3bded1f77b404c72ac2 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Mon, 11 May 2026 13:21:36 +0530 Subject: [PATCH 20/58] fix(gdscript): add missing .env.twig and typo 'scopoe' -> 'scope' --- src/SDK/Language/GDScript.php | 7 ++++++- templates/gdscript/.env.twig | 4 ++++ templates/godot/.env.twig | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 templates/gdscript/.env.twig diff --git a/src/SDK/Language/GDScript.php b/src/SDK/Language/GDScript.php index 194f970a4c..e53d0cb60a 100644 --- a/src/SDK/Language/GDScript.php +++ b/src/SDK/Language/GDScript.php @@ -207,7 +207,7 @@ public function getFiles(): array 'template' => 'gdscript/addons/icon.svg', ], [ - 'scopoe' => 'copy', + 'scope' => 'copy', 'destination' => '.gitignore', 'template' => 'gdscript/.gitignore', ], @@ -256,6 +256,11 @@ public function getFiles(): array 'destination' => 'addons/{{spec.title | caseSnake}}/operator.gd', 'template' => 'gdscript/addons/operator.gd.twig', ], + [ + 'scope' => 'default', + 'destination' => '.env', + 'template' => 'gdscript/.env.twig', + ], [ 'scope' => 'default', 'destination' => 'tests/test_query.gd', diff --git a/templates/gdscript/.env.twig b/templates/gdscript/.env.twig new file mode 100644 index 0000000000..a23ebcbd0d --- /dev/null +++ b/templates/gdscript/.env.twig @@ -0,0 +1,4 @@ +{% set prefix = spec.title | caseUpper %} +{{prefix}}_ENDPOINT={{ spec.endpointDocs | raw }} +{{prefix}}_PROJECT={{ spec.project | raw }} +{{prefix}}_KEY={{ spec.key | raw }} \ No newline at end of file diff --git a/templates/godot/.env.twig b/templates/godot/.env.twig index f5f7d75bc5..3db7c397c2 100644 --- a/templates/godot/.env.twig +++ b/templates/godot/.env.twig @@ -1,3 +1,3 @@ {% set prefix = spec.title | caseUpper %} -APPWRITE_ENDPOINT={{ spec.endpointDocs | raw }} -APPWRITE_PROJECT={{ spec.project | raw }} \ No newline at end of file +{{prefix}}_ENDPOINT={{ spec.endpointDocs | raw }} +{{prefix}}_PROJECT={{ spec.project | raw }} \ No newline at end of file From f1809f6fad7613c370d778c943e3c618b8620c86 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Mon, 11 May 2026 13:33:56 +0530 Subject: [PATCH 21/58] refactor(gdscript, godot): removed duplicate test_query entry in getFiles() --- src/SDK/Language/GDScript.php | 5 ----- src/SDK/Language/Godot.php | 5 ----- 2 files changed, 10 deletions(-) diff --git a/src/SDK/Language/GDScript.php b/src/SDK/Language/GDScript.php index e53d0cb60a..7fda12944f 100644 --- a/src/SDK/Language/GDScript.php +++ b/src/SDK/Language/GDScript.php @@ -271,11 +271,6 @@ public function getFiles(): array 'destination' => 'tests/test_roles.gd', 'template' => 'gdscript/addons/tests/test_roles.gd.twig', ], - [ - 'scope' => 'default', - 'destination' => 'tests/test_query.gd', - 'template' => 'gdscript/addons/tests/test_query.gd.twig', - ], [ 'scope' => 'default', 'destination' => 'tests/test_id.gd', diff --git a/src/SDK/Language/Godot.php b/src/SDK/Language/Godot.php index 910929af8c..8eba10b31f 100644 --- a/src/SDK/Language/Godot.php +++ b/src/SDK/Language/Godot.php @@ -115,11 +115,6 @@ public function getFiles(): array 'destination' => 'tests/test_roles.gd', 'template' => 'godot/addons/tests/test_roles.gd.twig', ], - [ - 'scope' => 'default', - 'destination' => 'tests/test_query.gd', - 'template' => 'godot/addons/tests/test_query.gd.twig', - ], [ 'scope' => 'default', 'destination' => 'tests/test_id.gd', From e57a5a6547765ded21a227d1a48f69edf9134fca Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Mon, 11 May 2026 15:07:28 +0530 Subject: [PATCH 22/58] fix(godot, gdscript): allow omitting optional parameters in service methods - Updated service templates to use 'Variant = null' for all optional parameters. - Added runtime type validation to ensure passed values match the intended types. - Included explicit type hints in documentation comments using [Type] notation. --- src/SDK/Language/GDScript.php | 8 ++++++-- .../gdscript/addons/services/service.gd.twig | 16 ++++++++++++++-- templates/godot/addons/services/service.gd.twig | 16 ++++++++++++++-- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/SDK/Language/GDScript.php b/src/SDK/Language/GDScript.php index 7fda12944f..5056f0631b 100644 --- a/src/SDK/Language/GDScript.php +++ b/src/SDK/Language/GDScript.php @@ -363,7 +363,7 @@ public function getTypeName(array $parameter, array $spec = []): string self::TYPE_BOOLEAN => 'bool', self::TYPE_ARRAY => 'Array', self::TYPE_OBJECT => 'Dictionary', - self::TYPE_FILE => 'RefCounted', + self::TYPE_FILE => 'AppwriteInputFile', default => 'Variant', }; } @@ -387,9 +387,11 @@ public function getParamDefault(array $param): string if (empty($default) && $default !== 0 && $default !== false) { switch ($type) { case self::TYPE_NUMBER: - case self::TYPE_INTEGER: $output .= '0.0'; break; + case self::TYPE_INTEGER: + $output .= '0'; + break; case self::TYPE_BOOLEAN: $output .= 'false'; break; @@ -458,6 +460,8 @@ public function getParamExample(array $param, string $lang = ''): string if (empty($example) && $example !== 0 && $example !== false) { switch ($type) { case self::TYPE_NUMBER: + $output .= '0.0'; + break; case self::TYPE_INTEGER: $output .= '0'; break; diff --git a/templates/gdscript/addons/services/service.gd.twig b/templates/gdscript/addons/services/service.gd.twig index 28d0f19c52..eaa5fa89d7 100644 --- a/templates/gdscript/addons/services/service.gd.twig +++ b/templates/gdscript/addons/services/service.gd.twig @@ -28,7 +28,7 @@ extends "../service.gd" {% if method.parameters.all|length > 0 %} ## Parameters:[br] {% for parameter in method.parameters.all %} -## - [param {{ parameter.name | caseSnake | escapeKeyword }}]: {{ parameter.description | default("No description provided.") | replace({"\n": "\n## "}) }}[br] +## - [param {{ parameter.name | caseSnake | escapeKeyword }}] [{{ parameter | typeName(spec) }}]: {{ parameter.description | default("No description provided.") | replace({"\n": "\n## "}) }}[br] {% endfor %} ##[br] {% endif %} @@ -37,7 +37,19 @@ extends "../service.gd" ##[br] ## Errors:[br] ## - Returns error data as [member {{ prefix }}Exception]. -func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters.all %}{{ parameter.name | caseSnake | escapeKeyword }}: {{ parameter | typeName(spec) }}{{ parameter | paramDefault }}{% if not loop.last %}, {% endif %}{% endfor %}) -> Variant : +func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters.all %}{{ parameter.name | caseSnake | escapeKeyword }}: {% if parameter.required %}{{ parameter | typeName(spec) }}{{ parameter | paramDefault }}{% else %}Variant = null{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}) -> Variant : + # Runtime type checking, GDScript typed vars don't support null or optional + {%~ for parameter in method.parameters.all %} + {%~ if not parameter.required %} + {%~ set type = parameter | typeName(spec) %} + {%~ if type != 'Variant' %} + {%~ if type starts with 'Array' %}{% set check_type = 'Array' %}{% else %}{% set check_type = type %}{% endif %} + if {{ parameter.name | caseSnake | escapeKeyword }} != null and not {{ parameter.name | caseSnake | escapeKeyword }} is {{ check_type }}: + return {{prefix}}Exception.new("Invalid type for parameter '{{ parameter.name | caseSnake | escapeKeyword }}'. Expected {{ type }}.", 0, "argument_error", "") + {%~ endif %} + {%~ endif %} + {%~ endfor %} + var _path := '{{ method.path }}' {%~ for parameter in method.parameters.path %} _path = _path.replace('{{ '{' }}{{ parameter.name }}{{ '}' }}', str({{ parameter.name | caseSnake | escapeKeyword }})) diff --git a/templates/godot/addons/services/service.gd.twig b/templates/godot/addons/services/service.gd.twig index e55fec3666..05087f958e 100644 --- a/templates/godot/addons/services/service.gd.twig +++ b/templates/godot/addons/services/service.gd.twig @@ -28,7 +28,7 @@ extends "../service.gd" {% if method.parameters.all|length > 0 %} ## Parameters:[br] {% for parameter in method.parameters.all %} -## - [param {{ parameter.name | caseSnake | escapeKeyword }}]: {{ parameter.description | default("No description provided.") | replace({"\n": "\n## "}) }}[br] +## - [param {{ parameter.name | caseSnake | escapeKeyword }}] [{{ parameter | typeName(spec) }}]: {{ parameter.description | default("No description provided.") | replace({"\n": "\n## "}) }}[br] {% endfor %} ##[br] {% endif %} @@ -37,7 +37,19 @@ extends "../service.gd" ##[br] ## Errors:[br] ## - Returns error data as [member {{ prefix }}Exception]. -func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters.all %}{{ parameter.name | caseSnake | escapeKeyword }}: {{ parameter | typeName(spec) }}{{ parameter | paramDefault }}{% if not loop.last %}, {% endif %}{% endfor %}) -> Variant : +func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters.all %}{{ parameter.name | caseSnake | escapeKeyword }}: {% if parameter.required %}{{ parameter | typeName(spec) }}{{ parameter | paramDefault }}{% else %}Variant = null{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}) -> Variant : + # Runtime type checking, GDScript typed vars don't support null or optional + {%~ for parameter in method.parameters.all %} + {%~ if not parameter.required %} + {%~ set type = parameter | typeName(spec) %} + {%~ if type != 'Variant' %} + {%~ if type starts with 'Array' %}{% set check_type = 'Array' %}{% else %}{% set check_type = type %}{% endif %} + if {{ parameter.name | caseSnake | escapeKeyword }} != null and not {{ parameter.name | caseSnake | escapeKeyword }} is {{ check_type }}: + return {{prefix}}Exception.new("Invalid type for parameter '{{ parameter.name | caseSnake | escapeKeyword }}'. Expected {{ type }}.", 0, "argument_error", "") + {%~ endif %} + {%~ endif %} + {%~ endfor %} + var _path := '{{ method.path }}' {%~ for parameter in method.parameters.path %} _path = _path.replace('{{ '{' }}{{ parameter.name }}{{ '}' }}', str({{ parameter.name | caseSnake | escapeKeyword }})) From f8486645c7e9a466232f531ff7b5fc8bda1f0c1e Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Mon, 11 May 2026 22:45:27 +0530 Subject: [PATCH 23/58] fix: dangling boundary line corrupts multipart body on empty inputs --- templates/gdscript/addons/client.gd.twig | 10 ++++++---- templates/godot/addons/client.gd.twig | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/templates/gdscript/addons/client.gd.twig b/templates/gdscript/addons/client.gd.twig index 1f382a9f53..5e3a35f2dc 100644 --- a/templates/gdscript/addons/client.gd.twig +++ b/templates/gdscript/addons/client.gd.twig @@ -130,24 +130,26 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: var value = params[key] if value == null: continue - body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) - if value is {{prefix}}InputFile: var data = value.get_data() if data.is_empty(): push_warning("{{prefix}}InputFile contains no data") continue + body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) body.append_array(("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" % [key, value.filename]).to_utf8_buffer()) var c_type = value.content_type if value.content_type != "" else "application/octet-stream" body.append_array(("Content-Type: %s\r\n\r\n" % c_type).to_utf8_buffer()) - body.append_array(value.get_data()) + body.append_array(data) elif value is Array: for i in range(value.size()): - if i > 0: + if i == 0: + body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) + else: body.append_array(("\r\n--" + boundary + "\r\n").to_utf8_buffer()) body.append_array(("Content-Disposition: form-data; name=\"%s[]\"\r\n\r\n" % key).to_utf8_buffer()) body.append_array((str(value[i])).to_utf8_buffer()) else: + body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) body.append_array(("Content-Disposition: form-data; name=\"%s\"\r\n\r\n" % key).to_utf8_buffer()) body.append_array((str(value)).to_utf8_buffer()) diff --git a/templates/godot/addons/client.gd.twig b/templates/godot/addons/client.gd.twig index ec03d3f5e6..5497ba41f1 100644 --- a/templates/godot/addons/client.gd.twig +++ b/templates/godot/addons/client.gd.twig @@ -141,24 +141,26 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: var value = params[key] if value == null: continue - body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) - if value is {{prefix}}InputFile: var data = value.get_data() if data.is_empty(): push_warning("{{prefix}}InputFile contains no data") continue + body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) body.append_array(("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" % [key, value.filename]).to_utf8_buffer()) var c_type = value.content_type if value.content_type != "" else "application/octet-stream" body.append_array(("Content-Type: %s\r\n\r\n" % c_type).to_utf8_buffer()) - body.append_array(value.get_data()) + body.append_array(data) elif value is Array: for i in range(value.size()): - if i > 0: + if i == 0: + body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) + else: body.append_array(("\r\n--" + boundary + "\r\n").to_utf8_buffer()) body.append_array(("Content-Disposition: form-data; name=\"%s[]\"\r\n\r\n" % key).to_utf8_buffer()) body.append_array((str(value[i])).to_utf8_buffer()) else: + body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) body.append_array(("Content-Disposition: form-data; name=\"%s\"\r\n\r\n" % key).to_utf8_buffer()) body.append_array((str(value)).to_utf8_buffer()) From 05256c7ca48b8a5a042fba2951d5b52b11271a2e Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Tue, 12 May 2026 21:17:05 +0530 Subject: [PATCH 24/58] feat(gdscript, godot): add chunk upload for large file + progress signal + nested model name fixed - Add _call_chunked to upload large files in parts - Add on_progress(progress, total_size, uploaded_size) signal to singleton - Changed AppwriteInputFile to InputFile. getParamExample doesn't allow sdk name prefix, hardcoded name will become debt - Nested model has now sdk name prefix --- src/SDK/Language/GDScript.php | 12 +++-- templates/gdscript/addons/appwrite.gd.twig | 3 ++ templates/gdscript/addons/client.gd.twig | 48 ++++++++++++++++-- templates/gdscript/addons/input_file.gd.twig | 36 +++++++++++--- .../gdscript/addons/models/model.gd.twig | 4 +- .../addons/tests/test_input_files.gd.twig | 6 +-- templates/godot/addons/appwrite.gd.twig | 3 ++ templates/godot/addons/client.gd.twig | 49 +++++++++++++++++-- templates/godot/addons/input_file.gd.twig | 36 +++++++++++--- templates/godot/addons/models/model.gd.twig | 4 +- .../addons/tests/test_input_files.gd.twig | 6 +-- 11 files changed, 170 insertions(+), 37 deletions(-) diff --git a/src/SDK/Language/GDScript.php b/src/SDK/Language/GDScript.php index 5056f0631b..f197902a38 100644 --- a/src/SDK/Language/GDScript.php +++ b/src/SDK/Language/GDScript.php @@ -321,12 +321,14 @@ public function getFiles(): array */ public function getTypeName(array $parameter, array $spec = []): string { + $prefix = $this->toPascalCase($spec['title'] ?? ''); + // ARRAY TYPES if (($parameter['type'] ?? null) === self::TYPE_ARRAY) { // Array of models if (!empty($parameter['array']['model'])) { - return 'Array[' . $this->toPascalCase($parameter['array']['model']) . ']'; + return 'Array[' . $prefix . $this->toPascalCase($parameter['array']['model']) . ']'; } // Array of enums @@ -343,7 +345,7 @@ public function getTypeName(array $parameter, array $spec = []): string // MODEL TYPE if (!empty($parameter['model'])) { - return $this->toPascalCase($parameter['model']); + return $prefix . $this->toPascalCase($parameter['model']); } // ENUM TYPE @@ -363,7 +365,7 @@ public function getTypeName(array $parameter, array $spec = []): string self::TYPE_BOOLEAN => 'bool', self::TYPE_ARRAY => 'Array', self::TYPE_OBJECT => 'Dictionary', - self::TYPE_FILE => 'AppwriteInputFile', + self::TYPE_FILE => 'InputFile', default => 'Variant', }; } @@ -478,7 +480,7 @@ public function getParamExample(array $param, string $lang = ''): string $output .= '{}'; break; case self::TYPE_FILE: - $output .= 'AppwriteInputFile.from_path("file.png")'; + $output .= 'InputFile.from_path("file.png")'; break; } } else { @@ -496,7 +498,7 @@ public function getParamExample(array $param, string $lang = ''): string $output .= "'{$example}'"; break; case self::TYPE_FILE: - $output .= 'AppwriteInputFile.from_path("file.png")'; + $output .= 'InputFile.from_path("file.png")'; break; } } diff --git a/templates/gdscript/addons/appwrite.gd.twig b/templates/gdscript/addons/appwrite.gd.twig index d53600d2e9..a866f85b26 100644 --- a/templates/gdscript/addons/appwrite.gd.twig +++ b/templates/gdscript/addons/appwrite.gd.twig @@ -2,6 +2,8 @@ extends Node ## [img width=32]res://addons/{{spec.title | caseSnake}}/icon.svg[/img] {{ spec.description | default("Appwrite _client.") | replace({"\n": "[br]\n## "}) }} +signal on_progress(progress: float, total_size: int, uploaded_size: int) + # Internal classes for type hinting {% for service in spec.services %} const _{{ service.name | caseUpper }} = preload("services/{{ service.name | caseSnake }}.gd") @@ -19,6 +21,7 @@ const {{ enumName | caseUpper }} = preload("enums/{{ enumName | caseSnake }}.gd" {% endfor %} func _ready() -> void: + _client.on_progress.connect(func(p, t, c): on_progress.emit(p, t, c)) var path = "res://.env" if not FileAccess.file_exists(path): return diff --git a/templates/gdscript/addons/client.gd.twig b/templates/gdscript/addons/client.gd.twig index 5e3a35f2dc..509f7a1fb5 100644 --- a/templates/gdscript/addons/client.gd.twig +++ b/templates/gdscript/addons/client.gd.twig @@ -1,6 +1,8 @@ # {% set prefix = spec.title | caseUcfirst %}Client extends RefCounted +signal on_progress(progress: float, total_size: int, uploaded_size: int) + var _chunk_size = 5 * 1024 * 1024 var _self_signed = false var _endpoint = '{{spec.endpoint}}' @@ -79,11 +81,17 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param request_path += "?" + query_http.query_string_from_dict(params) else: var has_files := false + var large_file_key := "" for key in params: - if params[key] is {{prefix}}InputFile: + if params[key] is InputFile: has_files = true + if params[key].get_size() > _chunk_size: + large_file_key = key break + if large_file_key != "" and not headers.has("content-range"): + return await _call_chunked(method, path, headers, params, large_file_key) + if has_files: var boundary := "Boundary-%x" % Time.get_ticks_msec() combined_headers["content-type"] = "multipart/form-data; boundary=" + boundary @@ -130,10 +138,10 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: var value = params[key] if value == null: continue - if value is {{prefix}}InputFile: + if value is InputFile: var data = value.get_data() if data.is_empty(): - push_warning("{{prefix}}InputFile contains no data") + push_warning("InputFile contains no data") continue body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) body.append_array(("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" % [key, value.filename]).to_utf8_buffer()) @@ -166,4 +174,36 @@ func _ensure_configured() -> bool: if not _global_headers.has("x-appwrite-project"): push_warning("{{prefix}}: project id is missing. Use set_project or .env.") return false - return true \ No newline at end of file + return true + + +func _call_chunked(method: String, path: String, headers: Dictionary, params: Dictionary, file_key: String) -> Dictionary: + var file: InputFile = params[file_key] + var size = file.get_size() + var start = 0 + var response := {} + + var headers_copy = headers.duplicate() + var params_copy = params.duplicate() + + while start < size: + var end = min(start + _chunk_size, size) + headers_copy["content-range"] = "bytes %d-%d/%d" % [start, end - 1, size] + + var chunk_data = file.get_chunk(start, end - start) + var chunk_file = InputFile.from_bytes(chunk_data, file.filename) + chunk_file.content_type = file.content_type + + params_copy[file_key] = chunk_file + + response = await call_api(method, path, headers_copy, params_copy) + + if response.has("body") and response["body"] is Dictionary and response["body"].has("$id"): + headers_copy["x-{{spec.title | caseLower }}-id"] = response["body"]["$id"] + + if response.get("statusCode", 0) >= 400: + return response + + start = end + on_progress.emit((float(start) / size) * 100.0, size, start) + return response \ No newline at end of file diff --git a/templates/gdscript/addons/input_file.gd.twig b/templates/gdscript/addons/input_file.gd.twig index 4ecc9f53a0..acc58c1cdd 100644 --- a/templates/gdscript/addons/input_file.gd.twig +++ b/templates/gdscript/addons/input_file.gd.twig @@ -1,5 +1,4 @@ -{% set prefix = spec.title | caseUcfirst %} -class_name {{prefix}}InputFile +class_name InputFile var path: String ## Path to the file var bytes: PackedByteArray ## File bytes @@ -18,16 +17,16 @@ func _init(file_path: String = "", bytes_data: PackedByteArray = PackedByteArray ## Creates a new InputFile from a file path -static func from_path(file_path: String, custom_filename: String = "") -> {{spec.title | caseUcfirst}}InputFile: - return {{spec.title | caseUcfirst}}InputFile.new(file_path, PackedByteArray(), custom_filename) +static func from_path(file_path: String, custom_filename: String = "") -> InputFile: + return InputFile.new(file_path, PackedByteArray(), custom_filename) ## Creates a new InputFile from bytes -static func from_bytes(bytes_data: PackedByteArray, custom_filename: String = "") -> {{spec.title | caseUcfirst}}InputFile: - return {{spec.title | caseUcfirst}}InputFile.new("", bytes_data, custom_filename) +static func from_bytes(bytes_data: PackedByteArray, custom_filename: String = "") -> InputFile: + return InputFile.new("", bytes_data, custom_filename) -static func from_text(text: String, custom_filename: String = "") -> {{spec.title | caseUcfirst}}InputFile: +static func from_text(text: String, custom_filename: String = "") -> InputFile: return from_bytes(text.to_utf8_buffer(), custom_filename) @@ -40,6 +39,29 @@ func get_data() -> PackedByteArray: return PackedByteArray() +## Return the size of the file +func get_size() -> int: + if not bytes.is_empty(): + return bytes.size() + if not path.is_empty(): + var file := FileAccess.open(path, FileAccess.READ) + if file: + return file.get_length() + return 0 + + +## Return a chunk of the file +func get_chunk(offset: int, length: int) -> PackedByteArray: + if not bytes.is_empty(): + return bytes.slice(offset, offset + length) + if not path.is_empty(): + var file := FileAccess.open(path, FileAccess.READ) + if file: + file.seek(offset) + return file.get_buffer(length) + return PackedByteArray() + + static func _guess_mime_type(filename: String) -> String: var ext := filename.get_extension().to_lower() diff --git a/templates/gdscript/addons/models/model.gd.twig b/templates/gdscript/addons/models/model.gd.twig index b1d8e1ec61..331896defc 100644 --- a/templates/gdscript/addons/models/model.gd.twig +++ b/templates/gdscript/addons/models/model.gd.twig @@ -38,7 +38,7 @@ static func from_dict(dict: Dictionary): {% set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (definition.name ~ (property.name | caseUcfirst)) %} {% if property.model %} if key == "{{ field }}" and value is Dictionary: - m.set(key, {{ property.model | caseUcfirst }}.from_dict(value)) + m.set(key, {{ prefix }}{{ property.model | caseUcfirst }}.from_dict(value)) continue {% endif %} {% if property.array.model %} @@ -46,7 +46,7 @@ static func from_dict(dict: Dictionary): var list := [] for item in value: if item is Dictionary: - list.append({{ property.array.model | caseUcfirst }}.from_dict(item)) + list.append({{ prefix }}{{ property.array.model | caseUcfirst }}.from_dict(item)) else: list.append(item) m.set(key, list) diff --git a/templates/gdscript/addons/tests/test_input_files.gd.twig b/templates/gdscript/addons/tests/test_input_files.gd.twig index fb8ac42ada..4d3d342ed1 100644 --- a/templates/gdscript/addons/tests/test_input_files.gd.twig +++ b/templates/gdscript/addons/tests/test_input_files.gd.twig @@ -2,15 +2,15 @@ extends "res://addons/gut/test.gd" func test_input_file_from_bytes(): var bytes = PackedByteArray([1,2,3]) - var file = {{spec.title | caseUcfirst}}InputFile.from_bytes(bytes, "test.txt") + var file = InputFile.from_bytes(bytes, "test.txt") assert_eq(file.filename, "test.txt") assert_eq(file.get_data(), bytes) func test_input_file_from_path_sets_filename(): - var file = {{spec.title | caseUcfirst}}InputFile.from_path("res://test.txt") + var file = InputFile.from_path("res://test.txt") assert_eq(file.filename, "test.txt") func test_input_file_empty(): - var file = {{spec.title | caseUcfirst}}InputFile.new() + var file = InputFile.new() assert_eq(file.get_data(), PackedByteArray()) \ No newline at end of file diff --git a/templates/godot/addons/appwrite.gd.twig b/templates/godot/addons/appwrite.gd.twig index d53600d2e9..a866f85b26 100644 --- a/templates/godot/addons/appwrite.gd.twig +++ b/templates/godot/addons/appwrite.gd.twig @@ -2,6 +2,8 @@ extends Node ## [img width=32]res://addons/{{spec.title | caseSnake}}/icon.svg[/img] {{ spec.description | default("Appwrite _client.") | replace({"\n": "[br]\n## "}) }} +signal on_progress(progress: float, total_size: int, uploaded_size: int) + # Internal classes for type hinting {% for service in spec.services %} const _{{ service.name | caseUpper }} = preload("services/{{ service.name | caseSnake }}.gd") @@ -19,6 +21,7 @@ const {{ enumName | caseUpper }} = preload("enums/{{ enumName | caseSnake }}.gd" {% endfor %} func _ready() -> void: + _client.on_progress.connect(func(p, t, c): on_progress.emit(p, t, c)) var path = "res://.env" if not FileAccess.file_exists(path): return diff --git a/templates/godot/addons/client.gd.twig b/templates/godot/addons/client.gd.twig index 5497ba41f1..5a1722be79 100644 --- a/templates/godot/addons/client.gd.twig +++ b/templates/godot/addons/client.gd.twig @@ -1,6 +1,8 @@ # {% set prefix = spec.title | caseUcfirst %}Client extends RefCounted +signal on_progress(progress: float, total_size: int, uploaded_size: int) + var _chunk_size = 5 * 1024 * 1024 var _self_signed = false var _endpoint = '{{spec.endpoint}}' @@ -82,11 +84,17 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param request_path += "?" + query_http.query_string_from_dict(params) else: var has_files := false + var large_file_key := "" for key in params: - if params[key] is {{prefix}}InputFile: + if params[key] is InputFile: has_files = true + if params[key].get_size() > _chunk_size: + large_file_key = key break + if large_file_key != "" and not headers.has("content-range"): + return await _call_chunked(method, path, headers, params, large_file_key) + if has_files: var boundary := "Boundary-%x" % Time.get_ticks_msec() combined_headers["content-type"] = "multipart/form-data; boundary=" + boundary @@ -141,10 +149,10 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: var value = params[key] if value == null: continue - if value is {{prefix}}InputFile: + if value is InputFile: var data = value.get_data() if data.is_empty(): - push_warning("{{prefix}}InputFile contains no data") + push_warning("InputFile contains no data") continue body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) body.append_array(("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" % [key, value.filename]).to_utf8_buffer()) @@ -177,4 +185,37 @@ func _ensure_configured() -> bool: if not _global_headers.has("x-appwrite-project"): push_warning("{{prefix}}: project id is missing. Use set_project or .env.") return false - return true \ No newline at end of file + return true + + +func _call_chunked(method: String, path: String, headers: Dictionary, params: Dictionary, file_key: String) -> Dictionary: + var file: InputFile = params[file_key] + var size = file.get_size() + var start = 0 + var response := {} + + var headers_copy = headers.duplicate() + var params_copy = params.duplicate() + + while start < size: + var end = min(start + _chunk_size, size) + headers_copy["content-range"] = "bytes %d-%d/%d" % [start, end - 1, size] + + var chunk_data = file.get_chunk(start, end - start) + var chunk_file = InputFile.from_bytes(chunk_data, file.filename) + chunk_file.content_type = file.content_type + + params_copy[file_key] = chunk_file + + response = await call_api(method, path, headers_copy, params_copy) + + if response.has("body") and response["body"] is Dictionary and response["body"].has("$id"): + headers_copy["x-{{spec.title | caseLower }}-id"] = response["body"]["$id"] + + if response.get("statusCode", 0) >= 400: + return response + + start = end + on_progress.emit((float(start) / size) * 100.0, size, start) + + return response \ No newline at end of file diff --git a/templates/godot/addons/input_file.gd.twig b/templates/godot/addons/input_file.gd.twig index 4ecc9f53a0..acc58c1cdd 100644 --- a/templates/godot/addons/input_file.gd.twig +++ b/templates/godot/addons/input_file.gd.twig @@ -1,5 +1,4 @@ -{% set prefix = spec.title | caseUcfirst %} -class_name {{prefix}}InputFile +class_name InputFile var path: String ## Path to the file var bytes: PackedByteArray ## File bytes @@ -18,16 +17,16 @@ func _init(file_path: String = "", bytes_data: PackedByteArray = PackedByteArray ## Creates a new InputFile from a file path -static func from_path(file_path: String, custom_filename: String = "") -> {{spec.title | caseUcfirst}}InputFile: - return {{spec.title | caseUcfirst}}InputFile.new(file_path, PackedByteArray(), custom_filename) +static func from_path(file_path: String, custom_filename: String = "") -> InputFile: + return InputFile.new(file_path, PackedByteArray(), custom_filename) ## Creates a new InputFile from bytes -static func from_bytes(bytes_data: PackedByteArray, custom_filename: String = "") -> {{spec.title | caseUcfirst}}InputFile: - return {{spec.title | caseUcfirst}}InputFile.new("", bytes_data, custom_filename) +static func from_bytes(bytes_data: PackedByteArray, custom_filename: String = "") -> InputFile: + return InputFile.new("", bytes_data, custom_filename) -static func from_text(text: String, custom_filename: String = "") -> {{spec.title | caseUcfirst}}InputFile: +static func from_text(text: String, custom_filename: String = "") -> InputFile: return from_bytes(text.to_utf8_buffer(), custom_filename) @@ -40,6 +39,29 @@ func get_data() -> PackedByteArray: return PackedByteArray() +## Return the size of the file +func get_size() -> int: + if not bytes.is_empty(): + return bytes.size() + if not path.is_empty(): + var file := FileAccess.open(path, FileAccess.READ) + if file: + return file.get_length() + return 0 + + +## Return a chunk of the file +func get_chunk(offset: int, length: int) -> PackedByteArray: + if not bytes.is_empty(): + return bytes.slice(offset, offset + length) + if not path.is_empty(): + var file := FileAccess.open(path, FileAccess.READ) + if file: + file.seek(offset) + return file.get_buffer(length) + return PackedByteArray() + + static func _guess_mime_type(filename: String) -> String: var ext := filename.get_extension().to_lower() diff --git a/templates/godot/addons/models/model.gd.twig b/templates/godot/addons/models/model.gd.twig index b1d8e1ec61..331896defc 100644 --- a/templates/godot/addons/models/model.gd.twig +++ b/templates/godot/addons/models/model.gd.twig @@ -38,7 +38,7 @@ static func from_dict(dict: Dictionary): {% set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (definition.name ~ (property.name | caseUcfirst)) %} {% if property.model %} if key == "{{ field }}" and value is Dictionary: - m.set(key, {{ property.model | caseUcfirst }}.from_dict(value)) + m.set(key, {{ prefix }}{{ property.model | caseUcfirst }}.from_dict(value)) continue {% endif %} {% if property.array.model %} @@ -46,7 +46,7 @@ static func from_dict(dict: Dictionary): var list := [] for item in value: if item is Dictionary: - list.append({{ property.array.model | caseUcfirst }}.from_dict(item)) + list.append({{ prefix }}{{ property.array.model | caseUcfirst }}.from_dict(item)) else: list.append(item) m.set(key, list) diff --git a/templates/godot/addons/tests/test_input_files.gd.twig b/templates/godot/addons/tests/test_input_files.gd.twig index fb8ac42ada..4d3d342ed1 100644 --- a/templates/godot/addons/tests/test_input_files.gd.twig +++ b/templates/godot/addons/tests/test_input_files.gd.twig @@ -2,15 +2,15 @@ extends "res://addons/gut/test.gd" func test_input_file_from_bytes(): var bytes = PackedByteArray([1,2,3]) - var file = {{spec.title | caseUcfirst}}InputFile.from_bytes(bytes, "test.txt") + var file = InputFile.from_bytes(bytes, "test.txt") assert_eq(file.filename, "test.txt") assert_eq(file.get_data(), bytes) func test_input_file_from_path_sets_filename(): - var file = {{spec.title | caseUcfirst}}InputFile.from_path("res://test.txt") + var file = InputFile.from_path("res://test.txt") assert_eq(file.filename, "test.txt") func test_input_file_empty(): - var file = {{spec.title | caseUcfirst}}InputFile.new() + var file = InputFile.new() assert_eq(file.get_data(), PackedByteArray()) \ No newline at end of file From 541df6fc46947089be53f86a7bf715e916d69b1d Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Wed, 13 May 2026 18:42:31 +0530 Subject: [PATCH 25/58] feat: add param to getParamExample method --- src/SDK/Language.php | 3 ++- src/SDK/Language/AgentSkills.php | 3 ++- src/SDK/Language/CLI.php | 3 ++- src/SDK/Language/CursorPlugin.php | 3 ++- src/SDK/Language/Dart.php | 3 ++- src/SDK/Language/Deno.php | 3 ++- src/SDK/Language/DotNet.php | 3 ++- src/SDK/Language/GDScript.php | 10 ++++++---- src/SDK/Language/Go.php | 3 ++- src/SDK/Language/GraphQL.php | 3 ++- src/SDK/Language/Kotlin.php | 5 +++-- src/SDK/Language/Markdown.php | 3 ++- src/SDK/Language/Node.php | 3 ++- src/SDK/Language/PHP.php | 3 ++- src/SDK/Language/Python.php | 3 ++- src/SDK/Language/REST.php | 3 ++- src/SDK/Language/ReactNative.php | 3 ++- src/SDK/Language/Ruby.php | 3 ++- src/SDK/Language/Rust.php | 3 ++- src/SDK/Language/Swift.php | 3 ++- src/SDK/Language/Web.php | 3 ++- src/SDK/SDK.php | 2 +- templates/gdscript/addons/client.gd.twig | 10 +++++----- templates/gdscript/addons/input_file.gd.twig | 17 +++++++++-------- .../addons/tests/test_input_files.gd.twig | 7 ++++--- templates/godot/addons/client.gd.twig | 10 +++++----- templates/godot/addons/input_file.gd.twig | 17 +++++++++-------- .../godot/addons/tests/test_input_files.gd.twig | 7 ++++--- 28 files changed, 84 insertions(+), 58 deletions(-) diff --git a/src/SDK/Language.php b/src/SDK/Language.php index f41ea1b2d7..8a2aab4fa6 100644 --- a/src/SDK/Language.php +++ b/src/SDK/Language.php @@ -71,9 +71,10 @@ abstract public function getParamDefault(array $param): string; /** * @param array $param * @param string $lang Optional language variant (for multi-language SDKs) + * @param array $spec * @return string */ - abstract public function getParamExample(array $param, string $lang = ''): string; + abstract public function getParamExample(array $param, string $lang = '', array $spec = []): string; /** * @param string $key diff --git a/src/SDK/Language/AgentSkills.php b/src/SDK/Language/AgentSkills.php index 13dc120a74..f1e3a53a91 100644 --- a/src/SDK/Language/AgentSkills.php +++ b/src/SDK/Language/AgentSkills.php @@ -77,9 +77,10 @@ public function getParamDefault(array $param): string /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { return $param['example'] ?? ''; } diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index 37bf23288e..36b59847ee 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -736,9 +736,10 @@ public function getTypeName(array $parameter, array $spec = []): string /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/CursorPlugin.php b/src/SDK/Language/CursorPlugin.php index e8b1bdccde..7316795ac1 100644 --- a/src/SDK/Language/CursorPlugin.php +++ b/src/SDK/Language/CursorPlugin.php @@ -77,9 +77,10 @@ public function getParamDefault(array $param): string /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { return $param['example'] ?? ''; } diff --git a/src/SDK/Language/Dart.php b/src/SDK/Language/Dart.php index 6be483fd1a..e0d249d699 100644 --- a/src/SDK/Language/Dart.php +++ b/src/SDK/Language/Dart.php @@ -253,9 +253,10 @@ public function getParamDefault(array $param): string /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/Deno.php b/src/SDK/Language/Deno.php index 0d13cbb57c..cf56a25d57 100644 --- a/src/SDK/Language/Deno.php +++ b/src/SDK/Language/Deno.php @@ -189,9 +189,10 @@ public function getTypeName(array $parameter, array $spec = []): string /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/DotNet.php b/src/SDK/Language/DotNet.php index d7b2883d6b..a5883d905c 100644 --- a/src/SDK/Language/DotNet.php +++ b/src/SDK/Language/DotNet.php @@ -280,9 +280,10 @@ public function getParamDefault(array $param): string /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/GDScript.php b/src/SDK/Language/GDScript.php index f197902a38..912b9ef3bc 100644 --- a/src/SDK/Language/GDScript.php +++ b/src/SDK/Language/GDScript.php @@ -365,7 +365,7 @@ public function getTypeName(array $parameter, array $spec = []): string self::TYPE_BOOLEAN => 'bool', self::TYPE_ARRAY => 'Array', self::TYPE_OBJECT => 'Dictionary', - self::TYPE_FILE => 'InputFile', + self::TYPE_FILE => $prefix . 'InputFile', default => 'Variant', }; } @@ -450,12 +450,14 @@ public function getParamDefault(array $param): string /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; + $prefix = $this->toPascalCase($spec['title'] ?? ''); $output = ''; @@ -480,7 +482,7 @@ public function getParamExample(array $param, string $lang = ''): string $output .= '{}'; break; case self::TYPE_FILE: - $output .= 'InputFile.from_path("file.png")'; + $output .= $prefix . 'InputFile.from_path("file.png")'; break; } } else { @@ -498,7 +500,7 @@ public function getParamExample(array $param, string $lang = ''): string $output .= "'{$example}'"; break; case self::TYPE_FILE: - $output .= 'InputFile.from_path("file.png")'; + $output .= $prefix . 'InputFile.from_path("file.png")'; break; } } diff --git a/src/SDK/Language/Go.php b/src/SDK/Language/Go.php index 39f9f09be9..5ca08ee66e 100644 --- a/src/SDK/Language/Go.php +++ b/src/SDK/Language/Go.php @@ -300,9 +300,10 @@ public function getParamDefault(array $param): string /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/GraphQL.php b/src/SDK/Language/GraphQL.php index 19b581e9b6..a9e5869717 100644 --- a/src/SDK/Language/GraphQL.php +++ b/src/SDK/Language/GraphQL.php @@ -129,9 +129,10 @@ public function getParamDefault(array $param): string /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/Kotlin.php b/src/SDK/Language/Kotlin.php index 024bce9477..8fbfe46b1d 100644 --- a/src/SDK/Language/Kotlin.php +++ b/src/SDK/Language/Kotlin.php @@ -223,10 +223,11 @@ public function getParamDefault(array $param): string /** * @param array $param - * @param string $lang Language variant: 'kotlin' (default) or 'java' + * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = 'kotlin'): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/Markdown.php b/src/SDK/Language/Markdown.php index a3d151aec1..170134500f 100644 --- a/src/SDK/Language/Markdown.php +++ b/src/SDK/Language/Markdown.php @@ -169,9 +169,10 @@ public function getParamDefault(array $param): string /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/Node.php b/src/SDK/Language/Node.php index 7062e85cb0..c725780e9d 100644 --- a/src/SDK/Language/Node.php +++ b/src/SDK/Language/Node.php @@ -99,9 +99,10 @@ public function getReturn(array $method, array $spec): string /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/PHP.php b/src/SDK/Language/PHP.php index 171a0a4575..613d2c173a 100644 --- a/src/SDK/Language/PHP.php +++ b/src/SDK/Language/PHP.php @@ -441,9 +441,10 @@ public function getParamDefault(array $param): string /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/Python.php b/src/SDK/Language/Python.php index 195bc27cf1..f91c108c14 100644 --- a/src/SDK/Language/Python.php +++ b/src/SDK/Language/Python.php @@ -438,9 +438,10 @@ public function getParamDefault(array $param): string /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/REST.php b/src/SDK/Language/REST.php index c83e1ec734..f686a1a717 100644 --- a/src/SDK/Language/REST.php +++ b/src/SDK/Language/REST.php @@ -85,9 +85,10 @@ public function getParamDefault(array $param): string /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/ReactNative.php b/src/SDK/Language/ReactNative.php index e82f7b2010..97b02943d3 100644 --- a/src/SDK/Language/ReactNative.php +++ b/src/SDK/Language/ReactNative.php @@ -165,9 +165,10 @@ public function getTypeName(array $parameter, array $method = []): string /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/Ruby.php b/src/SDK/Language/Ruby.php index 698b4250ad..67be4c0b0f 100644 --- a/src/SDK/Language/Ruby.php +++ b/src/SDK/Language/Ruby.php @@ -308,9 +308,10 @@ public function getParamDefault(array $param): string /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/Rust.php b/src/SDK/Language/Rust.php index c4fd98c561..7e4e5bd1fc 100644 --- a/src/SDK/Language/Rust.php +++ b/src/SDK/Language/Rust.php @@ -423,9 +423,10 @@ public function getParamDefault(array $param): string /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param["type"] ?? ""; $example = $param["example"] ?? ""; diff --git a/src/SDK/Language/Swift.php b/src/SDK/Language/Swift.php index 99a5339e02..8c32dc305e 100644 --- a/src/SDK/Language/Swift.php +++ b/src/SDK/Language/Swift.php @@ -433,9 +433,10 @@ public function getParamDefault(array $param): string /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/Web.php b/src/SDK/Language/Web.php index 28002b7c09..e584ea9dce 100644 --- a/src/SDK/Language/Web.php +++ b/src/SDK/Language/Web.php @@ -166,9 +166,10 @@ public function getFiles(): array /** * @param array $param * @param string $lang + * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = ''): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/SDK.php b/src/SDK/SDK.php index b40d85d4ad..2f5d1cc073 100644 --- a/src/SDK/SDK.php +++ b/src/SDK/SDK.php @@ -184,7 +184,7 @@ public function __construct(Language $language, Spec $spec) return $this->language->getParamDefault($value); }, ['is_safe' => ['html']])); $this->twig->addFilter(new TwigFilter('paramExample', function ($value) { - return $this->language->getParamExample($value); + return $this->language->getParamExample($value, '', $this->spec->getArrayCopy()); }, ['is_safe' => ['html']])); $this->twig->addFilter(new TwigFilter('comment1', function ($value) { $value = explode("\n", $value); diff --git a/templates/gdscript/addons/client.gd.twig b/templates/gdscript/addons/client.gd.twig index 509f7a1fb5..3f958d14a2 100644 --- a/templates/gdscript/addons/client.gd.twig +++ b/templates/gdscript/addons/client.gd.twig @@ -83,7 +83,7 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param var has_files := false var large_file_key := "" for key in params: - if params[key] is InputFile: + if params[key] is {{ prefix }}InputFile: has_files = true if params[key].get_size() > _chunk_size: large_file_key = key @@ -138,10 +138,10 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: var value = params[key] if value == null: continue - if value is InputFile: + if value is {{prefix}}InputFile: var data = value.get_data() if data.is_empty(): - push_warning("InputFile contains no data") + push_warning("{{prefix}}InputFile contains no data") continue body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) body.append_array(("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" % [key, value.filename]).to_utf8_buffer()) @@ -178,7 +178,7 @@ func _ensure_configured() -> bool: func _call_chunked(method: String, path: String, headers: Dictionary, params: Dictionary, file_key: String) -> Dictionary: - var file: InputFile = params[file_key] + var file: {{prefix}}InputFile = params[file_key] var size = file.get_size() var start = 0 var response := {} @@ -191,7 +191,7 @@ func _call_chunked(method: String, path: String, headers: Dictionary, params: Di headers_copy["content-range"] = "bytes %d-%d/%d" % [start, end - 1, size] var chunk_data = file.get_chunk(start, end - start) - var chunk_file = InputFile.from_bytes(chunk_data, file.filename) + var chunk_file = {{prefix}}InputFile.from_bytes(chunk_data, file.filename) chunk_file.content_type = file.content_type params_copy[file_key] = chunk_file diff --git a/templates/gdscript/addons/input_file.gd.twig b/templates/gdscript/addons/input_file.gd.twig index acc58c1cdd..84bec78b33 100644 --- a/templates/gdscript/addons/input_file.gd.twig +++ b/templates/gdscript/addons/input_file.gd.twig @@ -1,4 +1,5 @@ -class_name InputFile +{% set prefix = spec.title | caseUcfirst %} +class_name {{prefix}}InputFile var path: String ## Path to the file var bytes: PackedByteArray ## File bytes @@ -16,17 +17,17 @@ func _init(file_path: String = "", bytes_data: PackedByteArray = PackedByteArray self.content_type = _guess_mime_type(self.path.get_file()) -## Creates a new InputFile from a file path -static func from_path(file_path: String, custom_filename: String = "") -> InputFile: - return InputFile.new(file_path, PackedByteArray(), custom_filename) +## Creates a new {{prefix}}InputFile from a file path +static func from_path(file_path: String, custom_filename: String = "") -> {{prefix}}InputFile: + return {{prefix}}InputFile.new(file_path, PackedByteArray(), custom_filename) -## Creates a new InputFile from bytes -static func from_bytes(bytes_data: PackedByteArray, custom_filename: String = "") -> InputFile: - return InputFile.new("", bytes_data, custom_filename) +## Creates a new {{prefix}}InputFile from bytes +static func from_bytes(bytes_data: PackedByteArray, custom_filename: String = "") -> {{prefix}}InputFile: + return {{prefix}}InputFile.new("", bytes_data, custom_filename) -static func from_text(text: String, custom_filename: String = "") -> InputFile: +static func from_text(text: String, custom_filename: String = "") -> {{prefix}}InputFile: return from_bytes(text.to_utf8_buffer(), custom_filename) diff --git a/templates/gdscript/addons/tests/test_input_files.gd.twig b/templates/gdscript/addons/tests/test_input_files.gd.twig index 4d3d342ed1..2d7d8b42a1 100644 --- a/templates/gdscript/addons/tests/test_input_files.gd.twig +++ b/templates/gdscript/addons/tests/test_input_files.gd.twig @@ -1,16 +1,17 @@ +{% set prefix = spec.title | caseUcfirst %} extends "res://addons/gut/test.gd" func test_input_file_from_bytes(): var bytes = PackedByteArray([1,2,3]) - var file = InputFile.from_bytes(bytes, "test.txt") + var file = {{prefix}}InputFile.from_bytes(bytes, "test.txt") assert_eq(file.filename, "test.txt") assert_eq(file.get_data(), bytes) func test_input_file_from_path_sets_filename(): - var file = InputFile.from_path("res://test.txt") + var file = {{prefix}}InputFile.from_path("res://test.txt") assert_eq(file.filename, "test.txt") func test_input_file_empty(): - var file = InputFile.new() + var file = {{prefix}}InputFile.new() assert_eq(file.get_data(), PackedByteArray()) \ No newline at end of file diff --git a/templates/godot/addons/client.gd.twig b/templates/godot/addons/client.gd.twig index 5a1722be79..b1c72f64bf 100644 --- a/templates/godot/addons/client.gd.twig +++ b/templates/godot/addons/client.gd.twig @@ -86,7 +86,7 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param var has_files := false var large_file_key := "" for key in params: - if params[key] is InputFile: + if params[key] is {{prefix}}InputFile: has_files = true if params[key].get_size() > _chunk_size: large_file_key = key @@ -149,10 +149,10 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: var value = params[key] if value == null: continue - if value is InputFile: + if value is {{prefix}}InputFile: var data = value.get_data() if data.is_empty(): - push_warning("InputFile contains no data") + push_warning("{{prefix}}InputFile contains no data") continue body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) body.append_array(("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" % [key, value.filename]).to_utf8_buffer()) @@ -189,7 +189,7 @@ func _ensure_configured() -> bool: func _call_chunked(method: String, path: String, headers: Dictionary, params: Dictionary, file_key: String) -> Dictionary: - var file: InputFile = params[file_key] + var file: {{prefix}}InputFile = params[file_key] var size = file.get_size() var start = 0 var response := {} @@ -202,7 +202,7 @@ func _call_chunked(method: String, path: String, headers: Dictionary, params: Di headers_copy["content-range"] = "bytes %d-%d/%d" % [start, end - 1, size] var chunk_data = file.get_chunk(start, end - start) - var chunk_file = InputFile.from_bytes(chunk_data, file.filename) + var chunk_file = {{prefix}}InputFile.from_bytes(chunk_data, file.filename) chunk_file.content_type = file.content_type params_copy[file_key] = chunk_file diff --git a/templates/godot/addons/input_file.gd.twig b/templates/godot/addons/input_file.gd.twig index acc58c1cdd..84bec78b33 100644 --- a/templates/godot/addons/input_file.gd.twig +++ b/templates/godot/addons/input_file.gd.twig @@ -1,4 +1,5 @@ -class_name InputFile +{% set prefix = spec.title | caseUcfirst %} +class_name {{prefix}}InputFile var path: String ## Path to the file var bytes: PackedByteArray ## File bytes @@ -16,17 +17,17 @@ func _init(file_path: String = "", bytes_data: PackedByteArray = PackedByteArray self.content_type = _guess_mime_type(self.path.get_file()) -## Creates a new InputFile from a file path -static func from_path(file_path: String, custom_filename: String = "") -> InputFile: - return InputFile.new(file_path, PackedByteArray(), custom_filename) +## Creates a new {{prefix}}InputFile from a file path +static func from_path(file_path: String, custom_filename: String = "") -> {{prefix}}InputFile: + return {{prefix}}InputFile.new(file_path, PackedByteArray(), custom_filename) -## Creates a new InputFile from bytes -static func from_bytes(bytes_data: PackedByteArray, custom_filename: String = "") -> InputFile: - return InputFile.new("", bytes_data, custom_filename) +## Creates a new {{prefix}}InputFile from bytes +static func from_bytes(bytes_data: PackedByteArray, custom_filename: String = "") -> {{prefix}}InputFile: + return {{prefix}}InputFile.new("", bytes_data, custom_filename) -static func from_text(text: String, custom_filename: String = "") -> InputFile: +static func from_text(text: String, custom_filename: String = "") -> {{prefix}}InputFile: return from_bytes(text.to_utf8_buffer(), custom_filename) diff --git a/templates/godot/addons/tests/test_input_files.gd.twig b/templates/godot/addons/tests/test_input_files.gd.twig index 4d3d342ed1..2d7d8b42a1 100644 --- a/templates/godot/addons/tests/test_input_files.gd.twig +++ b/templates/godot/addons/tests/test_input_files.gd.twig @@ -1,16 +1,17 @@ +{% set prefix = spec.title | caseUcfirst %} extends "res://addons/gut/test.gd" func test_input_file_from_bytes(): var bytes = PackedByteArray([1,2,3]) - var file = InputFile.from_bytes(bytes, "test.txt") + var file = {{prefix}}InputFile.from_bytes(bytes, "test.txt") assert_eq(file.filename, "test.txt") assert_eq(file.get_data(), bytes) func test_input_file_from_path_sets_filename(): - var file = InputFile.from_path("res://test.txt") + var file = {{prefix}}InputFile.from_path("res://test.txt") assert_eq(file.filename, "test.txt") func test_input_file_empty(): - var file = InputFile.new() + var file = {{prefix}}InputFile.new() assert_eq(file.get_data(), PackedByteArray()) \ No newline at end of file From 8f9a895174b20137cf86239b73dfa514ec653285 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Wed, 13 May 2026 19:12:30 +0530 Subject: [PATCH 26/58] fix: djLint H014 issue --- templates/gdscript/addons/appwrite.gd.twig | 7 ------ templates/gdscript/addons/client.gd.twig | 10 -------- templates/gdscript/addons/enums/enum.gd.twig | 1 - templates/gdscript/addons/id.gd.twig | 2 -- templates/gdscript/addons/input_file.gd.twig | 7 ------ .../gdscript/addons/models/model.gd.twig | 5 ---- templates/gdscript/addons/operator.gd.twig | 24 ------------------- templates/gdscript/addons/plugin.gd.twig | 4 +--- templates/gdscript/addons/role.gd.twig | 6 ----- templates/gdscript/addons/service.gd.twig | 1 - .../addons/tests/test_operator.gd.twig | 19 --------------- .../gdscript/addons/tests/test_query.gd.twig | 10 -------- templates/godot/addons/appwrite.gd.twig | 7 ------ templates/godot/addons/client.gd.twig | 10 -------- templates/godot/addons/enums/enum.gd.twig | 1 - templates/godot/addons/id.gd.twig | 2 -- templates/godot/addons/input_file.gd.twig | 7 ------ templates/godot/addons/models/model.gd.twig | 5 ---- templates/godot/addons/operator.gd.twig | 24 ------------------- templates/godot/addons/plugin.gd.twig | 1 - templates/godot/addons/role.gd.twig | 6 ----- templates/godot/addons/service.gd.twig | 1 - .../godot/addons/tests/test_operator.gd.twig | 19 --------------- .../godot/addons/tests/test_query.gd.twig | 10 -------- 24 files changed, 1 insertion(+), 188 deletions(-) diff --git a/templates/gdscript/addons/appwrite.gd.twig b/templates/gdscript/addons/appwrite.gd.twig index a866f85b26..cb487a48a9 100644 --- a/templates/gdscript/addons/appwrite.gd.twig +++ b/templates/gdscript/addons/appwrite.gd.twig @@ -45,7 +45,6 @@ func _ready() -> void: _apply_env(key, value) - {% for header in spec.global.headers %} {% if header.description %} ## {{header.description}} @@ -53,28 +52,23 @@ func _ready() -> void: func set_{{header.key | caseSnake}}(value: String) -> void: _client.set_{{header.key | caseSnake}}(value) - {% endfor %} ## Set self signed status func set_self_signed(status: bool = true) -> void: _client.set_self_signed(status) - ## Set the endpoint func set_endpoint(endpoint: String) -> void: _client.set_endpoint(endpoint) - ## Add a header func add_header(key: String, value: String) -> void: _client.add_header(key, value) - ## Get all headers func get_headers() -> Dictionary: return _client.get_headers() - func _apply_env(key: String, value: String) -> void: match key: "{{ prefix | caseUpper }}_ENDPOINT": @@ -85,7 +79,6 @@ func _apply_env(key: String, value: String) -> void: "{{ prefix | caseUpper }}_{{ header.key | caseUpper }}": _client.set_{{ header.key | caseSnake }}(value) {% endfor %} - ## Ping {{prefix}} Server for testing connection func ping() -> String: diff --git a/templates/gdscript/addons/client.gd.twig b/templates/gdscript/addons/client.gd.twig index 3f958d14a2..0eda09e385 100644 --- a/templates/gdscript/addons/client.gd.twig +++ b/templates/gdscript/addons/client.gd.twig @@ -22,30 +22,23 @@ func set_self_signed(status: bool = true) -> RefCounted: _self_signed = status return self - func set_endpoint(endpoint: String) -> RefCounted: _endpoint = endpoint return self - func add_header(key: String, value: String) -> RefCounted: _global_headers[key.to_lower()] = value return self - func get_headers() -> Dictionary: return _global_headers.duplicate() - {% for header in spec.global.headers %} func set_{{header.key | caseSnake}}(value: String) -> RefCounted: _global_headers['{{header.name|lower}}'] = value return self - {% endfor %} - - func call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: if not _ensure_configured(): return {"statusCode": 400, "body": "Missing/wrong configuration. Please check logs for more info"} @@ -131,7 +124,6 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param else: return {"statusCode": response_code, "body": response_text} - func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: var body := PackedByteArray() for key in params: @@ -166,7 +158,6 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: body.append_array(("--" + boundary + "--\r\n").to_utf8_buffer()) return body - func _ensure_configured() -> bool: if _endpoint == "": push_warning("{{prefix}}: endpoint is missing. Use set_endpoint or .env.") @@ -176,7 +167,6 @@ func _ensure_configured() -> bool: return false return true - func _call_chunked(method: String, path: String, headers: Dictionary, params: Dictionary, file_key: String) -> Dictionary: var file: {{prefix}}InputFile = params[file_key] var size = file.get_size() diff --git a/templates/gdscript/addons/enums/enum.gd.twig b/templates/gdscript/addons/enums/enum.gd.twig index 040e0bc00a..e39c1ed42e 100644 --- a/templates/gdscript/addons/enums/enum.gd.twig +++ b/templates/gdscript/addons/enums/enum.gd.twig @@ -9,7 +9,6 @@ const {{ key | caseEnumKey }} = "{{ value }}" static func is_valid(value: String) -> bool: return value in values() - ## Get all values of enum static func values() -> Array[String]: return [ diff --git a/templates/gdscript/addons/id.gd.twig b/templates/gdscript/addons/id.gd.twig index 9adef4794d..c7f6195173 100644 --- a/templates/gdscript/addons/id.gd.twig +++ b/templates/gdscript/addons/id.gd.twig @@ -8,7 +8,6 @@ static func _hex_timestamp() -> String: var usec_hex := "%05x" % usec return sec_hex + usec_hex - ## Generate a unique ID with padding to have a longer ID static func unique(padding: int = 7) -> String: var id := _hex_timestamp() @@ -19,6 +18,5 @@ static func unique(padding: int = 7) -> String: id += sb return id - static func custom(id: String) -> String: return id diff --git a/templates/gdscript/addons/input_file.gd.twig b/templates/gdscript/addons/input_file.gd.twig index 84bec78b33..ffa6c47e68 100644 --- a/templates/gdscript/addons/input_file.gd.twig +++ b/templates/gdscript/addons/input_file.gd.twig @@ -16,21 +16,17 @@ func _init(file_path: String = "", bytes_data: PackedByteArray = PackedByteArray self.content_type = _guess_mime_type(self.path.get_file()) - ## Creates a new {{prefix}}InputFile from a file path static func from_path(file_path: String, custom_filename: String = "") -> {{prefix}}InputFile: return {{prefix}}InputFile.new(file_path, PackedByteArray(), custom_filename) - ## Creates a new {{prefix}}InputFile from bytes static func from_bytes(bytes_data: PackedByteArray, custom_filename: String = "") -> {{prefix}}InputFile: return {{prefix}}InputFile.new("", bytes_data, custom_filename) - static func from_text(text: String, custom_filename: String = "") -> {{prefix}}InputFile: return from_bytes(text.to_utf8_buffer(), custom_filename) - ## Return the PackedByteArray of the file func get_data() -> PackedByteArray: if not bytes.is_empty(): @@ -39,7 +35,6 @@ func get_data() -> PackedByteArray: return FileAccess.get_file_as_bytes(path) return PackedByteArray() - ## Return the size of the file func get_size() -> int: if not bytes.is_empty(): @@ -50,7 +45,6 @@ func get_size() -> int: return file.get_length() return 0 - ## Return a chunk of the file func get_chunk(offset: int, length: int) -> PackedByteArray: if not bytes.is_empty(): @@ -62,7 +56,6 @@ func get_chunk(offset: int, length: int) -> PackedByteArray: return file.get_buffer(length) return PackedByteArray() - static func _guess_mime_type(filename: String) -> String: var ext := filename.get_extension().to_lower() diff --git a/templates/gdscript/addons/models/model.gd.twig b/templates/gdscript/addons/models/model.gd.twig index 331896defc..759129967a 100644 --- a/templates/gdscript/addons/models/model.gd.twig +++ b/templates/gdscript/addons/models/model.gd.twig @@ -77,12 +77,9 @@ static func from_dict(dict: Dictionary): continue {% endif %} {% endfor %} - m.set(key, value) - return m - ## Convert to Dictionary func to_dict() -> Dictionary: var dict := {} @@ -107,7 +104,5 @@ func to_dict() -> Dictionary: continue {% endif %} {% endfor %} - dict[_FIELD_MAP[key]] = value - return dict \ No newline at end of file diff --git a/templates/gdscript/addons/operator.gd.twig b/templates/gdscript/addons/operator.gd.twig index 85534b7fa3..c293d90812 100644 --- a/templates/gdscript/addons/operator.gd.twig +++ b/templates/gdscript/addons/operator.gd.twig @@ -11,11 +11,9 @@ const CONTAINS := "contains" const IS_NULL := "isNull" const IS_NOT_NULL := "isNotNull" - var method: String var values: Array - ## Validate condition value static func is_valid(value: String) -> bool: return value in [ @@ -30,7 +28,6 @@ static func is_valid(value: String) -> bool: IS_NOT_NULL ] - ## Constructor func _init(_method: String, _values: Variant = null) -> void: method = _method @@ -43,7 +40,6 @@ func _init(_method: String, _values: Variant = null) -> void: else: values = [] - ## Convert operator object to JSON string func _to_string() -> String: return JSON.stringify({ @@ -51,7 +47,6 @@ func _to_string() -> String: "values": values }) - static func increment(value: float = 1, max: Variant = null) -> String: if not is_finite(value): push_error("Value cannot be NaN or Infinity") @@ -67,7 +62,6 @@ static func increment(value: float = 1, max: Variant = null) -> String: return AppwriteOperator.new("increment", vals).to_string() - static func decrement(value: float = 1, min: Variant = null) -> String: if not is_finite(value): push_error("Value cannot be NaN or Infinity") @@ -83,7 +77,6 @@ static func decrement(value: float = 1, min: Variant = null) -> String: return AppwriteOperator.new("decrement", vals).to_string() - static func multiply(factor: float, max: Variant = null) -> String: if not is_finite(factor): push_error("Factor cannot be NaN or Infinity") @@ -99,7 +92,6 @@ static func multiply(factor: float, max: Variant = null) -> String: return AppwriteOperator.new("multiply", vals).to_string() - static func divide(divisor: float, min: Variant = null) -> String: if not is_finite(divisor): push_error("Divisor cannot be NaN or Infinity") @@ -119,7 +111,6 @@ static func divide(divisor: float, min: Variant = null) -> String: return AppwriteOperator.new("divide", vals).to_string() - static func modulo(divisor: float) -> String: if not is_finite(divisor): push_error("Divisor cannot be NaN or Infinity") @@ -131,7 +122,6 @@ static func modulo(divisor: float) -> String: return AppwriteOperator.new("modulo", [divisor]).to_string() - static func power(exponent: float, max: Variant = null) -> String: if not is_finite(exponent): push_error("Exponent cannot be NaN or Infinity") @@ -147,35 +137,27 @@ static func power(exponent: float, max: Variant = null) -> String: return AppwriteOperator.new("power", vals).to_string() - static func array_append(values: Array) -> String: return AppwriteOperator.new("arrayAppend", values).to_string() - static func array_prepend(values: Array) -> String: return AppwriteOperator.new("arrayPrepend", values).to_string() - static func array_insert(index: int, value: Variant) -> String: return AppwriteOperator.new("arrayInsert", [index, value]).to_string() - static func array_remove(value: Variant) -> String: return AppwriteOperator.new("arrayRemove", [value]).to_string() - static func array_unique() -> String: return AppwriteOperator.new("arrayUnique", []).to_string() - static func array_intersect(values: Array) -> String: return AppwriteOperator.new("arrayIntersect", values).to_string() - static func array_diff(values: Array) -> String: return AppwriteOperator.new("arrayDiff", values).to_string() - static func array_filter(condition: String, value: Variant = null) -> String: if not is_valid(condition): push_error("Invalid condition: %s" % condition) @@ -183,26 +165,20 @@ static func array_filter(condition: String, value: Variant = null) -> String: return AppwriteOperator.new("arrayFilter", [condition, value]).to_string() - static func string_concat(value: Variant) -> String: return AppwriteOperator.new("stringConcat", [value]).to_string() - static func string_replace(search: String, replace: String) -> String: return AppwriteOperator.new("stringReplace", [search, replace]).to_string() - static func toggle() -> String: return AppwriteOperator.new("toggle", []).to_string() - static func date_add_days(days: int) -> String: return AppwriteOperator.new("dateAddDays", [days]).to_string() - static func date_sub_days(days: int) -> String: return AppwriteOperator.new("dateSubDays", [days]).to_string() - static func date_set_now() -> String: return AppwriteOperator.new("dateSetNow", []).to_string() \ No newline at end of file diff --git a/templates/gdscript/addons/plugin.gd.twig b/templates/gdscript/addons/plugin.gd.twig index 0f8b33bf97..1aca810b21 100644 --- a/templates/gdscript/addons/plugin.gd.twig +++ b/templates/gdscript/addons/plugin.gd.twig @@ -7,7 +7,5 @@ const AUTOLOAD_PATH : String = "res://addons/{{spec.title | caseSnake}}/{{spec.t func _enable_plugin() -> void: add_autoload_singleton(AUTOLOAD_NAME, AUTOLOAD_PATH) - func _disable_plugin() -> void: - remove_autoload_singleton(AUTOLOAD_NAME) - + remove_autoload_singleton(AUTOLOAD_NAME) \ No newline at end of file diff --git a/templates/gdscript/addons/role.gd.twig b/templates/gdscript/addons/role.gd.twig index d73b2e3829..0658cafb49 100644 --- a/templates/gdscript/addons/role.gd.twig +++ b/templates/gdscript/addons/role.gd.twig @@ -1,30 +1,25 @@ class_name {{spec.title | caseUcfirst}}Role - ## Any role static func any() -> String: return 'any' - ## User role static func user(id: String, status: String = '') -> String: if status == '': return 'user:%s' % id return 'user:%s/%s' % [id, status] - ## Users role static func users(status: String = '') -> String: if status == '': return 'users' return 'users/%s' % status - ## Guests role static func guests() -> String: return 'guests' - ## Team role static func team(id: String, role: String = '') -> String: if role == '': @@ -35,6 +30,5 @@ static func team(id: String, role: String = '') -> String: static func member(id: String) -> String: return 'member:%s' % id - static func label(name: String) -> String: return 'label:%s' % name diff --git a/templates/gdscript/addons/service.gd.twig b/templates/gdscript/addons/service.gd.twig index 76b6839cb9..16ac7f205c 100644 --- a/templates/gdscript/addons/service.gd.twig +++ b/templates/gdscript/addons/service.gd.twig @@ -3,7 +3,6 @@ extends RefCounted - var client: RefCounted func _init(p_client: RefCounted) -> void: diff --git a/templates/gdscript/addons/tests/test_operator.gd.twig b/templates/gdscript/addons/tests/test_operator.gd.twig index 3032b2ffe9..6e64afc3b7 100644 --- a/templates/gdscript/addons/tests/test_operator.gd.twig +++ b/templates/gdscript/addons/tests/test_operator.gd.twig @@ -8,7 +8,6 @@ func test_operator_to_string(): assert_eq(parsed["method"], "test") assert_eq(parsed["values"].size(), 2) - func test_operator_value_wrapping(): var op = {{prefix}}Operator.new("test", 5) var parsed = JSON.parse_string(op.to_string()) @@ -16,20 +15,17 @@ func test_operator_value_wrapping(): assert_true(parsed["values"] is Array) assert_eq(parsed["values"][0], 5) - func test_operator_null_values(): var op = {{prefix}}Operator.new("test", null) var parsed = JSON.parse_string(op.to_string()) assert_eq(parsed["values"].size(), 0) - func test_is_valid_condition(): assert_true({{prefix}}Operator.is_valid({{prefix}}Operator.EQUAL)) assert_true({{prefix}}Operator.is_valid({{prefix}}Operator.NOT_EQUAL)) assert_false({{prefix}}Operator.is_valid("invalid")) - func test_increment_basic(): var q = {{prefix}}Operator.increment(5) var parsed = JSON.parse_string(q) @@ -37,7 +33,6 @@ func test_increment_basic(): assert_eq(parsed["method"], "increment") assert_eq(parsed["values"][0], 5) - func test_increment_with_max(): var q = {{prefix}}Operator.increment(5, 10) var parsed = JSON.parse_string(q) @@ -45,20 +40,16 @@ func test_increment_with_max(): assert_eq(parsed["values"].size(), 2) assert_eq(parsed["values"][1], 10) - func test_increment_invalid_number(): var q = {{prefix}}Operator.increment(INF) assert_eq(q, "") # should fail gracefully assert_push_error("Value cannot be NaN or Infinity") - - func test_divide_by_zero(): var q = {{prefix}}Operator.divide(0) assert_eq(q, "") assert_push_error("Divisor cannot be zero") - func test_divide_valid(): var q = {{prefix}}Operator.divide(10) var parsed = JSON.parse_string(q) @@ -66,7 +57,6 @@ func test_divide_valid(): assert_eq(parsed["method"], "divide") assert_eq(parsed["values"][0], 10) - func test_array_append(): var q = {{prefix}}Operator.array_append([1, 2]) var parsed = JSON.parse_string(q) @@ -74,7 +64,6 @@ func test_array_append(): assert_eq(parsed["method"], "arrayAppend") assert_eq(parsed["values"].size(), 2) - func test_array_insert(): var q = {{prefix}}Operator.array_insert(1, "x") var parsed = JSON.parse_string(q) @@ -82,7 +71,6 @@ func test_array_insert(): assert_eq(parsed["values"][0], 1) assert_eq(parsed["values"][1], "x") - func test_array_unique(): var q = {{prefix}}Operator.array_unique() var parsed = JSON.parse_string(q) @@ -90,7 +78,6 @@ func test_array_unique(): assert_eq(parsed["method"], "arrayUnique") assert_eq(parsed["values"].size(), 0) - func test_array_filter_valid(): var q = {{prefix}}Operator.array_filter({{prefix}}Operator.EQUAL, 10) var parsed = JSON.parse_string(q) @@ -98,13 +85,11 @@ func test_array_filter_valid(): assert_eq(parsed["method"], "arrayFilter") assert_eq(parsed["values"][0], "equal") - func test_array_filter_invalid_condition(): var q = {{prefix}}Operator.array_filter("INVALID", 10) assert_eq(q, "") assert_push_error("Invalid condition: INVALID") - func test_string_concat(): var q = {{prefix}}Operator.string_concat("abc") var parsed = JSON.parse_string(q) @@ -112,7 +97,6 @@ func test_string_concat(): assert_eq(parsed["method"], "stringConcat") assert_eq(parsed["values"][0], "abc") - func test_string_replace(): var q = {{prefix}}Operator.string_replace("a", "b") var parsed = JSON.parse_string(q) @@ -120,7 +104,6 @@ func test_string_replace(): assert_eq(parsed["values"][0], "a") assert_eq(parsed["values"][1], "b") - func test_toggle(): var q = {{prefix}}Operator.toggle() var parsed = JSON.parse_string(q) @@ -128,7 +111,6 @@ func test_toggle(): assert_eq(parsed["method"], "toggle") assert_eq(parsed["values"].size(), 0) - func test_date_add_days(): var q = {{prefix}}Operator.date_add_days(5) var parsed = JSON.parse_string(q) @@ -136,7 +118,6 @@ func test_date_add_days(): assert_eq(parsed["method"], "dateAddDays") assert_eq(parsed["values"][0], 5) - func test_date_set_now(): var q = {{prefix}}Operator.date_set_now() var parsed = JSON.parse_string(q) diff --git a/templates/gdscript/addons/tests/test_query.gd.twig b/templates/gdscript/addons/tests/test_query.gd.twig index bf8946d3eb..51bb647099 100644 --- a/templates/gdscript/addons/tests/test_query.gd.twig +++ b/templates/gdscript/addons/tests/test_query.gd.twig @@ -9,7 +9,6 @@ func test_query_equal(): assert_eq(parsed["attribute"], "age") assert_eq(parsed["values"][0], 10) - func test_query_value_wrapping(): var q = {{spec.title | caseUcfirst}}Query.equal("age", 5) var parsed = JSON.parse_string(q) @@ -17,7 +16,6 @@ func test_query_value_wrapping(): assert_true(parsed["values"] is Array) assert_eq(parsed["values"].size(), 1) - func test_query_is_null(): var q = {{spec.title | caseUcfirst}}Query.isNull("field") var parsed = JSON.parse_string(q) @@ -25,7 +23,6 @@ func test_query_is_null(): assert_eq(parsed["method"], "isNull") assert_false(parsed.has("values")) - func test_query_between(): var q = {{spec.title | caseUcfirst}}Query.between("age", 10, 20) var parsed = JSON.parse_string(q) @@ -34,7 +31,6 @@ func test_query_between(): assert_eq(parsed["values"][0], 10) assert_eq(parsed["values"][1], 20) - func test_query_and(): var q1 = {{spec.title | caseUcfirst}}Query.equal("age", 10) var q2 = {{spec.title | caseUcfirst}}Query.equal("name", "abc") @@ -45,7 +41,6 @@ func test_query_and(): assert_eq(parsed["method"], "and") assert_eq(parsed["values"].size(), 2) - func test_query_or(): var q1 = {{spec.title | caseUcfirst}}Query.equal("age", 10) var q2 = {{spec.title | caseUcfirst}}Query.isNull("email") @@ -56,7 +51,6 @@ func test_query_or(): assert_eq(parsed["method"], "or") assert_eq(parsed["values"].size(), 2) - func test_query_limit(): var q = {{spec.title | caseUcfirst}}Query.limit(10) var parsed = JSON.parse_string(q) @@ -64,7 +58,6 @@ func test_query_limit(): assert_eq(parsed["method"], "limit") assert_eq(parsed["values"][0], 10) - func test_query_offset(): var q = {{spec.title | caseUcfirst}}Query.offset(5) var parsed = JSON.parse_string(q) @@ -72,7 +65,6 @@ func test_query_offset(): assert_eq(parsed["method"], "offset") assert_eq(parsed["values"][0], 5) - func test_query_order_asc(): var q = {{spec.title | caseUcfirst}}Query.orderAsc("age") var parsed = JSON.parse_string(q) @@ -80,7 +72,6 @@ func test_query_order_asc(): assert_eq(parsed["method"], "orderAsc") assert_eq(parsed["attribute"], "age") - func test_query_order_desc(): var q = {{spec.title | caseUcfirst}}Query.orderDesc("age") var parsed = JSON.parse_string(q) @@ -88,7 +79,6 @@ func test_query_order_desc(): assert_eq(parsed["method"], "orderDesc") assert_eq(parsed["attribute"], "age") - func test_query_invalid_json_skipped(): var combined = {{spec.title | caseUcfirst}}Query.and_query(["INVALID"]) var parsed = JSON.parse_string(combined) diff --git a/templates/godot/addons/appwrite.gd.twig b/templates/godot/addons/appwrite.gd.twig index a866f85b26..cb487a48a9 100644 --- a/templates/godot/addons/appwrite.gd.twig +++ b/templates/godot/addons/appwrite.gd.twig @@ -45,7 +45,6 @@ func _ready() -> void: _apply_env(key, value) - {% for header in spec.global.headers %} {% if header.description %} ## {{header.description}} @@ -53,28 +52,23 @@ func _ready() -> void: func set_{{header.key | caseSnake}}(value: String) -> void: _client.set_{{header.key | caseSnake}}(value) - {% endfor %} ## Set self signed status func set_self_signed(status: bool = true) -> void: _client.set_self_signed(status) - ## Set the endpoint func set_endpoint(endpoint: String) -> void: _client.set_endpoint(endpoint) - ## Add a header func add_header(key: String, value: String) -> void: _client.add_header(key, value) - ## Get all headers func get_headers() -> Dictionary: return _client.get_headers() - func _apply_env(key: String, value: String) -> void: match key: "{{ prefix | caseUpper }}_ENDPOINT": @@ -85,7 +79,6 @@ func _apply_env(key: String, value: String) -> void: "{{ prefix | caseUpper }}_{{ header.key | caseUpper }}": _client.set_{{ header.key | caseSnake }}(value) {% endfor %} - ## Ping {{prefix}} Server for testing connection func ping() -> String: diff --git a/templates/godot/addons/client.gd.twig b/templates/godot/addons/client.gd.twig index b1c72f64bf..bd58562739 100644 --- a/templates/godot/addons/client.gd.twig +++ b/templates/godot/addons/client.gd.twig @@ -25,30 +25,23 @@ func set_self_signed(status: bool = true) -> RefCounted: _self_signed = status return self - func set_endpoint(endpoint: String) -> RefCounted: _endpoint = endpoint return self - func add_header(key: String, value: String) -> RefCounted: _global_headers[key.to_lower()] = value return self - func get_headers() -> Dictionary: return _global_headers.duplicate() - {% for header in spec.global.headers %} func set_{{header.key | caseSnake}}(value: String) -> RefCounted: _global_headers['{{header.name|lower}}'] = value return self - {% endfor %} - - func call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: if not _ensure_configured(): return {"statusCode": 400, "body": "Missing/wrong configuration. Please check logs for more info"} @@ -142,7 +135,6 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param else: return {"statusCode": response_code, "body": response_text} - func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: var body := PackedByteArray() for key in params: @@ -177,7 +169,6 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: body.append_array(("--" + boundary + "--\r\n").to_utf8_buffer()) return body - func _ensure_configured() -> bool: if _endpoint == "": push_warning("{{prefix}}: endpoint is missing. Use set_endpoint or .env.") @@ -187,7 +178,6 @@ func _ensure_configured() -> bool: return false return true - func _call_chunked(method: String, path: String, headers: Dictionary, params: Dictionary, file_key: String) -> Dictionary: var file: {{prefix}}InputFile = params[file_key] var size = file.get_size() diff --git a/templates/godot/addons/enums/enum.gd.twig b/templates/godot/addons/enums/enum.gd.twig index 040e0bc00a..e39c1ed42e 100644 --- a/templates/godot/addons/enums/enum.gd.twig +++ b/templates/godot/addons/enums/enum.gd.twig @@ -9,7 +9,6 @@ const {{ key | caseEnumKey }} = "{{ value }}" static func is_valid(value: String) -> bool: return value in values() - ## Get all values of enum static func values() -> Array[String]: return [ diff --git a/templates/godot/addons/id.gd.twig b/templates/godot/addons/id.gd.twig index 9adef4794d..c7f6195173 100644 --- a/templates/godot/addons/id.gd.twig +++ b/templates/godot/addons/id.gd.twig @@ -8,7 +8,6 @@ static func _hex_timestamp() -> String: var usec_hex := "%05x" % usec return sec_hex + usec_hex - ## Generate a unique ID with padding to have a longer ID static func unique(padding: int = 7) -> String: var id := _hex_timestamp() @@ -19,6 +18,5 @@ static func unique(padding: int = 7) -> String: id += sb return id - static func custom(id: String) -> String: return id diff --git a/templates/godot/addons/input_file.gd.twig b/templates/godot/addons/input_file.gd.twig index 84bec78b33..ffa6c47e68 100644 --- a/templates/godot/addons/input_file.gd.twig +++ b/templates/godot/addons/input_file.gd.twig @@ -16,21 +16,17 @@ func _init(file_path: String = "", bytes_data: PackedByteArray = PackedByteArray self.content_type = _guess_mime_type(self.path.get_file()) - ## Creates a new {{prefix}}InputFile from a file path static func from_path(file_path: String, custom_filename: String = "") -> {{prefix}}InputFile: return {{prefix}}InputFile.new(file_path, PackedByteArray(), custom_filename) - ## Creates a new {{prefix}}InputFile from bytes static func from_bytes(bytes_data: PackedByteArray, custom_filename: String = "") -> {{prefix}}InputFile: return {{prefix}}InputFile.new("", bytes_data, custom_filename) - static func from_text(text: String, custom_filename: String = "") -> {{prefix}}InputFile: return from_bytes(text.to_utf8_buffer(), custom_filename) - ## Return the PackedByteArray of the file func get_data() -> PackedByteArray: if not bytes.is_empty(): @@ -39,7 +35,6 @@ func get_data() -> PackedByteArray: return FileAccess.get_file_as_bytes(path) return PackedByteArray() - ## Return the size of the file func get_size() -> int: if not bytes.is_empty(): @@ -50,7 +45,6 @@ func get_size() -> int: return file.get_length() return 0 - ## Return a chunk of the file func get_chunk(offset: int, length: int) -> PackedByteArray: if not bytes.is_empty(): @@ -62,7 +56,6 @@ func get_chunk(offset: int, length: int) -> PackedByteArray: return file.get_buffer(length) return PackedByteArray() - static func _guess_mime_type(filename: String) -> String: var ext := filename.get_extension().to_lower() diff --git a/templates/godot/addons/models/model.gd.twig b/templates/godot/addons/models/model.gd.twig index 331896defc..759129967a 100644 --- a/templates/godot/addons/models/model.gd.twig +++ b/templates/godot/addons/models/model.gd.twig @@ -77,12 +77,9 @@ static func from_dict(dict: Dictionary): continue {% endif %} {% endfor %} - m.set(key, value) - return m - ## Convert to Dictionary func to_dict() -> Dictionary: var dict := {} @@ -107,7 +104,5 @@ func to_dict() -> Dictionary: continue {% endif %} {% endfor %} - dict[_FIELD_MAP[key]] = value - return dict \ No newline at end of file diff --git a/templates/godot/addons/operator.gd.twig b/templates/godot/addons/operator.gd.twig index 85534b7fa3..c293d90812 100644 --- a/templates/godot/addons/operator.gd.twig +++ b/templates/godot/addons/operator.gd.twig @@ -11,11 +11,9 @@ const CONTAINS := "contains" const IS_NULL := "isNull" const IS_NOT_NULL := "isNotNull" - var method: String var values: Array - ## Validate condition value static func is_valid(value: String) -> bool: return value in [ @@ -30,7 +28,6 @@ static func is_valid(value: String) -> bool: IS_NOT_NULL ] - ## Constructor func _init(_method: String, _values: Variant = null) -> void: method = _method @@ -43,7 +40,6 @@ func _init(_method: String, _values: Variant = null) -> void: else: values = [] - ## Convert operator object to JSON string func _to_string() -> String: return JSON.stringify({ @@ -51,7 +47,6 @@ func _to_string() -> String: "values": values }) - static func increment(value: float = 1, max: Variant = null) -> String: if not is_finite(value): push_error("Value cannot be NaN or Infinity") @@ -67,7 +62,6 @@ static func increment(value: float = 1, max: Variant = null) -> String: return AppwriteOperator.new("increment", vals).to_string() - static func decrement(value: float = 1, min: Variant = null) -> String: if not is_finite(value): push_error("Value cannot be NaN or Infinity") @@ -83,7 +77,6 @@ static func decrement(value: float = 1, min: Variant = null) -> String: return AppwriteOperator.new("decrement", vals).to_string() - static func multiply(factor: float, max: Variant = null) -> String: if not is_finite(factor): push_error("Factor cannot be NaN or Infinity") @@ -99,7 +92,6 @@ static func multiply(factor: float, max: Variant = null) -> String: return AppwriteOperator.new("multiply", vals).to_string() - static func divide(divisor: float, min: Variant = null) -> String: if not is_finite(divisor): push_error("Divisor cannot be NaN or Infinity") @@ -119,7 +111,6 @@ static func divide(divisor: float, min: Variant = null) -> String: return AppwriteOperator.new("divide", vals).to_string() - static func modulo(divisor: float) -> String: if not is_finite(divisor): push_error("Divisor cannot be NaN or Infinity") @@ -131,7 +122,6 @@ static func modulo(divisor: float) -> String: return AppwriteOperator.new("modulo", [divisor]).to_string() - static func power(exponent: float, max: Variant = null) -> String: if not is_finite(exponent): push_error("Exponent cannot be NaN or Infinity") @@ -147,35 +137,27 @@ static func power(exponent: float, max: Variant = null) -> String: return AppwriteOperator.new("power", vals).to_string() - static func array_append(values: Array) -> String: return AppwriteOperator.new("arrayAppend", values).to_string() - static func array_prepend(values: Array) -> String: return AppwriteOperator.new("arrayPrepend", values).to_string() - static func array_insert(index: int, value: Variant) -> String: return AppwriteOperator.new("arrayInsert", [index, value]).to_string() - static func array_remove(value: Variant) -> String: return AppwriteOperator.new("arrayRemove", [value]).to_string() - static func array_unique() -> String: return AppwriteOperator.new("arrayUnique", []).to_string() - static func array_intersect(values: Array) -> String: return AppwriteOperator.new("arrayIntersect", values).to_string() - static func array_diff(values: Array) -> String: return AppwriteOperator.new("arrayDiff", values).to_string() - static func array_filter(condition: String, value: Variant = null) -> String: if not is_valid(condition): push_error("Invalid condition: %s" % condition) @@ -183,26 +165,20 @@ static func array_filter(condition: String, value: Variant = null) -> String: return AppwriteOperator.new("arrayFilter", [condition, value]).to_string() - static func string_concat(value: Variant) -> String: return AppwriteOperator.new("stringConcat", [value]).to_string() - static func string_replace(search: String, replace: String) -> String: return AppwriteOperator.new("stringReplace", [search, replace]).to_string() - static func toggle() -> String: return AppwriteOperator.new("toggle", []).to_string() - static func date_add_days(days: int) -> String: return AppwriteOperator.new("dateAddDays", [days]).to_string() - static func date_sub_days(days: int) -> String: return AppwriteOperator.new("dateSubDays", [days]).to_string() - static func date_set_now() -> String: return AppwriteOperator.new("dateSetNow", []).to_string() \ No newline at end of file diff --git a/templates/godot/addons/plugin.gd.twig b/templates/godot/addons/plugin.gd.twig index 0f8b33bf97..60e4561384 100644 --- a/templates/godot/addons/plugin.gd.twig +++ b/templates/godot/addons/plugin.gd.twig @@ -7,7 +7,6 @@ const AUTOLOAD_PATH : String = "res://addons/{{spec.title | caseSnake}}/{{spec.t func _enable_plugin() -> void: add_autoload_singleton(AUTOLOAD_NAME, AUTOLOAD_PATH) - func _disable_plugin() -> void: remove_autoload_singleton(AUTOLOAD_NAME) diff --git a/templates/godot/addons/role.gd.twig b/templates/godot/addons/role.gd.twig index d73b2e3829..0658cafb49 100644 --- a/templates/godot/addons/role.gd.twig +++ b/templates/godot/addons/role.gd.twig @@ -1,30 +1,25 @@ class_name {{spec.title | caseUcfirst}}Role - ## Any role static func any() -> String: return 'any' - ## User role static func user(id: String, status: String = '') -> String: if status == '': return 'user:%s' % id return 'user:%s/%s' % [id, status] - ## Users role static func users(status: String = '') -> String: if status == '': return 'users' return 'users/%s' % status - ## Guests role static func guests() -> String: return 'guests' - ## Team role static func team(id: String, role: String = '') -> String: if role == '': @@ -35,6 +30,5 @@ static func team(id: String, role: String = '') -> String: static func member(id: String) -> String: return 'member:%s' % id - static func label(name: String) -> String: return 'label:%s' % name diff --git a/templates/godot/addons/service.gd.twig b/templates/godot/addons/service.gd.twig index 76b6839cb9..16ac7f205c 100644 --- a/templates/godot/addons/service.gd.twig +++ b/templates/godot/addons/service.gd.twig @@ -3,7 +3,6 @@ extends RefCounted - var client: RefCounted func _init(p_client: RefCounted) -> void: diff --git a/templates/godot/addons/tests/test_operator.gd.twig b/templates/godot/addons/tests/test_operator.gd.twig index 3032b2ffe9..6e64afc3b7 100644 --- a/templates/godot/addons/tests/test_operator.gd.twig +++ b/templates/godot/addons/tests/test_operator.gd.twig @@ -8,7 +8,6 @@ func test_operator_to_string(): assert_eq(parsed["method"], "test") assert_eq(parsed["values"].size(), 2) - func test_operator_value_wrapping(): var op = {{prefix}}Operator.new("test", 5) var parsed = JSON.parse_string(op.to_string()) @@ -16,20 +15,17 @@ func test_operator_value_wrapping(): assert_true(parsed["values"] is Array) assert_eq(parsed["values"][0], 5) - func test_operator_null_values(): var op = {{prefix}}Operator.new("test", null) var parsed = JSON.parse_string(op.to_string()) assert_eq(parsed["values"].size(), 0) - func test_is_valid_condition(): assert_true({{prefix}}Operator.is_valid({{prefix}}Operator.EQUAL)) assert_true({{prefix}}Operator.is_valid({{prefix}}Operator.NOT_EQUAL)) assert_false({{prefix}}Operator.is_valid("invalid")) - func test_increment_basic(): var q = {{prefix}}Operator.increment(5) var parsed = JSON.parse_string(q) @@ -37,7 +33,6 @@ func test_increment_basic(): assert_eq(parsed["method"], "increment") assert_eq(parsed["values"][0], 5) - func test_increment_with_max(): var q = {{prefix}}Operator.increment(5, 10) var parsed = JSON.parse_string(q) @@ -45,20 +40,16 @@ func test_increment_with_max(): assert_eq(parsed["values"].size(), 2) assert_eq(parsed["values"][1], 10) - func test_increment_invalid_number(): var q = {{prefix}}Operator.increment(INF) assert_eq(q, "") # should fail gracefully assert_push_error("Value cannot be NaN or Infinity") - - func test_divide_by_zero(): var q = {{prefix}}Operator.divide(0) assert_eq(q, "") assert_push_error("Divisor cannot be zero") - func test_divide_valid(): var q = {{prefix}}Operator.divide(10) var parsed = JSON.parse_string(q) @@ -66,7 +57,6 @@ func test_divide_valid(): assert_eq(parsed["method"], "divide") assert_eq(parsed["values"][0], 10) - func test_array_append(): var q = {{prefix}}Operator.array_append([1, 2]) var parsed = JSON.parse_string(q) @@ -74,7 +64,6 @@ func test_array_append(): assert_eq(parsed["method"], "arrayAppend") assert_eq(parsed["values"].size(), 2) - func test_array_insert(): var q = {{prefix}}Operator.array_insert(1, "x") var parsed = JSON.parse_string(q) @@ -82,7 +71,6 @@ func test_array_insert(): assert_eq(parsed["values"][0], 1) assert_eq(parsed["values"][1], "x") - func test_array_unique(): var q = {{prefix}}Operator.array_unique() var parsed = JSON.parse_string(q) @@ -90,7 +78,6 @@ func test_array_unique(): assert_eq(parsed["method"], "arrayUnique") assert_eq(parsed["values"].size(), 0) - func test_array_filter_valid(): var q = {{prefix}}Operator.array_filter({{prefix}}Operator.EQUAL, 10) var parsed = JSON.parse_string(q) @@ -98,13 +85,11 @@ func test_array_filter_valid(): assert_eq(parsed["method"], "arrayFilter") assert_eq(parsed["values"][0], "equal") - func test_array_filter_invalid_condition(): var q = {{prefix}}Operator.array_filter("INVALID", 10) assert_eq(q, "") assert_push_error("Invalid condition: INVALID") - func test_string_concat(): var q = {{prefix}}Operator.string_concat("abc") var parsed = JSON.parse_string(q) @@ -112,7 +97,6 @@ func test_string_concat(): assert_eq(parsed["method"], "stringConcat") assert_eq(parsed["values"][0], "abc") - func test_string_replace(): var q = {{prefix}}Operator.string_replace("a", "b") var parsed = JSON.parse_string(q) @@ -120,7 +104,6 @@ func test_string_replace(): assert_eq(parsed["values"][0], "a") assert_eq(parsed["values"][1], "b") - func test_toggle(): var q = {{prefix}}Operator.toggle() var parsed = JSON.parse_string(q) @@ -128,7 +111,6 @@ func test_toggle(): assert_eq(parsed["method"], "toggle") assert_eq(parsed["values"].size(), 0) - func test_date_add_days(): var q = {{prefix}}Operator.date_add_days(5) var parsed = JSON.parse_string(q) @@ -136,7 +118,6 @@ func test_date_add_days(): assert_eq(parsed["method"], "dateAddDays") assert_eq(parsed["values"][0], 5) - func test_date_set_now(): var q = {{prefix}}Operator.date_set_now() var parsed = JSON.parse_string(q) diff --git a/templates/godot/addons/tests/test_query.gd.twig b/templates/godot/addons/tests/test_query.gd.twig index bf8946d3eb..51bb647099 100644 --- a/templates/godot/addons/tests/test_query.gd.twig +++ b/templates/godot/addons/tests/test_query.gd.twig @@ -9,7 +9,6 @@ func test_query_equal(): assert_eq(parsed["attribute"], "age") assert_eq(parsed["values"][0], 10) - func test_query_value_wrapping(): var q = {{spec.title | caseUcfirst}}Query.equal("age", 5) var parsed = JSON.parse_string(q) @@ -17,7 +16,6 @@ func test_query_value_wrapping(): assert_true(parsed["values"] is Array) assert_eq(parsed["values"].size(), 1) - func test_query_is_null(): var q = {{spec.title | caseUcfirst}}Query.isNull("field") var parsed = JSON.parse_string(q) @@ -25,7 +23,6 @@ func test_query_is_null(): assert_eq(parsed["method"], "isNull") assert_false(parsed.has("values")) - func test_query_between(): var q = {{spec.title | caseUcfirst}}Query.between("age", 10, 20) var parsed = JSON.parse_string(q) @@ -34,7 +31,6 @@ func test_query_between(): assert_eq(parsed["values"][0], 10) assert_eq(parsed["values"][1], 20) - func test_query_and(): var q1 = {{spec.title | caseUcfirst}}Query.equal("age", 10) var q2 = {{spec.title | caseUcfirst}}Query.equal("name", "abc") @@ -45,7 +41,6 @@ func test_query_and(): assert_eq(parsed["method"], "and") assert_eq(parsed["values"].size(), 2) - func test_query_or(): var q1 = {{spec.title | caseUcfirst}}Query.equal("age", 10) var q2 = {{spec.title | caseUcfirst}}Query.isNull("email") @@ -56,7 +51,6 @@ func test_query_or(): assert_eq(parsed["method"], "or") assert_eq(parsed["values"].size(), 2) - func test_query_limit(): var q = {{spec.title | caseUcfirst}}Query.limit(10) var parsed = JSON.parse_string(q) @@ -64,7 +58,6 @@ func test_query_limit(): assert_eq(parsed["method"], "limit") assert_eq(parsed["values"][0], 10) - func test_query_offset(): var q = {{spec.title | caseUcfirst}}Query.offset(5) var parsed = JSON.parse_string(q) @@ -72,7 +65,6 @@ func test_query_offset(): assert_eq(parsed["method"], "offset") assert_eq(parsed["values"][0], 5) - func test_query_order_asc(): var q = {{spec.title | caseUcfirst}}Query.orderAsc("age") var parsed = JSON.parse_string(q) @@ -80,7 +72,6 @@ func test_query_order_asc(): assert_eq(parsed["method"], "orderAsc") assert_eq(parsed["attribute"], "age") - func test_query_order_desc(): var q = {{spec.title | caseUcfirst}}Query.orderDesc("age") var parsed = JSON.parse_string(q) @@ -88,7 +79,6 @@ func test_query_order_desc(): assert_eq(parsed["method"], "orderDesc") assert_eq(parsed["attribute"], "age") - func test_query_invalid_json_skipped(): var combined = {{spec.title | caseUcfirst}}Query.and_query(["INVALID"]) var parsed = JSON.parse_string(combined) From 9d9f8e14acd61c5c3d38e0a8072e62c5c9ceba9f Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Thu, 14 May 2026 12:26:08 +0530 Subject: [PATCH 27/58] feat(model): added request models --- composer.lock | 74 ++++++------ src/SDK/Language/GDScript.php | 7 +- src/SDK/Language/Godot.php | 5 + .../addons/models/request_model.gd.twig | 108 ++++++++++++++++++ .../godot/addons/models/request_model.gd.twig | 108 ++++++++++++++++++ 5 files changed, 266 insertions(+), 36 deletions(-) create mode 100644 templates/gdscript/addons/models/request_model.gd.twig create mode 100644 templates/godot/addons/models/request_model.gd.twig diff --git a/composer.lock b/composer.lock index e6c4c33e33..75e4b3892c 100644 --- a/composer.lock +++ b/composer.lock @@ -131,16 +131,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -153,7 +153,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -178,7 +178,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -189,12 +189,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "symfony/polyfill-ctype", @@ -2604,16 +2608,16 @@ }, { "name": "symfony/console", - "version": "v8.0.8", + "version": "v8.0.11", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7" + "reference": "3156577f46a38aa1b9323aad223de7a9cd426782" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5b66d385dc58f69652e56f78a4184615e3f2b7f7", - "reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7", + "url": "https://api.github.com/repos/symfony/console/zipball/3156577f46a38aa1b9323aad223de7a9cd426782", + "reference": "3156577f46a38aa1b9323aad223de7a9cd426782", "shasum": "" }, "require": { @@ -2670,7 +2674,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.8" + "source": "https://github.com/symfony/console/tree/v8.0.11" }, "funding": [ { @@ -2690,7 +2694,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-05-13T12:07:53+00:00" }, { "name": "symfony/polyfill-intl-grapheme", @@ -2861,16 +2865,16 @@ }, { "name": "symfony/process", - "version": "v8.0.8", + "version": "v8.0.11", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc" + "reference": "26d89e459f037d2873300605d0a07e7a8ef84db0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc", - "reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc", + "url": "https://api.github.com/repos/symfony/process/zipball/26d89e459f037d2873300605d0a07e7a8ef84db0", + "reference": "26d89e459f037d2873300605d0a07e7a8ef84db0", "shasum": "" }, "require": { @@ -2902,7 +2906,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v8.0.8" + "source": "https://github.com/symfony/process/tree/v8.0.11" }, "funding": [ { @@ -2922,20 +2926,20 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-05-11T16:56:32+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { @@ -2953,7 +2957,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -2989,7 +2993,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { @@ -3009,20 +3013,20 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:30:57+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { "name": "symfony/string", - "version": "v8.0.8", + "version": "v8.0.11", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ae9488f874d7603f9d2dfbf120203882b645d963" + "reference": "39be2ad058a3c0bd558edca23e65f009865d75ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963", - "reference": "ae9488f874d7603f9d2dfbf120203882b645d963", + "url": "https://api.github.com/repos/symfony/string/zipball/39be2ad058a3c0bd558edca23e65f009865d75ff", + "reference": "39be2ad058a3c0bd558edca23e65f009865d75ff", "shasum": "" }, "require": { @@ -3079,7 +3083,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.8" + "source": "https://github.com/symfony/string/tree/v8.0.11" }, "funding": [ { @@ -3099,7 +3103,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-05-13T12:07:53+00:00" }, { "name": "theseer/tokenizer", @@ -3154,7 +3158,7 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -3163,6 +3167,6 @@ "ext-mbstring": "*", "ext-json": "*" }, - "platform-dev": [], - "plugin-api-version": "2.2.0" + "platform-dev": {}, + "plugin-api-version": "2.9.0" } diff --git a/src/SDK/Language/GDScript.php b/src/SDK/Language/GDScript.php index 912b9ef3bc..b5503417f6 100644 --- a/src/SDK/Language/GDScript.php +++ b/src/SDK/Language/GDScript.php @@ -306,6 +306,11 @@ public function getFiles(): array 'destination' => 'addons/{{ spec.title | caseSnake }}/models/{{ definition.name | caseSnake }}.gd', 'template' => 'gdscript/addons/models/model.gd.twig', ], + [ + 'scope' => 'requestModel', + 'destination' => 'addons/{{ spec.title | caseSnake }}/models/{{ requestModel.name | caseSnake }}.gd', + 'template' => 'gdscript/addons/models/request_model.gd.twig', + ], [ 'scope' => 'method', 'destination' => 'docs/examples/{{service.name | caseSnake}}/{{method.name | caseSnake}}.md', @@ -453,7 +458,7 @@ public function getParamDefault(array $param): string * @param array $spec * @return string */ - public function getParamExample(array $param, string $lang = '', array $spec = []): string + public function getParamExample(array $param, string $lang = '', array $spec = []): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/Godot.php b/src/SDK/Language/Godot.php index 8eba10b31f..1500ec9ed9 100644 --- a/src/SDK/Language/Godot.php +++ b/src/SDK/Language/Godot.php @@ -165,6 +165,11 @@ public function getFiles(): array 'destination' => 'addons/{{ spec.title | caseSnake }}/models/{{ definition.name | caseSnake }}.gd', 'template' => 'godot/addons/models/model.gd.twig', ], + [ + 'scope' => 'requestModel', + 'destination' => 'addons/{{ spec.title | caseSnake }}/models/{{ requestModel.name | caseSnake }}.gd', + 'template' => 'gdscript/addons/models/request_model.gd.twig', + ], [ 'scope' => 'method', 'destination' => 'docs/examples/{{service.name | caseSnake}}/{{method.name | caseSnake}}.md', diff --git a/templates/gdscript/addons/models/request_model.gd.twig b/templates/gdscript/addons/models/request_model.gd.twig new file mode 100644 index 0000000000..44ecc6041a --- /dev/null +++ b/templates/gdscript/addons/models/request_model.gd.twig @@ -0,0 +1,108 @@ +{% set prefix = spec.title | caseUcfirst %} +class_name {{ prefix }}{{ requestModel.name | caseUcfirst }} +extends RefCounted +## {{ requestModel.description | default("Request model class.") | replace({"\n": "\n## "}) }}[br] + +{% set imports = [] %} +{%~ for property in requestModel.properties %} +{%~ set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (requestModel.name ~ (property.name | caseUcfirst)) %} +{%~ if property.enum or property.items.enum is defined %} +{%~ if enumName not in imports %} +const _{{ enumName | caseUcfirst }} := preload("res://addons/{{ spec.title | caseSnake }}/enums/{{ enumName | caseSnake }}.gd") +{% set imports = imports|merge([enumName]) %} +{%~ endif %} +{%~ endif %} +{%~ endfor %} + +const _FIELD_MAP := { +{% for property in requestModel.properties %} + "{{ property.name | uniqueSnake }}": "{{ property.name }}", +{% endfor %} +} + +{% for property in requestModel.properties %} +{% set baseType = property | typeName(spec) %} +var {{ property.name | uniqueSnake }}: {{ baseType }} ## {{ property.description | default("No description provided.") }} +{% endfor %} + +## Convert dictionary to model +static func from_dict(dict: Dictionary): + var m := {{ prefix }}{{ requestModel.name | caseUcfirst }}.new() + + for key in _FIELD_MAP: + var raw_key = _FIELD_MAP[key] + var value = dict.get(raw_key) + +{% for property in requestModel.properties %} +{% set field = property.name | uniqueSnake %} +{% set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (requestModel.name ~ (property.name | caseUcfirst)) %} +{% if property.model %} + if key == "{{ field }}" and value is Dictionary: + m.set(key, {{ prefix }}{{ property.model | caseUcfirst }}.from_dict(value)) + continue +{% endif %} +{% if property.array.model %} + if key == "{{ field }}" and value is Array: + var list := [] + for item in value: + if item is Dictionary: + list.append({{ prefix }}{{ property.array.model | caseUcfirst }}.from_dict(item)) + else: + list.append(item) + m.set(key, list) + continue +{% endif %} +{% if property.enum %} + if key == "{{ field }}" and value != null: + if not _{{ enumName | caseUcfirst }}.is_valid(value): + push_error("Invalid enum value for {{ field }}: %s" % value) + return {{prefix}}Exception.new("Invalid enum value for {{ field }}: %s" % value, 0, "invalid_enum_value", str(dict)) + m.set(key, value) + continue +{% endif %} +{% if property.type == 'array' and property.items.enum is defined %} + if key == "{{ field }}" and value is Array: + var list := [] + for item in value: + if not _{{ enumName | caseUcfirst }}.is_valid(item): + push_error("Invalid enum value: %s" % item) + return {{prefix}}Exception.new("Invalid enum value for {{ field }}: %s" % value, 0, "invalid_enum_value", str(dict)) + list.append(item) + m.set(key, list) + continue +{% endif %} +{% if property.type == 'array' and not property.array.model %} + if key == "{{ field }}" and value is Array: + m.set(key, value) + continue +{% endif %} +{% endfor %} + m.set(key, value) + return m + +## Convert to Dictionary +func to_dict() -> Dictionary: + var dict := {} + + for key in _FIELD_MAP: + var value = get(key) + +{% for property in requestModel.properties %} +{% set field = property.name | uniqueSnake %} +{% if property.model %} + if key == "{{ field }}" and value != null: + dict[_FIELD_MAP[key]] = value.to_dict() + continue +{% endif %} +{% if property.array.model %} + if key == "{{ field }}" and value is Array: + var list := [] + for item in value: + if item != null: + list.append(item.to_dict()) + dict[_FIELD_MAP[key]] = list + continue +{% endif %} +{% endfor %} + dict[_FIELD_MAP[key]] = value + return dict \ No newline at end of file diff --git a/templates/godot/addons/models/request_model.gd.twig b/templates/godot/addons/models/request_model.gd.twig new file mode 100644 index 0000000000..44ecc6041a --- /dev/null +++ b/templates/godot/addons/models/request_model.gd.twig @@ -0,0 +1,108 @@ +{% set prefix = spec.title | caseUcfirst %} +class_name {{ prefix }}{{ requestModel.name | caseUcfirst }} +extends RefCounted +## {{ requestModel.description | default("Request model class.") | replace({"\n": "\n## "}) }}[br] + +{% set imports = [] %} +{%~ for property in requestModel.properties %} +{%~ set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (requestModel.name ~ (property.name | caseUcfirst)) %} +{%~ if property.enum or property.items.enum is defined %} +{%~ if enumName not in imports %} +const _{{ enumName | caseUcfirst }} := preload("res://addons/{{ spec.title | caseSnake }}/enums/{{ enumName | caseSnake }}.gd") +{% set imports = imports|merge([enumName]) %} +{%~ endif %} +{%~ endif %} +{%~ endfor %} + +const _FIELD_MAP := { +{% for property in requestModel.properties %} + "{{ property.name | uniqueSnake }}": "{{ property.name }}", +{% endfor %} +} + +{% for property in requestModel.properties %} +{% set baseType = property | typeName(spec) %} +var {{ property.name | uniqueSnake }}: {{ baseType }} ## {{ property.description | default("No description provided.") }} +{% endfor %} + +## Convert dictionary to model +static func from_dict(dict: Dictionary): + var m := {{ prefix }}{{ requestModel.name | caseUcfirst }}.new() + + for key in _FIELD_MAP: + var raw_key = _FIELD_MAP[key] + var value = dict.get(raw_key) + +{% for property in requestModel.properties %} +{% set field = property.name | uniqueSnake %} +{% set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (requestModel.name ~ (property.name | caseUcfirst)) %} +{% if property.model %} + if key == "{{ field }}" and value is Dictionary: + m.set(key, {{ prefix }}{{ property.model | caseUcfirst }}.from_dict(value)) + continue +{% endif %} +{% if property.array.model %} + if key == "{{ field }}" and value is Array: + var list := [] + for item in value: + if item is Dictionary: + list.append({{ prefix }}{{ property.array.model | caseUcfirst }}.from_dict(item)) + else: + list.append(item) + m.set(key, list) + continue +{% endif %} +{% if property.enum %} + if key == "{{ field }}" and value != null: + if not _{{ enumName | caseUcfirst }}.is_valid(value): + push_error("Invalid enum value for {{ field }}: %s" % value) + return {{prefix}}Exception.new("Invalid enum value for {{ field }}: %s" % value, 0, "invalid_enum_value", str(dict)) + m.set(key, value) + continue +{% endif %} +{% if property.type == 'array' and property.items.enum is defined %} + if key == "{{ field }}" and value is Array: + var list := [] + for item in value: + if not _{{ enumName | caseUcfirst }}.is_valid(item): + push_error("Invalid enum value: %s" % item) + return {{prefix}}Exception.new("Invalid enum value for {{ field }}: %s" % value, 0, "invalid_enum_value", str(dict)) + list.append(item) + m.set(key, list) + continue +{% endif %} +{% if property.type == 'array' and not property.array.model %} + if key == "{{ field }}" and value is Array: + m.set(key, value) + continue +{% endif %} +{% endfor %} + m.set(key, value) + return m + +## Convert to Dictionary +func to_dict() -> Dictionary: + var dict := {} + + for key in _FIELD_MAP: + var value = get(key) + +{% for property in requestModel.properties %} +{% set field = property.name | uniqueSnake %} +{% if property.model %} + if key == "{{ field }}" and value != null: + dict[_FIELD_MAP[key]] = value.to_dict() + continue +{% endif %} +{% if property.array.model %} + if key == "{{ field }}" and value is Array: + var list := [] + for item in value: + if item != null: + list.append(item.to_dict()) + dict[_FIELD_MAP[key]] = list + continue +{% endif %} +{% endfor %} + dict[_FIELD_MAP[key]] = value + return dict \ No newline at end of file From f93b17655ae5d4f4a5093729608fe30b8178d636 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Sat, 16 May 2026 21:23:46 +0530 Subject: [PATCH 28/58] test: add testing pipeline for Godot 4 - add test.gd for local godot testing only - skeleton for gdscript test pipeline - fix naming convension for methods in query.gd - Working on serialization for custom typed class --- templates/gdscript/addons/query.gd.twig | 82 +++---- templates/godot/addons/query.gd.twig | 82 +++---- tests/GDScript4Test.php | 36 +++ tests/Godot4Test.php | 35 +++ tests/languages/gdscript/test.gd | 288 ++++++++++++++++++++++++ tests/languages/godot/test.gd | 285 +++++++++++++++++++++++ 6 files changed, 726 insertions(+), 82 deletions(-) create mode 100644 tests/GDScript4Test.php create mode 100644 tests/Godot4Test.php create mode 100644 tests/languages/gdscript/test.gd create mode 100644 tests/languages/godot/test.gd diff --git a/templates/gdscript/addons/query.gd.twig b/templates/gdscript/addons/query.gd.twig index d50a9133f2..663a425efc 100644 --- a/templates/gdscript/addons/query.gd.twig +++ b/templates/gdscript/addons/query.gd.twig @@ -28,88 +28,88 @@ func _to_string() -> String: static func equal(attribute: String, value: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("equal", attribute, value)._to_string() -static func notEqual(attribute: String, value: Variant) -> String: +static func not_equal(attribute: String, value: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("notEqual", attribute, value)._to_string() static func regex(attribute: String, pattern: String) -> String: return {{spec.title | caseUcfirst}}Query.new("regex", attribute, pattern)._to_string() -static func lessThan(attribute: String, value: Variant) -> String: +static func less_than(attribute: String, value: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("lessThan", attribute, value)._to_string() -static func lessThanEqual(attribute: String, value: Variant) -> String: +static func less_than_equal(attribute: String, value: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("lessThanEqual", attribute, value)._to_string() -static func greaterThan(attribute: String, value: Variant) -> String: +static func greater_than(attribute: String, value: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("greaterThan", attribute, value)._to_string() -static func greaterThanEqual(attribute: String, value: Variant) -> String: +static func greater_than_equal(attribute: String, value: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("greaterThanEqual", attribute, value)._to_string() static func search(attribute: String, value: String) -> String: return {{spec.title | caseUcfirst}}Query.new("search", attribute, value)._to_string() -static func isNull(attribute: String) -> String: +static func is_null(attribute: String) -> String: return {{spec.title | caseUcfirst}}Query.new("isNull", attribute)._to_string() -static func isNotNull(attribute: String) -> String: +static func is_not_null(attribute: String) -> String: return {{spec.title | caseUcfirst}}Query.new("isNotNull", attribute)._to_string() static func exists(attributes: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("exists", null, attributes)._to_string() -static func notExists(attributes: Array) -> String: +static func not_exists(attributes: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("notExists", null, attributes)._to_string() static func between(attribute: String, start: Variant, end: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("between", attribute, [start, end])._to_string() -static func startsWith(attribute: String, value: String) -> String: +static func starts_with(attribute: String, value: String) -> String: return {{spec.title | caseUcfirst}}Query.new("startsWith", attribute, value)._to_string() -static func endsWith(attribute: String, value: String) -> String: +static func ends_with(attribute: String, value: String) -> String: return {{spec.title | caseUcfirst}}Query.new("endsWith", attribute, value)._to_string() static func contains(attribute: String, value: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("contains", attribute, value)._to_string() -static func containsAny(attribute: String, value: Array) -> String: +static func contains_any(attribute: String, value: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("containsAny", attribute, value)._to_string() -static func containsAll(attribute: String, value: Array) -> String: +static func contains_all(attribute: String, value: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("containsAll", attribute, value)._to_string() -static func notContains(attribute: String, value: Variant) -> String: +static func not_contains(attribute: String, value: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("notContains", attribute, value)._to_string() -static func notSearch(attribute: String, value: String) -> String: +static func not_search(attribute: String, value: String) -> String: return {{spec.title | caseUcfirst}}Query.new("notSearch", attribute, value)._to_string() -static func notBetween(attribute: String, start: Variant, end: Variant) -> String: +static func not_between(attribute: String, start: Variant, end: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("notBetween", attribute, [start, end])._to_string() -static func notStartsWith(attribute: String, value: String) -> String: +static func not_starts_with(attribute: String, value: String) -> String: return {{spec.title | caseUcfirst}}Query.new("notStartsWith", attribute, value)._to_string() -static func notEndsWith(attribute: String, value: String) -> String: +static func not_ends_with(attribute: String, value: String) -> String: return {{spec.title | caseUcfirst}}Query.new("notEndsWith", attribute, value)._to_string() -static func createdBefore(value: String) -> String: - return lessThan("$createdAt", value) +static func created_before(value: String) -> String: + return less_than("$createdAt", value) -static func createdAfter(value: String) -> String: - return greaterThan("$createdAt", value) +static func created_after(value: String) -> String: + return greater_than("$createdAt", value) -static func createdBetween(start: String, end: String) -> String: +static func created_between(start: String, end: String) -> String: return between("$createdAt", start, end) -static func updatedBefore(value: String) -> String: - return lessThan("$updatedAt", value) +static func updated_before(value: String) -> String: + return less_than("$updatedAt", value) -static func updatedAfter(value: String) -> String: - return greaterThan("$updatedAt", value) +static func updated_after(value: String) -> String: + return greater_than("$updatedAt", value) -static func updatedBetween(start: String, end: String) -> String: +static func updated_between(start: String, end: String) -> String: return between("$updatedAt", start, end) static func or_query(queries: Array) -> String: @@ -128,7 +128,7 @@ static func and_query(queries: Array) -> String: parsed_queries.append(parsed) return {{spec.title | caseUcfirst}}Query.new("and", null, parsed_queries)._to_string() -static func elemMatch(attribute: String, queries: Array) -> String: +static func elem_match(attribute: String, queries: Array) -> String: var parsed_queries := [] for q in queries: var parsed = JSON.parse_string(q) @@ -139,19 +139,19 @@ static func elemMatch(attribute: String, queries: Array) -> String: static func select(attributes: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("select", null, attributes)._to_string() -static func orderAsc(attribute: String) -> String: +static func order_asc(attribute: String) -> String: return {{spec.title | caseUcfirst}}Query.new("orderAsc", attribute)._to_string() -static func orderDesc(attribute: String) -> String: +static func order_desc(attribute: String) -> String: return {{spec.title | caseUcfirst}}Query.new("orderDesc", attribute)._to_string() -static func orderRandom() -> String: +static func order_random() -> String: return {{spec.title | caseUcfirst}}Query.new("orderRandom")._to_string() -static func cursorBefore(id: String) -> String: +static func cursor_before(id: String) -> String: return {{spec.title | caseUcfirst}}Query.new("cursorBefore", null, id)._to_string() -static func cursorAfter(id: String) -> String: +static func cursor_after(id: String) -> String: return {{spec.title | caseUcfirst}}Query.new("cursorAfter", null, id)._to_string() static func limit(p_limit: int) -> String: @@ -160,39 +160,39 @@ static func limit(p_limit: int) -> String: static func offset(p_offset: int) -> String: return {{spec.title | caseUcfirst}}Query.new("offset", null, p_offset)._to_string() -static func distanceEqual(attribute: String, values: Array, distance: float, meters: bool = true) -> String: +static func distance_equal(attribute: String, values: Array, distance: float, meters: bool = true) -> String: return {{spec.title | caseUcfirst}}Query.new("distanceEqual", attribute, [[values, distance, meters]])._to_string() -static func distanceNotEqual(attribute: String, values: Array, distance: float, meters: bool = true) -> String: +static func distance_not_equal(attribute: String, values: Array, distance: float, meters: bool = true) -> String: return {{spec.title | caseUcfirst}}Query.new("distanceNotEqual", attribute, [[values, distance, meters]])._to_string() -static func distanceGreaterThan(attribute: String, values: Array, distance: float, meters: bool = true) -> String: +static func distance_greater_than(attribute: String, values: Array, distance: float, meters: bool = true) -> String: return {{spec.title | caseUcfirst}}Query.new("distanceGreaterThan", attribute, [[values, distance, meters]])._to_string() -static func distanceLessThan(attribute: String, values: Array, distance: float, meters: bool = true) -> String: +static func distance_less_than(attribute: String, values: Array, distance: float, meters: bool = true) -> String: return {{spec.title | caseUcfirst}}Query.new("distanceLessThan", attribute, [[values, distance, meters]])._to_string() static func intersects(attribute: String, values: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("intersects", attribute, [values])._to_string() -static func notIntersects(attribute: String, values: Array) -> String: +static func not_intersects(attribute: String, values: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("notIntersects", attribute, [values])._to_string() static func crosses(attribute: String, values: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("crosses", attribute, [values])._to_string() -static func notCrosses(attribute: String, values: Array) -> String: +static func not_crosses(attribute: String, values: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("notCrosses", attribute, [values])._to_string() static func overlaps(attribute: String, values: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("overlaps", attribute, [values])._to_string() -static func notOverlaps(attribute: String, values: Array) -> String: +static func not_overlaps(attribute: String, values: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("notOverlaps", attribute, [values])._to_string() static func touches(attribute: String, values: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("touches", attribute, [values])._to_string() -static func notTouches(attribute: String, values: Array) -> String: +static func not_touches(attribute: String, values: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("notTouches", attribute, [values])._to_string() diff --git a/templates/godot/addons/query.gd.twig b/templates/godot/addons/query.gd.twig index d50a9133f2..663a425efc 100644 --- a/templates/godot/addons/query.gd.twig +++ b/templates/godot/addons/query.gd.twig @@ -28,88 +28,88 @@ func _to_string() -> String: static func equal(attribute: String, value: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("equal", attribute, value)._to_string() -static func notEqual(attribute: String, value: Variant) -> String: +static func not_equal(attribute: String, value: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("notEqual", attribute, value)._to_string() static func regex(attribute: String, pattern: String) -> String: return {{spec.title | caseUcfirst}}Query.new("regex", attribute, pattern)._to_string() -static func lessThan(attribute: String, value: Variant) -> String: +static func less_than(attribute: String, value: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("lessThan", attribute, value)._to_string() -static func lessThanEqual(attribute: String, value: Variant) -> String: +static func less_than_equal(attribute: String, value: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("lessThanEqual", attribute, value)._to_string() -static func greaterThan(attribute: String, value: Variant) -> String: +static func greater_than(attribute: String, value: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("greaterThan", attribute, value)._to_string() -static func greaterThanEqual(attribute: String, value: Variant) -> String: +static func greater_than_equal(attribute: String, value: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("greaterThanEqual", attribute, value)._to_string() static func search(attribute: String, value: String) -> String: return {{spec.title | caseUcfirst}}Query.new("search", attribute, value)._to_string() -static func isNull(attribute: String) -> String: +static func is_null(attribute: String) -> String: return {{spec.title | caseUcfirst}}Query.new("isNull", attribute)._to_string() -static func isNotNull(attribute: String) -> String: +static func is_not_null(attribute: String) -> String: return {{spec.title | caseUcfirst}}Query.new("isNotNull", attribute)._to_string() static func exists(attributes: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("exists", null, attributes)._to_string() -static func notExists(attributes: Array) -> String: +static func not_exists(attributes: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("notExists", null, attributes)._to_string() static func between(attribute: String, start: Variant, end: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("between", attribute, [start, end])._to_string() -static func startsWith(attribute: String, value: String) -> String: +static func starts_with(attribute: String, value: String) -> String: return {{spec.title | caseUcfirst}}Query.new("startsWith", attribute, value)._to_string() -static func endsWith(attribute: String, value: String) -> String: +static func ends_with(attribute: String, value: String) -> String: return {{spec.title | caseUcfirst}}Query.new("endsWith", attribute, value)._to_string() static func contains(attribute: String, value: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("contains", attribute, value)._to_string() -static func containsAny(attribute: String, value: Array) -> String: +static func contains_any(attribute: String, value: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("containsAny", attribute, value)._to_string() -static func containsAll(attribute: String, value: Array) -> String: +static func contains_all(attribute: String, value: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("containsAll", attribute, value)._to_string() -static func notContains(attribute: String, value: Variant) -> String: +static func not_contains(attribute: String, value: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("notContains", attribute, value)._to_string() -static func notSearch(attribute: String, value: String) -> String: +static func not_search(attribute: String, value: String) -> String: return {{spec.title | caseUcfirst}}Query.new("notSearch", attribute, value)._to_string() -static func notBetween(attribute: String, start: Variant, end: Variant) -> String: +static func not_between(attribute: String, start: Variant, end: Variant) -> String: return {{spec.title | caseUcfirst}}Query.new("notBetween", attribute, [start, end])._to_string() -static func notStartsWith(attribute: String, value: String) -> String: +static func not_starts_with(attribute: String, value: String) -> String: return {{spec.title | caseUcfirst}}Query.new("notStartsWith", attribute, value)._to_string() -static func notEndsWith(attribute: String, value: String) -> String: +static func not_ends_with(attribute: String, value: String) -> String: return {{spec.title | caseUcfirst}}Query.new("notEndsWith", attribute, value)._to_string() -static func createdBefore(value: String) -> String: - return lessThan("$createdAt", value) +static func created_before(value: String) -> String: + return less_than("$createdAt", value) -static func createdAfter(value: String) -> String: - return greaterThan("$createdAt", value) +static func created_after(value: String) -> String: + return greater_than("$createdAt", value) -static func createdBetween(start: String, end: String) -> String: +static func created_between(start: String, end: String) -> String: return between("$createdAt", start, end) -static func updatedBefore(value: String) -> String: - return lessThan("$updatedAt", value) +static func updated_before(value: String) -> String: + return less_than("$updatedAt", value) -static func updatedAfter(value: String) -> String: - return greaterThan("$updatedAt", value) +static func updated_after(value: String) -> String: + return greater_than("$updatedAt", value) -static func updatedBetween(start: String, end: String) -> String: +static func updated_between(start: String, end: String) -> String: return between("$updatedAt", start, end) static func or_query(queries: Array) -> String: @@ -128,7 +128,7 @@ static func and_query(queries: Array) -> String: parsed_queries.append(parsed) return {{spec.title | caseUcfirst}}Query.new("and", null, parsed_queries)._to_string() -static func elemMatch(attribute: String, queries: Array) -> String: +static func elem_match(attribute: String, queries: Array) -> String: var parsed_queries := [] for q in queries: var parsed = JSON.parse_string(q) @@ -139,19 +139,19 @@ static func elemMatch(attribute: String, queries: Array) -> String: static func select(attributes: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("select", null, attributes)._to_string() -static func orderAsc(attribute: String) -> String: +static func order_asc(attribute: String) -> String: return {{spec.title | caseUcfirst}}Query.new("orderAsc", attribute)._to_string() -static func orderDesc(attribute: String) -> String: +static func order_desc(attribute: String) -> String: return {{spec.title | caseUcfirst}}Query.new("orderDesc", attribute)._to_string() -static func orderRandom() -> String: +static func order_random() -> String: return {{spec.title | caseUcfirst}}Query.new("orderRandom")._to_string() -static func cursorBefore(id: String) -> String: +static func cursor_before(id: String) -> String: return {{spec.title | caseUcfirst}}Query.new("cursorBefore", null, id)._to_string() -static func cursorAfter(id: String) -> String: +static func cursor_after(id: String) -> String: return {{spec.title | caseUcfirst}}Query.new("cursorAfter", null, id)._to_string() static func limit(p_limit: int) -> String: @@ -160,39 +160,39 @@ static func limit(p_limit: int) -> String: static func offset(p_offset: int) -> String: return {{spec.title | caseUcfirst}}Query.new("offset", null, p_offset)._to_string() -static func distanceEqual(attribute: String, values: Array, distance: float, meters: bool = true) -> String: +static func distance_equal(attribute: String, values: Array, distance: float, meters: bool = true) -> String: return {{spec.title | caseUcfirst}}Query.new("distanceEqual", attribute, [[values, distance, meters]])._to_string() -static func distanceNotEqual(attribute: String, values: Array, distance: float, meters: bool = true) -> String: +static func distance_not_equal(attribute: String, values: Array, distance: float, meters: bool = true) -> String: return {{spec.title | caseUcfirst}}Query.new("distanceNotEqual", attribute, [[values, distance, meters]])._to_string() -static func distanceGreaterThan(attribute: String, values: Array, distance: float, meters: bool = true) -> String: +static func distance_greater_than(attribute: String, values: Array, distance: float, meters: bool = true) -> String: return {{spec.title | caseUcfirst}}Query.new("distanceGreaterThan", attribute, [[values, distance, meters]])._to_string() -static func distanceLessThan(attribute: String, values: Array, distance: float, meters: bool = true) -> String: +static func distance_less_than(attribute: String, values: Array, distance: float, meters: bool = true) -> String: return {{spec.title | caseUcfirst}}Query.new("distanceLessThan", attribute, [[values, distance, meters]])._to_string() static func intersects(attribute: String, values: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("intersects", attribute, [values])._to_string() -static func notIntersects(attribute: String, values: Array) -> String: +static func not_intersects(attribute: String, values: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("notIntersects", attribute, [values])._to_string() static func crosses(attribute: String, values: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("crosses", attribute, [values])._to_string() -static func notCrosses(attribute: String, values: Array) -> String: +static func not_crosses(attribute: String, values: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("notCrosses", attribute, [values])._to_string() static func overlaps(attribute: String, values: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("overlaps", attribute, [values])._to_string() -static func notOverlaps(attribute: String, values: Array) -> String: +static func not_overlaps(attribute: String, values: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("notOverlaps", attribute, [values])._to_string() static func touches(attribute: String, values: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("touches", attribute, [values])._to_string() -static func notTouches(attribute: String, values: Array) -> String: +static func not_touches(attribute: String, values: Array) -> String: return {{spec.title | caseUcfirst}}Query.new("notTouches", attribute, [values])._to_string() diff --git a/tests/GDScript4Test.php b/tests/GDScript4Test.php new file mode 100644 index 0000000000..db71425899 --- /dev/null +++ b/tests/GDScript4Test.php @@ -0,0 +1,36 @@ + void: + call_deferred("start") + + +func start() -> void: + await run_tests() + + print("\nAll tests completed") + + quit() + + +func run_tests() -> void: + Appwrite.add_header("Origin", "http://localhost:8080") + Appwrite.set_self_signed(true) + + Appwrite.set_endpoint("http://localhost:8080/v1") + + print("\nTest Started") + + var sdk_headers := Appwrite.get_headers() + + print( + "x-sdk-name: %s; x-sdk-platform: %s; x-sdk-language: %s; x-sdk-version: %s" + % [ + sdk_headers.get("x-sdk-name", ""), + sdk_headers.get("x-sdk-platform", ""), + sdk_headers.get("x-sdk-language", ""), + sdk_headers.get("x-sdk-version", "") + ] + ) + + await run_foo_tests() + await run_bar_tests() + await run_general_tests() + + run_query_tests() + run_permission_tests() + run_id_tests() + run_operator_tests() + + +func print_response(response) -> void: + if response == null: + print("null response") + return + + if response is AppwriteException: + print(response.message) + print(response.response) + return + + if response is Dictionary: + if response.has("result"): + print(response["result"]) + else: + print(response) + + return + + if "result" in response: + print(response.result) + return + + print(response) + + +func run_foo_tests() -> void: + print_response(await Appwrite.foo.get("string", 123, ["string in array"])) + print_response(await Appwrite.foo.post("string", 123, ["string in array"])) + print_response(await Appwrite.foo.put("string", 123, ["string in array"])) + print_response(await Appwrite.foo.patch("string", 123, ["string in array"])) + print_response(await Appwrite.foo.delete("string", 123, ["string in array"])) + + +func run_bar_tests() -> void: + print_response(await Appwrite.bar.get("string", 123, ["string in array"])) + print_response(await Appwrite.bar.post("string", 123, ["string in array"])) + print_response(await Appwrite.bar.put("string", 123, ["string in array"])) + print_response(await Appwrite.bar.patch("string", 123, ["string in array"])) + print_response(await Appwrite.bar.delete("string", 123, ["string in array"])) + + +func run_general_tests() -> void: + var response + + response = await Appwrite.general.redirect() + print_response(response) + + response = await Appwrite.general.upload( + "string", + 123, + ["string in array"], + AppwriteInputFile.from_path("res://tests/resources/file.png") + ) + + print_response(response) + + response = await Appwrite.general.upload( + "string", + 123, + ["string in array"], + AppwriteInputFile.from_path("res://tests/resources/large_file.mp4") + ) + + print_response(response) + + var file_bytes := FileAccess.get_file_as_bytes( + "res://tests/resources/file.png" + ) + + response = await Appwrite.general.upload( + "string", + 123, + ["string in array"], + AppwriteInputFile.from_bytes( + file_bytes, + "file.png", + "image/png" + ) + ) + + print_response(response) + + var large_file_bytes := FileAccess.get_file_as_bytes( + "res://tests/resources/large_file.mp4" + ) + + response = await Appwrite.general.upload( + "string", + 123, + ["string in array"], + AppwriteInputFile.from_bytes( + large_file_bytes, + "large_file.mp4", + "video/mp4" + ) + ) + + print_response(response) + + response = await Appwrite.general.enum(Appwrite.MOCKTYPE.FIRST) + print_response(response) + + response = await Appwrite.general.create_player( + AppwritePlayer.new({ + "id": "player1", + "name": "John Doe", + "score": 100 + }) + ) + + print_response(response) + + response = await Appwrite.general.create_players([ + { + "id": "player1", + "name": "John Doe", + "score": 100 + }, + { + "id": "player2", + "name": "Jane Doe", + "score": 200 + } + ]) + + print_response(response) + + await test_errors() + + await Appwrite.general.empty() + + var url := Appwrite.general.oauth2( + "clientId", + ["test"], + "123456", + "https://localhost", + "https://localhost" + ) + + print(url) + + response = await Appwrite.general.headers() + print_response(response) + + +func test_errors() -> void: + var response + + response = await Appwrite.general.error400() + print_response(response) + + response = await Appwrite.general.error500() + print_response(response) + + response = await Appwrite.general.error502() + print_response(response) + + var invalid_result = client.set_endpoint("htp://cloud.appwrite.io/v1") + + if invalid_result is AppwriteException: + print(invalid_result.message) + else: + print(invalid_result) + + +func run_query_tests() -> void: + print(AppwriteQuery.equal("released", [true])) + print(AppwriteQuery.equal("title", ["Spiderman", "Dr. Strange"])) + print(AppwriteQuery.not_equal("title", "Spiderman")) + + print(AppwriteQuery.less_than("releasedYear", 1990)) + print(AppwriteQuery.greater_than("releasedYear", 1990)) + + print(AppwriteQuery.search("name", "john")) + + print(AppwriteQuery.is_null("name")) + print(AppwriteQuery.is_not_null("name")) + + print(AppwriteQuery.limit(50)) + print(AppwriteQuery.offset(20)) + + print(AppwriteQuery.order_asc("title")) + print(AppwriteQuery.order_desc("title")) + + print(AppwriteQuery.contains("title", "Spider")) + + print(AppwriteQuery.or_queries([ + AppwriteQuery.equal("released", true), + AppwriteQuery.less_than("releasedYear", 1990) + ])) + + +func run_permission_tests() -> void: + print(AppwritePermission.read(AppwriteRole.any())) + + print( + AppwritePermission.write( + AppwriteRole.user(AppwriteID.custom("userid")) + ) + ) + + print(AppwritePermission.create(AppwriteRole.users())) + + print( + AppwritePermission.delete( + AppwriteRole.team("teamId", "owner") + ) + ) + + +func run_id_tests() -> void: + print(AppwriteID.unique()) + print(AppwriteID.custom("custom_id")) + + +func run_operator_tests() -> void: + print(Operator.increment()) + print(Operator.increment(5, 100)) + print(Operator.decrement()) + print(Operator.decrement(3, 0)) + print(Operator.multiply(2)) + print(Operator.multiply(3, 1000)) + print(Operator.divide(2)) + print(Operator.divide(4, 1)) + print(Operator.modulo(5)) + print(Operator.power(2)) + print(Operator.power(3, 100)) + print(Operator.array_append(['item1', 'item2'])) + print(Operator.array_prepend(['first', 'second'])) + print(Operator.array_insert(0, 'newItem')) + print(Operator.array_remove('oldItem')) + print(Operator.array_unique()) + print(Operator.array_intersect(['a', 'b', 'c'])) + print(Operator.array_diff(['x', 'y'])) + print(Operator.array_filter(Condition.EQUAL, 'test')) + print(Operator.string_concat('suffix')) + print(Operator.string_replace('old', 'new')) + print(Operator.toggle()) + print(Operator.date_add_days(7)) + print(Operator.date_sub_days(3)) + print(Operator.date_set_now()) + + response = general.headers() + print(response.result) diff --git a/tests/languages/godot/test.gd b/tests/languages/godot/test.gd new file mode 100644 index 0000000000..8c1101a37e --- /dev/null +++ b/tests/languages/godot/test.gd @@ -0,0 +1,285 @@ +extends SceneTree + +var _Appwrite = load("res://addons/appwrite/appwrite.gd") +var Appwrite = _Appwrite.new() + +func _init() -> void: + call_deferred("start") + + +func start() -> void: + await run_tests() + + print("\nAll tests completed") + + quit() + + +func run_tests() -> void: + Appwrite.add_header("Origin", "http://localhost:8080") + Appwrite.set_self_signed(true) + Appwrite.set_project("123456789") + Appwrite.set_endpoint("http://localhost:8080/v1") + + print("\nTest Started") + + var sdk_headers = Appwrite.get_headers() + + print( + "x-sdk-name: %s; x-sdk-platform: %s; x-sdk-language: %s; x-sdk-version: %s" + % [ + sdk_headers.get("x-sdk-name", ""), + sdk_headers.get("x-sdk-platform", ""), + sdk_headers.get("x-sdk-language", ""), + sdk_headers.get("x-sdk-version", "") + ] + ) + + await run_foo_tests() + await run_bar_tests() + await run_general_tests() + + run_query_tests() + run_permission_tests() + run_id_tests() + run_operator_tests() + + +func print_response(response) -> void: + if response == null: + print("null response") + return + + if response is AppwriteException: + print(response.message) + print(response.response) + return + + if response is Dictionary: + if response.has("result"): + print(response["result"]) + else: + print(response) + + return + + if "result" in response: + print(response.result) + return + + print(response) + + +func run_foo_tests() -> void: + print_response(await Appwrite.foo.xget("string", 123, ["string in array"])) + print_response(await Appwrite.foo.post("string", 123, ["string in array"])) + print_response(await Appwrite.foo.put("string", 123, ["string in array"])) + print_response(await Appwrite.foo.patch("string", 123, ["string in array"])) + print_response(await Appwrite.foo.delete("string", 123, ["string in array"])) + + +func run_bar_tests() -> void: + print_response(await Appwrite.bar.xget("string", 123, ["string in array"])) + print_response(await Appwrite.bar.post("string", 123, ["string in array"])) + print_response(await Appwrite.bar.put("string", 123, ["string in array"])) + print_response(await Appwrite.bar.patch("string", 123, ["string in array"])) + print_response(await Appwrite.bar.delete("string", 123, ["string in array"])) + + +func run_general_tests() -> void: + var response + + response = await Appwrite.general.redirect() + print_response(response) + + response = await Appwrite.general.upload( + "string", + 123, + ["string in array"], + AppwriteInputFile.from_path("res://tests/resources/file.png") + ) + + print_response(response) + + response = await Appwrite.general.upload( + "string", + 123, + ["string in array"], + AppwriteInputFile.from_path("res://tests/resources/large_file.mp4") + ) + + print_response(response) + + var file_bytes := FileAccess.get_file_as_bytes( + "res://tests/resources/file.png" + ) + + response = await Appwrite.general.upload( + "string", + 123, + ["string in array"], + AppwriteInputFile.from_bytes( + file_bytes, + "file.png", + ) + ) + + print_response(response) + + var large_file_bytes := FileAccess.get_file_as_bytes( + "res://tests/resources/large_file.mp4" + ) + + response = await Appwrite.general.upload( + "string", + 123, + ["string in array"], + AppwriteInputFile.from_bytes( + large_file_bytes, + "large_file.mp4", + ) + ) + + print_response(response) + + response = await Appwrite.general.xenum(Appwrite.MOCKTYPE.FIRST) + print_response(response) + + response = await Appwrite.general.create_player( + AppwritePlayer.from_dict({ + "id": "player1", + "name": "John Doe", + "score": 100 + }) + ) + + print_response(response) + + response = await Appwrite.general.create_players([ + AppwritePlayer.from_dict({ + "id": "player1", + "name": "John Doe", + "score": 100 + }), + AppwritePlayer.from_dict({ + "id": "player2", + "name": "Jane Doe", + "score": 200 + }) + ]) + + print_response(response) + + await test_errors() + + await Appwrite.general.empty() + + var url = await Appwrite.general.oauth2( + "clientId", + ["test"], + "123456", + "https://localhost:8080", + "https://localhost:8080" + ) + + print(url) + + response = await Appwrite.general.headers() + print_response(response) + + +func test_errors() -> void: + var response + + response = await Appwrite.general.error400() + print_response(response) + + response = await Appwrite.general.error500() + print_response(response) + + response = await Appwrite.general.error502() + print_response(response) + + Appwrite.set_endpoint("htp://cloud.appwrite.io/v1") + var invalid_result = await Appwrite.ping() + + if invalid_result is AppwriteException: + print(invalid_result.message) + else: + print(invalid_result) + + +func run_query_tests() -> void: + print(AppwriteQuery.equal("released", [true])) + print(AppwriteQuery.equal("title", ["Spiderman", "Dr. Strange"])) + print(AppwriteQuery.not_equal("title", "Spiderman")) + + print(AppwriteQuery.less_than("releasedYear", 1990)) + print(AppwriteQuery.greater_than("releasedYear", 1990)) + + print(AppwriteQuery.search("name", "john")) + + print(AppwriteQuery.is_null("name")) + print(AppwriteQuery.is_not_null("name")) + + print(AppwriteQuery.limit(50)) + print(AppwriteQuery.offset(20)) + + print(AppwriteQuery.order_asc("title")) + print(AppwriteQuery.order_desc("title")) + + print(AppwriteQuery.contains("title", "Spider")) + + +func run_permission_tests() -> void: + print(AppwritePermission.read(AppwriteRole.any())) + + print( + AppwritePermission.write( + AppwriteRole.user(AppwriteID.custom("userid")) + ) + ) + + print(AppwritePermission.create(AppwriteRole.users())) + + print( + AppwritePermission.delete( + AppwriteRole.team("teamId", "owner") + ) + ) + + +func run_id_tests() -> void: + print(AppwriteID.unique()) + print(AppwriteID.custom("custom_id")) + + +func run_operator_tests() -> void: + print(AppwriteOperator.increment()) + print(AppwriteOperator.increment(5, 100)) + print(AppwriteOperator.decrement()) + print(AppwriteOperator.decrement(3, 0)) + print(AppwriteOperator.multiply(2)) + print(AppwriteOperator.multiply(3, 1000)) + print(AppwriteOperator.divide(2)) + print(AppwriteOperator.divide(4, 1)) + print(AppwriteOperator.modulo(5)) + print(AppwriteOperator.power(2)) + print(AppwriteOperator.power(3, 100)) + print(AppwriteOperator.array_append(['item1', 'item2'])) + print(AppwriteOperator.array_prepend(['first', 'second'])) + print(AppwriteOperator.array_insert(0, 'newItem')) + print(AppwriteOperator.array_remove('oldItem')) + print(AppwriteOperator.array_unique()) + print(AppwriteOperator.array_intersect(['a', 'b', 'c'])) + print(AppwriteOperator.array_diff(['x', 'y'])) + print(AppwriteOperator.array_filter(AppwriteOperator.EQUAL, 'test')) + print(AppwriteOperator.string_concat('suffix')) + print(AppwriteOperator.string_replace('old', 'new')) + print(AppwriteOperator.toggle()) + print(AppwriteOperator.date_add_days(7)) + print(AppwriteOperator.date_sub_days(3)) + print(AppwriteOperator.date_set_now()) + + var headers = Appwrite.get_headers() + print(headers) From 383b733f48c5c0733e9e2472b037a243784760e1 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Mon, 18 May 2026 02:33:55 +0530 Subject: [PATCH 29/58] fix(godot): improve request serialization and query handling - add serialization to client - replace built-in query generation with custom query builder for array element support - streamline ping response handling for SDK consistency --- templates/godot/addons/appwrite.gd.twig | 20 +++++--- templates/godot/addons/client.gd.twig | 64 ++++++++++++++++++++++--- templates/godot/menu.gd | 2 +- tests/Godot4Test.php | 2 + tests/languages/godot/test.gd | 34 ++++--------- 5 files changed, 81 insertions(+), 41 deletions(-) diff --git a/templates/godot/addons/appwrite.gd.twig b/templates/godot/addons/appwrite.gd.twig index cb487a48a9..c7e8618068 100644 --- a/templates/godot/addons/appwrite.gd.twig +++ b/templates/godot/addons/appwrite.gd.twig @@ -49,21 +49,27 @@ func _ready() -> void: {% if header.description %} ## {{header.description}} {% endif %} -func set_{{header.key | caseSnake}}(value: String) -> void: +func set_{{header.key | caseSnake}}(value: String) -> {{ prefix }}: _client.set_{{header.key | caseSnake}}(value) + return self {% endfor %} ## Set self signed status -func set_self_signed(status: bool = true) -> void: +func set_self_signed(status: bool = true) -> {{ prefix }}: _client.set_self_signed(status) + return self ## Set the endpoint -func set_endpoint(endpoint: String) -> void: - _client.set_endpoint(endpoint) +func set_endpoint(endpoint: String) -> Variant: + var res = _client.set_endpoint(endpoint) + if res is {{ prefix }}Exception: + return res + return self ## Add a header -func add_header(key: String, value: String) -> void: +func add_header(key: String, value: String) -> {{ prefix }}: _client.add_header(key, value) + return self ## Get all headers func get_headers() -> Dictionary: @@ -81,6 +87,6 @@ func _apply_env(key: String, value: String) -> void: {% endfor %} ## Ping {{prefix}} Server for testing connection -func ping() -> String: +func ping() -> Variant: var response = await _client.call_api('GET', '/ping') - return str(response.get('body')) \ No newline at end of file + return response.get('body', {}) \ No newline at end of file diff --git a/templates/godot/addons/client.gd.twig b/templates/godot/addons/client.gd.twig index bd58562739..e6219caeb1 100644 --- a/templates/godot/addons/client.gd.twig +++ b/templates/godot/addons/client.gd.twig @@ -25,10 +25,19 @@ func set_self_signed(status: bool = true) -> RefCounted: _self_signed = status return self -func set_endpoint(endpoint: String) -> RefCounted: - _endpoint = endpoint +func set_endpoint(endpoint: String) -> Variant: + if not _is_valid_endpoint(endpoint): + push_error("Invalid endpoint URL: " + endpoint) + return {{prefix}}Exception.new("Invalid endpoint URL: " + endpoint) + + _endpoint = endpoint.trim_suffix("/") return self +func _is_valid_endpoint(url: String) -> bool: + var regex := RegEx.new() + regex.compile("^https?://.+") + return regex.search(url) != null + func add_header(key: String, value: String) -> RefCounted: _global_headers[key.to_lower()] = value return self @@ -71,10 +80,9 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param var request_path := path var body := PackedByteArray() - if http_method in [HTTPClient.METHOD_GET, HTTPClient.METHOD_DELETE]: + if http_method == HTTPClient.METHOD_GET: if not params.is_empty(): - var query_http := HTTPClient.new() - request_path += "?" + query_http.query_string_from_dict(params) + request_path += "?" + _query_string_from_dict(params) else: var has_files := false var large_file_key := "" @@ -93,7 +101,7 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param combined_headers["content-type"] = "multipart/form-data; boundary=" + boundary body = _build_multipart(params, boundary) else: - var body_str := JSON.stringify(params) + var body_str := JSON.stringify(_serialize(params)) body = body_str.to_utf8_buffer() combined_headers["content-type"] = "application/json" @@ -208,4 +216,46 @@ func _call_chunked(method: String, path: String, headers: Dictionary, params: Di start = end on_progress.emit((float(start) / size) * 100.0, size, start) - return response \ No newline at end of file + return response + +func _query_string_from_dict(p_dict: Dictionary) -> String: + var query := "" + for key in p_dict.keys(): + var value = p_dict[key] + var base_key := str(key) + match typeof(value): + TYPE_ARRAY: + # Always use 'key[]' syntax for arrays, even with a single element + var encoded_key := (base_key + "[]").uri_encode() + for element in value: + query += "&" + encoded_key + "=" + str(element).uri_encode() + TYPE_NIL: + query += "&" + base_key.uri_encode() + _: + query += "&" + base_key.uri_encode() + "=" + str(value).uri_encode() + if query.length() > 0: + query = query.substr(1) + return query + +func _serialize(value): + match typeof(value): + TYPE_ARRAY: + var arr := [] + for item in value: + arr.append(_serialize(item)) + return arr + + TYPE_DICTIONARY: + var dict := {} + for key in value: + dict[key] = _serialize(value[key]) + return dict + + TYPE_OBJECT: + if value != null and value.has_method("to_dict"): + return _serialize(value.to_dict()) + + return value + + _: + return value \ No newline at end of file diff --git a/templates/godot/menu.gd b/templates/godot/menu.gd index 57569738cd..47af6ca7cf 100755 --- a/templates/godot/menu.gd +++ b/templates/godot/menu.gd @@ -1,4 +1,4 @@ extends Control func _on_ping_click(): - $Status.text += await Appwrite.ping() + $Status.text += str(await Appwrite.ping()) diff --git a/tests/Godot4Test.php b/tests/Godot4Test.php index a7828c5984..b56171c9e3 100644 --- a/tests/Godot4Test.php +++ b/tests/Godot4Test.php @@ -14,6 +14,8 @@ class Godot4Test extends Base protected array $build = [ 'mkdir -p tests/sdks/godot/tests', 'cp tests/languages/godot/test.gd tests/sdks/godot/tests/test.gd', + 'mkdir tests/sdks/godot/tests/resources/', + 'cp -r tests/resources/ tests/sdks/godot/tests/', 'cd tests/sdks/godot && godot --headless --import --quit', ]; protected string $command = 'cd tests/sdks/godot && godot --headless --script tests/test.gd'; diff --git a/tests/languages/godot/test.gd b/tests/languages/godot/test.gd index 8c1101a37e..6d48ab2f06 100644 --- a/tests/languages/godot/test.gd +++ b/tests/languages/godot/test.gd @@ -6,25 +6,20 @@ var Appwrite = _Appwrite.new() func _init() -> void: call_deferred("start") - func start() -> void: await run_tests() - print("\nAll tests completed") - quit() - func run_tests() -> void: - Appwrite.add_header("Origin", "http://localhost:8080") - Appwrite.set_self_signed(true) - Appwrite.set_project("123456789") - Appwrite.set_endpoint("http://localhost:8080/v1") + Appwrite.add_header("Origin", "http://localhost:8080") \ + .set_project("123456") \ + .set_self_signed(true) \ + .set_endpoint("http://localhost:8080/v1") print("\nTest Started") var sdk_headers = Appwrite.get_headers() - print( "x-sdk-name: %s; x-sdk-platform: %s; x-sdk-language: %s; x-sdk-version: %s" % [ @@ -35,6 +30,7 @@ func run_tests() -> void: ] ) + print_response(await Appwrite.ping()) await run_foo_tests() await run_bar_tests() await run_general_tests() @@ -44,7 +40,6 @@ func run_tests() -> void: run_id_tests() run_operator_tests() - func print_response(response) -> void: if response == null: print("null response") @@ -69,7 +64,6 @@ func print_response(response) -> void: print(response) - func run_foo_tests() -> void: print_response(await Appwrite.foo.xget("string", 123, ["string in array"])) print_response(await Appwrite.foo.post("string", 123, ["string in array"])) @@ -77,7 +71,6 @@ func run_foo_tests() -> void: print_response(await Appwrite.foo.patch("string", 123, ["string in array"])) print_response(await Appwrite.foo.delete("string", 123, ["string in array"])) - func run_bar_tests() -> void: print_response(await Appwrite.bar.xget("string", 123, ["string in array"])) print_response(await Appwrite.bar.post("string", 123, ["string in array"])) @@ -85,7 +78,6 @@ func run_bar_tests() -> void: print_response(await Appwrite.bar.patch("string", 123, ["string in array"])) print_response(await Appwrite.bar.delete("string", 123, ["string in array"])) - func run_general_tests() -> void: var response @@ -166,7 +158,7 @@ func run_general_tests() -> void: "name": "Jane Doe", "score": 200 }) - ]) + ] as Array[AppwritePlayer]) print_response(response) @@ -187,7 +179,6 @@ func run_general_tests() -> void: response = await Appwrite.general.headers() print_response(response) - func test_errors() -> void: var response @@ -200,14 +191,8 @@ func test_errors() -> void: response = await Appwrite.general.error502() print_response(response) - Appwrite.set_endpoint("htp://cloud.appwrite.io/v1") - var invalid_result = await Appwrite.ping() - - if invalid_result is AppwriteException: - print(invalid_result.message) - else: - print(invalid_result) - + response = Appwrite.set_endpoint("htp://cloud.appwrite.io/v1") + print_response(response) func run_query_tests() -> void: print(AppwriteQuery.equal("released", [true])) @@ -230,7 +215,6 @@ func run_query_tests() -> void: print(AppwriteQuery.contains("title", "Spider")) - func run_permission_tests() -> void: print(AppwritePermission.read(AppwriteRole.any())) @@ -248,12 +232,10 @@ func run_permission_tests() -> void: ) ) - func run_id_tests() -> void: print(AppwriteID.unique()) print(AppwriteID.custom("custom_id")) - func run_operator_tests() -> void: print(AppwriteOperator.increment()) print(AppwriteOperator.increment(5, 100)) From 0d74c69759c7cdafbc5e42afe28d3e8e567fdb59 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Tue, 19 May 2026 18:29:32 +0530 Subject: [PATCH 30/58] feat(auth): add OAuth2 authentication flow support - Add WebAuth/OAuth authentication handling - Implement local callback server for redirect capture - Open browser and await authentication result - Return callback URL for SDK-side processing - Add success/failure HTML pages for OAuth redirects - Handle connection cleanup and callback lifecycle --- src/SDK/Language/Godot.php | 41 +-- templates/godot/addons/appwrite.gd.twig | 2 +- .../godot/addons/services/service.gd.twig | 6 +- .../godot/addons/{ => utils}/client.gd.twig | 21 +- .../addons/{ => utils}/exception.gd.twig | 0 templates/godot/addons/{ => utils}/id.gd.twig | 0 .../addons/{ => utils}/input_file.gd.twig | 0 templates/godot/addons/utils/oauth2.gd.twig | 261 ++++++++++++++++++ .../godot/addons/{ => utils}/operator.gd.twig | 0 .../addons/{ => utils}/permission.gd.twig | 0 .../godot/addons/{ => utils}/query.gd.twig | 0 .../godot/addons/{ => utils}/role.gd.twig | 0 .../godot/addons/{ => utils}/service.gd.twig | 5 +- tests/languages/godot/test.gd | 235 +++++++++++++++- 14 files changed, 531 insertions(+), 40 deletions(-) rename templates/godot/addons/{ => utils}/client.gd.twig (91%) rename templates/godot/addons/{ => utils}/exception.gd.twig (100%) rename templates/godot/addons/{ => utils}/id.gd.twig (100%) rename templates/godot/addons/{ => utils}/input_file.gd.twig (100%) create mode 100644 templates/godot/addons/utils/oauth2.gd.twig rename templates/godot/addons/{ => utils}/operator.gd.twig (100%) rename templates/godot/addons/{ => utils}/permission.gd.twig (100%) rename templates/godot/addons/{ => utils}/query.gd.twig (100%) rename templates/godot/addons/{ => utils}/role.gd.twig (100%) rename templates/godot/addons/{ => utils}/service.gd.twig (84%) diff --git a/src/SDK/Language/Godot.php b/src/SDK/Language/Godot.php index 1500ec9ed9..1ccd0eaeeb 100644 --- a/src/SDK/Language/Godot.php +++ b/src/SDK/Language/Godot.php @@ -62,48 +62,53 @@ public function getFiles(): array ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/client.gd', - 'template' => 'godot/addons/client.gd.twig', + 'destination' => 'addons/{{ spec.title | caseSnake }}/utils/client.gd', + 'template' => 'godot/addons/utils/client.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/service.gd', - 'template' => 'godot/addons/service.gd.twig', + 'destination' => 'addons/{{ spec.title | caseSnake }}/utils/oauth2.gd', + 'template' => 'godot/addons/utils/oauth2.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/exception.gd', - 'template' => 'godot/addons/exception.gd.twig', + 'destination' => 'addons/{{ spec.title | caseSnake }}/utils/service.gd', + 'template' => 'godot/addons/utils/service.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/id.gd', - 'template' => 'godot/addons/id.gd.twig', + 'destination' => 'addons/{{ spec.title | caseSnake }}/utils/exception.gd', + 'template' => 'godot/addons/utils/exception.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/permission.gd', - 'template' => 'godot/addons/permission.gd.twig', + 'destination' => 'addons/{{ spec.title | caseSnake }}/utils/id.gd', + 'template' => 'godot/addons/utils/id.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/role.gd', - 'template' => 'godot/addons/role.gd.twig', + 'destination' => 'addons/{{ spec.title | caseSnake }}/utils/permission.gd', + 'template' => 'godot/addons/utils/permission.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/query.gd', - 'template' => 'godot/addons/query.gd.twig', + 'destination' => 'addons/{{ spec.title | caseSnake }}/utils/role.gd', + 'template' => 'godot/addons/utils/role.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/input_file.gd', - 'template' => 'godot/addons/input_file.gd.twig', + 'destination' => 'addons/{{ spec.title | caseSnake }}/utils/query.gd', + 'template' => 'godot/addons/utils/query.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{spec.title | caseSnake}}/operator.gd', - 'template' => 'godot/addons/operator.gd.twig', + 'destination' => 'addons/{{ spec.title | caseSnake }}/utils/input_file.gd', + 'template' => 'godot/addons/utils/input_file.gd.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'addons/{{ spec.title | caseSnake }}/utils/operator.gd', + 'template' => 'godot/addons/utils/operator.gd.twig', ], [ 'scope' => 'default', diff --git a/templates/godot/addons/appwrite.gd.twig b/templates/godot/addons/appwrite.gd.twig index c7e8618068..cf8c8a9dd2 100644 --- a/templates/godot/addons/appwrite.gd.twig +++ b/templates/godot/addons/appwrite.gd.twig @@ -9,7 +9,7 @@ signal on_progress(progress: float, total_size: int, uploaded_size: int) const _{{ service.name | caseUpper }} = preload("services/{{ service.name | caseSnake }}.gd") {% endfor %} -var _client := preload("client.gd").new() +var _client := preload("utils/client.gd").new() {% for service in spec.services %} var {{ service.name | caseCamel }} : _{{ service.name | caseUpper }} = _{{ service.name | caseUpper }}.new(_client) ## {{ service.description | default("No description is provided") }} {% endfor %} diff --git a/templates/godot/addons/services/service.gd.twig b/templates/godot/addons/services/service.gd.twig index 05087f958e..a1339249f1 100644 --- a/templates/godot/addons/services/service.gd.twig +++ b/templates/godot/addons/services/service.gd.twig @@ -1,5 +1,5 @@ {% set prefix = spec.title | caseUcfirst %} -extends "../service.gd" +extends "../utils/service.gd" ## {{ service.description | default("Service class.") | replace({"\n": "\n## "}) }} {% for method in service.methods %} @@ -93,7 +93,11 @@ func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters. var model_script = null {%~ endif %} + {%~ if method.type == 'webAuth' %} + return await _call_web('{{ method.method }}', _path, _params) + {%~ else %} return await _call('{{ method.method }}', _path, _headers, _params, model_script) + {%~ endif %} {%~ endif %} {%~ endfor %} diff --git a/templates/godot/addons/client.gd.twig b/templates/godot/addons/utils/client.gd.twig similarity index 91% rename from templates/godot/addons/client.gd.twig rename to templates/godot/addons/utils/client.gd.twig index e6219caeb1..b2f8dfbca5 100644 --- a/templates/godot/addons/client.gd.twig +++ b/templates/godot/addons/utils/client.gd.twig @@ -18,7 +18,7 @@ var _global_headers = { {% endfor %} } -const _cookie_store_class = preload("persistence/cookie_store.gd") +const _cookie_store_class = preload("../persistence/cookie_store.gd") var _cookie_store = _cookie_store_class.new() func set_self_signed(status: bool = true) -> RefCounted: @@ -28,7 +28,7 @@ func set_self_signed(status: bool = true) -> RefCounted: func set_endpoint(endpoint: String) -> Variant: if not _is_valid_endpoint(endpoint): push_error("Invalid endpoint URL: " + endpoint) - return {{prefix}}Exception.new("Invalid endpoint URL: " + endpoint) + return {{prefix}}Exception.new("Invalid endpoint URL: " + endpoint, 0, "invalid_endpoint", "Invalid endpoint URL: " + endpoint) _endpoint = endpoint.trim_suffix("/") return self @@ -51,9 +51,22 @@ func set_{{header.key | caseSnake}}(value: String) -> RefCounted: return self {% endfor %} +func call_web_api(method: String, path: String = "", params: Dictionary = {}) -> Dictionary: + if not _ensure_configured(): + return {"statuscode": 400, "body": "missing/wrong configuration. please check logs for more info"} + var oauth_client = load("res://addons/{{ spec.title | caseSnake }}/utils/oauth2.gd").new() + + Engine.get_main_loop().root.add_child.call_deferred(oauth_client) + await oauth_client.tree_entered + + var uri := _endpoint.trim_suffix("/") + var request_path := path + (("?" + _query_string_from_dict(params)) if not params.is_empty() else "") + var res = await oauth_client.authenticate(uri+request_path) + return res + func call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: if not _ensure_configured(): - return {"statusCode": 400, "body": "Missing/wrong configuration. Please check logs for more info"} + return {"statuscode": 400, "body": "missing/wrong configuration. please check logs for more info"} var http := HTTPRequest.new() var tls_options := TLSOptions.client_unsafe() if _self_signed else TLSOptions.client() @@ -62,7 +75,7 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param Engine.get_main_loop().root.add_child.call_deferred(http) await http.tree_entered - # Merge headers + # merge headers var combined_headers := _global_headers.duplicate() for key in headers: combined_headers[key.to_lower()] = headers[key] diff --git a/templates/godot/addons/exception.gd.twig b/templates/godot/addons/utils/exception.gd.twig similarity index 100% rename from templates/godot/addons/exception.gd.twig rename to templates/godot/addons/utils/exception.gd.twig diff --git a/templates/godot/addons/id.gd.twig b/templates/godot/addons/utils/id.gd.twig similarity index 100% rename from templates/godot/addons/id.gd.twig rename to templates/godot/addons/utils/id.gd.twig diff --git a/templates/godot/addons/input_file.gd.twig b/templates/godot/addons/utils/input_file.gd.twig similarity index 100% rename from templates/godot/addons/input_file.gd.twig rename to templates/godot/addons/utils/input_file.gd.twig diff --git a/templates/godot/addons/utils/oauth2.gd.twig b/templates/godot/addons/utils/oauth2.gd.twig new file mode 100644 index 0000000000..5bc5cd534a --- /dev/null +++ b/templates/godot/addons/utils/oauth2.gd.twig @@ -0,0 +1,261 @@ +extends Node + +signal oauth_result(res) + +const PORT := 51419 +var server := TCPServer.new() +var _connection: StreamPeerTCP = null +var _waiting := false +var _request_buffer := "" + +func _ready(): + set_process(false) + +func authenticate(auth_url:String) -> Dictionary: + await ready + if _waiting: + return {"statuscode": 0, "error": "Already waiting for authentication"} + var err := server.listen(PORT) + if err != OK: + return {"statuscode": 0, "error": "Failed starting callback server: %s" % error_string(err)} + + _request_buffer = "" + _waiting = true + set_process(true) + + OS.shell_open(auth_url) + + var res = await oauth_result + return res + +func _process(_delta): + if _connection == null: + if server.is_connection_available(): + _connection = server.take_connection() + return + _connection.poll() + + if _connection.get_status() != StreamPeerTCP.STATUS_CONNECTED: + return + var bytes := _connection.get_available_bytes() + + if bytes > 0: + var chunk := _connection.get_utf8_string(bytes) + _request_buffer += chunk + + if "\r\n\r\n" not in _request_buffer: + return + + var parsed := extract_callback_url(_request_buffer) + + if parsed.is_empty(): + _connection.put_data( + failure_html("No authorization code received") + .to_utf8_buffer() + ) + else: + _connection.put_data( + success_html().to_utf8_buffer() + ) + + cleanup() + oauth_result.emit(parsed) + +func load_svg() -> String: + var file := FileAccess.open("res://icon.svg", FileAccess.READ) + if file == null: + return "" + var svg := file.get_as_text() + svg = svg.replace(" String: + var icon := load_svg() + return """ +HTTP/1.1 200 OK +Content-Type: text/html +Connection: close + + + + + +Authentication Success + + + +
+
+%s +
+
+✓ Success +
+

Authentication Complete

+

+You have successfully signed in. +Return to the game window. +

+
+This tab can now be closed. +
+
+ + +""" % icon + +func failure_html(message := "Authentication failed") -> String: + var icon := load_svg() + return """ +HTTP/1.1 401 Unauthorized +Content-Type: text/html +Connection: close + + + + + +Authentication Failed + + + +
+
+%s +
+
+✕ Failed +
+

Authentication Failed

+

+Something went wrong while signing in. +

+
+%s +
+
+ + +""" % [icon, message] + +func cleanup(): + if _connection != null: + _connection.poll() + if _connection.get_status() == StreamPeerTCP.STATUS_CONNECTED: + _connection.disconnect_from_host() + + if server.is_listening(): + server.stop() + + _connection = null + _request_buffer = "" + _waiting = false + set_process(false) + +func extract_callback_url(request:String) -> Dictionary: + var lines := request.split("\r\n") + if lines.is_empty(): + return {"statuscode": 0, "error": "Invalid request"} + var first_line := lines[0] + var pieces := first_line.split(" ") + if pieces.size() < 2: + return {"statuscode": 0, "error": "Invalid request"} + var path := pieces[1] + return {"statuscode": 200, "body": "http://localhost:%d%s" % [PORT, path]} \ No newline at end of file diff --git a/templates/godot/addons/operator.gd.twig b/templates/godot/addons/utils/operator.gd.twig similarity index 100% rename from templates/godot/addons/operator.gd.twig rename to templates/godot/addons/utils/operator.gd.twig diff --git a/templates/godot/addons/permission.gd.twig b/templates/godot/addons/utils/permission.gd.twig similarity index 100% rename from templates/godot/addons/permission.gd.twig rename to templates/godot/addons/utils/permission.gd.twig diff --git a/templates/godot/addons/query.gd.twig b/templates/godot/addons/utils/query.gd.twig similarity index 100% rename from templates/godot/addons/query.gd.twig rename to templates/godot/addons/utils/query.gd.twig diff --git a/templates/godot/addons/role.gd.twig b/templates/godot/addons/utils/role.gd.twig similarity index 100% rename from templates/godot/addons/role.gd.twig rename to templates/godot/addons/utils/role.gd.twig diff --git a/templates/godot/addons/service.gd.twig b/templates/godot/addons/utils/service.gd.twig similarity index 84% rename from templates/godot/addons/service.gd.twig rename to templates/godot/addons/utils/service.gd.twig index 16ac7f205c..96b856c860 100644 --- a/templates/godot/addons/service.gd.twig +++ b/templates/godot/addons/utils/service.gd.twig @@ -8,7 +8,10 @@ var client: RefCounted func _init(p_client: RefCounted) -> void: client = p_client -func _call(method: String, path: String, headers: Dictionary, params: Dictionary, model_script: Variant = null) -> Variant: +func _call_web(method: String, path: String, params: Dictionary = {}) -> Variant: + return await client.call_web_api(method, path, params) + +func _call(method: String, path: String, headers: Dictionary = {}, params: Dictionary = {}, model_script: Variant = null) -> Variant: var result = await client.call_api(method, path, headers, params) if result.statusCode == 0: diff --git a/tests/languages/godot/test.gd b/tests/languages/godot/test.gd index 6d48ab2f06..4039ffa790 100644 --- a/tests/languages/godot/test.gd +++ b/tests/languages/godot/test.gd @@ -166,18 +166,15 @@ func run_general_tests() -> void: await Appwrite.general.empty() - var url = await Appwrite.general.oauth2( - "clientId", - ["test"], - "123456", - "https://localhost:8080", - "https://localhost:8080" - ) - - print(url) +# response = await Appwrite.general.oauth2( +# "clientId", +# ["test"], +# "123456", +# "https://localhost:8080", +# "https://localhost:8080" +# ) - response = await Appwrite.general.headers() - print_response(response) +# print_response(response) func test_errors() -> void: var response @@ -192,7 +189,7 @@ func test_errors() -> void: print_response(response) response = Appwrite.set_endpoint("htp://cloud.appwrite.io/v1") - print_response(response) + print(response.message) func run_query_tests() -> void: print(AppwriteQuery.equal("released", [true])) @@ -203,17 +200,225 @@ func run_query_tests() -> void: print(AppwriteQuery.greater_than("releasedYear", 1990)) print(AppwriteQuery.search("name", "john")) - print(AppwriteQuery.is_null("name")) print(AppwriteQuery.is_not_null("name")) - print(AppwriteQuery.limit(50)) - print(AppwriteQuery.offset(20)) + print(AppwriteQuery.between("age", 50, 100)) + print(AppwriteQuery.between("age", 50.5, 100.5)) + print(AppwriteQuery.between("name", "Anna", "Brad")) + + print(AppwriteQuery.starts_with("name", "Ann")) + print(AppwriteQuery.ends_with("name", "nne")) + + print(AppwriteQuery.select(["name", "age"])) print(AppwriteQuery.order_asc("title")) print(AppwriteQuery.order_desc("title")) + print(AppwriteQuery.order_random()) + + print(AppwriteQuery.cursor_after("my_movie_id")) + print(AppwriteQuery.cursor_before("my_movie_id")) + + print(AppwriteQuery.limit(50)) + print(AppwriteQuery.offset(20)) print(AppwriteQuery.contains("title", "Spider")) + print(AppwriteQuery.contains("labels", "first")) + print(AppwriteQuery.contains_any("labels", ["first", "second"])) + print(AppwriteQuery.contains_all("labels", ["first", "second"])) + + # New query methods + + print(AppwriteQuery.not_contains("title", "Spider")) + print(AppwriteQuery.not_search("name", "john")) + + print(AppwriteQuery.not_between("age", 50, 100)) + + print(AppwriteQuery.not_starts_with("name", "Ann")) + print(AppwriteQuery.not_ends_with("name", "nne")) + + print(AppwriteQuery.created_before("2023-01-01")) + print(AppwriteQuery.created_after("2023-01-01")) + print(AppwriteQuery.created_between( + "2023-01-01", + "2023-12-31" + )) + + print(AppwriteQuery.updated_before("2023-01-01")) + print(AppwriteQuery.updated_after("2023-01-01")) + print(AppwriteQuery.updated_between( + "2023-01-01", + "2023-12-31" + )) + + # Spatial Distance + + print(AppwriteQuery.distance_equal( + "location", + [[40.7128,-74],[40.7128,-74]], + 1000 + )) + + print(AppwriteQuery.distance_equal( + "location", + [40.7128,-74], + 1000, + true + )) + + print(AppwriteQuery.distance_not_equal( + "location", + [40.7128,-74], + 1000 + )) + + print(AppwriteQuery.distance_not_equal( + "location", + [40.7128,-74], + 1000, + true + )) + + print(AppwriteQuery.distance_greater_than( + "location", + [40.7128,-74], + 1000 + )) + + print(AppwriteQuery.distance_greater_than( + "location", + [40.7128,-74], + 1000, + true + )) + + print(AppwriteQuery.distance_less_than( + "location", + [40.7128,-74], + 1000 + )) + + print(AppwriteQuery.distance_less_than( + "location", + [40.7128,-74], + 1000, + true + )) + + # Spatial queries + + print(AppwriteQuery.intersects( + "location", + [40.7128,-74] + )) + + print(AppwriteQuery.not_intersects( + "location", + [40.7128,-74] + )) + + print(AppwriteQuery.crosses( + "location", + [40.7128,-74] + )) + + print(AppwriteQuery.not_crosses( + "location", + [40.7128,-74] + )) + + print(AppwriteQuery.overlaps( + "location", + [40.7128,-74] + )) + + print(AppwriteQuery.not_overlaps( + "location", + [40.7128,-74] + )) + + print(AppwriteQuery.touches( + "location", + [40.7128,-74] + )) + + print(AppwriteQuery.not_touches( + "location", + [40.7128,-74] + )) + + print(AppwriteQuery.contains( + "location", + [[40.7128,-74],[40.7128,-74]] + )) + + print(AppwriteQuery.not_contains( + "location", + [[40.7128,-74],[40.7128,-74]] + )) + + print(AppwriteQuery.equal( + "location", + [[40.7128,-74],[40.7128,-74]] + )) + + print(AppwriteQuery.not_equal( + "location", + [[40.7128,-74],[40.7128,-74]] + )) + + print(AppwriteQuery.or_query([ + AppwriteQuery.equal( + "released", + true + ), + AppwriteQuery.less_than( + "releasedYear", + 1990 + ) + ])) + + print(AppwriteQuery.and_query([ + AppwriteQuery.equal( + "released", + false + ), + AppwriteQuery.greater_than( + "releasedYear", + 2015 + ) + ])) + + # regex / exists / elemMatch + + print(AppwriteQuery.regex( + "name", + "pattern.*" + )) + + print(AppwriteQuery.exists([ + "attr1", + "attr2" + ])) + + print(AppwriteQuery.not_exists([ + "attr1", + "attr2" + ])) + + print(AppwriteQuery.elem_match( + "friends", + [ + AppwriteQuery.equal( + "name", + "Alice" + ), + AppwriteQuery.greater_than( + "age", + 18 + ) + ] + )) func run_permission_tests() -> void: print(AppwritePermission.read(AppwriteRole.any())) From 733cf3a8b92e172255445f350b02d0e87b5a4206 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Tue, 19 May 2026 20:33:01 +0530 Subject: [PATCH 31/58] test(godot4): testing ci implemented with docker --- templates/godot/docs/example.md.twig | 4 ---- tests/Godot4Test.php | 20 ++++++++++++---- tests/languages/godot/test.gd | 36 ++++++++-------------------- 3 files changed, 25 insertions(+), 35 deletions(-) diff --git a/templates/godot/docs/example.md.twig b/templates/godot/docs/example.md.twig index d0969e683b..935740c959 100644 --- a/templates/godot/docs/example.md.twig +++ b/templates/godot/docs/example.md.twig @@ -21,11 +21,7 @@ func _ready(): {% endfor %} {% endif %} -{% if method.type != 'webAuth' %} var result = await {{prefix}}.{{ service.name | caseCamel }}.{{ method.name | caseSnake | escapeKeyword }}( -{% else %} - var result = {{prefix}}.{{ service.name | caseCamel }}.{{ method.name | caseSnake | escapeKeyword }}( -{% endif %} {% for parameter in method.parameters.all %} {{ parameter | paramExample }}{% if not loop.last %},{% endif %}{% if not parameter.required %} # optional{% endif %} diff --git a/tests/Godot4Test.php b/tests/Godot4Test.php index b56171c9e3..c3eb5592fe 100644 --- a/tests/Godot4Test.php +++ b/tests/Godot4Test.php @@ -11,14 +11,24 @@ class Godot4Test extends Base protected string $language = 'godot'; protected string $class = 'Appwrite\SDK\Language\Godot'; + protected array $build = [ - 'mkdir -p tests/sdks/godot/tests', 'cp tests/languages/godot/test.gd tests/sdks/godot/tests/test.gd', - 'mkdir tests/sdks/godot/tests/resources/', - 'cp -r tests/resources/ tests/sdks/godot/tests/', - 'cd tests/sdks/godot && godot --headless --import --quit', + 'docker run --rm \ + -v $(pwd)/tests/sdks/godot:/app \ + -v $(pwd)/tests/resources:/app/tests/resources:ro \ + -w /app \ + barichello/godot-ci:4.6 \ + godot --headless --import --quit' ]; - protected string $command = 'cd tests/sdks/godot && godot --headless --script tests/test.gd'; + + protected string $command = + 'docker run --network="mockapi" --rm \ + -v $(pwd)/tests/sdks/godot:/app \ + -v $(pwd)/tests/resources:/app/tests/resources:ro \ + -w /app \ + barichello/godot-ci:4.6 \ + godot --headless --script tests/test.gd'; protected array $expectedOutput = [ ...Base::PING_RESPONSE, diff --git a/tests/languages/godot/test.gd b/tests/languages/godot/test.gd index 4039ffa790..d33389fe86 100644 --- a/tests/languages/godot/test.gd +++ b/tests/languages/godot/test.gd @@ -12,10 +12,9 @@ func start() -> void: quit() func run_tests() -> void: - Appwrite.add_header("Origin", "http://localhost:8080") \ + Appwrite.add_header("Origin", "http://localhost") \ .set_project("123456") \ - .set_self_signed(true) \ - .set_endpoint("http://localhost:8080/v1") + .set_self_signed(true) print("\nTest Started") @@ -166,16 +165,6 @@ func run_general_tests() -> void: await Appwrite.general.empty() -# response = await Appwrite.general.oauth2( -# "clientId", -# ["test"], -# "123456", -# "https://localhost:8080", -# "https://localhost:8080" -# ) - -# print_response(response) - func test_errors() -> void: var response @@ -422,20 +411,15 @@ func run_query_tests() -> void: func run_permission_tests() -> void: print(AppwritePermission.read(AppwriteRole.any())) - - print( - AppwritePermission.write( - AppwriteRole.user(AppwriteID.custom("userid")) - ) - ) - + print(AppwritePermission.write(AppwriteRole.user(AppwriteID.custom("userid")))) print(AppwritePermission.create(AppwriteRole.users())) - - print( - AppwritePermission.delete( - AppwriteRole.team("teamId", "owner") - ) - ) + print(AppwritePermission.update(AppwriteRole.guests())) + print(AppwritePermission.delete(AppwriteRole.team("teamId", "owner"))) + print(AppwritePermission.delete(AppwriteRole.team("teamId"))) + print(AppwritePermission.create(AppwriteRole.member("memberId"))) + print(AppwritePermission.update(AppwriteRole.users("verified"))) + print(AppwritePermission.update(AppwriteRole.user(AppwriteID.custom("userid"), "unverified"))) + print(AppwritePermission.create(AppwriteRole.label("admin"))) func run_id_tests() -> void: print(AppwriteID.unique()) From f8ee09de4f2bd06a19ad6a29f9e0d8dbb14c43d7 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Tue, 19 May 2026 22:27:24 +0530 Subject: [PATCH 32/58] refract(godot): fix djlint issues --- templates/godot/addons/utils/oauth2.gd.twig | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/godot/addons/utils/oauth2.gd.twig b/templates/godot/addons/utils/oauth2.gd.twig index 5bc5cd534a..988993b5c2 100644 --- a/templates/godot/addons/utils/oauth2.gd.twig +++ b/templates/godot/addons/utils/oauth2.gd.twig @@ -2,7 +2,7 @@ extends Node signal oauth_result(res) -const PORT := 51419 +const PORT := 8888 var server := TCPServer.new() var _connection: StreamPeerTCP = null var _waiting := false @@ -48,9 +48,9 @@ func _process(_delta): var parsed := extract_callback_url(_request_buffer) - if parsed.is_empty(): + if parsed.is_empty() or parsed["body"].contains("failure"): _connection.put_data( - failure_html("No authorization code received") + failure_html("Something went wrong. Please try again") .to_utf8_buffer() ) else: @@ -66,7 +66,7 @@ func load_svg() -> String: if file == null: return "" var svg := file.get_as_text() - svg = svg.replace(" String: @@ -77,7 +77,7 @@ Content-Type: text/html Connection: close - + Authentication Success @@ -158,7 +158,7 @@ Content-Type: text/html Connection: close - + Authentication Failed From 365382b623c9c9a51f0037fb042230985180a859 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Wed, 20 May 2026 15:20:25 +0530 Subject: [PATCH 33/58] feat(gdscript): restructure SDK layout and improve Godot project support - move core utility templates into addons/utils namespace - reorganize client, service, query, role, id and helper files - add project.godot template for generated GDScript projects - update Appwrite and service templates for new structure - improve OAuth2 implementation in Godot templates - update GDScript and Godot test setup - fix resource handling and project import flow in tests - align generated test scripts with new project structure --- src/SDK/Language/GDScript.php | 41 +- templates/gdscript/addons/appwrite.gd.twig | 22 +- .../gdscript/addons/services/service.gd.twig | 8 +- .../addons/{ => utils}/client.gd.twig | 149 +++++++- .../addons/{ => utils}/exception.gd.twig | 0 .../gdscript/addons/{ => utils}/id.gd.twig | 0 .../addons/{ => utils}/input_file.gd.twig | 0 .../addons/{ => utils}/operator.gd.twig | 0 .../addons/{ => utils}/permission.gd.twig | 0 .../gdscript/addons/{ => utils}/query.gd.twig | 0 .../gdscript/addons/{ => utils}/role.gd.twig | 0 .../addons/{ => utils}/service.gd.twig | 5 +- templates/gdscript/project.godot.twig | 32 ++ templates/godot/addons/utils/oauth2.gd.twig | 8 +- tests/GDScript4Test.php | 17 +- tests/Godot4Test.php | 3 +- tests/languages/gdscript/test.gd | 352 +++++++++++++----- 17 files changed, 507 insertions(+), 130 deletions(-) rename templates/gdscript/addons/{ => utils}/client.gd.twig (58%) rename templates/gdscript/addons/{ => utils}/exception.gd.twig (100%) rename templates/gdscript/addons/{ => utils}/id.gd.twig (100%) rename templates/gdscript/addons/{ => utils}/input_file.gd.twig (100%) rename templates/gdscript/addons/{ => utils}/operator.gd.twig (100%) rename templates/gdscript/addons/{ => utils}/permission.gd.twig (100%) rename templates/gdscript/addons/{ => utils}/query.gd.twig (100%) rename templates/gdscript/addons/{ => utils}/role.gd.twig (100%) rename templates/gdscript/addons/{ => utils}/service.gd.twig (84%) create mode 100644 templates/gdscript/project.godot.twig diff --git a/src/SDK/Language/GDScript.php b/src/SDK/Language/GDScript.php index b5503417f6..9871e29ff4 100644 --- a/src/SDK/Language/GDScript.php +++ b/src/SDK/Language/GDScript.php @@ -213,54 +213,59 @@ public function getFiles(): array ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/client.gd', - 'template' => 'gdscript/addons/client.gd.twig', + 'destination' => 'addons/{{ spec.title | caseSnake }}/utils/client.gd', + 'template' => 'gdscript/addons/utils/client.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/service.gd', - 'template' => 'gdscript/addons/service.gd.twig', + 'destination' => 'addons/{{ spec.title | caseSnake }}/utils/service.gd', + 'template' => 'gdscript/addons/utils/service.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/exception.gd', - 'template' => 'gdscript/addons/exception.gd.twig', + 'destination' => 'addons/{{ spec.title | caseSnake }}/utils/exception.gd', + 'template' => 'gdscript/addons/utils/exception.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/id.gd', - 'template' => 'gdscript/addons/id.gd.twig', + 'destination' => 'addons/{{ spec.title | caseSnake }}/utils/id.gd', + 'template' => 'gdscript/addons/utils/id.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/permission.gd', - 'template' => 'gdscript/addons/permission.gd.twig', + 'destination' => 'addons/{{ spec.title | caseSnake }}/utils/permission.gd', + 'template' => 'gdscript/addons/utils/permission.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/role.gd', - 'template' => 'gdscript/addons/role.gd.twig', + 'destination' => 'addons/{{ spec.title | caseSnake }}/utils/role.gd', + 'template' => 'gdscript/addons/utils/role.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/query.gd', - 'template' => 'gdscript/addons/query.gd.twig', + 'destination' => 'addons/{{ spec.title | caseSnake }}/utils/query.gd', + 'template' => 'gdscript/addons/utils/query.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{ spec.title | caseSnake }}/input_file.gd', - 'template' => 'gdscript/addons/input_file.gd.twig', + 'destination' => 'addons/{{ spec.title | caseSnake }}/utils/input_file.gd', + 'template' => 'gdscript/addons/utils/input_file.gd.twig', ], [ 'scope' => 'default', - 'destination' => 'addons/{{spec.title | caseSnake}}/operator.gd', - 'template' => 'gdscript/addons/operator.gd.twig', + 'destination' => 'addons/{{spec.title | caseSnake}}/utils/operator.gd', + 'template' => 'gdscript/addons/utils/operator.gd.twig', ], [ 'scope' => 'default', 'destination' => '.env', 'template' => 'gdscript/.env.twig', ], + [ + 'scope' => 'default', + 'destination' => 'project.godot', + 'template' => 'gdscript/project.godot.twig', + ], [ 'scope' => 'default', 'destination' => 'tests/test_query.gd', diff --git a/templates/gdscript/addons/appwrite.gd.twig b/templates/gdscript/addons/appwrite.gd.twig index cb487a48a9..cf8c8a9dd2 100644 --- a/templates/gdscript/addons/appwrite.gd.twig +++ b/templates/gdscript/addons/appwrite.gd.twig @@ -9,7 +9,7 @@ signal on_progress(progress: float, total_size: int, uploaded_size: int) const _{{ service.name | caseUpper }} = preload("services/{{ service.name | caseSnake }}.gd") {% endfor %} -var _client := preload("client.gd").new() +var _client := preload("utils/client.gd").new() {% for service in spec.services %} var {{ service.name | caseCamel }} : _{{ service.name | caseUpper }} = _{{ service.name | caseUpper }}.new(_client) ## {{ service.description | default("No description is provided") }} {% endfor %} @@ -49,21 +49,27 @@ func _ready() -> void: {% if header.description %} ## {{header.description}} {% endif %} -func set_{{header.key | caseSnake}}(value: String) -> void: +func set_{{header.key | caseSnake}}(value: String) -> {{ prefix }}: _client.set_{{header.key | caseSnake}}(value) + return self {% endfor %} ## Set self signed status -func set_self_signed(status: bool = true) -> void: +func set_self_signed(status: bool = true) -> {{ prefix }}: _client.set_self_signed(status) + return self ## Set the endpoint -func set_endpoint(endpoint: String) -> void: - _client.set_endpoint(endpoint) +func set_endpoint(endpoint: String) -> Variant: + var res = _client.set_endpoint(endpoint) + if res is {{ prefix }}Exception: + return res + return self ## Add a header -func add_header(key: String, value: String) -> void: +func add_header(key: String, value: String) -> {{ prefix }}: _client.add_header(key, value) + return self ## Get all headers func get_headers() -> Dictionary: @@ -81,6 +87,6 @@ func _apply_env(key: String, value: String) -> void: {% endfor %} ## Ping {{prefix}} Server for testing connection -func ping() -> String: +func ping() -> Variant: var response = await _client.call_api('GET', '/ping') - return str(response.get('body')) \ No newline at end of file + return response.get('body', {}) \ No newline at end of file diff --git a/templates/gdscript/addons/services/service.gd.twig b/templates/gdscript/addons/services/service.gd.twig index eaa5fa89d7..a1339249f1 100644 --- a/templates/gdscript/addons/services/service.gd.twig +++ b/templates/gdscript/addons/services/service.gd.twig @@ -1,5 +1,5 @@ {% set prefix = spec.title | caseUcfirst %} -extends "../service.gd" +extends "../utils/service.gd" ## {{ service.description | default("Service class.") | replace({"\n": "\n## "}) }} {% for method in service.methods %} @@ -79,7 +79,7 @@ func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters. if {{ parameter.name | caseSnake | escapeKeyword }} != null: _params['{{ parameter.name }}'] = {{ parameter.name | caseSnake | escapeKeyword }} {%~ endif %} - {%~ endfor %} + {%~ endfor %} var _headers := { {%~ for key, value in method.headers %} @@ -93,7 +93,11 @@ func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters. var model_script = null {%~ endif %} + {%~ if method.type == 'webAuth' %} + return await _call_web('{{ method.method }}', _path, _params) + {%~ else %} return await _call('{{ method.method }}', _path, _headers, _params, model_script) + {%~ endif %} {%~ endif %} {%~ endfor %} diff --git a/templates/gdscript/addons/client.gd.twig b/templates/gdscript/addons/utils/client.gd.twig similarity index 58% rename from templates/gdscript/addons/client.gd.twig rename to templates/gdscript/addons/utils/client.gd.twig index 0eda09e385..6e90053c51 100644 --- a/templates/gdscript/addons/client.gd.twig +++ b/templates/gdscript/addons/utils/client.gd.twig @@ -23,9 +23,18 @@ func set_self_signed(status: bool = true) -> RefCounted: return self func set_endpoint(endpoint: String) -> RefCounted: - _endpoint = endpoint + if not _is_valid_endpoint(endpoint): + push_error("Invalid endpoint URL: " + endpoint) + return {{prefix}}Exception.new("Invalid endpoint URL: " + endpoint, 0, "invalid_endpoint", "Invalid endpoint URL: " + endpoint) + + _endpoint = endpoint.trim_suffix("/") return self +func _is_valid_endpoint(url: String) -> bool: + var regex := RegEx.new() + regex.compile("^https?://.+") + return regex.search(url) != null + func add_header(key: String, value: String) -> RefCounted: _global_headers[key.to_lower()] = value return self @@ -37,8 +46,93 @@ func get_headers() -> Dictionary: func set_{{header.key | caseSnake}}(value: String) -> RefCounted: _global_headers['{{header.name|lower}}'] = value return self - {% endfor %} + +func redirect(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Variant: + if not _ensure_configured(): + return {{prefix}}Exception.new("Missing/wrong configuration. Please check logs for more info", 400, "config_error", "") + var http := HTTPRequest.new() + http.max_redirects = 0 + + var tls_options := TLSOptions.client_unsafe() if _self_signed else TLSOptions.client() + http.set_tls_options(tls_options) + + Engine.get_main_loop().root.add_child.call_deferred(http) + await http.tree_entered + + # Merge headers + var combined_headers := _global_headers.duplicate() + for key in headers: + combined_headers[key.to_lower()] = headers[key] + + # Choose HTTP method constant + var http_method: int + match method.to_upper(): + "POST": http_method = HTTPClient.METHOD_POST + "PUT": http_method = HTTPClient.METHOD_PUT + "PATCH": http_method = HTTPClient.METHOD_PATCH + "DELETE": http_method = HTTPClient.METHOD_DELETE + _: http_method = HTTPClient.METHOD_GET + + var uri := _endpoint.trim_suffix("/") + var request_path := path + var body := PackedByteArray() + + if http_method == HTTPClient.METHOD_GET: + if not params.is_empty(): + request_path += "?" + _query_string_from_dict(params) + else: + var has_files := false + var large_file_key := "" + for key in params: + if params[key] is {{prefix}}InputFile: + has_files = true + if params[key].get_size() > _chunk_size: + large_file_key = key + break + + if large_file_key != "" and not headers.has("content-range"): + return await _call_chunked(method, path, headers, params, large_file_key) + + if has_files: + var boundary := "Boundary-%x" % Time.get_ticks_msec() + combined_headers["content-type"] = "multipart/form-data; boundary=" + boundary + body = _build_multipart(params, boundary) + else: + var body_str := JSON.stringify(_serialize(params)) + body = body_str.to_utf8_buffer() + combined_headers["content-type"] = "application/json" + + combined_headers["content-length"] = str(body.size()) + + # Build header array + var header_list := PackedStringArray() + for key in combined_headers: + if combined_headers[key] != "": + header_list.append(key + ": " + str(combined_headers[key])) + + var err = http.request_raw(uri + request_path, header_list, http_method, body) + if err != OK: + http.queue_free() + return {{prefix}}Exception.new("Request failed: " + error_string(err), 0, "network_error", "") + + var response = await http.request_completed + http.queue_free() + + var request_result: int = response[0] + var response_code: int = response[1] + + if response_code == 301 or response_code == 302: + for header in response[2]: + if header.to_lower().begins_with("location:"): + return header.substr(9).strip_edges() + + if request_result != HTTPRequest.RESULT_SUCCESS: + return {{prefix}}Exception.new("HTTP Request Error: " + str(request_result), 0, "network_error", "") + + var response_text := (response[3] as PackedByteArray).get_string_from_utf8() + return {{prefix}}Exception.new("Invalid redirect", response_code, "redirect_error", response_text) + func call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: if not _ensure_configured(): return {"statusCode": 400, "body": "Missing/wrong configuration. Please check logs for more info"} @@ -68,10 +162,9 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param var request_path := path var body := PackedByteArray() - if http_method in [HTTPClient.METHOD_GET, HTTPClient.METHOD_DELETE]: + if http_method == HTTPClient.METHOD_GET: if not params.is_empty(): - var query_http := HTTPClient.new() - request_path += "?" + query_http.query_string_from_dict(params) + request_path += "?" + _query_string_from_dict(params) else: var has_files := false var large_file_key := "" @@ -90,7 +183,7 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param combined_headers["content-type"] = "multipart/form-data; boundary=" + boundary body = _build_multipart(params, boundary) else: - var body_str := JSON.stringify(params) + var body_str := JSON.stringify(_serialize(params)) body = body_str.to_utf8_buffer() combined_headers["content-type"] = "application/json" @@ -196,4 +289,46 @@ func _call_chunked(method: String, path: String, headers: Dictionary, params: Di start = end on_progress.emit((float(start) / size) * 100.0, size, start) - return response \ No newline at end of file + return response + +func _query_string_from_dict(p_dict: Dictionary) -> String: + var query := "" + for key in p_dict.keys(): + var value = p_dict[key] + var base_key := str(key) + match typeof(value): + TYPE_ARRAY: + # Always use 'key[]' syntax for arrays, even with a single element + var encoded_key := (base_key + "[]").uri_encode() + for element in value: + query += "&" + encoded_key + "=" + str(element).uri_encode() + TYPE_NIL: + query += "&" + base_key.uri_encode() + _: + query += "&" + base_key.uri_encode() + "=" + str(value).uri_encode() + if query.length() > 0: + query = query.substr(1) + return query + +func _serialize(value): + match typeof(value): + TYPE_ARRAY: + var arr := [] + for item in value: + arr.append(_serialize(item)) + return arr + + TYPE_DICTIONARY: + var dict := {} + for key in value: + dict[key] = _serialize(value[key]) + return dict + + TYPE_OBJECT: + if value != null and value.has_method("to_dict"): + return _serialize(value.to_dict()) + + return value + + _: + return value \ No newline at end of file diff --git a/templates/gdscript/addons/exception.gd.twig b/templates/gdscript/addons/utils/exception.gd.twig similarity index 100% rename from templates/gdscript/addons/exception.gd.twig rename to templates/gdscript/addons/utils/exception.gd.twig diff --git a/templates/gdscript/addons/id.gd.twig b/templates/gdscript/addons/utils/id.gd.twig similarity index 100% rename from templates/gdscript/addons/id.gd.twig rename to templates/gdscript/addons/utils/id.gd.twig diff --git a/templates/gdscript/addons/input_file.gd.twig b/templates/gdscript/addons/utils/input_file.gd.twig similarity index 100% rename from templates/gdscript/addons/input_file.gd.twig rename to templates/gdscript/addons/utils/input_file.gd.twig diff --git a/templates/gdscript/addons/operator.gd.twig b/templates/gdscript/addons/utils/operator.gd.twig similarity index 100% rename from templates/gdscript/addons/operator.gd.twig rename to templates/gdscript/addons/utils/operator.gd.twig diff --git a/templates/gdscript/addons/permission.gd.twig b/templates/gdscript/addons/utils/permission.gd.twig similarity index 100% rename from templates/gdscript/addons/permission.gd.twig rename to templates/gdscript/addons/utils/permission.gd.twig diff --git a/templates/gdscript/addons/query.gd.twig b/templates/gdscript/addons/utils/query.gd.twig similarity index 100% rename from templates/gdscript/addons/query.gd.twig rename to templates/gdscript/addons/utils/query.gd.twig diff --git a/templates/gdscript/addons/role.gd.twig b/templates/gdscript/addons/utils/role.gd.twig similarity index 100% rename from templates/gdscript/addons/role.gd.twig rename to templates/gdscript/addons/utils/role.gd.twig diff --git a/templates/gdscript/addons/service.gd.twig b/templates/gdscript/addons/utils/service.gd.twig similarity index 84% rename from templates/gdscript/addons/service.gd.twig rename to templates/gdscript/addons/utils/service.gd.twig index 16ac7f205c..d37d99e3a6 100644 --- a/templates/gdscript/addons/service.gd.twig +++ b/templates/gdscript/addons/utils/service.gd.twig @@ -8,7 +8,10 @@ var client: RefCounted func _init(p_client: RefCounted) -> void: client = p_client -func _call(method: String, path: String, headers: Dictionary, params: Dictionary, model_script: Variant = null) -> Variant: +func _call_web(method: String, path: String, params: Dictionary = {}) -> Variant: + return await client.redirect(method, path, {}, params) + +func _call(method: String, path: String, headers: Dictionary = {}, params: Dictionary = {}, model_script: Variant = null) -> Variant: var result = await client.call_api(method, path, headers, params) if result.statusCode == 0: diff --git a/templates/gdscript/project.godot.twig b/templates/gdscript/project.godot.twig new file mode 100644 index 0000000000..9e3ee4dde6 --- /dev/null +++ b/templates/gdscript/project.godot.twig @@ -0,0 +1,32 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Appwrite test" +config/features=PackedStringArray("4.6", "GL Compatibility") + +[autoload] + +Appwrite="*res://addons/{{spec.title | caseSnake }}/appwrite.gd" + +[editor_plugins] + +enabled=PackedStringArray("res://addons/{{spec.title | caseSnake }}/plugin.cfg") + +[physics] + +3d/physics_engine="Jolt Physics" + +[rendering] + +rendering_device/driver.windows="d3d12" +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" diff --git a/templates/godot/addons/utils/oauth2.gd.twig b/templates/godot/addons/utils/oauth2.gd.twig index 988993b5c2..979156097b 100644 --- a/templates/godot/addons/utils/oauth2.gd.twig +++ b/templates/godot/addons/utils/oauth2.gd.twig @@ -23,7 +23,13 @@ func authenticate(auth_url:String) -> Dictionary: _waiting = true set_process(true) - OS.shell_open(auth_url) + # Open the authentication URL in the system browser + if OS.has_feature('JavaScript'): + JavaScript.eval(""" + window.open('%s', '_blank').focus(); + """.replace("%s", auth_url)) + else: + OS.shell_open(auth_url) var res = await oauth_result return res diff --git a/tests/GDScript4Test.php b/tests/GDScript4Test.php index db71425899..723c417f7d 100644 --- a/tests/GDScript4Test.php +++ b/tests/GDScript4Test.php @@ -12,12 +12,20 @@ class GDScript4Test extends Base protected string $language = 'gdscript'; protected string $class = 'Appwrite\SDK\Language\GDScript'; protected array $build = [ - 'mkdir -p tests/sdks/gdscript/tests', - 'cp tests/languages/gdscript/test.gd tests/sdks/gdscript/tests/test.gd', - 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/gdscript barichello/godot-ci:4.3 godot --headless --import --quit', + 'cp tests/languages/gdscript/test.gd tests/sdks/gdscript/test.gd', + 'cp -r tests/resources/ tests/sdks/gdscript/tests/', + 'docker run --rm \ + -v $(pwd)/tests/sdks/gdscript:/app \ + -w /app \ + barichello/godot-ci:4.6 \ + godot --headless --import --quit' ]; protected string $command = - 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/gdscript barichello/godot-ci:4.3 godot --headless -s tests/sdks/gdscript/tests/tests.gd'; + 'docker run --network="mockapi" --rm \ + -v $(pwd)/tests/sdks/gdscript:/app \ + -w /app \ + barichello/godot-ci:4.6 \ + godot --headless --script test.gd'; protected array $expectedOutput = [ ...Base::PING_RESPONSE, @@ -28,6 +36,7 @@ class GDScript4Test extends Base ...Base::ENUM_RESPONSES, ...Base::MODEL_RESPONSES, ...Base::EXCEPTION_RESPONSES, + ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, diff --git a/tests/Godot4Test.php b/tests/Godot4Test.php index c3eb5592fe..b7c0e39288 100644 --- a/tests/Godot4Test.php +++ b/tests/Godot4Test.php @@ -14,9 +14,9 @@ class Godot4Test extends Base protected array $build = [ 'cp tests/languages/godot/test.gd tests/sdks/godot/tests/test.gd', + 'cp -r tests/resources/ tests/sdks/godot/tests/', 'docker run --rm \ -v $(pwd)/tests/sdks/godot:/app \ - -v $(pwd)/tests/resources:/app/tests/resources:ro \ -w /app \ barichello/godot-ci:4.6 \ godot --headless --import --quit' @@ -25,7 +25,6 @@ class Godot4Test extends Base protected string $command = 'docker run --network="mockapi" --rm \ -v $(pwd)/tests/sdks/godot:/app \ - -v $(pwd)/tests/resources:/app/tests/resources:ro \ -w /app \ barichello/godot-ci:4.6 \ godot --headless --script tests/test.gd'; diff --git a/tests/languages/gdscript/test.gd b/tests/languages/gdscript/test.gd index 7ab7753622..34ec18ac60 100644 --- a/tests/languages/gdscript/test.gd +++ b/tests/languages/gdscript/test.gd @@ -1,27 +1,24 @@ -extends SceneTree +extends SceneTree + +var _Appwrite = load("res://addons/appwrite/appwrite.gd") +var Appwrite = _Appwrite.new() func _init() -> void: call_deferred("start") - func start() -> void: await run_tests() - print("\nAll tests completed") - quit() - func run_tests() -> void: - Appwrite.add_header("Origin", "http://localhost:8080") - Appwrite.set_self_signed(true) - - Appwrite.set_endpoint("http://localhost:8080/v1") + Appwrite.add_header("Origin", "http://localhost") \ + .set_project("123456") \ + .set_self_signed(true) print("\nTest Started") - var sdk_headers := Appwrite.get_headers() - + var sdk_headers = Appwrite.get_headers() print( "x-sdk-name: %s; x-sdk-platform: %s; x-sdk-language: %s; x-sdk-version: %s" % [ @@ -32,6 +29,7 @@ func run_tests() -> void: ] ) + print_response(await Appwrite.ping()) await run_foo_tests() await run_bar_tests() await run_general_tests() @@ -41,7 +39,6 @@ func run_tests() -> void: run_id_tests() run_operator_tests() - func print_response(response) -> void: if response == null: print("null response") @@ -66,23 +63,20 @@ func print_response(response) -> void: print(response) - func run_foo_tests() -> void: - print_response(await Appwrite.foo.get("string", 123, ["string in array"])) + print_response(await Appwrite.foo.xget("string", 123, ["string in array"])) print_response(await Appwrite.foo.post("string", 123, ["string in array"])) print_response(await Appwrite.foo.put("string", 123, ["string in array"])) print_response(await Appwrite.foo.patch("string", 123, ["string in array"])) print_response(await Appwrite.foo.delete("string", 123, ["string in array"])) - func run_bar_tests() -> void: - print_response(await Appwrite.bar.get("string", 123, ["string in array"])) + print_response(await Appwrite.bar.xget("string", 123, ["string in array"])) print_response(await Appwrite.bar.post("string", 123, ["string in array"])) print_response(await Appwrite.bar.put("string", 123, ["string in array"])) print_response(await Appwrite.bar.patch("string", 123, ["string in array"])) print_response(await Appwrite.bar.delete("string", 123, ["string in array"])) - func run_general_tests() -> void: var response @@ -118,7 +112,6 @@ func run_general_tests() -> void: AppwriteInputFile.from_bytes( file_bytes, "file.png", - "image/png" ) ) @@ -135,17 +128,16 @@ func run_general_tests() -> void: AppwriteInputFile.from_bytes( large_file_bytes, "large_file.mp4", - "video/mp4" ) ) print_response(response) - response = await Appwrite.general.enum(Appwrite.MOCKTYPE.FIRST) + response = await Appwrite.general.xenum(Appwrite.MOCKTYPE.FIRST) print_response(response) response = await Appwrite.general.create_player( - AppwritePlayer.new({ + AppwritePlayer.from_dict({ "id": "player1", "name": "John Doe", "score": 100 @@ -155,17 +147,17 @@ func run_general_tests() -> void: print_response(response) response = await Appwrite.general.create_players([ - { + AppwritePlayer.from_dict({ "id": "player1", "name": "John Doe", "score": 100 - }, - { + }), + AppwritePlayer.from_dict({ "id": "player2", "name": "Jane Doe", "score": 200 - } - ]) + }) + ] as Array[AppwritePlayer]) print_response(response) @@ -173,20 +165,16 @@ func run_general_tests() -> void: await Appwrite.general.empty() - var url := Appwrite.general.oauth2( + response = await Appwrite.general.oauth2( "clientId", ["test"], "123456", "https://localhost", "https://localhost" ) - - print(url) - - response = await Appwrite.general.headers() + print_response(response) - func test_errors() -> void: var response @@ -199,13 +187,8 @@ func test_errors() -> void: response = await Appwrite.general.error502() print_response(response) - var invalid_result = client.set_endpoint("htp://cloud.appwrite.io/v1") - - if invalid_result is AppwriteException: - print(invalid_result.message) - else: - print(invalid_result) - + response = Appwrite.set_endpoint("htp://cloud.appwrite.io/v1") + print(response.message) func run_query_tests() -> void: print(AppwriteQuery.equal("released", [true])) @@ -216,73 +199,268 @@ func run_query_tests() -> void: print(AppwriteQuery.greater_than("releasedYear", 1990)) print(AppwriteQuery.search("name", "john")) - print(AppwriteQuery.is_null("name")) print(AppwriteQuery.is_not_null("name")) - print(AppwriteQuery.limit(50)) - print(AppwriteQuery.offset(20)) + print(AppwriteQuery.between("age", 50, 100)) + print(AppwriteQuery.between("age", 50.5, 100.5)) + print(AppwriteQuery.between("name", "Anna", "Brad")) + + print(AppwriteQuery.starts_with("name", "Ann")) + print(AppwriteQuery.ends_with("name", "nne")) + + print(AppwriteQuery.select(["name", "age"])) print(AppwriteQuery.order_asc("title")) print(AppwriteQuery.order_desc("title")) + print(AppwriteQuery.order_random()) + + print(AppwriteQuery.cursor_after("my_movie_id")) + print(AppwriteQuery.cursor_before("my_movie_id")) + + print(AppwriteQuery.limit(50)) + print(AppwriteQuery.offset(20)) print(AppwriteQuery.contains("title", "Spider")) + print(AppwriteQuery.contains("labels", "first")) + print(AppwriteQuery.contains_any("labels", ["first", "second"])) + print(AppwriteQuery.contains_all("labels", ["first", "second"])) + + # New query methods + + print(AppwriteQuery.not_contains("title", "Spider")) + print(AppwriteQuery.not_search("name", "john")) + + print(AppwriteQuery.not_between("age", 50, 100)) + + print(AppwriteQuery.not_starts_with("name", "Ann")) + print(AppwriteQuery.not_ends_with("name", "nne")) + + print(AppwriteQuery.created_before("2023-01-01")) + print(AppwriteQuery.created_after("2023-01-01")) + print(AppwriteQuery.created_between( + "2023-01-01", + "2023-12-31" + )) + + print(AppwriteQuery.updated_before("2023-01-01")) + print(AppwriteQuery.updated_after("2023-01-01")) + print(AppwriteQuery.updated_between( + "2023-01-01", + "2023-12-31" + )) + + # Spatial Distance + + print(AppwriteQuery.distance_equal( + "location", + [[40.7128,-74],[40.7128,-74]], + 1000 + )) + + print(AppwriteQuery.distance_equal( + "location", + [40.7128,-74], + 1000, + true + )) + + print(AppwriteQuery.distance_not_equal( + "location", + [40.7128,-74], + 1000 + )) + + print(AppwriteQuery.distance_not_equal( + "location", + [40.7128,-74], + 1000, + true + )) + + print(AppwriteQuery.distance_greater_than( + "location", + [40.7128,-74], + 1000 + )) + + print(AppwriteQuery.distance_greater_than( + "location", + [40.7128,-74], + 1000, + true + )) + + print(AppwriteQuery.distance_less_than( + "location", + [40.7128,-74], + 1000 + )) + + print(AppwriteQuery.distance_less_than( + "location", + [40.7128,-74], + 1000, + true + )) + + # Spatial queries + + print(AppwriteQuery.intersects( + "location", + [40.7128,-74] + )) + + print(AppwriteQuery.not_intersects( + "location", + [40.7128,-74] + )) + + print(AppwriteQuery.crosses( + "location", + [40.7128,-74] + )) + + print(AppwriteQuery.not_crosses( + "location", + [40.7128,-74] + )) + + print(AppwriteQuery.overlaps( + "location", + [40.7128,-74] + )) + + print(AppwriteQuery.not_overlaps( + "location", + [40.7128,-74] + )) + + print(AppwriteQuery.touches( + "location", + [40.7128,-74] + )) + + print(AppwriteQuery.not_touches( + "location", + [40.7128,-74] + )) + + print(AppwriteQuery.contains( + "location", + [[40.7128,-74],[40.7128,-74]] + )) + + print(AppwriteQuery.not_contains( + "location", + [[40.7128,-74],[40.7128,-74]] + )) + + print(AppwriteQuery.equal( + "location", + [[40.7128,-74],[40.7128,-74]] + )) + + print(AppwriteQuery.not_equal( + "location", + [[40.7128,-74],[40.7128,-74]] + )) + + print(AppwriteQuery.or_query([ + AppwriteQuery.equal( + "released", + true + ), + AppwriteQuery.less_than( + "releasedYear", + 1990 + ) + ])) - print(AppwriteQuery.or_queries([ - AppwriteQuery.equal("released", true), - AppwriteQuery.less_than("releasedYear", 1990) + print(AppwriteQuery.and_query([ + AppwriteQuery.equal( + "released", + false + ), + AppwriteQuery.greater_than( + "releasedYear", + 2015 + ) ])) + # regex / exists / elemMatch -func run_permission_tests() -> void: - print(AppwritePermission.read(AppwriteRole.any())) + print(AppwriteQuery.regex( + "name", + "pattern.*" + )) - print( - AppwritePermission.write( - AppwriteRole.user(AppwriteID.custom("userid")) - ) - ) + print(AppwriteQuery.exists([ + "attr1", + "attr2" + ])) - print(AppwritePermission.create(AppwriteRole.users())) + print(AppwriteQuery.not_exists([ + "attr1", + "attr2" + ])) - print( - AppwritePermission.delete( - AppwriteRole.team("teamId", "owner") - ) - ) + print(AppwriteQuery.elem_match( + "friends", + [ + AppwriteQuery.equal( + "name", + "Alice" + ), + AppwriteQuery.greater_than( + "age", + 18 + ) + ] + )) +func run_permission_tests() -> void: + print(AppwritePermission.read(AppwriteRole.any())) + print(AppwritePermission.write(AppwriteRole.user(AppwriteID.custom("userid")))) + print(AppwritePermission.create(AppwriteRole.users())) + print(AppwritePermission.update(AppwriteRole.guests())) + print(AppwritePermission.delete(AppwriteRole.team("teamId", "owner"))) + print(AppwritePermission.delete(AppwriteRole.team("teamId"))) + print(AppwritePermission.create(AppwriteRole.member("memberId"))) + print(AppwritePermission.update(AppwriteRole.users("verified"))) + print(AppwritePermission.update(AppwriteRole.user(AppwriteID.custom("userid"), "unverified"))) + print(AppwritePermission.create(AppwriteRole.label("admin"))) func run_id_tests() -> void: print(AppwriteID.unique()) print(AppwriteID.custom("custom_id")) - func run_operator_tests() -> void: - print(Operator.increment()) - print(Operator.increment(5, 100)) - print(Operator.decrement()) - print(Operator.decrement(3, 0)) - print(Operator.multiply(2)) - print(Operator.multiply(3, 1000)) - print(Operator.divide(2)) - print(Operator.divide(4, 1)) - print(Operator.modulo(5)) - print(Operator.power(2)) - print(Operator.power(3, 100)) - print(Operator.array_append(['item1', 'item2'])) - print(Operator.array_prepend(['first', 'second'])) - print(Operator.array_insert(0, 'newItem')) - print(Operator.array_remove('oldItem')) - print(Operator.array_unique()) - print(Operator.array_intersect(['a', 'b', 'c'])) - print(Operator.array_diff(['x', 'y'])) - print(Operator.array_filter(Condition.EQUAL, 'test')) - print(Operator.string_concat('suffix')) - print(Operator.string_replace('old', 'new')) - print(Operator.toggle()) - print(Operator.date_add_days(7)) - print(Operator.date_sub_days(3)) - print(Operator.date_set_now()) - - response = general.headers() - print(response.result) + print(AppwriteOperator.increment()) + print(AppwriteOperator.increment(5, 100)) + print(AppwriteOperator.decrement()) + print(AppwriteOperator.decrement(3, 0)) + print(AppwriteOperator.multiply(2)) + print(AppwriteOperator.multiply(3, 1000)) + print(AppwriteOperator.divide(2)) + print(AppwriteOperator.divide(4, 1)) + print(AppwriteOperator.modulo(5)) + print(AppwriteOperator.power(2)) + print(AppwriteOperator.power(3, 100)) + print(AppwriteOperator.array_append(['item1', 'item2'])) + print(AppwriteOperator.array_prepend(['first', 'second'])) + print(AppwriteOperator.array_insert(0, 'newItem')) + print(AppwriteOperator.array_remove('oldItem')) + print(AppwriteOperator.array_unique()) + print(AppwriteOperator.array_intersect(['a', 'b', 'c'])) + print(AppwriteOperator.array_diff(['x', 'y'])) + print(AppwriteOperator.array_filter(AppwriteOperator.EQUAL, 'test')) + print(AppwriteOperator.string_concat('suffix')) + print(AppwriteOperator.string_replace('old', 'new')) + print(AppwriteOperator.toggle()) + print(AppwriteOperator.date_add_days(7)) + print(AppwriteOperator.date_sub_days(3)) + print(AppwriteOperator.date_set_now()) + + var headers = Appwrite.get_headers() + print(headers) From 9478c4490dfdc5220924341ca78a709eb8f39abb Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Wed, 20 May 2026 15:57:48 +0530 Subject: [PATCH 34/58] ci(godot): add Godot/GDScript CI validation and update docs - add Godot and GDScript SDK build validation workflows - add Godot4 and GDScript4 test execution in CI matrix - update README with Godot/GDScript SDK documentation - update dependencies and refresh composer.lock --- .github/workflows/sdk-build-validation.yml | 6 ++++++ .github/workflows/tests.yml | 2 ++ README.md | 3 +++ composer.lock | 2 +- 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sdk-build-validation.yml b/.github/workflows/sdk-build-validation.yml index c5ac910d43..ea56e9c9a1 100644 --- a/.github/workflows/sdk-build-validation.yml +++ b/.github/workflows/sdk-build-validation.yml @@ -36,6 +36,9 @@ jobs: - sdk: react-native platform: client + - sdk: godot + platform: client + # Server SDKs - sdk: node platform: server @@ -67,6 +70,9 @@ jobs: - sdk: rust platform: server + - sdk: gdscript + platform: server + # Console SDKs - sdk: cli platform: console diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d78c1ba7b0..e23b5f3eff 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,8 +26,10 @@ jobs: DotNet90, FlutterStable, FlutterBeta, + GDScript4, Go112, Go118, + Godot4, KotlinJava8, KotlinJava11, KotlinJava17, diff --git a/README.md b/README.md index 363fa5a34b..70749848e0 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,7 @@ php example.php agent-skills | Apple | `apple` | iOS 15+, macOS 11+, watchOS 7+, tvOS 13+ | [Swift Style Guide] | Swift Package Manager | `examples/apple/` | | Android | `android` | Android 5.0+; Java 17 in CI | [Android style guide] | Gradle, Maven | `examples/android/` | | React Native | `react-native` | React Native >=0.76.7 <1.0.0; Node.js >=18 | [NPM Coding Style] | NPM | `examples/react-native/` | +| Godot | `godot` | Godot 4.6+ | [GDScript Style Guide] | Native | `examples/godot/` | ### Server SDKs @@ -166,6 +167,7 @@ php example.php agent-skills | .NET | `dotnet` | .NET Standard 2.0; .NET Framework 4.6.2 | [C# Coding Conventions] | NuGet | `examples/dotnet/` | | Kotlin | `kotlin` | JVM 1.8 target; Java 17 in CI | [Kotlin style guide] | Gradle, Maven | `examples/kotlin/` | | Rust | `rust` | Rust >=1.83 | [Rust API Guidelines] | Cargo | `examples/rust/` | +| GDScript | `gdscript` | Godot 4.6+ | [GDScript Style Guide] | Native | `examples/gdscript/` | ### Tooling and Documentation @@ -190,6 +192,7 @@ php example.php agent-skills [Kotlin style guide]: https://kotlinlang.org/docs/coding-conventions.html [Android style guide]: https://developer.android.com/kotlin/style-guide [Rust API Guidelines]: https://rust-lang.github.io/api-guidelines/ +[GDScript Style Guide]: https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_styleguide.html ## Contributing diff --git a/composer.lock b/composer.lock index 75e4b3892c..5cca089454 100644 --- a/composer.lock +++ b/composer.lock @@ -450,7 +450,7 @@ }, { "name": "twig/twig", - "version": "v3.14.2", + "version": "3.14.x-dev", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", From 74e9e3cf52ce91939069fd025f3c053d5ff6441c Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Wed, 20 May 2026 17:06:45 +0530 Subject: [PATCH 35/58] fix: resolve rebase conflicts with upstream changes --- .github/workflows/tests.yml | 23 +++-- src/SDK/Language/CursorPlugin.php | 135 +++++------------------------- 2 files changed, 30 insertions(+), 128 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e23b5f3eff..94f877cb27 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,7 @@ jobs: php-version: ['8.3'] sdk: [ Android5Java17, - Android14Java17, + Android16Java17, CLIBun10, CLIBun11, CLIBun13, @@ -30,8 +30,6 @@ jobs: Go112, Go118, Godot4, - KotlinJava8, - KotlinJava11, KotlinJava17, Node16, Node18, @@ -47,23 +45,24 @@ jobs: Ruby30, Ruby31, Rust183, - AppleSwift56, - Swift56, + AppleSwift60, + Swift60, WebChromium, - WebNode + WebNode, + ReactNative ] steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: recursive - name: Docker Setup Buildx - uses: docker/setup-buildx-action@v3.0.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Setup PHP with PECL extension - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 with: php-version: ${{ matrix.php-version }} extensions: curl @@ -93,10 +92,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup PHP with PECL extension - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 with: php-version: '8.3' extensions: curl @@ -112,7 +111,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Make script executable run: chmod +x ./.github/scripts/max-line-length.sh diff --git a/src/SDK/Language/CursorPlugin.php b/src/SDK/Language/CursorPlugin.php index 7316795ac1..3447f31431 100644 --- a/src/SDK/Language/CursorPlugin.php +++ b/src/SDK/Language/CursorPlugin.php @@ -2,9 +2,7 @@ namespace Appwrite\SDK\Language; -use Appwrite\SDK\Language; - -class CursorPlugin extends Language +class CursorPlugin extends AgentSkills { /** * @return string @@ -14,158 +12,63 @@ public function getName(): string return 'CursorPlugin'; } - /** - * @return array - */ - public function getKeywords(): array - { - return []; - } - - /** - * @return array - */ - public function getIdentifierOverrides(): array - { - return []; - } - - /** - * @return string - */ - public function getStaticAccessOperator(): string - { - return '.'; - } - - /** - * @return string - */ - public function getStringQuote(): string - { - return '"'; - } - - /** - * @param string $elements - * @return string - */ - public function getArrayOf(string $elements): string - { - return '[' . $elements . ']'; - } - - /** - * @param array $parameter - * @param array $spec - * @return string - */ - public function getTypeName(array $parameter, array $spec = []): string - { - return $parameter['type'] ?? 'string'; - } - - /** - * @param array $param - * @return string - */ - public function getParamDefault(array $param): string - { - return $param['default'] ?? ''; - } - - /** - * @param array $param - * @param string $lang - * @param array $spec - * @return string - */ - public function getParamExample(array $param, string $lang = '', array $spec = []): string - { - return $param['example'] ?? ''; - } - /** * @return array */ public function getFiles(): array { - // Reuse the same language skills from agent-skills SDK - $languages = [ - 'typescript', - 'dart', - 'kotlin', - 'swift', - 'php', - 'python', - 'ruby', - 'go', - 'rust', - 'dotnet', - 'cli', - ]; - - $files = []; - - // Skills — reuse agent-skills templates - foreach ($languages as $lang) { - $files[] = [ - 'scope' => 'default', - 'destination' => 'skills/{{ spec.title | caseLower }}-' . $lang . '/SKILL.md', - 'template' => 'agent-skills/' . $lang . '.md.twig', - ]; - } + $files = $this->getSkillFiles(); // Logo $files[] = [ - 'scope' => 'copy', + 'scope' => 'copy', 'destination' => 'logo.svg', - 'template' => 'cursor-plugin/logo.svg', + 'template' => 'cursor-plugin/logo.svg', ]; // Plugin manifest $files[] = [ - 'scope' => 'default', + 'scope' => 'default', 'destination' => '.cursor-plugin/plugin.json', - 'template' => 'cursor-plugin/plugin.json.twig', + 'template' => 'cursor-plugin/plugin.json.twig', ]; // Commands $files[] = [ - 'scope' => 'default', + 'scope' => 'default', 'destination' => 'commands/deploy-site.md', - 'template' => 'cursor-plugin/commands/deploy-site.md.twig', + 'template' => 'plugin/commands/deploy-site.md.twig', ]; $files[] = [ - 'scope' => 'default', + 'scope' => 'default', 'destination' => 'commands/deploy-function.md', - 'template' => 'cursor-plugin/commands/deploy-function.md.twig', + 'template' => 'plugin/commands/deploy-function.md.twig', ]; // MCP server definitions $files[] = [ - 'scope' => 'default', + 'scope' => 'default', 'destination' => '.mcp.json', - 'template' => 'cursor-plugin/.mcp.json.twig', + 'template' => 'cursor-plugin/.mcp.json.twig', ]; // README, CHANGELOG, LICENSE $files[] = [ - 'scope' => 'default', + 'scope' => 'default', 'destination' => 'README.md', - 'template' => 'cursor-plugin/README.md.twig', + 'template' => 'cursor-plugin/README.md.twig', ]; $files[] = [ - 'scope' => 'default', + 'scope' => 'default', 'destination' => 'CHANGELOG.md', - 'template' => 'cursor-plugin/CHANGELOG.md.twig', + 'template' => 'cursor-plugin/CHANGELOG.md.twig', ]; $files[] = [ - 'scope' => 'default', + 'scope' => 'default', 'destination' => 'LICENSE', - 'template' => 'cursor-plugin/LICENSE.twig', + 'template' => 'plugin/LICENSE.twig', ]; return $files; } -} +} \ No newline at end of file From 450e849cb2eb318620ff3efb55dba426aad2d559 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Wed, 20 May 2026 17:38:12 +0530 Subject: [PATCH 36/58] fix(godot): align statusCode casing and request model template --- src/SDK/Language/Godot.php | 2 +- templates/gdscript/docs/example.md.twig | 4 ---- templates/godot/addons/utils/client.gd.twig | 4 ++-- templates/godot/addons/utils/oauth2.gd.twig | 10 +++++----- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/SDK/Language/Godot.php b/src/SDK/Language/Godot.php index 1ccd0eaeeb..1407f239df 100644 --- a/src/SDK/Language/Godot.php +++ b/src/SDK/Language/Godot.php @@ -173,7 +173,7 @@ public function getFiles(): array [ 'scope' => 'requestModel', 'destination' => 'addons/{{ spec.title | caseSnake }}/models/{{ requestModel.name | caseSnake }}.gd', - 'template' => 'gdscript/addons/models/request_model.gd.twig', + 'template' => 'godot/addons/models/request_model.gd.twig', ], [ 'scope' => 'method', diff --git a/templates/gdscript/docs/example.md.twig b/templates/gdscript/docs/example.md.twig index d0969e683b..935740c959 100644 --- a/templates/gdscript/docs/example.md.twig +++ b/templates/gdscript/docs/example.md.twig @@ -21,11 +21,7 @@ func _ready(): {% endfor %} {% endif %} -{% if method.type != 'webAuth' %} var result = await {{prefix}}.{{ service.name | caseCamel }}.{{ method.name | caseSnake | escapeKeyword }}( -{% else %} - var result = {{prefix}}.{{ service.name | caseCamel }}.{{ method.name | caseSnake | escapeKeyword }}( -{% endif %} {% for parameter in method.parameters.all %} {{ parameter | paramExample }}{% if not loop.last %},{% endif %}{% if not parameter.required %} # optional{% endif %} diff --git a/templates/godot/addons/utils/client.gd.twig b/templates/godot/addons/utils/client.gd.twig index b2f8dfbca5..c0e1735578 100644 --- a/templates/godot/addons/utils/client.gd.twig +++ b/templates/godot/addons/utils/client.gd.twig @@ -53,7 +53,7 @@ func set_{{header.key | caseSnake}}(value: String) -> RefCounted: {% endfor %} func call_web_api(method: String, path: String = "", params: Dictionary = {}) -> Dictionary: if not _ensure_configured(): - return {"statuscode": 400, "body": "missing/wrong configuration. please check logs for more info"} + return {"statusCode": 400, "body": "missing/wrong configuration. please check logs for more info"} var oauth_client = load("res://addons/{{ spec.title | caseSnake }}/utils/oauth2.gd").new() Engine.get_main_loop().root.add_child.call_deferred(oauth_client) @@ -66,7 +66,7 @@ func call_web_api(method: String, path: String = "", params: Dictionary = {}) -> func call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: if not _ensure_configured(): - return {"statuscode": 400, "body": "missing/wrong configuration. please check logs for more info"} + return {"statusCode": 400, "body": "missing/wrong configuration. please check logs for more info"} var http := HTTPRequest.new() var tls_options := TLSOptions.client_unsafe() if _self_signed else TLSOptions.client() diff --git a/templates/godot/addons/utils/oauth2.gd.twig b/templates/godot/addons/utils/oauth2.gd.twig index 979156097b..beed5eaba4 100644 --- a/templates/godot/addons/utils/oauth2.gd.twig +++ b/templates/godot/addons/utils/oauth2.gd.twig @@ -14,10 +14,10 @@ func _ready(): func authenticate(auth_url:String) -> Dictionary: await ready if _waiting: - return {"statuscode": 0, "error": "Already waiting for authentication"} + return {"statusCode": 0, "error": "Already waiting for authentication"} var err := server.listen(PORT) if err != OK: - return {"statuscode": 0, "error": "Failed starting callback server: %s" % error_string(err)} + return {"statusCode": 0, "error": "Failed starting callback server: %s" % error_string(err)} _request_buffer = "" _waiting = true @@ -258,10 +258,10 @@ func cleanup(): func extract_callback_url(request:String) -> Dictionary: var lines := request.split("\r\n") if lines.is_empty(): - return {"statuscode": 0, "error": "Invalid request"} + return {"statusCode": 0, "error": "Invalid request"} var first_line := lines[0] var pieces := first_line.split(" ") if pieces.size() < 2: - return {"statuscode": 0, "error": "Invalid request"} + return {"statusCode": 0, "error": "Invalid request"} var path := pieces[1] - return {"statuscode": 200, "body": "http://localhost:%d%s" % [PORT, path]} \ No newline at end of file + return {"statusCode": 200, "body": "http://localhost:%d%s" % [PORT, path]} \ No newline at end of file From ebcdee97d4589e3fbf79c62a3acfe4180eca2dfe Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Wed, 20 May 2026 18:38:04 +0530 Subject: [PATCH 37/58] fix(gdscript): normalize ping errors and fix multipart edge cases - route ping() through service error handling - return AppwriteException consistently on failures - avoid malformed multipart CRLF for empty arrays - clean up remaining GDScript generator issues --- src/SDK/Language/GDScript.php | 7 ------ src/SDK/Language/Godot.php | 2 +- templates/gdscript/addons/appwrite.gd.twig | 23 +++++++++++++++++++ .../gdscript/addons/utils/client.gd.twig | 14 +++++------ templates/godot/addons/appwrite.gd.twig | 23 +++++++++++++++++++ templates/godot/addons/utils/client.gd.twig | 14 +++++------ 6 files changed, 59 insertions(+), 24 deletions(-) diff --git a/src/SDK/Language/GDScript.php b/src/SDK/Language/GDScript.php index 9871e29ff4..501abdfa59 100644 --- a/src/SDK/Language/GDScript.php +++ b/src/SDK/Language/GDScript.php @@ -413,10 +413,6 @@ public function getParamDefault(array $param): string case self::TYPE_ARRAY: $output .= '[]'; break; - case self::TYPE_OBJECT: - case self::TYPE_FILE: - $output .= '{}'; - break; default: $output .= 'null'; break; @@ -439,9 +435,6 @@ public function getParamDefault(array $param): string case self::TYPE_OBJECT: $output .= $default; break; - case self::TYPE_FILE: - $output .= '{}'; - break; case self::TYPE_BOOLEAN: $output .= ($default) ? 'true' : 'false'; break; diff --git a/src/SDK/Language/Godot.php b/src/SDK/Language/Godot.php index 1407f239df..5f3b268cf2 100644 --- a/src/SDK/Language/Godot.php +++ b/src/SDK/Language/Godot.php @@ -207,4 +207,4 @@ public function getFiles(): array ] ]; } -} \ No newline at end of file +} diff --git a/templates/gdscript/addons/appwrite.gd.twig b/templates/gdscript/addons/appwrite.gd.twig index cf8c8a9dd2..83938b8a79 100644 --- a/templates/gdscript/addons/appwrite.gd.twig +++ b/templates/gdscript/addons/appwrite.gd.twig @@ -89,4 +89,27 @@ func _apply_env(key: String, value: String) -> void: ## Ping {{prefix}} Server for testing connection func ping() -> Variant: var response = await _client.call_api('GET', '/ping') + + if response.statusCode == 0: + var error_msg = response.get("error", "Unknown error") + push_error("{{prefix}} Network Error: %s" % error_msg) + return {{prefix}}Exception.new(error_msg, 0, "network_error", "") + + if response.statusCode >= 400: + var message = "" + var code = response.statusCode + var type = "" + var response_str = "" + if response.get("body") is Dictionary: + message = response.body.get("message", "") + code = response.body.get("code", response.statusCode) + type = response.body.get("type", "") + response_str = str(response.body) + else: + message = str(response.get("body", "")) + response_str = str(response.get("body", "")) + + push_error("{{prefix}} Error (%s): %s" % [type, message]) + return {{prefix}}Exception.new(message, code, type, response_str) + return response.get('body', {}) \ No newline at end of file diff --git a/templates/gdscript/addons/utils/client.gd.twig b/templates/gdscript/addons/utils/client.gd.twig index 6e90053c51..41953bc497 100644 --- a/templates/gdscript/addons/utils/client.gd.twig +++ b/templates/gdscript/addons/utils/client.gd.twig @@ -233,20 +233,18 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: var c_type = value.content_type if value.content_type != "" else "application/octet-stream" body.append_array(("Content-Type: %s\r\n\r\n" % c_type).to_utf8_buffer()) body.append_array(data) + body.append_array(("\r\n").to_utf8_buffer()) elif value is Array: - for i in range(value.size()): - if i == 0: - body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) - else: - body.append_array(("\r\n--" + boundary + "\r\n").to_utf8_buffer()) + for item in value: + body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) body.append_array(("Content-Disposition: form-data; name=\"%s[]\"\r\n\r\n" % key).to_utf8_buffer()) - body.append_array((str(value[i])).to_utf8_buffer()) + body.append_array((str(item)).to_utf8_buffer()) + body.append_array(("\r\n").to_utf8_buffer()) else: body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) body.append_array(("Content-Disposition: form-data; name=\"%s\"\r\n\r\n" % key).to_utf8_buffer()) body.append_array((str(value)).to_utf8_buffer()) - - body.append_array(("\r\n").to_utf8_buffer()) + body.append_array(("\r\n").to_utf8_buffer()) body.append_array(("--" + boundary + "--\r\n").to_utf8_buffer()) return body diff --git a/templates/godot/addons/appwrite.gd.twig b/templates/godot/addons/appwrite.gd.twig index cf8c8a9dd2..83938b8a79 100644 --- a/templates/godot/addons/appwrite.gd.twig +++ b/templates/godot/addons/appwrite.gd.twig @@ -89,4 +89,27 @@ func _apply_env(key: String, value: String) -> void: ## Ping {{prefix}} Server for testing connection func ping() -> Variant: var response = await _client.call_api('GET', '/ping') + + if response.statusCode == 0: + var error_msg = response.get("error", "Unknown error") + push_error("{{prefix}} Network Error: %s" % error_msg) + return {{prefix}}Exception.new(error_msg, 0, "network_error", "") + + if response.statusCode >= 400: + var message = "" + var code = response.statusCode + var type = "" + var response_str = "" + if response.get("body") is Dictionary: + message = response.body.get("message", "") + code = response.body.get("code", response.statusCode) + type = response.body.get("type", "") + response_str = str(response.body) + else: + message = str(response.get("body", "")) + response_str = str(response.get("body", "")) + + push_error("{{prefix}} Error (%s): %s" % [type, message]) + return {{prefix}}Exception.new(message, code, type, response_str) + return response.get('body', {}) \ No newline at end of file diff --git a/templates/godot/addons/utils/client.gd.twig b/templates/godot/addons/utils/client.gd.twig index c0e1735578..de2c8f878f 100644 --- a/templates/godot/addons/utils/client.gd.twig +++ b/templates/godot/addons/utils/client.gd.twig @@ -172,20 +172,18 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: var c_type = value.content_type if value.content_type != "" else "application/octet-stream" body.append_array(("Content-Type: %s\r\n\r\n" % c_type).to_utf8_buffer()) body.append_array(data) + body.append_array(("\r\n").to_utf8_buffer()) elif value is Array: - for i in range(value.size()): - if i == 0: - body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) - else: - body.append_array(("\r\n--" + boundary + "\r\n").to_utf8_buffer()) + for item in value: + body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) body.append_array(("Content-Disposition: form-data; name=\"%s[]\"\r\n\r\n" % key).to_utf8_buffer()) - body.append_array((str(value[i])).to_utf8_buffer()) + body.append_array((str(item)).to_utf8_buffer()) + body.append_array(("\r\n").to_utf8_buffer()) else: body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) body.append_array(("Content-Disposition: form-data; name=\"%s\"\r\n\r\n" % key).to_utf8_buffer()) body.append_array((str(value)).to_utf8_buffer()) - - body.append_array(("\r\n").to_utf8_buffer()) + body.append_array(("\r\n").to_utf8_buffer()) body.append_array(("--" + boundary + "--\r\n").to_utf8_buffer()) return body From f9fa4cd9acee9cd73a90564566e240b1551ddf4a Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Wed, 20 May 2026 20:38:23 +0530 Subject: [PATCH 38/58] fix(godot): resolve OAuth callback and web API issues --- src/SDK/Language/GDScript.php | 3 +++ templates/godot/addons/utils/oauth2.gd.twig | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/SDK/Language/GDScript.php b/src/SDK/Language/GDScript.php index 501abdfa59..9c4856a4b0 100644 --- a/src/SDK/Language/GDScript.php +++ b/src/SDK/Language/GDScript.php @@ -413,6 +413,9 @@ public function getParamDefault(array $param): string case self::TYPE_ARRAY: $output .= '[]'; break; + case self::TYPE_OBJECT: + $output .= '{}'; + break; default: $output .= 'null'; break; diff --git a/templates/godot/addons/utils/oauth2.gd.twig b/templates/godot/addons/utils/oauth2.gd.twig index beed5eaba4..97fbc55755 100644 --- a/templates/godot/addons/utils/oauth2.gd.twig +++ b/templates/godot/addons/utils/oauth2.gd.twig @@ -24,8 +24,8 @@ func authenticate(auth_url:String) -> Dictionary: set_process(true) # Open the authentication URL in the system browser - if OS.has_feature('JavaScript'): - JavaScript.eval(""" + if OS.has_feature('web'): + JavaScriptBridge.eval(""" window.open('%s', '_blank').focus(); """.replace("%s", auth_url)) else: @@ -54,9 +54,9 @@ func _process(_delta): var parsed := extract_callback_url(_request_buffer) - if parsed.is_empty() or parsed["body"].contains("failure"): + if parsed.is_empty() or parsed.has("error") or parsed.get("body", "").contains("failure"): _connection.put_data( - failure_html("Something went wrong. Please try again") + failure_html(parsed.get("error", "Something went wrong. Please try again")) .to_utf8_buffer() ) else: From 0bc30b9587f5b444f6fb8cd62f7103c3d99350e3 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Wed, 20 May 2026 21:16:52 +0530 Subject: [PATCH 39/58] fix: use filename instead of path.get_file() --- templates/gdscript/addons/utils/input_file.gd.twig | 2 +- templates/godot/addons/utils/input_file.gd.twig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/gdscript/addons/utils/input_file.gd.twig b/templates/gdscript/addons/utils/input_file.gd.twig index ffa6c47e68..7fcfb77193 100644 --- a/templates/gdscript/addons/utils/input_file.gd.twig +++ b/templates/gdscript/addons/utils/input_file.gd.twig @@ -14,7 +14,7 @@ func _init(file_path: String = "", bytes_data: PackedByteArray = PackedByteArray if self.filename == "" and self.path != "": self.filename = self.path.get_file() - self.content_type = _guess_mime_type(self.path.get_file()) + self.content_type = _guess_mime_type(self.filename) ## Creates a new {{prefix}}InputFile from a file path static func from_path(file_path: String, custom_filename: String = "") -> {{prefix}}InputFile: diff --git a/templates/godot/addons/utils/input_file.gd.twig b/templates/godot/addons/utils/input_file.gd.twig index ffa6c47e68..7fcfb77193 100644 --- a/templates/godot/addons/utils/input_file.gd.twig +++ b/templates/godot/addons/utils/input_file.gd.twig @@ -14,7 +14,7 @@ func _init(file_path: String = "", bytes_data: PackedByteArray = PackedByteArray if self.filename == "" and self.path != "": self.filename = self.path.get_file() - self.content_type = _guess_mime_type(self.path.get_file()) + self.content_type = _guess_mime_type(self.filename) ## Creates a new {{prefix}}InputFile from a file path static func from_path(file_path: String, custom_filename: String = "") -> {{prefix}}InputFile: From b78162dfbca9709f45c818012e4e6828c7566005 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Wed, 20 May 2026 21:49:30 +0530 Subject: [PATCH 40/58] refract: replaced hardcoded appwrite in project.godot --- src/SDK/Language/GDScript.php | 5 ----- templates/gdscript/project.godot.twig | 4 ++-- templates/godot/project.godot.twig | 4 ++-- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/SDK/Language/GDScript.php b/src/SDK/Language/GDScript.php index 9c4856a4b0..d74b2af998 100644 --- a/src/SDK/Language/GDScript.php +++ b/src/SDK/Language/GDScript.php @@ -51,7 +51,6 @@ public function getKeywords(): array 'breakpoint', 'preload', 'await', - 'yield', 'assert', 'void', 'PI', @@ -61,12 +60,8 @@ public function getKeywords(): array 'and', 'or', 'not', - 'master', '@export', '@onready', - 'puppet', - 'remote', - 'remotesync', '@tool', '_ready', 'name', diff --git a/templates/gdscript/project.godot.twig b/templates/gdscript/project.godot.twig index 9e3ee4dde6..5872f7c3e7 100644 --- a/templates/gdscript/project.godot.twig +++ b/templates/gdscript/project.godot.twig @@ -10,12 +10,12 @@ config_version=5 [application] -config/name="Appwrite test" +config/name="{{spec.title | caseUcfirst}} test" config/features=PackedStringArray("4.6", "GL Compatibility") [autoload] -Appwrite="*res://addons/{{spec.title | caseSnake }}/appwrite.gd" +{{spec.title | caseUcfirst}}="*res://addons/{{spec.title | caseSnake }}/{{spec.title | caseSnake}}.gd" [editor_plugins] diff --git a/templates/godot/project.godot.twig b/templates/godot/project.godot.twig index c6693cc4ad..a946d467df 100755 --- a/templates/godot/project.godot.twig +++ b/templates/godot/project.godot.twig @@ -10,14 +10,14 @@ config_version=5 [application] -config/name="Appwrite test" +config/name="{{spec.title | caseUcfirst}} test" run/main_scene="res://Menu.tscn" config/features=PackedStringArray("4.6", "GL Compatibility") config/icon="res://icon.svg" [autoload] -Appwrite="*res://addons/{{spec.title | caseSnake }}/appwrite.gd" +{{spec.title | caseUcfirst}}="*res://addons/{{spec.title | caseSnake }}/{{spec.title | caseSnake}}.gd" [editor_plugins] From 2dd045b97e8781b040548be42d29a5b6ee2cc602 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Wed, 20 May 2026 22:13:26 +0530 Subject: [PATCH 41/58] fix: added timeout to oauth flow --- templates/gdscript/addons/services/service.gd.twig | 2 +- templates/godot/addons/services/service.gd.twig | 2 +- templates/godot/addons/utils/oauth2.gd.twig | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/templates/gdscript/addons/services/service.gd.twig b/templates/gdscript/addons/services/service.gd.twig index a1339249f1..7e64b5eef2 100644 --- a/templates/gdscript/addons/services/service.gd.twig +++ b/templates/gdscript/addons/services/service.gd.twig @@ -44,7 +44,7 @@ func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters. {%~ set type = parameter | typeName(spec) %} {%~ if type != 'Variant' %} {%~ if type starts with 'Array' %}{% set check_type = 'Array' %}{% else %}{% set check_type = type %}{% endif %} - if {{ parameter.name | caseSnake | escapeKeyword }} != null and not {{ parameter.name | caseSnake | escapeKeyword }} is {{ check_type }}: + if {{ parameter.name | caseSnake | escapeKeyword }} != null and not {{ parameter.name | caseSnake | escapeKeyword }} is {{ check_type }}{% if check_type == 'float' %} and not typeof({{ parameter.name | caseSnake | escapeKeyword }}) == TYPE_INT{% endif %}: return {{prefix}}Exception.new("Invalid type for parameter '{{ parameter.name | caseSnake | escapeKeyword }}'. Expected {{ type }}.", 0, "argument_error", "") {%~ endif %} {%~ endif %} diff --git a/templates/godot/addons/services/service.gd.twig b/templates/godot/addons/services/service.gd.twig index a1339249f1..7e64b5eef2 100644 --- a/templates/godot/addons/services/service.gd.twig +++ b/templates/godot/addons/services/service.gd.twig @@ -44,7 +44,7 @@ func {{ methodNameSnake| escapeKeyword }}({% for parameter in method.parameters. {%~ set type = parameter | typeName(spec) %} {%~ if type != 'Variant' %} {%~ if type starts with 'Array' %}{% set check_type = 'Array' %}{% else %}{% set check_type = type %}{% endif %} - if {{ parameter.name | caseSnake | escapeKeyword }} != null and not {{ parameter.name | caseSnake | escapeKeyword }} is {{ check_type }}: + if {{ parameter.name | caseSnake | escapeKeyword }} != null and not {{ parameter.name | caseSnake | escapeKeyword }} is {{ check_type }}{% if check_type == 'float' %} and not typeof({{ parameter.name | caseSnake | escapeKeyword }}) == TYPE_INT{% endif %}: return {{prefix}}Exception.new("Invalid type for parameter '{{ parameter.name | caseSnake | escapeKeyword }}'. Expected {{ type }}.", 0, "argument_error", "") {%~ endif %} {%~ endif %} diff --git a/templates/godot/addons/utils/oauth2.gd.twig b/templates/godot/addons/utils/oauth2.gd.twig index 97fbc55755..7e36acdd63 100644 --- a/templates/godot/addons/utils/oauth2.gd.twig +++ b/templates/godot/addons/utils/oauth2.gd.twig @@ -31,6 +31,13 @@ func authenticate(auth_url:String) -> Dictionary: else: OS.shell_open(auth_url) + var timer := get_tree().create_timer(120.0) + timer.timeout.connect(func(): + if _waiting: + cleanup() + oauth_result.emit({"statusCode": 0, "error": "Authentication timed out"}) + ) + var res = await oauth_result return res From b825d39fd96460dccbf036333a2eea5303c84e70 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Wed, 20 May 2026 23:39:43 +0530 Subject: [PATCH 42/58] chore(godot): remove project template environment artifacts --- templates/gdscript/project.godot.twig | 5 ----- templates/godot/project.godot.twig | 5 ----- 2 files changed, 10 deletions(-) diff --git a/templates/gdscript/project.godot.twig b/templates/gdscript/project.godot.twig index 5872f7c3e7..b1a8a4b0a2 100644 --- a/templates/gdscript/project.godot.twig +++ b/templates/gdscript/project.godot.twig @@ -11,7 +11,6 @@ config_version=5 [application] config/name="{{spec.title | caseUcfirst}} test" -config/features=PackedStringArray("4.6", "GL Compatibility") [autoload] @@ -21,10 +20,6 @@ config/features=PackedStringArray("4.6", "GL Compatibility") enabled=PackedStringArray("res://addons/{{spec.title | caseSnake }}/plugin.cfg") -[physics] - -3d/physics_engine="Jolt Physics" - [rendering] rendering_device/driver.windows="d3d12" diff --git a/templates/godot/project.godot.twig b/templates/godot/project.godot.twig index a946d467df..f3db2d1bda 100755 --- a/templates/godot/project.godot.twig +++ b/templates/godot/project.godot.twig @@ -12,7 +12,6 @@ config_version=5 config/name="{{spec.title | caseUcfirst}} test" run/main_scene="res://Menu.tscn" -config/features=PackedStringArray("4.6", "GL Compatibility") config/icon="res://icon.svg" [autoload] @@ -23,10 +22,6 @@ config/icon="res://icon.svg" enabled=PackedStringArray("res://addons/{{spec.title | caseSnake }}/plugin.cfg") -[physics] - -3d/physics_engine="Jolt Physics" - [rendering] rendering_device/driver.windows="d3d12" From f8ec0e607bb5786f62afa8740fb49cad4fc0208a Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Wed, 20 May 2026 23:56:57 +0530 Subject: [PATCH 43/58] fix(godot): cleanup OAuth nodes after authentication --- templates/godot/addons/utils/client.gd.twig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/templates/godot/addons/utils/client.gd.twig b/templates/godot/addons/utils/client.gd.twig index de2c8f878f..aa128e76ee 100644 --- a/templates/godot/addons/utils/client.gd.twig +++ b/templates/godot/addons/utils/client.gd.twig @@ -62,6 +62,10 @@ func call_web_api(method: String, path: String = "", params: Dictionary = {}) -> var uri := _endpoint.trim_suffix("/") var request_path := path + (("?" + _query_string_from_dict(params)) if not params.is_empty() else "") var res = await oauth_client.authenticate(uri+request_path) + + if is_instance_valid(oauth_client): + oauth_client.queue_free() + return res func call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: From 202ddd27e786009426cdaebd4a76e768a0917082 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Thu, 21 May 2026 15:17:57 +0530 Subject: [PATCH 44/58] fix(godot): wrap OAuth web errors and refactor request handling --- .../gdscript/addons/utils/client.gd.twig | 116 +++++------------- .../gdscript/addons/utils/service.gd.twig | 9 +- templates/godot/addons/utils/client.gd.twig | 3 +- templates/godot/addons/utils/service.gd.twig | 9 +- 4 files changed, 46 insertions(+), 91 deletions(-) diff --git a/templates/gdscript/addons/utils/client.gd.twig b/templates/gdscript/addons/utils/client.gd.twig index 41953bc497..1430fedef5 100644 --- a/templates/gdscript/addons/utils/client.gd.twig +++ b/templates/gdscript/addons/utils/client.gd.twig @@ -48,15 +48,17 @@ func set_{{header.key | caseSnake}}(value: String) -> RefCounted: return self {% endfor %} -func redirect(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Variant: +func _http_request(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}, max_redirects: int = 8) -> Variant: if not _ensure_configured(): - return {{prefix}}Exception.new("Missing/wrong configuration. Please check logs for more info", 400, "config_error", "") - var http := HTTPRequest.new() - http.max_redirects = 0 + return {"statusCode": 400, "body": "Missing/wrong configuration. Please check logs for more info"} + # Create HTTP request + var http := HTTPRequest.new() + http.max_redirects = max_redirects var tls_options := TLSOptions.client_unsafe() if _self_signed else TLSOptions.client() http.set_tls_options(tls_options) + # Add http request to the tree Engine.get_main_loop().root.add_child.call_deferred(http) await http.tree_entered @@ -65,7 +67,7 @@ func redirect(method: String, path: String = "", headers: Dictionary = {}, param for key in headers: combined_headers[key.to_lower()] = headers[key] - # Choose HTTP method constant + # Choose http method constant var http_method: int match method.to_upper(): "POST": http_method = HTTPClient.METHOD_POST @@ -73,11 +75,13 @@ func redirect(method: String, path: String = "", headers: Dictionary = {}, param "PATCH": http_method = HTTPClient.METHOD_PATCH "DELETE": http_method = HTTPClient.METHOD_DELETE _: http_method = HTTPClient.METHOD_GET - + + # Build url var uri := _endpoint.trim_suffix("/") var request_path := path var body := PackedByteArray() + # Prepare body and path if http_method == HTTPClient.METHOD_GET: if not params.is_empty(): request_path += "?" + _query_string_from_dict(params) @@ -85,7 +89,7 @@ func redirect(method: String, path: String = "", headers: Dictionary = {}, param var has_files := false var large_file_key := "" for key in params: - if params[key] is {{prefix}}InputFile: + if params[key] is {{ prefix }}InputFile: has_files = true if params[key].get_size() > _chunk_size: large_file_key = key @@ -104,20 +108,27 @@ func redirect(method: String, path: String = "", headers: Dictionary = {}, param combined_headers["content-type"] = "application/json" combined_headers["content-length"] = str(body.size()) - + # Build header array var header_list := PackedStringArray() for key in combined_headers: if combined_headers[key] != "": header_list.append(key + ": " + str(combined_headers[key])) - + + # Make request var err = http.request_raw(uri + request_path, header_list, http_method, body) if err != OK: http.queue_free() - return {{prefix}}Exception.new("Request failed: " + error_string(err), 0, "network_error", "") - + return {"statusCode": 400, "body": "Request failed: " + error_string(err)} + + # Wait for response and clean node from tree on completion var response = await http.request_completed http.queue_free() + + return response + +func redirect(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Variant: + var response = await _http_request(method, path, headers, params, 0) var request_result: int = response[0] var response_code: int = response[1] @@ -125,83 +136,21 @@ func redirect(method: String, path: String = "", headers: Dictionary = {}, param if response_code == 301 or response_code == 302: for header in response[2]: if header.to_lower().begins_with("location:"): - return header.substr(9).strip_edges() + return { "statusCode": 200, "body": header.substr(9).strip_edges() } if request_result != HTTPRequest.RESULT_SUCCESS: - return {{prefix}}Exception.new("HTTP Request Error: " + str(request_result), 0, "network_error", "") + return { "statusCode": 0, "body": "HTTP Request Error: " + str(request_result) } var response_text := (response[3] as PackedByteArray).get_string_from_utf8() - return {{prefix}}Exception.new("Invalid redirect", response_code, "redirect_error", response_text) - -func call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: - if not _ensure_configured(): - return {"statusCode": 400, "body": "Missing/wrong configuration. Please check logs for more info"} - var http := HTTPRequest.new() - - var tls_options := TLSOptions.client_unsafe() if _self_signed else TLSOptions.client() - http.set_tls_options(tls_options) - - Engine.get_main_loop().root.add_child.call_deferred(http) - await http.tree_entered - - # Merge headers - var combined_headers := _global_headers.duplicate() - for key in headers: - combined_headers[key.to_lower()] = headers[key] - - # Choose HTTP method constant - var http_method: int - match method.to_upper(): - "POST": http_method = HTTPClient.METHOD_POST - "PUT": http_method = HTTPClient.METHOD_PUT - "PATCH": http_method = HTTPClient.METHOD_PATCH - "DELETE": http_method = HTTPClient.METHOD_DELETE - _: http_method = HTTPClient.METHOD_GET - - var uri := _endpoint.trim_suffix("/") - var request_path := path - var body := PackedByteArray() - - if http_method == HTTPClient.METHOD_GET: - if not params.is_empty(): - request_path += "?" + _query_string_from_dict(params) - else: - var has_files := false - var large_file_key := "" - for key in params: - if params[key] is {{ prefix }}InputFile: - has_files = true - if params[key].get_size() > _chunk_size: - large_file_key = key - break - - if large_file_key != "" and not headers.has("content-range"): - return await _call_chunked(method, path, headers, params, large_file_key) - - if has_files: - var boundary := "Boundary-%x" % Time.get_ticks_msec() - combined_headers["content-type"] = "multipart/form-data; boundary=" + boundary - body = _build_multipart(params, boundary) - else: - var body_str := JSON.stringify(_serialize(params)) - body = body_str.to_utf8_buffer() - combined_headers["content-type"] = "application/json" - - combined_headers["content-length"] = str(body.size()) - # Build header array - var header_list := PackedStringArray() - for key in combined_headers: - if combined_headers[key] != "": - header_list.append(key + ": " + str(combined_headers[key])) + var json := JSON.new() + if json.parse(response_text) == OK: + return {"statusCode": response_code, "body": json.data} - var err = http.request_raw(uri + request_path, header_list, http_method, body) - if err != OK: - http.queue_free() - return {"statusCode": 0, "error": "Request failed: " + error_string(err)} + return {"statusCode": response_code, "body": response_text} - var response = await http.request_completed - http.queue_free() +func call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: + var response = await _http_request(method, path, headers, params) var request_result: int = response[0] var response_code: int = response[1] @@ -211,11 +160,12 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param return {"statusCode": 0, "error": "HTTP Request Error: " + str(request_result)} var response_text := response_body.get_string_from_utf8() + var json := JSON.new() if json.parse(response_text) == OK: return {"statusCode": response_code, "body": json.data} - else: - return {"statusCode": response_code, "body": response_text} + + return {"statusCode": response_code, "body": response_text} func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: var body := PackedByteArray() diff --git a/templates/gdscript/addons/utils/service.gd.twig b/templates/gdscript/addons/utils/service.gd.twig index d37d99e3a6..38ce79f591 100644 --- a/templates/gdscript/addons/utils/service.gd.twig +++ b/templates/gdscript/addons/utils/service.gd.twig @@ -9,11 +9,14 @@ func _init(p_client: RefCounted) -> void: client = p_client func _call_web(method: String, path: String, params: Dictionary = {}) -> Variant: - return await client.redirect(method, path, {}, params) + var result = await client.redirect(method, path, {}, params) + return _handle_response(result) func _call(method: String, path: String, headers: Dictionary = {}, params: Dictionary = {}, model_script: Variant = null) -> Variant: var result = await client.call_api(method, path, headers, params) - + return _handle_response(result, model_script) + +func _handle_response(result: Dictionary, model_script: Variant = null) -> Variant: if result.statusCode == 0: var error_msg = result.get("error", "Unknown error") push_error("{{prefix}} Network Error: %s" % error_msg) @@ -37,7 +40,7 @@ func _call(method: String, path: String, headers: Dictionary = {}, params: Dicti return {{prefix}}Exception.new(message, code, type, response) if model_script == null: - return result.get("body") + return result.get("body", {}) if result.get("body") is Array: var list: Array[RefCounted] = [] diff --git a/templates/godot/addons/utils/client.gd.twig b/templates/godot/addons/utils/client.gd.twig index aa128e76ee..df0d998e0f 100644 --- a/templates/godot/addons/utils/client.gd.twig +++ b/templates/godot/addons/utils/client.gd.twig @@ -157,8 +157,7 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param var json := JSON.new() if json.parse(response_text) == OK: return {"statusCode": response_code, "body": json.data} - else: - return {"statusCode": response_code, "body": response_text} + return {"statusCode": response_code, "body": response_text} func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: var body := PackedByteArray() diff --git a/templates/godot/addons/utils/service.gd.twig b/templates/godot/addons/utils/service.gd.twig index 96b856c860..a70d46022d 100644 --- a/templates/godot/addons/utils/service.gd.twig +++ b/templates/godot/addons/utils/service.gd.twig @@ -9,11 +9,14 @@ func _init(p_client: RefCounted) -> void: client = p_client func _call_web(method: String, path: String, params: Dictionary = {}) -> Variant: - return await client.call_web_api(method, path, params) + var result = await client.call_web_api(method, path, params) + return _handle_response(result) func _call(method: String, path: String, headers: Dictionary = {}, params: Dictionary = {}, model_script: Variant = null) -> Variant: var result = await client.call_api(method, path, headers, params) - + return _handle_response(result, model_script) + +func _handle_response(result: Dictionary, model_script: Variant = null) -> Variant: if result.statusCode == 0: var error_msg = result.get("error", "Unknown error") push_error("{{prefix}} Network Error: %s" % error_msg) @@ -37,7 +40,7 @@ func _call(method: String, path: String, headers: Dictionary = {}, params: Dicti return {{prefix}}Exception.new(message, code, type, response) if model_script == null: - return result.get("body") + return result.get("body", {}) if result.get("body") is Array: var list: Array[RefCounted] = [] From 8e218a05edc8f72b0b36f428a559ce14cd012537 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Thu, 21 May 2026 15:55:26 +0530 Subject: [PATCH 45/58] fix: error handling during _apply_env and _http_request --- templates/gdscript/addons/appwrite.gd.twig | 4 +++- templates/gdscript/addons/utils/client.gd.twig | 10 ++++++++-- templates/godot/addons/appwrite.gd.twig | 4 +++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/templates/gdscript/addons/appwrite.gd.twig b/templates/gdscript/addons/appwrite.gd.twig index 83938b8a79..f24d8b9119 100644 --- a/templates/gdscript/addons/appwrite.gd.twig +++ b/templates/gdscript/addons/appwrite.gd.twig @@ -78,7 +78,9 @@ func get_headers() -> Dictionary: func _apply_env(key: String, value: String) -> void: match key: "{{ prefix | caseUpper }}_ENDPOINT": - _client.set_endpoint(value) + var err = _client.set_endpoint(value) + if err is {{ prefix }}Exception: + push_error("{{prefix}} Error (%s): %s" % [err.type, err.message]) "{{ prefix | caseUpper }}_SELF_SIGNED": _client.set_self_signed(value.to_lower() == "true") {% for header in spec.global.headers %} diff --git a/templates/gdscript/addons/utils/client.gd.twig b/templates/gdscript/addons/utils/client.gd.twig index 1430fedef5..aeb5d5402e 100644 --- a/templates/gdscript/addons/utils/client.gd.twig +++ b/templates/gdscript/addons/utils/client.gd.twig @@ -22,7 +22,7 @@ func set_self_signed(status: bool = true) -> RefCounted: _self_signed = status return self -func set_endpoint(endpoint: String) -> RefCounted: +func set_endpoint(endpoint: String) -> Variant: if not _is_valid_endpoint(endpoint): push_error("Invalid endpoint URL: " + endpoint) return {{prefix}}Exception.new("Invalid endpoint URL: " + endpoint, 0, "invalid_endpoint", "Invalid endpoint URL: " + endpoint) @@ -129,7 +129,10 @@ func _http_request(method: String, path: String = "", headers: Dictionary = {}, func redirect(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Variant: var response = await _http_request(method, path, headers, params, 0) - + + if response is Dictionary: + return response + var request_result: int = response[0] var response_code: int = response[1] @@ -152,6 +155,9 @@ func redirect(method: String, path: String = "", headers: Dictionary = {}, param func call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: var response = await _http_request(method, path, headers, params) + if response is Dictionary: + return response + var request_result: int = response[0] var response_code: int = response[1] var response_body: PackedByteArray = response[3] diff --git a/templates/godot/addons/appwrite.gd.twig b/templates/godot/addons/appwrite.gd.twig index 83938b8a79..f24d8b9119 100644 --- a/templates/godot/addons/appwrite.gd.twig +++ b/templates/godot/addons/appwrite.gd.twig @@ -78,7 +78,9 @@ func get_headers() -> Dictionary: func _apply_env(key: String, value: String) -> void: match key: "{{ prefix | caseUpper }}_ENDPOINT": - _client.set_endpoint(value) + var err = _client.set_endpoint(value) + if err is {{ prefix }}Exception: + push_error("{{prefix}} Error (%s): %s" % [err.type, err.message]) "{{ prefix | caseUpper }}_SELF_SIGNED": _client.set_self_signed(value.to_lower() == "true") {% for header in spec.global.headers %} From 42928a0bf18f3b7dd2a4c2e1fb550782cb6f2b45 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Thu, 21 May 2026 17:05:15 +0530 Subject: [PATCH 46/58] fix: propagate model exceptions and refract model --- src/SDK/Language/GDScript.php | 2 +- src/SDK/Language/Godot.php | 2 +- .../gdscript/addons/models/model.gd.twig | 31 +++-- .../addons/models/request_model.gd.twig | 108 ------------------ .../gdscript/addons/utils/service.gd.twig | 8 +- templates/godot/addons/models/model.gd.twig | 31 +++-- .../godot/addons/models/request_model.gd.twig | 108 ------------------ templates/godot/addons/utils/service.gd.twig | 10 +- 8 files changed, 53 insertions(+), 247 deletions(-) delete mode 100644 templates/gdscript/addons/models/request_model.gd.twig delete mode 100644 templates/godot/addons/models/request_model.gd.twig diff --git a/src/SDK/Language/GDScript.php b/src/SDK/Language/GDScript.php index d74b2af998..126545eb77 100644 --- a/src/SDK/Language/GDScript.php +++ b/src/SDK/Language/GDScript.php @@ -309,7 +309,7 @@ public function getFiles(): array [ 'scope' => 'requestModel', 'destination' => 'addons/{{ spec.title | caseSnake }}/models/{{ requestModel.name | caseSnake }}.gd', - 'template' => 'gdscript/addons/models/request_model.gd.twig', + 'template' => 'gdscript/addons/models/model.gd.twig', ], [ 'scope' => 'method', diff --git a/src/SDK/Language/Godot.php b/src/SDK/Language/Godot.php index 5f3b268cf2..f5fe4d44f3 100644 --- a/src/SDK/Language/Godot.php +++ b/src/SDK/Language/Godot.php @@ -173,7 +173,7 @@ public function getFiles(): array [ 'scope' => 'requestModel', 'destination' => 'addons/{{ spec.title | caseSnake }}/models/{{ requestModel.name | caseSnake }}.gd', - 'template' => 'godot/addons/models/request_model.gd.twig', + 'template' => 'godot/addons/models/model.gd.twig', ], [ 'scope' => 'method', diff --git a/templates/gdscript/addons/models/model.gd.twig b/templates/gdscript/addons/models/model.gd.twig index 759129967a..ca3bb90c17 100644 --- a/templates/gdscript/addons/models/model.gd.twig +++ b/templates/gdscript/addons/models/model.gd.twig @@ -1,11 +1,12 @@ +{% set model = requestModel ?? definition %} {% set prefix = spec.title | caseUcfirst %} -class_name {{ prefix }}{{ definition.name | caseUcfirst }} +class_name {{ prefix }}{{ model.name | caseUcfirst }} extends RefCounted -## {{ definition.description | default("Model class.") | replace({"\n": "\n## "}) }}[br] +## {{ model.description | default("Model class.") | replace({"\n": "\n## "}) }}[br] {% set imports = [] %} -{%~ for property in definition.properties %} -{%~ set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (definition.name ~ (property.name | caseUcfirst)) %} +{%~ for property in model.properties %} +{%~ set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (model.name ~ (property.name | caseUcfirst)) %} {%~ if property.enum or property.items.enum is defined %} {%~ if enumName not in imports %} const _{{ enumName | caseUcfirst }} := preload("res://addons/{{ spec.title | caseSnake }}/enums/{{ enumName | caseSnake }}.gd") @@ -15,30 +16,33 @@ const _{{ enumName | caseUcfirst }} := preload("res://addons/{{ spec.title | cas {%~ endfor %} const _FIELD_MAP := { -{% for property in definition.properties %} +{% for property in model.properties %} "{{ property.name | uniqueSnake }}": "{{ property.name }}", {% endfor %} } -{% for property in definition.properties %} +{% for property in model.properties %} {% set baseType = property | typeName(spec) %} var {{ property.name | uniqueSnake }}: {{ baseType }} ## {{ property.description | default("No description provided.") }} {% endfor %} ## Convert dictionary to model static func from_dict(dict: Dictionary): - var m := {{ prefix }}{{ definition.name | caseUcfirst }}.new() + var m := {{ prefix }}{{ model.name | caseUcfirst }}.new() for key in _FIELD_MAP: var raw_key = _FIELD_MAP[key] var value = dict.get(raw_key) -{% for property in definition.properties %} +{% for property in model.properties %} {% set field = property.name | uniqueSnake %} -{% set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (definition.name ~ (property.name | caseUcfirst)) %} +{% set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (model.name ~ (property.name | caseUcfirst)) %} {% if property.model %} if key == "{{ field }}" and value is Dictionary: - m.set(key, {{ prefix }}{{ property.model | caseUcfirst }}.from_dict(value)) + var nested = {{ prefix }}{{ property.model | caseUcfirst }}.from_dict(value) + if nested is {{prefix}}Exception: + return nested + m.set(key, nested) continue {% endif %} {% if property.array.model %} @@ -46,7 +50,10 @@ static func from_dict(dict: Dictionary): var list := [] for item in value: if item is Dictionary: - list.append({{ prefix }}{{ property.array.model | caseUcfirst }}.from_dict(item)) + var nested = {{ prefix }}{{ property.array.model | caseUcfirst }}.from_dict(item) + if nested is {{prefix}}Exception: + return nested + list.append(nested) else: list.append(item) m.set(key, list) @@ -87,7 +94,7 @@ func to_dict() -> Dictionary: for key in _FIELD_MAP: var value = get(key) -{% for property in definition.properties %} +{% for property in model.properties %} {% set field = property.name | uniqueSnake %} {% if property.model %} if key == "{{ field }}" and value != null: diff --git a/templates/gdscript/addons/models/request_model.gd.twig b/templates/gdscript/addons/models/request_model.gd.twig deleted file mode 100644 index 44ecc6041a..0000000000 --- a/templates/gdscript/addons/models/request_model.gd.twig +++ /dev/null @@ -1,108 +0,0 @@ -{% set prefix = spec.title | caseUcfirst %} -class_name {{ prefix }}{{ requestModel.name | caseUcfirst }} -extends RefCounted -## {{ requestModel.description | default("Request model class.") | replace({"\n": "\n## "}) }}[br] - -{% set imports = [] %} -{%~ for property in requestModel.properties %} -{%~ set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (requestModel.name ~ (property.name | caseUcfirst)) %} -{%~ if property.enum or property.items.enum is defined %} -{%~ if enumName not in imports %} -const _{{ enumName | caseUcfirst }} := preload("res://addons/{{ spec.title | caseSnake }}/enums/{{ enumName | caseSnake }}.gd") -{% set imports = imports|merge([enumName]) %} -{%~ endif %} -{%~ endif %} -{%~ endfor %} - -const _FIELD_MAP := { -{% for property in requestModel.properties %} - "{{ property.name | uniqueSnake }}": "{{ property.name }}", -{% endfor %} -} - -{% for property in requestModel.properties %} -{% set baseType = property | typeName(spec) %} -var {{ property.name | uniqueSnake }}: {{ baseType }} ## {{ property.description | default("No description provided.") }} -{% endfor %} - -## Convert dictionary to model -static func from_dict(dict: Dictionary): - var m := {{ prefix }}{{ requestModel.name | caseUcfirst }}.new() - - for key in _FIELD_MAP: - var raw_key = _FIELD_MAP[key] - var value = dict.get(raw_key) - -{% for property in requestModel.properties %} -{% set field = property.name | uniqueSnake %} -{% set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (requestModel.name ~ (property.name | caseUcfirst)) %} -{% if property.model %} - if key == "{{ field }}" and value is Dictionary: - m.set(key, {{ prefix }}{{ property.model | caseUcfirst }}.from_dict(value)) - continue -{% endif %} -{% if property.array.model %} - if key == "{{ field }}" and value is Array: - var list := [] - for item in value: - if item is Dictionary: - list.append({{ prefix }}{{ property.array.model | caseUcfirst }}.from_dict(item)) - else: - list.append(item) - m.set(key, list) - continue -{% endif %} -{% if property.enum %} - if key == "{{ field }}" and value != null: - if not _{{ enumName | caseUcfirst }}.is_valid(value): - push_error("Invalid enum value for {{ field }}: %s" % value) - return {{prefix}}Exception.new("Invalid enum value for {{ field }}: %s" % value, 0, "invalid_enum_value", str(dict)) - m.set(key, value) - continue -{% endif %} -{% if property.type == 'array' and property.items.enum is defined %} - if key == "{{ field }}" and value is Array: - var list := [] - for item in value: - if not _{{ enumName | caseUcfirst }}.is_valid(item): - push_error("Invalid enum value: %s" % item) - return {{prefix}}Exception.new("Invalid enum value for {{ field }}: %s" % value, 0, "invalid_enum_value", str(dict)) - list.append(item) - m.set(key, list) - continue -{% endif %} -{% if property.type == 'array' and not property.array.model %} - if key == "{{ field }}" and value is Array: - m.set(key, value) - continue -{% endif %} -{% endfor %} - m.set(key, value) - return m - -## Convert to Dictionary -func to_dict() -> Dictionary: - var dict := {} - - for key in _FIELD_MAP: - var value = get(key) - -{% for property in requestModel.properties %} -{% set field = property.name | uniqueSnake %} -{% if property.model %} - if key == "{{ field }}" and value != null: - dict[_FIELD_MAP[key]] = value.to_dict() - continue -{% endif %} -{% if property.array.model %} - if key == "{{ field }}" and value is Array: - var list := [] - for item in value: - if item != null: - list.append(item.to_dict()) - dict[_FIELD_MAP[key]] = list - continue -{% endif %} -{% endfor %} - dict[_FIELD_MAP[key]] = value - return dict \ No newline at end of file diff --git a/templates/gdscript/addons/utils/service.gd.twig b/templates/gdscript/addons/utils/service.gd.twig index 38ce79f591..03842446a7 100644 --- a/templates/gdscript/addons/utils/service.gd.twig +++ b/templates/gdscript/addons/utils/service.gd.twig @@ -45,7 +45,11 @@ func _handle_response(result: Dictionary, model_script: Variant = null) -> Varia if result.get("body") is Array: var list: Array[RefCounted] = [] for item in result.body: - list.append(model_script.from_dict(item)) + var parsed_model = model_script.from_dict(item) + if parsed_model is {{prefix}}Exception: + return parsed_model + list.append(parsed_model) return list - + + # Return model if successfull else Exception return model_script.from_dict(result.get("body", {})) diff --git a/templates/godot/addons/models/model.gd.twig b/templates/godot/addons/models/model.gd.twig index 759129967a..ca3bb90c17 100644 --- a/templates/godot/addons/models/model.gd.twig +++ b/templates/godot/addons/models/model.gd.twig @@ -1,11 +1,12 @@ +{% set model = requestModel ?? definition %} {% set prefix = spec.title | caseUcfirst %} -class_name {{ prefix }}{{ definition.name | caseUcfirst }} +class_name {{ prefix }}{{ model.name | caseUcfirst }} extends RefCounted -## {{ definition.description | default("Model class.") | replace({"\n": "\n## "}) }}[br] +## {{ model.description | default("Model class.") | replace({"\n": "\n## "}) }}[br] {% set imports = [] %} -{%~ for property in definition.properties %} -{%~ set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (definition.name ~ (property.name | caseUcfirst)) %} +{%~ for property in model.properties %} +{%~ set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (model.name ~ (property.name | caseUcfirst)) %} {%~ if property.enum or property.items.enum is defined %} {%~ if enumName not in imports %} const _{{ enumName | caseUcfirst }} := preload("res://addons/{{ spec.title | caseSnake }}/enums/{{ enumName | caseSnake }}.gd") @@ -15,30 +16,33 @@ const _{{ enumName | caseUcfirst }} := preload("res://addons/{{ spec.title | cas {%~ endfor %} const _FIELD_MAP := { -{% for property in definition.properties %} +{% for property in model.properties %} "{{ property.name | uniqueSnake }}": "{{ property.name }}", {% endfor %} } -{% for property in definition.properties %} +{% for property in model.properties %} {% set baseType = property | typeName(spec) %} var {{ property.name | uniqueSnake }}: {{ baseType }} ## {{ property.description | default("No description provided.") }} {% endfor %} ## Convert dictionary to model static func from_dict(dict: Dictionary): - var m := {{ prefix }}{{ definition.name | caseUcfirst }}.new() + var m := {{ prefix }}{{ model.name | caseUcfirst }}.new() for key in _FIELD_MAP: var raw_key = _FIELD_MAP[key] var value = dict.get(raw_key) -{% for property in definition.properties %} +{% for property in model.properties %} {% set field = property.name | uniqueSnake %} -{% set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (definition.name ~ (property.name | caseUcfirst)) %} +{% set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (model.name ~ (property.name | caseUcfirst)) %} {% if property.model %} if key == "{{ field }}" and value is Dictionary: - m.set(key, {{ prefix }}{{ property.model | caseUcfirst }}.from_dict(value)) + var nested = {{ prefix }}{{ property.model | caseUcfirst }}.from_dict(value) + if nested is {{prefix}}Exception: + return nested + m.set(key, nested) continue {% endif %} {% if property.array.model %} @@ -46,7 +50,10 @@ static func from_dict(dict: Dictionary): var list := [] for item in value: if item is Dictionary: - list.append({{ prefix }}{{ property.array.model | caseUcfirst }}.from_dict(item)) + var nested = {{ prefix }}{{ property.array.model | caseUcfirst }}.from_dict(item) + if nested is {{prefix}}Exception: + return nested + list.append(nested) else: list.append(item) m.set(key, list) @@ -87,7 +94,7 @@ func to_dict() -> Dictionary: for key in _FIELD_MAP: var value = get(key) -{% for property in definition.properties %} +{% for property in model.properties %} {% set field = property.name | uniqueSnake %} {% if property.model %} if key == "{{ field }}" and value != null: diff --git a/templates/godot/addons/models/request_model.gd.twig b/templates/godot/addons/models/request_model.gd.twig deleted file mode 100644 index 44ecc6041a..0000000000 --- a/templates/godot/addons/models/request_model.gd.twig +++ /dev/null @@ -1,108 +0,0 @@ -{% set prefix = spec.title | caseUcfirst %} -class_name {{ prefix }}{{ requestModel.name | caseUcfirst }} -extends RefCounted -## {{ requestModel.description | default("Request model class.") | replace({"\n": "\n## "}) }}[br] - -{% set imports = [] %} -{%~ for property in requestModel.properties %} -{%~ set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (requestModel.name ~ (property.name | caseUcfirst)) %} -{%~ if property.enum or property.items.enum is defined %} -{%~ if enumName not in imports %} -const _{{ enumName | caseUcfirst }} := preload("res://addons/{{ spec.title | caseSnake }}/enums/{{ enumName | caseSnake }}.gd") -{% set imports = imports|merge([enumName]) %} -{%~ endif %} -{%~ endif %} -{%~ endfor %} - -const _FIELD_MAP := { -{% for property in requestModel.properties %} - "{{ property.name | uniqueSnake }}": "{{ property.name }}", -{% endfor %} -} - -{% for property in requestModel.properties %} -{% set baseType = property | typeName(spec) %} -var {{ property.name | uniqueSnake }}: {{ baseType }} ## {{ property.description | default("No description provided.") }} -{% endfor %} - -## Convert dictionary to model -static func from_dict(dict: Dictionary): - var m := {{ prefix }}{{ requestModel.name | caseUcfirst }}.new() - - for key in _FIELD_MAP: - var raw_key = _FIELD_MAP[key] - var value = dict.get(raw_key) - -{% for property in requestModel.properties %} -{% set field = property.name | uniqueSnake %} -{% set enumName = property.enumName ?? attribute(property, 'x-enum-name') ?? (requestModel.name ~ (property.name | caseUcfirst)) %} -{% if property.model %} - if key == "{{ field }}" and value is Dictionary: - m.set(key, {{ prefix }}{{ property.model | caseUcfirst }}.from_dict(value)) - continue -{% endif %} -{% if property.array.model %} - if key == "{{ field }}" and value is Array: - var list := [] - for item in value: - if item is Dictionary: - list.append({{ prefix }}{{ property.array.model | caseUcfirst }}.from_dict(item)) - else: - list.append(item) - m.set(key, list) - continue -{% endif %} -{% if property.enum %} - if key == "{{ field }}" and value != null: - if not _{{ enumName | caseUcfirst }}.is_valid(value): - push_error("Invalid enum value for {{ field }}: %s" % value) - return {{prefix}}Exception.new("Invalid enum value for {{ field }}: %s" % value, 0, "invalid_enum_value", str(dict)) - m.set(key, value) - continue -{% endif %} -{% if property.type == 'array' and property.items.enum is defined %} - if key == "{{ field }}" and value is Array: - var list := [] - for item in value: - if not _{{ enumName | caseUcfirst }}.is_valid(item): - push_error("Invalid enum value: %s" % item) - return {{prefix}}Exception.new("Invalid enum value for {{ field }}: %s" % value, 0, "invalid_enum_value", str(dict)) - list.append(item) - m.set(key, list) - continue -{% endif %} -{% if property.type == 'array' and not property.array.model %} - if key == "{{ field }}" and value is Array: - m.set(key, value) - continue -{% endif %} -{% endfor %} - m.set(key, value) - return m - -## Convert to Dictionary -func to_dict() -> Dictionary: - var dict := {} - - for key in _FIELD_MAP: - var value = get(key) - -{% for property in requestModel.properties %} -{% set field = property.name | uniqueSnake %} -{% if property.model %} - if key == "{{ field }}" and value != null: - dict[_FIELD_MAP[key]] = value.to_dict() - continue -{% endif %} -{% if property.array.model %} - if key == "{{ field }}" and value is Array: - var list := [] - for item in value: - if item != null: - list.append(item.to_dict()) - dict[_FIELD_MAP[key]] = list - continue -{% endif %} -{% endfor %} - dict[_FIELD_MAP[key]] = value - return dict \ No newline at end of file diff --git a/templates/godot/addons/utils/service.gd.twig b/templates/godot/addons/utils/service.gd.twig index a70d46022d..722ce903a0 100644 --- a/templates/godot/addons/utils/service.gd.twig +++ b/templates/godot/addons/utils/service.gd.twig @@ -45,7 +45,11 @@ func _handle_response(result: Dictionary, model_script: Variant = null) -> Varia if result.get("body") is Array: var list: Array[RefCounted] = [] for item in result.body: - list.append(model_script.from_dict(item)) + var parsed_model = model_script.from_dict(item) + if parsed_model is {{prefix}}Exception: + return parsed_model + list.append(parsed_model) return list - - return model_script.from_dict(result.get("body", {})) + + # Return model if successfull else Exception + return model_script.from_dict(result.get("body", {})) \ No newline at end of file From a588f1e2eca671f096b6350ed07cc09dfb10591f Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Thu, 21 May 2026 17:42:26 +0530 Subject: [PATCH 47/58] fix(godot): parse Expires cookies and honor Max-Age precedence --- .../gdscript/addons/utils/client.gd.twig | 2 +- .../godot/addons/persistence/cookie.gd.twig | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/templates/gdscript/addons/utils/client.gd.twig b/templates/gdscript/addons/utils/client.gd.twig index aeb5d5402e..5b7f051dac 100644 --- a/templates/gdscript/addons/utils/client.gd.twig +++ b/templates/gdscript/addons/utils/client.gd.twig @@ -142,7 +142,7 @@ func redirect(method: String, path: String = "", headers: Dictionary = {}, param return { "statusCode": 200, "body": header.substr(9).strip_edges() } if request_result != HTTPRequest.RESULT_SUCCESS: - return { "statusCode": 0, "body": "HTTP Request Error: " + str(request_result) } + return { "statusCode": 0, "error": "HTTP Request Error: " + str(request_result) } var response_text := (response[3] as PackedByteArray).get_string_from_utf8() diff --git a/templates/godot/addons/persistence/cookie.gd.twig b/templates/godot/addons/persistence/cookie.gd.twig index 50b131c770..254bbf2f50 100644 --- a/templates/godot/addons/persistence/cookie.gd.twig +++ b/templates/godot/addons/persistence/cookie.gd.twig @@ -31,6 +31,9 @@ func parse(header: String) -> void: expires_at = 0 else: expires_at = int(Time.get_unix_time_from_system()) + max_age + "expires": + if expires_at == -1: + expires_at = _parse_http_date(attr_value) "path": path = attr_value "domain": @@ -59,3 +62,40 @@ func from_dict(dict: Dictionary) -> void: expires_at = int(dict.get("expires_at", -1)) path = dict.get("path", "/") domain = dict.get("domain", "") + +func _parse_http_date(date_str: String) -> int: + var regex := RegEx.new() + regex.compile( + "\\w+,\\s+(\\d+)\\s+(\\w+)\\s+(\\d+)\\s+(\\d+):(\\d+):(\\d+)\\s+GMT" + ) + + var result = regex.search(date_str) + + if result == null: + return -1 + + var months = { + "Jan":1, + "Feb":2, + "Mar":3, + "Apr":4, + "May":5, + "Jun":6, + "Jul":7, + "Aug":8, + "Sep":9, + "Oct":10, + "Nov":11, + "Dec":12 + } + + var dict = { + year = result.get_string(3).to_int(), + month = months.get(result.get_string(2),1), + day = result.get_string(1).to_int(), + hour = result.get_string(4).to_int(), + minute = result.get_string(5).to_int(), + second = result.get_string(6).to_int() + } + + return Time.get_unix_time_from_datetime_dict(dict) \ No newline at end of file From fd4c7a0041740d79e96442bd751a89abb4fa5d28 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Thu, 21 May 2026 18:17:59 +0530 Subject: [PATCH 48/58] refract(file): open guard and closing after use --- templates/gdscript/addons/appwrite.gd.twig | 14 +++++--- .../gdscript/addons/utils/input_file.gd.twig | 13 +++++-- templates/godot/addons/appwrite.gd.twig | 14 +++++--- .../addons/persistence/cookie_store.gd.twig | 36 +++++++++++-------- .../godot/addons/utils/input_file.gd.twig | 13 +++++-- templates/godot/addons/utils/oauth2.gd.twig | 1 + 6 files changed, 62 insertions(+), 29 deletions(-) diff --git a/templates/gdscript/addons/appwrite.gd.twig b/templates/gdscript/addons/appwrite.gd.twig index f24d8b9119..e138f04420 100644 --- a/templates/gdscript/addons/appwrite.gd.twig +++ b/templates/gdscript/addons/appwrite.gd.twig @@ -27,24 +27,30 @@ func _ready() -> void: return var file = FileAccess.open(path, FileAccess.READ) + if file == null: + push_error("Failed to open env file: %s" % path) + return + while not file.eof_reached(): var line = file.get_line().strip_edges() if line.is_empty() or line.begins_with("#"): continue - + var parts = line.split("=", true, 1) if parts.size() != 2: continue - + var key = parts[0].strip_edges() var value = parts[1].strip_edges() - + # Remove quotes if present if (value.begins_with("\"") and value.ends_with("\"")) or (value.begins_with("'") and value.ends_with("'")): value = value.substr(1, value.length() - 2) - + _apply_env(key, value) + file.close() + {% for header in spec.global.headers %} {% if header.description %} ## {{header.description}} diff --git a/templates/gdscript/addons/utils/input_file.gd.twig b/templates/gdscript/addons/utils/input_file.gd.twig index 7fcfb77193..dfc8095b59 100644 --- a/templates/gdscript/addons/utils/input_file.gd.twig +++ b/templates/gdscript/addons/utils/input_file.gd.twig @@ -32,7 +32,10 @@ func get_data() -> PackedByteArray: if not bytes.is_empty(): return bytes if not path.is_empty(): - return FileAccess.get_file_as_bytes(path) + var data = FileAccess.get_file_as_bytes(path) + if data.is_empty() and not FileAccess.file_exists(path): + push_error("File not found: %s" % path) + return data return PackedByteArray() ## Return the size of the file @@ -42,7 +45,9 @@ func get_size() -> int: if not path.is_empty(): var file := FileAccess.open(path, FileAccess.READ) if file: - return file.get_length() + var sz = file.get_length() + file.close() + return sz return 0 ## Return a chunk of the file @@ -53,7 +58,9 @@ func get_chunk(offset: int, length: int) -> PackedByteArray: var file := FileAccess.open(path, FileAccess.READ) if file: file.seek(offset) - return file.get_buffer(length) + var data = file.get_buffer(length) + file.close() + return data return PackedByteArray() static func _guess_mime_type(filename: String) -> String: diff --git a/templates/godot/addons/appwrite.gd.twig b/templates/godot/addons/appwrite.gd.twig index f24d8b9119..e138f04420 100644 --- a/templates/godot/addons/appwrite.gd.twig +++ b/templates/godot/addons/appwrite.gd.twig @@ -27,24 +27,30 @@ func _ready() -> void: return var file = FileAccess.open(path, FileAccess.READ) + if file == null: + push_error("Failed to open env file: %s" % path) + return + while not file.eof_reached(): var line = file.get_line().strip_edges() if line.is_empty() or line.begins_with("#"): continue - + var parts = line.split("=", true, 1) if parts.size() != 2: continue - + var key = parts[0].strip_edges() var value = parts[1].strip_edges() - + # Remove quotes if present if (value.begins_with("\"") and value.ends_with("\"")) or (value.begins_with("'") and value.ends_with("'")): value = value.substr(1, value.length() - 2) - + _apply_env(key, value) + file.close() + {% for header in spec.global.headers %} {% if header.description %} ## {{header.description}} diff --git a/templates/godot/addons/persistence/cookie_store.gd.twig b/templates/godot/addons/persistence/cookie_store.gd.twig index 099fb626cc..4ee6c7bf95 100644 --- a/templates/godot/addons/persistence/cookie_store.gd.twig +++ b/templates/godot/addons/persistence/cookie_store.gd.twig @@ -44,25 +44,31 @@ func get_cookie_header() -> String: func save_cookies() -> void: var file = FileAccess.open(_cookie_path, FileAccess.WRITE) - if file: - var dict = {} - for name in _cookies: - dict[name] = _cookies[name].to_dict() - file.store_string(JSON.stringify(dict)) - file.close() + if file == null: + push_error("Failed to save cookies: %s" % FileAccess.get_open_error()) + return + + var dict = {} + for name in _cookies: + dict[name] = _cookies[name].to_dict() + file.store_string(JSON.stringify(dict)) + file.close() func load_cookies() -> void: if FileAccess.file_exists(_cookie_path): var file = FileAccess.open(_cookie_path, FileAccess.READ) - if file: - var text = file.get_as_text() - var json = JSON.new() - if json.parse(text) == OK and typeof(json.data) == TYPE_DICTIONARY: - for name in json.data: - var data = json.data[name] - if typeof(data) == TYPE_DICTIONARY: - var cookie = _cookie_class.new() - cookie.from_dict(data) + if file == null: + push_error("Failed to load cookies: %s" % FileAccess.get_open_error()) + return + + var text = file.get_as_text() + var json = JSON.new() + if json.parse(text) == OK and typeof(json.data) == TYPE_DICTIONARY: + for name in json.data: + var data = json.data[name] + if typeof(data) == TYPE_DICTIONARY: + var cookie = _cookie_class.new() + cookie.from_dict(data) if not cookie.is_expired(): _cookies[name] = cookie file.close() diff --git a/templates/godot/addons/utils/input_file.gd.twig b/templates/godot/addons/utils/input_file.gd.twig index 7fcfb77193..dfc8095b59 100644 --- a/templates/godot/addons/utils/input_file.gd.twig +++ b/templates/godot/addons/utils/input_file.gd.twig @@ -32,7 +32,10 @@ func get_data() -> PackedByteArray: if not bytes.is_empty(): return bytes if not path.is_empty(): - return FileAccess.get_file_as_bytes(path) + var data = FileAccess.get_file_as_bytes(path) + if data.is_empty() and not FileAccess.file_exists(path): + push_error("File not found: %s" % path) + return data return PackedByteArray() ## Return the size of the file @@ -42,7 +45,9 @@ func get_size() -> int: if not path.is_empty(): var file := FileAccess.open(path, FileAccess.READ) if file: - return file.get_length() + var sz = file.get_length() + file.close() + return sz return 0 ## Return a chunk of the file @@ -53,7 +58,9 @@ func get_chunk(offset: int, length: int) -> PackedByteArray: var file := FileAccess.open(path, FileAccess.READ) if file: file.seek(offset) - return file.get_buffer(length) + var data = file.get_buffer(length) + file.close() + return data return PackedByteArray() static func _guess_mime_type(filename: String) -> String: diff --git a/templates/godot/addons/utils/oauth2.gd.twig b/templates/godot/addons/utils/oauth2.gd.twig index 7e36acdd63..1fb7251b9a 100644 --- a/templates/godot/addons/utils/oauth2.gd.twig +++ b/templates/godot/addons/utils/oauth2.gd.twig @@ -80,6 +80,7 @@ func load_svg() -> String: return "" var svg := file.get_as_text() svg = svg.replace(" String: From 3b4efdded6cc50696be7268feea893f05cbb7e93 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Thu, 21 May 2026 18:31:45 +0530 Subject: [PATCH 49/58] fix: avoid assigning null to typed model fields --- templates/gdscript/addons/models/model.gd.twig | 8 +++++++- templates/godot/addons/models/model.gd.twig | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/templates/gdscript/addons/models/model.gd.twig b/templates/gdscript/addons/models/model.gd.twig index ca3bb90c17..ef86911f62 100644 --- a/templates/gdscript/addons/models/model.gd.twig +++ b/templates/gdscript/addons/models/model.gd.twig @@ -32,6 +32,11 @@ static func from_dict(dict: Dictionary): for key in _FIELD_MAP: var raw_key = _FIELD_MAP[key] + + # Skip if key doesn't exist in dict + if not dict.has(raw_key): + continue + var value = dict.get(raw_key) {% for property in model.properties %} @@ -84,7 +89,8 @@ static func from_dict(dict: Dictionary): continue {% endif %} {% endfor %} - m.set(key, value) + if value != null: + m.set(key, value) return m ## Convert to Dictionary diff --git a/templates/godot/addons/models/model.gd.twig b/templates/godot/addons/models/model.gd.twig index ca3bb90c17..ef86911f62 100644 --- a/templates/godot/addons/models/model.gd.twig +++ b/templates/godot/addons/models/model.gd.twig @@ -32,6 +32,11 @@ static func from_dict(dict: Dictionary): for key in _FIELD_MAP: var raw_key = _FIELD_MAP[key] + + # Skip if key doesn't exist in dict + if not dict.has(raw_key): + continue + var value = dict.get(raw_key) {% for property in model.properties %} @@ -84,7 +89,8 @@ static func from_dict(dict: Dictionary): continue {% endif %} {% endfor %} - m.set(key, value) + if value != null: + m.set(key, value) return m ## Convert to Dictionary From 5a2bb8c7ef4093730c7b2260b6c6642530ebb851 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Thu, 21 May 2026 19:21:47 +0530 Subject: [PATCH 50/58] fix(godot): add FileAccess guards and improve cookie persistence handling --- templates/gdscript/addons/appwrite.gd.twig | 2 +- .../gdscript/addons/utils/input_file.gd.twig | 4 +-- .../gdscript/addons/utils/service.gd.twig | 26 ++++++++++++++----- templates/godot/addons/appwrite.gd.twig | 2 +- .../addons/persistence/cookie_store.gd.twig | 8 +++--- .../godot/addons/utils/input_file.gd.twig | 4 +-- templates/godot/addons/utils/oauth2.gd.twig | 7 +++-- templates/godot/addons/utils/service.gd.twig | 24 +++++++++++++---- 8 files changed, 54 insertions(+), 23 deletions(-) diff --git a/templates/gdscript/addons/appwrite.gd.twig b/templates/gdscript/addons/appwrite.gd.twig index e138f04420..d8e4b9b996 100644 --- a/templates/gdscript/addons/appwrite.gd.twig +++ b/templates/gdscript/addons/appwrite.gd.twig @@ -49,7 +49,7 @@ func _ready() -> void: _apply_env(key, value) - file.close() + file = null {% for header in spec.global.headers %} {% if header.description %} diff --git a/templates/gdscript/addons/utils/input_file.gd.twig b/templates/gdscript/addons/utils/input_file.gd.twig index dfc8095b59..eb598153a8 100644 --- a/templates/gdscript/addons/utils/input_file.gd.twig +++ b/templates/gdscript/addons/utils/input_file.gd.twig @@ -46,7 +46,7 @@ func get_size() -> int: var file := FileAccess.open(path, FileAccess.READ) if file: var sz = file.get_length() - file.close() + file = null return sz return 0 @@ -59,7 +59,7 @@ func get_chunk(offset: int, length: int) -> PackedByteArray: if file: file.seek(offset) var data = file.get_buffer(length) - file.close() + file = null return data return PackedByteArray() diff --git a/templates/gdscript/addons/utils/service.gd.twig b/templates/gdscript/addons/utils/service.gd.twig index 03842446a7..c7208d1644 100644 --- a/templates/gdscript/addons/utils/service.gd.twig +++ b/templates/gdscript/addons/utils/service.gd.twig @@ -45,11 +45,25 @@ func _handle_response(result: Dictionary, model_script: Variant = null) -> Varia if result.get("body") is Array: var list: Array[RefCounted] = [] for item in result.body: - var parsed_model = model_script.from_dict(item) - if parsed_model is {{prefix}}Exception: - return parsed_model - list.append(parsed_model) - return list + if item is Dictionary: + var parsed = model_script.from_dict(item) + if parsed is {{prefix}}Exception: + return parsed + list.append(parsed) + else: + return {{prefix}}Exception.new( + "Expected Dictionary in array response", + result.statusCode, + "invalid_response", + str(item) + ) + return list - # Return model if successfull else Exception + if not (result.get("body") is Dictionary): + return {{prefix}}Exception.new( + "Expected Dictionary response", + result.statusCode, + "invalid_response", + str(result.get("body")) + ) return model_script.from_dict(result.get("body", {})) diff --git a/templates/godot/addons/appwrite.gd.twig b/templates/godot/addons/appwrite.gd.twig index e138f04420..d8e4b9b996 100644 --- a/templates/godot/addons/appwrite.gd.twig +++ b/templates/godot/addons/appwrite.gd.twig @@ -49,7 +49,7 @@ func _ready() -> void: _apply_env(key, value) - file.close() + file = null {% for header in spec.global.headers %} {% if header.description %} diff --git a/templates/godot/addons/persistence/cookie_store.gd.twig b/templates/godot/addons/persistence/cookie_store.gd.twig index 4ee6c7bf95..8ac7345cbd 100644 --- a/templates/godot/addons/persistence/cookie_store.gd.twig +++ b/templates/godot/addons/persistence/cookie_store.gd.twig @@ -52,7 +52,7 @@ func save_cookies() -> void: for name in _cookies: dict[name] = _cookies[name].to_dict() file.store_string(JSON.stringify(dict)) - file.close() + file = null func load_cookies() -> void: if FileAccess.file_exists(_cookie_path): @@ -69,6 +69,6 @@ func load_cookies() -> void: if typeof(data) == TYPE_DICTIONARY: var cookie = _cookie_class.new() cookie.from_dict(data) - if not cookie.is_expired(): - _cookies[name] = cookie - file.close() + if not cookie.is_expired(): + _cookies[name] = cookie + file = null diff --git a/templates/godot/addons/utils/input_file.gd.twig b/templates/godot/addons/utils/input_file.gd.twig index dfc8095b59..eb598153a8 100644 --- a/templates/godot/addons/utils/input_file.gd.twig +++ b/templates/godot/addons/utils/input_file.gd.twig @@ -46,7 +46,7 @@ func get_size() -> int: var file := FileAccess.open(path, FileAccess.READ) if file: var sz = file.get_length() - file.close() + file = null return sz return 0 @@ -59,7 +59,7 @@ func get_chunk(offset: int, length: int) -> PackedByteArray: if file: file.seek(offset) var data = file.get_buffer(length) - file.close() + file = null return data return PackedByteArray() diff --git a/templates/godot/addons/utils/oauth2.gd.twig b/templates/godot/addons/utils/oauth2.gd.twig index 1fb7251b9a..9f234cd752 100644 --- a/templates/godot/addons/utils/oauth2.gd.twig +++ b/templates/godot/addons/utils/oauth2.gd.twig @@ -62,8 +62,11 @@ func _process(_delta): var parsed := extract_callback_url(_request_buffer) if parsed.is_empty() or parsed.has("error") or parsed.get("body", "").contains("failure"): + parsed["statusCode"] = 401 + if not parsed.has("error"): + parsed["error"] = "Authentication failed" _connection.put_data( - failure_html(parsed.get("error", "Something went wrong. Please try again")) + failure_html(parsed.get("error")) .to_utf8_buffer() ) else: @@ -80,7 +83,7 @@ func load_svg() -> String: return "" var svg := file.get_as_text() svg = svg.replace(" String: diff --git a/templates/godot/addons/utils/service.gd.twig b/templates/godot/addons/utils/service.gd.twig index 722ce903a0..c5ea77be02 100644 --- a/templates/godot/addons/utils/service.gd.twig +++ b/templates/godot/addons/utils/service.gd.twig @@ -45,11 +45,25 @@ func _handle_response(result: Dictionary, model_script: Variant = null) -> Varia if result.get("body") is Array: var list: Array[RefCounted] = [] for item in result.body: - var parsed_model = model_script.from_dict(item) - if parsed_model is {{prefix}}Exception: - return parsed_model - list.append(parsed_model) + if item is Dictionary: + var parsed = model_script.from_dict(item) + if parsed is {{prefix}}Exception: + return parsed + list.append(parsed) + else: + return {{prefix}}Exception.new( + "Expected Dictionary in array response", + result.statusCode, + "invalid_response", + str(item) + ) return list - # Return model if successfull else Exception + if not (result.get("body") is Dictionary): + return {{prefix}}Exception.new( + "Expected Dictionary response", + result.statusCode, + "invalid_response", + str(result.get("body")) + ) return model_script.from_dict(result.get("body", {})) \ No newline at end of file From 9f7122cc2dde2592a9d9bc8f0b284fe2276a703d Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Thu, 21 May 2026 21:24:28 +0530 Subject: [PATCH 51/58] fix: prevent HTTPRequest leaks --- templates/gdscript/addons/utils/client.gd.twig | 18 ++++++++---------- templates/godot/addons/utils/client.gd.twig | 15 ++++++++------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/templates/gdscript/addons/utils/client.gd.twig b/templates/gdscript/addons/utils/client.gd.twig index 5b7f051dac..cd52bfb8a6 100644 --- a/templates/gdscript/addons/utils/client.gd.twig +++ b/templates/gdscript/addons/utils/client.gd.twig @@ -51,16 +51,6 @@ func set_{{header.key | caseSnake}}(value: String) -> RefCounted: func _http_request(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}, max_redirects: int = 8) -> Variant: if not _ensure_configured(): return {"statusCode": 400, "body": "Missing/wrong configuration. Please check logs for more info"} - - # Create HTTP request - var http := HTTPRequest.new() - http.max_redirects = max_redirects - var tls_options := TLSOptions.client_unsafe() if _self_signed else TLSOptions.client() - http.set_tls_options(tls_options) - - # Add http request to the tree - Engine.get_main_loop().root.add_child.call_deferred(http) - await http.tree_entered # Merge headers var combined_headers := _global_headers.duplicate() @@ -115,6 +105,14 @@ func _http_request(method: String, path: String = "", headers: Dictionary = {}, if combined_headers[key] != "": header_list.append(key + ": " + str(combined_headers[key])) + # Create HTTPRequest instance + var http := HTTPRequest.new() + var tls_options := TLSOptions.client_unsafe() if _self_signed else TLSOptions.client() + http.max_redirects = max_redirects + http.set_tls_options(tls_options) + Engine.get_main_loop().root.add_child.call_deferred(http) + await http.tree_entered + # Make request var err = http.request_raw(uri + request_path, header_list, http_method, body) if err != OK: diff --git a/templates/godot/addons/utils/client.gd.twig b/templates/godot/addons/utils/client.gd.twig index df0d998e0f..0f1350775a 100644 --- a/templates/godot/addons/utils/client.gd.twig +++ b/templates/godot/addons/utils/client.gd.twig @@ -71,13 +71,6 @@ func call_web_api(method: String, path: String = "", params: Dictionary = {}) -> func call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: if not _ensure_configured(): return {"statusCode": 400, "body": "missing/wrong configuration. please check logs for more info"} - var http := HTTPRequest.new() - - var tls_options := TLSOptions.client_unsafe() if _self_signed else TLSOptions.client() - http.set_tls_options(tls_options) - - Engine.get_main_loop().root.add_child.call_deferred(http) - await http.tree_entered # merge headers var combined_headers := _global_headers.duplicate() @@ -134,6 +127,14 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param if cookie_header != "": header_list.append("Cookie: " + cookie_header) + # Create HTTPRequest instance + var http := HTTPRequest.new() + var tls_options := TLSOptions.client_unsafe() if _self_signed else TLSOptions.client() + http.set_tls_options(tls_options) + Engine.get_main_loop().root.add_child.call_deferred(http) + await http.tree_entered + + # Make request var err = http.request_raw(uri + request_path, header_list, http_method, body) if err != OK: http.queue_free() From 7059de874ac022056ef36ddfebf02e7338742951 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Thu, 21 May 2026 21:41:47 +0530 Subject: [PATCH 52/58] refract: statusCode set to 0 for network error --- templates/gdscript/addons/utils/client.gd.twig | 2 +- templates/godot/addons/utils/client.gd.twig | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/gdscript/addons/utils/client.gd.twig b/templates/gdscript/addons/utils/client.gd.twig index cd52bfb8a6..47a9968372 100644 --- a/templates/gdscript/addons/utils/client.gd.twig +++ b/templates/gdscript/addons/utils/client.gd.twig @@ -50,7 +50,7 @@ func set_{{header.key | caseSnake}}(value: String) -> RefCounted: func _http_request(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}, max_redirects: int = 8) -> Variant: if not _ensure_configured(): - return {"statusCode": 400, "body": "Missing/wrong configuration. Please check logs for more info"} + return {"statusCode": 0, "error": "Missing/wrong configuration. Please check logs for more info"} # Merge headers var combined_headers := _global_headers.duplicate() diff --git a/templates/godot/addons/utils/client.gd.twig b/templates/godot/addons/utils/client.gd.twig index 0f1350775a..b53c68eb9b 100644 --- a/templates/godot/addons/utils/client.gd.twig +++ b/templates/godot/addons/utils/client.gd.twig @@ -53,7 +53,7 @@ func set_{{header.key | caseSnake}}(value: String) -> RefCounted: {% endfor %} func call_web_api(method: String, path: String = "", params: Dictionary = {}) -> Dictionary: if not _ensure_configured(): - return {"statusCode": 400, "body": "missing/wrong configuration. please check logs for more info"} + return {"statusCode": 0, "error": "missing/wrong configuration. please check logs for more info"} var oauth_client = load("res://addons/{{ spec.title | caseSnake }}/utils/oauth2.gd").new() Engine.get_main_loop().root.add_child.call_deferred(oauth_client) @@ -70,7 +70,7 @@ func call_web_api(method: String, path: String = "", params: Dictionary = {}) -> func call_api(method: String, path: String = "", headers: Dictionary = {}, params: Dictionary = {}) -> Dictionary: if not _ensure_configured(): - return {"statusCode": 400, "body": "missing/wrong configuration. please check logs for more info"} + return {"statusCode": 0, "error": "missing/wrong configuration. please check logs for more info"} # merge headers var combined_headers := _global_headers.duplicate() From 3b1db642415099b51d7089d82dd0dc60ce7e28cc Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Wed, 27 May 2026 21:19:35 +0530 Subject: [PATCH 53/58] fix(godot): improve oauth callback errors and request/file handling - Escape web OAuth URL when calling JavaScriptBridge.eval - Parse OAuth failure query params for clearer error messages - Map HTTPRequest failure codes to human-readable messages - Add FileAccess existence/open/read guards in InputFile.get_data() - Normalize network failures to statusCode=0 and use error key --- .../gdscript/addons/utils/client.gd.twig | 23 ++++++++++++-- .../gdscript/addons/utils/input_file.gd.twig | 13 ++++++-- templates/godot/addons/utils/client.gd.twig | 19 +++++++++++- .../godot/addons/utils/input_file.gd.twig | 13 ++++++-- templates/godot/addons/utils/oauth2.gd.twig | 31 +++++++++++++++---- 5 files changed, 85 insertions(+), 14 deletions(-) diff --git a/templates/gdscript/addons/utils/client.gd.twig b/templates/gdscript/addons/utils/client.gd.twig index 47a9968372..5ca7947cb8 100644 --- a/templates/gdscript/addons/utils/client.gd.twig +++ b/templates/gdscript/addons/utils/client.gd.twig @@ -117,7 +117,7 @@ func _http_request(method: String, path: String = "", headers: Dictionary = {}, var err = http.request_raw(uri + request_path, header_list, http_method, body) if err != OK: http.queue_free() - return {"statusCode": 400, "body": "Request failed: " + error_string(err)} + return {"statusCode": 0, "error": "Request failed: " + error_string(err)} # Wait for response and clean node from tree on completion var response = await http.request_completed @@ -140,7 +140,7 @@ func redirect(method: String, path: String = "", headers: Dictionary = {}, param return { "statusCode": 200, "body": header.substr(9).strip_edges() } if request_result != HTTPRequest.RESULT_SUCCESS: - return { "statusCode": 0, "error": "HTTP Request Error: " + str(request_result) } + return { "statusCode": 0, "error": "HTTP Request Error: " + _get_request_error_message(request_result) } var response_text := (response[3] as PackedByteArray).get_string_from_utf8() @@ -161,7 +161,7 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param var response_body: PackedByteArray = response[3] if request_result != HTTPRequest.RESULT_SUCCESS: - return {"statusCode": 0, "error": "HTTP Request Error: " + str(request_result)} + return {"statusCode": 0, "error": "HTTP Request Error: " + _get_request_error_message(request_result)} var response_text := response_body.get_string_from_utf8() @@ -171,6 +171,23 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param return {"statusCode": response_code, "body": response_text} +func _get_request_error_message(request_result: int) -> String: + match request_result: + HTTPRequest.RESULT_CHUNKED_BODY_SIZE_MISMATCH: return "Chunked body size mismatch" + HTTPRequest.RESULT_CANT_CONNECT: return "Cannot connect to server" + HTTPRequest.RESULT_CANT_RESOLVE: return "Cannot resolve hostname" + HTTPRequest.RESULT_CONNECTION_ERROR: return "Connection error" + HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR: return "TLS handshake error" + HTTPRequest.RESULT_NO_RESPONSE: return "No response from server" + HTTPRequest.RESULT_BODY_SIZE_LIMIT_EXCEEDED: return "Body size limit exceeded" + HTTPRequest.RESULT_BODY_DECOMPRESS_FAILED: return "Body decompression failed" + HTTPRequest.RESULT_REQUEST_FAILED: return "Request failed" + HTTPRequest.RESULT_DOWNLOAD_FILE_CANT_OPEN: return "Cannot open download file" + HTTPRequest.RESULT_DOWNLOAD_FILE_WRITE_ERROR: return "Download file write error" + HTTPRequest.RESULT_REDIRECT_LIMIT_REACHED: return "Redirect limit reached" + HTTPRequest.RESULT_TIMEOUT: return "Request timeout" + _: return "Unknown error (" + str(request_result) + ")" + func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: var body := PackedByteArray() for key in params: diff --git a/templates/gdscript/addons/utils/input_file.gd.twig b/templates/gdscript/addons/utils/input_file.gd.twig index eb598153a8..ff59649f86 100644 --- a/templates/gdscript/addons/utils/input_file.gd.twig +++ b/templates/gdscript/addons/utils/input_file.gd.twig @@ -32,9 +32,18 @@ func get_data() -> PackedByteArray: if not bytes.is_empty(): return bytes if not path.is_empty(): - var data = FileAccess.get_file_as_bytes(path) - if data.is_empty() and not FileAccess.file_exists(path): + if not FileAccess.file_exists(path): push_error("File not found: %s" % path) + return PackedByteArray() + var file := FileAccess.open(path, FileAccess.READ) + if not file: + push_error("Failed to open file: %s" % path) + return PackedByteArray() + var data = file.get_buffer(file.get_length()) + file = null + if data.is_empty(): + push_error("Failed to read file: %s" % path) + return PackedByteArray() return data return PackedByteArray() diff --git a/templates/godot/addons/utils/client.gd.twig b/templates/godot/addons/utils/client.gd.twig index b53c68eb9b..254ed89dfb 100644 --- a/templates/godot/addons/utils/client.gd.twig +++ b/templates/godot/addons/utils/client.gd.twig @@ -148,7 +148,7 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param var response_body: PackedByteArray = response[3] if request_result != HTTPRequest.RESULT_SUCCESS: - return {"statusCode": 0, "error": "HTTP Request Error: " + str(request_result)} + return {"statusCode": 0, "error": "HTTP Request Error: " + _get_request_error_message(request_result)} for header in response[2]: if header.to_lower().begins_with("set-cookie:"): @@ -160,6 +160,23 @@ func call_api(method: String, path: String = "", headers: Dictionary = {}, param return {"statusCode": response_code, "body": json.data} return {"statusCode": response_code, "body": response_text} +func _get_request_error_message(request_result: int) -> String: + match request_result: + HTTPRequest.RESULT_CHUNKED_BODY_SIZE_MISMATCH: return "Chunked body size mismatch" + HTTPRequest.RESULT_CANT_CONNECT: return "Cannot connect to server" + HTTPRequest.RESULT_CANT_RESOLVE: return "Cannot resolve hostname" + HTTPRequest.RESULT_CONNECTION_ERROR: return "Connection error" + HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR: return "TLS handshake error" + HTTPRequest.RESULT_NO_RESPONSE: return "No response from server" + HTTPRequest.RESULT_BODY_SIZE_LIMIT_EXCEEDED: return "Body size limit exceeded" + HTTPRequest.RESULT_BODY_DECOMPRESS_FAILED: return "Body decompression failed" + HTTPRequest.RESULT_REQUEST_FAILED: return "Request failed" + HTTPRequest.RESULT_DOWNLOAD_FILE_CANT_OPEN: return "Cannot open download file" + HTTPRequest.RESULT_DOWNLOAD_FILE_WRITE_ERROR: return "Download file write error" + HTTPRequest.RESULT_REDIRECT_LIMIT_REACHED: return "Redirect limit reached" + HTTPRequest.RESULT_TIMEOUT: return "Request timeout" + _: return "Unknown error (" + str(request_result) + ")" + func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: var body := PackedByteArray() for key in params: diff --git a/templates/godot/addons/utils/input_file.gd.twig b/templates/godot/addons/utils/input_file.gd.twig index eb598153a8..ff59649f86 100644 --- a/templates/godot/addons/utils/input_file.gd.twig +++ b/templates/godot/addons/utils/input_file.gd.twig @@ -32,9 +32,18 @@ func get_data() -> PackedByteArray: if not bytes.is_empty(): return bytes if not path.is_empty(): - var data = FileAccess.get_file_as_bytes(path) - if data.is_empty() and not FileAccess.file_exists(path): + if not FileAccess.file_exists(path): push_error("File not found: %s" % path) + return PackedByteArray() + var file := FileAccess.open(path, FileAccess.READ) + if not file: + push_error("Failed to open file: %s" % path) + return PackedByteArray() + var data = file.get_buffer(file.get_length()) + file = null + if data.is_empty(): + push_error("Failed to read file: %s" % path) + return PackedByteArray() return data return PackedByteArray() diff --git a/templates/godot/addons/utils/oauth2.gd.twig b/templates/godot/addons/utils/oauth2.gd.twig index 9f234cd752..537ffd66ae 100644 --- a/templates/godot/addons/utils/oauth2.gd.twig +++ b/templates/godot/addons/utils/oauth2.gd.twig @@ -25,9 +25,8 @@ func authenticate(auth_url:String) -> Dictionary: # Open the authentication URL in the system browser if OS.has_feature('web'): - JavaScriptBridge.eval(""" - window.open('%s', '_blank').focus(); - """.replace("%s", auth_url)) + var safe_url := JSON.stringify(auth_url) + JavaScriptBridge.eval("window.open(" + safe_url + ", '_blank').focus();") else: OS.shell_open(auth_url) @@ -61,10 +60,30 @@ func _process(_delta): var parsed := extract_callback_url(_request_buffer) - if parsed.is_empty() or parsed.has("error") or parsed.get("body", "").contains("failure"): - parsed["statusCode"] = 401 + if parsed.is_empty() or parsed.has("error"): if not parsed.has("error"): - parsed["error"] = "Authentication failed" + parsed["error"] = "Invalid callback request" + if not parsed.has("statusCode"): + parsed["statusCode"] = 0 + _connection.put_data( + failure_html(parsed.get("error")) + .to_utf8_buffer() + ) + elif parsed.get("body", "").contains("failure") or parsed.get("body", "").contains("?error=") or parsed.get("body", "").contains("&error="): + var err_msg = "Authentication failed" + var body_str: String = parsed.get("body", "") + if body_str.contains("?"): + var query_parts = body_str.split("?")[1].split("&") + for p in query_parts: + var kv = p.split("=") + if kv.size() == 2: + if kv[0] == "error_description": + err_msg = kv[1].uri_decode().replace("+", " ") + break + if kv[0] == "error" and err_msg == "Authentication failed": + err_msg = kv[1].uri_decode().replace("+", " ") + parsed["statusCode"] = 401 + parsed["error"] = err_msg _connection.put_data( failure_html(parsed.get("error")) .to_utf8_buffer() From bed951e00a248451894e3f7e05562255a3a4b1da Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Wed, 27 May 2026 23:15:22 +0530 Subject: [PATCH 54/58] fix(tests): use snake_case Query methods in GUT templates --- templates/gdscript/addons/appwrite.gd.twig | 3 ++ .../gdscript/addons/tests/test_query.gd.twig | 8 ++-- .../gdscript/addons/utils/input_file.gd.twig | 1 + .../gdscript/addons/utils/operator.gd.twig | 40 +++++++++---------- .../gdscript/addons/utils/service.gd.twig | 3 ++ templates/godot/addons/appwrite.gd.twig | 3 ++ .../godot/addons/tests/test_query.gd.twig | 8 ++-- .../godot/addons/utils/input_file.gd.twig | 1 + templates/godot/addons/utils/operator.gd.twig | 40 +++++++++---------- templates/godot/addons/utils/service.gd.twig | 3 ++ 10 files changed, 62 insertions(+), 48 deletions(-) diff --git a/templates/gdscript/addons/appwrite.gd.twig b/templates/gdscript/addons/appwrite.gd.twig index d8e4b9b996..1dfaf994e3 100644 --- a/templates/gdscript/addons/appwrite.gd.twig +++ b/templates/gdscript/addons/appwrite.gd.twig @@ -113,6 +113,9 @@ func ping() -> Variant: code = response.body.get("code", response.statusCode) type = response.body.get("type", "") response_str = str(response.body) + elif response.has("error"): + message = str(response.get("error", "")) + response_str = message else: message = str(response.get("body", "")) response_str = str(response.get("body", "")) diff --git a/templates/gdscript/addons/tests/test_query.gd.twig b/templates/gdscript/addons/tests/test_query.gd.twig index 51bb647099..b6845b628d 100644 --- a/templates/gdscript/addons/tests/test_query.gd.twig +++ b/templates/gdscript/addons/tests/test_query.gd.twig @@ -17,7 +17,7 @@ func test_query_value_wrapping(): assert_eq(parsed["values"].size(), 1) func test_query_is_null(): - var q = {{spec.title | caseUcfirst}}Query.isNull("field") + var q = {{spec.title | caseUcfirst}}Query.is_null("field") var parsed = JSON.parse_string(q) assert_eq(parsed["method"], "isNull") @@ -43,7 +43,7 @@ func test_query_and(): func test_query_or(): var q1 = {{spec.title | caseUcfirst}}Query.equal("age", 10) - var q2 = {{spec.title | caseUcfirst}}Query.isNull("email") + var q2 = {{spec.title | caseUcfirst}}Query.is_null("email") var combined = {{spec.title | caseUcfirst}}Query.or_query([q1, q2]) var parsed = JSON.parse_string(combined) @@ -66,14 +66,14 @@ func test_query_offset(): assert_eq(parsed["values"][0], 5) func test_query_order_asc(): - var q = {{spec.title | caseUcfirst}}Query.orderAsc("age") + var q = {{spec.title | caseUcfirst}}Query.order_asc("age") var parsed = JSON.parse_string(q) assert_eq(parsed["method"], "orderAsc") assert_eq(parsed["attribute"], "age") func test_query_order_desc(): - var q = {{spec.title | caseUcfirst}}Query.orderDesc("age") + var q = {{spec.title | caseUcfirst}}Query.order_desc("age") var parsed = JSON.parse_string(q) assert_eq(parsed["method"], "orderDesc") diff --git a/templates/gdscript/addons/utils/input_file.gd.twig b/templates/gdscript/addons/utils/input_file.gd.twig index ff59649f86..d775d3b3db 100644 --- a/templates/gdscript/addons/utils/input_file.gd.twig +++ b/templates/gdscript/addons/utils/input_file.gd.twig @@ -1,5 +1,6 @@ {% set prefix = spec.title | caseUcfirst %} class_name {{prefix}}InputFile +extends RefCounted var path: String ## Path to the file var bytes: PackedByteArray ## File bytes diff --git a/templates/gdscript/addons/utils/operator.gd.twig b/templates/gdscript/addons/utils/operator.gd.twig index c293d90812..d3c837c42e 100644 --- a/templates/gdscript/addons/utils/operator.gd.twig +++ b/templates/gdscript/addons/utils/operator.gd.twig @@ -60,7 +60,7 @@ static func increment(value: float = 1, max: Variant = null) -> String: if max != null: vals.append(max) - return AppwriteOperator.new("increment", vals).to_string() + return {{spec.title | caseUcfirst}}Operator.new("increment", vals).to_string() static func decrement(value: float = 1, min: Variant = null) -> String: if not is_finite(value): @@ -75,7 +75,7 @@ static func decrement(value: float = 1, min: Variant = null) -> String: if min != null: vals.append(min) - return AppwriteOperator.new("decrement", vals).to_string() + return {{spec.title | caseUcfirst}}Operator.new("decrement", vals).to_string() static func multiply(factor: float, max: Variant = null) -> String: if not is_finite(factor): @@ -90,7 +90,7 @@ static func multiply(factor: float, max: Variant = null) -> String: if max != null: vals.append(max) - return AppwriteOperator.new("multiply", vals).to_string() + return {{spec.title | caseUcfirst}}Operator.new("multiply", vals).to_string() static func divide(divisor: float, min: Variant = null) -> String: if not is_finite(divisor): @@ -109,7 +109,7 @@ static func divide(divisor: float, min: Variant = null) -> String: if min != null: vals.append(min) - return AppwriteOperator.new("divide", vals).to_string() + return {{spec.title | caseUcfirst}}Operator.new("divide", vals).to_string() static func modulo(divisor: float) -> String: if not is_finite(divisor): @@ -120,7 +120,7 @@ static func modulo(divisor: float) -> String: push_error("Divisor cannot be zero") return "" - return AppwriteOperator.new("modulo", [divisor]).to_string() + return {{spec.title | caseUcfirst}}Operator.new("modulo", [divisor]).to_string() static func power(exponent: float, max: Variant = null) -> String: if not is_finite(exponent): @@ -135,50 +135,50 @@ static func power(exponent: float, max: Variant = null) -> String: if max != null: vals.append(max) - return AppwriteOperator.new("power", vals).to_string() + return {{spec.title | caseUcfirst}}Operator.new("power", vals).to_string() static func array_append(values: Array) -> String: - return AppwriteOperator.new("arrayAppend", values).to_string() + return {{spec.title | caseUcfirst}}Operator.new("arrayAppend", values).to_string() static func array_prepend(values: Array) -> String: - return AppwriteOperator.new("arrayPrepend", values).to_string() + return {{spec.title | caseUcfirst}}Operator.new("arrayPrepend", values).to_string() static func array_insert(index: int, value: Variant) -> String: - return AppwriteOperator.new("arrayInsert", [index, value]).to_string() + return {{spec.title | caseUcfirst}}Operator.new("arrayInsert", [index, value]).to_string() static func array_remove(value: Variant) -> String: - return AppwriteOperator.new("arrayRemove", [value]).to_string() + return {{spec.title | caseUcfirst}}Operator.new("arrayRemove", [value]).to_string() static func array_unique() -> String: - return AppwriteOperator.new("arrayUnique", []).to_string() + return {{spec.title | caseUcfirst}}Operator.new("arrayUnique", []).to_string() static func array_intersect(values: Array) -> String: - return AppwriteOperator.new("arrayIntersect", values).to_string() + return {{spec.title | caseUcfirst}}Operator.new("arrayIntersect", values).to_string() static func array_diff(values: Array) -> String: - return AppwriteOperator.new("arrayDiff", values).to_string() + return {{spec.title | caseUcfirst}}Operator.new("arrayDiff", values).to_string() static func array_filter(condition: String, value: Variant = null) -> String: if not is_valid(condition): push_error("Invalid condition: %s" % condition) return "" - return AppwriteOperator.new("arrayFilter", [condition, value]).to_string() + return {{spec.title | caseUcfirst}}Operator.new("arrayFilter", [condition, value]).to_string() static func string_concat(value: Variant) -> String: - return AppwriteOperator.new("stringConcat", [value]).to_string() + return {{spec.title | caseUcfirst}}Operator.new("stringConcat", [value]).to_string() static func string_replace(search: String, replace: String) -> String: - return AppwriteOperator.new("stringReplace", [search, replace]).to_string() + return {{spec.title | caseUcfirst}}Operator.new("stringReplace", [search, replace]).to_string() static func toggle() -> String: - return AppwriteOperator.new("toggle", []).to_string() + return {{spec.title | caseUcfirst}}Operator.new("toggle", []).to_string() static func date_add_days(days: int) -> String: - return AppwriteOperator.new("dateAddDays", [days]).to_string() + return {{spec.title | caseUcfirst}}Operator.new("dateAddDays", [days]).to_string() static func date_sub_days(days: int) -> String: - return AppwriteOperator.new("dateSubDays", [days]).to_string() + return {{spec.title | caseUcfirst}}Operator.new("dateSubDays", [days]).to_string() static func date_set_now() -> String: - return AppwriteOperator.new("dateSetNow", []).to_string() \ No newline at end of file + return {{spec.title | caseUcfirst}}Operator.new("dateSetNow", []).to_string() \ No newline at end of file diff --git a/templates/gdscript/addons/utils/service.gd.twig b/templates/gdscript/addons/utils/service.gd.twig index c7208d1644..744850125d 100644 --- a/templates/gdscript/addons/utils/service.gd.twig +++ b/templates/gdscript/addons/utils/service.gd.twig @@ -32,6 +32,9 @@ func _handle_response(result: Dictionary, model_script: Variant = null) -> Varia code = result.body.get("code", result.statusCode) type = result.body.get("type", "") response = str(result.body) + elif result.has("error"): + message = str(result.get("error", "")) + response = message else: message = str(result.get("body", "")) response = str(result.get("body", "")) diff --git a/templates/godot/addons/appwrite.gd.twig b/templates/godot/addons/appwrite.gd.twig index d8e4b9b996..1dfaf994e3 100644 --- a/templates/godot/addons/appwrite.gd.twig +++ b/templates/godot/addons/appwrite.gd.twig @@ -113,6 +113,9 @@ func ping() -> Variant: code = response.body.get("code", response.statusCode) type = response.body.get("type", "") response_str = str(response.body) + elif response.has("error"): + message = str(response.get("error", "")) + response_str = message else: message = str(response.get("body", "")) response_str = str(response.get("body", "")) diff --git a/templates/godot/addons/tests/test_query.gd.twig b/templates/godot/addons/tests/test_query.gd.twig index 51bb647099..b6845b628d 100644 --- a/templates/godot/addons/tests/test_query.gd.twig +++ b/templates/godot/addons/tests/test_query.gd.twig @@ -17,7 +17,7 @@ func test_query_value_wrapping(): assert_eq(parsed["values"].size(), 1) func test_query_is_null(): - var q = {{spec.title | caseUcfirst}}Query.isNull("field") + var q = {{spec.title | caseUcfirst}}Query.is_null("field") var parsed = JSON.parse_string(q) assert_eq(parsed["method"], "isNull") @@ -43,7 +43,7 @@ func test_query_and(): func test_query_or(): var q1 = {{spec.title | caseUcfirst}}Query.equal("age", 10) - var q2 = {{spec.title | caseUcfirst}}Query.isNull("email") + var q2 = {{spec.title | caseUcfirst}}Query.is_null("email") var combined = {{spec.title | caseUcfirst}}Query.or_query([q1, q2]) var parsed = JSON.parse_string(combined) @@ -66,14 +66,14 @@ func test_query_offset(): assert_eq(parsed["values"][0], 5) func test_query_order_asc(): - var q = {{spec.title | caseUcfirst}}Query.orderAsc("age") + var q = {{spec.title | caseUcfirst}}Query.order_asc("age") var parsed = JSON.parse_string(q) assert_eq(parsed["method"], "orderAsc") assert_eq(parsed["attribute"], "age") func test_query_order_desc(): - var q = {{spec.title | caseUcfirst}}Query.orderDesc("age") + var q = {{spec.title | caseUcfirst}}Query.order_desc("age") var parsed = JSON.parse_string(q) assert_eq(parsed["method"], "orderDesc") diff --git a/templates/godot/addons/utils/input_file.gd.twig b/templates/godot/addons/utils/input_file.gd.twig index ff59649f86..d775d3b3db 100644 --- a/templates/godot/addons/utils/input_file.gd.twig +++ b/templates/godot/addons/utils/input_file.gd.twig @@ -1,5 +1,6 @@ {% set prefix = spec.title | caseUcfirst %} class_name {{prefix}}InputFile +extends RefCounted var path: String ## Path to the file var bytes: PackedByteArray ## File bytes diff --git a/templates/godot/addons/utils/operator.gd.twig b/templates/godot/addons/utils/operator.gd.twig index c293d90812..d3c837c42e 100644 --- a/templates/godot/addons/utils/operator.gd.twig +++ b/templates/godot/addons/utils/operator.gd.twig @@ -60,7 +60,7 @@ static func increment(value: float = 1, max: Variant = null) -> String: if max != null: vals.append(max) - return AppwriteOperator.new("increment", vals).to_string() + return {{spec.title | caseUcfirst}}Operator.new("increment", vals).to_string() static func decrement(value: float = 1, min: Variant = null) -> String: if not is_finite(value): @@ -75,7 +75,7 @@ static func decrement(value: float = 1, min: Variant = null) -> String: if min != null: vals.append(min) - return AppwriteOperator.new("decrement", vals).to_string() + return {{spec.title | caseUcfirst}}Operator.new("decrement", vals).to_string() static func multiply(factor: float, max: Variant = null) -> String: if not is_finite(factor): @@ -90,7 +90,7 @@ static func multiply(factor: float, max: Variant = null) -> String: if max != null: vals.append(max) - return AppwriteOperator.new("multiply", vals).to_string() + return {{spec.title | caseUcfirst}}Operator.new("multiply", vals).to_string() static func divide(divisor: float, min: Variant = null) -> String: if not is_finite(divisor): @@ -109,7 +109,7 @@ static func divide(divisor: float, min: Variant = null) -> String: if min != null: vals.append(min) - return AppwriteOperator.new("divide", vals).to_string() + return {{spec.title | caseUcfirst}}Operator.new("divide", vals).to_string() static func modulo(divisor: float) -> String: if not is_finite(divisor): @@ -120,7 +120,7 @@ static func modulo(divisor: float) -> String: push_error("Divisor cannot be zero") return "" - return AppwriteOperator.new("modulo", [divisor]).to_string() + return {{spec.title | caseUcfirst}}Operator.new("modulo", [divisor]).to_string() static func power(exponent: float, max: Variant = null) -> String: if not is_finite(exponent): @@ -135,50 +135,50 @@ static func power(exponent: float, max: Variant = null) -> String: if max != null: vals.append(max) - return AppwriteOperator.new("power", vals).to_string() + return {{spec.title | caseUcfirst}}Operator.new("power", vals).to_string() static func array_append(values: Array) -> String: - return AppwriteOperator.new("arrayAppend", values).to_string() + return {{spec.title | caseUcfirst}}Operator.new("arrayAppend", values).to_string() static func array_prepend(values: Array) -> String: - return AppwriteOperator.new("arrayPrepend", values).to_string() + return {{spec.title | caseUcfirst}}Operator.new("arrayPrepend", values).to_string() static func array_insert(index: int, value: Variant) -> String: - return AppwriteOperator.new("arrayInsert", [index, value]).to_string() + return {{spec.title | caseUcfirst}}Operator.new("arrayInsert", [index, value]).to_string() static func array_remove(value: Variant) -> String: - return AppwriteOperator.new("arrayRemove", [value]).to_string() + return {{spec.title | caseUcfirst}}Operator.new("arrayRemove", [value]).to_string() static func array_unique() -> String: - return AppwriteOperator.new("arrayUnique", []).to_string() + return {{spec.title | caseUcfirst}}Operator.new("arrayUnique", []).to_string() static func array_intersect(values: Array) -> String: - return AppwriteOperator.new("arrayIntersect", values).to_string() + return {{spec.title | caseUcfirst}}Operator.new("arrayIntersect", values).to_string() static func array_diff(values: Array) -> String: - return AppwriteOperator.new("arrayDiff", values).to_string() + return {{spec.title | caseUcfirst}}Operator.new("arrayDiff", values).to_string() static func array_filter(condition: String, value: Variant = null) -> String: if not is_valid(condition): push_error("Invalid condition: %s" % condition) return "" - return AppwriteOperator.new("arrayFilter", [condition, value]).to_string() + return {{spec.title | caseUcfirst}}Operator.new("arrayFilter", [condition, value]).to_string() static func string_concat(value: Variant) -> String: - return AppwriteOperator.new("stringConcat", [value]).to_string() + return {{spec.title | caseUcfirst}}Operator.new("stringConcat", [value]).to_string() static func string_replace(search: String, replace: String) -> String: - return AppwriteOperator.new("stringReplace", [search, replace]).to_string() + return {{spec.title | caseUcfirst}}Operator.new("stringReplace", [search, replace]).to_string() static func toggle() -> String: - return AppwriteOperator.new("toggle", []).to_string() + return {{spec.title | caseUcfirst}}Operator.new("toggle", []).to_string() static func date_add_days(days: int) -> String: - return AppwriteOperator.new("dateAddDays", [days]).to_string() + return {{spec.title | caseUcfirst}}Operator.new("dateAddDays", [days]).to_string() static func date_sub_days(days: int) -> String: - return AppwriteOperator.new("dateSubDays", [days]).to_string() + return {{spec.title | caseUcfirst}}Operator.new("dateSubDays", [days]).to_string() static func date_set_now() -> String: - return AppwriteOperator.new("dateSetNow", []).to_string() \ No newline at end of file + return {{spec.title | caseUcfirst}}Operator.new("dateSetNow", []).to_string() \ No newline at end of file diff --git a/templates/godot/addons/utils/service.gd.twig b/templates/godot/addons/utils/service.gd.twig index c5ea77be02..3107f261ca 100644 --- a/templates/godot/addons/utils/service.gd.twig +++ b/templates/godot/addons/utils/service.gd.twig @@ -32,6 +32,9 @@ func _handle_response(result: Dictionary, model_script: Variant = null) -> Varia code = result.body.get("code", result.statusCode) type = result.body.get("type", "") response = str(result.body) + elif result.has("error"): + message = str(result.get("error", "")) + response = message else: message = str(result.get("body", "")) response = str(result.get("body", "")) From 80b67f7e0fe695f86595276b2368acf03043bf7f Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Wed, 27 May 2026 23:22:11 +0530 Subject: [PATCH 55/58] fix: added serialization for multipart --- templates/gdscript/addons/utils/client.gd.twig | 4 ++-- templates/godot/addons/utils/client.gd.twig | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/gdscript/addons/utils/client.gd.twig b/templates/gdscript/addons/utils/client.gd.twig index 5ca7947cb8..4ddde9ef3a 100644 --- a/templates/gdscript/addons/utils/client.gd.twig +++ b/templates/gdscript/addons/utils/client.gd.twig @@ -209,12 +209,12 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: for item in value: body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) body.append_array(("Content-Disposition: form-data; name=\"%s[]\"\r\n\r\n" % key).to_utf8_buffer()) - body.append_array((str(item)).to_utf8_buffer()) + body.append_array((JSON.stringify(_serialize(item))).to_utf8_buffer()) body.append_array(("\r\n").to_utf8_buffer()) else: body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) body.append_array(("Content-Disposition: form-data; name=\"%s\"\r\n\r\n" % key).to_utf8_buffer()) - body.append_array((str(value)).to_utf8_buffer()) + body.append_array((JSON.stringify(_serialize(value))).to_utf8_buffer()) body.append_array(("\r\n").to_utf8_buffer()) body.append_array(("--" + boundary + "--\r\n").to_utf8_buffer()) diff --git a/templates/godot/addons/utils/client.gd.twig b/templates/godot/addons/utils/client.gd.twig index 254ed89dfb..c2d5d81a3f 100644 --- a/templates/godot/addons/utils/client.gd.twig +++ b/templates/godot/addons/utils/client.gd.twig @@ -198,12 +198,12 @@ func _build_multipart(params: Dictionary, boundary: String) -> PackedByteArray: for item in value: body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) body.append_array(("Content-Disposition: form-data; name=\"%s[]\"\r\n\r\n" % key).to_utf8_buffer()) - body.append_array((str(item)).to_utf8_buffer()) + body.append_array((JSON.stringify(_serialize(item))).to_utf8_buffer()) body.append_array(("\r\n").to_utf8_buffer()) else: body.append_array(("--" + boundary + "\r\n").to_utf8_buffer()) body.append_array(("Content-Disposition: form-data; name=\"%s\"\r\n\r\n" % key).to_utf8_buffer()) - body.append_array((str(value)).to_utf8_buffer()) + body.append_array((JSON.stringify(_serialize(value))).to_utf8_buffer()) body.append_array(("\r\n").to_utf8_buffer()) body.append_array(("--" + boundary + "--\r\n").to_utf8_buffer()) From cb52c6c37e734af99c140d6466b2535ba61ad2f7 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Thu, 28 May 2026 14:19:52 +0530 Subject: [PATCH 56/58] fix(oauth): add the null guard to oauth_script load --- templates/gdscript/addons/plugin.gd.twig | 4 ++-- templates/godot/addons/plugin.gd.twig | 2 +- templates/godot/addons/utils/client.gd.twig | 7 +++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/templates/gdscript/addons/plugin.gd.twig b/templates/gdscript/addons/plugin.gd.twig index 1aca810b21..3ee5dd3293 100644 --- a/templates/gdscript/addons/plugin.gd.twig +++ b/templates/gdscript/addons/plugin.gd.twig @@ -2,10 +2,10 @@ extends EditorPlugin const AUTOLOAD_NAME : String = "{{spec.title | caseUcfirst}}" -const AUTOLOAD_PATH : String = "res://addons/{{spec.title | caseSnake}}/{{spec.title | caseLower}}.gd" +const AUTOLOAD_PATH : String = "res://addons/{{spec.title | caseSnake}}/{{spec.title | caseSnake}}.gd" func _enable_plugin() -> void: add_autoload_singleton(AUTOLOAD_NAME, AUTOLOAD_PATH) func _disable_plugin() -> void: - remove_autoload_singleton(AUTOLOAD_NAME) \ No newline at end of file + remove_autoload_singleton(AUTOLOAD_NAME) diff --git a/templates/godot/addons/plugin.gd.twig b/templates/godot/addons/plugin.gd.twig index 60e4561384..ea889b6103 100644 --- a/templates/godot/addons/plugin.gd.twig +++ b/templates/godot/addons/plugin.gd.twig @@ -2,7 +2,7 @@ extends EditorPlugin const AUTOLOAD_NAME : String = "{{spec.title | caseUcfirst}}" -const AUTOLOAD_PATH : String = "res://addons/{{spec.title | caseSnake}}/{{spec.title | caseLower}}.gd" +const AUTOLOAD_PATH : String = "res://addons/{{spec.title | caseSnake}}/{{spec.title | caseSnake }}.gd" func _enable_plugin() -> void: add_autoload_singleton(AUTOLOAD_NAME, AUTOLOAD_PATH) diff --git a/templates/godot/addons/utils/client.gd.twig b/templates/godot/addons/utils/client.gd.twig index c2d5d81a3f..193f7f8110 100644 --- a/templates/godot/addons/utils/client.gd.twig +++ b/templates/godot/addons/utils/client.gd.twig @@ -54,8 +54,11 @@ func set_{{header.key | caseSnake}}(value: String) -> RefCounted: func call_web_api(method: String, path: String = "", params: Dictionary = {}) -> Dictionary: if not _ensure_configured(): return {"statusCode": 0, "error": "missing/wrong configuration. please check logs for more info"} - var oauth_client = load("res://addons/{{ spec.title | caseSnake }}/utils/oauth2.gd").new() - + + var oauth_script = load("res://addons/{{ spec.title | caseSnake }}/utils/oauth2.gd") + if oauth_script == null: + return {"statusCode": 0, "error": "Failed loading OAuth2 script"} + var oauth_client = oauth_script.new() Engine.get_main_loop().root.add_child.call_deferred(oauth_client) await oauth_client.tree_entered From 2b0952c6ad55a1a0c578968c65970346fa269d6b Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Thu, 28 May 2026 15:16:48 +0530 Subject: [PATCH 57/58] refactor(test): removed trailing slash from cp command --- composer.lock | 12 ++++++------ tests/GDScript4Test.php | 2 +- tests/Godot4Test.php | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.lock b/composer.lock index ab8fbe5d11..173f2ee7a0 100644 --- a/composer.lock +++ b/composer.lock @@ -285,16 +285,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/14c5439eec4ccff081ac14eca2dc57feb2a66d92", + "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92", "shasum": "" }, "require": { @@ -346,7 +346,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.1" }, "funding": [ { @@ -446,7 +446,7 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { "name": "twig/twig", diff --git a/tests/GDScript4Test.php b/tests/GDScript4Test.php index 723c417f7d..43c6822e1a 100644 --- a/tests/GDScript4Test.php +++ b/tests/GDScript4Test.php @@ -13,7 +13,7 @@ class GDScript4Test extends Base protected string $class = 'Appwrite\SDK\Language\GDScript'; protected array $build = [ 'cp tests/languages/gdscript/test.gd tests/sdks/gdscript/test.gd', - 'cp -r tests/resources/ tests/sdks/gdscript/tests/', + 'cp -r tests/resources tests/sdks/gdscript/tests/', 'docker run --rm \ -v $(pwd)/tests/sdks/gdscript:/app \ -w /app \ diff --git a/tests/Godot4Test.php b/tests/Godot4Test.php index b7c0e39288..84e9374e81 100644 --- a/tests/Godot4Test.php +++ b/tests/Godot4Test.php @@ -14,7 +14,7 @@ class Godot4Test extends Base protected array $build = [ 'cp tests/languages/godot/test.gd tests/sdks/godot/tests/test.gd', - 'cp -r tests/resources/ tests/sdks/godot/tests/', + 'cp -r tests/resources tests/sdks/godot/tests/', 'docker run --rm \ -v $(pwd)/tests/sdks/godot:/app \ -w /app \ From af48cb3f889de254172cfb5d460b95e91af32f4a Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Thu, 28 May 2026 16:32:32 +0530 Subject: [PATCH 58/58] refactor: removed rendering section from project setting --- templates/gdscript/project.godot.twig | 6 ------ templates/godot/project.godot.twig | 6 ------ 2 files changed, 12 deletions(-) diff --git a/templates/gdscript/project.godot.twig b/templates/gdscript/project.godot.twig index b1a8a4b0a2..5ab1922db2 100644 --- a/templates/gdscript/project.godot.twig +++ b/templates/gdscript/project.godot.twig @@ -19,9 +19,3 @@ config/name="{{spec.title | caseUcfirst}} test" [editor_plugins] enabled=PackedStringArray("res://addons/{{spec.title | caseSnake }}/plugin.cfg") - -[rendering] - -rendering_device/driver.windows="d3d12" -renderer/rendering_method="gl_compatibility" -renderer/rendering_method.mobile="gl_compatibility" diff --git a/templates/godot/project.godot.twig b/templates/godot/project.godot.twig index f3db2d1bda..0f85c9223e 100755 --- a/templates/godot/project.godot.twig +++ b/templates/godot/project.godot.twig @@ -21,9 +21,3 @@ config/icon="res://icon.svg" [editor_plugins] enabled=PackedStringArray("res://addons/{{spec.title | caseSnake }}/plugin.cfg") - -[rendering] - -rendering_device/driver.windows="d3d12" -renderer/rendering_method="gl_compatibility" -renderer/rendering_method.mobile="gl_compatibility"