diff --git a/composer.lock b/composer.lock index eb0e7c2a9..693e434dc 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "matthiasmullie/minify", - "version": "1.3.70", + "version": "1.3.71", "source": { "type": "git", "url": "https://github.com/matthiasmullie/minify.git", - "reference": "2807d9f9bece6877577ad44acb5c801bb3ae536b" + "reference": "ae42a47d7fecc1fbb7277b2f2d84c37a33edc3b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/matthiasmullie/minify/zipball/2807d9f9bece6877577ad44acb5c801bb3ae536b", - "reference": "2807d9f9bece6877577ad44acb5c801bb3ae536b", + "url": "https://api.github.com/repos/matthiasmullie/minify/zipball/ae42a47d7fecc1fbb7277b2f2d84c37a33edc3b1", + "reference": "ae42a47d7fecc1fbb7277b2f2d84c37a33edc3b1", "shasum": "" }, "require": { @@ -67,7 +67,7 @@ ], "support": { "issues": "https://github.com/matthiasmullie/minify/issues", - "source": "https://github.com/matthiasmullie/minify/tree/1.3.70" + "source": "https://github.com/matthiasmullie/minify/tree/1.3.71" }, "funding": [ { @@ -75,7 +75,7 @@ "type": "github" } ], - "time": "2022-12-09T12:56:44+00:00" + "time": "2023-04-25T20:33:03+00:00" }, { "name": "matthiasmullie/path-converter", @@ -297,16 +297,16 @@ }, { "name": "twig/twig", - "version": "v3.5.1", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "a6e0510cc793912b451fd40ab983a1d28f611c15" + "reference": "7e7d5839d4bec168dfeef0ac66d5c5a2edbabffd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/a6e0510cc793912b451fd40ab983a1d28f611c15", - "reference": "a6e0510cc793912b451fd40ab983a1d28f611c15", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/7e7d5839d4bec168dfeef0ac66d5c5a2edbabffd", + "reference": "7e7d5839d4bec168dfeef0ac66d5c5a2edbabffd", "shasum": "" }, "require": { @@ -315,15 +315,10 @@ "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { - "psr/container": "^1.0", + "psr/container": "^1.0|^2.0", "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } - }, "autoload": { "psr-4": { "Twig\\": "src/" @@ -357,7 +352,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.5.1" + "source": "https://github.com/twigphp/Twig/tree/v3.6.1" }, "funding": [ { @@ -369,22 +364,22 @@ "type": "tidelift" } ], - "time": "2023-02-08T07:49:20+00:00" + "time": "2023-06-08T12:52:13+00:00" } ], "packages-dev": [ { "name": "brianium/paratest", - "version": "v6.9.1", + "version": "v6.10.0", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "51691208db882922c55d6c465be3e7d95028c449" + "reference": "c2243b20bcd99c3f651018d1447144372f39b4fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/51691208db882922c55d6c465be3e7d95028c449", - "reference": "51691208db882922c55d6c465be3e7d95028c449", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/c2243b20bcd99c3f651018d1447144372f39b4fa", + "reference": "c2243b20bcd99c3f651018d1447144372f39b4fa", "shasum": "" }, "require": { @@ -451,7 +446,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v6.9.1" + "source": "https://github.com/paratestphp/paratest/tree/v6.10.0" }, "funding": [ { @@ -463,7 +458,7 @@ "type": "paypal" } ], - "time": "2023-03-03T09:35:17+00:00" + "time": "2023-05-25T13:47:58+00:00" }, { "name": "doctrine/instantiator", @@ -716,16 +711,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.15.4", + "version": "v4.16.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290" + "reference": "19526a33fb561ef417e822e85f08a00db4059c17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290", - "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", + "reference": "19526a33fb561ef417e822e85f08a00db4059c17", "shasum": "" }, "require": { @@ -766,9 +761,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" }, - "time": "2023-03-05T19:49:14+00:00" + "time": "2023-06-25T14:52:30+00:00" }, { "name": "phar-io/manifest", @@ -1201,16 +1196,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.6", + "version": "9.6.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b65d59a059d3004a040c16a82e07bbdf6cfdd115" + "reference": "a6d351645c3fe5a30f5e86be6577d946af65a328" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b65d59a059d3004a040c16a82e07bbdf6cfdd115", - "reference": "b65d59a059d3004a040c16a82e07bbdf6cfdd115", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a6d351645c3fe5a30f5e86be6577d946af65a328", + "reference": "a6d351645c3fe5a30f5e86be6577d946af65a328", "shasum": "" }, "require": { @@ -1284,7 +1279,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.6" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.10" }, "funding": [ { @@ -1300,7 +1295,7 @@ "type": "tidelift" } ], - "time": "2023-03-27T11:43:46+00:00" + "time": "2023-07-10T04:04:23+00:00" }, { "name": "psr/container", @@ -1655,16 +1650,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", "shasum": "" }, "require": { @@ -1709,7 +1704,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" }, "funding": [ { @@ -1717,7 +1712,7 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2023-05-07T05:35:17+00:00" }, { "name": "sebastian/environment", @@ -2378,23 +2373,23 @@ }, { "name": "symfony/console", - "version": "v6.2.8", + "version": "v6.3.0", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "3582d68a64a86ec25240aaa521ec8bc2342b369b" + "reference": "8788808b07cf0bdd6e4b7fdd23d8ddb1470c83b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/3582d68a64a86ec25240aaa521ec8bc2342b369b", - "reference": "3582d68a64a86ec25240aaa521ec8bc2342b369b", + "url": "https://api.github.com/repos/symfony/console/zipball/8788808b07cf0bdd6e4b7fdd23d8ddb1470c83b7", + "reference": "8788808b07cf0bdd6e4b7fdd23d8ddb1470c83b7", "shasum": "" }, "require": { "php": ">=8.1", - "symfony/deprecation-contracts": "^2.1|^3", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^1.1|^2|^3", + "symfony/service-contracts": "^2.5|^3", "symfony/string": "^5.4|^6.0" }, "conflict": { @@ -2416,12 +2411,6 @@ "symfony/process": "^5.4|^6.0", "symfony/var-dumper": "^5.4|^6.0" }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" - }, "type": "library", "autoload": { "psr-4": { @@ -2454,7 +2443,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.2.8" + "source": "https://github.com/symfony/console/tree/v6.3.0" }, "funding": [ { @@ -2470,20 +2459,20 @@ "type": "tidelift" } ], - "time": "2023-03-29T21:42:15+00:00" + "time": "2023-05-29T12:49:39+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.2.1", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e" + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", - "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", "shasum": "" }, "require": { @@ -2492,7 +2481,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.3-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -2521,7 +2510,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.2.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0" }, "funding": [ { @@ -2537,7 +2526,7 @@ "type": "tidelift" } ], - "time": "2023-03-01T10:25:55+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/polyfill-intl-grapheme", @@ -2706,16 +2695,16 @@ }, { "name": "symfony/process", - "version": "v6.2.8", + "version": "v6.3.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "75ed64103df4f6615e15a7fe38b8111099f47416" + "reference": "8741e3ed7fe2e91ec099e02446fb86667a0f1628" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/75ed64103df4f6615e15a7fe38b8111099f47416", - "reference": "75ed64103df4f6615e15a7fe38b8111099f47416", + "url": "https://api.github.com/repos/symfony/process/zipball/8741e3ed7fe2e91ec099e02446fb86667a0f1628", + "reference": "8741e3ed7fe2e91ec099e02446fb86667a0f1628", "shasum": "" }, "require": { @@ -2747,7 +2736,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.2.8" + "source": "https://github.com/symfony/process/tree/v6.3.0" }, "funding": [ { @@ -2763,20 +2752,20 @@ "type": "tidelift" } ], - "time": "2023-03-09T16:20:02+00:00" + "time": "2023-05-19T08:06:44+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.2.1", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "a8c9cedf55f314f3a186041d19537303766df09a" + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a8c9cedf55f314f3a186041d19537303766df09a", - "reference": "a8c9cedf55f314f3a186041d19537303766df09a", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", "shasum": "" }, "require": { @@ -2786,13 +2775,10 @@ "conflict": { "ext-psr": "<1.1|>=2" }, - "suggest": { - "symfony/service-implementation": "" - }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.3-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -2832,7 +2818,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.2.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.3.0" }, "funding": [ { @@ -2848,20 +2834,20 @@ "type": "tidelift" } ], - "time": "2023-03-01T10:32:47+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/string", - "version": "v6.2.8", + "version": "v6.3.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "193e83bbd6617d6b2151c37fff10fa7168ebddef" + "reference": "f2e190ee75ff0f5eced645ec0be5c66fac81f51f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/193e83bbd6617d6b2151c37fff10fa7168ebddef", - "reference": "193e83bbd6617d6b2151c37fff10fa7168ebddef", + "url": "https://api.github.com/repos/symfony/string/zipball/f2e190ee75ff0f5eced645ec0be5c66fac81f51f", + "reference": "f2e190ee75ff0f5eced645ec0be5c66fac81f51f", "shasum": "" }, "require": { @@ -2872,13 +2858,13 @@ "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "symfony/translation-contracts": "<2.0" + "symfony/translation-contracts": "<2.5" }, "require-dev": { "symfony/error-handler": "^5.4|^6.0", "symfony/http-client": "^5.4|^6.0", "symfony/intl": "^6.2", - "symfony/translation-contracts": "^2.0|^3.0", + "symfony/translation-contracts": "^2.5|^3.0", "symfony/var-exporter": "^5.4|^6.0" }, "type": "library", @@ -2918,7 +2904,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.2.8" + "source": "https://github.com/symfony/string/tree/v6.3.0" }, "funding": [ { @@ -2934,7 +2920,7 @@ "type": "tidelift" } ], - "time": "2023-03-20T16:06:02+00:00" + "time": "2023-03-21T21:06:29+00:00" }, { "name": "theseer/tokenizer", diff --git a/src/SDK/Language/Dart.php b/src/SDK/Language/Dart.php index 16c704092..d225b3768 100644 --- a/src/SDK/Language/Dart.php +++ b/src/SDK/Language/Dart.php @@ -401,6 +401,61 @@ public function getFiles(): array 'destination' => 'docs/examples/{{service.name | caseLower}}/{{method.name | caseDash}}.md', 'template' => 'dart/docs/example.md.twig', ], + [ + 'scope' => 'service', + 'destination' => '/test/services/{{service.name | caseDash}}_test.dart', + 'template' => 'dart/test/services/service_test.dart.twig', + ], + [ + 'scope' => 'definition', + 'destination' => '/test/src/models/{{definition.name | caseSnake }}_test.dart', + 'template' => 'dart/test/src/models/model_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/id_test.dart', + 'template' => 'dart/test/id_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/permission_test.dart', + 'template' => 'dart/test/permission_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/query_test.dart', + 'template' => 'dart/test/query_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/role_test.dart', + 'template' => 'dart/test/role_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/src/enums_test.dart', + 'template' => 'dart/test/src/enums_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/src/upload_progress_test.dart', + 'template' => 'dart/test/src/upload_progress_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/src/exception_test.dart', + 'template' => 'dart/test/src/exception_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/src/input_file_test.dart', + 'template' => 'dart/test/src/input_file_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/src/response_test.dart', + 'template' => 'dart/test/src/response_test.dart.twig', + ], [ 'scope' => 'default', 'destination' => '.travis.yml', diff --git a/src/SDK/Language/DotNet.php b/src/SDK/Language/DotNet.php index 5c83ed503..709f56ea5 100644 --- a/src/SDK/Language/DotNet.php +++ b/src/SDK/Language/DotNet.php @@ -236,7 +236,7 @@ public function getParamExample(array $param): string if (empty($example) && $example !== 0 && $example !== false) { switch ($type) { case self::TYPE_FILE: - $output .= 'new File("./path-to-files/image.jpg")'; + $output .= 'InputFile.FromPath("./path-to-files/image.jpg")'; break; case self::TYPE_NUMBER: case self::TYPE_INTEGER: diff --git a/src/SDK/Language/Flutter.php b/src/SDK/Language/Flutter.php index 1b5dea379..806e19054 100644 --- a/src/SDK/Language/Flutter.php +++ b/src/SDK/Language/Flutter.php @@ -240,6 +240,86 @@ public function getFiles(): array 'destination' => '/lib/services/{{service.name | caseDash}}.dart', 'template' => 'flutter/lib/services/service.dart.twig', ], + [ + 'scope' => 'service', + 'destination' => '/test/services/{{service.name | caseDash}}_test.dart', + 'template' => 'dart/test/services/service_test.dart.twig', + ], + [ + 'scope' => 'definition', + 'destination' => '/test/src/models/{{definition.name | caseSnake }}_test.dart', + 'template' => 'dart/test/src/models/model_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/id_test.dart', + 'template' => 'dart/test/id_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/permission_test.dart', + 'template' => 'dart/test/permission_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/query_test.dart', + 'template' => 'dart/test/query_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/role_test.dart', + 'template' => 'dart/test/role_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/src/cookie_manager_test.dart', + 'template' => 'flutter/test/src/cookie_manager_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/src/interceptor_test.dart', + 'template' => 'flutter/test/src/interceptor_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/src/realtime_response_test.dart', + 'template' => 'flutter/test/src/realtime_response_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/src/realtime_response_connected_test.dart', + 'template' => 'flutter/test/src/realtime_response_connected_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/src/realtime_subscription_test.dart', + 'template' => 'flutter/test/src/realtime_subscription_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/src/enums_test.dart', + 'template' => 'dart/test/src/enums_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/src/upload_progress_test.dart', + 'template' => 'dart/test/src/upload_progress_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/src/input_file_test.dart', + 'template' => 'dart/test/src/input_file_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/src/exception_test.dart', + 'template' => 'dart/test/src/exception_test.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/test/src/response_test.dart', + 'template' => 'dart/test/src/response_test.dart.twig', + ], [ 'scope' => 'method', 'destination' => 'docs/examples/{{service.name | caseLower}}/{{method.name | caseDash}}.md', diff --git a/src/Spec/Swagger2.php b/src/Spec/Swagger2.php index 202e7dc43..bf343ea83 100644 --- a/src/Spec/Swagger2.php +++ b/src/Spec/Swagger2.php @@ -319,6 +319,7 @@ public function getDefinitions() foreach ($sch['properties'] as $name => $def) { $sch['properties'][$name]['name'] = $name; $sch['properties'][$name]['description'] = $def['description']; + $sch['properties'][$name]['example'] = $def['x-example']; $sch['properties'][$name]['required'] = in_array($name, $sch['required']); if (isset($def['items']['$ref'])) { //nested model diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index cdd1bea2c..b212d9160 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -82,6 +82,7 @@ const client = new Command("client") } let client = new Client().setEndpoint(endpoint); + client.setProject('console'); if (selfSigned || globalConfig.getSelfSigned()) { client.setSelfSigned(true); } diff --git a/templates/dart/README.md.twig b/templates/dart/README.md.twig index 662b88934..beeb8be9b 100644 --- a/templates/dart/README.md.twig +++ b/templates/dart/README.md.twig @@ -2,7 +2,7 @@ [![pub package](https://img.shields.io/pub/v/{{ language.params.packageName }}.svg?style=flat-square)](https://pub.dartlang.org/packages/{{ language.params.packageName }}) ![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) +![Version](https://img.shields.io/badge/api%20version-{{spec.version | split('.') | slice(0,2) | join('.') ~ '.x' | 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 }}) diff --git a/templates/dart/lib/id.dart.twig b/templates/dart/lib/id.dart.twig index 30360f557..4d880aff9 100644 --- a/templates/dart/lib/id.dart.twig +++ b/templates/dart/lib/id.dart.twig @@ -1,13 +1,16 @@ part of {{ language.params.packageName }}; +/// Helper class to generate ID strings for resources. class ID { - ID._(); - - static String unique() { - return 'unique()'; - } + ID._(); - static String custom(String id) { - return id; - } + /// Have Appwrite generate a unique ID for you. + static String unique() { + return 'unique()'; + } + + /// Uses [id] as the ID for the resource. + static String custom(String id) { + return id; + } } \ No newline at end of file diff --git a/templates/dart/lib/models.dart.twig b/templates/dart/lib/models.dart.twig index da68b7f32..1a15137f2 100644 --- a/templates/dart/lib/models.dart.twig +++ b/templates/dart/lib/models.dart.twig @@ -1,3 +1,4 @@ +/// {{spec.title | caseUcfirst}} Models library {{ language.params.packageName }}.models; part 'src/models/model.dart'; diff --git a/templates/dart/lib/package.dart.twig b/templates/dart/lib/package.dart.twig index e184657cb..2ff4077c0 100644 --- a/templates/dart/lib/package.dart.twig +++ b/templates/dart/lib/package.dart.twig @@ -1,3 +1,8 @@ +/// {{spec.title | caseUcfirst}} {{sdk.name}} SDK +/// +/// This SDK is compatible with Appwrite server version {{spec.version | split('.') | slice(0,2) | join('.') ~ '.x' }}. +/// For older versions, please check +/// [previous releases](https://github.com/{{sdk.gitUserName}}/{{sdk.gitRepoName}}/releases). library {{ language.params.packageName }}; import 'dart:async'; diff --git a/templates/dart/lib/permission.dart.twig b/templates/dart/lib/permission.dart.twig index 618f25bb0..746b31ba8 100644 --- a/templates/dart/lib/permission.dart.twig +++ b/templates/dart/lib/permission.dart.twig @@ -1,21 +1,34 @@ part of {{ language.params.packageName }}; +/// Helper class to generate permission strings for resources. class Permission { - Permission._(); + Permission._(); - static String read(String role) { - return 'read("$role")'; - } - static String write(String role) { - return 'write("$role")'; - } - static String create(String role) { - return 'create("$role")'; - } - static String update(String role) { - return 'update("$role")'; - } - static String delete(String role) { - return 'delete("$role")'; - } + /// Read permission for provided [role] + static String read(String role) { + return 'read("$role")'; + } + + /// Write permission for provided [role] + /// + /// This is an alias of update, delete, and possibly create. + /// Don't use write in combination with update, delete, or create. + static String write(String role) { + return 'write("$role")'; + } + + /// Create permission for provided [role] + static String create(String role) { + return 'create("$role")'; + } + + /// Update permission for provided [role] + static String update(String role) { + return 'update("$role")'; + } + + /// Delete permission for provided [role] + static String delete(String role) { + return 'delete("$role")'; + } } diff --git a/templates/dart/lib/query.dart.twig b/templates/dart/lib/query.dart.twig index 8996f1344..249a5f7db 100644 --- a/templates/dart/lib/query.dart.twig +++ b/templates/dart/lib/query.dart.twig @@ -1,61 +1,99 @@ part of {{ language.params.packageName }}; +/// Helper class to generate query strings. class Query { - Query._(); - + Query._(); + + /// Filter resources where [attribute] is equal to [value]. + /// + /// [value] can be a single value or a list. If a list is used + /// the query will return resources where [attribute] is equal + /// to any of the values in the list. static String equal(String attribute, dynamic value) => _addQuery(attribute, 'equal', value); + /// Filter resources where [attribute] is not equal to [value]. + /// + /// [value] can be a single value or a list. If a list is used + /// the query will return resources where [attribute] is equal + /// to any of the values in the list. static String notEqual(String attribute, dynamic value) => _addQuery(attribute, 'notEqual', value); + /// Filter resources where [attribute] is less than [value]. static String lessThan(String attribute, dynamic value) => _addQuery(attribute, 'lessThan', value); + /// Filter resources where [attribute] is less than or equal to [value]. static String lessThanEqual(String attribute, dynamic value) => _addQuery(attribute, 'lessThanEqual', value); + /// Filter resources where [attribute] is greater than [value]. static String greaterThan(String attribute, dynamic value) => _addQuery(attribute, 'greaterThan', value); + /// Filter resources where [attribute] is greater than or equal to [value]. static String greaterThanEqual(String attribute, dynamic value) => _addQuery(attribute, 'greaterThanEqual', value); + /// Filter resources where by searching [attribute] for [value]. static String search(String attribute, String value) => _addQuery(attribute, 'search', value); + /// Filter resources where [attribute] is null. static String isNull(String attribute) => 'isNull("$attribute")'; + /// Filter resources where [attribute] is not null. static String isNotNull(String attribute) => 'isNotNull("$attribute")'; + /// Filter resources where [attribute] is between [start] and [end] (inclusive). static String between(String attribute, dynamic start, dynamic end) => _addQuery(attribute, 'between', [start, end]); + /// Filter resources where [attribute] starts with [value]. static String startsWith(String attribute, String value) => _addQuery(attribute, 'startsWith', value); + /// Filter resources where [attribute] ends with [value]. static String endsWith(String attribute, String value) => _addQuery(attribute, 'endsWith', value); - static String select(List attributes) => 'select([${attributes.map((attr) => "\"$attr\"").join(",")}])'; + /// Specify which attributes should be returned by the API call. + static String select(List attributes) => + 'select([${attributes.map((attr) => "\"$attr\"").join(",")}])'; + /// Sort results by [attribute] ascending. static String orderAsc(String attribute) => 'orderAsc("$attribute")'; + /// Sort results by [attribute] descending. static String orderDesc(String attribute) => 'orderDesc("$attribute")'; + /// Return results before [id]. + /// + /// Refer to the [Cursor Based Pagination]({{sdk.url}}/docs/pagination#cursor-pagination) + /// docs for more information. static String cursorBefore(String id) => 'cursorBefore("$id")'; + /// Return results after [id]. + /// + /// Refer to the [Cursor Based Pagination]({{sdk.url}}/docs/pagination#cursor-pagination) + /// docs for more information. static String cursorAfter(String id) => 'cursorAfter("$id")'; + /// Return only [limit] results. static String limit(int limit) => 'limit($limit)'; + /// Return results from [offset]. + /// + /// Refer to the [Offset Pagination]({{sdk.url}}/docs/pagination#offset-pagination) + /// docs for more information. static String offset(int offset) => 'offset($offset)'; static String _addQuery(String attribute, String method, dynamic value) => (value is List) - ? '$method("$attribute", [${value.map((item) => parseValues(item)).join(",")}])' - : '$method("$attribute", [${parseValues(value)}])'; + ? '$method("$attribute", [${value.map((item) => _parseValues(item)).join(",")}])' + : '$method("$attribute", [${_parseValues(value)}])'; - static String parseValues(dynamic value) => + static String _parseValues(dynamic value) => (value is String) ? '"$value"' : '$value'; -} +} \ No newline at end of file diff --git a/templates/dart/lib/role.dart.twig b/templates/dart/lib/role.dart.twig index 9b4461c85..eea9e3c65 100644 --- a/templates/dart/lib/role.dart.twig +++ b/templates/dart/lib/role.dart.twig @@ -1,12 +1,20 @@ part of {{ language.params.packageName }}; +/// Helper class to generate role strings for [Permission]. class Role { Role._(); + /// Grants access to anyone. + /// + /// This includes authenticated and unauthenticated users. static String any() { return 'any'; } + /// Grants access to a specific user by user ID. + /// + /// You can optionally pass verified or unverified for + /// [status] to target specific types of users. static String user(String id, [String status = '']) { if(status.isEmpty) { return 'user:$id'; @@ -14,6 +22,10 @@ class Role { return 'user:$id/$status'; } + /// Grants access to any authenticated or anonymous user. + /// + /// You can optionally pass verified or unverified for + /// [status] to target specific types of users. static String users([String status = '']) { if(status.isEmpty) { return 'users'; @@ -21,10 +33,17 @@ class Role { return 'users/$status'; } + /// Grants access to any guest user without a session. + /// + /// Authenticated users don't have access to this role. static String guests() { return 'guests'; } + /// Grants access to a team by team ID. + /// + /// You can optionally pass a role for [role] to target + /// team members with the specified role. static String team(String id, [String role = '']) { if(role.isEmpty) { return 'team:$id'; @@ -32,6 +51,10 @@ class Role { return 'team:$id/$role'; } + /// Grants access to a specific member of a team. + /// + /// When the member is removed from the team, they will + /// no longer have access. static String member(String id) { return 'member:$id'; } diff --git a/templates/dart/lib/services/service.dart.twig b/templates/dart/lib/services/service.dart.twig index b92e11ab2..00141e5a3 100644 --- a/templates/dart/lib/services/service.dart.twig +++ b/templates/dart/lib/services/service.dart.twig @@ -8,17 +8,16 @@ part of {{ language.params.packageName }}; {% endmacro %} {%if service.description %} -{{- service.description|dartComment}} +{{- service.description|dartComment | split(' ///') | join('///')}} {% endif %} class {{ service.name | caseUcfirst }} extends Service { {{ service.name | caseUcfirst }}(super.client); - {% for method in service.methods %} + /// {{ method.title }} -{%~ if method.description %} /// +{%~ if method.description %} {{ method.description | dartComment }} - /// {% endif %} {% if method.type == 'location' %}Future{% else %}{% if method.responseModel and method.responseModel != 'any' %}Future{% else %}Future{% endif %}{% endif %} {{ method.name | caseCamel }}({{ _self.method_parameters(method.parameters.all, method.consumes) }}) async { final String path = '{{ method.path }}'{% for parameter in method.parameters.path %}.replaceAll('{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}', {{ parameter.name | caseCamel | overrideIdentifier }}){% endfor %}; @@ -30,7 +29,6 @@ class {{ service.name | caseUcfirst }} extends Service { {% else %} {{ include('dart/base/requests/api.twig') }} {% endif %} - } {% endfor %} } \ No newline at end of file diff --git a/templates/dart/lib/src/client.dart.twig b/templates/dart/lib/src/client.dart.twig index 417670035..6376d9922 100644 --- a/templates/dart/lib/src/client.dart.twig +++ b/templates/dart/lib/src/client.dart.twig @@ -5,31 +5,47 @@ import 'client_stub.dart' import 'response.dart'; import 'upload_progress.dart'; +/// [Client] that handles requests to {{spec.title | caseUcfirst}} abstract class Client { + /// The size for cunked uploads in bytes. static const int CHUNK_SIZE = 5*1024*1024; + + /// Holds configuration such as project. late Map config; late String _endPoint; + /// {{spec.title | caseUcfirst}} endpoint. String get endPoint => _endPoint; + /// Initializes a [Client]. factory Client( {String endPoint = '{{ spec.endpoint }}', bool selfSigned = false}) => createClient(endPoint: endPoint, selfSigned: selfSigned); + /// Set self signed to [status]. + /// + /// If self signed is true, [Client] will ignore invalid certificates. + /// This is helpful in environments where your {{spec.title | caseUcfirst}} + /// instance does not have a valid SSL certificate. Client setSelfSigned({bool status = true}); + /// Set the {{spec.title | caseUcfirst}} endpoint. Client setEndpoint(String endPoint); {% for header in spec.global.headers %} + /// Set {{header.key | caseUcfirst}} {% if header.description %} + /// /// {{header.description}} {% endif %} Client set{{header.key | caseUcfirst}}(value); -{% endfor %} +{% endfor %} + /// Add headers that should be sent with all API calls. Client addHeader(String key, String value); + /// Upload a file in chunks. Future chunkedUpload({ required String path, required Map params, @@ -39,6 +55,7 @@ abstract class Client { Function(UploadProgress)? onProgress, }); + /// Send the API request. Future call(HttpMethod method, { String path = '', Map headers = const {}, diff --git a/templates/dart/lib/src/client_io.dart.twig b/templates/dart/lib/src/client_io.dart.twig index 1b8194c21..d90ba15c0 100644 --- a/templates/dart/lib/src/client_io.dart.twig +++ b/templates/dart/lib/src/client_io.dart.twig @@ -43,7 +43,7 @@ class ClientIO extends ClientBase with ClientMixin { 'x-sdk-platform': '{{ sdk.platform }}', 'x-sdk-language': '{{ language.name | caseLower }}', 'x-sdk-version': '{{ sdk.version }}', - 'user-agent' : '{{spec.title | caseUcfirst}}{{ language.name | caseUcfirst }}SDK/{{ sdk.version }} ({{Platform.operatingSystem}}; {{Platform.operatingSystemVersion}})', + 'user-agent' : '{{spec.title | caseUcfirst}}{{ language.name | caseUcfirst }}SDK/{{ sdk.version }} (${Platform.operatingSystem}; ${Platform.operatingSystemVersion})', {% for key,header in spec.global.defaultHeaders %} '{{key}}' : '{{header}}', {% endfor %} diff --git a/templates/dart/lib/src/enums.dart.twig b/templates/dart/lib/src/enums.dart.twig index 9f84bd939..6566f2e13 100644 --- a/templates/dart/lib/src/enums.dart.twig +++ b/templates/dart/lib/src/enums.dart.twig @@ -1,6 +1,8 @@ +/// HTTP methods. enum HttpMethod { get, post, put, delete, patch } extension HttpMethodString on HttpMethod { + /// Returns the HTTP method in all caps. String name() { return toString().split('.').last.toUpperCase(); } diff --git a/templates/dart/lib/src/exception.dart.twig b/templates/dart/lib/src/exception.dart.twig index b4635f076..dfb1816f4 100644 --- a/templates/dart/lib/src/exception.dart.twig +++ b/templates/dart/lib/src/exception.dart.twig @@ -1,14 +1,23 @@ +/// Exception thrown by the {{language.params.packageName}} package. class {{spec.title | caseUcfirst}}Exception implements Exception { + /// Error message. final String? message; + + /// Error type. + /// + /// See [Error Types]({{sdk.url}}/docs/response-codes#errorTypes) + /// for more information. final String? type; final int? code; final dynamic response; + /// Initializes an {{spec.title | caseUcfirst}} Exception. {{spec.title | caseUcfirst}}Exception([this.message = "", this.code, this.type, this.response]); + /// Returns the error type, message, and code. @override String toString() { - if (message == null) return "{{spec.title | caseUcfirst}}Exception"; - return "{{spec.title | caseUcfirst}}Exception: $type, $message (${code ?? 0})"; + if (message == null || message == "") return "{{spec.title | caseUcfirst}}Exception"; + return "{{spec.title | caseUcfirst}}Exception: ${type ?? ''}, $message (${code ?? 0})"; } } diff --git a/templates/dart/lib/src/input_file.dart.twig b/templates/dart/lib/src/input_file.dart.twig index 5c6170832..70c00bc24 100644 --- a/templates/dart/lib/src/input_file.dart.twig +++ b/templates/dart/lib/src/input_file.dart.twig @@ -1,5 +1,6 @@ import 'exception.dart'; +/// Helper class to handle files. class InputFile { late final String? path; late final List? bytes; @@ -9,13 +10,13 @@ class InputFile { @Deprecated('Use `InputFile.fromPath` or `InputFile.fromBytes` instead.') InputFile({this.path, this.filename, this.contentType, this.bytes}) { if (path == null && bytes == null) { - throw {{ spec.title | caseUcfirst }}Exception('One of `path` or `bytes` is required'); + throw {{ spec.title | caseUcfirst }}Exception('One of `path` or `bytes` is required'); } } InputFile._({this.path, this.filename, this.contentType, this.bytes}) { if (path == null && bytes == null) { - throw {{ spec.title | caseUcfirst }}Exception('One of `path` or `bytes` is required'); + throw {{ spec.title | caseUcfirst }}Exception('One of `path` or `bytes` is required'); } } diff --git a/templates/dart/lib/src/response.dart.twig b/templates/dart/lib/src/response.dart.twig index bf4eea3f1..3bc8b4c79 100644 --- a/templates/dart/lib/src/response.dart.twig +++ b/templates/dart/lib/src/response.dart.twig @@ -1,8 +1,11 @@ import 'dart:convert'; +/// {{spec.title | caseUcfirst}} Response class Response { + /// Initializes a [Response] Response({this.data}); + /// HTTP body returned from {{spec.title | caseUcfirst}} T? data; @override diff --git a/templates/dart/lib/src/upload_progress.dart.twig b/templates/dart/lib/src/upload_progress.dart.twig index 4ed965d2d..44cde383d 100644 --- a/templates/dart/lib/src/upload_progress.dart.twig +++ b/templates/dart/lib/src/upload_progress.dart.twig @@ -1,11 +1,23 @@ import 'dart:convert'; +/// Progress of a File Upload class UploadProgress { + /// ID of the file. final String $id; + + /// Progress percentage. final double progress; + + /// Size uploaded in bytes. final int sizeUploaded; + + /// Total number of chunks. final int chunksTotal; + + /// Number of chunks uploaded. final int chunksUploaded; + + /// Initializes an [UploadProgress] UploadProgress({ required this.$id, required this.progress, @@ -14,6 +26,7 @@ class UploadProgress { required this.chunksUploaded, }); + /// Initializes an [UploadProgress] from a [Map] factory UploadProgress.fromMap(Map map) { return UploadProgress( $id: map['\$id'] ?? '', @@ -24,6 +37,7 @@ class UploadProgress { ); } + /// Converts an [UploadProgress] to a [Map] Map toMap() { return { "\$id": $id, @@ -34,11 +48,14 @@ class UploadProgress { }; } + /// Converts an [UploadProgress] to a JSON [String] String toJson() => json.encode(toMap()); + /// Initializes an [UploadProgress] from a JSON [String] factory UploadProgress.fromJson(String source) => UploadProgress.fromMap(json.decode(source)); + /// Returns a string representation of an [UploadProgress] @override String toString() { return 'UploadProgress(\$id: ${$id}, progress: $progress, sizeUploaded: $sizeUploaded, chunksTotal: $chunksTotal, chunksUploaded: $chunksUploaded)'; diff --git a/templates/dart/pubspec.yaml.twig b/templates/dart/pubspec.yaml.twig index 50968e292..c9b59af20 100644 --- a/templates/dart/pubspec.yaml.twig +++ b/templates/dart/pubspec.yaml.twig @@ -13,3 +13,4 @@ dependencies: dev_dependencies: lints: ^2.0.1 test: ^1.22.0 + mockito: ^5.4.0 diff --git a/templates/dart/test/id_test.dart.twig b/templates/dart/test/id_test.dart.twig new file mode 100644 index 000000000..d86449f8f --- /dev/null +++ b/templates/dart/test/id_test.dart.twig @@ -0,0 +1,20 @@ +import 'package:{{ language.params.packageName }}/{{ language.params.packageName }}.dart'; +{% if 'dart' in language.params.packageName %} +import 'package:test/test.dart'; +{% else %} +import 'package:flutter_test/flutter_test.dart'; +{% endif %} + +void main() { + group('unique()', () { + test('returns unique()', () { + expect(ID.unique(), 'unique()'); + }); + }); + + group('custom()', () { + test('returns the custom string', () { + expect(ID.custom('custom'), 'custom'); + }); + }); +} diff --git a/templates/dart/test/permission_test.dart.twig b/templates/dart/test/permission_test.dart.twig new file mode 100644 index 000000000..c8fa58e00 --- /dev/null +++ b/templates/dart/test/permission_test.dart.twig @@ -0,0 +1,38 @@ +import 'package:{{ language.params.packageName }}/{{ language.params.packageName }}.dart'; +{% if 'dart' in language.params.packageName %} +import 'package:test/test.dart'; +{% else %} +import 'package:flutter_test/flutter_test.dart'; +{% endif %} + +void main() { + group('read()', () { + test('returns read', () { + expect(Permission.read(Role.any()), 'read("any")'); + }); + }); + + group('write()', () { + test('returns write', () { + expect(Permission.write(Role.any()), 'write("any")'); + }); + }); + + group('create()', () { + test('returns create', () { + expect(Permission.create(Role.any()), 'create("any")'); + }); + }); + + group('update()', () { + test('returns update', () { + expect(Permission.update(Role.any()), 'update("any")'); + }); + }); + + group('delete()', () { + test('returns delete', () { + expect(Permission.delete(Role.any()), 'delete("any")'); + }); + }); +} diff --git a/templates/dart/test/query_test.dart.twig b/templates/dart/test/query_test.dart.twig new file mode 100644 index 000000000..9ed2f12e4 --- /dev/null +++ b/templates/dart/test/query_test.dart.twig @@ -0,0 +1,196 @@ +import 'package:{{ language.params.packageName }}/{{ language.params.packageName }}.dart'; +{% if 'dart' in language.params.packageName %} +import 'package:test/test.dart'; +{% else %} +import 'package:flutter_test/flutter_test.dart'; +{% endif %} + +class BasicFilterQueryTest { + final String description; + final dynamic value; + final String expectedValues; + + BasicFilterQueryTest({ + required this.description, + required this.value, + required this.expectedValues, + }); +} + +void main() { + group('basic filter tests', () { + final tests = [ + BasicFilterQueryTest( + description: 'with a string', + value: 's', + expectedValues: '["s"]', + ), + BasicFilterQueryTest( + description: 'with an integer', + value: 1, + expectedValues: '[1]', + ), + BasicFilterQueryTest( + description: 'with a double', + value: 1.2, + expectedValues: '[1.2]', + ), + BasicFilterQueryTest( + description: 'with a whole number double', + value: 1.0, + expectedValues: '[1.0]', + ), + BasicFilterQueryTest( + description: 'with a bool', + value: false, + expectedValues: '[false]', + ), + BasicFilterQueryTest( + description: 'with a list', + value: ['a', 'b', 'c'], + expectedValues: '["a","b","c"]', + ), + ]; + + group('equal()', () { + for (var t in tests) { + test(t.description, () { + expect( + Query.equal('attr', t.value), + 'equal("attr", ${t.expectedValues})', + ); + }); + } + }); + + group('notEqual()', () { + for (var t in tests) { + test(t.description, () { + expect( + Query.notEqual('attr', t.value), + 'notEqual("attr", ${t.expectedValues})', + ); + }); + } + }); + + group('lessThan()', () { + for (var t in tests) { + test(t.description, () { + expect( + Query.lessThan('attr', t.value), + 'lessThan("attr", ${t.expectedValues})', + ); + }); + } + }); + + group('lessThanEqual()', () { + for (var t in tests) { + test(t.description, () { + expect( + Query.lessThanEqual('attr', t.value), + 'lessThanEqual("attr", ${t.expectedValues})', + ); + }); + } + }); + + group('greaterThan()', () { + for (var t in tests) { + test(t.description, () { + expect( + Query.greaterThan('attr', t.value), + 'greaterThan("attr", ${t.expectedValues})', + ); + }); + } + }); + + group('greaterThanEqual()', () { + for (var t in tests) { + test(t.description, () { + expect( + Query.greaterThanEqual('attr', t.value), + 'greaterThanEqual("attr", ${t.expectedValues})', + ); + }); + } + }); + }); + + group('search()', () { + test('returns search', () { + expect(Query.search('attr', 'keyword1 keyword2'), 'search("attr", ["keyword1 keyword2"])'); + }); + }); + + group('isNull()', () { + test('returns isNull', () { + expect(Query.isNull('attr'), 'isNull("attr")'); + }); + }); + + group('isNotNull()', () { + test('returns isNotNull', () { + expect(Query.isNotNull('attr'), 'isNotNull("attr")'); + }); + }); + + group('between()', () { + test('with integers', () { + expect(Query.between('attr', 1, 2), 'between("attr", [1,2])'); + }); + + test('with doubles', () { + expect(Query.between('attr', 1.0, 2.0), 'between("attr", [1.0,2.0])'); + }); + + test('with strings', () { + expect(Query.between('attr', "a", "z"), 'between("attr", ["a","z"])'); + }); + }); + + group('select()', () { + test('returns select', () { + expect(Query.select(['attr1', 'attr2']), 'select(["attr1","attr2"])'); + }); + }); + + group('orderAsc()', () { + test('returns orderAsc', () { + expect(Query.orderAsc('attr'), 'orderAsc("attr")'); + }); + }); + + group('orderDesc()', () { + test('returns orderDesc', () { + expect(Query.orderDesc('attr'), 'orderDesc("attr")'); + }); + }); + + group('cursorBefore()', () { + test('returns cursorBefore', () { + expect(Query.cursorBefore(ID.custom('custom')), 'cursorBefore("custom")'); + }); + }); + + group('cursorAfter()', () { + test('returns cursorAfter', () { + expect(Query.cursorAfter(ID.custom('custom')), 'cursorAfter("custom")'); + }); + }); + + group('limit()', () { + test('returns limit', () { + expect(Query.limit(1), 'limit(1)'); + }); + }); + + group('offset()', () { + test('returns offset', () { + expect(Query.offset(1), 'offset(1)'); + }); + }); +} + diff --git a/templates/dart/test/role_test.dart.twig b/templates/dart/test/role_test.dart.twig new file mode 100644 index 000000000..1c06086b7 --- /dev/null +++ b/templates/dart/test/role_test.dart.twig @@ -0,0 +1,56 @@ +import 'package:{{ language.params.packageName }}/{{ language.params.packageName }}.dart'; +{% if 'dart' in language.params.packageName %} +import 'package:test/test.dart'; +{% else %} +import 'package:flutter_test/flutter_test.dart'; +{% endif %} + +void main() { + group('any()', () { + test('returns any', () { + expect(Role.any(), 'any'); + }); + }); + + group('user()', () { + test('without status', () { + expect(Role.user('custom'), 'user:custom'); + }); + + test('with status', () { + expect(Role.user('custom', 'verified'), 'user:custom/verified'); + }); + }); + + group('users()', () { + test('without status', () { + expect(Role.users(), 'users'); + }); + + test('with status', () { + expect(Role.users('verified'), 'users/verified'); + }); + }); + + group('guests()', () { + test('returns guests', () { + expect(Role.guests(), 'guests'); + }); + }); + + group('team()', () { + test('without role', () { + expect(Role.team('custom'), 'team:custom'); + }); + + test('with role', () { + expect(Role.team('custom', 'owner'), 'team:custom/owner'); + }); + }); + + group('member()', () { + test('returns member', () { + expect(Role.member('custom'), 'member:custom'); + }); + }); +} diff --git a/templates/dart/test/services/service_test.dart.twig b/templates/dart/test/services/service_test.dart.twig new file mode 100644 index 000000000..b5997121a --- /dev/null +++ b/templates/dart/test/services/service_test.dart.twig @@ -0,0 +1,111 @@ +{% import 'flutter/base/utils.twig' as utils %} +{% if 'dart' in language.params.packageName %} +import 'package:test/test.dart'; +{% else %} +import 'package:flutter_test/flutter_test.dart'; +{% endif %} +import 'package:mockito/mockito.dart'; +import 'package:{{ language.params.packageName }}/models.dart' as models; +import 'package:{{ language.params.packageName }}/src/enums.dart'; +import 'package:{{ language.params.packageName }}/src/response.dart'; +import 'dart:typed_data'; +import 'package:{{ language.params.packageName }}/{{ language.params.packageName }}.dart'; + +class MockClient extends Mock implements Client { + Map config = {'project': 'testproject'}; + String endPoint = 'https://localhost/v1'; + @override + Future call( + HttpMethod? method, { + String path = '', + Map headers = const {}, + Map params = const {}, + ResponseType? responseType, + }) async { + return super.noSuchMethod(Invocation.method(#call, [method]), + returnValue: Response()); + } + + @override + Future webAuth( + Uri? url, + { + String? callbackUrlScheme, + } + ) async { + return super.noSuchMethod(Invocation.method(#webAuth, [url]), returnValue: 'done'); + } + + @override + Future chunkedUpload({ + String? path, + Map? params, + String? paramName, + String? idParamName, + Map? headers, + Function(UploadProgress)? onProgress, + }) async { + return super.noSuchMethod(Invocation.method(#chunkedUpload, [path, params, paramName, idParamName, headers]), returnValue: Response(data: {})); + } +} + +void main() { + group('{{service.name | caseUcfirst}} test', () { + late MockClient client; + late {{service.name | caseUcfirst}} {{service.name | caseCamel}}; + + setUp(() { + client = MockClient(); + {{service.name | caseCamel}} = {{service.name | caseUcfirst}}(client); + }); + +{% for method in service.methods %} + test('test method {{method.name | caseCamel}}()', () async { + {%- if method.type == 'webAuth' -%} + {%~ elseif method.type == 'location' -%} + final Uint8List data = Uint8List.fromList([]); + {%- else -%} + + {%~ if method.responseModel and method.responseModel != 'any' ~%} + final Map data = { + {%- for definition in spec.definitions ~%}{%~ if definition.name == method.responseModel -%}{%~ for property in definition.properties | filter((param) => param.required) ~%} + '{{property.name | escapeDollarSign}}': {% if property.type == 'object' %}{}{% elseif property.type == 'array' %}[]{% elseif property.type == 'string' %}'{{property.example | escapeDollarSign}}'{% elseif property.type == 'boolean' %}true{% else %}{{property.example}}{% endif %},{%~ endfor ~%}{% set break = true %}{%- else -%}{% set continue = true %}{%- endif -%}{%~ endfor -%} + + }; + {%~ else ~%} + final data = ''; + {%- endif -%} + {% endif %} + + {%~ if method.type == 'webAuth' ~%} + when(client.webAuth( + Uri(), + )).thenAnswer((_) async => 'done'); + {%~ elseif 'multipart/form-data' in method.consumes ~%} + when(client.chunkedUpload( + path: argThat(isNotNull), + params: argThat(isNotNull), + paramName: argThat(isNotNull), + idParamName: argThat(isNotNull), + headers: argThat(isNotNull), + )).thenAnswer((_) async => Response(data: data)); + {%~ else ~%} + when(client.call( + HttpMethod.{{method.method | caseLower}}, + )).thenAnswer((_) async => Response(data: data)); + {%~ endif ~%} + + final response = await {{service.name | caseCamel}}.{{method.name | caseCamel}}({%~ for parameter in method.parameters.all | filter((param) => param.required) ~%} + {{parameter.name | escapeKeyword | caseCamel}}: {% if parameter.type == 'object' %}{}{% elseif parameter.type == 'array' %}[]{% elseif parameter.type == 'file' %}InputFile.fromPath(path: './image.png'){% elseif parameter.type == 'boolean' %}true{% elseif parameter.type == 'string' %}'{% if parameter.example is not empty %}{{parameter.example | escapeDollarSign}}{% endif %}'{% elseif parameter.type == 'integer' and parameter['x-example'] is empty %}1{% elseif parameter.type == 'number' and parameter['x-example'] is empty %}1.0{% else %}{{parameter.example}}{%~ endif ~%},{%~ endfor ~%} + ); + + {%- if method.type == 'location' ~%} + expect(response, isA()); + {%~ endif ~%}{%~ if method.responseModel and method.responseModel != 'any' ~%} + expect(response, isA()); + {%~ endif ~%} + }); + +{% endfor %} + }); +} \ No newline at end of file diff --git a/templates/dart/test/src/enums_test.dart.twig b/templates/dart/test/src/enums_test.dart.twig new file mode 100644 index 000000000..de09cd6e8 --- /dev/null +++ b/templates/dart/test/src/enums_test.dart.twig @@ -0,0 +1,16 @@ +import 'package:{{ language.params.packageName }}/src/enums.dart'; +{% if 'dart' in language.params.packageName %} +import 'package:test/test.dart'; +{% else %} +import 'package:flutter_test/flutter_test.dart'; +{% endif %} + +void main() { + group('name()', () { + for (final method in HttpMethod.values) { + test('returns ${method.toString().split('.').last.toUpperCase()} for $method', () { + expect(method.name(), method.toString().split('.').last.toUpperCase()); + }); + } + }); +} \ No newline at end of file diff --git a/templates/dart/test/src/exception_test.dart.twig b/templates/dart/test/src/exception_test.dart.twig new file mode 100644 index 000000000..efaf713d5 --- /dev/null +++ b/templates/dart/test/src/exception_test.dart.twig @@ -0,0 +1,21 @@ +import 'package:{{language.params.packageName}}/src/exception.dart'; +{% if 'dart' in language.params.packageName %} +import 'package:test/test.dart'; +{% else %} +import 'package:flutter_test/flutter_test.dart'; +{% endif %} + +void main() { + group('{{spec.title | caseUcfirst}}Exception', () { + test('toString should return correct string representation', () { + final exception1 = {{spec.title | caseUcfirst}}Exception(); + expect(exception1.toString(), equals('{{spec.title | caseUcfirst}}Exception')); + + final exception2 = {{spec.title | caseUcfirst}}Exception('Some error message'); + expect(exception2.toString(), equals('{{spec.title | caseUcfirst}}Exception: , Some error message (0)')); + + final exception3 = {{spec.title | caseUcfirst}}Exception('Invalid request', 400, 'ValidationError'); + expect(exception3.toString(), equals('{{spec.title | caseUcfirst}}Exception: ValidationError, Invalid request (400)')); + }); + }); +} diff --git a/templates/dart/test/src/input_file_test.dart.twig b/templates/dart/test/src/input_file_test.dart.twig new file mode 100644 index 000000000..c51938804 --- /dev/null +++ b/templates/dart/test/src/input_file_test.dart.twig @@ -0,0 +1,51 @@ +{% if 'dart' in language.params.packageName %} +import 'package:test/test.dart'; +{% else %} +import 'package:flutter_test/flutter_test.dart'; +{% endif %} +import 'package:{{language.params.packageName}}/src/exception.dart'; +import 'package:{{language.params.packageName}}/src/input_file.dart'; + +void main() { + group('InputFile', () { + test('throws exception when neither path nor bytes are provided', () { + expect( + () => InputFile(), + throwsA(isA<{{spec.title | caseUcfirst}}Exception>().having( + (e) => e.message, + 'message', + 'One of `path` or `bytes` is required', + )), + ); + }); + + test('throws exception when path and bytes are both null', () { + expect( + () => InputFile(path: null, bytes: null), + throwsA(isA<{{spec.title | caseUcfirst}}Exception>().having( + (e) => e.message, + 'message', + 'One of `path` or `bytes` is required', + )), + ); + }); + + test('creates InputFile from path', () { + final inputFile = InputFile.fromPath(path: '/path/to/file'); + + expect(inputFile.path, '/path/to/file'); + expect(inputFile.filename, isNull); + expect(inputFile.contentType, isNull); + expect(inputFile.bytes, isNull); + }); + + test('creates InputFile from bytes', () { + final inputFile = InputFile.fromBytes(bytes: [1, 2, 3], filename: 'file.txt'); + + expect(inputFile.path, isNull); + expect(inputFile.filename, 'file.txt'); + expect(inputFile.contentType, isNull); + expect(inputFile.bytes, [1, 2, 3]); + }); + }); +} diff --git a/templates/dart/test/src/models/model_test.dart.twig b/templates/dart/test/src/models/model_test.dart.twig new file mode 100644 index 000000000..ef1dad283 --- /dev/null +++ b/templates/dart/test/src/models/model_test.dart.twig @@ -0,0 +1,30 @@ +{% macro sub_schema(property) %}{% if property.sub_schema %}{% if property.type == 'array' %}List<{{property.sub_schema | caseUcfirst | overrideIdentifier}}>{% else %}{{property.sub_schema | caseUcfirst | overrideIdentifier }}{% endif %}{% else %}{% if property.type == 'object' and property.additionalProperties %}Map{% else %}{{property | typeName}}{% endif %}{% endif %}{% endmacro %} +import 'package:{{ language.params.packageName }}/models.dart'; +{% if 'dart' in language.params.packageName %} +import 'package:test/test.dart'; +{% else %} +import 'package:flutter_test/flutter_test.dart'; +{% endif %} + +void main() { + group('{{ definition.name | caseUcfirst | overrideIdentifier }}', () { + + test('model', () { + final model = {{ definition.name | caseUcfirst | overrideIdentifier }}( +{% for property in definition.properties | filter(p => p.required) %} + {{ property.name | escapeKeyword }}: {% if property.type == 'array' %}[]{% elseif property.type == 'object' and (property.sub_schema == 'prefs' or property.sub_schema == 'preferences') %}Preferences(data: {}){% elseif property.type == 'object' %}{}{% elseif property.type == 'string' %}'{{property['x-example'] | escapeDollarSign}}'{% elseif property.type == 'boolean' %}true{% else %}{{property['x-example']}}{% endif %}, +{% endfor %} +{% if definition.additionalProperties %} + data: {}, +{% endif %} + ); + + final map = model.toMap(); + final result = {{ definition.name | caseUcfirst | overrideIdentifier }}.fromMap(map); + +{% for property in definition.properties | filter(p => p.required) %} + expect(result.{{ property.name | escapeKeyword }}{% if property.type == 'object' and (property.sub_schema == 'prefs' or property.sub_schema == 'preferences') %}.data{% endif %}, {% if property.type == 'array' %}[]{% elseif property.type == 'object' and (property.sub_schema == 'prefs' or property.sub_schema == 'preferences') %}{"data": {}}{% elseif property.type == 'object' %}{}{% elseif property.type == 'string' %}'{{property['x-example'] | escapeDollarSign}}'{% elseif property.type == 'boolean' %}true{% else %}{{property['x-example']}}{% endif %}); +{% endfor %} + }); + }); +} diff --git a/templates/dart/test/src/response_test.dart.twig b/templates/dart/test/src/response_test.dart.twig new file mode 100644 index 000000000..d217b4ac0 --- /dev/null +++ b/templates/dart/test/src/response_test.dart.twig @@ -0,0 +1,22 @@ +import 'package:{{ language.params.packageName }}/src/response.dart'; +{% if 'dart' in language.params.packageName %} +import 'package:test/test.dart'; +{% else %} +import 'package:flutter_test/flutter_test.dart'; +{% endif %} + +void main() { + group('toString()', () { + test('with a string', () { + final response = Response(data: 'string'); + + expect(response.toString(), 'string'); + }); + + test('with a map', () { + final response = Response(data: {'x': 1}); + + expect(response.toString(), '{"x":1}'); + }); + }); +} \ No newline at end of file diff --git a/templates/dart/test/src/upload_progress_test.dart.twig b/templates/dart/test/src/upload_progress_test.dart.twig new file mode 100644 index 000000000..606954789 --- /dev/null +++ b/templates/dart/test/src/upload_progress_test.dart.twig @@ -0,0 +1,94 @@ +import 'dart:convert'; +{% if 'dart' in language.params.packageName %} +import 'package:test/test.dart'; +{% else %} +import 'package:flutter_test/flutter_test.dart'; +{% endif %} +import 'package:{{language.params.packageName}}/src/upload_progress.dart'; + +void main() { + group('UploadProgress', () { + final id = '12345'; + final progress = 0.75; + final sizeUploaded = 1024; + final chunksTotal = 10; + final chunksUploaded = 5; + final progressMap = { + "\$id": id, + "progress": progress, + "sizeUploaded": sizeUploaded, + "chunksTotal": chunksTotal, + "chunksUploaded": chunksUploaded + }; + final uploadProgress = UploadProgress( + $id: id, + progress: progress, + sizeUploaded: sizeUploaded, + chunksTotal: chunksTotal, + chunksUploaded: chunksUploaded, + ); + + test('fromMap should create an instance from a map', () { + final result = UploadProgress.fromMap(progressMap); + + expect(result.$id, equals(id)); + expect(result.progress, equals(progress)); + expect(result.sizeUploaded, equals(sizeUploaded)); + expect(result.chunksTotal, equals(chunksTotal)); + expect(result.chunksUploaded, equals(chunksUploaded)); + }); + + test('toMap should return a map representation of the progress', () { + final result = uploadProgress.toMap(); + + expect(result, equals(progressMap)); + }); + + test('toJson and fromJson should convert to/from JSON', () { + final jsonString = uploadProgress.toJson(); + + final result = UploadProgress.fromJson(jsonString); + + expect(result.$id, equals(id)); + expect(result.progress, equals(progress)); + expect(result.sizeUploaded, equals(sizeUploaded)); + expect(result.chunksTotal, equals(chunksTotal)); + expect(result.chunksUploaded, equals(chunksUploaded)); + }); + + test('toString should return a string representation of the progress', () { + final expectedString = + 'UploadProgress(\$id: $id, progress: $progress, sizeUploaded: $sizeUploaded, chunksTotal: $chunksTotal, chunksUploaded: $chunksUploaded)'; + final resultString = uploadProgress.toString(); + + expect(resultString, equals(expectedString)); + }); + + test('equality operator should compare two instances', () { + final uploadProgress2 = UploadProgress( + $id: id, + progress: progress, + sizeUploaded: sizeUploaded, + chunksTotal: chunksTotal, + chunksUploaded: chunksUploaded, + ); + + expect(uploadProgress == uploadProgress2, isTrue); + }); + + test('hashCode should return a unique hash value', () { + final hashCode1 = uploadProgress.hashCode; + + final uploadProgress2 = UploadProgress( + $id: id, + progress: progress, + sizeUploaded: sizeUploaded, + chunksTotal: chunksTotal, + chunksUploaded: chunksUploaded, + ); + final hashCode2 = uploadProgress2.hashCode; + + expect(hashCode1, equals(hashCode2)); + }); + }); +} diff --git a/templates/dotnet/docs/example.md.twig b/templates/dotnet/docs/example.md.twig index d90cde076..2ce25f701 100644 --- a/templates/dotnet/docs/example.md.twig +++ b/templates/dotnet/docs/example.md.twig @@ -1,4 +1,5 @@ using {{ spec.title | caseUcfirst }}; +using {{ spec.title | caseUcfirst }}.Services; using {{ spec.title | caseUcfirst }}.Models; var client = new Client() diff --git a/templates/dotnet/icon.png b/templates/dotnet/icon.png index 460b9f040..dadbae8ba 100644 Binary files a/templates/dotnet/icon.png and b/templates/dotnet/icon.png differ diff --git a/templates/dotnet/src/Appwrite/Appwrite.csproj.twig b/templates/dotnet/src/Appwrite/Appwrite.csproj.twig index fbb3d1b75..74673a1eb 100644 --- a/templates/dotnet/src/Appwrite/Appwrite.csproj.twig +++ b/templates/dotnet/src/Appwrite/Appwrite.csproj.twig @@ -9,6 +9,7 @@ {{sdk.shortDescription}} icon.png + README.md {{spec.licenseName}} {{sdk.gitURL}} git @@ -22,6 +23,7 @@ + diff --git a/templates/dotnet/src/Appwrite/Exception.cs.twig b/templates/dotnet/src/Appwrite/Exception.cs.twig index eb967f973..d78108a12 100644 --- a/templates/dotnet/src/Appwrite/Exception.cs.twig +++ b/templates/dotnet/src/Appwrite/Exception.cs.twig @@ -5,14 +5,17 @@ namespace {{spec.title | caseUcfirst}} public class {{spec.title | caseUcfirst}}Exception : Exception { public int? Code { get; set; } + public string? Type { get; set; } = null; public string? Response { get; set; } = null; public {{spec.title | caseUcfirst}}Exception( string? message = null, int? code = null, + string? type = null, string? response = null) : base(message) { this.Code = code; + this.Type = type this.Response = response; } public {{spec.title | caseUcfirst}}Exception(string message, Exception inner) diff --git a/templates/dotnet/src/Appwrite/Models/Model.cs.twig b/templates/dotnet/src/Appwrite/Models/Model.cs.twig index d30396036..6e83a251a 100644 --- a/templates/dotnet/src/Appwrite/Models/Model.cs.twig +++ b/templates/dotnet/src/Appwrite/Models/Model.cs.twig @@ -40,7 +40,7 @@ namespace {{ spec.title | caseUcfirst }}.Models public static {{ definition.name | caseUcfirst | overrideIdentifier}} From(Dictionary map) => new {{ definition.name | caseUcfirst | overrideIdentifier}}( {%~ for property in definition.properties %} - {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}: {% if property.sub_schema %}{% if property.type == 'array' %}((JArray)map["{{ property.name }}"]).ToObject>>().Select(it => {{property.sub_schema | caseUcfirst | overrideIdentifier}}.From(map: it)).ToList(){% else %}{{property.sub_schema | caseUcfirst | overrideIdentifier}}.From(map: ((JObject)map["{{ property.name }}"]).ToObject>()!){% endif %}{% else %}{% if property.type == 'array' %}((JArray)map["{{ property.name }}"]).ToObject<{{ property | typeName }}>(){% else %}{% if property.type == "integer" or property.type == "number" %}{% if not property.required %}map["{{ property.name }}"] == null ? null : {% endif %}Convert.To{% if property.type == "integer" %}Int64{% else %}Double{% endif %}(map["{{ property.name }}"]){% else %}({{ property | typeName }}{% if not property.required %}?{% endif %})map["{{ property.name }}"]{% endif %}{% endif %}{% endif %}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}: {% if property.sub_schema %}{% if property.type == 'array' %}((JArray)map["{{ property.name }}"]).ToObject>>().Select(it => {{property.sub_schema | caseUcfirst | overrideIdentifier}}.From(map: it)).ToList(){% else %}{{property.sub_schema | caseUcfirst | overrideIdentifier}}.From(map: ((JObject)map["{{ property.name }}"]).ToObject>()!){% endif %}{% else %}{% if property.type == 'array' %}((JArray)map["{{ property.name }}"]).ToObject<{{ property | typeName }}>(){% else %}{% if property.type == "integer" or property.type == "number" %}{% if not property.required %}map["{{ property.name }}"] == null ? null : {% endif %}Convert.To{% if property.type == "integer" %}Int64{% else %}Double{% endif %}(map["{{ property.name }}"]){% else %}{% if property.type == "boolean" %}({{ property | typeName }}{% if not property.required %}?{% endif %})map["{{ property.name }}"]{% else %}map["{{ property.name }}"]{% if not property.required %}?{% endif %}.ToString(){% endif %}{% endif %}{% endif %}{% endif %}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} {%~ endfor %} {%~ if definition.additionalProperties %} diff --git a/templates/flutter/.travis.yml.twig b/templates/flutter/.travis.yml.twig index 1b299c31b..0e2d21508 100644 --- a/templates/flutter/.travis.yml.twig +++ b/templates/flutter/.travis.yml.twig @@ -24,6 +24,6 @@ install: deploy: provider: script skip_cleanup: true - script: cd $TRAVIS_BUILD_DIR && dart format ./lib/ && flutter pub publish -f + script: cd $TRAVIS_BUILD_DIR && dart format ./lib/ && dart format ./test/ && flutter pub publish -f on: tags: true diff --git a/templates/flutter/README.md.twig b/templates/flutter/README.md.twig index cb4af6c67..f63f1988b 100644 --- a/templates/flutter/README.md.twig +++ b/templates/flutter/README.md.twig @@ -2,7 +2,7 @@ [![pub package](https://img.shields.io/pub/v/{{ language.params.packageName }}?style=flat-square)](https://pub.dartlang.org/packages/{{ language.params.packageName }}) ![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) +![Version](https://img.shields.io/badge/api%20version-{{spec.version | split('.') | slice(0,2) | join('.') ~ '.x' | 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 }}) diff --git a/templates/flutter/lib/package.dart.twig b/templates/flutter/lib/package.dart.twig index 90711b99a..9d92197b1 100644 --- a/templates/flutter/lib/package.dart.twig +++ b/templates/flutter/lib/package.dart.twig @@ -1,3 +1,8 @@ +/// {{spec.title | caseUcfirst}} {{sdk.name}} SDK +/// +/// This SDK is compatible with Appwrite server version {{spec.version | split('.') | slice(0,2) | join('.') ~ '.x' }}. +/// For older versions, please check +/// [previous releases](https://github.com/{{sdk.gitUserName}}/{{sdk.gitRepoName}}/releases). library {{ language.params.packageName }}; import 'dart:async'; diff --git a/templates/flutter/lib/services/service.dart.twig b/templates/flutter/lib/services/service.dart.twig index 0adfa9bb4..0cc609d03 100644 --- a/templates/flutter/lib/services/service.dart.twig +++ b/templates/flutter/lib/services/service.dart.twig @@ -8,17 +8,17 @@ part of {{ language.params.packageName }}; {% endmacro %} {%if service.description %} -{{- service.description|dartComment}} +{{- service.description|dartComment | split(' ///') | join('///')}} {% endif %} class {{ service.name | caseUcfirst }} extends Service { + /// Initializes a [{{ service.name | caseUcfirst }}] service {{ service.name | caseUcfirst }}(super.client); {% for method in service.methods %} /// {{ method.title }} -{%~ if method.description %} /// +{%~ if method.description %} {{ method.description|dartComment }} - /// {% endif %} {% if method.type == 'webAuth' %}Future{% elseif method.type == 'location' %}Future{% else %}{% if method.responseModel and method.responseModel != 'any' %}Future{% else %}Future{% endif %}{% endif %} {{ method.name | caseCamel }}({{ _self.method_parameters(method.parameters.all, method.consumes) }}) async { {% if method.parameters.path | length > 0 %}final{% else %}const{% endif %} String path = '{{ method.path }}'{% for parameter in method.parameters.path %}.replaceAll('{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}', {{ parameter.name | caseCamel | overrideIdentifier }}){% endfor %}; diff --git a/templates/flutter/lib/src/client.dart.twig b/templates/flutter/lib/src/client.dart.twig index c910de109..d7e69e96d 100644 --- a/templates/flutter/lib/src/client.dart.twig +++ b/templates/flutter/lib/src/client.dart.twig @@ -5,22 +5,32 @@ import 'client_stub.dart' import 'response.dart'; import 'upload_progress.dart'; +/// [Client] that handles requests to {{spec.title | caseUcfirst}}. +/// +/// The [Client] is also responsible for managing user's sessions. abstract class Client { + /// The size for cunked uploads in bytes. static const int CHUNK_SIZE = 5*1024*1024; + /// Holds configuration such as project. late Map config; late String _endPoint; late String? _endPointRealtime; + /// {{spec.title | caseUcfirst}} endpoint. String get endPoint => _endPoint; + /// {{spec.title | caseUcfirst}} realtime endpoint. String? get endPointRealtime => _endPointRealtime; + /// Initializes a [Client]. factory Client( {String endPoint = '{{ spec.endpoint }}', bool selfSigned = false}) => createClient(endPoint: endPoint, selfSigned: selfSigned); + /// Handle OAuth2 session creation. Future webAuth(Uri url, {String? callbackUrlScheme}); + /// Upload a file in chunks. Future chunkedUpload({ required String path, required Map params, @@ -30,21 +40,32 @@ abstract class Client { Function(UploadProgress)? onProgress, }); + /// Set self signed to [status]. + /// + /// If self signed is true, [Client] will ignore invalid certificates. + /// This is helpful in environments where your {{spec.title | caseUcfirst}} + /// instance does not have a valid SSL certificate. Client setSelfSigned({bool status = true}); + /// Set the {{spec.title | caseUcfirst}} endpoint. Client setEndpoint(String endPoint); + /// Set the {{spec.title | caseUcfirst}} realtime endpoint. Client setEndPointRealtime(String endPoint); {% for header in spec.global.headers %} + /// Set {{header.key | caseUcfirst}}. {% if header.description %} - /// {{header.description}} + /// + /// {{header.description}}. {% endif %} Client set{{header.key | caseUcfirst}}(value); -{% endfor %} +{% endfor %} + /// Add headers that should be sent with all API calls. Client addHeader(String key, String value); + /// Send the API request. Future call(HttpMethod method, { String path = '', Map headers = const {}, diff --git a/templates/flutter/lib/src/realtime.dart.twig b/templates/flutter/lib/src/realtime.dart.twig index f30ad0d2d..fffb7c62b 100644 --- a/templates/flutter/lib/src/realtime.dart.twig +++ b/templates/flutter/lib/src/realtime.dart.twig @@ -5,7 +5,9 @@ import 'realtime_subscription.dart'; import 'service.dart'; import 'client.dart'; +/// Realtime allows you to listen to any events on the server-side in realtime using the subscribe method. abstract class Realtime extends Service { + /// Initializes a [Realtime] service factory Realtime(Client client) => createRealtime(client); /// Subscribes to Appwrite events and returns a `RealtimeSubscription` object, which can be used @@ -42,9 +44,8 @@ abstract class Realtime extends Service { /// RealtimeSubscription subscribe(List channels); - // The [close code][] set when the WebSocket connection is closed. + /// The [close code](https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5) set when the WebSocket connection is closed. /// /// Before the connection has been closed, this will be `null`. - int? get closeCode => null; } diff --git a/templates/flutter/lib/src/realtime_message.dart.twig b/templates/flutter/lib/src/realtime_message.dart.twig index 9e7b8793c..9dc0423f7 100644 --- a/templates/flutter/lib/src/realtime_message.dart.twig +++ b/templates/flutter/lib/src/realtime_message.dart.twig @@ -1,11 +1,24 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; +/// Realtime Message class RealtimeMessage { + /// All permutations of the system event that triggered this message + /// + /// The first event in the list is the most specfic event without wildcards. final List events; + + /// The data related to the event final Map payload; + + /// All channels that match this event final List channels; + + /// ISO 8601 formatted timestamp in UTC timezone in + /// which the event was sent from {{spec.title | caseUcfirst}} final String timestamp; + + /// Initializes a [RealtimeMessage] RealtimeMessage({ required this.events, required this.payload, @@ -13,6 +26,7 @@ class RealtimeMessage { required this.timestamp, }); + /// Returns a copy of this [RealtimeMessage] with specified attributes overridden. RealtimeMessage copyWith({ List? events, Map? payload, @@ -27,6 +41,7 @@ class RealtimeMessage { ); } + /// Returns a [Map] representation of this [RealtimeMessage]. Map toMap() { return { 'events': events, @@ -36,6 +51,7 @@ class RealtimeMessage { }; } + /// Initializes a [RealtimeMessage] from a [Map]. factory RealtimeMessage.fromMap(Map map) { return RealtimeMessage( events: List.from(map['events'] ?? []), @@ -45,11 +61,14 @@ class RealtimeMessage { ); } + /// Converts a [RealtimeMessage] to a JSON [String]. String toJson() => json.encode(toMap()); + /// Initializes a [RealtimeMessage] from a JSON [String]. factory RealtimeMessage.fromJson(String source) => RealtimeMessage.fromMap(json.decode(source)); + /// Returns a string representation of this [RealtimeMessage]. @override String toString() { return 'RealtimeMessage(events: $events, payload: $payload, channels: $channels, timestamp: $timestamp)'; diff --git a/templates/flutter/lib/src/realtime_subscription.dart.twig b/templates/flutter/lib/src/realtime_subscription.dart.twig index b36a86292..e45d3b419 100644 --- a/templates/flutter/lib/src/realtime_subscription.dart.twig +++ b/templates/flutter/lib/src/realtime_subscription.dart.twig @@ -1,8 +1,13 @@ import 'realtime_message.dart'; +/// Realtime Subscription class RealtimeSubscription { + /// Stream of [RealtimeMessage]s final Stream stream; + + /// Closes the subscription final Future Function() close; + /// Initializes a [RealtimeSubscription] RealtimeSubscription({required this.stream, required this.close}); } diff --git a/templates/flutter/pubspec.yaml.twig b/templates/flutter/pubspec.yaml.twig index 0bcb43298..c2463e971 100644 --- a/templates/flutter/pubspec.yaml.twig +++ b/templates/flutter/pubspec.yaml.twig @@ -32,3 +32,4 @@ dev_dependencies: flutter_lints: ^2.0.1 flutter_test: sdk: flutter + mockito: ^5.4.0 \ No newline at end of file diff --git a/templates/flutter/test/src/cookie_manager_test.dart.twig b/templates/flutter/test/src/cookie_manager_test.dart.twig new file mode 100644 index 000000000..78df8709d --- /dev/null +++ b/templates/flutter/test/src/cookie_manager_test.dart.twig @@ -0,0 +1,99 @@ +import 'package:{{ language.params.packageName }}/src/cookie_manager.dart'; +import 'package:cookie_jar/cookie_jar.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart'; + +void main() { + group('getCookies()', () { + test('conversion', () { + List cookies = [ + Cookie('name', 'value'), + Cookie('name2', 'value2'), + ]; + + expect( + CookieManager.getCookies(cookies), + "name=value; name2=value2", + ); + }); + }); + + group('onRequest()', () { + late CookieJar cookieJar; + late CookieManager cookieManager; + + setUp(() { + cookieJar = CookieJar(); + cookieManager = CookieManager(cookieJar); + }); + + test('without cookie', () async { + final request = Request('GET', Uri.parse('{{sdk.url}}')); + await cookieManager.onRequest(request); + expect(request.headers, {}); + }); + + test('with cookie', () async { + final uri = Uri.parse('{{sdk.url}}'); + final cookies = [ + Cookie('name', 'value'), + Cookie('name2', 'value2'), + ]; + cookieJar.saveFromResponse(uri, cookies); + + final request = Request('GET', uri); + await cookieManager.onRequest(request); + expect(request.headers, { + 'cookie': 'name=value; name2=value2' + }); + }); + }); + + group('onResponse()', () { + late CookieJar cookieJar; + late CookieManager cookieManager; + + setUp(() { + cookieJar = CookieJar(); + cookieManager = CookieManager(cookieJar); + }); + + test('without cookie', () async { + final uri = Uri.parse('{{sdk.url}}'); + final request = Request('POST', uri); + final response = Response( + 'body', + 200, + headers: {}, + request: request, + ); + + await cookieManager.onResponse(response); + + final cookies = await cookieJar.loadForRequest(uri); + + expect(cookies, []); + }); + + test('with cookie', () async { + final uri = Uri.parse('{{sdk.url}}'); + final request = Request('POST', uri); + final response = Response( + 'body', + 200, + headers: { + 'set-cookie': 'name=value' + }, + request: request, + ); + + await cookieManager.onResponse(response); + + final cookies = await cookieJar.loadForRequest(uri); + + expect(cookies.length, 1); + expect(cookies.first.name, 'name'); + expect(cookies.first.value, 'value'); + }); + }); +} \ No newline at end of file diff --git a/templates/flutter/test/src/interceptor_test.dart.twig b/templates/flutter/test/src/interceptor_test.dart.twig new file mode 100644 index 000000000..5b9680e5c --- /dev/null +++ b/templates/flutter/test/src/interceptor_test.dart.twig @@ -0,0 +1,39 @@ +import 'dart:async'; +import 'package:http/http.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:{{language.params.packageName}}/src/interceptor.dart'; + +class MockRequest extends Mock implements BaseRequest { + final Map headers = {}; + + @override + Future send() async { + final response = StreamedResponse(ByteStream.fromBytes([]), 200); + response.headers.addAll(headers); + return response; + } +} + +void main() { + group('HeadersInterceptor', () { + test('onRequest should add headers to the request', () async { + final headers = {'Authorization': 'Bearer token123'}; + final interceptor = HeadersInterceptor(headers); + final request = MockRequest(); + + final interceptedRequest = await interceptor.onRequest(request); + + expect(interceptedRequest.headers, equals(headers)); + }); + + test('onResponse should return the same response', () async { + final response = Response('', 200); + final interceptor = HeadersInterceptor({}); + + final interceptedResponse = await interceptor.onResponse(response); + + expect(interceptedResponse, equals(response)); + }); + }); +} diff --git a/templates/flutter/test/src/realtime_response_connected_test.dart.twig b/templates/flutter/test/src/realtime_response_connected_test.dart.twig new file mode 100644 index 000000000..8866121d9 --- /dev/null +++ b/templates/flutter/test/src/realtime_response_connected_test.dart.twig @@ -0,0 +1,68 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:{{language.params.packageName}}/src/realtime_response_connected.dart'; + +void main() { + group('RealtimeResponseConnected', () { + final channels = ['channel1', 'channel2']; + final user = {'id': 123, 'name': 'John Doe'}; + final response1 = RealtimeResponseConnected(channels: channels, user: user); + + test('copyWith should create a new instance with updated properties', () { + final newChannels = ['channel3', 'channel4']; + final newUser = {'id': 456, 'name': 'Jane Smith'}; + + final updatedResponse = response1.copyWith(channels: newChannels, user: newUser); + + expect(updatedResponse.channels, equals(newChannels)); + expect(updatedResponse.user, equals(newUser)); + }); + + test('toMap should return a map representation of the response', () { + final responseMap = response1.toMap(); + + expect(responseMap['channels'], equals(channels)); + expect(responseMap['user'], equals(user)); + }); + + test('fromMap should create an instance from a map', () { + final responseMap = {'channels': channels, 'user': user}; + + final response2 = RealtimeResponseConnected.fromMap(responseMap); + + expect(response2.channels, equals(channels)); + expect(response2.user, equals(user)); + }); + + test('toJson and fromJson should convert to/from JSON', () { + final jsonString = response1.toJson(); + + final response3 = RealtimeResponseConnected.fromJson(jsonString); + + expect(response3.channels, equals(channels)); + expect(response3.user, equals(user)); + }); + + test('toString should return a string representation of the response', () { + final responseString = response1.toString(); + + expect(responseString, equals('RealtimeResponseConnected(channels: $channels, user: $user)')); + }); + + test('equality operator should compare two instances', () { + final response2 = RealtimeResponseConnected(channels: channels, user: user); + + expect(response1 == response2, isTrue); + }); + + test('hashCode should return a unique hash value', () { + final hashCode1 = response1.hashCode; + + final response2 = RealtimeResponseConnected(channels: channels, user: user); + final hashCode2 = response2.hashCode; + + expect(hashCode1, equals(hashCode2)); + }); + }); +} diff --git a/templates/flutter/test/src/realtime_response_test.dart.twig b/templates/flutter/test/src/realtime_response_test.dart.twig new file mode 100644 index 000000000..3713628c9 --- /dev/null +++ b/templates/flutter/test/src/realtime_response_test.dart.twig @@ -0,0 +1,69 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:{{language.params.packageName}}/src/realtime_response.dart'; + +void main() { + group('RealtimeResponse', () { + final type = 'event'; + final data = {'event': 'message', 'payload': 'Hello, world!'}; + final response1 = RealtimeResponse(type: type, data: data); + + test('copyWith should create a new instance with updated properties', () { + final newType = 'response'; + final newData = {'result': true}; + + final updatedResponse = response1.copyWith(type: newType, data: newData); + + expect(updatedResponse.type, equals(newType)); + expect(updatedResponse.data, equals(newData)); + }); + + test('toMap should return a map representation of the response', () { + final responseMap = response1.toMap(); + + expect(responseMap['type'], equals(type)); + expect(responseMap['data'], equals(data)); + }); + + test('fromMap should create an instance from a map', () { + final responseMap = {'type': type, 'data': data}; + + final response2 = RealtimeResponse.fromMap(responseMap); + + expect(response2.type, equals(type)); + expect(response2.data, equals(data)); + }); + + test('toJson and fromJson should convert to/from JSON', () { + final jsonString = response1.toJson(); + + final response3 = RealtimeResponse.fromJson(jsonString); + + expect(response3.type, equals(type)); + expect(response3.data, equals(data)); + }); + + test('toString should return a string representation of the response', () { + final responseString = response1.toString(); + + expect( + responseString, equals('RealtimeResponse(type: $type, data: $data)')); + }); + + test('equality operator should compare two instances', () { + final response2 = RealtimeResponse(type: type, data: data); + + expect(response1 == response2, isTrue); + }); + + test('hashCode should return a unique hash value', () { + final hashCode1 = response1.hashCode; + + final response2 = RealtimeResponse(type: type, data: data); + final hashCode2 = response2.hashCode; + + expect(hashCode1, equals(hashCode2)); + }); + }); +} diff --git a/templates/flutter/test/src/realtime_subscription_test.dart.twig b/templates/flutter/test/src/realtime_subscription_test.dart.twig new file mode 100644 index 000000000..1cf82e163 --- /dev/null +++ b/templates/flutter/test/src/realtime_subscription_test.dart.twig @@ -0,0 +1,21 @@ +import 'package:mockito/mockito.dart'; +import 'package:{{language.params.packageName}}/src/realtime_message.dart'; +import 'package:{{language.params.packageName}}/src/realtime_subscription.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class MockStream extends Mock implements Stream {} + + + +void main() { + group('RealtimeSubscription', () { + final mockStream = MockStream(); + final mockCloseFunction = () async {}; + final subscription = RealtimeSubscription(stream: mockStream, close: mockCloseFunction); + + test('should have the correct stream and close function', () { + expect(subscription.stream, equals(mockStream)); + expect(subscription.close, equals(mockCloseFunction)); + }); + }); +} diff --git a/templates/python/.travis.yml.twig b/templates/python/.travis.yml.twig index 712f791bb..bf9a9dffd 100644 --- a/templates/python/.travis.yml.twig +++ b/templates/python/.travis.yml.twig @@ -1,5 +1,7 @@ language: python +dist: bionic + python: - "3.8" diff --git a/templates/python/base/params.twig b/templates/python/base/params.twig index faf595f08..1e7373295 100644 --- a/templates/python/base/params.twig +++ b/templates/python/base/params.twig @@ -1,7 +1,7 @@ params = {} {% if method.parameters.all | length %} {% for parameter in method.parameters.all %} -{% if parameter.required %} +{% if parameter.required and not parameter.nullable %} if {{ parameter.name | escapeKeyword | caseSnake }} is None: raise {{spec.title | caseUcfirst}}Exception('Missing required parameter: "{{ parameter.name | escapeKeyword | caseSnake }}"') diff --git a/templates/python/package/client.py.twig b/templates/python/package/client.py.twig index 71babb46c..f18116226 100644 --- a/templates/python/package/client.py.twig +++ b/templates/python/package/client.py.twig @@ -50,6 +50,8 @@ class Client: if params is None: params = {} + params = {k: v for k, v in params.items() if v is not None} # Remove None values from params dictionary + data = {} json = {} files = {} diff --git a/templates/python/setup.cfg.twig b/templates/python/setup.cfg.twig index 224a77957..0f94f377b 100644 --- a/templates/python/setup.cfg.twig +++ b/templates/python/setup.cfg.twig @@ -1,2 +1,2 @@ [metadata] -description-file = README.md \ No newline at end of file +description_file = README.md \ No newline at end of file diff --git a/tests/Python310Test.php b/tests/Python310Test.php index a8c5f2cef..945525bb9 100644 --- a/tests/Python310Test.php +++ b/tests/Python310Test.php @@ -14,7 +14,7 @@ class Python310Test extends Base protected array $build = [ 'cp tests/languages/python/tests.py tests/sdks/python/test.py', 'echo "" > tests/sdks/python/__init__.py', - 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor python:3.10 pip install -r tests/sdks/python/requirements.txt --upgrade', + 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor python:3.10-alpine pip install -r tests/sdks/python/requirements.txt --upgrade', ]; protected string $command = 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor --env PYTHONPATH=tests/sdks/python/vendor python:3.10-alpine python tests/sdks/python/test.py'; diff --git a/tests/Python38Test.php b/tests/Python38Test.php index 0fc836224..594f43ffd 100644 --- a/tests/Python38Test.php +++ b/tests/Python38Test.php @@ -14,7 +14,7 @@ class Python38Test extends Base protected array $build = [ 'cp tests/languages/python/tests.py tests/sdks/python/test.py', 'echo "" > tests/sdks/python/__init__.py', - 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor python:3.8 pip install -r tests/sdks/python/requirements.txt --upgrade', + 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor python:3.8-alpine pip install -r tests/sdks/python/requirements.txt --upgrade', ]; protected string $command = 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor --env PYTHONPATH=tests/sdks/python/vendor python:3.8-alpine python tests/sdks/python/test.py'; diff --git a/tests/Python39Test.php b/tests/Python39Test.php index 5dbfe4fcf..0b97471d9 100644 --- a/tests/Python39Test.php +++ b/tests/Python39Test.php @@ -14,7 +14,7 @@ class Python39Test extends Base protected array $build = [ 'cp tests/languages/python/tests.py tests/sdks/python/test.py', 'echo "" > tests/sdks/python/__init__.py', - 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor python:3.9 pip install -r tests/sdks/python/requirements.txt --upgrade', + 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor python:3.9-alpine pip install -r tests/sdks/python/requirements.txt --upgrade', ]; protected string $command = 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor --env PYTHONPATH=tests/sdks/python/vendor python:3.9-alpine python tests/sdks/python/test.py'; diff --git a/tests/languages/cli/test.js b/tests/languages/cli/test.js index 6c814738e..a083b3814 100644 --- a/tests/languages/cli/test.js +++ b/tests/languages/cli/test.js @@ -1,6 +1,6 @@ const { exec, execSync } = require('child_process'); -execSync("node index client --endpoint 'https://stage.appwrite.io/v1' --projectId console --key=35y3h5h345 --selfSigned true", { stdio: 'inherit' }); +execSync("node index client --endpoint 'https://stage.cloud.appwrite.io/v1' --projectId console --key=35y3h5h345 --selfSigned true", { stdio: 'inherit' }); var output; console.log('\nTest Started');