diff --git a/.github/workflows/djlint.yml b/.github/workflows/djlint.yml new file mode 100644 index 0000000000..64cbf2be14 --- /dev/null +++ b/.github/workflows/djlint.yml @@ -0,0 +1,31 @@ +name: Twig Linting + +on: + pull_request: + paths: + - '**.twig' + - '.github/workflows/djlint.yml' + - 'pyproject.toml' + push: + branches: + - master + paths: + - '**.twig' + - '.github/workflows/djlint.yml' + - 'pyproject.toml' + +jobs: + djlint: + name: djLint - Twig Linter + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Run djLint linter + run: | + uvx djlint templates/ --lint diff --git a/.github/workflows/sdk-build-validation.yml b/.github/workflows/sdk-build-validation.yml new file mode 100644 index 0000000000..a983bfb584 --- /dev/null +++ b/.github/workflows/sdk-build-validation.yml @@ -0,0 +1,297 @@ +name: Appwrite SDK Build Validation + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: [pull_request] + +jobs: + generate-and-build: + name: ${{ matrix.sdk }} (${{ matrix.platform }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + # Client SDKs + - sdk: web + platform: client + + - sdk: flutter + platform: client + + - sdk: apple + platform: client + + - sdk: android + platform: client + + - sdk: react-native + platform: client + + # Server SDKs + - sdk: node + platform: server + + - sdk: php + platform: server + + - sdk: python + platform: server + + - sdk: ruby + platform: server + + - sdk: dart + platform: server + + - sdk: go + platform: server + + - sdk: swift + platform: server + + - sdk: dotnet + platform: server + + - sdk: kotlin + platform: server + + # Console SDKs + - sdk: cli + platform: console + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: curl + + - name: Install Composer Dependencies + run: composer install + + - name: Generate SDK for ${{ matrix.sdk }} + run: php example.php ${{ matrix.sdk }} ${{ matrix.platform }} + + # Language-specific setup + - name: Setup Node.js + if: matrix.sdk == 'web' || matrix.sdk == 'node' || matrix.sdk == 'cli' || matrix.sdk == 'react-native' + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup Flutter + if: matrix.sdk == 'flutter' + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + + - name: Setup Swift + if: matrix.sdk == 'apple' || matrix.sdk == 'swift' + run: | + sudo apt-get update + sudo apt-get install -y wget + wget https://download.swift.org/swift-5.9.2-release/ubuntu2204/swift-5.9.2-RELEASE/swift-5.9.2-RELEASE-ubuntu22.04.tar.gz + tar xzf swift-5.9.2-RELEASE-ubuntu22.04.tar.gz + sudo mv swift-5.9.2-RELEASE-ubuntu22.04 /usr/share/swift + echo "/usr/share/swift/usr/bin" >> $GITHUB_PATH + + - name: Setup Java + if: matrix.sdk == 'android' || matrix.sdk == 'kotlin' + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Setup Python + if: matrix.sdk == 'python' + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Setup Ruby + if: matrix.sdk == 'ruby' + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.1' + + - name: Setup Dart + if: matrix.sdk == 'dart' + uses: dart-lang/setup-dart@v1 + with: + sdk: 'stable' + + - name: Setup Go + if: matrix.sdk == 'go' + uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: Setup .NET + if: matrix.sdk == 'dotnet' + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Build SDK + working-directory: examples/${{ matrix.sdk }} + run: | + case "${{ matrix.sdk }}" in + web|node) + npm install + npm run build + ;; + cli) + npm install + npm run linux-x64 + ;; + react-native) + npm install + npm run build || echo "No build script, checking syntax only" + ;; + flutter) + flutter pub get + dart analyze --no-fatal-warnings + ;; + apple|swift) + swift build + ;; + android) + chmod +x ./gradlew || true + ./gradlew build -x lint + ;; + kotlin) + chmod +x ./gradlew || true + ./gradlew build + ;; + php) + composer install + find . -name "*.php" ! -path "./vendor/*" -exec php -l {} + + ;; + python) + pip install -e . + python -m compileall appwrite/ + ;; + ruby) + bundle install + ;; + dart) + dart pub get + dart analyze --no-fatal-warnings + ;; + go) + go mod tidy || true + go build ./... + ;; + dotnet) + dotnet build + ;; + *) + echo "Unknown SDK: ${{ matrix.sdk }}" + exit 1 + ;; + esac + + - name: Run Tests + working-directory: examples/${{ matrix.sdk }} + run: | + case "${{ matrix.sdk }}" in + web|node|cli|react-native) + if [ -f "package.json" ] && grep -q '"test"' package.json; then + # Check if test script is not a placeholder/error message + if grep -q '"test".*"echo.*no test' package.json; then + echo "No tests configured (placeholder script found)" + else + npm test + fi + else + echo "No tests configured in package.json" + fi + ;; + flutter) + if [ -d "test" ] && find test -name "*_test.dart" 2>/dev/null | grep -q .; then + flutter test + else + echo "No Flutter tests found" + fi + ;; + apple|swift) + if [ -d "Tests" ] && find Tests -name "*.swift" 2>/dev/null | grep -q .; then + swift test + else + echo "No Swift tests found" + fi + ;; + android) + if [ -d "library/src/test" ] || [ -d "app/src/test" ]; then + ./gradlew test + else + echo "No Android tests found" + fi + ;; + kotlin) + if [ -d "src/test" ]; then + ./gradlew test + else + echo "No Kotlin tests found" + fi + ;; + php) + if [ -f "vendor/bin/phpunit" ] && [ -d "tests" ]; then + vendor/bin/phpunit tests/ + else + echo "No PHPUnit tests configured" + fi + ;; + python) + if [ -d "tests" ] || find . -maxdepth 2 -name "test_*.py" -o -name "*_test.py" 2>/dev/null | grep -q .; then + python -m pytest + else + echo "No pytest tests found" + fi + ;; + ruby) + if [ -d "test" ] || [ -d "spec" ]; then + if [ -f "Rakefile" ] && grep -q "test" Rakefile; then + bundle exec rake test + elif [ -d "spec" ]; then + bundle exec rspec + else + echo "No Ruby tests configured" + fi + else + echo "No Ruby tests found" + fi + ;; + dart) + if [ -d "test" ] && find test -name "*_test.dart" 2>/dev/null | grep -q .; then + dart test + else + echo "No Dart tests found" + fi + ;; + go) + if find . -name "*_test.go" 2>/dev/null | grep -q .; then + go test ./... + else + echo "No Go tests found" + fi + ;; + dotnet) + if find . -name "*.csproj" -exec grep -l "Microsoft.NET.Test.Sdk" {} \; 2>/dev/null | grep -q .; then + dotnet test + else + echo "No .NET tests configured" + fi + ;; + *) + echo "No tests for SDK: ${{ matrix.sdk }}" + ;; + esac diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1f3a6c753b..6ecda58423 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,6 +6,9 @@ concurrency: on: [pull_request] +env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + jobs: build: runs-on: ubuntu-latest @@ -20,8 +23,6 @@ jobs: CLINode18, DartBeta, DartStable, - Deno1193, - Deno1303, DotNet60, DotNet80, DotNet90, @@ -47,6 +48,7 @@ jobs: Ruby31, AppleSwift56, Swift56, + Unity2021, WebChromium, WebNode ] @@ -96,7 +98,7 @@ jobs: - name: Setup PHP with PECL extension uses: shivammathur/setup-php@v2 with: - php-version: ${{ matrix.php-version }} + php-version: '8.3' extensions: curl - name: Install diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8a1fb06eda..e6cfd7b10f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -122,6 +122,31 @@ docker run --rm -v $(pwd):/app -w /app php:8.3-cli php example.php Check your output files at: /examples/new-lang and make sure the SDK works. When possible, add some unit tests. +## Linting Twig Templates + +We use [djLint](https://djlint.com/) to lint Twig template files for syntax errors and common issues. The linter runs automatically on pull requests. + +**To lint templates locally:** +```bash +composer lint-twig +``` + +**Requirements:** +- [uv](https://github.com/astral-sh/uv) must be installed (for running `uvx` commands) + +**Configuration:** +- Located in `pyproject.toml` +- Only linting is enabled (formatting is disabled to avoid breaking code generation) +- Several rules are ignored as they produce false positives for code generation templates + +**What the linter catches:** +- Template syntax errors +- Missing closing tags +- Extra blank lines +- Basic HTML structure issues + +**Note:** If you encounter linting errors that seem incorrect for code generation templates, please discuss in your PR rather than disabling the linter. + ## SDK Checklist It is very important for us to create a consistent structure and architecture, as well as a language-native feel for the SDKs we generate. diff --git a/README.md b/README.md index 2094a072c1..5617318438 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,19 @@ $sdk->generate(__DIR__ . '/examples/php'); // Generate source code ``` +## Linting Twig Templates + +This project uses [djLint](https://djlint.com/) to lint Twig template files for syntax and common issues. + +**Note:** Formatting is disabled as it breaks code generation syntax. Only linting is used. + +**Available command:** +```bash +composer lint-twig # Check for linting errors +``` + +Requires [uv](https://github.com/astral-sh/uv) to be installed. Configuration is in `pyproject.toml`. The linter runs automatically on pull requests via GitHub Actions. + ## Supported Specs * [Swagger 2](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md) diff --git a/cgch.txt b/cgch.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/composer.json b/composer.json index 12f8f3eebc..868b52cad6 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ "scripts": { "test": "vendor/bin/phpunit", "lint": "vendor/bin/phpcs", - "format": "vendor/bin/phpcbf" + "format": "vendor/bin/phpcbf", + "lint-twig": "uvx djlint templates/ --lint" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 54b0f3840a..59427e00c3 100644 --- a/composer.lock +++ b/composer.lock @@ -801,16 +801,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.1", + "version": "v5.6.2", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" + "reference": "3a454ca033b9e06b63282ce19562e892747449bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb", "shasum": "" }, "require": { @@ -853,9 +853,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" }, - "time": "2025-08-13T20:13:15+00:00" + "time": "2025-10-21T19:32:17+00:00" }, { "name": "phar-io/manifest", @@ -977,16 +977,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "11.0.10", + "version": "11.0.11", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "1a800a7446add2d79cc6b3c01c45381810367d76" + "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1a800a7446add2d79cc6b3c01c45381810367d76", - "reference": "1a800a7446add2d79cc6b3c01c45381810367d76", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", + "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", "shasum": "" }, "require": { @@ -1043,7 +1043,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/show" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11" }, "funding": [ { @@ -1063,7 +1063,7 @@ "type": "tidelift" } ], - "time": "2025-06-18T08:56:18+00:00" + "time": "2025-08-27T14:37:49+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1312,16 +1312,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.34", + "version": "11.5.44", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3e4c6ef395f7cb61a6206c23e0e04b31724174f2" + "reference": "c346885c95423eda3f65d85a194aaa24873cda82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3e4c6ef395f7cb61a6206c23e0e04b31724174f2", - "reference": "3e4c6ef395f7cb61a6206c23e0e04b31724174f2", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c346885c95423eda3f65d85a194aaa24873cda82", + "reference": "c346885c95423eda3f65d85a194aaa24873cda82", "shasum": "" }, "require": { @@ -1335,7 +1335,7 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.10", + "phpunit/php-code-coverage": "^11.0.11", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", @@ -1345,7 +1345,7 @@ "sebastian/comparator": "^6.3.2", "sebastian/diff": "^6.0.2", "sebastian/environment": "^7.2.1", - "sebastian/exporter": "^6.3.0", + "sebastian/exporter": "^6.3.2", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", "sebastian/type": "^5.1.3", @@ -1393,7 +1393,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.34" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.44" }, "funding": [ { @@ -1417,7 +1417,7 @@ "type": "tidelift" } ], - "time": "2025-08-20T14:41:45+00:00" + "time": "2025-11-13T07:17:35+00:00" }, { "name": "psr/container", @@ -1937,16 +1937,16 @@ }, { "name": "sebastian/exporter", - "version": "6.3.0", + "version": "6.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", "shasum": "" }, "require": { @@ -1960,7 +1960,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.1-dev" + "dev-main": "6.3-dev" } }, "autoload": { @@ -2003,15 +2003,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" }, "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/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-12-05T09:17:50+00:00" + "time": "2025-09-24T06:12:51+00:00" }, { "name": "sebastian/global-state", @@ -2448,16 +2460,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.13.2", + "version": "3.13.5", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "5b5e3821314f947dd040c70f7992a64eac89025c" + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c", - "reference": "5b5e3821314f947dd040c70f7992a64eac89025c", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", "shasum": "" }, "require": { @@ -2474,11 +2486,6 @@ "bin/phpcs" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" @@ -2528,7 +2535,7 @@ "type": "thanks_dev" } ], - "time": "2025-06-17T22:17:01+00:00" + "time": "2025-11-04T16:30:35+00:00" }, { "name": "staabm/side-effects-detector", @@ -2584,16 +2591,16 @@ }, { "name": "symfony/console", - "version": "v7.3.2", + "version": "v7.3.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1" + "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5f360ebc65c55265a74d23d7fe27f957870158a1", - "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1", + "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", "shasum": "" }, "require": { @@ -2658,7 +2665,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.2" + "source": "https://github.com/symfony/console/tree/v7.3.6" }, "funding": [ { @@ -2678,7 +2685,7 @@ "type": "tidelift" } ], - "time": "2025-07-30T17:13:41+00:00" + "time": "2025-11-04T01:21:42+00:00" }, { "name": "symfony/polyfill-intl-grapheme", @@ -2849,16 +2856,16 @@ }, { "name": "symfony/process", - "version": "v7.3.0", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", "shasum": "" }, "require": { @@ -2890,7 +2897,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.0" + "source": "https://github.com/symfony/process/tree/v7.3.4" }, "funding": [ { @@ -2901,25 +2908,29 @@ "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": "2025-04-17T09:11:12+00:00" + "time": "2025-09-11T10:12:26+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -2973,7 +2984,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -2984,25 +2995,29 @@ "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": "2025-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/string", - "version": "v7.3.2", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca" + "reference": "f96476035142921000338bad71e5247fbc138872" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca", - "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca", + "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", + "reference": "f96476035142921000338bad71e5247fbc138872", "shasum": "" }, "require": { @@ -3017,7 +3032,6 @@ }, "require-dev": { "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", @@ -3060,7 +3074,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.2" + "source": "https://github.com/symfony/string/tree/v7.3.4" }, "funding": [ { @@ -3080,20 +3094,20 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:47:49+00:00" + "time": "2025-09-11T14:36:48+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -3122,7 +3136,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -3130,7 +3144,7 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], diff --git a/example.php b/example.php index 45464d767e..fc4259c667 100644 --- a/example.php +++ b/example.php @@ -13,7 +13,6 @@ use Appwrite\SDK\Language\Ruby; use Appwrite\SDK\Language\Dart; use Appwrite\SDK\Language\Go; -use Appwrite\SDK\Language\Deno; use Appwrite\SDK\Language\REST; use Appwrite\SDK\Language\Swift; use Appwrite\SDK\Language\Apple; @@ -22,6 +21,7 @@ use Appwrite\SDK\Language\Android; use Appwrite\SDK\Language\Kotlin; use Appwrite\SDK\Language\ReactNative; +use Appwrite\SDK\Language\Unity; try { @@ -37,472 +37,243 @@ function getSSLPage($url) { return $result; } - // Leave the platform you want uncommented - // $platform = 'client'; - $platform = 'console'; - // $platform = 'server'; + function configureSDK($sdk, $overrides = []) { + $defaults = [ + 'name' => 'NAME', + 'version' => '0.0.0', + 'description' => 'Repo description goes here', + 'shortDescription' => 'Repo short description goes here', + 'url' => 'https://example.com', + 'logo' => 'https://appwrite.io/images/logos/logo.svg', + 'licenseContent' => 'test test test', + 'warning' => '**WORK IN PROGRESS - NOT READY FOR USAGE**', + 'changelog' => '**CHANGELOG**', + 'gitUserName' => 'repoowner', + 'gitRepoName' => 'reponame', + 'twitter' => 'appwrite', + 'discord' => ['564160730845151244', 'https://appwrite.io/discord'], + 'defaultHeaders' => ['X-Appwrite-Response-Format' => '1.6.0'], + 'readme' => '**README**', + ]; + + $config = array_merge($defaults, $overrides); + + $sdk->setName($config['name']) + ->setVersion($config['version']) + ->setDescription($config['description']) + ->setShortDescription($config['shortDescription']) + ->setURL($config['url']) + ->setLogo($config['logo']) + ->setLicenseContent($config['licenseContent']) + ->setWarning($config['warning']) + ->setChangelog($config['changelog']) + ->setGitUserName($config['gitUserName']) + ->setGitRepoName($config['gitRepoName']) + ->setTwitter($config['twitter']) + ->setDiscord($config['discord'][0], $config['discord'][1]) + ->setDefaultHeaders($config['defaultHeaders']) + ->setReadme($config['readme']); + + if (isset($config['namespace'])) { + $sdk->setNamespace($config['namespace']); + } + if (isset($config['exclude'])) { + $sdk->setExclude($config['exclude']); + } + + return $sdk; + } + + $requestedSdk = isset($argv[1]) ? $argv[1] : null; + $requestedPlatform = isset($argv[2]) ? $argv[2] : null; + + if ($requestedPlatform) { + $platform = $requestedPlatform; + } else { + $platform = 'console'; + // $platform = 'client'; + // $platform = 'server'; + } + $branch = '1.8.x'; $version = '1.8.x'; - $spec = getSSLPage("https://raw.githubusercontent.com/appwrite/appwrite/{$version}/app/config/specs/swagger2-{$version}-{$platform}.json"); + $spec = getSSLPage("https://raw.githubusercontent.com/appwrite/appwrite/{$branch}/app/config/specs/swagger2-{$version}-{$platform}.json"); if(empty($spec)) { throw new Exception('Failed to fetch spec from Appwrite server'); } + if ($requestedSdk) { + echo "Generating SDK: $requestedSdk (platform: $platform)\n"; + } + // PHP - $php = new PHP(); - $php - ->setComposerVendor('appwrite') - ->setComposerPackage('appwrite'); - $sdk = new SDK($php, new Swagger2($spec)); - - $sdk - ->setName('NAME') - ->setDescription('Repo description goes here') - ->setShortDescription('Repo short description goes here') - ->setURL('https://example.com') - ->setLogo('https://appwrite.io/images/github.png') - ->setLicenseContent('test test test') - ->setWarning('**WORK IN PROGRESS - NOT READY FOR USAGE**') - ->setChangelog('**CHANGELOG**') - ->setGitUserName('repoowner') - ->setGitRepoName('reponame') - ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord') - ->setDefaultHeaders([ - 'X-Appwrite-Response-Format' => '1.6.0', - ]) - ; - - $sdk->generate(__DIR__ . '/examples/php'); + if (!$requestedSdk || $requestedSdk === 'php') { + $php = new PHP(); + $php + ->setComposerVendor('appwrite') + ->setComposerPackage('appwrite'); + $sdk = new SDK($php, new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/php'); + } + + // Unity + if (!$requestedSdk || $requestedSdk === 'unity') { + $sdk = new SDK(new Unity(), new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/unity'); + } // Web - $sdk = new SDK(new Web(), new Swagger2($spec)); - - $sdk - ->setName('NAME') - ->setDescription('Repo description goes here') - ->setShortDescription('Repo short description goes here') - ->setVersion('0.0.0') - ->setURL('https://example.com') - ->setLogo('https://appwrite.io/v1/images/console.png') - ->setLicenseContent('test test test') - ->setWarning('**WORK IN PROGRESS - NOT READY FOR USAGE**') - ->setChangelog('**CHANGELOG**') - ->setReadme("## Getting Started") - ->setGitUserName('repoowner') - ->setGitRepoName('reponame') - ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord') - ->setDefaultHeaders([ - 'X-Appwrite-Response-Format' => '1.6.0', - ]) - ; - - $sdk->generate(__DIR__ . '/examples/web'); - - // Deno - $sdk = new SDK(new Deno(), new Swagger2($spec)); - - $sdk - ->setName('NAME') - ->setDescription('Repo description goes here') - ->setShortDescription('Repo short description goes here') - ->setVersion('0.0.0') - ->setURL('https://example.com') - ->setLogo('https://appwrite.io/v1/images/console.png') - ->setLicenseContent('test test test') - ->setWarning('**WORK IN PROGRESS - NOT READY FOR USAGE**') - ->setChangelog('**CHANGELOG**') - ->setGitUserName('repoowner') - ->setGitRepoName('reponame') - ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord') - ->setDefaultHeaders([ - 'X-Appwrite-Response-Format' => '1.6.0', - ]) - ; - - $sdk->generate(__DIR__ . '/examples/deno'); + if (!$requestedSdk || $requestedSdk === 'web') { + $sdk = new SDK(new Web(), new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/web'); + } // Node - $sdk = new SDK(new Node(), new Swagger2($spec)); - - $sdk - ->setName('NAME') - ->setDescription('Repo description goes here') - ->setShortDescription('Repo short description goes here') - ->setURL('https://example.com') - ->setLogo('https://appwrite.io/v1/images/console.png') - ->setLicenseContent('test test test') - ->setWarning('**WORK IN PROGRESS - NOT READY FOR USAGE**') - ->setChangelog('**CHANGELOG**') - ->setGitUserName('repoowner') - ->setGitRepoName('reponame') - ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord') - ->setDefaultHeaders([ - 'X-Appwrite-Response-Format' => '1.6.0', - ]) - ; - - $sdk->generate(__DIR__ . '/examples/node'); + if (!$requestedSdk || $requestedSdk === 'node') { + $sdk = new SDK(new Node(), new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/node'); + } // CLI - $language = new CLI(); - $language->setNPMPackage('appwrite-cli'); - $language->setExecutableName('appwrite'); - $language->setLogo(json_encode(" - _ _ _ ___ __ _____ + if (!$requestedSdk || $requestedSdk === 'cli') { + $language = new CLI(); + $language->setNPMPackage('appwrite-cli'); + $language->setExecutableName('appwrite'); + $language->setLogo(json_encode(" + _ _ _ ___ __ _____ /_\ _ __ _ ____ ___ __(_) |_ ___ / __\ / / \_ \ //_\\\| '_ \| '_ \ \ /\ / / '__| | __/ _ \ / / / / / /\/ - / _ \ |_) | |_) \ V V /| | | | || __/ / /___/ /___/\/ /_ - \_/ \_/ .__/| .__/ \_/\_/ |_| |_|\__\___| \____/\____/\____/ - |_| |_| + / _ \ |_) | |_) \ V V /| | | | || __/ / /___/ /___/\/ /_ + \_/ \_/ .__/| .__/ \_/\_/ |_| |_|\__\___| \____/\____/\____/ + |_| |_| ")); - $language->setLogoUnescaped(" - _ _ _ ___ __ _____ + $language->setLogoUnescaped(" + _ _ _ ___ __ _____ /_\ _ __ _ ____ ___ __(_) |_ ___ / __\ / / \_ \ //_\\\| '_ \| '_ \ \ /\ / / '__| | __/ _ \ / / / / / /\/ - / _ \ |_) | |_) \ V V /| | | | || __/ / /___/ /___/\/ /_ - \_/ \_/ .__/| .__/ \_/\_/ |_| |_|\__\___| \____/\____/\____/ + / _ \ |_) | |_) \ V V /| | | | || __/ / /___/ /___/\/ /_ + \_/ \_/ .__/| .__/ \_/\_/ |_| |_|\__\___| \____/\____/\____/ |_| |_| "); - $sdk = new SDK($language, new Swagger2($spec)); - - $sdk - ->setName('NAME') - ->setVersion('0.16.0') - ->setDescription('Repo description goes here') - ->setShortDescription('Repo short description goes here') - ->setURL('https://appwrite.io') - ->setLogo('https://appwrite.io/v1/images/console.png') - ->setLicense('BSD-3-Clause') - ->setLicenseContent('test test test') - ->setWarning('**WORK IN PROGRESS - NOT READY FOR USAGE**') - ->setChangelog('**CHANGELOG**') - ->setGitUserName('appwrite') - ->setGitRepoName('sdk-for-cli') - ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord') - ->setDefaultHeaders([ - 'X-Appwrite-Response-Format' => '1.7.0', - ]) - ->setExclude([ - 'services' => [ - ['name' => 'assistant'], - ['name' => 'avatars'], + $sdk = new SDK($language, new Swagger2($spec)); + configureSDK($sdk, [ + 'exclude' => [ + 'services' => [ + ['name' => 'assistant'], + ['name' => 'avatars'], + ], ], - ]) - ; + ]); - $sdk->generate(__DIR__ . '/examples/cli'); + $sdk->generate(__DIR__ . '/examples/cli'); + } // Ruby - $sdk = new SDK(new Ruby(), new Swagger2($spec)); - - $sdk - ->setName('NAME') - ->setDescription('Repo description goes here') - ->setShortDescription('Repo short description goes here') - ->setURL('https://example.com') - ->setLogo('https://appwrite.io/v1/images/console.png') - ->setLicenseContent('test test test') - ->setWarning('**WORK IN PROGRESS - NOT READY FOR USAGE**') - ->setChangelog('**CHANGELOG**') - ->setGitUserName('repoowner') - ->setGitRepoName('reponame') - ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord') - ->setDefaultHeaders([ - 'X-Appwrite-Response-Format' => '1.6.0', - ]) - ; - - $sdk->generate(__DIR__ . '/examples/ruby'); + if (!$requestedSdk || $requestedSdk === 'ruby') { + $sdk = new SDK(new Ruby(), new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/ruby'); + } // Python - $sdk = new SDK(new Python(), new Swagger2($spec)); - - $sdk - ->setName('NAME') - ->setVersion('7.2.0') - ->setDescription('Repo description goes here') - ->setShortDescription('Repo short description goes here') - ->setURL('https://example.com') - ->setLogo('https://appwrite.io/v1/images/console.png') - ->setLicenseContent('test test test') - ->setWarning('**WORK IN PROGRESS - NOT READY FOR USAGE**') - ->setChangelog('**CHANGELOG**') - ->setGitUserName('repoowner') - ->setGitRepoName('reponame') - ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord') - ->setDefaultHeaders([ - 'X-Appwrite-Response-Format' => '1.6.0', - ]) - ; - - $sdk->generate(__DIR__ . '/examples/python'); + if (!$requestedSdk || $requestedSdk === 'python') { + $sdk = new SDK(new Python(), new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/python'); + } // Dart - $dart = new Dart(); - $dart->setPackageName('dart_appwrite'); - - $sdk = new SDK($dart, new Swagger2($spec)); - - $sdk - ->setName('NAME') - ->setDescription('Repo description goes here') - ->setShortDescription('Repo short description goes here') - ->setURL('https://example.com') - ->setLogo('https://appwrite.io/v1/images/console.png') - ->setLicenseContent('test test test') - ->setWarning('**WORK IN PROGRESS - NOT READY FOR USAGE**') - ->setChangelog('**CHANGELOG**') - ->setExamples('**EXAMPLES** ') - ->setVersion('0.0.1') - ->setGitUserName('repoowner') - ->setGitRepoName('reponame') - ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord') - ->setDefaultHeaders([ - 'X-Appwrite-Response-Format' => '1.6.0', - ]) - ; - - $sdk->generate(__DIR__ . '/examples/dart'); + if (!$requestedSdk || $requestedSdk === 'dart') { + $dart = new Dart(); + $dart->setPackageName('dart_appwrite'); + $sdk = new SDK($dart, new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/dart'); + } // Flutter - $flutter = new Flutter(); - $flutter->setPackageName('appwrite'); - $sdk = new SDK($flutter, new Swagger2($spec)); - - $sdk - ->setName('NAME') - ->setDescription('Repo description goes here') - ->setShortDescription('Repo short description goes here') - ->setURL('https://example.com') - ->setLogo('https://appwrite.io/v1/images/console.png') - ->setLicenseContent('test test test') - ->setWarning('**WORK IN PROGRESS - NOT READY FOR USAGE**') - ->setChangelog('**CHANGELOG**') - ->setExamples('**EXAMPLES** ') - ->setVersion('0.0.1') - ->setGitUserName('repoowner') - ->setGitRepoName('reponame') - ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord') - ->setDefaultHeaders([ - 'X-Appwrite-Response-Format' => '1.6.0', - ]) - ; - - $sdk->generate(__DIR__ . '/examples/flutter'); + if (!$requestedSdk || $requestedSdk === 'flutter') { + $flutter = new Flutter(); + $flutter->setPackageName('appwrite'); + $sdk = new SDK($flutter, new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/flutter'); + } // React Native - $reactNative = new ReactNative(); - $reactNative->setNPMPackage('react-native-appwrite'); - $sdk = new SDK($reactNative, new Swagger2($spec)); - - $sdk - ->setName('NAME') - ->setDescription('Repo description goes here') - ->setShortDescription('Repo short description goes here') - ->setURL('https://example.com') - ->setLogo('https://appwrite.io/v1/images/console.png') - ->setLicenseContent('test test test') - ->setWarning('**WORK IN PROGRESS - NOT READY FOR USAGE**') - ->setChangelog('**CHANGELOG**') - ->setExamples('**EXAMPLES** ') - ->setVersion('0.0.1') - ->setGitUserName('repoowner') - ->setGitRepoName('reponame') - ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord') - ->setDefaultHeaders([ - 'X-Appwrite-Response-Format' => '1.6.0', - ]) - ; - - $sdk->generate(__DIR__ . '/examples/react-native'); + if (!$requestedSdk || $requestedSdk === 'react-native') { + $reactNative = new ReactNative(); + $reactNative->setNPMPackage('react-native-appwrite'); + $sdk = new SDK($reactNative, new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/react-native'); + } // GO - $sdk = new SDK(new Go(), new Swagger2($spec)); - - $sdk - ->setName('NAME') - ->setDescription('Repo description goes here') - ->setShortDescription('Repo short description goes here') - ->setURL('https://example.com') - ->setVersion('0.0.1') - ->setGitUserName('appwrite') - ->setGitRepoName('sdk-for-go') - ->setLogo('https://appwrite.io/v1/images/console.png') - ->setLicenseContent('test test test') - ->setWarning('**WORK IN PROGRESS - NOT READY FOR USAGE**') - ->setChangelog('**CHANGELOG**') - ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord') - ->setDefaultHeaders([ - 'X-Appwrite-Response-Format' => '1.6.0', - ]) - ; - - $sdk->generate(__DIR__ . '/examples/go'); - + if (!$requestedSdk || $requestedSdk === 'go') { + $sdk = new SDK(new Go(), new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/go'); + } // Swift - $sdk = new SDK(new Swift(), new Swagger2($spec)); - - $sdk - ->setName('NAME') - ->setDescription('Repo description goes here') - ->setShortDescription('Repo short description goes here') - ->setURL('https://example.com') - ->setLogo('https://appwrite.io/v1/images/console.png') - ->setLicenseContent('test test test') - ->setWarning('**WORK IN PROGRESS - NOT READY FOR USAGE**') - ->setChangelog('**CHANGELOG**') - ->setVersion('0.0.1') - ->setGitUserName('repoowner') - ->setGitRepoName('reponame') - ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord') - ->setDefaultHeaders([ - 'X-Appwrite-Response-Format' => '1.6.0', - ]) - ; - - $sdk->generate(__DIR__ . '/examples/swift'); + if (!$requestedSdk || $requestedSdk === 'swift') { + $sdk = new SDK(new Swift(), new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/swift'); + } // Apple - $sdk = new SDK(new Apple(), new Swagger2($spec)); - - $sdk - ->setName('NAME') - ->setDescription('Repo description goes here') - ->setShortDescription('Repo short description goes here') - ->setURL('https://example.com') - ->setLogo('https://appwrite.io/v1/images/console.png') - ->setLicenseContent('test test test') - ->setWarning('**WORK IN PROGRESS - NOT READY FOR USAGE**') - ->setChangelog('**CHANGELOG**') - ->setVersion('0.0.1') - ->setGitUserName('repoowner') - ->setGitRepoName('reponame') - ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord') - ->setDefaultHeaders([ - 'X-Appwrite-Response-Format' => '1.6.0', - ]) - ; - - $sdk->generate(__DIR__ . '/examples/apple'); + if (!$requestedSdk || $requestedSdk === 'apple') { + $sdk = new SDK(new Apple(), new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/apple'); + } // DotNet - $sdk = new SDK(new DotNet(), new Swagger2($spec)); - - $sdk - ->setName('NAME') - ->setDescription('Repo description goes here') - ->setShortDescription('Repo short description goes here') - ->setURL('https://example.com') - ->setLogo('https://appwrite.io/v1/images/console.png') - ->setLicenseContent('test test test') - ->setWarning('**WORK IN PROGRESS - NOT READY FOR USAGE**') - ->setChangelog('**CHANGELOG**') - ->setVersion('0.0.1') - ->setGitUserName('repoowner') - ->setGitRepoName('reponame') - ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord') - ->setDefaultHeaders([ - 'X-Appwrite-Response-Format' => '1.6.0', - ]) - ; - - $sdk->generate(__DIR__ . '/examples/dotnet'); + if (!$requestedSdk || $requestedSdk === 'dotnet') { + $sdk = new SDK(new DotNet(), new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/dotnet'); + } // REST - $sdk = new SDK(new REST(), new Swagger2($spec)); - - $sdk - ->setName('NAME') - ->setDescription('Repo description goes here') - ->setShortDescription('Repo short description goes here') - ->setURL('https://example.com') - ->setLogo('https://appwrite.io/v1/images/console.png') - ->setLicenseContent('test test test') - ->setWarning('**WORK IN PROGRESS - NOT READY FOR USAGE**') - ->setChangelog('**CHANGELOG**') - ->setVersion('0.0.1') - ->setGitUserName('repoowner') - ->setGitRepoName('reponame') - ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord') - ; - - $sdk->generate(__DIR__ . '/examples/REST'); + if (!$requestedSdk || $requestedSdk === 'rest') { + $sdk = new SDK(new REST(), new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/REST'); + } // Android - $sdk = new SDK(new Android(), new Swagger2($spec)); - - $sdk - ->setName('Android') - ->setNamespace('io appwrite') - ->setDescription('Appwrite is an open-source backend as a service server that abstract and simplify complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Flutter SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to https://appwrite.io/docs') - ->setShortDescription('Appwrite Android SDK') - ->setURL('https://example.com') - ->setGitUserName('appwrite') - ->setGitRepoName('sdk-for-android') - ->setLogo('https://appwrite.io/v1/images/console.png') - ->setLicenseContent('test test test') - ->setWarning('**This SDK is compatible with Appwrite server version 0.7.x. For older versions, please check previous releases.**') - ->setChangelog('**CHANGELOG**') - ->setVersion('0.0.0-SNAPSHOT') - ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord') - ->setDefaultHeaders([ - 'x-appwrite-response-format' => '0.7.0', - ]) - ; - $sdk->generate(__DIR__ . '/examples/android'); + if (!$requestedSdk || $requestedSdk === 'android') { + $sdk = new SDK(new Android(), new Swagger2($spec)); + configureSDK($sdk, [ + 'namespace' => 'io.appwrite', + ]); + $sdk->generate(__DIR__ . '/examples/android'); + } // Kotlin - $sdk = new SDK(new Kotlin(), new Swagger2($spec)); - - $sdk - ->setName('Kotlin') - ->setNamespace('io appwrite') - ->setDescription('Appwrite is an open-source backend as a service server that abstract and simplify complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Flutter SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to https://appwrite.io/docs') - ->setShortDescription('Appwrite Kotlin SDK') - ->setURL('https://example.com') - ->setGitUserName('appwrite') - ->setGitRepoName('sdk-for-kotlin') - ->setLogo('https://appwrite.io/v1/images/console.png') - ->setLicenseContent('test test test') - ->setWarning('**This SDK is compatible with Appwrite server version 0.7.x. For older versions, please check previous releases.**') - ->setChangelog('**CHANGELOG**') - ->setVersion('0.0.0-SNAPSHOT') - ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord') - ->setDefaultHeaders([ - 'x-appwrite-response-format' => '0.8.0', - ]) - ; - $sdk->generate(__DIR__ . '/examples/kotlin'); + if (!$requestedSdk || $requestedSdk === 'kotlin') { + $sdk = new SDK(new Kotlin(), new Swagger2($spec)); + configureSDK($sdk, [ + 'namespace' => 'io.appwrite', + ]); + $sdk->generate(__DIR__ . '/examples/kotlin'); + } // GraphQL - $sdk = new SDK(new GraphQL(), new Swagger2($spec)); - - $sdk - ->setName('GraphQL') - ->setDescription('Appwrite is an open-source backend as a service server that abstract and simplify complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Flutter SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to https://appwrite.io/docs') - ->setLogo('https://appwrite.io/v1/images/console.png') - ; - $sdk->generate(__DIR__ . '/examples/graphql'); + if (!$requestedSdk || $requestedSdk === 'graphql') { + $sdk = new SDK(new GraphQL(), new Swagger2($spec)); + configureSDK($sdk); + $sdk->generate(__DIR__ . '/examples/graphql'); + } } catch (Exception $exception) { echo 'Error: ' . $exception->getMessage() . ' on ' . $exception->getFile() . ':' . $exception->getLine() . "\n"; diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..4bdd9cceae --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[tool.djlint] + +profile="jinja" +extension="twig" + +ignore="H012,H025,H030,H031,T001,T002,T027,T032" +# H012: spaces around = - false positive for TypeScript generics like +# H025: orphan tags - false positive for code generics like , , +# H030/H031: meta description/keywords (not applicable to SDK code templates) +# T001: whitespace in tags - {%~ is intentional Twig whitespace control +# T002: double quotes - templates need flexibility with quote styles +# T027: unclosed strings - false positive for ${{ secrets.X }} in GitHub Actions +# T032: extra whitespace in tags - often intentional for formatting/line continuation + +exclude=".git,vendor,tests/sdks,node_modules,examples" + +use_gitignore=true +max_line_length=1200 diff --git a/src/SDK/Language.php b/src/SDK/Language.php index 496c8ac2b9..099a530663 100644 --- a/src/SDK/Language.php +++ b/src/SDK/Language.php @@ -32,6 +32,25 @@ abstract public function getKeywords(): array; */ abstract public function getIdentifierOverrides(): array; + /** + * Get the static access operator for the language (e.g. '::' for PHP, '.' for JS) + * @return string + */ + abstract public function getStaticAccessOperator(): string; + + /** + * Get the string quote character for the language (e.g. '"' for PHP, "'" for JS) + * @return string + */ + abstract public function getStringQuote(): string; + + /** + * Wrap elements in an array syntax for the language + * @param string $elements Comma-separated elements + * @return string + */ + abstract public function getArrayOf(string $elements): string; + /** * @return array */ @@ -51,9 +70,10 @@ abstract public function getParamDefault(array $param): string; /** * @param array $param + * @param string $lang Optional language variant (for multi-language SDKs) * @return string */ - abstract public function getParamExample(array $param): string; + abstract public function getParamExample(array $param, string $lang = ''): string; /** * @param string $key @@ -137,4 +157,124 @@ protected function toUpperSnakeCase($str): string { return \strtoupper($this->toSnakeCase($str)); } + + public function isPermissionString(string $string): bool + { + $pattern = '/^\["(read|update|delete|write)\(\\"[^\\"]+\\"\)"(,\s*"(read|update|delete|write)\(\\"[^\\"]+\\"\)")*\]$/'; + return preg_match($pattern, $string) === 1; + } + + public function extractPermissionParts(string $string): array + { + $inner = substr($string, 1, -1); + preg_match_all('/"(read|update|delete|write)\(\\"([^\\"]+)\\"\)"/', $inner, $matches, PREG_SET_ORDER); + + $result = []; + foreach ($matches as $match) { + $action = $match[1]; + $roleString = $match[2]; + + $role = null; + $id = null; + $innerRole = null; + + if (strpos($roleString, ':') !== false) { + $role = explode(':', $roleString, 2)[0]; + $idString = explode(':', $roleString, 2)[1]; + + if (strpos($idString, '/') !== false) { + $id = explode('/', $idString, 2)[0]; + $innerRole = explode('/', $idString, 2)[1]; + } else { + $id = $idString; + } + } else { + $role = $roleString; + } + + $result[] = [ + 'action' => $action, + 'role' => $role, + 'id' => $id ?? null, + 'innerRole' => $innerRole + ]; + } + + return $result; + } + + public function hasPermissionParam(array $parameters): bool + { + foreach ($parameters as $param) { + $example = $param['example'] ?? ''; + if (!empty($example) && is_string($example) && $this->isPermissionString($example)) { + return true; + } + } + return false; + } + + /** + * Get the prefix for Permission and Role classes (e.g., 'sdk.' for Node) + * @return string + */ + protected function getPermissionPrefix(): string + { + return ''; + } + + /** + * Transform permission action name for language-specific casing + * Override in child classes if needed (e.g., DotNet uses ucfirst) + * @param string $action + * @return string + */ + protected function transformPermissionAction(string $action): string + { + return $action; + } + + /** + * Transform permission role name for language-specific casing + * Override in child classes if needed (e.g., DotNet uses ucfirst) + * @param string $role + * @return string + */ + protected function transformPermissionRole(string $role): string + { + return $role; + } + + /** + * Generate permission example code for the language + * @param string $example Permission string example + * @return string + */ + public function getPermissionExample(string $example): string + { + $permissions = []; + $staticOp = $this->getStaticAccessOperator(); + $quote = $this->getStringQuote(); + $prefix = $this->getPermissionPrefix(); + + foreach ($this->extractPermissionParts($example) as $permission) { + $args = []; + if ($permission['id'] !== null) { + $args[] = $quote . $permission['id'] . $quote; + } + if ($permission['innerRole'] !== null) { + $args[] = $quote . $permission['innerRole'] . $quote; + } + $argsString = implode(', ', $args); + + $action = $permission['action']; + $role = $permission['role']; + $action = $this->transformPermissionAction($action); + $role = $this->transformPermissionRole($role); + + $permissions[] = $prefix . 'Permission' . $staticOp . $action . '(' . $prefix . 'Role' . $staticOp . $role . '(' . $argsString . '))'; + } + + return $this->getArrayOf(implode(', ', $permissions)); + } } diff --git a/src/SDK/Language/Android.php b/src/SDK/Language/Android.php index c9430f03d8..19a22f01a3 100644 --- a/src/SDK/Language/Android.php +++ b/src/SDK/Language/Android.php @@ -125,6 +125,11 @@ public function getFiles(): array 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/Query.kt', 'template' => '/android/library/src/main/java/io/package/Query.kt.twig', ], + [ + 'scope' => 'default', + 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/Operator.kt', + 'template' => '/android/library/src/main/java/io/package/Operator.kt.twig', + ], [ 'scope' => 'default', 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/exceptions/{{spec.title | caseUcfirst}}Exception.kt', diff --git a/src/SDK/Language/Apple.php b/src/SDK/Language/Apple.php index 9b8f5521bf..4313f9d34a 100644 --- a/src/SDK/Language/Apple.php +++ b/src/SDK/Language/Apple.php @@ -75,6 +75,11 @@ public function getFiles(): array 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Query.swift', 'template' => 'swift/Sources/Query.swift.twig', ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Operator.swift', + 'template' => 'swift/Sources/Operator.swift.twig', + ], [ 'scope' => 'default', 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Models/UploadProgress.swift', @@ -219,7 +224,7 @@ public function getFiles(): array [ 'scope' => 'default', 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Services/Realtime.swift', - 'template' => '/swift/Sources/Services/Realtime.swift.twig', + 'template' => '/apple/Sources/Services/Realtime.swift.twig', ], [ 'scope' => 'default', diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index 5cd135dade..db72e8778b 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -377,9 +377,10 @@ public function getTypeName(array $parameter, array $spec = []): string /** * @param array $param + * @param string $lang * @return string */ - public function getParamExample(array $param): string + public function getParamExample(array $param, string $lang = ''): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/Dart.php b/src/SDK/Language/Dart.php index ad12c97fc1..6ad8efbaa2 100644 --- a/src/SDK/Language/Dart.php +++ b/src/SDK/Language/Dart.php @@ -121,6 +121,21 @@ public function getIdentifierOverrides(): array ]; } + public function getStaticAccessOperator(): string + { + return '.'; + } + + public function getStringQuote(): string + { + return "'"; + } + + public function getArrayOf(string $elements): string + { + return '[' . $elements . ']'; + } + /** * @param array $parameter * @return string @@ -219,9 +234,10 @@ public function getParamDefault(array $param): string /** * @param array $param + * @param string $lang * @return string */ - public function getParamExample(array $param): string + public function getParamExample(array $param, string $lang = ''): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; @@ -240,7 +256,8 @@ public function getParamExample(array $param): string } return match ($type) { - self::TYPE_ARRAY, self::TYPE_FILE, self::TYPE_INTEGER, self::TYPE_NUMBER => $example, + self::TYPE_ARRAY => $this->isPermissionString($example) ? $this->getPermissionExample($example) : $example, + self::TYPE_FILE, self::TYPE_INTEGER, self::TYPE_NUMBER => $example, self::TYPE_BOOLEAN => ($example) ? 'true' : 'false', self::TYPE_OBJECT => ($decoded = json_decode($example, true)) !== null ? (empty($decoded) && $example === '{}' @@ -342,6 +359,11 @@ public function getFiles(): array 'destination' => '/lib/query.dart', 'template' => 'dart/lib/query.dart.twig', ], + [ + 'scope' => 'default', + 'destination' => '/lib/operator.dart', + 'template' => 'dart/lib/operator.dart.twig', + ], [ 'scope' => 'default', 'destination' => '/lib/{{ language.params.packageName }}.dart', @@ -432,6 +454,11 @@ public function getFiles(): array 'destination' => '/test/query_test.dart', 'template' => 'dart/test/query_test.dart.twig', ], + [ + 'scope' => 'default', + 'destination' => '/test/operator_test.dart', + 'template' => 'dart/test/operator_test.dart.twig', + ], [ 'scope' => 'default', 'destination' => '/test/role_test.dart', diff --git a/src/SDK/Language/Deno.php b/src/SDK/Language/Deno.php index c7e4df328a..c3145c60de 100644 --- a/src/SDK/Language/Deno.php +++ b/src/SDK/Language/Deno.php @@ -12,6 +12,21 @@ public function getName(): string return 'Deno'; } + public function getStaticAccessOperator(): string + { + return '.'; + } + + public function getStringQuote(): string + { + return "'"; + } + + public function getArrayOf(string $elements): string + { + return '[' . $elements . ']'; + } + /** * @return array */ @@ -63,11 +78,21 @@ public function getFiles(): array 'destination' => 'src/query.ts', 'template' => 'deno/src/query.ts.twig', ], + [ + 'scope' => 'default', + 'destination' => 'src/operator.ts', + 'template' => 'deno/src/operator.ts.twig', + ], [ 'scope' => 'default', 'destination' => 'test/query.test.ts', 'template' => 'deno/test/query.test.ts.twig', ], + [ + 'scope' => 'default', + 'destination' => 'test/operator.test.ts', + 'template' => 'deno/test/operator.test.ts.twig', + ], [ 'scope' => 'default', 'destination' => 'src/inputFile.ts', @@ -156,9 +181,10 @@ public function getTypeName(array $parameter, array $spec = []): string /** * @param array $param + * @param string $lang * @return string */ - public function getParamExample(array $param): string + public function getParamExample(array $param, string $lang = ''): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; @@ -176,7 +202,8 @@ public function getParamExample(array $param): string } return match ($type) { - self::TYPE_ARRAY, self::TYPE_INTEGER, self::TYPE_NUMBER => $example, + self::TYPE_ARRAY => $this->isPermissionString($example) ? $this->getPermissionExample($example) : $example, + self::TYPE_INTEGER, self::TYPE_NUMBER => $example, self::TYPE_FILE => 'InputFile.fromPath(\'/path/to/file.png\', \'file.png\')', self::TYPE_BOOLEAN => ($example) ? 'true' : 'false', self::TYPE_OBJECT => ($example === '{}') diff --git a/src/SDK/Language/DotNet.php b/src/SDK/Language/DotNet.php index bb9f4312a7..c9fce13e26 100644 --- a/src/SDK/Language/DotNet.php +++ b/src/SDK/Language/DotNet.php @@ -4,6 +4,7 @@ use Appwrite\SDK\Language; use Twig\TwigFilter; +use Twig\TwigFunction; class DotNet extends Language { @@ -145,6 +146,31 @@ public function getIdentifierOverrides(): array ]; } + public function getStaticAccessOperator(): string + { + return '.'; + } + + public function getStringQuote(): string + { + return '"'; + } + + public function getArrayOf(string $elements): string + { + return 'new List { ' . $elements . ' }'; + } + + protected function transformPermissionAction(string $action): string + { + return ucfirst($action); + } + + protected function transformPermissionRole(string $role): string + { + return ucfirst($role); + } + public function getPropertyOverrides(): array { return [ @@ -235,9 +261,10 @@ public function getParamDefault(array $param): string /** * @param array $param + * @param string $lang * @return string */ - public function getParamExample(array $param): string + public function getParamExample(array $param, string $lang = ''): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; @@ -281,9 +308,11 @@ public function getParamExample(array $param): string case self::TYPE_FILE: case self::TYPE_NUMBER: case self::TYPE_INTEGER: - case self::TYPE_ARRAY: $output .= $example; break; + case self::TYPE_ARRAY: + $output .= $this->isPermissionString($example) ? $this->getPermissionExample($example) : $example; + break; case self::TYPE_OBJECT: if ($example === '{}') { $output .= '[object]'; @@ -380,6 +409,11 @@ public function getFiles(): array 'destination' => '{{ spec.title | caseUcfirst }}/Query.cs', 'template' => 'dotnet/Package/Query.cs.twig', ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}/Operator.cs', + 'template' => 'dotnet/Package/Operator.cs.twig', + ], [ 'scope' => 'default', 'destination' => '{{ spec.title | caseUcfirst }}/Role.cs', @@ -439,6 +473,85 @@ public function getFiles(): array 'scope' => 'default', 'destination' => '{{ spec.title | caseUcfirst }}/Enums/IEnum.cs', 'template' => 'dotnet/Package/Enums/IEnum.cs.twig', + ], + // Tests + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/{{ spec.title | caseUcfirst }}.Tests.csproj', + 'template' => 'dotnet/Package.Tests/Tests.csproj.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/.gitignore', + 'template' => 'dotnet/Package.Tests/.gitignore', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/ClientTests.cs', + 'template' => 'dotnet/Package.Tests/ClientTests.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/IDTests.cs', + 'template' => 'dotnet/Package.Tests/IDTests.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/PermissionTests.cs', + 'template' => 'dotnet/Package.Tests/PermissionTests.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/RoleTests.cs', + 'template' => 'dotnet/Package.Tests/RoleTests.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/QueryTests.cs', + 'template' => 'dotnet/Package.Tests/QueryTests.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/ExceptionTests.cs', + 'template' => 'dotnet/Package.Tests/ExceptionTests.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/UploadProgressTests.cs', + 'template' => 'dotnet/Package.Tests/UploadProgressTests.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/Models/InputFileTests.cs', + 'template' => 'dotnet/Package.Tests/Models/InputFileTests.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/Converters/ObjectToInferredTypesConverterTests.cs', + 'template' => 'dotnet/Package.Tests/Converters/ObjectToInferredTypesConverterTests.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/Converters/ValueClassConverterTests.cs', + 'template' => 'dotnet/Package.Tests/Converters/ValueClassConverterTests.cs.twig', + ], + // Tests for each definition (model) + [ + 'scope' => 'definition', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/Models/{{ definition.name | caseUcfirst | overrideIdentifier }}Tests.cs', + 'template' => 'dotnet/Package.Tests/Models/ModelTests.cs.twig', + ], + // Tests for each enum + [ + 'scope' => 'enum', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/Enums/{{ enum.name | caseUcfirst | overrideIdentifier }}Tests.cs', + 'template' => 'dotnet/Package.Tests/Enums/EnumTests.cs.twig', + ], + // Tests for each service + [ + 'scope' => 'service', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/Services/{{service.name | caseUcfirst}}Tests.cs', + 'template' => 'dotnet/Package.Tests/Services/ServiceTests.cs.twig', ] ]; } @@ -462,6 +575,148 @@ public function getFilters(): array } return $property; }), + new TwigFilter('escapeCsString', function ($value) { + if (is_string($value)) { + return addcslashes($value, '\\"'); + } + return $value; + }), + ]; + } + + /** + * get sub_scheme, property_name and parse_value functions + * @return TwigFunction[] + */ + public function getFunctions(): array + { + return [ + new TwigFunction('sub_schema', function (array $property) { + $result = ''; + + if (isset($property['sub_schema']) && !empty($property['sub_schema'])) { + if ($property['type'] === 'array') { + $result = 'List<' . $this->toPascalCase($property['sub_schema']) . '>'; + } else { + $result = $this->toPascalCase($property['sub_schema']); + } + } elseif (isset($property['enum']) && !empty($property['enum'])) { + $enumName = $property['enumName'] ?? $property['name']; + $result = $this->toPascalCase($enumName); + } else { + $result = $this->getTypeName($property); + } + + if (!($property['required'] ?? true)) { + $result .= '?'; + } + + return $result; + }, ['is_safe' => ['html']]), + new TwigFunction('test_item_type', function (array $property) { + // For test templates: returns the item type for arrays without the List<> wrapper + $result = ''; + + if (isset($property['sub_schema']) && !empty($property['sub_schema'])) { + // Model type + $result = $this->toPascalCase($property['sub_schema']); + $result = 'Appwrite.Models.' . $result; + } elseif (isset($property['enum']) && !empty($property['enum'])) { + // Enum type + $enumName = $property['enumName'] ?? $property['name']; + $result = 'Appwrite.Enums.' . $this->toPascalCase($enumName); + } elseif (isset($property['items']) && isset($property['items']['type'])) { + // Primitive array type (for definitions) + $result = $this->getTypeName($property['items']); + } elseif (isset($property['array']) && isset($property['array']['type'])) { + // Primitive array type (for method parameters) + $result = $this->getTypeName($property['array']); + } else { + $result = 'object'; + } + + return $result; + }, ['is_safe' => ['html']]), + new TwigFunction('property_name', function (array $definition, array $property) { + $name = $property['name']; + $name = \str_replace('$', '', $name); + $name = $this->toPascalCase($name); + if (\in_array($name, $this->getKeywords())) { + $name = '@' . $name; + } + return $name; + }), + new TwigFunction('parse_value', function (array $property, string $mapAccess, string $v) { + $required = $property['required'] ?? false; + + // Handle sub_schema + if (isset($property['sub_schema']) && !empty($property['sub_schema'])) { + $subSchema = \ucfirst($property['sub_schema']); + + if ($property['type'] === 'array') { + $src = $required ? $mapAccess : $v; + return "{$src}.ToEnumerable().Select(it => {$subSchema}.From(map: (Dictionary)it)).ToList()"; + } else { + if ($required) { + return "{$subSchema}.From(map: (Dictionary){$mapAccess})"; + } + return "({$v} as Dictionary) is { } obj ? {$subSchema}.From(map: obj) : null"; + } + } + + // Handle enum + if (isset($property['enum']) && !empty($property['enum'])) { + $enumName = $property['enumName'] ?? $property['name']; + $enumClass = \ucfirst($enumName); + + if ($required) { + return "new {$enumClass}({$mapAccess}.ToString())"; + } + return "{$v} == null ? null : new {$enumClass}({$v}.ToString())"; + } + + // Handle arrays + if ($property['type'] === 'array') { + $itemsType = $property['items']['type'] ?? 'object'; + $src = $required ? $mapAccess : $v; + + $selectExpression = match ($itemsType) { + 'string' => 'x.ToString()', + 'integer' => 'Convert.ToInt64(x)', + 'number' => 'Convert.ToDouble(x)', + 'boolean' => '(bool)x', + default => 'x' + }; + + return "{$src}.ToEnumerable().Select(x => {$selectExpression}).ToList()"; + } + + // Handle integer/number + if ($property['type'] === 'integer' || $property['type'] === 'number') { + $convertMethod = $property['type'] === 'integer' ? 'Int64' : 'Double'; + + if ($required) { + return "Convert.To{$convertMethod}({$mapAccess})"; + } + return "{$v} == null ? null : Convert.To{$convertMethod}({$v})"; + } + + // Handle boolean + if ($property['type'] === 'boolean') { + $typeName = $this->getTypeName($property); + + if ($required) { + return "({$typeName}){$mapAccess}"; + } + return "({$typeName}?){$v}"; + } + + // Handle string type + if ($required) { + return "{$mapAccess}.ToString()"; + } + return "{$v}?.ToString()"; + }, ['is_safe' => ['html']]), ]; } diff --git a/src/SDK/Language/Flutter.php b/src/SDK/Language/Flutter.php index 986bceafd9..ab0c5ec3a5 100644 --- a/src/SDK/Language/Flutter.php +++ b/src/SDK/Language/Flutter.php @@ -85,6 +85,11 @@ public function getFiles(): array 'destination' => '/lib/query.dart', 'template' => 'dart/lib/query.dart.twig', ], + [ + 'scope' => 'default', + 'destination' => '/lib/operator.dart', + 'template' => 'dart/lib/operator.dart.twig', + ], [ 'scope' => 'definition', 'destination' => '/lib/src/models/{{definition.name | caseSnake }}.dart', @@ -275,6 +280,11 @@ public function getFiles(): array 'destination' => '/test/query_test.dart', 'template' => 'dart/test/query_test.dart.twig', ], + [ + 'scope' => 'default', + 'destination' => '/test/operator_test.dart', + 'template' => 'dart/test/operator_test.dart.twig', + ], [ 'scope' => 'default', 'destination' => '/test/role_test.dart', diff --git a/src/SDK/Language/Go.php b/src/SDK/Language/Go.php index 1129139a80..f32a118f80 100644 --- a/src/SDK/Language/Go.php +++ b/src/SDK/Language/Go.php @@ -44,6 +44,21 @@ public function getKeywords(): array ]; } + public function getStaticAccessOperator(): string + { + return '.'; + } + + public function getStringQuote(): string + { + return '"'; + } + + public function getArrayOf(string $elements): string + { + return '[' . $elements . ']'; + } + /** * @return array */ @@ -98,6 +113,11 @@ public function getFiles(): array 'destination' => 'query/query.go', 'template' => 'go/query.go.twig', ], + [ + 'scope' => 'default', + 'destination' => 'operator/operator.go', + 'template' => 'go/operator.go.twig', + ], [ 'scope' => 'default', 'destination' => 'permission/permission.go', @@ -218,9 +238,10 @@ public function getParamDefault(array $param): string /** * @param array $param + * @param string $lang * @return string */ - public function getParamExample(array $param): string + public function getParamExample(array $param, string $lang = ''): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/GraphQL.php b/src/SDK/Language/GraphQL.php index 0f82bf0c81..19b581e9b6 100644 --- a/src/SDK/Language/GraphQL.php +++ b/src/SDK/Language/GraphQL.php @@ -12,6 +12,21 @@ public function getName(): string return 'GraphQL'; } + public function getStaticAccessOperator(): string + { + return '.'; + } + + public function getStringQuote(): string + { + return '"'; + } + + public function getArrayOf(string $elements): string + { + return '[' . $elements . ']'; + } + /** * @param $type * @return string @@ -113,9 +128,10 @@ public function getParamDefault(array $param): string /** * @param array $param + * @param string $lang * @return string */ - public function getParamExample(array $param): string + public function getParamExample(array $param, string $lang = ''): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/Kotlin.php b/src/SDK/Language/Kotlin.php index edd8206a49..0f0abe6093 100644 --- a/src/SDK/Language/Kotlin.php +++ b/src/SDK/Language/Kotlin.php @@ -99,6 +99,21 @@ public function getIdentifierOverrides(): array return []; } + public function getStaticAccessOperator(): string + { + return '.'; + } + + public function getStringQuote(): string + { + return '"'; + } + + public function getArrayOf(string $elements): string + { + return 'listOf(' . $elements . ')'; + } + /** * @param array $parameter * @param array $spec @@ -190,9 +205,10 @@ public function getParamDefault(array $param): string /** * @param array $param + * @param string $lang Language variant: 'kotlin' (default) or 'java' * @return string */ - public function getParamExample(array $param): string + public function getParamExample(array $param, string $lang = 'kotlin'): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; @@ -215,10 +231,12 @@ public function getParamExample(array $param): string $output .= "\"\""; break; case self::TYPE_OBJECT: - $output .= 'mapOf( "a" to "b" )'; + $output .= $lang === 'java' + ? 'Map.of("a", "b")' + : 'mapOf( "a" to "b" )'; break; case self::TYPE_ARRAY: - $output .= 'listOf()'; + $output .= $lang === 'java' ? 'List.of()' : 'listOf()'; break; } } else { @@ -226,29 +244,15 @@ public function getParamExample(array $param): string case self::TYPE_OBJECT: $decoded = json_decode($example, true); if ($decoded && is_array($decoded)) { - $mapEntries = []; - foreach ($decoded as $key => $value) { - $formattedKey = '"' . $key . '"'; - if (is_string($value)) { - $formattedValue = '"' . $value . '"'; - } elseif (is_bool($value)) { - $formattedValue = $value ? 'true' : 'false'; - } elseif (is_null($value)) { - $formattedValue = 'null'; - } elseif (is_array($value)) { - $formattedValue = 'listOf()'; // Simplified for nested arrays - } else { - $formattedValue = (string)$value; - } - $mapEntries[] = ' ' . $formattedKey . ' to ' . $formattedValue; - } - if (count($mapEntries) > 0) { - $output .= "mapOf(\n" . implode(",\n", $mapEntries) . "\n )"; + if ($lang === 'java') { + $output .= $this->getJavaMapExample($decoded); } else { - $output .= 'mapOf( "a" to "b" )'; + $output .= $this->getKotlinMapExample($decoded); } } else { - $output .= 'mapOf( "a" to "b" )'; + $output .= $lang === 'java' + ? 'Map.of("a", "b")' + : 'mapOf( "a" to "b" )'; } break; case self::TYPE_FILE: @@ -257,13 +261,11 @@ public function getParamExample(array $param): string $output .= $example; break; case self::TYPE_ARRAY: - if (\str_starts_with($example, '[')) { - $example = \substr($example, 1); - } - if (\str_ends_with($example, ']')) { - $example = \substr($example, 0, -1); + if ($this->isPermissionString($example)) { + $output .= $this->getPermissionExample($example, $lang); + } else { + $output .= $this->getArrayExample($example, $lang); } - $output .= 'listOf(' . $example . ')'; break; case self::TYPE_BOOLEAN: $output .= ($example) ? 'true' : 'false'; @@ -277,6 +279,190 @@ public function getParamExample(array $param): string return $output; } + /** + * Generate Kotlin-style map initialization + * + * @param array $data + * @param int $indentLevel Indentation level for nested maps + * @return string + */ + protected function getKotlinMapExample(array $data, int $indentLevel = 0): string + { + $mapEntries = []; + $baseIndent = str_repeat(' ', $indentLevel + 2); + + foreach ($data as $key => $value) { + $formattedKey = '"' . $key . '"'; + if (is_string($value)) { + $formattedValue = '"' . $value . '"'; + } elseif (is_bool($value)) { + $formattedValue = $value ? 'true' : 'false'; + } elseif (is_null($value)) { + $formattedValue = 'null'; + } elseif (is_array($value)) { + // Check if it's an associative array (object) or indexed array + $isObject = !array_is_list($value); + if ($isObject) { + $formattedValue = $this->getKotlinMapExample($value, $indentLevel + 1); + } else { + $formattedValue = $this->getArrayExample(json_encode($value), 'kotlin'); + } + } else { + $formattedValue = (string)$value; + } + $mapEntries[] = $baseIndent . $formattedKey . ' to ' . $formattedValue; + } + + if (count($mapEntries) > 0) { + $closeIndent = str_repeat(' ', $indentLevel + 1); + return "mapOf(\n" . implode(",\n", $mapEntries) . "\n" . $closeIndent . ")"; + } else { + return 'mapOf( "a" to "b" )'; + } + } + + /** + * Generate Java-style map initialization using Map.of() + * + * @param array $data + * @param int $indentLevel Indentation level for nested maps + * @return string + */ + protected function getJavaMapExample(array $data, int $indentLevel = 0): string + { + $mapEntries = []; + $baseIndent = str_repeat(' ', $indentLevel + 2); + + foreach ($data as $key => $value) { + $formattedKey = '"' . $key . '"'; + if (is_string($value)) { + $formattedValue = '"' . $value . '"'; + } elseif (is_bool($value)) { + $formattedValue = $value ? 'true' : 'false'; + } elseif (is_null($value)) { + $formattedValue = 'null'; + } elseif (is_array($value)) { + // Check if it's an associative array (object) or indexed array + $isObject = !array_is_list($value); + if ($isObject) { + $formattedValue = $this->getJavaMapExample($value, $indentLevel + 1); + } else { + $formattedValue = $this->getArrayExample(json_encode($value), 'java'); + } + } else { + $formattedValue = (string)$value; + } + $mapEntries[] = $baseIndent . $formattedKey . ', ' . $formattedValue; + } + + if (count($mapEntries) > 0) { + $closeIndent = str_repeat(' ', $indentLevel + 1); + return "Map.of(\n" . implode(",\n", $mapEntries) . "\n" . $closeIndent . ")"; + } else { + return 'Map.of("a", "b")'; + } + } + + /** + * Generate array example for the given language + * + * @param string $example Array example like '[1, 2, 3]' or '[{"key": "value"}]' + * @param string $lang Language variant: 'kotlin' or 'java' + * @return string + */ + protected function getArrayExample(string $example, string $lang = 'kotlin'): string + { + // Try to decode as JSON to handle arrays of objects + $decoded = json_decode($example, true); + if ($decoded && is_array($decoded)) { + $arrayItems = []; + foreach ($decoded as $item) { + if (is_array($item)) { + // Check if it's an associative array (object) or indexed array (nested array) + $isObject = !array_is_list($item); + + if ($isObject) { + // It's an object/map, convert it + if ($lang === 'java') { + $arrayItems[] = $this->getJavaMapExample($item); + } else { + $arrayItems[] = $this->getKotlinMapExample($item); + } + } else { + // It's a nested array, recursively convert it + $arrayItems[] = $this->getArrayExample(json_encode($item), $lang); + } + } else { + // Primitive value + if (is_string($item)) { + $arrayItems[] = '"' . $item . '"'; + } elseif (is_bool($item)) { + $arrayItems[] = $item ? 'true' : 'false'; + } elseif (is_null($item)) { + $arrayItems[] = 'null'; + } else { + $arrayItems[] = (string)$item; + } + } + } + return $lang === 'java' + ? 'List.of(' . implode(', ', $arrayItems) . ')' + : 'listOf(' . implode(', ', $arrayItems) . ')'; + } + + // Fallback to old behavior for non-JSON arrays + if (\str_starts_with($example, '[')) { + $example = \substr($example, 1); + } + if (\str_ends_with($example, ']')) { + $example = \substr($example, 0, -1); + } + return $lang === 'java' + ? 'List.of(' . $example . ')' + : 'listOf(' . $example . ')'; + } + + /** + * Generate permission example for the given language + * + * @param string $example Permission string like '["read(\"any\")"]' + * @param string $lang Language variant: 'kotlin' or 'java' + * @return string + */ + public function getPermissionExample(string $example, string $lang = 'kotlin'): string + { + $permissions = []; + $staticOp = $this->getStaticAccessOperator(); + $quote = $this->getStringQuote(); + $prefix = $this->getPermissionPrefix(); + + foreach ($this->extractPermissionParts($example) as $permission) { + $args = []; + if ($permission['id'] !== null) { + $args[] = $quote . $permission['id'] . $quote; + } + if ($permission['innerRole'] !== null) { + $args[] = $quote . $permission['innerRole'] . $quote; + } + $argsString = implode(', ', $args); + + $action = $permission['action']; + $role = $permission['role']; + $action = $this->transformPermissionAction($action); + $role = $this->transformPermissionRole($role); + + $permissions[] = $prefix . 'Permission' . $staticOp . $action . '(' . $prefix . 'Role' . $staticOp . $role . '(' . $argsString . '))'; + } + + $permissionsString = implode(', ', $permissions); + + // For Java, use List.of() instead of listOf() + if ($lang === 'java') { + return 'List.of(' . $permissionsString . ')'; + } + return 'listOf(' . $permissionsString . ')'; + } + /** * @return array */ @@ -394,6 +580,11 @@ public function getFiles(): array 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/Query.kt', 'template' => '/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig', ], + [ + 'scope' => 'default', + 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/Operator.kt', + 'template' => '/kotlin/src/main/kotlin/io/appwrite/Operator.kt.twig', + ], [ 'scope' => 'default', 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/coroutines/Callback.kt', @@ -469,6 +660,12 @@ public function getFilters(): array } return $this->toUpperSnakeCase($value); }), + new TwigFilter('propertyAssignment', function (array $property, array $spec) { + return $this->getPropertyAssignment($property, $spec); + }), + new TwigFilter('javaParamExample', function (array $param) { + return $this->getParamExample($param, 'java'); + }, ['is_safe' => ['html']]), ]; } @@ -518,6 +715,9 @@ protected function getPropertyType(array $property, array $spec, string $generic if ($property['type'] === 'array') { $type = 'List<' . $type . '>'; } + } elseif (isset($property['enum'])) { + $enumName = $property['enumName'] ?? $property['name']; + $type = \ucfirst($enumName); } else { $type = $this->getTypeName($property); } @@ -551,4 +751,71 @@ protected function hasGenericType(?string $model, array $spec): string return false; } + + /** + * Generate property assignment logic for model deserialization + * + * @param array $property + * @param array $spec + * @return string + */ + protected function getPropertyAssignment(array $property, array $spec): string + { + $propertyName = $property['name']; + $escapedPropertyName = str_replace('$', '\$', $propertyName); + $mapKey = "map[\"$escapedPropertyName\"]"; + + // Handle sub-schema (nested objects) + if (isset($property['sub_schema']) && !empty($property['sub_schema'])) { + $subSchemaClass = $this->toPascalCase($property['sub_schema']); + $hasGenericType = $this->hasGenericType($property['sub_schema'], $spec); + $nestedTypeParam = $hasGenericType ? ', nestedType' : ''; + + if ($property['type'] === 'array') { + return "($mapKey as List>).map { " . + "$subSchemaClass.from(map = it$nestedTypeParam) }"; + } else { + return "$subSchemaClass.from(" . + "map = $mapKey as Map$nestedTypeParam" . + ")"; + } + } + + // Handle enum properties + if (isset($property['enum']) && !empty($property['enum'])) { + $enumName = $property['enumName'] ?? $property['name']; + $enumClass = $this->toPascalCase($enumName); + $nullCheck = $property['required'] ? '!!' : ' ?: null'; + + if ($property['required']) { + return "$enumClass.values().find { " . + "it.value == $mapKey as String " . + "}$nullCheck"; + } + + return "$enumClass.values().find { " . + "it.value == ($mapKey as? String) " . + "}$nullCheck"; + } + + // Handle primitive types + $nullableModifier = $property['required'] ? '' : '?'; + + if ($property['type'] === 'integer') { + return "($mapKey as$nullableModifier Number)" . + ($nullableModifier ? '?' : '') . '.toLong()'; + } + + if ($property['type'] === 'number') { + return "($mapKey as$nullableModifier Number)" . + ($nullableModifier ? '?' : '') . '.toDouble()'; + } + + // Handle other types (string, boolean, etc.) + $kotlinType = $this->getPropertyType($property, $spec); + // Remove nullable modifier from type since we handle it in the cast + $kotlinType = str_replace('?', '', $kotlinType); + + return "$mapKey as$nullableModifier $kotlinType"; + } } diff --git a/src/SDK/Language/Node.php b/src/SDK/Language/Node.php index 4696817904..31d86b8e03 100644 --- a/src/SDK/Language/Node.php +++ b/src/SDK/Language/Node.php @@ -12,6 +12,26 @@ public function getName(): string return 'NodeJS'; } + public function getStaticAccessOperator(): string + { + return '.'; + } + + public function getStringQuote(): string + { + return "'"; + } + + public function getArrayOf(string $elements): string + { + return '[' . $elements . ']'; + } + + protected function getPermissionPrefix(): string + { + return 'sdk.'; + } + public function getTypeName(array $parameter, array $method = []): string { if (isset($parameter['enumName'])) { @@ -120,9 +140,10 @@ public function getReturn(array $method, array $spec): string /** * @param array $param + * @param string $lang * @return string */ - public function getParamExample(array $param): string + public function getParamExample(array $param, string $lang = ''): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; @@ -140,7 +161,8 @@ public function getParamExample(array $param): string } return match ($type) { - self::TYPE_ARRAY, self::TYPE_FILE, self::TYPE_INTEGER, self::TYPE_NUMBER => $example, + self::TYPE_ARRAY => $this->isPermissionString($example) ? $this->getPermissionExample($example) : $example, + self::TYPE_FILE, self::TYPE_INTEGER, self::TYPE_NUMBER => $example, self::TYPE_BOOLEAN => ($example) ? 'true' : 'false', self::TYPE_OBJECT => ($example === '{}') ? '{}' @@ -202,6 +224,11 @@ public function getFiles(): array 'destination' => 'src/query.ts', 'template' => 'web/src/query.ts.twig', ], + [ + 'scope' => 'default', + 'destination' => 'src/operator.ts', + 'template' => 'node/src/operator.ts.twig', + ], [ 'scope' => 'default', 'destination' => 'README.md', diff --git a/src/SDK/Language/PHP.php b/src/SDK/Language/PHP.php index 27d181e5c3..47e28932ca 100644 --- a/src/SDK/Language/PHP.php +++ b/src/SDK/Language/PHP.php @@ -130,6 +130,21 @@ public function getIdentifierOverrides(): array return []; } + public function getStaticAccessOperator(): string + { + return '::'; + } + + public function getStringQuote(): string + { + return '"'; + } + + public function getArrayOf(string $elements): string + { + return '[' . $elements . ']'; + } + /** * @return array */ @@ -157,6 +172,11 @@ public function getFiles(): array 'destination' => 'composer.json', 'template' => 'php/composer.json.twig', ], + [ + 'scope' => 'default', + 'destination' => 'phpunit.xml', + 'template' => 'php/phpunit.xml.twig', + ], [ 'scope' => 'service', 'destination' => 'docs/{{service.name | caseLower}}.md', @@ -212,6 +232,16 @@ public function getFiles(): array 'destination' => 'tests/{{ spec.title | caseUcfirst}}/QueryTest.php', 'template' => 'php/tests/QueryTest.php.twig', ], + [ + 'scope' => 'default', + 'destination' => 'src/{{ spec.title | caseUcfirst}}/Operator.php', + 'template' => 'php/src/Operator.php.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'tests/{{ spec.title | caseUcfirst}}/OperatorTest.php', + 'template' => 'php/tests/OperatorTest.php.twig', + ], [ 'scope' => 'default', 'destination' => 'src/{{ spec.title | caseUcfirst}}/InputFile.php', @@ -325,9 +355,10 @@ public function getParamDefault(array $param): string /** * @param array $param + * @param string $lang * @return string */ - public function getParamExample(array $param): string + public function getParamExample(array $param, string $lang = ''): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; @@ -356,9 +387,11 @@ public function getParamExample(array $param): string switch ($type) { case self::TYPE_NUMBER: case self::TYPE_INTEGER: - case self::TYPE_ARRAY: $output .= $example; break; + case self::TYPE_ARRAY: + $output .= $this->isPermissionString($example) ? $this->getPermissionExample($example) : $example; + break; case self::TYPE_OBJECT: $output .= $this->jsonToAssoc(json_decode($example, true)); break; diff --git a/src/SDK/Language/Python.php b/src/SDK/Language/Python.php index b4f60970c3..dd2107c07c 100644 --- a/src/SDK/Language/Python.php +++ b/src/SDK/Language/Python.php @@ -84,6 +84,21 @@ public function getIdentifierOverrides(): array return []; } + public function getStaticAccessOperator(): string + { + return '.'; + } + + public function getStringQuote(): string + { + return '"'; + } + + public function getArrayOf(string $elements): string + { + return '[' . $elements . ']'; + } + /** * @return array */ @@ -125,6 +140,16 @@ public function getFiles(): array 'destination' => '{{ spec.title | caseSnake}}/__init__.py', 'template' => 'python/package/__init__.py.twig', ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseSnake}}/utils/deprecated.py', + 'template' => 'python/package/utils/deprecated.py.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseSnake}}/utils/__init__.py', + 'template' => 'python/package/utils/__init__.py.twig', + ], [ 'scope' => 'default', 'destination' => '{{ spec.title | caseSnake}}/client.py', @@ -150,6 +175,11 @@ public function getFiles(): array 'destination' => '{{ spec.title | caseSnake}}/query.py', 'template' => 'python/package/query.py.twig', ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseSnake}}/operator.py', + 'template' => 'python/package/operator.py.twig', + ], [ 'scope' => 'default', 'destination' => '{{ spec.title | caseSnake}}/exception.py', @@ -225,32 +255,48 @@ public function getFiles(): array */ public function getTypeName(array $parameter, array $spec = []): string { + $typeName = ''; + if (isset($parameter['enumName'])) { - return \ucfirst($parameter['enumName']); - } - if (!empty($parameter['enumValues'])) { - return \ucfirst($parameter['name']); + $typeName = \ucfirst($parameter['enumName']); + } elseif (!empty($parameter['enumValues'])) { + $typeName = \ucfirst($parameter['name']); + } else { + switch ($parameter['type'] ?? '') { + case self::TYPE_FILE: + $typeName = 'InputFile'; + break; + case self::TYPE_NUMBER: + case self::TYPE_INTEGER: + $typeName = 'float'; + break; + case self::TYPE_BOOLEAN: + $typeName = 'bool'; + break; + case self::TYPE_STRING: + $typeName = 'str'; + break; + case self::TYPE_ARRAY: + if (!empty(($parameter['array'] ?? [])['type']) && !\is_array($parameter['array']['type'])) { + $typeName = 'List[' . $this->getTypeName($parameter['array']) . ']'; + } else { + $typeName = 'List[Any]'; + } + break; + case self::TYPE_OBJECT: + $typeName = 'dict'; + break; + default: + $typeName = $parameter['type']; + break; + } } - switch ($parameter['type'] ?? '') { - case self::TYPE_FILE: - return 'InputFile'; - case self::TYPE_NUMBER: - case self::TYPE_INTEGER: - return 'float'; - case self::TYPE_BOOLEAN: - return 'bool'; - case self::TYPE_STRING: - return 'str'; - case self::TYPE_ARRAY: - if (!empty(($parameter['array'] ?? [])['type']) && !\is_array($parameter['array']['type'])) { - return 'List[' . $this->getTypeName($parameter['array']) . ']'; - } - return 'List[Any]'; - case self::TYPE_OBJECT: - return 'dict'; - default: - return $parameter['type']; + + if (!($parameter['required'] ?? true) || ($parameter['nullable'] ?? false)) { + return 'Optional[' . $typeName . ']'; } + + return $typeName; } /** @@ -312,9 +358,10 @@ public function getParamDefault(array $param): string /** * @param array $param + * @param string $lang * @return string */ - public function getParamExample(array $param): string + public function getParamExample(array $param, string $lang = ''): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; @@ -332,7 +379,8 @@ public function getParamExample(array $param): string } return match ($type) { - self::TYPE_ARRAY, self::TYPE_FILE, self::TYPE_INTEGER, self::TYPE_NUMBER => $example, + self::TYPE_ARRAY => $this->isPermissionString($example) ? $this->getPermissionExample($example) : $example, + self::TYPE_FILE, self::TYPE_INTEGER, self::TYPE_NUMBER => $example, self::TYPE_BOOLEAN => ($example) ? 'True' : 'False', self::TYPE_OBJECT => ($example === '{}') ? '{}' @@ -352,6 +400,12 @@ public function getFilters(): array new TwigFilter('getPropertyType', function ($value, $method = []) { return $this->getTypeName($value, $method); }), + new TwigFilter('formatParamValue', function (string $paramName, string $paramType, bool $isMultipartFormData) { + if ($isMultipartFormData && $paramType === self::TYPE_BOOLEAN) { + return "str({$paramName}).lower() if type({$paramName}) is bool else {$paramName}"; + } + return $paramName; + }), ]; } } diff --git a/src/SDK/Language/REST.php b/src/SDK/Language/REST.php index 3f956e744c..c83e1ec734 100644 --- a/src/SDK/Language/REST.php +++ b/src/SDK/Language/REST.php @@ -12,6 +12,21 @@ public function getName(): string return 'REST'; } + public function getStaticAccessOperator(): string + { + return '.'; + } + + public function getStringQuote(): string + { + return '"'; + } + + public function getArrayOf(string $elements): string + { + return '[' . $elements . ']'; + } + /** * @param array $param * @return string @@ -69,9 +84,10 @@ public function getParamDefault(array $param): string /** * @param array $param + * @param string $lang * @return string */ - public function getParamExample(array $param): string + public function getParamExample(array $param, string $lang = ''): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/ReactNative.php b/src/SDK/Language/ReactNative.php index c896603171..81748709cb 100644 --- a/src/SDK/Language/ReactNative.php +++ b/src/SDK/Language/ReactNative.php @@ -65,6 +65,11 @@ public function getFiles(): array 'destination' => 'src/query.ts', 'template' => 'react-native/src/query.ts.twig', ], + [ + 'scope' => 'default', + 'destination' => 'src/operator.ts', + 'template' => 'react-native/src/operator.ts.twig', + ], [ 'scope' => 'default', 'destination' => 'README.md', @@ -183,9 +188,10 @@ public function getTypeName(array $parameter, array $method = []): string /** * @param array $param + * @param string $lang * @return string */ - public function getParamExample(array $param): string + public function getParamExample(array $param, string $lang = ''): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; diff --git a/src/SDK/Language/Ruby.php b/src/SDK/Language/Ruby.php index 50972b0e6a..011d831b09 100644 --- a/src/SDK/Language/Ruby.php +++ b/src/SDK/Language/Ruby.php @@ -86,6 +86,21 @@ public function getIdentifierOverrides(): array return []; } + public function getStaticAccessOperator(): string + { + return '.'; + } + + public function getStringQuote(): string + { + return "'"; + } + + public function getArrayOf(string $elements): string + { + return '[' . $elements . ']'; + } + /** * @return array */ @@ -147,6 +162,11 @@ public function getFiles(): array 'destination' => 'lib/{{ spec.title | caseDash }}/query.rb', 'template' => 'ruby/lib/container/query.rb.twig', ], + [ + 'scope' => 'default', + 'destination' => 'lib/{{ spec.title | caseDash }}/operator.rb', + 'template' => 'ruby/lib/container/operator.rb.twig', + ], [ 'scope' => 'default', 'destination' => 'lib/{{ spec.title | caseDash }}/service.rb', @@ -269,9 +289,10 @@ public function getParamDefault(array $param): string /** * @param array $param + * @param string $lang * @return string */ - public function getParamExample(array $param): string + public function getParamExample(array $param, string $lang = ''): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; @@ -302,9 +323,11 @@ public function getParamExample(array $param): string switch ($type) { case self::TYPE_NUMBER: case self::TYPE_INTEGER: - case self::TYPE_ARRAY: $output .= $example; break; + case self::TYPE_ARRAY: + $output .= $this->isPermissionString($example) ? $this->getPermissionExample($example) : $example; + break; case self::TYPE_OBJECT: $output .= $this->jsonToHash(json_decode($example, true)); break; diff --git a/src/SDK/Language/Swift.php b/src/SDK/Language/Swift.php index c6cdc1fca0..6e36e42514 100644 --- a/src/SDK/Language/Swift.php +++ b/src/SDK/Language/Swift.php @@ -101,6 +101,21 @@ public function getIdentifierOverrides(): array ]; } + public function getStaticAccessOperator(): string + { + return '.'; + } + + public function getStringQuote(): string + { + return '"'; + } + + public function getArrayOf(string $elements): string + { + return '[' . $elements . ']'; + } + /** * @return array */ @@ -172,6 +187,11 @@ public function getFiles(): array 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Query.swift', 'template' => 'swift/Sources/Query.swift.twig', ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Operator.swift', + 'template' => 'swift/Sources/Operator.swift.twig', + ], [ 'scope' => 'default', 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Models/UploadProgress.swift', @@ -389,9 +409,10 @@ public function getParamDefault(array $param): string /** * @param array $param + * @param string $lang * @return string */ - public function getParamExample(array $param): string + public function getParamExample(array $param, string $lang = ''): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; @@ -425,9 +446,11 @@ public function getParamExample(array $param): string case self::TYPE_FILE: case self::TYPE_NUMBER: case self::TYPE_INTEGER: - case self::TYPE_ARRAY: $output .= $example; break; + case self::TYPE_ARRAY: + $output .= $this->isPermissionString($example) ? $this->getPermissionExample($example) : $example; + break; case self::TYPE_BOOLEAN: $output .= ($example) ? 'true' : 'false'; break; @@ -577,7 +600,7 @@ protected function getPropertyType(array $property, array $spec, string $generic $type = '[' . $type . ']'; } } else { - $type = $this->getTypeName($property, isProperty: true); + $type = $this->getTypeName($property, $spec, true); } return $type; diff --git a/src/SDK/Language/Unity.php b/src/SDK/Language/Unity.php new file mode 100644 index 0000000000..5b9b1e8e9a --- /dev/null +++ b/src/SDK/Language/Unity.php @@ -0,0 +1,414 @@ + 'default', + 'destination' => 'CHANGELOG.md', + 'template' => 'unity/CHANGELOG.md.twig', + ], + [ + 'scope' => 'copy', + 'destination' => '/icon.png', + 'template' => 'unity/icon.png', + ], + [ + 'scope' => 'default', + 'destination' => 'LICENSE', + 'template' => 'unity/LICENSE.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'README.md', + 'template' => 'unity/README.md.twig', + ], + [ + 'scope' => 'method', + 'destination' => 'Assets/docs~/examples/{{service.name | caseLower}}/{{method.name | caseDash}}.md', + 'template' => 'unity/docs/example.md.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/package.json', + 'template' => 'unity/package.json.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/{{ spec.title | caseUcfirst }}.asmdef', + 'template' => 'unity/Assets/Runtime/Appwrite.asmdef.twig', + ], + // Appwrite + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/{{ spec.title | caseUcfirst }}Config.cs', + 'template' => 'unity/Assets/Runtime/AppwriteConfig.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/{{ spec.title | caseUcfirst }}Manager.cs', + 'template' => 'unity/Assets/Runtime/AppwriteManager.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Realtime.cs', + 'template' => 'unity/Assets/Runtime/Realtime.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Utilities/{{ spec.title | caseUcfirst }}Utilities.cs', + 'template' => 'unity/Assets/Runtime/Utilities/AppwriteUtilities.cs.twig', + ], + // Appwrite.Core + [ + 'scope' => 'copy', + 'destination' => 'Assets/Runtime/Core/csc.rsp', + 'template' => 'unity/Assets/Runtime/Core/csc.rsp', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/{{ spec.title | caseUcfirst }}.Core.asmdef', + 'template' => 'unity/Assets/Runtime/Core/Appwrite.Core.asmdef.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Client.cs', + 'template' => 'unity/Assets/Runtime/Core/Client.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/{{ spec.title | caseUcfirst }}Exception.cs', + 'template' => 'dotnet/Package/Exception.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/ID.cs', + 'template' => 'dotnet/Package/ID.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Permission.cs', + 'template' => 'dotnet/Package/Permission.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Query.cs', + 'template' => 'dotnet/Package/Query.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Role.cs', + 'template' => 'dotnet/Package/Role.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Operator.cs', + 'template' => 'dotnet/Package/Operator.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/CookieContainer.cs', + 'template' => 'unity/Assets/Runtime/Core/CookieContainer.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Converters/ValueClassConverter.cs', + 'template' => 'dotnet/Package/Converters/ValueClassConverter.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Converters/ObjectToInferredTypesConverter.cs', + 'template' => 'dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Extensions/Extensions.cs', + 'template' => 'dotnet/Package/Extensions/Extensions.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Models/OrderType.cs', + 'template' => 'dotnet/Package/Models/OrderType.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Models/UploadProgress.cs', + 'template' => 'dotnet/Package/Models/UploadProgress.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Models/InputFile.cs', + 'template' => 'dotnet/Package/Models/InputFile.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Services/Service.cs', + 'template' => 'dotnet/Package/Services/Service.cs.twig', + ], + [ + 'scope' => 'service', + 'destination' => 'Assets/Runtime/Core/Services/{{service.name | caseUcfirst}}.cs', + 'template' => 'unity/Assets/Runtime/Core/Services/ServiceTemplate.cs.twig', + ], + [ + 'scope' => 'definition', + 'destination' => 'Assets/Runtime/Core/Models/{{ definition.name | caseUcfirst | overrideIdentifier }}.cs', + 'template' => 'dotnet/Package/Models/Model.cs.twig', + ], + [ + 'scope' => 'enum', + 'destination' => 'Assets/Runtime/Core/Enums/{{ enum.name | caseUcfirst | overrideIdentifier }}.cs', + 'template' => 'dotnet/Package/Enums/Enum.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/WebAuthComponent.cs', + 'template' => 'unity/Assets/Runtime/Core/WebAuthComponent.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Enums/IEnum.cs', + 'template' => 'dotnet/Package/Enums/IEnum.cs.twig', + ], + // Plugins + [ + 'scope' => 'copy', + 'destination' => 'Assets/Runtime/Core/Plugins/Microsoft.Bcl.AsyncInterfaces.dll', + 'template' => 'unity/Assets/Runtime/Core/Plugins/Microsoft.Bcl.AsyncInterfaces.dll', + ], + [ + 'scope' => 'copy', + 'destination' => 'Assets/Runtime/Core/Plugins/System.IO.Pipelines.dll', + 'template' => 'unity/Assets/Runtime/Core/Plugins/System.IO.Pipelines.dll', + ], + [ + 'scope' => 'copy', + 'destination' => 'Assets/Runtime/Core/Plugins/System.Runtime.CompilerServices.Unsafe.dll', + 'template' => 'unity/Assets/Runtime/Core/Plugins/System.Runtime.CompilerServices.Unsafe.dll', + ], + [ + 'scope' => 'copy', + 'destination' => 'Assets/Runtime/Core/Plugins/System.Text.Encodings.Web.dll', + 'template' => 'unity/Assets/Runtime/Core/Plugins/System.Text.Encodings.Web.dll', + ], + [ + 'scope' => 'copy', + 'destination' => 'Assets/Runtime/Core/Plugins/Android/AndroidManifest.xml', + 'template' => 'unity/Assets/Runtime/Core/Plugins/Android/AndroidManifest.xml', + ], + [ + 'scope' => 'copy', + 'destination' => 'Assets/Runtime/Core/Plugins/WebGLCookies.jslib', + 'template' => 'unity/Assets/Runtime/Core/Plugins/WebGLCookies.jslib', + ], + [ + 'scope' => 'copy', + 'destination' => 'Assets/Runtime/Core/Plugins/System.Text.Json.dll', + 'template' => 'unity/Assets/Runtime/Core/Plugins/System.Text.Json.dll', + ], + // Appwrite.Editor + [ + 'scope' => 'default', + 'destination' => 'Assets/Editor/{{ spec.title | caseUcfirst }}.Editor.asmdef', + 'template' => 'unity/Assets/Editor/Appwrite.Editor.asmdef.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Editor/{{ spec.title | caseUcfirst }}SetupAssistant.cs', + 'template' => 'unity/Assets/Editor/AppwriteSetupAssistant.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Editor/{{ spec.title | caseUcfirst }}SetupWindow.cs', + 'template' => 'unity/Assets/Editor/AppwriteSetupWindow.cs.twig', + ], + // Samples + [ + 'scope' => 'default', + 'destination' => 'Assets/Samples~/{{ spec.title | caseUcfirst }}Example/{{ spec.title | caseUcfirst }}Example.cs', + 'template' => 'unity/Assets/Samples~/AppwriteExample/AppwriteExample.cs.twig', + ], + // Packages + [ + 'scope' => 'copy', + 'destination' => 'Packages/manifest.json', + 'template' => 'unity/Packages/manifest.json', + ], + [ + 'scope' => 'copy', + 'destination' => 'Packages/packages-lock.json', + 'template' => 'unity/Packages/packages-lock.json', + ], + // ProjectSettings + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/AudioManager.asset', + 'template' => 'unity/ProjectSettings/AudioManager.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/boot.config', + 'template' => 'unity/ProjectSettings/boot.config', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/ClusterInputManager.asset', + 'template' => 'unity/ProjectSettings/ClusterInputManager.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/DynamicsManager.asset', + 'template' => 'unity/ProjectSettings/DynamicsManager.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/EditorBuildSettings.asset', + 'template' => 'unity/ProjectSettings/EditorBuildSettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/EditorSettings.asset', + 'template' => 'unity/ProjectSettings/EditorSettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/GraphicsSettings.asset', + 'template' => 'unity/ProjectSettings/GraphicsSettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/InputManager.asset', + 'template' => 'unity/ProjectSettings/InputManager.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/MemorySettings.asset', + 'template' => 'unity/ProjectSettings/MemorySettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/NavMeshAreas.asset', + 'template' => 'unity/ProjectSettings/NavMeshAreas.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/NetworkManager.asset', + 'template' => 'unity/ProjectSettings/NetworkManager.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/PackageManagerSettings.asset', + 'template' => 'unity/ProjectSettings/PackageManagerSettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/Physics2DSettings.asset', + 'template' => 'unity/ProjectSettings/Physics2DSettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/PresetManager.asset', + 'template' => 'unity/ProjectSettings/PresetManager.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/ProjectSettings.asset', + 'template' => 'unity/ProjectSettings/ProjectSettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/ProjectVersion.txt', + 'template' => 'unity/ProjectSettings/ProjectVersion.txt', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/QualitySettings.asset', + 'template' => 'unity/ProjectSettings/QualitySettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/TagManager.asset', + 'template' => 'unity/ProjectSettings/TagManager.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/TimeManager.asset', + 'template' => 'unity/ProjectSettings/TimeManager.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/UnityConnectSettings.asset', + 'template' => 'unity/ProjectSettings/UnityConnectSettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/VersionControlSettings.asset', + 'template' => 'unity/ProjectSettings/VersionControlSettings.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/VFXManager.asset', + 'template' => 'unity/ProjectSettings/VFXManager.asset', + ], + [ + 'scope' => 'copy', + 'destination' => 'ProjectSettings/XRSettings.asset', + 'template' => 'unity/ProjectSettings/XRSettings.asset', + ], + ]; + + // Filter out problematic files in test mode + // Check if we're in test mode by looking for a global variable + if (isset($GLOBALS['UNITY_TEST_MODE']) && $GLOBALS['UNITY_TEST_MODE'] === true) { + $excludeInTest = [ + 'Assets/Runtime/Utilities/{{ spec.title | caseUcfirst }}Utilities.cs', + 'Assets/Runtime/{{ spec.title | caseUcfirst }}Config.cs', + 'Assets/Runtime/{{ spec.title | caseUcfirst }}Manager.cs', + 'Assets/Editor/{{ spec.title | caseUcfirst }}.Editor.asmdef', + 'Assets/Editor/{{ spec.title | caseUcfirst }}SetupAssistant.cs', + 'Assets/Editor/{{ spec.title | caseUcfirst }}SetupWindow.cs', + ]; + + $files = array_filter($files, function ($file) use ($excludeInTest): bool { + return !in_array($file['destination'], $excludeInTest); + }); + } + + return $files; + } +} diff --git a/src/SDK/Language/Web.php b/src/SDK/Language/Web.php index 8653d3807f..2490f833f9 100644 --- a/src/SDK/Language/Web.php +++ b/src/SDK/Language/Web.php @@ -14,6 +14,21 @@ public function getName(): string return 'Web'; } + public function getStaticAccessOperator(): string + { + return '.'; + } + + public function getStringQuote(): string + { + return "'"; + } + + public function getArrayOf(string $elements): string + { + return '[' . $elements . ']'; + } + /** * @return array */ @@ -40,6 +55,11 @@ public function getFiles(): array 'destination' => 'src/services/{{service.name | caseKebab}}.ts', 'template' => 'web/src/services/template.ts.twig', ], + [ + 'scope' => 'default', + 'destination' => 'src/services/realtime.ts', + 'template' => 'web/src/services/realtime.ts.twig', + ], [ 'scope' => 'default', 'destination' => 'src/models.ts', @@ -65,6 +85,11 @@ public function getFiles(): array 'destination' => 'src/query.ts', 'template' => 'web/src/query.ts.twig', ], + [ + 'scope' => 'default', + 'destination' => 'src/operator.ts', + 'template' => 'web/src/operator.ts.twig', + ], [ 'scope' => 'default', 'destination' => 'README.md', @@ -125,9 +150,10 @@ public function getFiles(): array /** * @param array $param + * @param string $lang * @return string */ - public function getParamExample(array $param): string + public function getParamExample(array $param, string $lang = ''): string { $type = $param['type'] ?? ''; $example = $param['example'] ?? ''; @@ -145,7 +171,8 @@ public function getParamExample(array $param): string } return match ($type) { - self::TYPE_ARRAY, self::TYPE_INTEGER, self::TYPE_NUMBER => $example, + self::TYPE_ARRAY => $this->isPermissionString($example) ? $this->getPermissionExample($example) : $example, + self::TYPE_INTEGER, self::TYPE_NUMBER => $example, self::TYPE_FILE => 'document.getElementById(\'uploader\').files[0]', self::TYPE_BOOLEAN => ($example) ? 'true' : 'false', self::TYPE_OBJECT => ($example === '{}') @@ -316,7 +343,7 @@ public function getReturn(array $method, array $spec): string return 'Promise<{}>'; } - public function getSubSchema(array $property, array $spec): string + public function getSubSchema(array $property, array $spec, string $methodName = ''): string { if (array_key_exists('sub_schema', $property)) { $ret = ''; @@ -336,6 +363,14 @@ public function getSubSchema(array $property, array $spec): string return $ret; } + if (array_key_exists('enum', $property) && !empty($methodName)) { + if (isset($property['enumName'])) { + return $this->toPascalCase($property['enumName']); + } + + return $this->toPascalCase($methodName) . $this->toPascalCase($property['name']); + } + return $this->getTypeName($property); } @@ -348,8 +383,8 @@ public function getFilters(): array new TwigFilter('getReadOnlyProperties', function ($value, $responseModel, $spec = []) { return $this->getReadOnlyProperties($value, $responseModel, $spec); }), - new TwigFilter('getSubSchema', function (array $property, array $spec) { - return $this->getSubSchema($property, $spec); + new TwigFilter('getSubSchema', function (array $property, array $spec, string $methodName = '') { + return $this->getSubSchema($property, $spec, $methodName); }), new TwigFilter('getGenerics', function (string $model, array $spec, bool $skipAdditional = false) { return $this->getGenerics($model, $spec, $skipAdditional); diff --git a/src/SDK/SDK.php b/src/SDK/SDK.php index 32638628db..7bea56bab3 100644 --- a/src/SDK/SDK.php +++ b/src/SDK/SDK.php @@ -226,6 +226,9 @@ public function __construct(Language $language, Spec $spec) } return $parts[0] . '.' . $toSnake($parts[1]); })); + $this->twig->addFilter(new TwigFilter('hasPermissionParam', function ($value) { + return $this->language->hasPermissionParam($value); + })); } /** @@ -633,7 +636,9 @@ public function generate(string $target): void 'contactURL' => $this->spec->getContactURL(), 'contactEmail' => $this->spec->getContactEmail(), 'services' => $this->getFilteredServices(), - 'enums' => $this->spec->getEnums(), + 'requestEnums' => $this->spec->getRequestEnums(), + 'responseEnums' => $this->spec->getResponseEnums(), + 'allEnums' => $this->spec->getAllEnums(), 'definitions' => $this->spec->getDefinitions(), 'global' => [ 'headers' => $this->spec->getGlobalHeaders(), @@ -724,7 +729,7 @@ public function generate(string $target): void } break; case 'enum': - foreach ($this->spec->getEnums() as $key => $enum) { + foreach ($this->spec->getAllEnums() as $key => $enum) { $params['enum'] = $enum; $this->render($template, $destination, $block, $params, $minify); diff --git a/src/Spec/Spec.php b/src/Spec/Spec.php index bbd9a949a9..8466b5a688 100644 --- a/src/Spec/Spec.php +++ b/src/Spec/Spec.php @@ -178,9 +178,16 @@ public function setAttribute($key, $value, $type = self::SET_TYPE_ASSIGN) } /** - * Get Enums + * Get Request Enums * * @return array */ - abstract public function getEnums(); + abstract public function getRequestEnums(); + + /** + * Get Response Enums + * + * @return array + */ + abstract public function getResponseEnums(); } diff --git a/src/Spec/Swagger2.php b/src/Spec/Swagger2.php index 15ee3225b9..f8d6e13f61 100644 --- a/src/Spec/Swagger2.php +++ b/src/Spec/Swagger2.php @@ -229,7 +229,7 @@ protected function parseMethod(string $methodName, string $pathName, array $meth $param['default'] = (is_array($param['default']) || $param['default'] instanceof stdClass) ? json_encode($param['default']) : $param['default']; if (isset($parameter['enum'])) { $param['enumValues'] = $parameter['enum']; - $param['enumName'] = $parameter['x-enum-name']; + $param['enumName'] = $parameter['x-enum-name'] ?? $param['name']; $param['enumKeys'] = $parameter['x-enum-keys']; } @@ -268,7 +268,7 @@ protected function parseMethod(string $methodName, string $pathName, array $meth if (isset($value['enum'])) { $temp['enumValues'] = $value['enum']; - $temp['enumName'] = $value['x-enum-name']; + $temp['enumName'] = $value['x-enum-name'] ?? $temp['name']; $temp['enumKeys'] = $value['x-enum-keys']; } @@ -489,6 +489,13 @@ public function getDefinitions() //nested model $sch['properties'][$name]['sub_schemas'] = \array_map(fn($schema) => str_replace('#/definitions/', '', $schema['$ref']), $def['items']['x-oneOf']); } + + if (isset($def['enum'])) { + // enum property + $sch['properties'][$name]['enum'] = $def['enum']; + $sch['properties'][$name]['enumName'] = $def['x-enum-name'] ?? ucfirst($key) . ucfirst($name); + $sch['properties'][$name]['enumKeys'] = $def['x-enum-keys'] ?? []; + } } } $list[$key] = $sch; @@ -499,7 +506,7 @@ public function getDefinitions() /** * @return array */ - public function getEnums(): array + public function getRequestEnums(): array { $list = []; @@ -523,4 +530,62 @@ public function getEnums(): array return \array_values($list); } + + /** + * @return array + */ + public function getResponseEnums(): array + { + $list = []; + $definitions = $this->getDefinitions(); + + foreach ($definitions as $modelName => $model) { + if (isset($model['properties']) && is_array($model['properties'])) { + foreach ($model['properties'] as $propertyName => $property) { + if (isset($property['enum'])) { + $enumName = $property['x-enum-name'] ?? ucfirst($modelName) . ucfirst($propertyName); + + if (!isset($list[$enumName])) { + $list[$enumName] = [ + 'name' => $enumName, + 'enum' => $property['enum'], + 'keys' => $property['x-enum-keys'] ?? [], + ]; + } + } + + // array of enums + if ((($property['type'] ?? null) === 'array') && isset($property['items']['enum'])) { + $enumName = $property['x-enum-name'] ?? ucfirst($modelName) . ucfirst($propertyName); + + if (!isset($list[$enumName])) { + $list[$enumName] = [ + 'name' => $enumName, + 'enum' => $property['items']['enum'], + 'keys' => $property['items']['x-enum-keys'] ?? [], + ]; + } + } + } + } + } + + return \array_values($list); + } + + /** + * @return array + */ + public function getAllEnums(): array + { + $list = []; + foreach ($this->getRequestEnums() as $enum) { + $list[$enum['name']] = $enum; + } + foreach ($this->getResponseEnums() as $enum) { + $list[$enum['name']] = $enum; + } + + return \array_values($list); + } } diff --git a/templates/android/build.gradle.twig b/templates/android/build.gradle.twig index 0b0c86cd5f..a1957d4a17 100644 --- a/templates/android/build.gradle.twig +++ b/templates/android/build.gradle.twig @@ -29,6 +29,5 @@ task clean(type: Delete) { delete rootProject.buildDir } - apply from: "${rootDir}/scripts/publish-config.gradle" diff --git a/templates/android/docs/java/example.md.twig b/templates/android/docs/java/example.md.twig index 34a28a6321..663e7c2575 100644 --- a/templates/android/docs/java/example.md.twig +++ b/templates/android/docs/java/example.md.twig @@ -3,20 +3,17 @@ import {{ sdk.namespace | caseDot }}.coroutines.CoroutineCallback; {% if method.parameters.all | filter((param) => param.type == 'file') | length > 0 %} import {{ sdk.namespace | caseDot }}.models.InputFile; {% endif %} +{% if method.parameters.all | hasPermissionParam %} +import {{ sdk.namespace | caseDot }}.Permission; +import {{ sdk.namespace | caseDot }}.Role; +{% endif %} import {{ sdk.namespace | caseDot }}.services.{{ service.name | caseUcfirst }}; {% set added = [] %} {% for parameter in method.parameters.all %} -{% if method == parameter.required %} {% if parameter.enumValues is not empty %} -{% if parameter.enumName is not empty %} -{% set name = parameter.enumName %} -{% else %} -{% set name = parameter.name %} -{% endif %} -{% if name not in added %} -import {{ sdk.namespace | caseDot }}.enums.{{ name | caseUcfirst }}; -{% set added = added|merge([name]) %} -{% endif %} +{% if parameter.enumName not in added %} +import {{ sdk.namespace | caseDot }}.enums.{{ parameter.enumName | caseUcfirst }}; +{% set added = added|merge([parameter.enumName]) %} {% endif %} {% endif %} {% endfor %} @@ -43,17 +40,10 @@ Client client = new Client(context) }));{% endif %} {% for parameter in method.parameters.all %} - {%~ if parameter.enumValues is not empty -%} - {%~ if parameter.enumName is not empty -%} - {%~ set name = parameter.enumName -%} - {%~ else -%} - {%~ set name = parameter.name -%} - {%~ endif %} - {{ name }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}, // {{ parameter.name }} {% if not parameter.required %}(optional){% endif %} - {%~ else %} - {{ parameter | paramExample }}, // {{ parameter.name }} {% if not parameter.required %}(optional){% endif %} - {%~ endif %} - {%~ if loop.last %} + {% if parameter.enumValues is not empty %}{{ parameter.enumName | caseUcfirst }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}, // {{ parameter.name }} {% if not parameter.required %}(optional){% endif %} +{% else %}{{ parameter | javaParamExample }}, // {{ parameter.name }} {% if not parameter.required %}(optional){% endif %} +{% endif %} +{%~ if loop.last %} new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/templates/android/docs/kotlin/example.md.twig b/templates/android/docs/kotlin/example.md.twig index e481ba2905..b8924589e7 100644 --- a/templates/android/docs/kotlin/example.md.twig +++ b/templates/android/docs/kotlin/example.md.twig @@ -6,20 +6,17 @@ import {{ sdk.namespace | caseDot }}.models.InputFile import {{ sdk.namespace | caseDot }}.services.{{ service.name | caseUcfirst }} {% set added = [] %} {% for parameter in method.parameters.all %} -{% if method == parameter.required %} {% if parameter.enumValues is not empty %} -{% if parameter.enumName is not empty %} -{% set name = parameter.enumName %} -{% else %} -{% set name = parameter.name %} -{% endif %} -{% if name not in added %} -import {{ sdk.namespace | caseDot }}.enums.{{ name | caseUcfirst }} -{% set added = added|merge([name]) %} -{% endif %} +{% if parameter.enumName not in added %} +import {{ sdk.namespace | caseDot }}.enums.{{ parameter.enumName | caseUcfirst }} +{% set added = added|merge([parameter.enumName]) %} {% endif %} {% endif %} {% endfor %} +{% if method.parameters.all | hasPermissionParam %} +import {{ sdk.namespace | caseDot }}.Permission +import {{ sdk.namespace | caseDot }}.Role +{% endif %} val client = Client(context) {%~ if method.auth|length > 0 %} @@ -38,13 +35,8 @@ val {{ service.name | caseCamel }} = {{ service.name | caseUcfirst }}(client) val result = {% endif %}{{ service.name | caseCamel }}.{{ method.name | caseCamel }}({% if method.parameters.all | length == 0 %}){% endif %} {%~ for parameter in method.parameters.all %} - {%~ if parameter.enumValues is not empty -%} - {%~ if parameter.enumName is not empty -%} - {%~ set name = parameter.enumName -%} - {%~ else -%} - {%~ set name = parameter.name -%} - {%~ endif %} - {{ parameter.name }} = {{ name }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }},{% if not parameter.required %} // (optional){% endif %} + {%~ if parameter.enumValues is not empty %} + {{ parameter.name }} = {{ parameter.enumName }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }},{% if not parameter.required %} // (optional){% endif %} {%~ else %} {{ parameter.name }} = {{ parameter | paramExample }}, {% if not parameter.required %}// (optional){% endif %} {%~ endif %} diff --git a/templates/android/example/src/main/java/io/package/android/ui/accounts/AccountsFragment.kt.twig b/templates/android/example/src/main/java/io/package/android/ui/accounts/AccountsFragment.kt.twig index 94905a61f4..5193b7667d 100644 --- a/templates/android/example/src/main/java/io/package/android/ui/accounts/AccountsFragment.kt.twig +++ b/templates/android/example/src/main/java/io/package/android/ui/accounts/AccountsFragment.kt.twig @@ -13,7 +13,6 @@ import androidx.fragment.app.viewModels import {{ sdk.namespace | caseDot }}.android.R import {{ sdk.namespace | caseDot }}.android.databinding.FragmentAccountBinding - class AccountsFragment : Fragment() { private lateinit var binding: FragmentAccountBinding diff --git a/templates/android/library/src/main/java/io/package/Client.kt.twig b/templates/android/library/src/main/java/io/package/Client.kt.twig index 74c45ec297..3d441ccf51 100644 --- a/templates/android/library/src/main/java/io/package/Client.kt.twig +++ b/templates/android/library/src/main/java/io/package/Client.kt.twig @@ -507,7 +507,7 @@ class Client @JvmOverloads constructor( val warnings = response.headers["x-{{ spec.title | lower }}-warning"] if (warnings != null) { warnings.split(";").forEach { warning -> - println("Warning: $warning") + System.err.println("Warning: $warning") } } diff --git a/templates/android/library/src/main/java/io/package/Operator.kt.twig b/templates/android/library/src/main/java/io/package/Operator.kt.twig new file mode 100644 index 0000000000..a58312f433 --- /dev/null +++ b/templates/android/library/src/main/java/io/package/Operator.kt.twig @@ -0,0 +1,130 @@ +package {{ sdk.namespace | caseDot }} + +import {{ sdk.namespace | caseDot }}.extensions.toJson + +enum class Condition(val value: String) { + EQUAL("equal"), + NOT_EQUAL("notEqual"), + GREATER_THAN("greaterThan"), + GREATER_THAN_EQUAL("greaterThanEqual"), + LESS_THAN("lessThan"), + LESS_THAN_EQUAL("lessThanEqual"), + CONTAINS("contains"), + IS_NULL("isNull"), + IS_NOT_NULL("isNotNull"); + + override fun toString() = value +} + +class Operator( + val method: String, + val values: List? = null, +) { + override fun toString() = this.toJson() + + companion object { + fun increment(value: Number = 1, max: Number? = null): String { + require(!value.toDouble().isNaN() && !value.toDouble().isInfinite()) { "Value cannot be NaN or Infinity" } + max?.let { require(!it.toDouble().isNaN() && !it.toDouble().isInfinite()) { "Max cannot be NaN or Infinity" } } + val values = mutableListOf(value) + max?.let { values.add(it) } + return Operator("increment", values).toJson() + } + + fun decrement(value: Number = 1, min: Number? = null): String { + require(!value.toDouble().isNaN() && !value.toDouble().isInfinite()) { "Value cannot be NaN or Infinity" } + min?.let { require(!it.toDouble().isNaN() && !it.toDouble().isInfinite()) { "Min cannot be NaN or Infinity" } } + val values = mutableListOf(value) + min?.let { values.add(it) } + return Operator("decrement", values).toJson() + } + + fun multiply(factor: Number, max: Number? = null): String { + require(!factor.toDouble().isNaN() && !factor.toDouble().isInfinite()) { "Factor cannot be NaN or Infinity" } + max?.let { require(!it.toDouble().isNaN() && !it.toDouble().isInfinite()) { "Max cannot be NaN or Infinity" } } + val values = mutableListOf(factor) + max?.let { values.add(it) } + return Operator("multiply", values).toJson() + } + + fun divide(divisor: Number, min: Number? = null): String { + require(!divisor.toDouble().isNaN() && !divisor.toDouble().isInfinite()) { "Divisor cannot be NaN or Infinity" } + min?.let { require(!it.toDouble().isNaN() && !it.toDouble().isInfinite()) { "Min cannot be NaN or Infinity" } } + require(divisor.toDouble() != 0.0) { "Divisor cannot be zero" } + val values = mutableListOf(divisor) + min?.let { values.add(it) } + return Operator("divide", values).toJson() + } + + fun modulo(divisor: Number): String { + require(!divisor.toDouble().isNaN() && !divisor.toDouble().isInfinite()) { "Divisor cannot be NaN or Infinity" } + require(divisor.toDouble() != 0.0) { "Divisor cannot be zero" } + return Operator("modulo", listOf(divisor)).toJson() + } + + fun power(exponent: Number, max: Number? = null): String { + require(!exponent.toDouble().isNaN() && !exponent.toDouble().isInfinite()) { "Exponent cannot be NaN or Infinity" } + max?.let { require(!it.toDouble().isNaN() && !it.toDouble().isInfinite()) { "Max cannot be NaN or Infinity" } } + val values = mutableListOf(exponent) + max?.let { values.add(it) } + return Operator("power", values).toJson() + } + + fun arrayAppend(values: List): String { + return Operator("arrayAppend", values).toJson() + } + + fun arrayPrepend(values: List): String { + return Operator("arrayPrepend", values).toJson() + } + + fun arrayInsert(index: Int, value: Any): String { + return Operator("arrayInsert", listOf(index, value)).toJson() + } + + fun arrayRemove(value: Any): String { + return Operator("arrayRemove", listOf(value)).toJson() + } + + fun arrayUnique(): String { + return Operator("arrayUnique", emptyList()).toJson() + } + + fun arrayIntersect(values: List): String { + return Operator("arrayIntersect", values).toJson() + } + + fun arrayDiff(values: List): String { + return Operator("arrayDiff", values).toJson() + } + + fun arrayFilter(condition: Condition, value: Any? = null): String { + val values = listOf(condition.value, value) + return Operator("arrayFilter", values).toJson() + } + + fun stringConcat(value: Any): String { + return Operator("stringConcat", listOf(value)).toJson() + } + + fun stringReplace(search: String, replace: String): String { + return Operator("stringReplace", listOf(search, replace)).toJson() + } + + fun toggle(): String { + return Operator("toggle", emptyList()).toJson() + } + + fun dateAddDays(days: Int): String { + return Operator("dateAddDays", listOf(days)).toJson() + } + + fun dateSubDays(days: Int): String { + return Operator("dateSubDays", listOf(days)).toJson() + } + + fun dateSetNow(): String { + return Operator("dateSetNow", emptyList()).toJson() + } + } +} diff --git a/templates/android/library/src/main/java/io/package/Query.kt.twig b/templates/android/library/src/main/java/io/package/Query.kt.twig index e57b55b396..4450dbd9b5 100644 --- a/templates/android/library/src/main/java/io/package/Query.kt.twig +++ b/templates/android/library/src/main/java/io/package/Query.kt.twig @@ -151,6 +151,13 @@ class Query( */ fun orderDesc(attribute: String) = Query("orderDesc", attribute).toJson() + /** + * Sort results randomly. + * + * @returns The query string. + */ + fun orderRandom() = Query("orderRandom").toJson() + /** * Return results before documentId. * @@ -244,7 +251,7 @@ class Query( * @param value The date value to compare against. * @returns The query string. */ - fun createdBefore(value: String) = Query("createdBefore", null, listOf(value)).toJson() + fun createdBefore(value: String) = lessThan("\$createdAt", value) /** * Filter resources where document was created after date. @@ -252,7 +259,7 @@ class Query( * @param value The date value to compare against. * @returns The query string. */ - fun createdAfter(value: String) = Query("createdAfter", null, listOf(value)).toJson() + fun createdAfter(value: String) = greaterThan("\$createdAt", value) /** * Filter resources where document was created between start and end dates (inclusive). @@ -261,7 +268,7 @@ class Query( * @param end The end date value. * @returns The query string. */ - fun createdBetween(start: String, end: String) = Query("createdBetween", null, listOf(start, end)).toJson() + fun createdBetween(start: String, end: String) = between("\$createdAt", start, end) /** * Filter resources where document was updated before date. @@ -269,7 +276,7 @@ class Query( * @param value The date value to compare against. * @returns The query string. */ - fun updatedBefore(value: String) = Query("updatedBefore", null, listOf(value)).toJson() + fun updatedBefore(value: String) = lessThan("\$updatedAt", value) /** * Filter resources where document was updated after date. @@ -277,7 +284,7 @@ class Query( * @param value The date value to compare against. * @returns The query string. */ - fun updatedAfter(value: String) = Query("updatedAfter", null, listOf(value)).toJson() + fun updatedAfter(value: String) = greaterThan("\$updatedAt", value) /** * Filter resources where document was updated between start and end dates (inclusive). @@ -286,7 +293,7 @@ class Query( * @param end The end date value. * @returns The query string. */ - fun updatedBetween(start: String, end: String) = Query("updatedBetween", null, listOf(start, end)).toJson() + fun updatedBetween(start: String, end: String) = between("\$updatedAt", start, end) /** * Combine multiple queries using logical OR operator. diff --git a/templates/android/library/src/main/java/io/package/models/Model.kt.twig b/templates/android/library/src/main/java/io/package/models/Model.kt.twig index 27e153ed82..2f71cedc0a 100644 --- a/templates/android/library/src/main/java/io/package/models/Model.kt.twig +++ b/templates/android/library/src/main/java/io/package/models/Model.kt.twig @@ -2,6 +2,11 @@ package {{ sdk.namespace | caseDot }}.models import com.google.gson.annotations.SerializedName import io.appwrite.extensions.jsonCast +{%~ for property in definition.properties %} +{%~ if property.enum %} +import {{ sdk.namespace | caseDot }}.enums.{{ property.enumName | caseUcfirst }} +{%~ endif %} +{%~ endfor %} /** * {{ definition.description | replace({"\n": "\n * "}) | raw }} @@ -27,7 +32,7 @@ import io.appwrite.extensions.jsonCast ) { fun toMap(): Map = mapOf( {%~ for property in definition.properties %} - "{{ property.name | escapeDollarSign }}" to {% if property.sub_schema %}{% if property.type == 'array' %}{{property.name | escapeKeyword | removeDollarSign}}.map { it.toMap() }{% else %}{{property.name | escapeKeyword | removeDollarSign}}.toMap(){% endif %}{% else %}{{property.name | escapeKeyword | removeDollarSign}}{% endif %} as Any, + "{{ property.name | escapeDollarSign }}" to {% if property.sub_schema %}{% if property.type == 'array' %}{{property.name | escapeKeyword | removeDollarSign}}.map { it.toMap() }{% else %}{{property.name | escapeKeyword | removeDollarSign}}.toMap(){% endif %}{% elseif property.enum %}{{property.name | escapeKeyword | removeDollarSign}}{% if not property.required %}?{% endif %}.value{% else %}{{property.name | escapeKeyword | removeDollarSign}}{% endif %} as Any, {%~ endfor %} {%~ if definition.additionalProperties %} "data" to data!!.jsonCast(to = Map::class.java) @@ -61,10 +66,10 @@ import io.appwrite.extensions.jsonCast {%~ endif %} ) = {{ definition | modelType(spec) | raw }}( {%~ for property in definition.properties %} - {{ property.name | escapeKeyword | removeDollarSign }} = {% if property.sub_schema %}{% if property.type == 'array' %}(map["{{ property.name | escapeDollarSign }}"] as List>).map { {{ property.sub_schema | caseUcfirst }}.from(map = it{% if property.sub_schema | hasGenericType(spec) %}, nestedType{% endif %}) }{% else %}{{ property.sub_schema | caseUcfirst }}.from(map = map["{{property.name | escapeDollarSign }}"] as Map{% if property.sub_schema | hasGenericType(spec) %}, nestedType{% endif %}){% endif %}{% else %}{% if property.type == "integer" or property.type == "number" %}({% endif %}map["{{ property.name | escapeDollarSign }}"]{% if property.type == "integer" or property.type == "number" %} as{% if not property.required %}?{% endif %} Number){% endif %}{% if property.type == "integer" %}{% if not property.required %}?{% endif %}.toLong(){% elseif property.type == "number" %}{% if not property.required %}?{% endif %}.toDouble(){% else %} as{% if not property.required %}?{% endif %} {{ property | propertyType(spec) | raw }}{% endif %}{% endif %}, + {{ property.name | escapeKeyword | removeDollarSign }} = {{ property | propertyAssignment(spec) | raw }}, {%~ endfor %} {%~ if definition.additionalProperties %} - data = map.jsonCast(to = nestedType) + data = map["data"]?.jsonCast(to = nestedType) ?: map.jsonCast(to = nestedType) {%~ endif %} ) } diff --git a/templates/apple/Package.swift.twig b/templates/apple/Package.swift.twig index 12b50297f4..b8d3e4008b 100644 --- a/templates/apple/Package.swift.twig +++ b/templates/apple/Package.swift.twig @@ -34,7 +34,7 @@ let package = Package( {%~ if spec.definitions is not empty %} "{{spec.title | caseUcfirst}}Models", {%~ endif %} - {%~ if spec.enums is not empty %} + {%~ if spec.allEnums is not empty %} "{{spec.title | caseUcfirst}}Enums", {%~ endif %} "JSONCodable" @@ -44,11 +44,14 @@ let package = Package( .target( name: "{{spec.title | caseUcfirst}}Models", dependencies: [ + {%~ if spec.allEnums is not empty %} + "{{spec.title | caseUcfirst}}Enums", + {%~ endif %} "JSONCodable" ] ), {%~ endif %} - {%~ if spec.enums is not empty %} + {%~ if spec.allEnums is not empty %} .target( name: "{{spec.title | caseUcfirst}}Enums" ), diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig index ca97921235..e969436a6e 100644 --- a/templates/apple/Sources/Client.swift.twig +++ b/templates/apple/Sources/Client.swift.twig @@ -5,6 +5,7 @@ import NIOSSL import Foundation import AsyncHTTPClient @_exported import {{spec.title | caseUcfirst}}Models +@_exported import JSONCodable let DASHDASH = "--" let CRLF = "\r\n" @@ -37,7 +38,6 @@ open class Client { internal var http: HTTPClient - private static let boundaryChars = "abcdefghijklmnopqrstuvwxyz1234567890" private static let boundary = randomBoundary() @@ -265,7 +265,6 @@ open class Client { var request = HTTPClientRequest(url: endPoint + path + queryParameters) request.method = .RAW(value: method) - for (key, value) in self.headers.merging(headers, uniquingKeysWith: { $1 }) { request.headers.add(name: key, value: value) } @@ -304,7 +303,7 @@ open class Client { if let warning = response.headers["x-{{ spec.title | lower }}-warning"].first { warning.split(separator: ";").forEach { warning in - print("Warning: \(warning)") + fputs("Warning: \(warning)\n", stderr) } } diff --git a/templates/swift/Sources/Services/Realtime.swift.twig b/templates/apple/Sources/Services/Realtime.swift.twig similarity index 91% rename from templates/swift/Sources/Services/Realtime.swift.twig rename to templates/apple/Sources/Services/Realtime.swift.twig index 88b06f9ab5..5c2c2c401b 100644 --- a/templates/swift/Sources/Services/Realtime.swift.twig +++ b/templates/apple/Sources/Services/Realtime.swift.twig @@ -22,6 +22,22 @@ open class Realtime : Service { private var reconnectAttempts = 0 private var subscriptionsCounter = 0 private var reconnect = true + + private var onErrorCallbacks: [((Swift.Error?, HTTPResponseStatus?) -> Void)] = [] + private var onCloseCallbacks: [(() -> Void)] = [] + private var onOpenCallbacks: [(() -> Void)] = [] + + public func onError(_ callback: @escaping (Swift.Error?, HTTPResponseStatus?) -> Void) { + self.onErrorCallbacks.append(callback) + } + + public func onClose(_ callback: @escaping () -> Void) { + self.onCloseCallbacks.append(callback) + } + + public func onOpen(_ callback: @escaping () -> Void) { + self.onOpenCallbacks.append(callback) + } private func startHeartbeat() { stopHeartbeat() @@ -191,6 +207,7 @@ extension Realtime: WebSocketClientDelegate { public func onOpen(channel: Channel) { self.reconnectAttempts = 0 + onOpenCallbacks.forEach { $0() } startHeartbeat() } @@ -210,6 +227,8 @@ extension Realtime: WebSocketClientDelegate { public func onClose(channel: Channel, data: Data) async throws { stopHeartbeat() + + onCloseCallbacks.forEach { $0() } if (!reconnect) { reconnect = true @@ -230,6 +249,8 @@ extension Realtime: WebSocketClientDelegate { public func onError(error: Swift.Error?, status: HTTPResponseStatus?) { stopHeartbeat() print(error?.localizedDescription ?? "Unknown error") + + onErrorCallbacks.forEach { $0(error, status) } } func handleResponseError(from json: [String: Any]) throws { diff --git a/templates/cli/install.ps1.twig b/templates/cli/install.ps1.twig index b3dffba931..c04d8a23c7 100644 --- a/templates/cli/install.ps1.twig +++ b/templates/cli/install.ps1.twig @@ -35,7 +35,6 @@ function Greeting { Write-Host "Welcome to the {{ spec.title | caseUcfirst }} CLI install shield." } - function CheckSystemInfo { Write-Host "[1/4] Getting System Info ..." if ((Get-ExecutionPolicy) -gt 'RemoteSigned' -or (Get-ExecutionPolicy) -eq 'ByPass') { @@ -60,7 +59,6 @@ function DownloadBinary { Move-Item ${{ spec.title | upper }}_DOWNLOAD_DIR ${{ spec.title | upper }}_INSTALL_PATH } - function Install { Write-Host "[3/4] Starting installation ..." @@ -83,7 +81,6 @@ function InstallCompleted { Write-Host "As first step, you can login to your {{ spec.title | caseUcfirst }} account using 'appwrite login'" } - Greeting CheckSystemInfo DownloadBinary diff --git a/templates/cli/install.sh.twig b/templates/cli/install.sh.twig index 7faa92a6ab..c27e23346d 100644 --- a/templates/cli/install.sh.twig +++ b/templates/cli/install.sh.twig @@ -39,7 +39,6 @@ RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' # No Color - greeting() { echo -e "${RED}" cat << "EOF" diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 0c550de099..0ebb00798f 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -172,12 +172,11 @@ const deleteSession = async (accountId) => { parseOutput: false, sdk: client }) - - globalConfig.removeSession(accountId); } catch (e) { error('Unable to log out, removing locally saved session information') + } finally { + globalConfig.removeSession(accountId); } - globalConfig.removeSession(accountId); } const logout = new Command("logout") @@ -195,6 +194,7 @@ const logout = new Command("logout") } if (sessions.length === 1) { await deleteSession(current); + globalConfig.setCurrentSession(''); success("Logging out"); return; @@ -216,6 +216,8 @@ const logout = new Command("logout") globalConfig.setCurrentSession(accountId); success(`Current account is ${accountId}`); + } else if (remainingSessions.length === 0) { + globalConfig.setCurrentSession(''); } success("Logging out"); diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index 8102da476f..036a7a517a 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -19,6 +19,7 @@ const { questionsCreateBucket, questionsCreateMessagingTopic, questionsCreateCollection, + questionsCreateTable, questionsInitProject, questionsInitProjectAutopull, questionsInitResources, @@ -34,10 +35,11 @@ const initResources = async () => { const actions = { function: initFunction, site: initSite, - collection: initCollection, + table: initTable, bucket: initBucket, team: initTeam, - message: initTopic + message: initTopic, + collection: initCollection } const answers = await inquirer.prompt(questionsInitResources[0]); @@ -160,6 +162,40 @@ const initTeam = async () => { log("Next you can use 'appwrite push team' to deploy the changes."); }; +const initTable = async () => { + const answers = await inquirer.prompt(questionsCreateTable) + const newDatabase = (answers.method ?? '').toLowerCase() !== 'existing'; + + if (!newDatabase) { + answers.databaseId = answers.database; + answers.databaseName = localConfig.getTablesDB(answers.database).name; + } + + const databaseId = answers.databaseId === 'unique()' ? ID.unique() : answers.databaseId; + + if (newDatabase || !localConfig.getTablesDB(answers.databaseId)) { + localConfig.addTablesDB({ + $id: databaseId, + name: answers.databaseName, + enabled: true + }); + } + + localConfig.addTable({ + $id: answers.id === 'unique()' ? ID.unique() : answers.id, + $permissions: [], + databaseId: databaseId, + name: answers.table, + enabled: true, + rowSecurity: answers.rowSecurity.toLowerCase() === 'yes', + columns: [], + indexes: [], + }); + + success("Initialing table"); + log("Next you can use 'appwrite push table' to deploy the changes."); +}; + const initCollection = async () => { const answers = await inquirer.prompt(questionsCreateCollection) const newDatabase = (answers.method ?? '').toLowerCase() !== 'existing'; @@ -557,6 +593,12 @@ init .description("Init a new {{ spec.title|caseUcfirst }} collection") .action(actionRunner(initCollection)); +init + .command("table") + .alias("tables") + .description("Init a new {{ spec.title|caseUcfirst }} table") + .action(actionRunner(initTable)); + init .command("topic") .alias("topics") diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index 7d20b5123d..4560b224d1 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -358,7 +358,7 @@ const pullTable = async () => { }); if (fetchResponse["databases"].length <= 0) { log("No tables found."); - success(`Successfully pulled ${chalk.bold(totalTables)} tables from ${chalk.bold(totalTablesDBs)} tables databases.`); + success(`Successfully pulled ${chalk.bold(totalTables)} tables from ${chalk.bold(totalTablesDBs)} tableDBs.`); return; } @@ -398,7 +398,7 @@ const pullTable = async () => { } } - success(`Successfully pulled ${chalk.bold(totalTables)} tables from ${chalk.bold(totalTablesDBs)} tables databases.`); + success(`Successfully pulled ${chalk.bold(totalTables)} tables from ${chalk.bold(totalTablesDBs)} tableDBs.`); } const pullBucket = async () => { diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 899d5370f5..09ae6c6364 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -43,6 +43,12 @@ const { databasesUpdateEnumAttribute, databasesUpdateRelationshipAttribute, databasesCreateRelationshipAttribute, + databasesCreatePointAttribute, + databasesUpdatePointAttribute, + databasesCreateLineAttribute, + databasesUpdateLineAttribute, + databasesCreatePolygonAttribute, + databasesUpdatePolygonAttribute, databasesDeleteAttribute, databasesDeleteIndex, databasesListAttributes, @@ -50,10 +56,16 @@ const { databasesUpdateCollection } = require("./databases"); const { + tablesDBCreate, tablesDBGet, + tablesDBUpdate, + tablesDBCreateTable, tablesDBGetTable, tablesDBUpdateTable, - tablesDBCreateTable + tablesDBList, + tablesDBDelete, + tablesDBListTables, + tablesDBDeleteTable } = require("./tables-db"); const { storageGetBucket, storageUpdateBucket, storageCreateBucket @@ -548,7 +560,7 @@ const createAttribute = (databaseId, collectionId, attribute) => { return databasesCreateRelationshipAttribute({ databaseId, collectionId, - relatedCollectionId: attribute.relatedCollection, + relatedCollectionId: attribute.relatedTable ?? attribute.relatedCollection, type: attribute.relationType, twoWay: attribute.twoWay, key: attribute.key, @@ -556,6 +568,33 @@ const createAttribute = (databaseId, collectionId, attribute) => { onDelete: attribute.onDelete, parseOutput: false }) + case 'point': + return databasesCreatePointAttribute({ + databaseId, + collectionId, + key:attribute.key, + required:attribute.required, + xdefault:attribute.default, + parseOutput:false + }) + case 'linestring': + return databasesCreateLineAttribute({ + databaseId, + collectionId, + key:attribute.key, + required:attribute.required, + xdefault:attribute.default, + parseOutput:false + }) + case 'polygon': + return databasesCreatePolygonAttribute({ + databaseId, + collectionId, + key:attribute.key, + required:attribute.required, + xdefault:attribute.default, + parseOutput:false + }) default: throw new Error(`Unsupported attribute type: ${attribute.type}`); } @@ -667,7 +706,7 @@ const updateAttribute = (databaseId, collectionId, attribute) => { return databasesUpdateRelationshipAttribute({ databaseId, collectionId, - relatedCollectionId: attribute.relatedCollection, + relatedCollectionId: attribute.relatedTable ?? attribute.relatedCollection, type: attribute.relationType, twoWay: attribute.twoWay, key: attribute.key, @@ -675,6 +714,33 @@ const updateAttribute = (databaseId, collectionId, attribute) => { onDelete: attribute.onDelete, parseOutput: false }) + case 'point': + return databasesUpdatePointAttribute({ + databaseId, + collectionId, + key:attribute.key, + required:attribute.required, + xdefault:attribute.default, + parseOutput:false + }) + case 'linestring': + return databasesUpdateLineAttribute({ + databaseId, + collectionId, + key:attribute.key, + required:attribute.required, + xdefault:attribute.default, + parseOutput:false + }) + case 'polygon': + return databasesUpdatePolygonAttribute({ + databaseId, + collectionId, + key:attribute.key, + required:attribute.required, + xdefault:attribute.default, + parseOutput:false + }) default: throw new Error(`Unsupported attribute type: ${attribute.type}`); } @@ -745,7 +811,6 @@ const compareAttribute = (remote, local, reason, key) => { return reason } - /** * Check if attribute non-changeable fields has been changed * If so return the differences as an object. @@ -763,7 +828,7 @@ const checkAttributeChanges = (remote, local, collection, recreating = true) => const keyName = `${chalk.yellow(local.key)} in ${collection.name} (${collection['$id']})`; const action = chalk.cyan(recreating ? 'recreating' : 'changing'); let reason = ''; - let attribute = remote; + let attribute = recreating ? remote : local; for (let key of Object.keys(remote)) { if (!KeysAttributes.has(key)) { @@ -832,14 +897,16 @@ const attributesToCreate = async (remoteAttributes, localAttributes, collection, if (!cliConfig.force) { if (deleting.length > 0 && !isIndex) { - console.log(`${chalk.red('-------------------------------------------------------')}`); + console.log(`${chalk.red('------------------------------------------------------')}`); console.log(`${chalk.red('| WARNING: Attribute deletion may cause loss of data |')}`); - console.log(`${chalk.red('-------------------------------------------------------')}`); + console.log(`${chalk.red('------------------------------------------------------')}`); + console.log(); } if (conflicts.length > 0 && !isIndex) { - console.log(`${chalk.red('---------------------------------------------------------')}`); + console.log(`${chalk.red('--------------------------------------------------------')}`); console.log(`${chalk.red('| WARNING: Attribute recreation may cause loss of data |')}`); - console.log(`${chalk.red('---------------------------------------------------------')}`); + console.log(`${chalk.red('--------------------------------------------------------')}`); + console.log(); } if ((await getConfirmation()) !== true) { @@ -881,7 +948,7 @@ const createIndexes = async (indexes, collection) => { collectionId: collection['$id'], key: index.key, type: index.type, - attributes: index.attributes, + attributes: index.columns ?? index.attributes, orders: index.orders, parseOutput: false }); @@ -1626,7 +1693,6 @@ const pushFunction = async ({ functionId, async, code, withVariables } = { retur parseOutput: false }); - const status = response['status']; if (status === 'ready') { successfullyDeployed++; @@ -1698,6 +1764,158 @@ const pushFunction = async ({ functionId, async, code, withVariables } = { retur } } +const checkAndApplyTablesDBChanges = async () => { + log('Checking for tablesDB changes ...'); + + const localTablesDBs = localConfig.getTablesDBs(); + const { databases: remoteTablesDBs } = await paginate(tablesDBList, { parseOutput: false }, 100, 'databases'); + + if (localTablesDBs.length === 0 && remoteTablesDBs.length === 0) { + return { applied: false, resyncNeeded: false }; + } + + const changes = []; + const toCreate = []; + const toUpdate = []; + const toDelete = []; + + // Check for deletions - remote DBs that aren't in local config + for (const remoteDB of remoteTablesDBs) { + const localDB = localTablesDBs.find(db => db.$id === remoteDB.$id); + if (!localDB) { + toDelete.push(remoteDB); + changes.push({ + id: remoteDB.$id, + action: chalk.red('deleting'), + key: 'Database', + remote: remoteDB.name, + local: '(deleted locally)' + }); + } + } + + // Check for additions and updates + for (const localDB of localTablesDBs) { + const remoteDB = remoteTablesDBs.find(db => db.$id === localDB.$id); + + if (!remoteDB) { + toCreate.push(localDB); + changes.push({ + id: localDB.$id, + action: chalk.green('creating'), + key: 'Database', + remote: '(does not exist)', + local: localDB.name + }); + } else { + let hasChanges = false; + + if (remoteDB.name !== localDB.name) { + hasChanges = true; + changes.push({ + id: localDB.$id, + action: chalk.yellow('updating'), + key: 'Name', + remote: remoteDB.name, + local: localDB.name + }); + } + + if (remoteDB.enabled !== localDB.enabled) { + hasChanges = true; + changes.push({ + id: localDB.$id, + action: chalk.yellow('updating'), + key: 'Enabled', + remote: remoteDB.enabled, + local: localDB.enabled + }); + } + + if (hasChanges) { + toUpdate.push(localDB); + } + } + } + + if (changes.length === 0) { + console.log('No changes found in tablesDB resource'); + console.log(); + return { applied: false, resyncNeeded: false }; + } + + log('Found changes in tablesDB resource:'); + drawTable(changes); + + if (toDelete.length > 0) { + console.log(`${chalk.red('------------------------------------------------------------------')}`); + console.log(`${chalk.red('| WARNING: Database deletion will also delete all related tables |')}`); + console.log(`${chalk.red('------------------------------------------------------------------')}`); + console.log(); + } + + if ((await getConfirmation()) !== true) { + return { applied: false, resyncNeeded: false }; + } + + // Apply deletions first + let needsResync = false; + for (const db of toDelete) { + try { + log(`Deleting database ${db.name} ( ${db.$id} ) ...`); + await tablesDBDelete({ + databaseId: db.$id, + parseOutput: false + }); + success(`Deleted ${db.name} ( ${db.$id} )`); + needsResync = true; + } catch (e) { + error(`Failed to delete database ${db.name} ( ${db.$id} ): ${e.message}`); + throw new Error(`Database sync failed during deletion of ${db.$id}. Some changes may have been applied.`); + } + } + + // Apply creations + for (const db of toCreate) { + try { + log(`Creating database ${db.name} ( ${db.$id} ) ...`); + await tablesDBCreate({ + databaseId: db.$id, + name: db.name, + enabled: db.enabled, + parseOutput: false + }); + success(`Created ${db.name} ( ${db.$id} )`); + } catch (e) { + error(`Failed to create database ${db.name} ( ${db.$id} ): ${e.message}`); + throw new Error(`Database sync failed during creation of ${db.$id}. Some changes may have been applied.`); + } + } + + // Apply updates + for (const db of toUpdate) { + try { + log(`Updating database ${db.name} ( ${db.$id} ) ...`); + await tablesDBUpdate({ + databaseId: db.$id, + name: db.name, + enabled: db.enabled, + parseOutput: false + }); + success(`Updated ${db.name} ( ${db.$id} )`); + } catch (e) { + error(`Failed to update database ${db.name} ( ${db.$id} ): ${e.message}`); + throw new Error(`Database sync failed during update of ${db.$id}. Some changes may have been applied.`); + } + } + + if (toDelete.length === 0){ + console.log(); + } + + return { applied: true, resyncNeeded: needsResync }; +}; + const pushTable = async ({ returnOnZero, attempts } = { returnOnZero: false }) => { const tables = []; @@ -1705,6 +1923,85 @@ const pushTable = async ({ returnOnZero, attempts } = { returnOnZero: false }) = pollMaxDebounces = attempts; } + const { applied: tablesDBApplied, resyncNeeded } = await checkAndApplyTablesDBChanges(); + if (resyncNeeded) { + log('Resyncing configuration due to tablesDB deletions ...'); + + const remoteTablesDBs = (await paginate(tablesDBList, { parseOutput: false }, 100, 'databases')).databases; + const localTablesDBs = localConfig.getTablesDBs(); + + const remoteDatabaseIds = new Set(remoteTablesDBs.map(db => db.$id)); + const localTables = localConfig.getTables(); + const validTables = localTables.filter(table => remoteDatabaseIds.has(table.databaseId)); + + localConfig.set('tables', validTables); + + const validTablesDBs = localTablesDBs.filter(db => remoteDatabaseIds.has(db.$id)); + localConfig.set('tablesDB', validTablesDBs); + + success('Configuration resynced successfully.'); + console.log(); + } + + log('Checking for deleted tables ...'); + const localTablesDBs = localConfig.getTablesDBs(); + const localTables = localConfig.getTables(); + const tablesToDelete = []; + + for (const db of localTablesDBs) { + try { + const { tables: remoteTables } = await paginate(tablesDBListTables, { + databaseId: db.$id, + parseOutput: false + }, 100, 'tables'); + + for (const remoteTable of remoteTables) { + const localTable = localTables.find(t => t.$id === remoteTable.$id && t.databaseId === db.$id); + if (!localTable) { + tablesToDelete.push({ + ...remoteTable, + databaseId: db.$id, + databaseName: db.name + }); + } + } + } catch (e) { + // Skip if database doesn't exist or other errors + } + } + + if (tablesToDelete.length > 0) { + log('Found tables that exist remotely but not locally:'); + const deletionChanges = tablesToDelete.map(table => ({ + id: table.$id, + action: chalk.red('deleting'), + key: 'Table', + database: table.databaseName, + remote: table.name, + local: '(deleted locally)' + })); + drawTable(deletionChanges); + + if ((await getConfirmation()) === true) { + for (const table of tablesToDelete) { + try { + log(`Deleting table ${table.name} ( ${table.$id} ) from database ${table.databaseName} ...`); + await tablesDBDeleteTable({ + databaseId: table.databaseId, + tableId: table.$id, + parseOutput: false + }); + success(`Deleted ${table.name} ( ${table.$id} )`); + } catch (e) { + error(`Failed to delete table ${table.name} ( ${table.$id} ): ${e.message}`); + } + } + } + } else { + console.log('No tables found to delete'); + } + console.log(); + if (cliConfig.all) { checkDeployConditions(localConfig); tables.push(...localConfig.getTables()); @@ -1728,43 +2025,12 @@ const pushTable = async ({ returnOnZero, attempts } = { returnOnZero: false }) = return; } - const databases = Array.from(new Set(tables.map(table => table['databaseId']))); - - // Parallel db actions - await Promise.all(databases.map(async (databaseId) => { - const localDatabase = localConfig.getTablesDB(databaseId); - - try { - const database = await tablesDBGet({ - databaseId: databaseId, - parseOutput: false, - }); - - if (database.name !== (localDatabase.name ?? databaseId)) { - await databasesUpdate({ - databaseId: databaseId, - name: localDatabase.name ?? databaseId, - parseOutput: false - }) - - success(`Updated ${localDatabase.name} ( ${databaseId} ) name`); - } - } catch (err) { - log(`Database ${databaseId} not found. Creating it now ...`); - - await databasesCreate({ - databaseId: databaseId, - name: localDatabase.name ?? databaseId, - parseOutput: false, - }); - } - })); - - - if (!(await approveChanges(tables, tablesDBGetTable, KeysTable, 'tableId', 'tables', ['columns', 'indexes'], 'databaseId', 'databaseId',))) { + if (!(await approveChanges(tables, tablesDBGetTable, KeysTable, 'tableId', 'tables', ['columns', 'indexes'], 'databaseId', 'databaseId'))) { return; } - // Parallel collection actions + let tablesChanged = new Set(); + + // Parallel tables actions await Promise.all(tables.map(async (table) => { try { const remoteTable = await tablesDBGetTable({ @@ -1773,15 +2039,24 @@ const pushTable = async ({ returnOnZero, attempts } = { returnOnZero: false }) = parseOutput: false, }); - if (remoteTable.name !== table.name) { + const changes = []; + if (remoteTable.name !== table.name) changes.push('name'); + if (remoteTable.rowSecurity !== table.rowSecurity) changes.push('rowSecurity'); + if (remoteTable.enabled !== table.enabled) changes.push('enabled'); + if (JSON.stringify(remoteTable['$permissions']) !== JSON.stringify(table['$permissions'])) changes.push('permissions'); + + if (changes.length > 0) { await tablesDBUpdateTable({ databaseId: table['databaseId'], tableId: table['$id'], name: table.name, - parseOutput: false + parseOutput: false, + rowSecurity: table.rowSecurity, + permissions: table['$permissions'] }) - success(`Updated ${table.name} ( ${table['$id']} ) name`); + success(`Updated ${table.name} ( ${table['$id']} ) - ${changes.join(', ')}`); + tablesChanged.add(table['$id']); } table.remoteVersion = remoteTable; @@ -1794,16 +2069,19 @@ const pushTable = async ({ returnOnZero, attempts } = { returnOnZero: false }) = databaseId: table['databaseId'], tableId: table['$id'], name: table.name, - documentSecurity: table.documentSecurity, + rowSecurity: table.rowSecurity, permissions: table['$permissions'], parseOutput: false }) + + success(`Created ${table.name} ( ${table['$id']} )`); + tablesChanged.add(table['$id']); } else { throw e; } } })) - let numberOfTables = 0; + // Serialize attribute actions for (let table of tables) { let columns = table.columns; @@ -1831,11 +2109,11 @@ const pushTable = async ({ returnOnZero, attempts } = { returnOnZero: false }) = } catch (e) { throw e; } - numberOfTables++; + tablesChanged.add(table['$id']); success(`Successfully pushed ${table.name} ( ${table['$id']} )`); } - success(`Successfully pushed ${numberOfTables} tables`); + success(`Successfully pushed ${tablesChanged.size} tables`); } const pushCollection = async ({ returnOnZero, attempts } = { returnOnZero: false }) => { @@ -1901,7 +2179,6 @@ const pushCollection = async ({ returnOnZero, attempts } = { returnOnZero: false } })); - if (!(await approveChanges(collections, databasesGetCollection, KeysCollection, 'collectionId', 'collections', ['attributes', 'indexes'], 'databaseId', 'databaseId',))) { return; } @@ -2100,7 +2377,6 @@ const pushTeam = async ({ returnOnZero } = { returnOnZero: false }) => { return; } - log('Pushing teams ...'); for (let team of teams) { diff --git a/templates/cli/lib/commands/update.js.twig b/templates/cli/lib/commands/update.js.twig index 16fe8838ae..370a665a04 100644 --- a/templates/cli/lib/commands/update.js.twig +++ b/templates/cli/lib/commands/update.js.twig @@ -45,8 +45,6 @@ const isInstalledViaHomebrew = () => { } }; - - /** * Execute command and return promise */ diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index aea469f781..85b2528797 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -9,7 +9,7 @@ const KeysSite = new Set(["path", "$id", "name", "enabled", "logging", "timeout" const KeysFunction = new Set(["path", "$id", "execute", "name", "enabled", "logging", "runtime", "specification", "scopes", "events", "schedule", "timeout", "entrypoint", "commands", "vars"]); const KeysDatabase = new Set(["$id", "name", "enabled"]); const KeysCollection = new Set(["$id", "$permissions", "databaseId", "name", "enabled", "documentSecurity", "attributes", "indexes"]); -const KeysTable = new Set(["$id", "$permissions", "databaseId", "name", "enabled", "documentSecurity", "columns", "indexes"]); +const KeysTable = new Set(["$id", "$permissions", "databaseId", "name", "enabled", "rowSecurity", "columns", "indexes"]); const KeysStorage = new Set(["$id", "$permissions", "fileSecurity", "name", "enabled", "maximumFileSize", "allowedFileExtensions", "compression", "encryption", "antivirus"]); const KeysTopics = new Set(["$id", "name", "subscribe"]); const KeysTeams = new Set(["$id", "name"]); @@ -62,12 +62,13 @@ const KeysColumns = new Set([ "onDelete", "side", // Indexes - "attributes", + "columns", "orders", // Strings "encrypt", ]); const KeyIndexes = new Set(["key", "type", "status", "attributes", "orders"]); +const KeyIndexesColumns = new Set(["key", "type", "status", "columns", "orders"]); function whitelistKeys(value, keys, nestedKeys = {}) { if (Array.isArray(value)) { @@ -404,7 +405,7 @@ class Local extends Config { addTable(props) { props = whitelistKeys(props, KeysTable, { columns: KeysColumns, - indexes: KeyIndexes + indexes: KeyIndexesColumns }); if (!this.has("tables")) { @@ -680,15 +681,25 @@ class Global extends Config { getSessions() { const sessions = Object.keys(this.data).filter((key) => !Global.IGNORE_ATTRIBUTES.includes(key)) + const current = this.getCurrentSession(); - return sessions.map((session) => { - - return { - id: session, - endpoint: this.data[session][Global.PREFERENCE_ENDPOINT], - email: this.data[session][Global.PREFERENCE_EMAIL] + const sessionMap = new Map(); + + sessions.forEach((sessionId) => { + const email = this.data[sessionId][Global.PREFERENCE_EMAIL]; + const endpoint = this.data[sessionId][Global.PREFERENCE_ENDPOINT]; + const key = `${email}|${endpoint}`; + + if (sessionId === current || !sessionMap.has(key)) { + sessionMap.set(key, { + id: sessionId, + endpoint: this.data[sessionId][Global.PREFERENCE_ENDPOINT], + email: this.data[sessionId][Global.PREFERENCE_EMAIL] + }); } - }) + }); + + return Array.from(sessionMap.values()); } addSession(session, data) { diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index c05f089330..0c9c5bdb4f 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -138,7 +138,6 @@ const parseError = (err) => { log(`To report this error you can:\n - Create a support ticket in our Discord server https://appwrite.io/discord \n - Create an issue in our Github\n ${githubIssueUrl.href}\n`); - error('\n Stack Trace: \n'); console.error(err); process.exit(1); diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 4b2265bb6c..c2cba2c803 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -397,8 +397,6 @@ const questionsCreateFunctionSelectTemplate = (templates) => { ]; }; - - const questionsCreateBucket = [ { type: "input", @@ -496,7 +494,73 @@ const questionsCreateCollection = [ { type: "list", name: "documentSecurity", - message: "Enable Document-Security for configuring permissions for individual documents", + message: "Enable document security for configuring permissions for individual documents", + choices: ["No", "Yes"] + } +]; + +const questionsCreateTable = [ + { + type: "list", + name: "method", + message: "What database would you like to use for your table?", + choices: ["New", "Existing"], + when: async () => { + return localConfig.getTablesDBs().length !== 0; + } + }, + { + type: "search-list", + name: "database", + message: "Choose the table database", + choices: async () => { + const databases = localConfig.getTablesDBs(); + + let choices = databases.map((database, idx) => { + return { + name: `${database.name} (${database.$id})`, + value: database.$id + } + }) + + if (choices.length === 0) { + throw new Error("No databases found. Please create one in project console.") + } + + return choices; + }, + when: (answers) => (answers.method ?? '').toLowerCase() === 'existing' + }, + { + type: "input", + name: "databaseName", + message: "What would you like to name your database?", + default: "My Awesome Database", + when: (answers) => (answers.method ?? '').toLowerCase() !== 'existing' + }, + { + type: "input", + name: "databaseId", + message: "What ID would you like to have for your database?", + default: "unique()", + when: (answers) => (answers.method ?? '').toLowerCase() !== 'existing' + }, + { + type: "input", + name: "table", + message: "What would you like to name your table?", + default: "My Awesome Table" + }, + { + type: "input", + name: "id", + message: "What ID would you like to have for your table?", + default: "unique()" + }, + { + type: "list", + name: "rowSecurity", + message: "Enable row security for configuring permissions for individual rows", choices: ["No", "Yes"] } ]; @@ -1001,6 +1065,7 @@ module.exports = { questionsCreateFunctionSelectTemplate, questionsCreateBucket, questionsCreateCollection, + questionsCreateTable, questionsCreateMessagingTopic, questionsPullFunctions, questionsPullFunctionsCode, diff --git a/templates/cli/lib/spinner.js.twig b/templates/cli/lib/spinner.js.twig index bcc7e6bf6c..99d8897c0d 100644 --- a/templates/cli/lib/spinner.js.twig +++ b/templates/cli/lib/spinner.js.twig @@ -96,7 +96,6 @@ class Spinner { } } - module.exports = { Spinner, SPINNER_ARC, diff --git a/templates/cli/lib/type-generation/attribute.js.twig b/templates/cli/lib/type-generation/attribute.js.twig index 70bfc38698..e36d0558ce 100644 --- a/templates/cli/lib/type-generation/attribute.js.twig +++ b/templates/cli/lib/type-generation/attribute.js.twig @@ -9,6 +9,9 @@ const AttributeType = { URL: "url", ENUM: "enum", RELATIONSHIP: "relationship", + POINT: "point", + LINESTRING: "linestring", + POLYGON: "polygon", }; module.exports = { diff --git a/templates/cli/lib/type-generation/languages/csharp.js.twig b/templates/cli/lib/type-generation/languages/csharp.js.twig index 1df7c7cdb7..ff85bf675a 100644 --- a/templates/cli/lib/type-generation/languages/csharp.js.twig +++ b/templates/cli/lib/type-generation/languages/csharp.js.twig @@ -3,7 +3,7 @@ const { AttributeType } = require('../attribute'); const { LanguageMeta } = require("./language"); class CSharp extends LanguageMeta { - getType(attribute, collections) { + getType(attribute, collections, collectionName) { let type = ""; switch (attribute.type) { case AttributeType.STRING: @@ -11,7 +11,7 @@ class CSharp extends LanguageMeta { case AttributeType.DATETIME: type = "string"; if (attribute.format === AttributeType.ENUM) { - type = LanguageMeta.toPascalCase(attribute.key); + type = LanguageMeta.toPascalCase(collectionName) + LanguageMeta.toPascalCase(attribute.key); } break; case AttributeType.INTEGER: @@ -33,6 +33,15 @@ class CSharp extends LanguageMeta { type = `List<${type}>`; } break; + case AttributeType.POINT: + type = "List"; + break; + case AttributeType.LINESTRING: + type = "List>"; + break; + case AttributeType.POLYGON: + type = "List>>"; + break; default: throw new Error(`Unknown attribute type: ${attribute.type}`); } @@ -60,7 +69,7 @@ namespace Appwrite.Models <% for (const attribute of collection.attributes) { -%> <% if (attribute.format === 'enum') { -%> -public enum <%- toPascalCase(attribute.key) %> { +public enum <%- toPascalCase(collection.name) %><%- toPascalCase(attribute.key) %> { <% for (const [index, element] of Object.entries(attribute.elements) ) { -%> [JsonPropertyName("<%- element %>")] <%- toPascalCase(element) %><% if (index < attribute.elements.length - 1) { %>,<% } %> @@ -72,13 +81,13 @@ public class <%= toPascalCase(collection.name) %> { <% for (const [index, attribute] of Object.entries(collection.attributes)) { -%> [JsonPropertyName("<%- attribute.key %>")] - public <%- getType(attribute, collections) %> <%= toPascalCase(attribute.key) %> { get; private set; } + public <%- getType(attribute, collections, collection.name) %> <%= toPascalCase(attribute.key) %> { get; private set; } <% } -%> public <%= toPascalCase(collection.name) %>( <% for (const [index, attribute] of Object.entries(collection.attributes)) { -%> - <%- getType(attribute, collections) %> <%= toCamelCase(attribute.key) %><% if (index < collection.attributes.length - 1) { %>,<% } %> + <%- getType(attribute, collections, collection.name) %> <%= toCamelCase(attribute.key) %><% if (index < collection.attributes.length - 1) { %>,<% } %> <% } -%> ) { @@ -93,9 +102,9 @@ public class <%= toPascalCase(collection.name) %> // ENUM if (attribute.format === 'enum') { if (attribute.array) { - -%>((IEnumerable)map["<%- attribute.key %>"]).Select(e => Enum.Parse>(e.ToString()!, true)).ToList()<% + -%>((IEnumerable)map["<%- attribute.key %>"]).Select(e => Enum.Parse<%- toPascalCase(attribute.key) %>>(e.ToString()!, true)).ToList()<% } else { - -%>Enum.Parse>(map["<%- attribute.key %>"].ToString()!, true)<% + -%>Enum.Parse<%- toPascalCase(attribute.key) %>>(map["<%- attribute.key %>"].ToString()!, true)<% } // RELATIONSHIP } else if (attribute.type === 'relationship') { @@ -122,7 +131,7 @@ public class <%= toPascalCase(collection.name) %> } else if (attribute.type === 'double') { -%><%- !attribute.required ? 'map["' + attribute.key + '"] == null ? null : ' : '' %>Convert.ToDouble(map["<%- attribute.key %>"])<% } else if (attribute.type === 'boolean') { - -%>(<%- getType(attribute, collections) %>)map["<%- attribute.key %>"]<% + -%>(<%- getType(attribute, collections, collection.name) %>)map["<%- attribute.key %>"]<% } else if (attribute.type === 'string' || attribute.type === 'datetime' || attribute.type === 'email') { -%>map["<%- attribute.key %>"]<%- !attribute.required ? '?' : '' %>.ToString()<%- attribute.required ? '!' : ''%><% } else { diff --git a/templates/cli/lib/type-generation/languages/dart.js.twig b/templates/cli/lib/type-generation/languages/dart.js.twig index 5161a18f1a..d48ca48684 100644 --- a/templates/cli/lib/type-generation/languages/dart.js.twig +++ b/templates/cli/lib/type-generation/languages/dart.js.twig @@ -40,7 +40,7 @@ class Dart extends LanguageMeta { return 'appwrite'; } - getType(attribute, collections) { + getType(attribute, collections, collectionName) { let type = ""; switch (attribute.type) { case AttributeType.STRING: @@ -48,7 +48,7 @@ class Dart extends LanguageMeta { case AttributeType.DATETIME: type = "String"; if (attribute.format === AttributeType.ENUM) { - type = LanguageMeta.toPascalCase(attribute.key); + type = LanguageMeta.toPascalCase(collectionName) + LanguageMeta.toPascalCase(attribute.key); } break; case AttributeType.INTEGER: @@ -70,6 +70,15 @@ class Dart extends LanguageMeta { type = `List<${type}>`; } break; + case AttributeType.POINT: + type = "List"; + break; + case AttributeType.LINESTRING: + type = "List>"; + break; + case AttributeType.POLYGON: + type = "List>>"; + break; default: throw new Error(`Unknown attribute type: ${attribute.type}`); } @@ -83,104 +92,101 @@ class Dart extends LanguageMeta { } getTemplate() { - return `<% for (const attribute of collection.attributes) { -%> -<% if (attribute.type === 'relationship') { -%> -import '<%- toSnakeCase(collections.find(c => c.$id === attribute.relatedCollection).name) %>.dart'; - -<% } -%> -<% } -%> -/// This file is auto-generated by the Appwrite CLI. -/// You can regenerate it by running \`appwrite ${process.argv.slice(2).join(' ')}\`. - -<% for (const attribute of collection.attributes) { -%> -<% if (attribute.format === 'enum') { -%> -enum <%- toPascalCase(attribute.key) %> { + return `// This file is auto-generated by the Appwrite CLI. +// You can regenerate it by running \`appwrite ${process.argv.slice(2).join(' ')}\`. +<% const __relatedImportsSeen = new Set(); + const sortedAttributes = collection.attributes.slice().sort((a, b) => { + if (a.required === b.required) return 0; + return a.required ? -1 : 1; + }); -%> +<% const __attrs = sortedAttributes; -%> +<% for (const attribute of __attrs) { -%> +<% if (attribute.type === '${AttributeType.RELATIONSHIP}') { -%> +<% const related = collections.find(c => c.$id === attribute.relatedCollection); -%> +<% if (related && !__relatedImportsSeen.has(toSnakeCase(related.name))) { -%> +import '<%- toSnakeCase(related.name) %>.dart'; +<% __relatedImportsSeen.add(toSnakeCase(related.name)); -%> +<% } -%> +<% } -%> +<% } -%> + +<% for (const attribute of __attrs) { -%> +<% if (attribute.format === '${AttributeType.ENUM}') { -%> +enum <%- toPascalCase(collection.name) %><%- toPascalCase(attribute.key) %> { <% for (const [index, element] of Object.entries(attribute.elements)) { -%> - <%- strict ? toCamelCase(element) : element %><% if (index < attribute.elements.length - 1) { %>,<% } %> + <%- strict ? toCamelCase(element) : element %><% if (index < attribute.elements.length - 1) { -%>,<% } %> <% } -%> } <% } -%> <% } -%> class <%= toPascalCase(collection.name) %> { -<% for (const [index, attribute] of Object.entries(collection.attributes)) { -%> - <%- getType(attribute, collections) %> <%= strict ? toCamelCase(attribute.key) : attribute.key %>; +<% for (const [index, attribute] of Object.entries(__attrs)) { -%> + <%- getType(attribute, collections, collection.name) %> <%= strict ? toCamelCase(attribute.key) : attribute.key %>; <% } -%> <%= toPascalCase(collection.name) %>({ - <% for (const [index, attribute] of Object.entries(collection.attributes)) { -%> - <% if (attribute.required) { %>required <% } %>this.<%= strict ? toCamelCase(attribute.key) : attribute.key %><% if (index < collection.attributes.length - 1) { %>,<% } %> + <% for (const [index, attribute] of Object.entries(__attrs)) { -%> + <% if (attribute.required) { %>required <% } %>this.<%= strict ? toCamelCase(attribute.key) : attribute.key %><% if (index < __attrs.length - 1) { -%>,<% } %> <% } -%> }); factory <%= toPascalCase(collection.name) %>.fromMap(Map map) { return <%= toPascalCase(collection.name) %>( -<% for (const [index, attribute] of Object.entries(collection.attributes)) { -%> - <%= strict ? toCamelCase(attribute.key) : attribute.key %>: <% if (attribute.type === 'string' || attribute.type === 'email' || attribute.type === 'datetime') { -%> -<% if (attribute.format === 'enum') { -%> +<% for (const [index, attribute] of Object.entries(__attrs)) { -%> + <%= strict ? toCamelCase(attribute.key) : attribute.key %>: <% if (attribute.type === '${AttributeType.STRING}' || attribute.type === '${AttributeType.EMAIL}' || attribute.type === '${AttributeType.DATETIME}') { -%> +<% if (attribute.format === '${AttributeType.ENUM}') { -%> <% if (attribute.array) { -%> -(map['<%= attribute.key %>'] as List?)?.map((e) => <%- toPascalCase(attribute.key) %>.values.firstWhere((element) => element.name == e)).toList()<% if (!attribute.required) { %> ?? []<% } -%> -<% } else { -%> +(map['<%= attribute.key %>'] as List?)?.map((e) => <%- toPascalCase(collection.name) %><%- toPascalCase(attribute.key) %>.values.firstWhere((element) => element.name == e)).toList()<% } else { -%> <% if (!attribute.required) { -%> -map['<%= attribute.key %>'] != null ? <%- toPascalCase(attribute.key) %>.values.where((e) => e.name == map['<%= attribute.key %>']).firstOrNull : null<% } else { -%> -<%- toPascalCase(attribute.key) %>.values.firstWhere((e) => e.name == map['<%= attribute.key %>'])<% } -%> +map['<%= attribute.key %>'] != null ? <%- toPascalCase(collection.name) %><%- toPascalCase(attribute.key) %>.values.where((e) => e.name == map['<%= attribute.key %>']).firstOrNull : null<% } else { -%> +<%- toPascalCase(collection.name) %><%- toPascalCase(attribute.key) %>.values.firstWhere((e) => e.name == map['<%= attribute.key %>'])<% } -%> <% } -%> <% } else { -%> <% if (attribute.array) { -%> -List.from(map['<%= attribute.key %>'] ?? [])<% if (!attribute.required) { %> ?? []<% } -%> -<% } else { -%> -map['<%= attribute.key %>']<% if (!attribute.required) { %>?<% } %>.toString()<% if (!attribute.required) { %> ?? null<% } -%> -<% } -%> +List.from(map['<%= attribute.key %>'] ?? [])<% } else { -%> +map['<%= attribute.key %>']<% if (!attribute.required) { %>?<% } %>.toString()<% } -%> <% } -%> -<% } else if (attribute.type === 'integer') { -%> +<% } else if (attribute.type === '${AttributeType.INTEGER}') { -%> <% if (attribute.array) { -%> -List.from(map['<%= attribute.key %>'] ?? [])<% if (!attribute.required) { %> ?? []<% } -%> -<% } else { -%> -map['<%= attribute.key %>']<% if (!attribute.required) { %> ?? null<% } -%> -<% } -%> -<% } else if (attribute.type === 'float') { -%> +List.from(map['<%= attribute.key %>'] ?? [])<% } else { -%> +map['<%= attribute.key %>']<% } -%> +<% } else if (attribute.type === '${AttributeType.FLOAT}') { -%> <% if (attribute.array) { -%> -List.from(map['<%= attribute.key %>'] ?? [])<% if (!attribute.required) { %> ?? []<% } -%> -<% } else { -%> -map['<%= attribute.key %>']<% if (!attribute.required) { %> ?? null<% } -%> -<% } -%> -<% } else if (attribute.type === 'boolean') { -%> +List.from(map['<%= attribute.key %>'] ?? [])<% } else { -%> +map['<%= attribute.key %>']<% } -%> +<% } else if (attribute.type === '${AttributeType.BOOLEAN}') { -%> <% if (attribute.array) { -%> -List.from(map['<%= attribute.key %>'] ?? [])<% if (!attribute.required) { %> ?? []<% } -%> -<% } else { -%> -map['<%= attribute.key %>']<% if (!attribute.required) { %> ?? null<% } -%> -<% } -%> -<% } else if (attribute.type === 'relationship') { -%> +List.from(map['<%= attribute.key %>'] ?? [])<% } else { -%> +map['<%= attribute.key %>']<% } -%> +<% } else if (attribute.type === '${AttributeType.RELATIONSHIP}') { -%> <% if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { -%> -(map['<%= attribute.key %>'] as List?)?.map((e) => <%- toPascalCase(collections.find(c => c.$id === attribute.relatedCollection).name) %>.fromMap(e)).toList()<% if (!attribute.required) { %> ?? []<% } -%> +(map['<%= attribute.key %>'] as List?)?.map((e) => <%- toPascalCase(collections.find(c => c.$id === attribute.relatedCollection).name) %>.fromMap(e)).toList() <% } else { -%> <% if (!attribute.required) { -%> map['<%= attribute.key %>'] != null ? <%- toPascalCase(collections.find(c => c.$id === attribute.relatedCollection).name) %>.fromMap(map['<%= attribute.key %>']) : null<% } else { -%> <%- toPascalCase(collections.find(c => c.$id === attribute.relatedCollection).name) %>.fromMap(map['<%= attribute.key %>'])<% } -%> <% } -%> -<% } -%><% if (index < collection.attributes.length - 1) { %>,<% } %> +<% } -%><% if (index < __attrs.length - 1) { -%>,<% } %> <% } -%> ); } Map toMap() { return { -<% for (const [index, attribute] of Object.entries(collection.attributes)) { -%> - "<%= attribute.key %>": <% if (attribute.type === 'relationship') { -%> +<% for (const [index, attribute] of Object.entries(__attrs)) { -%> + '<%= attribute.key %>': <% if (attribute.type === '${AttributeType.RELATIONSHIP}') { -%> <% if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { -%> -<%= strict ? toCamelCase(attribute.key) : attribute.key %><% if (!attribute.required) { %>?<% } %>.map((e) => e.toMap()).toList()<% if (!attribute.required) { %> ?? []<% } -%> +<%= strict ? toCamelCase(attribute.key) : attribute.key %><% if (!attribute.required) { %>?<% } %>.map((e) => e.toMap()).toList() <% } else { -%> -<%= strict ? toCamelCase(attribute.key) : attribute.key %><% if (!attribute.required) { %>?<% } %>.toMap()<% if (!attribute.required) { %> ?? {}<% } -%> -<% } -%> -<% } else if (attribute.format === 'enum') { -%> +<%= strict ? toCamelCase(attribute.key) : attribute.key %><% if (!attribute.required) { %>?<% } %>.toMap()<% } -%> +<% } else if (attribute.format === '${AttributeType.ENUM}') { -%> <% if (attribute.array) { -%> -<%= strict ? toCamelCase(attribute.key) : attribute.key %><% if (!attribute.required) { %>?<% } %>.map((e) => e.name).toList()<% if (!attribute.required) { %> ?? []<% } -%> -<% } else { -%> -<%= strict ? toCamelCase(attribute.key) : attribute.key %><% if (!attribute.required) { %>?<% } %>.name<% if (!attribute.required) { %> ?? null<% } -%> -<% } -%> +<%= strict ? toCamelCase(attribute.key) : attribute.key %><% if (!attribute.required) { %>?<% } %>.map((e) => e.name).toList()<% } else { -%> +<%= strict ? toCamelCase(attribute.key) : attribute.key %><% if (!attribute.required) { %>?<% } %>.name<% } -%> <% } else { -%> <%= strict ? toCamelCase(attribute.key) : attribute.key -%> -<% } -%><% if (index < collection.attributes.length - 1) { %>,<% } %> +<% } -%><% if (index < __attrs.length - 1) { -%>,<% } %> <% } -%> }; } diff --git a/templates/cli/lib/type-generation/languages/java.js.twig b/templates/cli/lib/type-generation/languages/java.js.twig index 9ab86a82cd..e45f22e540 100644 --- a/templates/cli/lib/type-generation/languages/java.js.twig +++ b/templates/cli/lib/type-generation/languages/java.js.twig @@ -3,7 +3,7 @@ const { AttributeType } = require('../attribute'); const { LanguageMeta } = require("./language"); class Java extends LanguageMeta { - getType(attribute, collections) { + getType(attribute, collections, collectionName) { let type = ""; switch (attribute.type) { case AttributeType.STRING: @@ -11,7 +11,7 @@ class Java extends LanguageMeta { case AttributeType.DATETIME: type = "String"; if (attribute.format === AttributeType.ENUM) { - type = LanguageMeta.toPascalCase(attribute.key); + type = LanguageMeta.toPascalCase(collectionName) + LanguageMeta.toPascalCase(attribute.key); } break; case AttributeType.INTEGER: @@ -33,6 +33,15 @@ class Java extends LanguageMeta { type = "List<" + type + ">"; } break; + case AttributeType.POINT: + type = "List"; + break; + case AttributeType.LINESTRING: + type = "List>"; + break; + case AttributeType.POLYGON: + type = "List>>"; + break; default: throw new Error(`Unknown attribute type: ${attribute.type}`); } @@ -61,7 +70,7 @@ public class <%- toPascalCase(collection.name) %> { <% for (const attribute of collection.attributes) { -%> <% if (attribute.format === 'enum') { -%> - public enum <%- toPascalCase(attribute.key) %> { + public enum <%- toPascalCase(collection.name) %><%- toPascalCase(attribute.key) %> { <% for (const [index, element] of Object.entries(attribute.elements)) { -%> <%- strict ? toUpperSnakeCase(element) : element %><%- index < attribute.elements.length - 1 ? ',' : ';' %> <% } -%> @@ -70,7 +79,7 @@ public class <%- toPascalCase(collection.name) %> { <% } -%> <% } -%> <% for (const attribute of collection.attributes) { -%> - private <%- getType(attribute, collections) %> <%- strict ? toCamelCase(attribute.key) : attribute.key %>; + private <%- getType(attribute, collections, collection.name) %> <%- strict ? toCamelCase(attribute.key) : attribute.key %>; <% } -%> public <%- toPascalCase(collection.name) %>() { @@ -78,7 +87,7 @@ public class <%- toPascalCase(collection.name) %> { public <%- toPascalCase(collection.name) %>( <% for (const [index, attribute] of Object.entries(collection.attributes)) { -%> - <%- getType(attribute, collections) %> <%= strict ? toCamelCase(attribute.key) : attribute.key %><%- index < collection.attributes.length - 1 ? ',' : '' %> + <%- getType(attribute, collections, collection.name) %> <%= strict ? toCamelCase(attribute.key) : attribute.key %><%- index < collection.attributes.length - 1 ? ',' : '' %> <% } -%> ) { <% for (const attribute of collection.attributes) { -%> @@ -87,11 +96,11 @@ public class <%- toPascalCase(collection.name) %> { } <% for (const attribute of collection.attributes) { -%> - public <%- getType(attribute, collections) %> get<%- toPascalCase(attribute.key) %>() { + public <%- getType(attribute, collections, collection.name) %> get<%- toPascalCase(attribute.key) %>() { return <%= strict ? toCamelCase(attribute.key) : attribute.key %>; } - public void set<%- toPascalCase(attribute.key) %>(<%- getType(attribute, collections) %> <%= strict ? toCamelCase(attribute.key) : attribute.key %>) { + public void set<%- toPascalCase(attribute.key) %>(<%- getType(attribute, collections, collection.name) %> <%= strict ? toCamelCase(attribute.key) : attribute.key %>) { this.<%= strict ? toCamelCase(attribute.key) : attribute.key %> = <%= strict ? toCamelCase(attribute.key) : attribute.key %>; } diff --git a/templates/cli/lib/type-generation/languages/javascript.js.twig b/templates/cli/lib/type-generation/languages/javascript.js.twig index 5c40f2925d..1d19cfb44c 100644 --- a/templates/cli/lib/type-generation/languages/javascript.js.twig +++ b/templates/cli/lib/type-generation/languages/javascript.js.twig @@ -38,6 +38,15 @@ class JavaScript extends LanguageMeta { type = `${type}[]`; } break; + case AttributeType.POINT: + type = "number[]"; + break; + case AttributeType.LINESTRING: + type = "number[][]"; + break; + case AttributeType.POLYGON: + type = "number[][][]"; + break; default: throw new Error(`Unknown attribute type: ${attribute.type}`); } diff --git a/templates/cli/lib/type-generation/languages/kotlin.js.twig b/templates/cli/lib/type-generation/languages/kotlin.js.twig index 2c377174fa..8cecd74bac 100644 --- a/templates/cli/lib/type-generation/languages/kotlin.js.twig +++ b/templates/cli/lib/type-generation/languages/kotlin.js.twig @@ -3,7 +3,7 @@ const { AttributeType } = require('../attribute'); const { LanguageMeta } = require("./language"); class Kotlin extends LanguageMeta { - getType(attribute, collections) { + getType(attribute, collections, collectionName) { let type = ""; switch (attribute.type) { case AttributeType.STRING: @@ -11,7 +11,7 @@ class Kotlin extends LanguageMeta { case AttributeType.DATETIME: type = "String"; if (attribute.format === AttributeType.ENUM) { - type = LanguageMeta.toPascalCase(attribute.key); + type = LanguageMeta.toPascalCase(collectionName) + LanguageMeta.toPascalCase(attribute.key); } break; case AttributeType.INTEGER: @@ -33,6 +33,15 @@ class Kotlin extends LanguageMeta { type = `List<${type}>`; } break; + case AttributeType.POINT: + type = "List"; + break; + case AttributeType.LINESTRING: + type = "List>"; + break; + case AttributeType.POLYGON: + type = "List>>"; + break; default: throw new Error(`Unknown attribute type: ${attribute.type}`); } @@ -61,7 +70,7 @@ import <%- toPascalCase(collections.find(c => c.$id === attribute.relatedCollect <% for (const attribute of collection.attributes) { -%> <% if (attribute.format === 'enum') { -%> -enum class <%- toPascalCase(attribute.key) %> { +enum class <%- toPascalCase(collection.name) %><%- toPascalCase(attribute.key) %> { <% for (const [index, element] of Object.entries(attribute.elements)) { -%> <%- strict ? toUpperSnakeCase(element) : element %><%- index < attribute.elements.length - 1 ? ',' : '' %> <% } -%> @@ -71,7 +80,7 @@ enum class <%- toPascalCase(attribute.key) %> { <% } -%> data class <%- toPascalCase(collection.name) %>( <% for (const [index, attribute] of Object.entries(collection.attributes)) { -%> - val <%- strict ? toCamelCase(attribute.key) : attribute.key %>: <%- getType(attribute, collections) %><% if (index < collection.attributes.length - 1) { %>,<% } %> + val <%- strict ? toCamelCase(attribute.key) : attribute.key %>: <%- getType(attribute, collections, collection.name) %><% if (index < collection.attributes.length - 1) { %>,<% } %> <% } -%> ) `; diff --git a/templates/cli/lib/type-generation/languages/php.js.twig b/templates/cli/lib/type-generation/languages/php.js.twig index 942b585e31..d316796fa3 100644 --- a/templates/cli/lib/type-generation/languages/php.js.twig +++ b/templates/cli/lib/type-generation/languages/php.js.twig @@ -3,7 +3,7 @@ const { AttributeType } = require('../attribute'); const { LanguageMeta } = require("./language"); class PHP extends LanguageMeta { - getType(attribute, collections) { + getType(attribute, collections, collectionName) { if (attribute.array) { return "array"; } @@ -14,7 +14,7 @@ class PHP extends LanguageMeta { case AttributeType.DATETIME: type = "string"; if (attribute.format === AttributeType.ENUM) { - type = LanguageMeta.toPascalCase(attribute.key); + type = LanguageMeta.toPascalCase(collectionName) + LanguageMeta.toPascalCase(attribute.key); } break; case AttributeType.INTEGER: @@ -36,6 +36,11 @@ class PHP extends LanguageMeta { type = "array"; } break; + case AttributeType.POINT: + case AttributeType.LINESTRING: + case AttributeType.POLYGON: + type = "array"; + break; default: throw new Error(`Unknown attribute type: ${attribute.type}`); } @@ -60,9 +65,9 @@ use Appwrite\\Models\\<%- toPascalCase(collections.find(c => c.$id === attribute <% } -%> <% for (const attribute of collection.attributes) { -%> <% if (attribute.format === 'enum') { -%> -enum <%- toPascalCase(attribute.key) %>: string { +enum <%- toPascalCase(collection.name) %><%- toPascalCase(attribute.key) %>: string { <% for (const [index, element] of Object.entries(attribute.elements)) { -%> - case <%- strict ? toUpperSnakeCase(element) : element %> = '<%- element %>'; + case <%- toUpperSnakeCase(element) %> = '<%- element %>'; <% } -%> } @@ -70,15 +75,15 @@ enum <%- toPascalCase(attribute.key) %>: string { <% } -%> class <%- toPascalCase(collection.name) %> { <% for (const attribute of collection.attributes ){ -%> - private <%- getType(attribute, collections) %> $<%- strict ? toCamelCase(attribute.key) : attribute.key %>; + private <%- getType(attribute, collections, collection.name) %> $<%- strict ? toCamelCase(attribute.key) : attribute.key %>; <% } -%> public function __construct( <% for (const attribute of collection.attributes ){ -%> <% if (attribute.required) { -%> - <%- getType(attribute, collections).replace('|null', '') %> $<%- strict ? toCamelCase(attribute.key) : attribute.key %><% if (collection.attributes.indexOf(attribute) < collection.attributes.length - 1) { %>,<% } %> + <%- getType(attribute, collections, collection.name).replace('|null', '') %> $<%- strict ? toCamelCase(attribute.key) : attribute.key %><% if (collection.attributes.indexOf(attribute) < collection.attributes.length - 1) { %>,<% } %> <% } else { -%> - ?<%- getType(attribute, collections).replace('|null', '') %> $<%- strict ? toCamelCase(attribute.key) : attribute.key %> = null<% if (collection.attributes.indexOf(attribute) < collection.attributes.length - 1) { %>,<% } %> + ?<%- getType(attribute, collections, collection.name).replace('|null', '') %> $<%- strict ? toCamelCase(attribute.key) : attribute.key %> = null<% if (collection.attributes.indexOf(attribute) < collection.attributes.length - 1) { %>,<% } %> <% } -%> <% } -%> ) { @@ -88,11 +93,11 @@ class <%- toPascalCase(collection.name) %> { } <% for (const [index, attribute] of Object.entries(collection.attributes)) { -%> - public function get<%- toPascalCase(attribute.key) %>(): <%- getType(attribute, collections) %> { + public function get<%- toPascalCase(attribute.key) %>(): <%- getType(attribute, collections, collection.name) %> { return $this-><%- strict ? toCamelCase(attribute.key) : attribute.key %>; } - public function set<%- toPascalCase(attribute.key) %>(<%- getType(attribute, collections) %> $<%- strict ? toCamelCase(attribute.key) : attribute.key %>): void { + public function set<%- toPascalCase(attribute.key) %>(<%- getType(attribute, collections, collection.name) %> $<%- strict ? toCamelCase(attribute.key) : attribute.key %>): void { $this-><%- strict ? toCamelCase(attribute.key) : attribute.key %> = $<%- strict ? toCamelCase(attribute.key) : attribute.key %>; } <% if (index < collection.attributes.length - 1) { %> diff --git a/templates/cli/lib/type-generation/languages/swift.js.twig b/templates/cli/lib/type-generation/languages/swift.js.twig index 4322404ec1..8cb25748c8 100644 --- a/templates/cli/lib/type-generation/languages/swift.js.twig +++ b/templates/cli/lib/type-generation/languages/swift.js.twig @@ -3,7 +3,7 @@ const { AttributeType } = require('../attribute'); const { LanguageMeta } = require("./language"); class Swift extends LanguageMeta { - getType(attribute, collections) { + getType(attribute, collections, collectionName) { let type = ""; switch (attribute.type) { case AttributeType.STRING: @@ -11,7 +11,7 @@ class Swift extends LanguageMeta { case AttributeType.DATETIME: type = "String"; if (attribute.format === AttributeType.ENUM) { - type = LanguageMeta.toPascalCase(attribute.key); + type = LanguageMeta.toPascalCase(collectionName) + LanguageMeta.toPascalCase(attribute.key); } break; case AttributeType.INTEGER: @@ -33,6 +33,15 @@ class Swift extends LanguageMeta { type = `[${type}]`; } break; + case AttributeType.POINT: + type = "[Double]"; + break; + case AttributeType.LINESTRING: + type = "[[Double]]"; + break; + case AttributeType.POLYGON: + type = "[[[Double]]]"; + break; default: throw new Error(`Unknown attribute type: ${attribute.type}`); } @@ -53,7 +62,7 @@ class Swift extends LanguageMeta { <% for (const attribute of collection.attributes) { -%> <% if (attribute.format === 'enum') { -%> -public enum <%- toPascalCase(attribute.key) %>: String, Codable, CaseIterable { +public enum <%- toPascalCase(collection.name) %><%- toPascalCase(attribute.key) %>: String, Codable, CaseIterable { <% for (const [index, element] of Object.entries(attribute.elements)) { -%> case <%- strict ? toCamelCase(element) : element %> = "<%- element %>" <% } -%> @@ -63,7 +72,7 @@ public enum <%- toPascalCase(attribute.key) %>: String, Codable, CaseIterable { <% } -%> public class <%- toPascalCase(collection.name) %>: Codable { <% for (const attribute of collection.attributes) { -%> - public let <%- strict ? toCamelCase(attribute.key) : attribute.key %>: <%- getType(attribute, collections) %> + public let <%- strict ? toCamelCase(attribute.key) : attribute.key %>: <%- getType(attribute, collections, collection.name) %> <% } %> enum CodingKeys: String, CodingKey { <% for (const attribute of collection.attributes) { -%> @@ -73,7 +82,7 @@ public class <%- toPascalCase(collection.name) %>: Codable { public init( <% for (const [index, attribute] of Object.entries(collection.attributes)) { -%> - <%- strict ? toCamelCase(attribute.key) : attribute.key %>: <%- getType(attribute, collections) %><% if (index < collection.attributes.length - 1) { %>,<% } %> + <%- strict ? toCamelCase(attribute.key) : attribute.key %>: <%- getType(attribute, collections, collection.name) %><% if (index < collection.attributes.length - 1) { %>,<% } %> <% } -%> ) { <% for (const attribute of collection.attributes) { -%> @@ -86,9 +95,9 @@ public class <%- toPascalCase(collection.name) %>: Codable { <% for (const attribute of collection.attributes) { -%> <% if (!(!attribute.required && attribute.default === null)) { -%> - self.<%- strict ? toCamelCase(attribute.key) : attribute.key %> = try container.decode(<%- getType(attribute, collections).replace('?', '') %>.self, forKey: .<%- strict ? toCamelCase(attribute.key) : attribute.key %>) + self.<%- strict ? toCamelCase(attribute.key) : attribute.key %> = try container.decode(<%- getType(attribute, collections, collection.name).replace('?', '') %>.self, forKey: .<%- strict ? toCamelCase(attribute.key) : attribute.key %>) <% } else { -%> - self.<%- strict ? toCamelCase(attribute.key) : attribute.key %> = try container.decodeIfPresent(<%- getType(attribute, collections).replace('?', '') %>.self, forKey: .<%- strict ? toCamelCase(attribute.key) : attribute.key %>) + self.<%- strict ? toCamelCase(attribute.key) : attribute.key %> = try container.decodeIfPresent(<%- getType(attribute, collections, collection.name).replace('?', '') %>.self, forKey: .<%- strict ? toCamelCase(attribute.key) : attribute.key %>) <% } -%> <% } -%> } @@ -144,7 +153,7 @@ public class <%- toPascalCase(collection.name) %>: Codable { <% if ((attribute.type === 'string' || attribute.type === 'email' || attribute.type === 'datetime') && attribute.format !== 'enum') { -%> <%- strict ? toCamelCase(attribute.key) : attribute.key %>: map["<%- attribute.key %>"] as<% if (!attribute.required) { %>?<% } else { %>!<% } %> String<% if (index < collection.attributes.length - 1) { %>,<% } %> <% } else if (attribute.type === 'string' && attribute.format === 'enum') { -%> - <%- strict ? toCamelCase(attribute.key) : attribute.key %>: <%- toPascalCase(attribute.key) %>(rawValue: map["<%- attribute.key %>"] as! String)!<% if (index < collection.attributes.length - 1) { %>,<% } %> + <%- strict ? toCamelCase(attribute.key) : attribute.key %>: <%- toPascalCase(collection.name) %><%- toPascalCase(attribute.key) %>(rawValue: map["<%- attribute.key %>"] as! String)!<% if (index < collection.attributes.length - 1) { %>,<% } %> <% } else if (attribute.type === 'integer') { -%> <%- strict ? toCamelCase(attribute.key) : attribute.key %>: map["<%- attribute.key %>"] as<% if (!attribute.required) { %>?<% } else { %>!<% } %> Int<% if (index < collection.attributes.length - 1) { %>,<% } %> <% } else if (attribute.type === 'float') { -%> diff --git a/templates/cli/lib/type-generation/languages/typescript.js.twig b/templates/cli/lib/type-generation/languages/typescript.js.twig index d189d23e3f..d3fdd67b83 100644 --- a/templates/cli/lib/type-generation/languages/typescript.js.twig +++ b/templates/cli/lib/type-generation/languages/typescript.js.twig @@ -6,7 +6,7 @@ const { AttributeType } = require('../attribute'); const { LanguageMeta } = require("./language"); class TypeScript extends LanguageMeta { - getType(attribute, collections) { + getType(attribute, collections, collectionName) { let type = "" switch (attribute.type) { case AttributeType.STRING: @@ -16,7 +16,7 @@ class TypeScript extends LanguageMeta { case AttributeType.URL: type = "string"; if (attribute.format === AttributeType.ENUM) { - type = LanguageMeta.toPascalCase(attribute.key); + type = LanguageMeta.toPascalCase(collectionName) + LanguageMeta.toPascalCase(attribute.key); } break; case AttributeType.INTEGER: @@ -38,6 +38,15 @@ class TypeScript extends LanguageMeta { type = `${type}[]`; } break; + case AttributeType.POINT: + type = "Array"; + break; + case AttributeType.LINESTRING: + type = "Array>"; + break; + case AttributeType.POLYGON: + type = "Array>>"; + break; default: throw new Error(`Unknown attribute type: ${attribute.type}`); } @@ -69,7 +78,7 @@ class TypeScript extends LanguageMeta { } getTemplate() { - return `import { type Models } from '${this._getAppwriteDependency()}'; + return `import type { Models } from '${this._getAppwriteDependency()}'; // This file is auto-generated by the Appwrite CLI. // You can regenerate it by running \`appwrite ${process.argv.slice(2).join(' ')}\`. @@ -77,7 +86,7 @@ class TypeScript extends LanguageMeta { <% for (const collection of collections) { -%> <% for (const attribute of collection.attributes) { -%> <% if (attribute.format === 'enum') { -%> -export enum <%- toPascalCase(attribute.key) %> { +export enum <%- toPascalCase(collection.name) %><%- toPascalCase(attribute.key) %> { <% const entries = Object.entries(attribute.elements); -%> <% for (let i = 0; i < entries.length; i++) { -%> <%- toUpperSnakeCase(entries[i][1]) %> = "<%- entries[i][1] %>"<% if (i !== entries.length - 1) { %>,<% } %> @@ -92,7 +101,7 @@ export type <%- toPascalCase(collection.name) %> = Models.Row & { <% for (const attribute of collection.attributes) { -%> <% const propertyName = strict ? toCamelCase(attribute.key) : attribute.key; -%> <% const isValidIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(propertyName); -%> - <% if (isValidIdentifier) { %><%- propertyName %><% } else { %>"<%- propertyName %>"<% } %>: <%- getType(attribute, collections) %>; + <% if (isValidIdentifier) { %><%- propertyName %><% } else { %>"<%- propertyName %>"<% } %>: <%- getType(attribute, collections, collection.name) %>; <% } -%> }<% if (index < collections.length - 1) { %> <% } %> diff --git a/templates/cli/lib/utils.js.twig b/templates/cli/lib/utils.js.twig index 44098b1112..680c66134a 100644 --- a/templates/cli/lib/utils.js.twig +++ b/templates/cli/lib/utils.js.twig @@ -156,7 +156,6 @@ function getAccountPath(action) { function getDatabasePath(action, ids) { let path = '/databases'; - if (['get', 'listcollections', 'getcollection', 'listattributes', 'listdocuments', 'getdocument', 'listindexes', 'getdatabaseusage'].includes(action)) { path += `/database-${ids[0]}`; } @@ -179,7 +178,6 @@ function getDatabasePath(action, ids) { path += `/document-${ids[2]}`; } - return path; } diff --git a/templates/cli/package.json.twig b/templates/cli/package.json.twig index 3b307ce630..ffc01b8d28 100644 --- a/templates/cli/package.json.twig +++ b/templates/cli/package.json.twig @@ -39,7 +39,7 @@ "dotenv": "^16.4.5" }, "devDependencies": { - "pkg": "5.8.1" + "@yao-pkg/pkg": "^6.9.0" }, "pkg": { "scripts": [ diff --git a/templates/dart/.github/workflows/format.yml.twig b/templates/dart/.github/workflows/format.yml.twig index 3926b5ab01..567bb67c4b 100644 --- a/templates/dart/.github/workflows/format.yml.twig +++ b/templates/dart/.github/workflows/format.yml.twig @@ -20,6 +20,9 @@ jobs: persist-credentials: true ref: ${{ '{{'}} github.event.pull_request.head.ref {{ '}}' }} + - name: Install dependencies + run: dart pub get + - name: Format Dart code run: dart format . @@ -29,5 +32,5 @@ jobs: - name: Add & Commit uses: EndBug/add-and-commit@v9.1.4 with: - add: lib + add: '["lib", "test"]' diff --git a/templates/dart/README.md.twig b/templates/dart/README.md.twig index beeb8be9b0..9bbee2e89b 100644 --- a/templates/dart/README.md.twig +++ b/templates/dart/README.md.twig @@ -17,8 +17,6 @@ {{ sdk.description }} - - {% if sdk.logo %} ![{{ spec.title }}]({{ sdk.logo }}) {% endif %} diff --git a/templates/dart/base/requests/oauth.twig b/templates/dart/base/requests/oauth.twig index 6a1b627194..7a7a3acf6d 100644 --- a/templates/dart/base/requests/oauth.twig +++ b/templates/dart/base/requests/oauth.twig @@ -16,10 +16,11 @@ params.forEach((key, value) { if (value is List) { for (var item in value) { - query.add(Uri.encodeComponent(key + '[]') + '=' + Uri.encodeComponent(item)); + query.add( + '${Uri.encodeComponent('$key[]')}=${Uri.encodeComponent(item)}'); } } else if (value != null) { - query.add(Uri.encodeComponent(key) + '=' + Uri.encodeComponent(value)); + query.add('${Uri.encodeComponent(key)}=${Uri.encodeComponent(value)}'); } }); diff --git a/templates/dart/base/utils.twig b/templates/dart/base/utils.twig index df33f1a330..876f92859d 100644 --- a/templates/dart/base/utils.twig +++ b/templates/dart/base/utils.twig @@ -1,6 +1,10 @@ {% macro map_parameter(parameters) %} {% for parameter in parameters %} +{% if not parameter.nullable and not parameter.required %} +if ({{ parameter.name | caseCamel | overrideIdentifier }} != null) '{{ parameter.name }}': {{ parameter.name | caseCamel | overrideIdentifier }}{% if parameter.enumValues | length > 0 %}!.value{% endif %}, +{% else %} '{{ parameter.name }}': {{ parameter.name | caseCamel | overrideIdentifier }}{% if parameter.enumValues | length > 0 %}{% if not parameter.required %}?{% endif %}.value{% endif %}, +{% endif %} {% endfor %} {% endmacro %} diff --git a/templates/dart/docs/example.md.twig b/templates/dart/docs/example.md.twig index 8cab9fbda6..a406238ac7 100644 --- a/templates/dart/docs/example.md.twig +++ b/templates/dart/docs/example.md.twig @@ -2,6 +2,10 @@ import 'dart:io'; {% endif %} import 'package:{{ language.params.packageName }}/{{ language.params.packageName }}.dart'; +{% if method.parameters.all | hasPermissionParam %} +import 'package:{{ language.params.packageName }}/permission.dart'; +import 'package:{{ language.params.packageName }}/role.dart'; +{% endif %} Client client = Client() {%~ if method.auth|length > 0 %} @@ -15,10 +19,10 @@ Client client = Client() {{ service.name | caseUcfirst }} {{ service.name | caseCamel }} = {{service.name | caseUcfirst}}(client); -{% if method.method != 'delete' and method.type != 'webAuth' %}{% if method.type == 'location' %}UInt8List{% else %}{{ method.responseModel | caseUcfirst | overrideIdentifier }}{% endif %} result = {% endif %}await {{ service.name | caseCamel }}.{{ method.name | caseCamel }}({% if method.parameters.all | length == 0 %});{% endif %} +{% if method.method != 'delete' and method.type != 'webAuth' %}{% if method.type == 'location' %}Uint8List{% else %}{{ method.responseModel | caseUcfirst | overrideIdentifier }}{% endif %} result = {% endif %}await {{ service.name | caseCamel }}.{{ method.name | caseCamel }}({% if method.parameters.all | length == 0 %});{% endif %} {%~ for parameter in method.parameters.all %} - {{ parameter.name | caseCamel | overrideIdentifier }}: {% if parameter.enumValues | length > 0 %}{{ parameter.enumName }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseCamel | replace({'-': ''}) }}{% else %}{{ parameter | paramExample | replace({'': (parameter.name | caseCamel) }) | raw }}{% endif %},{% if not parameter.required %} // (optional){% endif %} + {{ parameter.name | caseCamel | overrideIdentifier }}: {% if parameter.enumValues | length > 0 %}{{ parameter.enumName | caseUcfirst | overrideIdentifier }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample | replace({'': (parameter.name | caseCamel) }) | raw }}{% endif %},{% if not parameter.required %} // (optional){% endif %} {%~ endfor %} {% if method.parameters.all | length > 0 %}); diff --git a/templates/dart/lib/enums.dart.twig b/templates/dart/lib/enums.dart.twig index 76589d06df..3aa676d8d7 100644 --- a/templates/dart/lib/enums.dart.twig +++ b/templates/dart/lib/enums.dart.twig @@ -1,6 +1,6 @@ /// {{spec.title | caseUcfirst}} Enums library {{ language.params.packageName }}.enums; -{% for enum in spec.enums %} +{% for enum in spec.allEnums %} part 'src/enums/{{enum.name | caseSnake}}.dart'; {% endfor %} \ No newline at end of file diff --git a/templates/dart/lib/models.dart.twig b/templates/dart/lib/models.dart.twig index 1a15137f23..0d1e087d0c 100644 --- a/templates/dart/lib/models.dart.twig +++ b/templates/dart/lib/models.dart.twig @@ -1,6 +1,10 @@ /// {{spec.title | caseUcfirst}} Models library {{ language.params.packageName }}.models; +{% if (spec.requestEnums | length) > 0 or (spec.responseEnums | length) > 0 %} +import 'enums.dart' as enums; +{% endif %} + part 'src/models/model.dart'; {% for definition in spec.definitions %} part 'src/models/{{definition.name | caseSnake}}.dart'; diff --git a/templates/dart/lib/operator.dart.twig b/templates/dart/lib/operator.dart.twig new file mode 100644 index 0000000000..981c02feb6 --- /dev/null +++ b/templates/dart/lib/operator.dart.twig @@ -0,0 +1,190 @@ +part of '{{ language.params.packageName }}.dart'; + +/// Filter condition for array operations +enum Condition { + equal('equal'), + notEqual('notEqual'), + greaterThan('greaterThan'), + greaterThanEqual('greaterThanEqual'), + lessThan('lessThan'), + lessThanEqual('lessThanEqual'), + contains('contains'), + isNull('isNull'), + isNotNull('isNotNull'); + + final String value; + const Condition(this.value); + + @override + String toString() => value; +} + +/// Helper class to generate operator strings for atomic operations. +class Operator { + final String method; + final dynamic values; + + Operator._(this.method, [this.values = null]); + + Map toJson() { + final result = {}; + + result['method'] = method; + + if(values != null) { + result['values'] = values is List ? values : [values]; + } + + return result; + } + + @override + String toString() => jsonEncode(toJson()); + + /// Increment a numeric attribute by a specified value. + static String increment([num value = 1, num? max]) { + if (value.toDouble().isNaN || value.toDouble().isInfinite) { + throw ArgumentError('Value cannot be NaN or Infinity'); + } + if (max != null && (max.toDouble().isNaN || max.toDouble().isInfinite)) { + throw ArgumentError('Max cannot be NaN or Infinity'); + } + final values = [value]; + if (max != null) { + values.add(max); + } + return Operator._('increment', values).toString(); + } + + /// Decrement a numeric attribute by a specified value. + static String decrement([num value = 1, num? min]) { + if (value.toDouble().isNaN || value.toDouble().isInfinite) { + throw ArgumentError('Value cannot be NaN or Infinity'); + } + if (min != null && (min.toDouble().isNaN || min.toDouble().isInfinite)) { + throw ArgumentError('Min cannot be NaN or Infinity'); + } + final values = [value]; + if (min != null) { + values.add(min); + } + return Operator._('decrement', values).toString(); + } + + /// Multiply a numeric attribute by a specified factor. + static String multiply(num factor, [num? max]) { + if (factor.toDouble().isNaN || factor.toDouble().isInfinite) { + throw ArgumentError('Factor cannot be NaN or Infinity'); + } + if (max != null && (max.toDouble().isNaN || max.toDouble().isInfinite)) { + throw ArgumentError('Max cannot be NaN or Infinity'); + } + final values = [factor]; + if (max != null) { + values.add(max); + } + return Operator._('multiply', values).toString(); + } + + /// Divide a numeric attribute by a specified divisor. + static String divide(num divisor, [num? min]) { + if (divisor.toDouble().isNaN || divisor.toDouble().isInfinite) { + throw ArgumentError('Divisor cannot be NaN or Infinity'); + } + if (min != null && (min.toDouble().isNaN || min.toDouble().isInfinite)) { + throw ArgumentError('Min cannot be NaN or Infinity'); + } + if (divisor == 0) { + throw ArgumentError('Divisor cannot be zero'); + } + final values = [divisor]; + if (min != null) { + values.add(min); + } + return Operator._('divide', values).toString(); + } + + /// Apply modulo operation on a numeric attribute. + static String modulo(num divisor) { + if (divisor.toDouble().isNaN || divisor.toDouble().isInfinite) { + throw ArgumentError('Divisor cannot be NaN or Infinity'); + } + if (divisor == 0) { + throw ArgumentError('Divisor cannot be zero'); + } + return Operator._('modulo', [divisor]).toString(); + } + + /// Raise a numeric attribute to a specified power. + static String power(num exponent, [num? max]) { + if (exponent.toDouble().isNaN || exponent.toDouble().isInfinite) { + throw ArgumentError('Exponent cannot be NaN or Infinity'); + } + if (max != null && (max.toDouble().isNaN || max.toDouble().isInfinite)) { + throw ArgumentError('Max cannot be NaN or Infinity'); + } + final values = [exponent]; + if (max != null) { + values.add(max); + } + return Operator._('power', values).toString(); + } + + /// Append values to an array attribute. + static String arrayAppend(List values) => + Operator._('arrayAppend', values).toString(); + + /// Prepend values to an array attribute. + static String arrayPrepend(List values) => + Operator._('arrayPrepend', values).toString(); + + /// Insert a value at a specific index in an array attribute. + static String arrayInsert(int index, dynamic value) => + Operator._('arrayInsert', [index, value]).toString(); + + /// Remove a value from an array attribute. + static String arrayRemove(dynamic value) => + Operator._('arrayRemove', [value]).toString(); + + /// Remove duplicate values from an array attribute. + static String arrayUnique() => + Operator._('arrayUnique', []).toString(); + + /// Keep only values that exist in both the current array and the provided array. + static String arrayIntersect(List values) => + Operator._('arrayIntersect', values).toString(); + + /// Remove values from the array that exist in the provided array. + static String arrayDiff(List values) => + Operator._('arrayDiff', values).toString(); + + /// Filter array values based on a condition. + static String arrayFilter(Condition condition, [dynamic value]) { + final values = [condition.value, value]; + return Operator._('arrayFilter', values).toString(); + } + + /// Concatenate a value to a string or array attribute. + static String stringConcat(dynamic value) => + Operator._('stringConcat', [value]).toString(); + + /// Replace occurrences of a search string with a replacement string. + static String stringReplace(String search, String replace) => + Operator._('stringReplace', [search, replace]).toString(); + + /// Toggle a boolean attribute. + static String toggle() => + Operator._('toggle', []).toString(); + + /// Add days to a date attribute. + static String dateAddDays(int days) => + Operator._('dateAddDays', [days]).toString(); + + /// Subtract days from a date attribute. + static String dateSubDays(int days) => + Operator._('dateSubDays', [days]).toString(); + + /// Set a date attribute to the current date and time. + static String dateSetNow() => + Operator._('dateSetNow', []).toString(); +} diff --git a/templates/dart/lib/package.dart.twig b/templates/dart/lib/package.dart.twig index 429301aac3..bca6862a36 100644 --- a/templates/dart/lib/package.dart.twig +++ b/templates/dart/lib/package.dart.twig @@ -27,6 +27,7 @@ part 'query.dart'; part 'permission.dart'; part 'role.dart'; part 'id.dart'; +part 'operator.dart'; {% for service in spec.services %} part 'services/{{service.name | caseSnake}}.dart'; {% endfor %} diff --git a/templates/dart/lib/query.dart.twig b/templates/dart/lib/query.dart.twig index 013129442e..6a55e166d5 100644 --- a/templates/dart/lib/query.dart.twig +++ b/templates/dart/lib/query.dart.twig @@ -6,7 +6,7 @@ class Query { final String? attribute; final dynamic values; - Query._(this.method, [this.attribute = null, this.values = null]); + Query._(this.method, [this.attribute, this.values]); Map toJson() { final result = {}; @@ -107,27 +107,27 @@ class Query { /// Filter resources where document was created before [value]. static String createdBefore(String value) => - Query._('createdBefore', null, value).toString(); + lessThan('\$createdAt', value); /// Filter resources where document was created after [value]. static String createdAfter(String value) => - Query._('createdAfter', null, value).toString(); + greaterThan('\$createdAt', value); /// Filter resources where document was created between [start] and [end] (inclusive). static String createdBetween(String start, String end) => - Query._('createdBetween', null, [start, end]).toString(); + between('\$createdAt', start, end); /// Filter resources where document was updated before [value]. static String updatedBefore(String value) => - Query._('updatedBefore', null, value).toString(); + lessThan('\$updatedAt', value); /// Filter resources where document was updated after [value]. static String updatedAfter(String value) => - Query._('updatedAfter', null, value).toString(); + greaterThan('\$updatedAt', value); /// Filter resources where document was updated between [start] and [end] (inclusive). static String updatedBetween(String start, String end) => - Query._('updatedBetween', null, [start, end]).toString(); + between('\$updatedAt', start, end); static String or(List queries) => Query._( 'or', @@ -153,6 +153,10 @@ class Query { static String orderDesc(String attribute) => Query._('orderDesc', attribute).toString(); + /// Sort results randomly. + static String orderRandom() => + Query._('orderRandom').toString(); + /// Return results before [id]. /// /// Refer to the [Cursor Based Pagination]({{sdk.url}}/docs/pagination#cursor-pagination) diff --git a/templates/dart/lib/src/client.dart.twig b/templates/dart/lib/src/client.dart.twig index fb30c5bdfd..45381a8b53 100644 --- a/templates/dart/lib/src/client.dart.twig +++ b/templates/dart/lib/src/client.dart.twig @@ -7,8 +7,8 @@ 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; + /// The size for chunked uploads in bytes. + static const int chunkSize = 5 * 1024 * 1024; /// Holds configuration such as project. late Map config; @@ -42,7 +42,7 @@ abstract class Client { /// /// {{header.description}} {% endif %} - Client set{{header.key | caseUcfirst}}(value); + Client set{{header.key | caseUcfirst}}(String value); {% endfor %} /// Add headers that should be sent with all API calls. diff --git a/templates/dart/lib/src/client_browser.dart.twig b/templates/dart/lib/src/client_browser.dart.twig index 05e646ce0e..29a0dafd71 100644 --- a/templates/dart/lib/src/client_browser.dart.twig +++ b/templates/dart/lib/src/client_browser.dart.twig @@ -16,7 +16,7 @@ ClientBase createClient({ ClientBrowser(endPoint: endPoint, selfSigned: selfSigned); class ClientBrowser extends ClientBase with ClientMixin { - static const int CHUNK_SIZE = 5*1024*1024; + static const int chunkSize = 5*1024*1024; String _endPoint; Map? _headers; @override @@ -106,7 +106,7 @@ class ClientBrowser extends ClientBase with ClientMixin { int size = file.bytes!.length; late Response res; - if (size <= CHUNK_SIZE) { + if (size <= chunkSize) { params[paramName] = http.MultipartFile.fromBytes(paramName, file.bytes!, filename: file.filename); return call( HttpMethod.post, @@ -122,25 +122,25 @@ class ClientBrowser extends ClientBase with ClientMixin { try { res = await call( HttpMethod.get, - path: path + '/' + params[idParamName], + path: '$path/${params[idParamName]}', headers: headers, ); final int chunksUploaded = res.data['chunksUploaded'] as int; - offset = chunksUploaded * CHUNK_SIZE; + offset = chunksUploaded * chunkSize; } on {{spec.title | caseUcfirst}}Exception catch (_) {} } while (offset < size) { List chunk = []; - final end = min(offset + CHUNK_SIZE, size); + final end = min(offset + chunkSize, size); chunk = file.bytes!.getRange(offset, end).toList(); params[paramName] = http.MultipartFile.fromBytes(paramName, chunk, filename: file.filename); headers['content-range'] = - 'bytes $offset-${min((offset + CHUNK_SIZE - 1), size - 1)}/$size'; + 'bytes $offset-${min((offset + chunkSize - 1), size - 1)}/$size'; res = await call(HttpMethod.post, path: path, headers: headers, params: params); - offset += CHUNK_SIZE; + offset += chunkSize; if (offset < size) { headers['x-{{spec.title | caseLower }}-id'] = res.data['\$id']; } diff --git a/templates/dart/lib/src/client_io.dart.twig b/templates/dart/lib/src/client_io.dart.twig index 952e361f59..3afe387f43 100644 --- a/templates/dart/lib/src/client_io.dart.twig +++ b/templates/dart/lib/src/client_io.dart.twig @@ -20,7 +20,7 @@ ClientBase createClient({ ); class ClientIO extends ClientBase with ClientMixin { - static const int CHUNK_SIZE = 5*1024*1024; + static const int chunkSize = 5*1024*1024; String _endPoint; Map? _headers; @override @@ -120,7 +120,7 @@ class ClientIO extends ClientBase with ClientMixin { } late Response res; - if (size <= CHUNK_SIZE) { + if (size <= chunkSize) { if (file.path != null) { params[paramName] = await http.MultipartFile.fromPath( paramName, file.path!, @@ -143,11 +143,11 @@ class ClientIO extends ClientBase with ClientMixin { try { res = await call( HttpMethod.get, - path: path + '/' + params[idParamName], + path: '$path/${params[idParamName]}', headers: headers, ); final int chunksUploaded = res.data['chunksUploaded'] as int; - offset = chunksUploaded * CHUNK_SIZE; + offset = chunksUploaded * chunkSize; } on {{spec.title | caseUcfirst}}Exception catch (_) {} } @@ -160,19 +160,19 @@ class ClientIO extends ClientBase with ClientMixin { while (offset < size) { List chunk = []; if (file.bytes != null) { - final end = min(offset + CHUNK_SIZE, size); + final end = min(offset + chunkSize, size); chunk = file.bytes!.getRange(offset, end).toList(); } else { raf!.setPositionSync(offset); - chunk = raf.readSync(CHUNK_SIZE); + chunk = raf.readSync(chunkSize); } params[paramName] = http.MultipartFile.fromBytes(paramName, chunk, filename: file.filename); headers['content-range'] = - 'bytes $offset-${min((offset + CHUNK_SIZE - 1), size - 1)}/$size'; + 'bytes $offset-${min((offset + chunkSize - 1), size - 1)}/$size'; res = await call(HttpMethod.post, path: path, headers: headers, params: params); - offset += CHUNK_SIZE; + offset += chunkSize; if (offset < size) { headers['x-{{spec.title | caseLower }}-id'] = res.data['\$id']; } diff --git a/templates/dart/lib/src/client_mixin.dart.twig b/templates/dart/lib/src/client_mixin.dart.twig index 7353b03f49..d2b5569982 100644 --- a/templates/dart/lib/src/client_mixin.dart.twig +++ b/templates/dart/lib/src/client_mixin.dart.twig @@ -12,23 +12,25 @@ mixin ClientMixin { required Map headers, required Map params, }) { - if (params.isNotEmpty) { - params.removeWhere((key, value) => value == null); - } http.BaseRequest request = http.Request(method.name(), uri); if (headers['content-type'] == 'multipart/form-data') { request = http.MultipartRequest(method.name(), uri); if (params.isNotEmpty) { params.forEach((key, value) { + if (value == null) { + return; + } if (value is http.MultipartFile) { (request as http.MultipartRequest).files.add(value); } else { if (value is List) { value.asMap().forEach((i, v) { - (request as http.MultipartRequest) - .fields - .addAll({"$key[$i]": v.toString()}); + if (v != null) { + (request as http.MultipartRequest) + .fields + .addAll({"$key[$i]": v.toString()}); + } }); } else { (request as http.MultipartRequest) @@ -40,15 +42,19 @@ mixin ClientMixin { } } else if (method == HttpMethod.get) { if (params.isNotEmpty) { - params = params.map((key, value){ - if (value is int || value is double) { - return MapEntry(key, value.toString()); - } - if (value is List) { - return MapEntry(key + "[]", value); + Map filteredParams = {}; + params.forEach((key, value) { + if (value != null) { + if (value is int || value is double) { + filteredParams[key] = value.toString(); + } else if (value is List) { + filteredParams["$key[]"] = value; + } else { + filteredParams[key] = value; + } } - return MapEntry(key, value); }); + params = filteredParams; } uri = Uri( fragment: uri.fragment, diff --git a/templates/dart/lib/src/models/model.dart.twig b/templates/dart/lib/src/models/model.dart.twig index f27126ff37..6ee3db0310 100644 --- a/templates/dart/lib/src/models/model.dart.twig +++ b/templates/dart/lib/src/models/model.dart.twig @@ -32,6 +32,13 @@ class {{ definition.name | caseUcfirst | overrideIdentifier }} implements Model {%- else -%} {{property.sub_schema | caseUcfirst | overrideIdentifier}}.fromMap(map['{{property.name | escapeDollarSign }}']) {%- endif -%} + {%- elseif property.enum -%} + {%- set enumName = property['enumName'] ?? property.name -%} + {%- if property.required -%} + enums.{{ enumName | caseUcfirst }}.values.firstWhere((e) => e.value == map['{{property.name | escapeDollarSign }}']) + {%- else -%} + map['{{property.name | escapeDollarSign }}'] != null ? enums.{{ enumName | caseUcfirst }}.values.firstWhere((e) => e.value == map['{{property.name | escapeDollarSign }}']) : null + {%- endif -%} {%- else -%} {%- if property.type == 'array' -%} List.from(map['{{property.name | escapeDollarSign }}'] ?? []) @@ -47,15 +54,16 @@ class {{ definition.name | caseUcfirst | overrideIdentifier }} implements Model {%- endif -%}, {% endfor %} {% if definition.additionalProperties %} - data: map, + data: map["data"] ?? map, {% endif %} ); } + @override Map toMap() { return { {% for property in definition.properties %} - "{{ property.name | escapeDollarSign }}": {% if property.sub_schema %}{% if property.type == 'array' %}{{property.name | escapeKeyword}}.map((p) => p.toMap()).toList(){% else %}{{property.name | escapeKeyword}}.toMap(){% endif %}{% else %}{{property.name | escapeKeyword }}{% endif %}, + "{{ property.name | escapeDollarSign }}": {% if property.sub_schema %}{% if property.type == 'array' %}{{property.name | escapeKeyword}}.map((p) => p.toMap()).toList(){% else %}{{property.name | escapeKeyword}}.toMap(){% endif %}{% elseif property.enum %}{{property.name | escapeKeyword}}{% if not property.required %}?{% endif %}.value{% else %}{{property.name | escapeKeyword }}{% endif %}, {% endfor %} {% if definition.additionalProperties %} "data": data, diff --git a/templates/dart/test/operator_test.dart.twig b/templates/dart/test/operator_test.dart.twig new file mode 100644 index 0000000000..06380d4b20 --- /dev/null +++ b/templates/dart/test/operator_test.dart.twig @@ -0,0 +1,160 @@ +import 'dart:convert'; + +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() { + test('returns increment', () { + final op = jsonDecode(Operator.increment(1)); + expect(op['method'], 'increment'); + expect(op['values'], [1]); + }); + + test('returns increment with max', () { + final op = jsonDecode(Operator.increment(5, 100)); + expect(op['method'], 'increment'); + expect(op['values'], [5, 100]); + }); + + test('returns decrement', () { + final op = jsonDecode(Operator.decrement(1)); + expect(op['method'], 'decrement'); + expect(op['values'], [1]); + }); + + test('returns decrement with min', () { + final op = jsonDecode(Operator.decrement(3, 0)); + expect(op['method'], 'decrement'); + expect(op['values'], [3, 0]); + }); + + test('returns multiply', () { + final op = jsonDecode(Operator.multiply(2)); + expect(op['method'], 'multiply'); + expect(op['values'], [2]); + }); + + test('returns multiply with max', () { + final op = jsonDecode(Operator.multiply(3, 1000)); + expect(op['method'], 'multiply'); + expect(op['values'], [3, 1000]); + }); + + test('returns divide', () { + final op = jsonDecode(Operator.divide(2)); + expect(op['method'], 'divide'); + expect(op['values'], [2]); + }); + + test('returns divide with min', () { + final op = jsonDecode(Operator.divide(4, 1)); + expect(op['method'], 'divide'); + expect(op['values'], [4, 1]); + }); + + test('returns modulo', () { + final op = jsonDecode(Operator.modulo(5)); + expect(op['method'], 'modulo'); + expect(op['values'], [5]); + }); + + test('returns power', () { + final op = jsonDecode(Operator.power(2)); + expect(op['method'], 'power'); + expect(op['values'], [2]); + }); + + test('returns power with max', () { + final op = jsonDecode(Operator.power(3, 100)); + expect(op['method'], 'power'); + expect(op['values'], [3, 100]); + }); + + test('returns arrayAppend', () { + final op = jsonDecode(Operator.arrayAppend(['item1', 'item2'])); + expect(op['method'], 'arrayAppend'); + expect(op['values'], ['item1', 'item2']); + }); + + test('returns arrayPrepend', () { + final op = jsonDecode(Operator.arrayPrepend(['first', 'second'])); + expect(op['method'], 'arrayPrepend'); + expect(op['values'], ['first', 'second']); + }); + + test('returns arrayInsert', () { + final op = jsonDecode(Operator.arrayInsert(0, 'newItem')); + expect(op['method'], 'arrayInsert'); + expect(op['values'], [0, 'newItem']); + }); + + test('returns arrayRemove', () { + final op = jsonDecode(Operator.arrayRemove('oldItem')); + expect(op['method'], 'arrayRemove'); + expect(op['values'], ['oldItem']); + }); + + test('returns arrayUnique', () { + final op = jsonDecode(Operator.arrayUnique()); + expect(op['method'], 'arrayUnique'); + expect(op['values'], []); + }); + + test('returns arrayIntersect', () { + final op = jsonDecode(Operator.arrayIntersect(['a', 'b', 'c'])); + expect(op['method'], 'arrayIntersect'); + expect(op['values'], ['a', 'b', 'c']); + }); + + test('returns arrayDiff', () { + final op = jsonDecode(Operator.arrayDiff(['x', 'y'])); + expect(op['method'], 'arrayDiff'); + expect(op['values'], ['x', 'y']); + }); + + test('returns arrayFilter', () { + final op = jsonDecode(Operator.arrayFilter(Condition.equal, 'test')); + expect(op['method'], 'arrayFilter'); + expect(op['values'], ['equal', 'test']); + }); + + test('returns stringConcat', () { + final op = jsonDecode(Operator.stringConcat('suffix')); + expect(op['method'], 'stringConcat'); + expect(op['values'], ['suffix']); + }); + + test('returns stringReplace', () { + final op = jsonDecode(Operator.stringReplace('old', 'new')); + expect(op['method'], 'stringReplace'); + expect(op['values'], ['old', 'new']); + }); + + test('returns toggle', () { + final op = jsonDecode(Operator.toggle()); + expect(op['method'], 'toggle'); + expect(op['values'], []); + }); + + test('returns dateAddDays', () { + final op = jsonDecode(Operator.dateAddDays(7)); + expect(op['method'], 'dateAddDays'); + expect(op['values'], [7]); + }); + + test('returns dateSubDays', () { + final op = jsonDecode(Operator.dateSubDays(3)); + expect(op['method'], 'dateSubDays'); + expect(op['values'], [3]); + }); + + test('returns dateSetNow', () { + final op = jsonDecode(Operator.dateSetNow()); + expect(op['method'], 'dateSetNow'); + expect(op['values'], []); + }); +} diff --git a/templates/dart/test/query_test.dart.twig b/templates/dart/test/query_test.dart.twig index ba804b9362..797c045bb9 100644 --- a/templates/dart/test/query_test.dart.twig +++ b/templates/dart/test/query_test.dart.twig @@ -190,6 +190,13 @@ void main() { expect(query['method'], 'orderDesc'); }); + test('returns orderRandom', () { + final query = jsonDecode(Query.orderRandom()); + expect(query['attribute'], null); + expect(query['values'], null); + expect(query['method'], 'orderRandom'); + }); + test('returns cursorBefore', () { final query = jsonDecode(Query.cursorBefore('custom')); expect(query['attribute'], null); @@ -271,44 +278,44 @@ void main() { test('returns createdBefore', () { final query = jsonDecode(Query.createdBefore('2023-01-01')); - expect(query['attribute'], null); + expect(query['attribute'], '\$createdAt'); expect(query['values'], ['2023-01-01']); - expect(query['method'], 'createdBefore'); + expect(query['method'], 'lessThan'); }); test('returns createdAfter', () { final query = jsonDecode(Query.createdAfter('2023-01-01')); - expect(query['attribute'], null); + expect(query['attribute'], '\$createdAt'); expect(query['values'], ['2023-01-01']); - expect(query['method'], 'createdAfter'); + expect(query['method'], 'greaterThan'); }); test('returns createdBetween', () { final query = jsonDecode(Query.createdBetween('2023-01-01', '2023-12-31')); - expect(query['attribute'], null); + expect(query['attribute'], '\$createdAt'); expect(query['values'], ['2023-01-01', '2023-12-31']); - expect(query['method'], 'createdBetween'); + expect(query['method'], 'between'); }); test('returns updatedBefore', () { final query = jsonDecode(Query.updatedBefore('2023-01-01')); - expect(query['attribute'], null); + expect(query['attribute'], '\$updatedAt'); expect(query['values'], ['2023-01-01']); - expect(query['method'], 'updatedBefore'); + expect(query['method'], 'lessThan'); }); test('returns updatedAfter', () { final query = jsonDecode(Query.updatedAfter('2023-01-01')); - expect(query['attribute'], null); + expect(query['attribute'], '\$updatedAt'); expect(query['values'], ['2023-01-01']); - expect(query['method'], 'updatedAfter'); + expect(query['method'], 'greaterThan'); }); test('returns updatedBetween', () { final query = jsonDecode(Query.updatedBetween('2023-01-01', '2023-12-31')); - expect(query['attribute'], null); + expect(query['attribute'], '\$updatedAt'); expect(query['values'], ['2023-01-01', '2023-12-31']); - expect(query['method'], 'updatedBetween'); + expect(query['method'], 'between'); }); } diff --git a/templates/dart/test/services/service_test.dart.twig b/templates/dart/test/services/service_test.dart.twig index c61fe4a4d0..32f3c09c03 100644 --- a/templates/dart/test/services/service_test.dart.twig +++ b/templates/dart/test/services/service_test.dart.twig @@ -1,6 +1,6 @@ {% macro sub_schema(definitions, property) %}{% if property.sub_schema %}{% if property.type == 'array' %}List<>{% else %}{ {% if definitions[property.sub_schema] %}{% for property in definitions[property.sub_schema].properties | filter(p => p.required) %} - '{{property.name | escapeDollarSign}}': {% if property.type == 'object' %}{% if property.sub_schema and (property.sub_schema != 'prefs' and property.sub_schema != 'preferences') %}{{_self.sub_schema(spec.definitions, property)}}{% else %}{}{% endif %}{% elseif property.type == 'array' %}[]{% elseif property.type == 'string' %}'{{property.example | escapeDollarSign}}'{% elseif property.type == 'boolean' %}true{% else %}{{property.example}}{% endif %}, + '{{property.name | escapeDollarSign}}': {% if property.type == 'object' %}{% if property.sub_schema and (property.sub_schema != 'prefs' and property.sub_schema != 'preferences') %}{{_self.sub_schema(spec.definitions, property)}}{% else %}{}{% endif %}{% elseif property.type == 'array' %}[]{% elseif property.type == 'string' %}{% if property.enum %}'{{property.enum[0]}}'{% else %}'{{property.example | escapeDollarSign}}'{% endif %}{% elseif property.type == 'boolean' %}true{% else %}{{property.example}}{% endif %}, {% endfor %}{% endif %}}{% endif %}{% else %}{% if property.type == 'object' and property.additionalProperties %}Map{% else %}{{property | typeName}}{% endif %}{% endif %}{% endmacro %} {% import 'flutter/base/utils.twig' as utils %} {% if 'dart' in language.params.packageName %} @@ -69,7 +69,7 @@ void main() { {%~ 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' %}{% if property.sub_schema and (property.sub_schema != 'prefs' and property.sub_schema != 'preferences') %}{{_self.sub_schema(spec.definitions, property)}}{% else %}{}{% endif %}{% 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 -%} + '{{property.name | escapeDollarSign}}': {% if property.type == 'object' %}{% if property.sub_schema and (property.sub_schema != 'prefs' and property.sub_schema != 'preferences') %}{{_self.sub_schema(spec.definitions, property)}}{% else %}{}{% endif %}{% elseif property.type == 'array' %}[]{% elseif property.type == 'string' %}{% if property.enum %}'{{property.enum[0]}}'{% else %}'{{property.example | escapeDollarSign}}'{% endif %}{% elseif property.type == 'boolean' %}true{% else %}{{property.example}}{% endif %},{%~ endfor ~%}{% set break = true %}{%- else -%}{% set continue = true %}{%- endif -%}{%~ endfor -%} }; {%~ else ~%} diff --git a/templates/dart/test/src/models/model_test.dart.twig b/templates/dart/test/src/models/model_test.dart.twig index 2197587642..5df798fc31 100644 --- a/templates/dart/test/src/models/model_test.dart.twig +++ b/templates/dart/test/src/models/model_test.dart.twig @@ -3,6 +3,9 @@ {{ 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' and property.sub_schema %}{{_self.sub_schema(definitions, property)}}{% elseif property.type == 'string' %}'{{property['x-example'] | escapeDollarSign}}'{% elseif property.type == 'boolean' %}true{% else %}{{property['x-example']}}{% endif %}, {% endfor %}{% endif %}){% endif %}{% else %}{% if property.type == 'object' and property.additionalProperties %}Map{% else %}{{property | typeName}}{% endif %}{% endif %}{% endmacro %} import 'package:{{ language.params.packageName }}/models.dart'; +{% if definition.properties | filter(p => p.enum) | length > 0 %} +import 'package:{{ language.params.packageName }}/enums.dart'; +{% endif %} {% if 'dart' in language.params.packageName %} import 'package:test/test.dart'; {% else %} @@ -14,7 +17,7 @@ void main() { 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' and property.sub_schema %}{{_self.sub_schema(spec.definitions, property)}}{% elseif property.type == 'object' %}{}{% elseif property.type == 'string' %}'{{property['x-example'] | escapeDollarSign}}'{% elseif property.type == 'boolean' %}true{% else %}{{property['x-example']}}{% endif %}, + {{ 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' and property.sub_schema %}{{_self.sub_schema(spec.definitions, property)}}{% elseif property.type == 'object' %}{}{% elseif property.enum %}{{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }}{% elseif property.type == 'string' %}'{{property['x-example'] | escapeDollarSign}}'{% elseif property.type == 'boolean' %}true{% else %}{{property['x-example']}}{% endif %}, {% endfor %} {% if definition.additionalProperties %} data: {}, @@ -26,7 +29,7 @@ void main() { {% for property in definition.properties | filter(p => p.required) %} {% if property.type != 'object' or not property.sub_schema or (property.sub_schema == 'prefs' and property.sub_schema == 'preferences') %} - 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 %}); + 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.enum %}{{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }}{% elseif property.type == 'string' %}'{{property['x-example'] | escapeDollarSign}}'{% elseif property.type == 'boolean' %}true{% else %}{{property['x-example']}}{% endif %}); {% endif %} {% endfor %} }); diff --git a/templates/deno/README.md.twig b/templates/deno/README.md.twig index e3dfe8cf31..5c27c7fb0b 100644 --- a/templates/deno/README.md.twig +++ b/templates/deno/README.md.twig @@ -16,7 +16,6 @@ {{ sdk.description }} - {% if sdk.logo %} ![{{ spec.title }}]({{ sdk.logo }}) {% endif %} diff --git a/templates/deno/docs/example.md.twig b/templates/deno/docs/example.md.twig index 3f179194db..c8c4c4830f 100644 --- a/templates/deno/docs/example.md.twig +++ b/templates/deno/docs/example.md.twig @@ -1,4 +1,4 @@ -import { Client, {{ service.name | caseUcfirst }}{% for parameter in method.parameters.all %}{% if parameter.enumValues | length > 0%}, {{ parameter.enumName | caseUcfirst}}{% endif %}{% endfor %} } from "https://deno.land/x/{{ spec.title | caseDash }}/mod.ts"; +import { Client, {{ service.name | caseUcfirst }}{% for parameter in method.parameters.all %}{% if parameter.enumValues | length > 0%}, {{ parameter.enumName | caseUcfirst}}{% endif %}{% endfor %}{% if method.parameters.all | hasPermissionParam %}, Permission, Role{% endif %} } from "https://deno.land/x/{{ spec.title | caseDash }}/mod.ts"; const client = new Client() {%~ if method.auth|length > 0 %} diff --git a/templates/deno/mod.ts.twig b/templates/deno/mod.ts.twig index 262134dee4..2db80109c1 100644 --- a/templates/deno/mod.ts.twig +++ b/templates/deno/mod.ts.twig @@ -3,12 +3,13 @@ import { Query } from "./src/query.ts"; import { Permission } from "./src/permission.ts"; import { Role } from "./src/role.ts"; import { ID } from "./src/id.ts"; +import { Operator, Condition } from "./src/operator.ts"; import { InputFile } from "./src/inputFile.ts"; import { {{spec.title | caseUcfirst}}Exception } from "./src/exception.ts"; {% for service in spec.services %} import { {{service.name | caseUcfirst}} } from "./src/services/{{service.name | caseKebab}}.ts"; {% endfor %} -{% for enum in spec.enums %} +{% for enum in spec.allEnums %} import { {{enum.name | caseUcfirst}} } from "./src/enums/{{enum.name | caseKebab}}.ts"; {% endfor %} @@ -18,12 +19,14 @@ export { Permission, Role, ID, + Operator, + Condition, InputFile, {{spec.title | caseUcfirst}}Exception, {% for service in spec.services %} {{service.name | caseUcfirst}}, {% endfor %} -{% for enum in spec.enums %} +{% for enum in spec.allEnums %} {{enum.name | caseUcfirst}}, {% endfor %} }; diff --git a/templates/deno/src/client.ts.twig b/templates/deno/src/client.ts.twig index fa263da6c2..a21ea416e9 100644 --- a/templates/deno/src/client.ts.twig +++ b/templates/deno/src/client.ts.twig @@ -46,6 +46,10 @@ export class Client { * @return this */ setEndpoint(endpoint: string): this { + if (!endpoint || typeof endpoint !== 'string') { + throw new {{spec.title | caseUcfirst}}Exception('Endpoint must be a valid string'); + } + if (!endpoint.startsWith('http://') && !endpoint.startsWith('https://')) { throw new {{spec.title | caseUcfirst}}Exception('Invalid endpoint URL: ' + endpoint); } diff --git a/templates/deno/src/operator.ts.twig b/templates/deno/src/operator.ts.twig new file mode 100644 index 0000000000..2386a6c414 --- /dev/null +++ b/templates/deno/src/operator.ts.twig @@ -0,0 +1,308 @@ +type OperatorValuesSingle = string | number | boolean; +export type OperatorValuesList = string[] | number[] | boolean[] | any[]; +export type OperatorValues = OperatorValuesSingle | OperatorValuesList; + +export enum Condition { + Equal = "equal", + NotEqual = "notEqual", + GreaterThan = "greaterThan", + GreaterThanEqual = "greaterThanEqual", + LessThan = "lessThan", + LessThanEqual = "lessThanEqual", + Contains = "contains", + IsNull = "isNull", + IsNotNull = "isNotNull", +} + +/** + * Helper class to generate operator strings for atomic operations. + */ +export class Operator { + method: string; + values: OperatorValuesList | undefined; + + /** + * Constructor for Operator class. + * + * @param {string} method + * @param {OperatorValues} values + */ + constructor( + method: string, + values?: OperatorValues + ) { + this.method = method; + + if (values !== undefined) { + if (Array.isArray(values)) { + this.values = values; + } else { + this.values = [values] as OperatorValuesList; + } + } + } + + /** + * Convert the operator object to a JSON string. + * + * @returns {string} + */ + toString(): string { + return JSON.stringify({ + method: this.method, + values: this.values, + }); + } + + /** + * Increment a numeric attribute by a specified value. + * + * @param {number} value + * @param {number} max + * @returns {string} + */ + static increment = (value: number = 1, max?: number): string => { + if (isNaN(value) || !isFinite(value)) { + throw new Error("Value cannot be NaN or Infinity"); + } + if (max !== undefined && (isNaN(max) || !isFinite(max))) { + throw new Error("Max cannot be NaN or Infinity"); + } + const values: any[] = [value]; + if (max !== undefined) { + values.push(max); + } + return new Operator("increment", values).toString(); + }; + + /** + * Decrement a numeric attribute by a specified value. + * + * @param {number} value + * @param {number} min + * @returns {string} + */ + static decrement = (value: number = 1, min?: number): string => { + if (isNaN(value) || !isFinite(value)) { + throw new Error("Value cannot be NaN or Infinity"); + } + if (min !== undefined && (isNaN(min) || !isFinite(min))) { + throw new Error("Min cannot be NaN or Infinity"); + } + const values: any[] = [value]; + if (min !== undefined) { + values.push(min); + } + return new Operator("decrement", values).toString(); + }; + + /** + * Multiply a numeric attribute by a specified factor. + * + * @param {number} factor + * @param {number} max + * @returns {string} + */ + static multiply = (factor: number, max?: number): string => { + if (isNaN(factor) || !isFinite(factor)) { + throw new Error("Factor cannot be NaN or Infinity"); + } + if (max !== undefined && (isNaN(max) || !isFinite(max))) { + throw new Error("Max cannot be NaN or Infinity"); + } + const values: any[] = [factor]; + if (max !== undefined) { + values.push(max); + } + return new Operator("multiply", values).toString(); + }; + + /** + * Divide a numeric attribute by a specified divisor. + * + * @param {number} divisor + * @param {number} min + * @returns {string} + */ + static divide = (divisor: number, min?: number): string => { + if (isNaN(divisor) || !isFinite(divisor)) { + throw new Error("Divisor cannot be NaN or Infinity"); + } + if (min !== undefined && (isNaN(min) || !isFinite(min))) { + throw new Error("Min cannot be NaN or Infinity"); + } + if (divisor === 0) { + throw new Error("Divisor cannot be zero"); + } + const values: any[] = [divisor]; + if (min !== undefined) { + values.push(min); + } + return new Operator("divide", values).toString(); + }; + + /** + * Apply modulo operation on a numeric attribute. + * + * @param {number} divisor + * @returns {string} + */ + static modulo = (divisor: number): string => { + if (isNaN(divisor) || !isFinite(divisor)) { + throw new Error("Divisor cannot be NaN or Infinity"); + } + if (divisor === 0) { + throw new Error("Divisor cannot be zero"); + } + return new Operator("modulo", [divisor]).toString(); + }; + + /** + * Raise a numeric attribute to a specified power. + * + * @param {number} exponent + * @param {number} max + * @returns {string} + */ + static power = (exponent: number, max?: number): string => { + if (isNaN(exponent) || !isFinite(exponent)) { + throw new Error("Exponent cannot be NaN or Infinity"); + } + if (max !== undefined && (isNaN(max) || !isFinite(max))) { + throw new Error("Max cannot be NaN or Infinity"); + } + const values: any[] = [exponent]; + if (max !== undefined) { + values.push(max); + } + return new Operator("power", values).toString(); + }; + + /** + * Append values to an array attribute. + * + * @param {any[]} values + * @returns {string} + */ + static arrayAppend = (values: any[]): string => + new Operator("arrayAppend", values).toString(); + + /** + * Prepend values to an array attribute. + * + * @param {any[]} values + * @returns {string} + */ + static arrayPrepend = (values: any[]): string => + new Operator("arrayPrepend", values).toString(); + + /** + * Insert a value at a specific index in an array attribute. + * + * @param {number} index + * @param {any} value + * @returns {string} + */ + static arrayInsert = (index: number, value: any): string => + new Operator("arrayInsert", [index, value]).toString(); + + /** + * Remove a value from an array attribute. + * + * @param {any} value + * @returns {string} + */ + static arrayRemove = (value: any): string => + new Operator("arrayRemove", [value]).toString(); + + /** + * Remove duplicate values from an array attribute. + * + * @returns {string} + */ + static arrayUnique = (): string => + new Operator("arrayUnique", []).toString(); + + /** + * Keep only values that exist in both the current array and the provided array. + * + * @param {any[]} values + * @returns {string} + */ + static arrayIntersect = (values: any[]): string => + new Operator("arrayIntersect", values).toString(); + + /** + * Remove values from the array that exist in the provided array. + * + * @param {any[]} values + * @returns {string} + */ + static arrayDiff = (values: any[]): string => + new Operator("arrayDiff", values).toString(); + + /** + * Filter array values based on a condition. + * + * @param {Condition} condition + * @param {any} value + * @returns {string} + */ + static arrayFilter = (condition: Condition, value?: any): string => { + const values: any[] = [condition as string, value === undefined ? null : value]; + return new Operator("arrayFilter", values).toString(); + }; + + /** + * Concatenate a value to a string or array attribute. + * + * @param {any} value + * @returns {string} + */ + static stringConcat = (value: any): string => + new Operator("stringConcat", [value]).toString(); + + /** + * Replace occurrences of a search string with a replacement string. + * + * @param {string} search + * @param {string} replace + * @returns {string} + */ + static stringReplace = (search: string, replace: string): string => + new Operator("stringReplace", [search, replace]).toString(); + + /** + * Toggle a boolean attribute. + * + * @returns {string} + */ + static toggle = (): string => + new Operator("toggle", []).toString(); + + /** + * Add days to a date attribute. + * + * @param {number} days + * @returns {string} + */ + static dateAddDays = (days: number): string => + new Operator("dateAddDays", [days]).toString(); + + /** + * Subtract days from a date attribute. + * + * @param {number} days + * @returns {string} + */ + static dateSubDays = (days: number): string => + new Operator("dateSubDays", [days]).toString(); + + /** + * Set a date attribute to the current date and time. + * + * @returns {string} + */ + static dateSetNow = (): string => + new Operator("dateSetNow", []).toString(); +} diff --git a/templates/deno/src/query.ts.twig b/templates/deno/src/query.ts.twig index f00a6ef87f..f97b23e3d9 100644 --- a/templates/deno/src/query.ts.twig +++ b/templates/deno/src/query.ts.twig @@ -81,6 +81,9 @@ export class Query { static orderAsc = (attribute: string): string => new Query("orderAsc", attribute).toString(); + static orderRandom = (): string => + new Query("orderRandom").toString(); + static cursorAfter = (documentId: string): string => new Query("cursorAfter", undefined, documentId).toString(); @@ -162,7 +165,7 @@ export class Query { * @returns {string} */ static createdBefore = (value: string): string => - new Query("createdBefore", undefined, value).toString(); + Query.lessThan("$createdAt", value); /** * Filter resources where document was created after date. @@ -171,7 +174,7 @@ export class Query { * @returns {string} */ static createdAfter = (value: string): string => - new Query("createdAfter", undefined, value).toString(); + Query.greaterThan("$createdAt", value); /** * Filter resources where document was created between dates. @@ -181,7 +184,7 @@ export class Query { * @returns {string} */ static createdBetween = (start: string, end: string): string => - new Query("createdBetween", undefined, [start, end] as QueryTypesList).toString(); + Query.between("$createdAt", start, end); /** * Filter resources where document was updated before date. @@ -190,7 +193,7 @@ export class Query { * @returns {string} */ static updatedBefore = (value: string): string => - new Query("updatedBefore", undefined, value).toString(); + Query.lessThan("$updatedAt", value); /** * Filter resources where document was updated after date. @@ -199,7 +202,7 @@ export class Query { * @returns {string} */ static updatedAfter = (value: string): string => - new Query("updatedAfter", undefined, value).toString(); + Query.greaterThan("$updatedAt", value); /** * Filter resources where document was updated between dates. @@ -209,7 +212,7 @@ export class Query { * @returns {string} */ static updatedBetween = (start: string, end: string): string => - new Query("updatedBetween", undefined, [start, end] as QueryTypesList).toString(); + Query.between("$updatedAt", start, end); static or = (queries: string[]) => new Query("or", undefined, queries.map((query) => JSON.parse(query))).toString(); diff --git a/templates/deno/src/services/service.ts.twig b/templates/deno/src/services/service.ts.twig index 98e0abb8e2..f5544affff 100644 --- a/templates/deno/src/services/service.ts.twig +++ b/templates/deno/src/services/service.ts.twig @@ -36,14 +36,9 @@ import { Query } from '../query.ts'; {% for method in service.methods %} {% for parameter in method.parameters.all %} {% if parameter.enumValues is not empty %} -{% if parameter.enumName is not empty %} -{% set name = parameter.enumName %} -{% else %} -{% set name = parameter.name %} -{% endif %} -{% if name not in added %} -import { {{ name | caseUcfirst }} } from '../enums/{{ name | caseKebab }}.ts'; -{% set added = added|merge([name]) %} +{% if parameter.enumName not in added %} +import { {{ parameter.enumName | caseUcfirst }} } from '../enums/{{ parameter.enumName | caseKebab }}.ts'; +{% set added = added|merge([parameter.enumName]) %} {% endif %} {% endif %} {% endfor %} diff --git a/templates/deno/test/operator.test.ts.twig b/templates/deno/test/operator.test.ts.twig new file mode 100644 index 0000000000..5f8a7c3486 --- /dev/null +++ b/templates/deno/test/operator.test.ts.twig @@ -0,0 +1,130 @@ +import {describe, it as test} from "https://deno.land/std@0.149.0/testing/bdd.ts"; +import {assertEquals} from "https://deno.land/std@0.204.0/assert/assert_equals.ts"; +import {Operator, Condition} from "../src/operator.ts"; + +describe('Operator', () => { + test('increment', () => assertEquals( + Operator.increment().toString(), + '{"method":"increment","values":[1]}', + )); + + test('increment with max', () => assertEquals( + Operator.increment(5, 100).toString(), + '{"method":"increment","values":[5,100]}', + )); + + test('decrement', () => assertEquals( + Operator.decrement().toString(), + '{"method":"decrement","values":[1]}', + )); + + test('decrement with min', () => assertEquals( + Operator.decrement(3, 0).toString(), + '{"method":"decrement","values":[3,0]}', + )); + + test('multiply', () => assertEquals( + Operator.multiply(2).toString(), + '{"method":"multiply","values":[2]}', + )); + + test('multiply with max', () => assertEquals( + Operator.multiply(3, 1000).toString(), + '{"method":"multiply","values":[3,1000]}', + )); + + test('divide', () => assertEquals( + Operator.divide(2).toString(), + '{"method":"divide","values":[2]}', + )); + + test('divide with min', () => assertEquals( + Operator.divide(4, 1).toString(), + '{"method":"divide","values":[4,1]}', + )); + + test('modulo', () => assertEquals( + Operator.modulo(5).toString(), + '{"method":"modulo","values":[5]}', + )); + + test('power', () => assertEquals( + Operator.power(2).toString(), + '{"method":"power","values":[2]}', + )); + + test('power with max', () => assertEquals( + Operator.power(3, 100).toString(), + '{"method":"power","values":[3,100]}', + )); + + test('arrayAppend', () => assertEquals( + Operator.arrayAppend(['item1', 'item2']).toString(), + '{"method":"arrayAppend","values":["item1","item2"]}', + )); + + test('arrayPrepend', () => assertEquals( + Operator.arrayPrepend(['first', 'second']).toString(), + '{"method":"arrayPrepend","values":["first","second"]}', + )); + + test('arrayInsert', () => assertEquals( + Operator.arrayInsert(0, 'newItem').toString(), + '{"method":"arrayInsert","values":[0,"newItem"]}', + )); + + test('arrayRemove', () => assertEquals( + Operator.arrayRemove('oldItem').toString(), + '{"method":"arrayRemove","values":["oldItem"]}', + )); + + test('arrayUnique', () => assertEquals( + Operator.arrayUnique().toString(), + '{"method":"arrayUnique","values":[]}', + )); + + test('arrayIntersect', () => assertEquals( + Operator.arrayIntersect(['a', 'b', 'c']).toString(), + '{"method":"arrayIntersect","values":["a","b","c"]}', + )); + + test('arrayDiff', () => assertEquals( + Operator.arrayDiff(['x', 'y']).toString(), + '{"method":"arrayDiff","values":["x","y"]}', + )); + + test('arrayFilter', () => assertEquals( + Operator.arrayFilter(Condition.Equal, 'test').toString(), + '{"method":"arrayFilter","values":["equal","test"]}', + )); + + test('stringConcat', () => assertEquals( + Operator.stringConcat('suffix').toString(), + '{"method":"stringConcat","values":["suffix"]}', + )); + + test('stringReplace', () => assertEquals( + Operator.stringReplace('old', 'new').toString(), + '{"method":"stringReplace","values":["old","new"]}', + )); + + test('toggle', () => assertEquals( + Operator.toggle().toString(), + '{"method":"toggle","values":[]}', + )); + + test('dateAddDays', () => assertEquals( + Operator.dateAddDays(7).toString(), + '{"method":"dateAddDays","values":[7]}', + )); + + test('dateSubDays', () => assertEquals( + Operator.dateSubDays(3).toString(), + '{"method":"dateSubDays","values":[3]}', + )); + + test('dateSetNow', () => assertEquals( + Operator.dateSetNow().toString(), + '{"method":"dateSetNow","values":[]}', + )); +}) diff --git a/templates/deno/test/query.test.ts.twig b/templates/deno/test/query.test.ts.twig index 06bfc43216..62024de549 100644 --- a/templates/deno/test/query.test.ts.twig +++ b/templates/deno/test/query.test.ts.twig @@ -153,6 +153,11 @@ describe('Query', () => { `{"method":"orderDesc","attribute":"attr"}`, )); + test('orderRandom', () => assertEquals( + Query.orderRandom().toString(), + `{"method":"orderRandom"}`, + )); + test('cursorBefore', () => assertEquals( Query.cursorBefore('attr').toString(), `{"method":"cursorBefore","attribute":"attr"}`, @@ -210,31 +215,31 @@ describe('Query', () => { test('createdBefore', () => assertEquals( Query.createdBefore('2023-01-01').toString(), - `{"method":"createdBefore","values":["2023-01-01"]}`, + `{"method":"lessThan","attribute":"$createdAt","values":["2023-01-01"]}`, )); test('createdAfter', () => assertEquals( Query.createdAfter('2023-01-01').toString(), - `{"method":"createdAfter","values":["2023-01-01"]}`, + `{"method":"greaterThan","attribute":"$createdAt","values":["2023-01-01"]}`, )); test('createdBetween', () => assertEquals( Query.createdBetween('2023-01-01', '2023-12-31').toString(), - `{"method":"createdBetween","values":["2023-01-01","2023-12-31"]}`, + `{"method":"between","attribute":"$createdAt","values":["2023-01-01","2023-12-31"]}`, )); test('updatedBefore', () => assertEquals( Query.updatedBefore('2023-01-01').toString(), - `{"method":"updatedBefore","values":["2023-01-01"]}`, + `{"method":"lessThan","attribute":"$updatedAt","values":["2023-01-01"]}`, )); test('updatedAfter', () => assertEquals( Query.updatedAfter('2023-01-01').toString(), - `{"method":"updatedAfter","values":["2023-01-01"]}`, + `{"method":"greaterThan","attribute":"$updatedAt","values":["2023-01-01"]}`, )); test('updatedBetween', () => assertEquals( Query.updatedBetween('2023-01-01', '2023-12-31').toString(), - `{"method":"updatedBetween","values":["2023-01-01","2023-12-31"]}`, + `{"method":"between","attribute":"$updatedAt","values":["2023-01-01","2023-12-31"]}`, )); }) diff --git a/templates/dotnet/Package.Tests/.gitignore b/templates/dotnet/Package.Tests/.gitignore new file mode 100644 index 0000000000..9eb3c7a5a5 --- /dev/null +++ b/templates/dotnet/Package.Tests/.gitignore @@ -0,0 +1,23 @@ +# Test results +TestResults/ +*.trx +*.coverage +*.coveragexml + +# Coverage reports +coverage/ +coverage.json +coverage.opencover.xml +lcov.info + +# Build outputs +bin/ +obj/ +*.user +*.suo + +# Rider +.idea/ + +# Visual Studio +.vs/ diff --git a/templates/dotnet/Package.Tests/ClientTests.cs.twig b/templates/dotnet/Package.Tests/ClientTests.cs.twig new file mode 100644 index 0000000000..61b20a3468 --- /dev/null +++ b/templates/dotnet/Package.Tests/ClientTests.cs.twig @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; +using {{ spec.title | caseUcfirst }}; + +namespace {{ spec.title | caseUcfirst }}.Tests +{ + public class ClientTests + { + [Fact] + public void Constructor_Default_CreatesClient() + { + // Act + var client = new Client(); + + // Assert + Assert.NotNull(client); + Assert.Equal("{{spec.endpoint}}", client.Endpoint); + Assert.NotNull(client.Config); + } + + [Fact] + public void Constructor_WithCustomEndpoint_SetsEndpoint() + { + // Arrange + var customEndpoint = "https://custom.example.com/v1"; + + // Act + var client = new Client(endpoint: customEndpoint); + + // Assert + Assert.Equal(customEndpoint, client.Endpoint); + } + + [Fact] + public void Constructor_WithSelfSigned_EnablesSelfSigned() + { + // Act + var client = new Client(selfSigned: true); + + // Assert + Assert.NotNull(client); + } + + [Fact] + public void Constructor_WithHttpClient_UsesProvidedClient() + { + // Arrange + var httpClient = new HttpClient(); + + // Act + var client = new Client(http: httpClient); + + // Assert + Assert.NotNull(client); + } + + [Fact] + public void SetEndpoint_UpdatesEndpoint() + { + // Arrange + var client = new Client(); + var newEndpoint = "https://new.example.com/v1"; + + // Act + var result = client.SetEndpoint(newEndpoint); + + // Assert + Assert.Equal(newEndpoint, client.Endpoint); + Assert.Same(client, result); + } + + [Theory] + {%~ for header in spec.global.headers %} + [InlineData("{{header.key}}", "test-{{header.key}}")] + {%~ endfor %} + public void SetHeader_SetsCustomHeader(string key, string value) + { + // Arrange + var client = new Client(); + + // Act + var result = client.AddHeader(key, value); + + // Assert + Assert.Same(client, result); + } + + [Fact] + public void Config_IsNotNull() + { + // Arrange & Act + var client = new Client(); + + // Assert + Assert.NotNull(client.Config); + } + + [Fact] + public void SetProject_UpdatesConfig() + { + // Arrange + var client = new Client(); + var projectId = "test-project-id"; + + // Act + var result = client.SetProject(projectId); + + // Assert + Assert.True(client.Config.ContainsKey("project")); + Assert.Equal(projectId, client.Config["project"]); + Assert.Same(client, result); + } + + [Fact] + public void SetSelfSigned_EnablesSelfSignedCertificates() + { + // Arrange + var client = new Client(); + + // Act + var result = client.SetSelfSigned(true); + + // Assert + Assert.NotNull(result); + Assert.Same(client, result); + } + + [Fact] + public void SetSelfSigned_DisablesSelfSignedCertificates() + { + // Arrange + var client = new Client(selfSigned: true); + + // Act + var result = client.SetSelfSigned(false); + + // Assert + Assert.NotNull(result); + Assert.Same(client, result); + } + + [Fact] + public void DeserializerOptions_IsNotNull() + { + // Assert + Assert.NotNull(Client.DeserializerOptions); + } + + [Fact] + public void SerializerOptions_IsNotNull() + { + // Assert + Assert.NotNull(Client.SerializerOptions); + } + + [Fact] + public void DeserializerOptions_HasConverters() + { + // Assert + Assert.NotEmpty(Client.DeserializerOptions.Converters); + } + + [Fact] + public void SerializerOptions_HasConverters() + { + // Assert + Assert.NotEmpty(Client.SerializerOptions.Converters); + } + + [Fact] + public void Endpoint_CanBeRetrieved() + { + // Arrange + var endpoint = "https://test.example.com/v1"; + var client = new Client(endpoint: endpoint); + + // Act + var result = client.Endpoint; + + // Assert + Assert.Equal(endpoint, result); + } + + [Fact] + public void Config_CanBeRetrieved() + { + // Arrange + var client = new Client(); + + // Act + var config = client.Config; + + // Assert + Assert.NotNull(config); + Assert.IsType>(config); + } + + [Fact] + public void ChainedCalls_Work() + { + // Arrange & Act + var client = new Client() + .SetEndpoint("https://example.com/v1") + .SetProject("test-project") + .SetSelfSigned(false); + + // Assert + Assert.NotNull(client); + Assert.Equal("https://example.com/v1", client.Endpoint); + Assert.Equal("test-project", client.Config["project"]); + } + } +} diff --git a/templates/dotnet/Package.Tests/Converters/ObjectToInferredTypesConverterTests.cs.twig b/templates/dotnet/Package.Tests/Converters/ObjectToInferredTypesConverterTests.cs.twig new file mode 100644 index 0000000000..cc0b1f2231 --- /dev/null +++ b/templates/dotnet/Package.Tests/Converters/ObjectToInferredTypesConverterTests.cs.twig @@ -0,0 +1,313 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using Xunit; +using {{ spec.title | caseUcfirst }}.Converters; + +namespace {{ spec.title | caseUcfirst }}.Tests.Converters +{ + public class ObjectToInferredTypesConverterTests + { + private readonly JsonSerializerOptions _options; + + public ObjectToInferredTypesConverterTests() + { + _options = new JsonSerializerOptions(); + _options.Converters.Add(new ObjectToInferredTypesConverter()); + } + + [Fact] + public void Read_WithString_ReturnsString() + { + // Arrange + var json = "\"test string\""; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType(result); + Assert.Equal("test string", result); + } + + [Fact] + public void Read_WithInteger_ReturnsLong() + { + // Arrange + var json = "123"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType(result); + Assert.Equal(123L, result); + } + + [Fact] + public void Read_WithDouble_ReturnsDouble() + { + // Arrange + var json = "123.45"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType(result); + Assert.Equal(123.45, result); + } + + [Fact] + public void Read_WithBoolean_ReturnsBoolean() + { + // Arrange + var json = "true"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType(result); + Assert.True((bool)result); + } + + [Fact] + public void Read_WithNull_ReturnsNull() + { + // Arrange + var json = "null"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Read_WithObject_ReturnsDictionary() + { + // Arrange + var json = "{\"key\":\"value\",\"number\":42}"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType>(result); + var dict = (Dictionary)result; + Assert.Equal(2, dict.Count); + Assert.Equal("value", dict["key"]); + Assert.Equal(42L, dict["number"]); + } + + [Fact] + public void Read_WithArray_ReturnsList() + { + // Arrange + var json = "[1,2,3,4,5]"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType>(result); + var list = (List)result; + Assert.Equal(5, list.Count); + Assert.Equal(1L, list[0]); + Assert.Equal(5L, list[4]); + } + + [Fact] + public void Read_WithNestedObject_ReturnsNestedDictionary() + { + // Arrange + var json = "{\"outer\":{\"inner\":\"value\"}}"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType>(result); + var dict = (Dictionary)result; + Assert.IsType>(dict["outer"]); + var nested = (Dictionary)dict["outer"]; + Assert.Equal("value", nested["inner"]); + } + + [Fact] + public void Read_WithArrayOfObjects_ReturnsListOfDictionaries() + { + // Arrange + var json = "[{\"id\":1},{\"id\":2}]"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType>(result); + var list = (List)result; + Assert.Equal(2, list.Count); + Assert.IsType>(list[0]); + } + + [Fact] + public void Read_WithMixedTypes_ConvertsCorrectly() + { + // Arrange + var json = "{\"string\":\"text\",\"number\":123,\"bool\":true,\"null\":null,\"array\":[1,2,3]}"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType>(result); + var dict = (Dictionary)result; + Assert.Equal("text", dict["string"]); + Assert.Equal(123L, dict["number"]); + Assert.True((bool)dict["bool"]); + Assert.Null(dict["null"]); + Assert.IsType>(dict["array"]); + } + + [Fact] + public void Read_WithDateTime_ReturnsDateTime() + { + // Arrange + var json = "\"2023-10-16T12:00:00Z\""; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType(result); + } + + [Fact] + public void Read_WithEmptyObject_ReturnsEmptyDictionary() + { + // Arrange + var json = "{}"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType>(result); + var dict = (Dictionary)result; + Assert.Empty(dict); + } + + [Fact] + public void Read_WithEmptyArray_ReturnsEmptyList() + { + // Arrange + var json = "[]"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType>(result); + var list = (List)result; + Assert.Empty(list); + } + + [Fact] + public void Write_WithString_WritesString() + { + // Arrange + var value = "test"; + + // Act + var json = JsonSerializer.Serialize(value, _options); + + // Assert + Assert.Equal("\"test\"", json); + } + + [Fact] + public void Write_WithInteger_WritesInteger() + { + // Arrange + var value = 123; + + // Act + var json = JsonSerializer.Serialize(value, _options); + + // Assert + Assert.Equal("123", json); + } + + [Fact] + public void Write_WithBoolean_WritesBoolean() + { + // Arrange + var value = true; + + // Act + var json = JsonSerializer.Serialize(value, _options); + + // Assert + Assert.Equal("true", json); + } + + [Fact] + public void Write_WithNull_WritesNull() + { + // Arrange + object value = null; + + // Act + var json = JsonSerializer.Serialize(value, _options); + + // Assert + Assert.Equal("null", json); + } + + [Fact] + public void Write_WithDictionary_WritesObject() + { + // Arrange + var value = new Dictionary + { + { "key", "value" }, + { "number", 42 } + }; + + // Act + var json = JsonSerializer.Serialize(value, _options); + + // Assert + Assert.Contains("\"key\"", json); + Assert.Contains("\"value\"", json); + Assert.Contains("\"number\"", json); + Assert.Contains("42", json); + } + + [Fact] + public void RoundTrip_PreservesData() + { + // Arrange + var original = new Dictionary + { + { "string", "test" }, + { "number", 123L }, + { "bool", true }, + { "array", new List { 1L, 2L, 3L } } + }; + + // Act + var json = JsonSerializer.Serialize(original, _options); + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType>(result); + var dict = (Dictionary)result; + Assert.Equal("test", dict["string"]); + Assert.Equal(123L, dict["number"]); + Assert.True((bool)dict["bool"]); + } + } +} diff --git a/templates/dotnet/Package.Tests/Converters/ValueClassConverterTests.cs.twig b/templates/dotnet/Package.Tests/Converters/ValueClassConverterTests.cs.twig new file mode 100644 index 0000000000..4be51bf513 --- /dev/null +++ b/templates/dotnet/Package.Tests/Converters/ValueClassConverterTests.cs.twig @@ -0,0 +1,223 @@ +using System; +using System.Text.Json; +using Xunit; +using {{ spec.title | caseUcfirst }}.Converters; +using {{ spec.title | caseUcfirst }}.Enums; + +namespace {{ spec.title | caseUcfirst }}.Tests.Converters +{ + public class ValueClassConverterTests + { + private readonly JsonSerializerOptions _options; + + public ValueClassConverterTests() + { + _options = new JsonSerializerOptions(); + _options.Converters.Add(new ValueClassConverter()); + _options.PropertyNameCaseInsensitive = true; + } + + [Fact] + public void CanConvert_WithIEnumType_ReturnsTrue() + { + // Arrange + var converter = new ValueClassConverter(); + + // Act + var result = converter.CanConvert(typeof(IEnum)); + + // Assert + Assert.True(result); + } + + [Fact] + public void CanConvert_WithStringType_ReturnsFalse() + { + // Arrange + var converter = new ValueClassConverter(); + + // Act + var result = converter.CanConvert(typeof(string)); + + // Assert + Assert.False(result); + } + + [Fact] + public void CanConvert_WithIntType_ReturnsFalse() + { + // Arrange + var converter = new ValueClassConverter(); + + // Act + var result = converter.CanConvert(typeof(int)); + + // Assert + Assert.False(result); + } + + [Fact] + public void Write_WithValidEnum_WritesStringValue() + { + // Arrange + var testEnum = new TestEnum("testValue"); + + // Act + var json = JsonSerializer.Serialize(testEnum, _options); + + // Assert + Assert.Equal("\"testValue\"", json); + } + + [Fact] + public void Write_WithNull_WritesNull() + { + // Arrange + TestEnum testEnum = null; + + // Act + var json = JsonSerializer.Serialize(testEnum, _options); + + // Assert + Assert.Equal("null", json); + } + + [Fact] + public void Write_WithEmptyValue_WritesEmptyString() + { + // Arrange + var testEnum = new TestEnum(""); + + // Act + var json = JsonSerializer.Serialize(testEnum, _options); + + // Assert + Assert.Equal("\"\"", json); + } + + [Fact] + public void Write_WithSpecialCharacters_EscapesCorrectly() + { + // Arrange + var testEnum = new TestEnum("test\"value"); + + // Act + var json = JsonSerializer.Serialize(testEnum, _options); + + // Assert + Assert.Contains("\\u0022", json); + } + + [Fact] + public void Read_WithValidString_CreatesEnum() + { + // Arrange + var json = "\"testValue\""; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.NotNull(result); + Assert.Equal("testValue", result.Value); + } + + [Fact] + public void Read_WithEmptyString_CreatesEnumWithEmptyValue() + { + // Arrange + var json = "\"\""; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.NotNull(result); + Assert.Equal("", result.Value); + } + + [Fact] + public void RoundTrip_PreservesValue() + { + // Arrange + var original = new TestEnum("originalValue"); + + // Act + var json = JsonSerializer.Serialize(original, _options); + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.Equal(original.Value, result.Value); + } + + [Fact] + public void RoundTrip_WithMultipleValues_PreservesAllValues() + { + // Arrange + var values = new[] { "value1", "value2", "value3" }; + + foreach (var value in values) + { + var original = new TestEnum(value); + + // Act + var json = JsonSerializer.Serialize(original, _options); + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.Equal(original.Value, result.Value); + } + } + + [Fact] + public void Write_InComplexObject_SerializesCorrectly() + { + // Arrange + var obj = new + { + EnumValue = new TestEnum("test"), + StringValue = "string" + }; + + // Act + var json = JsonSerializer.Serialize(obj, _options); + + // Assert + Assert.Contains("\"test\"", json); + Assert.Contains("\"string\"", json); + } + + [Fact] + public void Read_FromComplexObject_DeserializesCorrectly() + { + // Arrange + var json = "{\"enumValue\":\"testValue\",\"stringValue\":\"string\"}"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.EnumValue); + Assert.Equal("testValue", result.EnumValue.Value); + Assert.Equal("string", result.StringValue); + } + + // Test helper classes + private class TestEnum : IEnum + { + public string Value { get; private set; } + + public TestEnum(string value) + { + Value = value; + } + } + + private class ComplexObject + { + public TestEnum EnumValue { get; set; } + public string StringValue { get; set; } + } + } +} diff --git a/templates/dotnet/Package.Tests/Enums/EnumTests.cs.twig b/templates/dotnet/Package.Tests/Enums/EnumTests.cs.twig new file mode 100644 index 0000000000..3505713c9c --- /dev/null +++ b/templates/dotnet/Package.Tests/Enums/EnumTests.cs.twig @@ -0,0 +1,111 @@ +using Xunit; +using System.Linq; +using {{ spec.title | caseUcfirst }}.Enums; + +namespace {{ spec.title | caseUcfirst }}.Tests.Enums +{ + public class {{ enum.name | caseUcfirst | overrideIdentifier }}Tests + { + [Fact] + public void Constructor_WithValue_CreatesInstance() + { + // Arrange & Act + var enumValue = new {{ enum.name | caseUcfirst | overrideIdentifier }}("test"); + + // Assert + Assert.NotNull(enumValue); + Assert.Equal("test", enumValue.Value); + } + + {%~ for value in enum.enum %} + {%~ set key = enum.keys is empty ? value : enum.keys[loop.index0] %} + [Fact] + public void {{ key | caseEnumKey }}_ReturnsCorrectValue() + { + // Act + var enumValue = {{ enum.name | caseUcfirst | overrideIdentifier }}.{{ key | caseEnumKey }}; + + // Assert + Assert.NotNull(enumValue); + Assert.Equal("{{ value }}", enumValue.Value); + } + + {%~ endfor %} + + [Theory] + {%~ for value in enum.enum %} + [InlineData("{{ value }}")] + {%~ endfor %} + public void Value_WithValidString_IsCorrect(string value) + { + // Act + var enumValue = new {{ enum.name | caseUcfirst | overrideIdentifier }}(value); + + // Assert + Assert.Equal(value, enumValue.Value); + } + + [Fact] + public void StaticProperties_AreNotNull() + { + {%~ for value in enum.enum %} + {%~ set key = enum.keys is empty ? value : enum.keys[loop.index0] %} + Assert.NotNull({{ enum.name | caseUcfirst | overrideIdentifier }}.{{ key | caseEnumKey }}); + {%~ endfor %} + } + + [Fact] + public void StaticProperties_HaveUniqueValues() + { + var values = new[] + { + {%~ for value in enum.enum %} + {%~ set key = enum.keys is empty ? value : enum.keys[loop.index0] %} + {{ enum.name | caseUcfirst | overrideIdentifier }}.{{ key | caseEnumKey }}.Value{% if not loop.last %},{% endif %} + + {%~ endfor %} + }; + + Assert.Equal(values.Length, values.Distinct().Count()); + } + + [Fact] + public void Implements_IEnum() + { + // Arrange + {%~ set firstValue = enum.enum[0] %} + {%~ set firstKey = enum.keys is empty ? firstValue : enum.keys[0] %} + var enumValue = {{ enum.name | caseUcfirst | overrideIdentifier }}.{{ firstKey | caseEnumKey }}; + + // Assert + Assert.IsAssignableFrom(enumValue); + } + + [Fact] + public void Value_CanBeSetInConstructor() + { + // Arrange + var customValue = "customValue"; + + // Act + var enumValue = new {{ enum.name | caseUcfirst | overrideIdentifier }}(customValue); + + // Assert + Assert.Equal(customValue, enumValue.Value); + } + + [Fact] + public void ToString_ReturnsValue() + { + // Arrange + {%~ set firstValue = enum.enum[0] %} + {%~ set firstKey = enum.keys is empty ? firstValue : enum.keys[0] %} + var enumValue = {{ enum.name | caseUcfirst | overrideIdentifier }}.{{ firstKey | caseEnumKey }}; + + // Act & Assert + // Value property should return the string value + Assert.NotNull(enumValue.Value); + Assert.IsType(enumValue.Value); + } + } +} diff --git a/templates/dotnet/Package.Tests/ExceptionTests.cs.twig b/templates/dotnet/Package.Tests/ExceptionTests.cs.twig new file mode 100644 index 0000000000..d45e5b311d --- /dev/null +++ b/templates/dotnet/Package.Tests/ExceptionTests.cs.twig @@ -0,0 +1,143 @@ +using System; +using Xunit; +using {{ spec.title | caseUcfirst }}; + +namespace {{ spec.title | caseUcfirst }}.Tests +{ + public class ExceptionTests + { + [Fact] + public void Constructor_Default_CreatesException() + { + var exception = new {{spec.title | caseUcfirst}}Exception(); + + Assert.NotNull(exception); + Assert.NotNull(exception.Message); + Assert.Null(exception.Code); + Assert.Null(exception.Type); + Assert.Null(exception.Response); + } + + [Fact] + public void Constructor_WithMessage_SetsMessage() + { + var message = "Some error message"; + var exception = new {{spec.title | caseUcfirst}}Exception(message); + + Assert.NotNull(exception); + Assert.Equal(message, exception.Message); + Assert.Null(exception.Code); + Assert.Null(exception.Type); + Assert.Null(exception.Response); + } + + [Fact] + public void Constructor_WithAllParameters_SetsAllProperties() + { + var message = "Invalid request"; + var code = 400; + var type = "ValidationError"; + var response = "{\"error\":\"validation failed\"}"; + + var exception = new {{spec.title | caseUcfirst}}Exception(message, code, type, response); + + Assert.NotNull(exception); + Assert.Equal(message, exception.Message); + Assert.Equal(code, exception.Code); + Assert.Equal(type, exception.Type); + Assert.Equal(response, exception.Response); + } + + [Fact] + public void Constructor_WithMessageAndCode_SetsCorrectly() + { + var message = "Not found"; + var code = 404; + + var exception = new {{spec.title | caseUcfirst}}Exception(message, code); + + Assert.NotNull(exception); + Assert.Equal(message, exception.Message); + Assert.Equal(code, exception.Code); + Assert.Null(exception.Type); + Assert.Null(exception.Response); + } + + [Fact] + public void Constructor_WithInnerException_SetsInnerException() + { + var message = "Outer exception"; + var innerException = new Exception("Inner exception"); + + var exception = new {{spec.title | caseUcfirst}}Exception(message, innerException); + + Assert.NotNull(exception); + Assert.Equal(message, exception.Message); + Assert.NotNull(exception.InnerException); + Assert.Equal("Inner exception", exception.InnerException.Message); + } + + [Fact] + public void Exception_CanBeCaught() + { + var caught = false; + + try + { + throw new {{spec.title | caseUcfirst}}Exception("Test exception"); + } + catch ({{spec.title | caseUcfirst}}Exception) + { + caught = true; + } + + Assert.True(caught); + } + + [Fact] + public void Exception_WithCode_ReturnsCorrectCode() + { + var exception = new {{spec.title | caseUcfirst}}Exception("Error", 500, "ServerError"); + + Assert.Equal(500, exception.Code); + } + + [Fact] + public void Exception_WithType_ReturnsCorrectType() + { + var exception = new {{spec.title | caseUcfirst}}Exception("Error", 401, "Unauthorized"); + + Assert.Equal("Unauthorized", exception.Type); + } + + [Fact] + public void Exception_WithResponse_ReturnsCorrectResponse() + { + var response = "{\"message\":\"error\"}"; + var exception = new {{spec.title | caseUcfirst}}Exception("Error", 400, "BadRequest", response); + + Assert.Equal(response, exception.Response); + } + + [Fact] + public void ToString_WithDefaultConstructor_ReturnsCorrectString() + { + var exception = new {{spec.title | caseUcfirst}}Exception(); + var result = exception.ToString(); + + Assert.NotNull(result); + Assert.Contains("{{spec.title | caseUcfirst}}Exception", result); + } + + [Fact] + public void ToString_WithMessage_ReturnsCorrectString() + { + var exception = new {{spec.title | caseUcfirst}}Exception("Some error message"); + var result = exception.ToString(); + + Assert.NotNull(result); + Assert.Contains("{{spec.title | caseUcfirst}}Exception", result); + Assert.Contains("Some error message", result); + } + } +} diff --git a/templates/dotnet/Package.Tests/IDTests.cs.twig b/templates/dotnet/Package.Tests/IDTests.cs.twig new file mode 100644 index 0000000000..23e89258bd --- /dev/null +++ b/templates/dotnet/Package.Tests/IDTests.cs.twig @@ -0,0 +1,58 @@ +using Xunit; +using {{ spec.title | caseUcfirst }}; + +namespace {{ spec.title | caseUcfirst }}.Tests +{ + public class IDTests + { + [Fact] + public void Unique_ReturnsUniqueID() + { + var id = ID.Unique(); + Assert.NotNull(id); + Assert.NotEmpty(id); + Assert.Equal(20, id.Length); + } + + [Fact] + public void Unique_WithCustomPadding_ReturnsCorrectLength() + { + var padding = 10; + var id = ID.Unique(padding); + Assert.NotNull(id); + Assert.NotEmpty(id); + Assert.Equal(13 + padding, id.Length); // 13 is base timestamp length + } + + [Fact] + public void Unique_GeneratesUniqueIDs() + { + var id1 = ID.Unique(); + var id2 = ID.Unique(); + Assert.NotEqual(id1, id2); + } + + [Fact] + public void Custom_ReturnsCustomString() + { + var customId = "custom"; + var result = ID.Custom(customId); + Assert.Equal(customId, result); + } + + [Fact] + public void Custom_WithEmptyString_ReturnsEmptyString() + { + var result = ID.Custom(""); + Assert.Equal("", result); + } + + [Fact] + public void Custom_WithSpecialCharacters_ReturnsExactString() + { + var customId = "test-123_abc@xyz"; + var result = ID.Custom(customId); + Assert.Equal(customId, result); + } + } +} diff --git a/templates/dotnet/Package.Tests/Models/InputFileTests.cs.twig b/templates/dotnet/Package.Tests/Models/InputFileTests.cs.twig new file mode 100644 index 0000000000..61f398d6d5 --- /dev/null +++ b/templates/dotnet/Package.Tests/Models/InputFileTests.cs.twig @@ -0,0 +1,219 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using Xunit; +using {{ spec.title | caseUcfirst }}.Models; + +namespace {{ spec.title | caseUcfirst }}.Tests.Models +{ + public class InputFileTests + { + [Fact] + public void FromPath_WithValidPath_CreatesInputFile() + { + // Arrange + var path = "test.txt"; + + // Act + var inputFile = InputFile.FromPath(path); + + // Assert + Assert.NotNull(inputFile); + Assert.Equal(path, inputFile.Path); + Assert.Equal("test.txt", inputFile.Filename); + Assert.Equal("path", inputFile.SourceType); + Assert.NotNull(inputFile.MimeType); + } + + [Fact] + public void FromPath_ExtractsCorrectFilename() + { + // Arrange + var path = "/some/directory/file.jpg"; + + // Act + var inputFile = InputFile.FromPath(path); + + // Assert + Assert.Equal("file.jpg", inputFile.Filename); + } + + [Fact] + public void FromPath_WithWindowsPath_ExtractsCorrectFilename() + { + // Arrange + var path = @"C:\Users\test\document.pdf"; + + // Act + var inputFile = InputFile.FromPath(path); + + // Assert + string expectedFilename = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "document.pdf" : path; + Assert.Equal(expectedFilename, inputFile.Filename); + } + + [Fact] + public void FromFileInfo_WithValidFileInfo_CreatesInputFile() + { + // Arrange + var tempFile = Path.GetTempFileName(); + var fileInfo = new FileInfo(tempFile); + + try + { + // Act + var inputFile = InputFile.FromFileInfo(fileInfo); + + // Assert + Assert.NotNull(inputFile); + Assert.Equal(fileInfo.FullName, inputFile.Path); + Assert.Equal(fileInfo.Name, inputFile.Filename); + Assert.Equal("path", inputFile.SourceType); + } + finally + { + System.IO.File.Delete(tempFile); + } + } + + [Fact] + public void FromStream_WithValidStream_CreatesInputFile() + { + // Arrange + var stream = new MemoryStream(new byte[] { 1, 2, 3 }); + var filename = "test.bin"; + var mimeType = "application/octet-stream"; + + // Act + var inputFile = InputFile.FromStream(stream, filename, mimeType); + + // Assert + Assert.NotNull(inputFile); + Assert.Equal(stream, inputFile.Data); + Assert.Equal(filename, inputFile.Filename); + Assert.Equal(mimeType, inputFile.MimeType); + Assert.Equal("stream", inputFile.SourceType); + } + + [Fact] + public void FromStream_WithCustomMimeType_SetsCorrectMimeType() + { + // Arrange + var stream = new MemoryStream(); + var customMimeType = "image/png"; + + // Act + var inputFile = InputFile.FromStream(stream, "image.png", customMimeType); + + // Assert + Assert.Equal(customMimeType, inputFile.MimeType); + } + + [Fact] + public void FromBytes_WithValidBytes_CreatesInputFile() + { + // Arrange + var bytes = new byte[] { 1, 2, 3, 4, 5 }; + var filename = "data.bin"; + var mimeType = "application/octet-stream"; + + // Act + var inputFile = InputFile.FromBytes(bytes, filename, mimeType); + + // Assert + Assert.NotNull(inputFile); + Assert.Equal(bytes, inputFile.Data); + Assert.Equal(filename, inputFile.Filename); + Assert.Equal(mimeType, inputFile.MimeType); + Assert.Equal("bytes", inputFile.SourceType); + } + + [Fact] + public void FromBytes_WithEmptyBytes_CreatesInputFile() + { + // Arrange + var bytes = new byte[] { }; + var filename = "empty.bin"; + var mimeType = "application/octet-stream"; + + // Act + var inputFile = InputFile.FromBytes(bytes, filename, mimeType); + + // Assert + Assert.NotNull(inputFile); + Assert.Equal(bytes, inputFile.Data); + Assert.Equal(filename, inputFile.Filename); + } + + [Fact] + public void FromBytes_WithImageData_SetsCorrectMimeType() + { + // Arrange + var bytes = new byte[] { 137, 80, 78, 71 }; // PNG header + var mimeType = "image/png"; + + // Act + var inputFile = InputFile.FromBytes(bytes, "image.png", mimeType); + + // Assert + Assert.Equal(mimeType, inputFile.MimeType); + } + + [Fact] + public void SourceType_Path_IsCorrect() + { + var inputFile = InputFile.FromPath("test.txt"); + Assert.Equal("path", inputFile.SourceType); + } + + [Fact] + public void SourceType_Stream_IsCorrect() + { + var inputFile = InputFile.FromStream(new MemoryStream(), "test.txt", "text/plain"); + Assert.Equal("stream", inputFile.SourceType); + } + + [Fact] + public void SourceType_Bytes_IsCorrect() + { + var inputFile = InputFile.FromBytes(new byte[] { 1 }, "test.bin", "application/octet-stream"); + Assert.Equal("bytes", inputFile.SourceType); + } + + [Fact] + public void Properties_CanBeSet() + { + // Arrange & Act + var inputFile = new InputFile + { + Path = "custom/path.txt", + Filename = "custom.txt", + MimeType = "text/plain", + SourceType = "custom", + Data = new object() + }; + + // Assert + Assert.Equal("custom/path.txt", inputFile.Path); + Assert.Equal("custom.txt", inputFile.Filename); + Assert.Equal("text/plain", inputFile.MimeType); + Assert.Equal("custom", inputFile.SourceType); + Assert.NotNull(inputFile.Data); + } + + [Fact] + public void DefaultConstructor_InitializesProperties() + { + // Act + var inputFile = new InputFile(); + + // Assert + Assert.NotNull(inputFile); + Assert.NotNull(inputFile.Path); + Assert.NotNull(inputFile.Filename); + Assert.NotNull(inputFile.MimeType); + Assert.NotNull(inputFile.SourceType); + Assert.NotNull(inputFile.Data); + } + } +} diff --git a/templates/dotnet/Package.Tests/Models/ModelTests.cs.twig b/templates/dotnet/Package.Tests/Models/ModelTests.cs.twig new file mode 100644 index 0000000000..fd2d8ac308 --- /dev/null +++ b/templates/dotnet/Package.Tests/Models/ModelTests.cs.twig @@ -0,0 +1,309 @@ +{% set DefinitionClass = definition.name | caseUcfirst | overrideIdentifier %} +{% macro generate_sub_dict(sub_def) %} +new Dictionary { +{% for subprop in sub_def.properties | filter(p => p.required) %} +{ "{{ subprop.name }}", {% if subprop.enum %}{{ subprop.enumName | caseUcfirst }}.{{ (subprop.enumKeys[0] ?? subprop.enum[0]) | caseEnumKey }}.Value{% elseif subprop.type == 'string' %}"{{ subprop['x-example'] | default('test') | escapeCsString }}"{% elseif subprop.type == 'integer' %}{{ subprop['x-example'] | default(1) }}{% elseif subprop.type == 'number' %}{{ subprop['x-example'] | default(1.0) }}{% elseif subprop.type == 'boolean' %}{% if subprop['x-example'] is defined %}{% if subprop['x-example'] is same as(true) or subprop['x-example'] == 'true' or subprop['x-example'] == 1 %}true{% else %}false{% endif %}{% else %}true{% endif %}{% elseif subprop.sub_schema %}new Dictionary(){% else %}"{{ subprop['x-example'] | default('test') | escapeCsString }}"{% endif %} }{% if not loop.last %},{% endif %} +{% endfor %} +} +{% endmacro %} +using System; +using System.Collections.Generic; +using Xunit; +using {{ spec.title | caseUcfirst }}.Models; +{% if definition.properties | filter(p => p.enum) | length > 0 %} +using {{ spec.title | caseUcfirst }}.Enums; +{% endif %} + +namespace {{ spec.title | caseUcfirst }}.Tests.Models +{ + public class {{ DefinitionClass }}Tests + { + [Fact] + public void Constructor_WithValidParameters_CreatesInstance() + { + // Arrange & Act + var model = new {{ spec.title | caseUcfirst }}.Models.{{ DefinitionClass }}( + {%~ for property in definition.properties %} + {%~ if property.enum %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }} + {%~ elseif property.type == 'string' %} + {{ property.name | caseCamel | escapeKeyword }}: "{{ property['x-example'] | default('test') | escapeCsString }}" + {%~ elseif property.type == 'boolean' %} + {%~ if property['x-example'] is defined %} + {{ property.name | caseCamel | escapeKeyword }}: {% if property['x-example'] is same as(true) or property['x-example'] == 'true' or property['x-example'] == 1 %}true{% else %}false{% endif %} + {%~ else %} + {{ property.name | caseCamel | escapeKeyword }}: true + {%~ endif %} + {%~ elseif property.type == 'integer' %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property['x-example'] | default(1) }} + {%~ elseif property.type == 'number' %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property['x-example'] | default(1.0) }} + {%~ elseif property.type == 'array' %} + {%~ set itemType = test_item_type(property) %} + {{ property.name | caseCamel | escapeKeyword }}: new List<{{ itemType }}>() + {%~ elseif property.type == 'object' and not property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: new Dictionary() + {%~ elseif property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.sub_schema | caseUcfirst }}.From({{ _self.generate_sub_dict(spec.definitions | filter(d => d.name == property.sub_schema) | first) }}) + {%~ else %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property['x-example'] | default('null') | escapeCsString }} + {%~ endif -%} + {%~ if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {%~ endfor %} + {%~ if definition.additionalProperties %} + data: new Dictionary() + {%~ endif %} + ); + + // Assert + Assert.NotNull(model); + {%~ for property in definition.properties %} + {%~ if property.enum %} + Assert.Equal({{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }}.Value, model.{{ property_name(definition, property) | overrideProperty(definition.name) }}.Value); + {%~ elseif property.type == 'string' %} + Assert.Equal("{{ property['x-example'] | default('test') | escapeCsString }}", model.{{ property_name(definition, property) | overrideProperty(definition.name) }}); + {%~ elseif property.type == 'boolean' %} + {%~ if property['x-example'] is defined %} + Assert.Equal({% if property['x-example'] is same as(true) or property['x-example'] == 'true' or property['x-example'] == 1 %}true{% else %}false{% endif %}, model.{{ property_name(definition, property) | overrideProperty(definition.name) }}); + {%~ else %} + Assert.Equal(true, model.{{ property_name(definition, property) | overrideProperty(definition.name) }}); + {%~ endif %} + {%~ elseif property.type == 'integer' or property.type == 'number' %} + Assert.Equal({{ property['x-example'] | default(1) }}, model.{{ property_name(definition, property) | overrideProperty(definition.name) }}); + {%~ elseif property.type == 'array' or (property.type == 'object' and not property.sub_schema) %} + Assert.NotNull(model.{{ property_name(definition, property) | overrideProperty(definition.name) }}); + {%~ endif %} + {%~ endfor %} + } + + [Fact] + public void ToMap_ReturnsCorrectDictionary() + { + // Arrange + var model = new {{ spec.title | caseUcfirst }}.Models.{{ DefinitionClass }}( + {%~ for property in definition.properties %} + {%~ if property.enum %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }} + {%~ elseif property.type == 'string' %} + {{ property.name | caseCamel | escapeKeyword }}: "{{ property['x-example'] | default('test') | escapeCsString }}" + {%~ elseif property.type == 'integer' %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property['x-example'] | default(1) }} + {%~ elseif property.type == 'number' %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property['x-example'] | default(1.0) }} + {%~ elseif property.type == 'boolean' %} + {%~ if property['x-example'] is defined %} + {{ property.name | caseCamel | escapeKeyword }}: {% if property['x-example'] is same as(true) or property['x-example'] == 'true' or property['x-example'] == 1 %}true{% else %}false{% endif %} + {%~ else %} + {{ property.name | caseCamel | escapeKeyword }}: true + {%~ endif %} + {%~ elseif property.type == 'array' %} + {%~ set itemType = test_item_type(property) %} + {{ property.name | caseCamel | escapeKeyword }}: new List<{{ itemType }}>() + {%~ elseif property.type == 'object' and not property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: new Dictionary() + {%~ elseif property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.sub_schema | caseUcfirst }}.From({{ _self.generate_sub_dict(spec.definitions | filter(d => d.name == property.sub_schema) | first) }}) + {%~ else %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property['x-example'] | default('null') }} + {%~ endif %} + {%~ if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {%~ endfor %} + {%~ if definition.additionalProperties %} + data: new Dictionary() + {%~ endif %} + ); + + // Act + var map = model.ToMap(); + + // Assert + Assert.NotNull(map); + {%~ for property in definition.properties %} + Assert.True(map.ContainsKey("{{ property.name }}")); + {%~ endfor %} + } + + [Fact] + public void From_WithValidMap_CreatesInstance() + { + // Arrange + var map = new Dictionary + { + {%~ for property in definition.properties | filter(p => p.required) %} + {%~ if property.enum %} + { "{{ property.name }}", {{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }}.Value } + {%~ elseif property.type == 'string' %} + { "{{ property.name }}", "{{ property['x-example'] | default('test') | escapeCsString }}" } + {%~ elseif property.type == 'integer' %} + { "{{ property.name }}", {{ property['x-example'] | default(1) }} } + {%~ elseif property.type == 'number' %} + { "{{ property.name }}", {{ property['x-example'] | default(1.0) }} } + {%~ elseif property.type == 'boolean' %} + {%~ if property['x-example'] is defined %} + { "{{ property.name }}", {% if property['x-example'] is same as(true) or property['x-example'] == 'true' or property['x-example'] == 1 %}true{% else %}false{% endif %} } + {%~ else %} + { "{{ property.name }}", true } + {%~ endif %} + {%~ elseif property.type == 'array' %} + { "{{ property.name }}", new List() } + {%~ elseif property.type == 'object' and not property.sub_schema %} + { "{{ property.name }}", new Dictionary() } + {%~ elseif property.sub_schema %} + { "{{ property.name }}", {{ _self.generate_sub_dict(spec.definitions | filter(d => d.name == property.sub_schema) | first) }} } + {%~ else %} + { "{{ property.name }}", "{{ property['x-example'] | default('test') | escapeCsString }}" } + {%~ endif %} + {%~ if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {%~ endfor %} + }; + + // Act + var model = {{ spec.title | caseUcfirst }}.Models.{{ DefinitionClass }}.From(map); + + // Assert + Assert.NotNull(model); + {%~ for property in definition.properties | filter(p => p.required) %} + Assert.NotNull(model.{{ property_name(definition, property) | overrideProperty(definition.name) }}); + {%~ endfor %} + } + + [Fact] + public void ToMap_AndFrom_RoundTrip_PreservesData() + { + // Arrange + var original = new {{ spec.title | caseUcfirst }}.Models.{{ DefinitionClass }}( + {%~ for property in definition.properties %} + {%~ if property.enum %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }} + {%~ elseif property.type == 'string' %} + {{ property.name | caseCamel | escapeKeyword }}: "{{ property['x-example'] | default('test') | escapeCsString }}" + {%~ elseif property.type == 'integer' %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property['x-example'] | default(1) }} + {%~ elseif property.type == 'number' %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property['x-example'] | default(1.0) }} + {%~ elseif property.type == 'boolean' %} + {%~ if property['x-example'] is defined %} + {{ property.name | caseCamel | escapeKeyword }}: {% if property['x-example'] is same as(true) or property['x-example'] == 'true' or property['x-example'] == 1 %}true{% else %}false{% endif %} + {%~ else %} + {{ property.name | caseCamel | escapeKeyword }}: true + {%~ endif %} + {%~ elseif property.type == 'array' %} + {%~ set itemType = test_item_type(property) %} + {{ property.name | caseCamel | escapeKeyword }}: new List<{{ itemType }}>() + {%~ elseif property.type == 'object' and not property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: new Dictionary() + {%~ elseif property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.sub_schema | caseUcfirst }}.From({{ _self.generate_sub_dict(spec.definitions | filter(d => d.name == property.sub_schema) | first) }}) + {%~ else %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property['x-example'] | default('null') }} + {%~ endif %} + {%~ if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {%~ endfor %} + {%~ if definition.additionalProperties %} + data: new Dictionary() + {%~ endif %} + ); + + // Act + var map = original.ToMap(); + var result = {{ spec.title | caseUcfirst }}.Models.{{ DefinitionClass }}.From(map); + + // Assert + {%~ for property in definition.properties | filter(p => p.required) %} + {%~ if property.enum %} + Assert.Equal(original.{{ property_name(definition, property) | overrideProperty(definition.name) }}.Value, result.{{ property_name(definition, property) | overrideProperty(definition.name) }}.Value); + {%~ elseif property.type == 'string' or property.type == 'integer' or property.type == 'number' or property.type == 'boolean' %} + Assert.Equal(original.{{ property_name(definition, property) | overrideProperty(definition.name) }}, result.{{ property_name(definition, property) | overrideProperty(definition.name) }}); + {%~ endif %} + {%~ endfor %} + } + {%~ if definition.additionalProperties %} + + [Fact] + public void ConvertTo_WithValidFunction_ConvertsCorrectly() + { + // Arrange + var data = new Dictionary + { + { "customKey", "customValue" } + }; + var model = new {{ spec.title | caseUcfirst }}.Models.{{ DefinitionClass }}( + {%~ for property in definition.properties %} + {%~ if property.type == 'string' %} + {{ property.name | caseCamel | escapeKeyword }}: "test" + {%~ elseif property.type == 'integer' %} + {{ property.name | caseCamel | escapeKeyword }}: 1 + {%~ elseif property.type == 'number' %} + {{ property.name | caseCamel | escapeKeyword }}: 1.0 + {%~ elseif property.type == 'boolean' %} + {{ property.name | caseCamel | escapeKeyword }}: true + {%~ elseif property.type == 'array' %} + {%~ set itemType = test_item_type(property) %} + {{ property.name | caseCamel | escapeKeyword }}: new List<{{ itemType }}>() + {%~ elseif property.type == 'object' and not property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: new Dictionary() + {%~ elseif property.enum %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }} + {%~ elseif property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.sub_schema | caseUcfirst }}.From({{ _self.generate_sub_dict(spec.definitions | filter(d => d.name == property.sub_schema) | first) }}) + {%~ else %} + {{ property.name | caseCamel | escapeKeyword }}: null + {%~ endif %} + {%~ if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {%~ endfor %} + {%~ if definition.additionalProperties %} + data: data + {%~ endif %} + ); + + // Act + var result = model.ConvertTo(d => d["customKey"].ToString()); + + // Assert + Assert.Equal("customValue", result); + } + {%~ endif %} + + [Fact] + public void Properties_AreReadOnly() + { + // Arrange + var model = new {{ spec.title | caseUcfirst }}.Models.{{ DefinitionClass }}( + {%~ for property in definition.properties %} + {%~ if property.enum %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }} + {%~ elseif property.type == 'string' %} + {{ property.name | caseCamel | escapeKeyword }}: "test" + {%~ elseif property.type == 'integer' %} + {{ property.name | caseCamel | escapeKeyword }}: 1 + {%~ elseif property.type == 'number' %} + {{ property.name | caseCamel | escapeKeyword }}: 1.0 + {%~ elseif property.type == 'boolean' %} + {{ property.name | caseCamel | escapeKeyword }}: true + {%~ elseif property.type == 'array' %} + {%~ set itemType = test_item_type(property) %} + {{ property.name | caseCamel | escapeKeyword }}: new List<{{ itemType }}>() + {%~ elseif property.type == 'object' and not property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: new Dictionary() + {%~ elseif property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.sub_schema | caseUcfirst }}.From({{ _self.generate_sub_dict(spec.definitions | filter(d => d.name == property.sub_schema) | first) }}) + {%~ else %} + {{ property.name | caseCamel | escapeKeyword }}: null + {%~ endif %} + {%~ if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {%~ endfor %} + {%~ if definition.additionalProperties %} + data: new Dictionary() + {%~ endif %} + ); + + // Assert - properties should have private setters + {%~ for property in definition.properties | slice(0, 1) %} + var propertyInfo = typeof({{ spec.title | caseUcfirst }}.Models.{{ DefinitionClass }}).GetProperty("{{ property_name(definition, property) | overrideProperty(definition.name) }}"); + Assert.NotNull(propertyInfo); + Assert.Null(propertyInfo.GetSetMethod()); + {%~ endfor %} + } + } +} + diff --git a/templates/dotnet/Package.Tests/OperatorTests.cs.twig b/templates/dotnet/Package.Tests/OperatorTests.cs.twig new file mode 100644 index 0000000000..8cba20d45e --- /dev/null +++ b/templates/dotnet/Package.Tests/OperatorTests.cs.twig @@ -0,0 +1,335 @@ +using System.Collections.Generic; +using System.Text.Json; +using Xunit; + +namespace {{ spec.title | caseUcfirst }}.Tests +{ + public class OperatorTests + { + [Fact] + public void Increment_ReturnsCorrectOperator() + { + var result = Operator.Increment(1); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("increment", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal(1, ((JsonElement)op.Values[0]).GetInt32()); + } + + [Fact] + public void Increment_WithMax_ReturnsCorrectOperator() + { + var result = Operator.Increment(5, 100); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("increment", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + Assert.Equal(5, ((JsonElement)op.Values[0]).GetInt32()); + Assert.Equal(100, ((JsonElement)op.Values[1]).GetInt32()); + } + + [Fact] + public void Decrement_ReturnsCorrectOperator() + { + var result = Operator.Decrement(1); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("decrement", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal(1, ((JsonElement)op.Values[0]).GetInt32()); + } + + [Fact] + public void Decrement_WithMin_ReturnsCorrectOperator() + { + var result = Operator.Decrement(3, 0); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("decrement", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + Assert.Equal(3, ((JsonElement)op.Values[0]).GetInt32()); + Assert.Equal(0, ((JsonElement)op.Values[1]).GetInt32()); + } + + [Fact] + public void Multiply_ReturnsCorrectOperator() + { + var result = Operator.Multiply(2); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("multiply", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal(2, ((JsonElement)op.Values[0]).GetInt32()); + } + + [Fact] + public void Multiply_WithMax_ReturnsCorrectOperator() + { + var result = Operator.Multiply(3, 1000); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("multiply", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + Assert.Equal(3, ((JsonElement)op.Values[0]).GetInt32()); + Assert.Equal(1000, ((JsonElement)op.Values[1]).GetInt32()); + } + + [Fact] + public void Divide_ReturnsCorrectOperator() + { + var result = Operator.Divide(2); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("divide", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal(2, ((JsonElement)op.Values[0]).GetInt32()); + } + + [Fact] + public void Divide_WithMin_ReturnsCorrectOperator() + { + var result = Operator.Divide(4, 1); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("divide", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + Assert.Equal(4, ((JsonElement)op.Values[0]).GetInt32()); + Assert.Equal(1, ((JsonElement)op.Values[1]).GetInt32()); + } + + [Fact] + public void Modulo_ReturnsCorrectOperator() + { + var result = Operator.Modulo(5); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("modulo", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal(5, ((JsonElement)op.Values[0]).GetInt32()); + } + + [Fact] + public void Power_ReturnsCorrectOperator() + { + var result = Operator.Power(2); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("power", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal(2, ((JsonElement)op.Values[0]).GetInt32()); + } + + [Fact] + public void Power_WithMax_ReturnsCorrectOperator() + { + var result = Operator.Power(3, 100); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("power", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + Assert.Equal(3, ((JsonElement)op.Values[0]).GetInt32()); + Assert.Equal(100, ((JsonElement)op.Values[1]).GetInt32()); + } + + [Fact] + public void ArrayAppend_ReturnsCorrectOperator() + { + var result = Operator.ArrayAppend(new List() { "item1", "item2" }); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("arrayAppend", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + } + + [Fact] + public void ArrayPrepend_ReturnsCorrectOperator() + { + var result = Operator.ArrayPrepend(new List() { "first", "second" }); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("arrayPrepend", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + } + + [Fact] + public void ArrayInsert_ReturnsCorrectOperator() + { + var result = Operator.ArrayInsert(0, "newItem"); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("arrayInsert", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + Assert.Equal(0, ((JsonElement)op.Values[0]).GetInt32()); + Assert.Equal("newItem", op.Values[1].ToString()); + } + + [Fact] + public void ArrayRemove_ReturnsCorrectOperator() + { + var result = Operator.ArrayRemove("oldItem"); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("arrayRemove", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal("oldItem", op.Values[0].ToString()); + } + + [Fact] + public void ArrayUnique_ReturnsCorrectOperator() + { + var result = Operator.ArrayUnique(); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("arrayUnique", op.Method); + Assert.NotNull(op.Values); + Assert.Empty(op.Values); + } + + [Fact] + public void ArrayIntersect_ReturnsCorrectOperator() + { + var result = Operator.ArrayIntersect(new List() { "a", "b", "c" }); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("arrayIntersect", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(3, op.Values.Count); + } + + [Fact] + public void ArrayDiff_ReturnsCorrectOperator() + { + var result = Operator.ArrayDiff(new List() { "x", "y" }); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("arrayDiff", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + } + + [Fact] + public void ArrayFilter_ReturnsCorrectOperator() + { + var result = Operator.ArrayFilter(Condition.Equal, "test"); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("arrayFilter", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + Assert.Equal("equal", op.Values[0].ToString()); + Assert.Equal("test", op.Values[1].ToString()); + } + + [Fact] + public void StringConcat_ReturnsCorrectOperator() + { + var result = Operator.StringConcat("suffix"); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("stringConcat", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal("suffix", op.Values[0].ToString()); + } + + [Fact] + public void StringReplace_ReturnsCorrectOperator() + { + var result = Operator.StringReplace("old", "new"); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("stringReplace", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + Assert.Equal("old", op.Values[0].ToString()); + Assert.Equal("new", op.Values[1].ToString()); + } + + [Fact] + public void Toggle_ReturnsCorrectOperator() + { + var result = Operator.Toggle(); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("toggle", op.Method); + Assert.NotNull(op.Values); + Assert.Empty(op.Values); + } + + [Fact] + public void DateAddDays_ReturnsCorrectOperator() + { + var result = Operator.DateAddDays(7); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("dateAddDays", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal(7, ((JsonElement)op.Values[0]).GetInt32()); + } + + [Fact] + public void DateSubDays_ReturnsCorrectOperator() + { + var result = Operator.DateSubDays(3); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("dateSubDays", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal(3, ((JsonElement)op.Values[0]).GetInt32()); + } + + [Fact] + public void DateSetNow_ReturnsCorrectOperator() + { + var result = Operator.DateSetNow(); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("dateSetNow", op.Method); + Assert.NotNull(op.Values); + Assert.Empty(op.Values); + } + } +} diff --git a/templates/dotnet/Package.Tests/PermissionTests.cs.twig b/templates/dotnet/Package.Tests/PermissionTests.cs.twig new file mode 100644 index 0000000000..b58ffea8fa --- /dev/null +++ b/templates/dotnet/Package.Tests/PermissionTests.cs.twig @@ -0,0 +1,78 @@ +using Xunit; +using {{ spec.title | caseUcfirst }}; + +namespace {{ spec.title | caseUcfirst }}.Tests +{ + public class PermissionTests + { + [Fact] + public void Read_ReturnsCorrectPermission() + { + var result = Permission.Read(Role.Any()); + Assert.Equal("read(\"any\")", result); + } + + [Fact] + public void Write_ReturnsCorrectPermission() + { + var result = Permission.Write(Role.Any()); + Assert.Equal("write(\"any\")", result); + } + + [Fact] + public void Create_ReturnsCorrectPermission() + { + var result = Permission.Create(Role.Any()); + Assert.Equal("create(\"any\")", result); + } + + [Fact] + public void Update_ReturnsCorrectPermission() + { + var result = Permission.Update(Role.Any()); + Assert.Equal("update(\"any\")", result); + } + + [Fact] + public void Delete_ReturnsCorrectPermission() + { + var result = Permission.Delete(Role.Any()); + Assert.Equal("delete(\"any\")", result); + } + + [Fact] + public void Read_WithUserRole_ReturnsCorrectFormat() + { + var result = Permission.Read(Role.User("123")); + Assert.Equal("read(\"user:123\")", result); + } + + [Fact] + public void Write_WithTeamRole_ReturnsCorrectFormat() + { + var result = Permission.Write(Role.Team("team123", "owner")); + Assert.Equal("write(\"team:team123/owner\")", result); + } + + [Fact] + public void Create_WithGuestsRole_ReturnsCorrectFormat() + { + var result = Permission.Create(Role.Guests()); + Assert.Equal("create(\"guests\")", result); + } + + [Fact] + public void Update_WithLabelRole_ReturnsCorrectFormat() + { + var result = Permission.Update(Role.Label("admin")); + Assert.Equal("update(\"label:admin\")", result); + } + + [Fact] + public void Delete_WithMemberRole_ReturnsCorrectFormat() + { + var result = Permission.Delete(Role.Member("member123")); + Assert.Equal("delete(\"member:member123\")", result); + } + } +} diff --git a/templates/dotnet/Package.Tests/QueryTests.cs.twig b/templates/dotnet/Package.Tests/QueryTests.cs.twig new file mode 100644 index 0000000000..64a6c0bc9c --- /dev/null +++ b/templates/dotnet/Package.Tests/QueryTests.cs.twig @@ -0,0 +1,580 @@ +using System.Collections.Generic; +using System.Text.Json; +using Xunit; + +namespace {{ spec.title | caseUcfirst }}.Tests +{ + public class QueryTests + { + [Fact] + public void Equal_WithString_ReturnsCorrectQuery() + { + var result = Query.Equal("attr", "value"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("equal", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("value", query.Values[0].ToString()); + } + + [Fact] + public void Equal_WithInteger_ReturnsCorrectQuery() + { + var result = Query.Equal("attr", 1); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("equal", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal(1, ((JsonElement)query.Values[0]).GetInt32()); + } + + [Fact] + public void Equal_WithDouble_ReturnsCorrectQuery() + { + var result = Query.Equal("attr", 1.5); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("equal", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal(1.5, ((JsonElement)query.Values[0]).GetDouble()); + } + + [Fact] + public void Equal_WithBoolean_ReturnsCorrectQuery() + { + var result = Query.Equal("attr", true); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("equal", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.True(((JsonElement)query.Values[0]).GetBoolean()); + } + + [Fact] + public void Equal_WithList_ReturnsCorrectQuery() + { + var result = Query.Equal("attr", new[] { "a", "b", "c" }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("equal", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(3, query.Values.Count); + } + + [Fact] + public void NotEqual_WithString_ReturnsCorrectQuery() + { + var result = Query.NotEqual("attr", "value"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notEqual", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void LessThan_WithInteger_ReturnsCorrectQuery() + { + var result = Query.LessThan("attr", 10); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("lessThan", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void LessThanEqual_WithInteger_ReturnsCorrectQuery() + { + var result = Query.LessThanEqual("attr", 10); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("lessThanEqual", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void GreaterThan_WithInteger_ReturnsCorrectQuery() + { + var result = Query.GreaterThan("attr", 5); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("greaterThan", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void GreaterThanEqual_WithInteger_ReturnsCorrectQuery() + { + var result = Query.GreaterThanEqual("attr", 5); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("greaterThanEqual", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void Search_ReturnsCorrectQuery() + { + var result = Query.Search("attr", "keyword1 keyword2"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("search", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("keyword1 keyword2", query.Values[0].ToString()); + } + + [Fact] + public void IsNull_ReturnsCorrectQuery() + { + var result = Query.IsNull("attr"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("isNull", query.Method); + Assert.Equal("attr", query.Attribute); + } + + [Fact] + public void IsNotNull_ReturnsCorrectQuery() + { + var result = Query.IsNotNull("attr"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("isNotNull", query.Method); + Assert.Equal("attr", query.Attribute); + } + + [Fact] + public void Between_WithIntegers_ReturnsCorrectQuery() + { + var result = Query.Between("attr", 1, 10); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("between", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + } + + [Fact] + public void Between_WithDoubles_ReturnsCorrectQuery() + { + var result = Query.Between("attr", 1.5, 10.5); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("between", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + } + + [Fact] + public void Between_WithStrings_ReturnsCorrectQuery() + { + var result = Query.Between("attr", "a", "z"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("between", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + } + + [Fact] + public void StartsWith_ReturnsCorrectQuery() + { + var result = Query.StartsWith("attr", "prefix"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("startsWith", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("prefix", query.Values[0].ToString()); + } + + [Fact] + public void EndsWith_ReturnsCorrectQuery() + { + var result = Query.EndsWith("attr", "suffix"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("endsWith", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("suffix", query.Values[0].ToString()); + } + + [Fact] + public void Select_WithSingleAttribute_ReturnsCorrectQuery() + { + var result = Query.Select(new List() { "attr1" }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("select", query.Method); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + } + + [Fact] + public void Select_WithMultipleAttributes_ReturnsCorrectQuery() + { + var result = Query.Select(new List() { "attr1", "attr2", "attr3" }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("select", query.Method); + Assert.NotNull(query.Values); + Assert.Equal(3, query.Values.Count); + } + + [Fact] + public void OrderAsc_ReturnsCorrectQuery() + { + var result = Query.OrderAsc("attr"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("orderAsc", query.Method); + Assert.Equal("attr", query.Attribute); + } + + [Fact] + public void OrderDesc_ReturnsCorrectQuery() + { + var result = Query.OrderDesc("attr"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("orderDesc", query.Method); + Assert.Equal("attr", query.Attribute); + } + + [Fact] + public void CursorAfter_ReturnsCorrectQuery() + { + var result = Query.CursorAfter("documentId"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("cursorAfter", query.Method); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("documentId", query.Values[0].ToString()); + } + + [Fact] + public void CursorBefore_ReturnsCorrectQuery() + { + var result = Query.CursorBefore("documentId"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("cursorBefore", query.Method); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("documentId", query.Values[0].ToString()); + } + + [Fact] + public void Limit_ReturnsCorrectQuery() + { + var result = Query.Limit(25); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("limit", query.Method); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal(25, ((JsonElement)query.Values[0]).GetInt32()); + } + + [Fact] + public void Offset_ReturnsCorrectQuery() + { + var result = Query.Offset(10); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("offset", query.Method); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal(10, ((JsonElement)query.Values[0]).GetInt32()); + } + + [Fact] + public void Contains_ReturnsCorrectQuery() + { + var result = Query.Contains("attr", "value"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("contains", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("value", query.Values[0].ToString()); + } + + [Fact] + public void Or_WithMultipleQueries_ReturnsCorrectQuery() + { + var result = Query.Or(new List() { + Query.Equal("attr1", "value1"), + Query.Equal("attr2", "value2") + }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("or", query.Method); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + } + + [Fact] + public void And_WithMultipleQueries_ReturnsCorrectQuery() + { + var result = Query.And(new List() { + Query.Equal("attr1", "value1"), + Query.Equal("attr2", "value2") + }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("and", query.Method); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + } + + [Fact] + public void NotContains_ReturnsCorrectQuery() + { + var result = Query.NotContains("attr", "value"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notContains", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("value", query.Values[0].ToString()); + } + + [Fact] + public void NotSearch_ReturnsCorrectQuery() + { + var result = Query.NotSearch("attr", "keyword1 keyword2"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notSearch", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("keyword1 keyword2", query.Values[0].ToString()); + } + + [Fact] + public void NotBetween_WithIntegers_ReturnsCorrectQuery() + { + var result = Query.NotBetween("attr", 1, 2); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notBetween", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + Assert.Equal(1, ((JsonElement)query.Values[0]).GetInt32()); + Assert.Equal(2, ((JsonElement)query.Values[1]).GetInt32()); + } + + [Fact] + public void NotBetween_WithDoubles_ReturnsCorrectQuery() + { + var result = Query.NotBetween("attr", 1.0, 2.0); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notBetween", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + Assert.Equal(1.0, ((JsonElement)query.Values[0]).GetDouble()); + Assert.Equal(2.0, ((JsonElement)query.Values[1]).GetDouble()); + } + + [Fact] + public void NotBetween_WithStrings_ReturnsCorrectQuery() + { + var result = Query.NotBetween("attr", "a", "z"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notBetween", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + Assert.Equal("a", query.Values[0].ToString()); + Assert.Equal("z", query.Values[1].ToString()); + } + + [Fact] + public void NotStartsWith_ReturnsCorrectQuery() + { + var result = Query.NotStartsWith("attr", "prefix"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notStartsWith", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("prefix", query.Values[0].ToString()); + } + + [Fact] + public void NotEndsWith_ReturnsCorrectQuery() + { + var result = Query.NotEndsWith("attr", "suffix"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notEndsWith", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("suffix", query.Values[0].ToString()); + } + + [Fact] + public void CreatedBefore_ReturnsCorrectQuery() + { + var result = Query.CreatedBefore("2023-01-01"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("lessThan", query.Method); + Assert.Equal("$createdAt", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("2023-01-01", query.Values[0].ToString()); + } + + [Fact] + public void CreatedAfter_ReturnsCorrectQuery() + { + var result = Query.CreatedAfter("2023-01-01"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("greaterThan", query.Method); + Assert.Equal("$createdAt", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("2023-01-01", query.Values[0].ToString()); + } + + [Fact] + public void CreatedBetween_ReturnsCorrectQuery() + { + var result = Query.CreatedBetween("2023-01-01", "2023-12-31"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("between", query.Method); + Assert.Equal("$createdAt", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + Assert.Equal("2023-01-01", query.Values[0].ToString()); + Assert.Equal("2023-12-31", query.Values[1].ToString()); + } + + [Fact] + public void UpdatedBefore_ReturnsCorrectQuery() + { + var result = Query.UpdatedBefore("2023-01-01"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("lessThan", query.Method); + Assert.Equal("$updatedAt", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("2023-01-01", query.Values[0].ToString()); + } + + [Fact] + public void UpdatedAfter_ReturnsCorrectQuery() + { + var result = Query.UpdatedAfter("2023-01-01"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("greaterThan", query.Method); + Assert.Equal("$updatedAt", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("2023-01-01", query.Values[0].ToString()); + } + + [Fact] + public void UpdatedBetween_ReturnsCorrectQuery() + { + var result = Query.UpdatedBetween("2023-01-01", "2023-12-31"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("between", query.Method); + Assert.Equal("$updatedAt", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + Assert.Equal("2023-01-01", query.Values[0].ToString()); + Assert.Equal("2023-12-31", query.Values[1].ToString()); + } + + [Fact] + public void OrderRandom_ReturnsCorrectQuery() + { + var result = Query.OrderRandom(); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("orderRandom", query.Method); + } + } +} diff --git a/templates/dotnet/Package.Tests/RoleTests.cs.twig b/templates/dotnet/Package.Tests/RoleTests.cs.twig new file mode 100644 index 0000000000..df11633421 --- /dev/null +++ b/templates/dotnet/Package.Tests/RoleTests.cs.twig @@ -0,0 +1,108 @@ +using Xunit; +using {{ spec.title | caseUcfirst }}; + +namespace {{ spec.title | caseUcfirst }}.Tests +{ + public class RoleTests + { + [Fact] + public void Any_ReturnsCorrectRole() + { + var result = Role.Any(); + Assert.Equal("any", result); + } + + [Fact] + public void User_WithoutStatus_ReturnsCorrectRole() + { + var result = Role.User("custom"); + Assert.Equal("user:custom", result); + } + + [Fact] + public void User_WithStatus_ReturnsCorrectRole() + { + var result = Role.User("custom", "verified"); + Assert.Equal("user:custom/verified", result); + } + + [Fact] + public void User_WithUnverifiedStatus_ReturnsCorrectRole() + { + var result = Role.User("user123", "unverified"); + Assert.Equal("user:user123/unverified", result); + } + + [Fact] + public void Users_WithoutStatus_ReturnsCorrectRole() + { + var result = Role.Users(); + Assert.Equal("users", result); + } + + [Fact] + public void Users_WithStatus_ReturnsCorrectRole() + { + var result = Role.Users("verified"); + Assert.Equal("users/verified", result); + } + + [Fact] + public void Users_WithUnverifiedStatus_ReturnsCorrectRole() + { + var result = Role.Users("unverified"); + Assert.Equal("users/unverified", result); + } + + [Fact] + public void Guests_ReturnsCorrectRole() + { + var result = Role.Guests(); + Assert.Equal("guests", result); + } + + [Fact] + public void Team_WithoutRole_ReturnsCorrectRole() + { + var result = Role.Team("custom"); + Assert.Equal("team:custom", result); + } + + [Fact] + public void Team_WithRole_ReturnsCorrectRole() + { + var result = Role.Team("custom", "owner"); + Assert.Equal("team:custom/owner", result); + } + + [Fact] + public void Team_WithMemberRole_ReturnsCorrectRole() + { + var result = Role.Team("team123", "member"); + Assert.Equal("team:team123/member", result); + } + + [Fact] + public void Member_ReturnsCorrectRole() + { + var result = Role.Member("custom"); + Assert.Equal("member:custom", result); + } + + [Fact] + public void Label_ReturnsCorrectRole() + { + var result = Role.Label("admin"); + Assert.Equal("label:admin", result); + } + + [Fact] + public void Label_WithMultipleLabels_ReturnsCorrectRole() + { + var result1 = Role.Label("moderator"); + var result2 = Role.Label("vip"); + Assert.Equal("label:moderator", result1); + Assert.Equal("label:vip", result2); + } + } +} diff --git a/templates/dotnet/Package.Tests/Services/ServiceTests.cs.twig b/templates/dotnet/Package.Tests/Services/ServiceTests.cs.twig new file mode 100644 index 0000000000..c308c064eb --- /dev/null +++ b/templates/dotnet/Package.Tests/Services/ServiceTests.cs.twig @@ -0,0 +1,209 @@ +{% import 'dotnet/base/utils.twig' as utils %} +{% macro generate_sub_dict(sub_def) %} +new Dictionary { +{% for subprop in sub_def.properties | filter(p => p.required) %} +{ "{{ subprop.name }}", {% if subprop.enum %}{{ subprop.enumName | caseUcfirst }}.{{ (subprop.enumKeys[0] ?? subprop.enum[0]) | caseEnumKey }}.Value{% elseif subprop.type == 'string' %}"{{ subprop['x-example'] | default('test') | escapeCsString }}"{% elseif subprop.type == 'integer' %}{{ subprop['x-example'] | default(1) }}{% elseif subprop.type == 'number' %}{{ subprop['x-example'] | default(1.0) }}{% elseif subprop.type == 'boolean' %}{% if subprop['x-example'] is defined %}{% if subprop['x-example'] is same as(true) or subprop['x-example'] == 'true' or subprop['x-example'] == 1 %}true{% else %}false{% endif %}{% else %}true{% endif %}{% elseif subprop.type == 'array' %}new List(){% elseif subprop.sub_schema %}new Dictionary(){% else %}"{{ subprop['x-example'] | default('test') | escapeCsString }}"{% endif %} }{% if not loop.last %},{% endif %} +{% endfor %} +} +{% endmacro %} +#pragma warning disable CS0618 // Type or member is obsolete +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using Moq; +using {{ spec.title | caseUcfirst }}; +using {{ spec.title | caseUcfirst }}.Services; +{% if spec.definitions is not empty %} +using {{ spec.title | caseUcfirst }}.Models; +{% endif %} +{%- set hasEnums = spec.requestEnums is not empty -%} +{%- if hasEnums -%} +using {{ spec.title | caseUcfirst }}.Enums; +{% endif %} + +namespace {{ spec.title | caseUcfirst }}.Tests.Services +{ + public class {{ service.name | caseUcfirst }}Tests + { + private Mock _mockClient; + private {{ spec.title | caseUcfirst }}.Services.{{ service.name | caseUcfirst }} _{{ service.name | caseCamel }}; + + public {{ service.name | caseUcfirst }}Tests() + { + _mockClient = new Mock(); + _{{ service.name | caseCamel }} = new {{ spec.title | caseUcfirst }}.Services.{{ service.name | caseUcfirst }}(_mockClient.Object); + } + + [Fact] + public void Constructor_WithClient_CreatesInstance() + { + // Arrange + var client = new Mock().Object; + + // Act + var service = new {{ spec.title | caseUcfirst }}.Services.{{ service.name | caseUcfirst }}(client); + + // Assert + Assert.NotNull(service); + } + + {%~ for method in service.methods %} + [Fact] + public async Task {{ method.name | caseUcfirst }}_CallsClient() + { + // Arrange + {%~ if method.responseModel and method.responseModel != 'any' %} + var expectedResponse = new Dictionary + { + {%- for definition in spec.definitions ~%}{%~ if definition.name == method.responseModel -%}{%~ for property in definition.properties | filter((param) => param.required) ~%} + { "{{property.name}}", {% if property.enum %}{{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }}.Value{% elseif property.type == 'string' %}"{{property['x-example'] | default('test')}}"{% elseif property.type == 'boolean' %}true{% elseif property.type == 'integer' %}{{property['x-example'] | default(1)}}{% elseif property.type == 'number' %}{{property['x-example'] | default(1.0)}}{% elseif property.type == 'array' %}new List(){% elseif property.type == 'object' and not property.sub_schema %}new Dictionary(){% elseif property.sub_schema %}{{ _self.generate_sub_dict(spec.definitions | filter(d => d.name == property.sub_schema) | first) }}{% else %}null{% endif %} }, + {%~ endfor ~%}{%- endif -%}{%~ endfor -%} + }; + {%~ elseif method.type == 'location' %} + var expectedResponse = new byte[] { 1, 2, 3 }; + {%~ elseif method.type == 'webAuth' %} + var expectedResponse = "success"; + {%~ else %} + var expectedResponse = new Dictionary(); + {%~ endif %} + + {%~ if method.type == 'webAuth' %} + _mockClient.Setup(c => c.Redirect( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny>() + )).ReturnsAsync(expectedResponse); + {%~ elseif 'multipart/form-data' in method.consumes %} + _mockClient.Setup(c => c.ChunkedUpload<{{ utils.resultType(spec.title, method) }}>( + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny, {{ utils.resultType(spec.title, method) }}>>(), + It.IsAny(), + It.IsAny(), + It.IsAny>() + )).ReturnsAsync({% if method.responseModel and method.responseModel != 'any' %}{{ spec.title | caseUcfirst }}.Models.{{ method.responseModel | caseUcfirst | overrideIdentifier }}.From(expectedResponse){% else %}expectedResponse{% endif %}); + {%~ else %} + _mockClient.Setup(c => c.Call<{{ utils.resultType(spec.title, method) }}>( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny>(){% if method.responseModel %}, + It.IsAny, {{ utils.resultType(spec.title, method) }}>>() + {% else %},null{% endif %} + )).ReturnsAsync({% if method.responseModel and method.responseModel != 'any' and method.type != 'location' %}{{ spec.title | caseUcfirst }}.Models.{{ method.responseModel | caseUcfirst | overrideIdentifier }}.From(expectedResponse){% else %}expectedResponse{% endif %}); + {%~ endif %} + + // Act + {%~ if method.parameters.all | length > 0 %} + var result = await _{{ service.name | caseCamel }}.{{ method.name | caseUcfirst }}( + {%~ for parameter in method.parameters.all | filter((param) => param.required) ~%} + {{parameter.name | caseCamel | escapeKeyword}}: {% if parameter.enumValues is not empty %}{{ spec.title | caseUcfirst }}.Enums.{{ (parameter.enumName ?? parameter.name) | caseUcfirst }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% elseif parameter.type == 'file' %}InputFile.FromPath("./test.png"){% elseif parameter.type == 'object' %}new Dictionary(){% elseif parameter.type == 'array' %}{% set itemType = test_item_type(parameter) %}new List<{{ itemType }}> { {% if itemType == 'string' %}"item1"{% elseif itemType == 'long' %}1{% elseif itemType == 'double' %}1.0{% elseif itemType == 'bool' %}true{% elseif itemType == 'object' %}new object(){% else %}null{% endif %} }{% elseif parameter.type == 'boolean' %}true{% elseif parameter.type == 'integer' %}{{parameter['x-example'] | default(1)}}{% elseif parameter.type == 'number' %}{{parameter['x-example'] | default(1.0)}}{% elseif parameter.type == 'string' %}"{% if parameter['x-example'] is not empty %}{{parameter['x-example']}}{% else %}test{% endif %}"{% else %}null{% endif %}{% if not loop.last %},{% endif %} + + {%~ endfor ~%} + ); + {%~ else %} + var result = await _{{ service.name | caseCamel }}.{{ method.name | caseUcfirst }}(); + {%~ endif %} + + // Assert + {%~ if method.responseModel and method.responseModel != 'any' %} + Assert.NotNull(result); + Assert.IsType<{{ spec.title | caseUcfirst }}.Models.{{ method.responseModel | caseUcfirst | overrideIdentifier }}>(result); + {%~ elseif method.type == 'location' %} + Assert.NotNull(result); + {%~ elseif method.type == 'webAuth' %} + Assert.NotNull(result); + Assert.Equal(expectedResponse, result); + {%~ endif %} + + {%~ if method.type == 'webAuth' %} + _mockClient.Verify(c => c.Redirect( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny>() + ), Times.Once); + {%~ elseif 'multipart/form-data' in method.consumes %} + _mockClient.Verify(c => c.ChunkedUpload<{{ utils.resultType(spec.title, method) }}>( + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny, {{ utils.resultType(spec.title, method) }}>>(), + It.IsAny(), + It.IsAny(), + It.IsAny>() + ), Times.Once); + {%~ else %} + _mockClient.Verify(c => c.Call<{{ utils.resultType(spec.title, method) }}>( + "{{ method.method | upper }}", + It.IsAny(), + It.IsAny>(), + It.IsAny>(){% if method.responseModel %}, + It.IsAny, {{ utils.resultType(spec.title, method) }}>>() + {% else %},null{% endif %} + ), Times.Once); + {%~ endif %} + } + + {%~ if method.parameters.all | filter((param) => param.required) | length > 0 %} + [Fact] + public async Task {{ method.name | caseUcfirst }}_WithParameters_PassesCorrectParameters() + { + // Arrange + {%~ for parameter in method.parameters.all | filter((param) => param.required) | slice(0, 3) ~%} + {% if parameter.type == 'file' %}InputFile{% else %}var{% endif %} {{parameter.name | caseCamel | escapeKeyword}} = {% if parameter.enumValues is not empty %}{{ spec.title | caseUcfirst }}.Enums.{{ (parameter.enumName ?? parameter.name) | caseUcfirst }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% elseif parameter.type == 'file' %}InputFile.FromPath("./test.png"){% elseif parameter.type == 'object' %}new Dictionary(){% elseif parameter.type == 'array' %}{% set itemType = test_item_type(parameter) %}new List<{{ itemType }}> { {% if itemType == 'string' %}"item1"{% elseif itemType == 'long' %}1{% elseif itemType == 'double' %}1.0{% elseif itemType == 'bool' %}true{% elseif itemType == 'object' %}new object(){% else %}null{% endif %} }{% elseif parameter.type == 'boolean' %}true{% elseif parameter.type == 'integer' or parameter.type == 'number' %}{{parameter['x-example'] | default(123)}}{% elseif parameter.type == 'string' %}"test{{parameter.name}}"{% else %}null{% endif %}; + {%~ endfor ~%} + + {%~ if method.responseModel and method.responseModel != 'any' %} + var expectedResponse = new Dictionary + { + {%- for definition in spec.definitions ~%}{%~ if definition.name == method.responseModel -%}{%~ for property in definition.properties | filter((param) => param.required) ~%} + { "{{property.name}}", {% if property.enum %}{{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }}.Value{% elseif property.type == 'string' %}"test"{% elseif property.type == 'integer' or property.type == 'number'%}1{% elseif property.type == 'boolean' %}true{% elseif property.type == 'array' %}new List(){% elseif property.type == 'object' and not property.sub_schema %}new Dictionary(){% elseif property.sub_schema %}{{ _self.generate_sub_dict(spec.definitions | filter(d => d.name == property.sub_schema) | first) }}{% else %}new Dictionary(){% endif %} }, + {%~ endfor ~%}{%- endif -%}{%~ endfor -%} + }; + {%~ elseif method.type == 'location' %} + var expectedResponse = new byte[] { 1, 2, 3 }; + {%~ else %} + var expectedResponse = new Dictionary(); + {%~ endif %} + + {%~ if not (method.type == 'webAuth' or 'multipart/form-data' in method.consumes) %} + _mockClient.Setup(c => c.Call<{{ utils.resultType(spec.title, method) }}>( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny>(){% if method.responseModel %}, + It.IsAny, {{ utils.resultType(spec.title, method) }}>>() + {% else %},null{% endif %} + )).ReturnsAsync({% if method.responseModel and method.responseModel != 'any' and method.type != 'location' %}{{ spec.title | caseUcfirst }}.Models.{{ method.responseModel | caseUcfirst | overrideIdentifier }}.From(expectedResponse){% else %}expectedResponse{% endif %}); + {%~ endif %} + + // Act + {%~ if method.parameters.all | length > 0 %} + await _{{ service.name | caseCamel }}.{{ method.name | caseUcfirst }}( + {%~ for parameter in method.parameters.all | filter((param) => param.required) ~%} + {{parameter.name | caseCamel | escapeKeyword}}: {% if loop.index0 < 3 %}{{parameter.name | caseCamel | escapeKeyword}}{% else %}{% if parameter.enumValues is not empty %}{{ spec.title | caseUcfirst }}.Enums.{{ (parameter.enumName ?? parameter.name) | caseUcfirst }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% elseif parameter.type == 'file' %}InputFile.FromPath("./test.png"){% elseif parameter.type == 'object' %}new Dictionary(){% elseif parameter.type == 'array' %}{% set itemType = test_item_type(parameter) %}new List<{{ itemType }}> { {% if itemType == 'string' %}"item1"{% elseif itemType == 'long' %}1{% elseif itemType == 'double' %}1.0{% elseif itemType == 'bool' %}true{% elseif itemType == 'object' %}new object(){% else %}null{% endif %} }{% elseif parameter.type == 'boolean' %}true{% elseif parameter.type == 'integer' or parameter.type == 'number' %}1{% elseif parameter.type == 'string' %}"test"{% else %}null{% endif %}{% endif %}{% if not loop.last %},{% endif %} + + {%~ endfor ~%} + ); + {%~ endif %} + + // Assert - parameters were set correctly (implicitly tested by successful call) + Assert.True(true); + } + {%~ endif %} + + {%~ endfor %} + + [Fact] + public void Service_InheritsFromBaseService() + { + // Assert + Assert.IsAssignableFrom(_{{ service.name | caseCamel }}); + } + } +} diff --git a/templates/dotnet/Package.Tests/Tests.csproj.twig b/templates/dotnet/Package.Tests/Tests.csproj.twig new file mode 100644 index 0000000000..85145bb28e --- /dev/null +++ b/templates/dotnet/Package.Tests/Tests.csproj.twig @@ -0,0 +1,28 @@ + + + + net8.0 + {{ spec.title | caseUcfirst }}.Tests + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/templates/dotnet/Package.Tests/UploadProgressTests.cs.twig b/templates/dotnet/Package.Tests/UploadProgressTests.cs.twig new file mode 100644 index 0000000000..5d4de2c1f9 --- /dev/null +++ b/templates/dotnet/Package.Tests/UploadProgressTests.cs.twig @@ -0,0 +1,166 @@ +using Xunit; +using {{ spec.title | caseUcfirst }}; + +namespace {{ spec.title | caseUcfirst }}.Tests +{ + public class UploadProgressTests + { + [Fact] + public void Constructor_WithValidParameters_CreatesInstance() + { + // Arrange + var id = "test-id"; + var progress = 50.0; + var sizeUploaded = 1024L; + var chunksTotal = 10L; + var chunksUploaded = 5L; + + // Act + var uploadProgress = new UploadProgress(id, progress, sizeUploaded, chunksTotal, chunksUploaded); + + // Assert + Assert.NotNull(uploadProgress); + Assert.Equal(id, uploadProgress.Id); + Assert.Equal(progress, uploadProgress.Progress); + Assert.Equal(sizeUploaded, uploadProgress.SizeUploaded); + Assert.Equal(chunksTotal, uploadProgress.ChunksTotal); + Assert.Equal(chunksUploaded, uploadProgress.ChunksUploaded); + } + + [Fact] + public void Constructor_WithZeroProgress_CreatesInstance() + { + // Arrange & Act + var uploadProgress = new UploadProgress("id", 0.0, 0L, 10L, 0L); + + // Assert + Assert.Equal(0.0, uploadProgress.Progress); + Assert.Equal(0L, uploadProgress.SizeUploaded); + Assert.Equal(0L, uploadProgress.ChunksUploaded); + } + + [Fact] + public void Constructor_WithCompleteProgress_CreatesInstance() + { + // Arrange & Act + var uploadProgress = new UploadProgress("id", 100.0, 5120L, 5L, 5L); + + // Assert + Assert.Equal(100.0, uploadProgress.Progress); + Assert.Equal(5L, uploadProgress.ChunksTotal); + Assert.Equal(5L, uploadProgress.ChunksUploaded); + } + + [Fact] + public void Id_IsReadOnly() + { + // Arrange + var uploadProgress = new UploadProgress("test-id", 50.0, 1024L, 10L, 5L); + + // Assert + var propertyInfo = typeof(UploadProgress).GetProperty("Id"); + Assert.NotNull(propertyInfo); + Assert.Null(propertyInfo.GetSetMethod()); + } + + [Fact] + public void Progress_IsReadOnly() + { + // Arrange + var uploadProgress = new UploadProgress("test-id", 50.0, 1024L, 10L, 5L); + + // Assert + var propertyInfo = typeof(UploadProgress).GetProperty("Progress"); + Assert.NotNull(propertyInfo); + Assert.Null(propertyInfo.GetSetMethod()); + } + + [Fact] + public void SizeUploaded_IsReadOnly() + { + // Arrange + var uploadProgress = new UploadProgress("test-id", 50.0, 1024L, 10L, 5L); + + // Assert + var propertyInfo = typeof(UploadProgress).GetProperty("SizeUploaded"); + Assert.NotNull(propertyInfo); + Assert.Null(propertyInfo.GetSetMethod()); + } + + [Fact] + public void ChunksTotal_IsReadOnly() + { + // Arrange + var uploadProgress = new UploadProgress("test-id", 50.0, 1024L, 10L, 5L); + + // Assert + var propertyInfo = typeof(UploadProgress).GetProperty("ChunksTotal"); + Assert.NotNull(propertyInfo); + Assert.Null(propertyInfo.GetSetMethod()); + } + + [Fact] + public void ChunksUploaded_IsReadOnly() + { + // Arrange + var uploadProgress = new UploadProgress("test-id", 50.0, 1024L, 10L, 5L); + + // Assert + var propertyInfo = typeof(UploadProgress).GetProperty("ChunksUploaded"); + Assert.NotNull(propertyInfo); + Assert.Null(propertyInfo.GetSetMethod()); + } + + [Fact] + public void Progress_WithDecimalValue_StoresCorrectly() + { + // Arrange & Act + var uploadProgress = new UploadProgress("id", 75.5, 3840L, 10L, 7L); + + // Assert + Assert.Equal(75.5, uploadProgress.Progress); + } + + [Fact] + public void SizeUploaded_WithLargeValue_StoresCorrectly() + { + // Arrange + var largeSize = long.MaxValue; + + // Act + var uploadProgress = new UploadProgress("id", 100.0, largeSize, 1000L, 1000L); + + // Assert + Assert.Equal(largeSize, uploadProgress.SizeUploaded); + } + + [Fact] + public void ChunksTotal_MatchesChunksUploaded_WhenComplete() + { + // Arrange & Act + var uploadProgress = new UploadProgress("id", 100.0, 10240L, 10L, 10L); + + // Assert + Assert.Equal(uploadProgress.ChunksTotal, uploadProgress.ChunksUploaded); + } + + [Theory] + [InlineData("id1", 25.0, 256L, 4L, 1L)] + [InlineData("id2", 50.0, 512L, 4L, 2L)] + [InlineData("id3", 75.0, 768L, 4L, 3L)] + [InlineData("id4", 100.0, 1024L, 4L, 4L)] + public void Constructor_WithVariousValues_CreatesCorrectInstance( + string id, double progress, long sizeUploaded, long chunksTotal, long chunksUploaded) + { + // Act + var uploadProgress = new UploadProgress(id, progress, sizeUploaded, chunksTotal, chunksUploaded); + + // Assert + Assert.Equal(id, uploadProgress.Id); + Assert.Equal(progress, uploadProgress.Progress); + Assert.Equal(sizeUploaded, uploadProgress.SizeUploaded); + Assert.Equal(chunksTotal, uploadProgress.ChunksTotal); + Assert.Equal(chunksUploaded, uploadProgress.ChunksUploaded); + } + } +} diff --git a/templates/dotnet/Package.sln b/templates/dotnet/Package.sln index a8e4b4e574..c4ffeb4bde 100644 --- a/templates/dotnet/Package.sln +++ b/templates/dotnet/Package.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.30114.128 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Appwrite", "Appwrite\Appwrite.csproj", "{ABD3EB63-B648-49D6-B7FD-C17A762A3EC3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Appwrite.Tests", "Appwrite.Tests\Appwrite.Tests.csproj", "{B9B36337-BF75-4601-AB9C-C2A7ACC91DF6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,8 @@ Global {ABD3EB63-B648-49D6-B7FD-C17A762A3EC3}.Debug|Any CPU.Build.0 = Debug|Any CPU {ABD3EB63-B648-49D6-B7FD-C17A762A3EC3}.Release|Any CPU.ActiveCfg = Release|Any CPU {ABD3EB63-B648-49D6-B7FD-C17A762A3EC3}.Release|Any CPU.Build.0 = Release|Any CPU + {B9B36337-BF75-4601-AB9C-C2A7ACC91DF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9B36337-BF75-4601-AB9C-C2A7ACC91DF6}.Debug|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/templates/dotnet/Package/Client.cs.twig b/templates/dotnet/Package/Client.cs.twig index 8f95277902..cc624a7368 100644 --- a/templates/dotnet/Package/Client.cs.twig +++ b/templates/dotnet/Package/Client.cs.twig @@ -88,6 +88,27 @@ namespace {{ spec.title | caseUcfirst }} } } + // Parameterless constructor required for mocking frameworks (Moq/Castle) + // Initializes minimal defaults so proxies can be created without errors. + protected Client() + { + _endpoint = "{{spec.endpoint}}"; + _http = new HttpClient(); + _httpForRedirect = new HttpClient(new HttpClientHandler(){ AllowAutoRedirect = false }); + + _headers = new Dictionary() + { + { "content-type", "application/json" }, + { "user-agent" , $"{{spec.title | caseUcfirst}}{{ language.name | caseUcfirst }}SDK/{{ sdk.version }} ({Environment.OSVersion.Platform}; {Environment.OSVersion.VersionString})"}, + { "x-sdk-name", "{{ sdk.name }}" }, + { "x-sdk-platform", "{{ sdk.platform }}" }, + { "x-sdk-language", "{{ language.name | caseLower }}" }, + { "x-sdk-version", "{{ sdk.version }}" } + }; + + _config = new Dictionary(); + } + public Client SetSelfSigned(bool selfSigned) { var handler = new HttpClientHandler() @@ -154,6 +175,7 @@ namespace {{ spec.title | caseUcfirst }} foreach (var parameter in parameters) { + if (parameter.Value == null) continue; if (parameter.Key == "file") { var fileContent = parameters["file"] as MultipartFormDataContent; @@ -222,7 +244,7 @@ namespace {{ spec.title | caseUcfirst }} return request; } - public async Task Redirect( + public virtual async Task Redirect( string method, string path, Dictionary headers, @@ -268,7 +290,7 @@ namespace {{ spec.title | caseUcfirst }} return response.Headers.Location?.OriginalString ?? string.Empty; } - public Task> Call( + public virtual Task> Call( string method, string path, Dictionary headers, @@ -277,7 +299,7 @@ namespace {{ spec.title | caseUcfirst }} return Call>(method, path, headers, parameters); } - public async Task Call( + public virtual async Task Call( string method, string path, Dictionary headers, @@ -293,7 +315,7 @@ namespace {{ spec.title | caseUcfirst }} { foreach (var warning in warnings) { - Console.WriteLine("Warning: " + warning); + Console.Error.WriteLine("Warning: " + warning); } } @@ -352,7 +374,7 @@ namespace {{ spec.title | caseUcfirst }} } } - public async Task ChunkedUpload( + public virtual async Task ChunkedUpload( string path, Dictionary headers, Dictionary parameters, diff --git a/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig b/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig index 563f92992a..ce772c93df 100644 --- a/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig +++ b/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig @@ -7,32 +7,60 @@ namespace {{ spec.title | caseUcfirst }}.Converters { public class ObjectToInferredTypesConverter : JsonConverter { - public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - switch (reader.TokenType) + using (JsonDocument document = JsonDocument.ParseValue(ref reader)) { - case JsonTokenType.True: - return true; - case JsonTokenType.False: - return false; - case JsonTokenType.Number: - if (reader.TryGetInt64(out long l)) + return ConvertElement(document.RootElement); + } + } + + private object? ConvertElement(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + var dictionary = new Dictionary(); + foreach (var property in element.EnumerateObject()) { - return l; + dictionary[property.Name] = ConvertElement(property.Value); + } + return dictionary; + + case JsonValueKind.Array: + var list = new List(); + foreach (var item in element.EnumerateArray()) + { + list.Add(ConvertElement(item)); } - return reader.GetDouble(); - case JsonTokenType.String: - if (reader.TryGetDateTime(out DateTime datetime)) + return list; + + case JsonValueKind.String: + if (element.TryGetDateTime(out DateTime datetime)) { return datetime; } - return reader.GetString()!; - case JsonTokenType.StartObject: - return JsonSerializer.Deserialize>(ref reader, options)!; - case JsonTokenType.StartArray: - return JsonSerializer.Deserialize(ref reader, options)!; + return element.GetString(); + + case JsonValueKind.Number: + if (element.TryGetInt64(out long l)) + { + return l; + } + return element.GetDouble(); + + case JsonValueKind.True: + return true; + + case JsonValueKind.False: + return false; + + case JsonValueKind.Null: + case JsonValueKind.Undefined: + return null; + default: - return JsonDocument.ParseValue(ref reader).RootElement.Clone(); + throw new JsonException($"Unsupported JsonValueKind: {element.ValueKind}"); } } diff --git a/templates/dotnet/Package/Exception.cs.twig b/templates/dotnet/Package/Exception.cs.twig index e78d78c2cc..31d9c70adc 100644 --- a/templates/dotnet/Package/Exception.cs.twig +++ b/templates/dotnet/Package/Exception.cs.twig @@ -18,10 +18,10 @@ namespace {{spec.title | caseUcfirst}} this.Type = type; this.Response = response; } + public {{spec.title | caseUcfirst}}Exception(string message, Exception inner) : base(message, inner) { } } } - diff --git a/templates/dotnet/Package/Extensions/Extensions.cs.twig b/templates/dotnet/Package/Extensions/Extensions.cs.twig index d57318077e..4fe485e1c6 100644 --- a/templates/dotnet/Package/Extensions/Extensions.cs.twig +++ b/templates/dotnet/Package/Extensions/Extensions.cs.twig @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Text.Json; namespace {{ spec.title | caseUcfirst }}.Extensions @@ -12,6 +13,17 @@ namespace {{ spec.title | caseUcfirst }}.Extensions return JsonSerializer.Serialize(dict, Client.SerializerOptions); } + public static IEnumerable ToEnumerable(this object value) + { + return value switch + { + object[] array => array, + IEnumerable enumerable => enumerable, + IEnumerable nonGeneric => nonGeneric.Cast(), + _ => throw new InvalidCastException($"Cannot convert {value?.GetType().Name ?? "null"} to IEnumerable") + }; + } + public static string ToQueryString(this Dictionary parameters) { var query = new List(); @@ -624,4 +636,4 @@ namespace {{ spec.title | caseUcfirst }}.Extensions return GetMimeTypeFromExtension(System.IO.Path.GetExtension(path)); } } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Models/InputFile.cs.twig b/templates/dotnet/Package/Models/InputFile.cs.twig index 241a3adad5..aaf7a66202 100644 --- a/templates/dotnet/Package/Models/InputFile.cs.twig +++ b/templates/dotnet/Package/Models/InputFile.cs.twig @@ -1,5 +1,5 @@ using System.IO; -using Appwrite.Extensions; +using {{ spec.title | caseUcfirst }}.Extensions; namespace {{ spec.title | caseUcfirst }}.Models { @@ -38,4 +38,4 @@ namespace {{ spec.title | caseUcfirst }}.Models SourceType = "bytes" }; } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Models/Model.cs.twig b/templates/dotnet/Package/Models/Model.cs.twig index ff46ff18e4..2ff72fac9e 100644 --- a/templates/dotnet/Package/Models/Model.cs.twig +++ b/templates/dotnet/Package/Models/Model.cs.twig @@ -1,28 +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 %}{{property | typeName}}{% endif %}{% if not property.required %}?{% endif %}{% endmacro %} -{% macro property_name(definition, property) %}{{ property.name | caseUcfirst | removeDollarSign | escapeKeyword }}{% endmacro %} - +{% set DefinitionClass = definition.name | caseUcfirst | overrideIdentifier %} using System; using System.Linq; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; +{% if definition.properties | filter(p => p.enum) | length > 0 %} +using {{ spec.title | caseUcfirst }}.Enums; +{% endif %} +using {{ spec.title | caseUcfirst }}.Extensions; namespace {{ spec.title | caseUcfirst }}.Models { - public class {{ definition.name | caseUcfirst | overrideIdentifier }} + public class {{ DefinitionClass }} { {%~ for property in definition.properties %} [JsonPropertyName("{{ property.name }}")] - public {{ _self.sub_schema(property) }} {{ _self.property_name(definition, property) | overrideProperty(definition.name) }} { get; private set; } + public {{ sub_schema(property) }} {{ property_name(definition, property) | overrideProperty(definition.name) }} { get; private set; } {%~ endfor %} {%~ if definition.additionalProperties %} public Dictionary Data { get; private set; } {%~ endif %} - public {{ definition.name | caseUcfirst | overrideIdentifier }}( + public {{ DefinitionClass }}( {%~ for property in definition.properties %} - {{ _self.sub_schema(property) }} {{ property.name | caseCamel | escapeKeyword }}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {{ sub_schema(property) }} {{ property.name | caseCamel | escapeKeyword }}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} {%~ endfor %} {%~ if definition.additionalProperties %} @@ -30,53 +32,33 @@ namespace {{ spec.title | caseUcfirst }}.Models {%~ endif %} ) { {%~ for property in definition.properties %} - {{ _self.property_name(definition, property) | overrideProperty(definition.name) }} = {{ property.name | caseCamel | escapeKeyword }}; + {{ property_name(definition, property) | overrideProperty(definition.name) }} = {{ property.name | caseCamel | escapeKeyword }}; {%~ endfor %} {%~ if definition.additionalProperties %} Data = data; {%~ endif %} } - public static {{ definition.name | caseUcfirst | overrideIdentifier }} From(Dictionary map) => new {{ definition.name | caseUcfirst | overrideIdentifier }}( + public static {{ DefinitionClass }} From(Dictionary map) => new {{ DefinitionClass }}( {%~ for property in definition.properties %} + {%~ set v = 'v' ~ loop.index0 %} + {%~ set mapAccess = 'map["' ~ property.name ~ '"]' %} {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}:{{' '}} - {%- if property.sub_schema %} - {%- if property.type == 'array' -%} - map["{{ property.name }}"] is JsonElement jsonArray{{ loop.index }} ? jsonArray{{ loop.index }}.Deserialize>>()!.Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: it)).ToList() : ((IEnumerable>)map["{{ property.name }}"]).Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: it)).ToList() - {%- else -%} - {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: map["{{ property.name }}"] is JsonElement jsonObj{{ loop.index }} ? jsonObj{{ loop.index }}.Deserialize>()! : (Dictionary)map["{{ property.name }}"]) - {%- endif %} - {%- else %} - {%- if property.type == 'array' -%} - map["{{ property.name }}"] is JsonElement jsonArrayProp{{ loop.index }} ? jsonArrayProp{{ loop.index }}.Deserialize<{{ property | typeName }}>()! : ({{ property | typeName }})map["{{ property.name }}"] - {%- 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 %} - {%- if not property.required -%} - map.TryGetValue("{{ property.name }}", out var {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}) ? {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}?.ToString() : null - {%- else -%} - map["{{ property.name }}"].ToString() - {%- endif %} - {%- endif %} - {%~ endif %} - {%~ endif %} - {%~ endif %} + {%- if not property.required -%}map.TryGetValue("{{ property.name }}", out var {{ v }}) ? {% endif -%} +{{ parse_value(property, mapAccess, v) }} + {%- if not property.required %} : null{% endif -%} {%- if not loop.last or (loop.last and definition.additionalProperties) %}, {%~ endif %} {%~ endfor %} {%- if definition.additionalProperties %} - data: map + data: map.TryGetValue("data", out var dataValue) ? (Dictionary)dataValue : map {%- endif ~%} ); public Dictionary ToMap() => new Dictionary() { {%~ for property in definition.properties %} - { "{{ property.name }}", {% if property.sub_schema %}{% if property.type == 'array' %}{{ _self.property_name(definition, property) | overrideProperty(definition.name) }}.Select(it => it.ToMap()){% else %}{{ _self.property_name(definition, property) | overrideProperty(definition.name) }}.ToMap(){% endif %}{% else %}{{ _self.property_name(definition, property) | overrideProperty(definition.name) }}{% endif %}{{ ' }' }}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + { "{{ property.name }}", {% if property.sub_schema %}{% if property.type == 'array' %}{{ property_name(definition, property) | overrideProperty(definition.name) }}.Select(it => it.ToMap()){% else %}{{ property_name(definition, property) | overrideProperty(definition.name) }}.ToMap(){% endif %}{% elseif property.enum %}{{ property_name(definition, property) | overrideProperty(definition.name) }}{% if not property.required %}?{% endif %}.Value{% else %}{{ property_name(definition, property) | overrideProperty(definition.name) }}{% endif %}{{ ' }' }}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} {%~ endfor %} {%~ if definition.additionalProperties %} diff --git a/templates/dotnet/Package/Models/UploadProgress.cs.twig b/templates/dotnet/Package/Models/UploadProgress.cs.twig index 47c78391ce..ee6fb58ba3 100644 --- a/templates/dotnet/Package/Models/UploadProgress.cs.twig +++ b/templates/dotnet/Package/Models/UploadProgress.cs.twig @@ -23,4 +23,4 @@ namespace {{ spec.title | caseUcfirst }} ChunksUploaded = chunksUploaded; } } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Operator.cs.twig b/templates/dotnet/Package/Operator.cs.twig new file mode 100644 index 0000000000..f47202409b --- /dev/null +++ b/templates/dotnet/Package/Operator.cs.twig @@ -0,0 +1,281 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace {{ spec.title | caseUcfirst }} +{ + public enum Condition + { + [EnumMember(Value = "equal")] + Equal, + [EnumMember(Value = "notEqual")] + NotEqual, + [EnumMember(Value = "greaterThan")] + GreaterThan, + [EnumMember(Value = "greaterThanEqual")] + GreaterThanEqual, + [EnumMember(Value = "lessThan")] + LessThan, + [EnumMember(Value = "lessThanEqual")] + LessThanEqual, + [EnumMember(Value = "contains")] + Contains, + [EnumMember(Value = "isNull")] + IsNull, + [EnumMember(Value = "isNotNull")] + IsNotNull + } + + public static class ConditionExtensions + { + public static string ToValue(this Condition condition) + { + return condition switch + { + Condition.Equal => "equal", + Condition.NotEqual => "notEqual", + Condition.GreaterThan => "greaterThan", + Condition.GreaterThanEqual => "greaterThanEqual", + Condition.LessThan => "lessThan", + Condition.LessThanEqual => "lessThanEqual", + Condition.Contains => "contains", + Condition.IsNull => "isNull", + Condition.IsNotNull => "isNotNull", + _ => throw new ArgumentOutOfRangeException(nameof(condition), condition, null) + }; + } + } + + public class Operator + { + [JsonPropertyName("method")] + public string Method { get; set; } = string.Empty; + + [JsonPropertyName("values")] + public List? Values { get; set; } + + public Operator() + { + } + + public Operator(string method, object? values) + { + this.Method = method; + + if (values is IList valuesList) + { + this.Values = new List(); + foreach (var value in valuesList) + { + this.Values.Add(value); + } + } + else if (values != null) + { + this.Values = new List { values }; + } + } + + public override string ToString() + { + return JsonSerializer.Serialize(this, Client.SerializerOptions); + } + + public static string Increment(double value = 1, double? max = null) + { + if (Double.IsNaN(value) || Double.IsInfinity(value)) + { + throw new ArgumentException("Value cannot be NaN or Infinity", nameof(value)); + } + if (max.HasValue && (Double.IsNaN(max.Value) || Double.IsInfinity(max.Value))) + { + throw new ArgumentException("Max cannot be NaN or Infinity", nameof(max)); + } + var values = new List { value }; + if (max.HasValue) + { + values.Add(max.Value); + } + return new Operator("increment", values).ToString(); + } + + public static string Decrement(double value = 1, double? min = null) + { + if (Double.IsNaN(value) || Double.IsInfinity(value)) + { + throw new ArgumentException("Value cannot be NaN or Infinity", nameof(value)); + } + if (min.HasValue && (Double.IsNaN(min.Value) || Double.IsInfinity(min.Value))) + { + throw new ArgumentException("Min cannot be NaN or Infinity", nameof(min)); + } + var values = new List { value }; + if (min.HasValue) + { + values.Add(min.Value); + } + return new Operator("decrement", values).ToString(); + } + + public static string Multiply(double factor, double? max = null) + { + if (Double.IsNaN(factor) || Double.IsInfinity(factor)) + { + throw new ArgumentException("Factor cannot be NaN or Infinity", nameof(factor)); + } + if (max.HasValue && (Double.IsNaN(max.Value) || Double.IsInfinity(max.Value))) + { + throw new ArgumentException("Max cannot be NaN or Infinity", nameof(max)); + } + var values = new List { factor }; + if (max.HasValue) + { + values.Add(max.Value); + } + return new Operator("multiply", values).ToString(); + } + + public static string Divide(double divisor, double? min = null) + { + if (Double.IsNaN(divisor) || Double.IsInfinity(divisor)) + { + throw new ArgumentException("Divisor cannot be NaN or Infinity", nameof(divisor)); + } + if (min.HasValue && (Double.IsNaN(min.Value) || Double.IsInfinity(min.Value))) + { + throw new ArgumentException("Min cannot be NaN or Infinity", nameof(min)); + } + if (divisor == 0) + { + throw new ArgumentException("Divisor cannot be zero", nameof(divisor)); + } + var values = new List { divisor }; + if (min.HasValue) + { + values.Add(min.Value); + } + return new Operator("divide", values).ToString(); + } + + public static string Modulo(double divisor) + { + if (Double.IsNaN(divisor) || Double.IsInfinity(divisor)) + { + throw new ArgumentException("Divisor cannot be NaN or Infinity", nameof(divisor)); + } + if (divisor == 0) + { + throw new ArgumentException("Divisor cannot be zero", nameof(divisor)); + } + return new Operator("modulo", new List { divisor }).ToString(); + } + + public static string Power(double exponent, double? max = null) + { + if (Double.IsNaN(exponent) || Double.IsInfinity(exponent)) + { + throw new ArgumentException("Exponent cannot be NaN or Infinity", nameof(exponent)); + } + if (max.HasValue && (Double.IsNaN(max.Value) || Double.IsInfinity(max.Value))) + { + throw new ArgumentException("Max cannot be NaN or Infinity", nameof(max)); + } + var values = new List { exponent }; + if (max.HasValue) + { + values.Add(max.Value); + } + return new Operator("power", values).ToString(); + } + + public static string ArrayAppend(List values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + return new Operator("arrayAppend", values).ToString(); + } + + public static string ArrayPrepend(List values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + return new Operator("arrayPrepend", values).ToString(); + } + + public static string ArrayInsert(int index, object value) + { + return new Operator("arrayInsert", new List { index, value }).ToString(); + } + + public static string ArrayRemove(object value) + { + return new Operator("arrayRemove", new List { value }).ToString(); + } + + public static string ArrayUnique() + { + return new Operator("arrayUnique", new List()).ToString(); + } + + public static string ArrayIntersect(List values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + return new Operator("arrayIntersect", values).ToString(); + } + + public static string ArrayDiff(List values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + return new Operator("arrayDiff", values).ToString(); + } + + public static string ArrayFilter(Condition condition, object? value = null) + { + var values = new List { condition.ToValue(), value }; + return new Operator("arrayFilter", values).ToString(); + } + + public static string StringConcat(object value) + { + return new Operator("stringConcat", new List { value }).ToString(); + } + + public static string StringReplace(string search, string replace) + { + return new Operator("stringReplace", new List { search, replace }).ToString(); + } + + public static string Toggle() + { + return new Operator("toggle", new List()).ToString(); + } + + public static string DateAddDays(int days) + { + return new Operator("dateAddDays", new List { days }).ToString(); + } + + public static string DateSubDays(int days) + { + return new Operator("dateSubDays", new List { days }).ToString(); + } + + public static string DateSetNow() + { + return new Operator("dateSetNow", new List()).ToString(); + } + } +} diff --git a/templates/dotnet/Package/Query.cs.twig b/templates/dotnet/Package/Query.cs.twig index 1145329e6e..375eb5af4f 100644 --- a/templates/dotnet/Package/Query.cs.twig +++ b/templates/dotnet/Package/Query.cs.twig @@ -4,7 +4,6 @@ using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; - namespace {{ spec.title | caseUcfirst }} { public class Query @@ -41,7 +40,7 @@ namespace {{ spec.title | caseUcfirst }} } } - override public string ToString() + public override string ToString() { return JsonSerializer.Serialize(this, Client.SerializerOptions); } @@ -138,6 +137,10 @@ namespace {{ spec.title | caseUcfirst }} return new Query("orderDesc", attribute, null).ToString(); } + public static string OrderRandom() { + return new Query("orderRandom", null, null).ToString(); + } + public static string Limit(int limit) { return new Query("limit", null, limit).ToString(); } @@ -179,27 +182,27 @@ namespace {{ spec.title | caseUcfirst }} } public static string CreatedBefore(string value) { - return new Query("createdBefore", null, value).ToString(); + return LessThan("$createdAt", value); } public static string CreatedAfter(string value) { - return new Query("createdAfter", null, value).ToString(); + return GreaterThan("$createdAt", value); } public static string CreatedBetween(string start, string end) { - return new Query("createdBetween", null, new List { start, end }).ToString(); + return Between("$createdAt", start, end); } public static string UpdatedBefore(string value) { - return new Query("updatedBefore", null, value).ToString(); + return LessThan("$updatedAt", value); } public static string UpdatedAfter(string value) { - return new Query("updatedAfter", null, value).ToString(); + return GreaterThan("$updatedAt", value); } public static string UpdatedBetween(string start, string end) { - return new Query("updatedBetween", null, new List { start, end }).ToString(); + return Between("$updatedAt", start, end); } public static string Or(List queries) { @@ -270,4 +273,4 @@ namespace {{ spec.title | caseUcfirst }} return new Query("notTouches", attribute, new List { values }).ToString(); } } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Role.cs.twig b/templates/dotnet/Package/Role.cs.twig index b3ecf2610b..3c7b2b33f3 100644 --- a/templates/dotnet/Package/Role.cs.twig +++ b/templates/dotnet/Package/Role.cs.twig @@ -1,4 +1,4 @@ -namespace Appwrite +namespace {{ spec.title | caseUcfirst }} { /// /// Helper class to generate role strings for Permission. @@ -89,4 +89,4 @@ namespace Appwrite return $"label:{name}"; } } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Services/ServiceTemplate.cs.twig b/templates/dotnet/Package/Services/ServiceTemplate.cs.twig index 99cf15653b..ea96412674 100644 --- a/templates/dotnet/Package/Services/ServiceTemplate.cs.twig +++ b/templates/dotnet/Package/Services/ServiceTemplate.cs.twig @@ -1,5 +1,4 @@ {% import 'dotnet/base/utils.twig' as utils %} - using System; using System.Collections.Generic; using System.Linq; @@ -7,7 +6,7 @@ using System.Threading.Tasks; {% if spec.definitions is not empty %} using {{ spec.title | caseUcfirst }}.Models; {% endif %} -{% if spec.enums is not empty %} +{% if spec.requestEnums is not empty %} using {{ spec.title | caseUcfirst }}.Enums; {% endif %} diff --git a/templates/dotnet/README.md.twig b/templates/dotnet/README.md.twig index 0acd3ea547..186a213e56 100644 --- a/templates/dotnet/README.md.twig +++ b/templates/dotnet/README.md.twig @@ -39,7 +39,6 @@ Install-Package {{ spec.title | caseUcfirst }} -Version {{ sdk.version }} dotnet add package {{ spec.title | caseUcfirst }} --version {{ sdk.version }} ``` - {% if sdk.gettingStarted %} {{ sdk.gettingStarted|raw }} diff --git a/templates/dotnet/base/utils.twig b/templates/dotnet/base/utils.twig index 19ea870059..7ebb547a66 100644 --- a/templates/dotnet/base/utils.twig +++ b/templates/dotnet/base/utils.twig @@ -12,5 +12,5 @@ {% if (method.type == "webAuth" or method.type == "location") and method.auth|length > 0 %}{{ true }}{% else %}{{false}}{% endif %} {% endmacro %} {% macro resultType(namespace, method) %} -{% if method.type == "webAuth" %}bool{% elseif method.type == "location" %}byte[]{% elseif not method.responseModel or method.responseModel == 'any' %}object{% else %}Models.{{method.responseModel | caseUcfirst | overrideIdentifier }}{% endif %} +{% if method.type == "webAuth" %}bool{% elseif method.type == "location" %}byte[]{% elseif not method.responseModel or method.responseModel == 'any' %}object{% else %}Appwrite.Models.{{method.responseModel | caseUcfirst | overrideIdentifier }}{% endif %} {% endmacro %} \ No newline at end of file diff --git a/templates/dotnet/docs/example.md.twig b/templates/dotnet/docs/example.md.twig index c8aab98b53..b36e22853b 100644 --- a/templates/dotnet/docs/example.md.twig +++ b/templates/dotnet/docs/example.md.twig @@ -22,7 +22,7 @@ Client client = new Client() {% if method.method != 'delete' and method.type != 'webAuth' %}{% if method.type == 'location' %}byte[]{% else %}{{ method.responseModel | caseUcfirst | overrideIdentifier }}{% endif %} result = {% endif %}await {{ service.name | caseCamel }}.{{ method.name | caseUcfirst }}({% if method.parameters.all | length == 0 %});{% endif %} {%~ for parameter in method.parameters.all %} - {{ parameter.name }}: {% if parameter.enumValues | length > 0%}{{ parameter.enumName }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %}{% if not parameter.required %} // optional{% endif %} + {{ parameter.name }}: {% if parameter.enumValues | length > 0%}{{ parameter.enumName | caseUcfirst }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %}{% if not parameter.required %} // optional{% endif %} {%~ endfor %} {% if method.parameters.all | length > 0 %});{% endif %} \ No newline at end of file diff --git a/templates/flutter/.github/workflows/format.yml.twig b/templates/flutter/.github/workflows/format.yml.twig index ae216a5fc8..73fc9eaf87 100644 --- a/templates/flutter/.github/workflows/format.yml.twig +++ b/templates/flutter/.github/workflows/format.yml.twig @@ -10,8 +10,6 @@ on: jobs: format: runs-on: ubuntu-latest - container: - image: dart:stable steps: - name: Checkout repository @@ -20,6 +18,14 @@ jobs: persist-credentials: true ref: ${{ '{{'}} github.event.pull_request.head.ref {{ '}}' }} + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + + - name: Install dependencies + run: flutter pub get + - name: Format Dart code run: dart format . @@ -29,5 +35,5 @@ jobs: - name: Add & Commit uses: EndBug/add-and-commit@v9.1.4 with: - add: lib + add: '["lib", "test"]' diff --git a/templates/flutter/README.md.twig b/templates/flutter/README.md.twig index f63f1988bf..bf80692f39 100644 --- a/templates/flutter/README.md.twig +++ b/templates/flutter/README.md.twig @@ -17,8 +17,6 @@ {{ sdk.description }} - - {% if sdk.logo %} ![{{ spec.title }}]({{ sdk.logo }}) {% endif %} diff --git a/templates/flutter/base/requests/oauth.twig b/templates/flutter/base/requests/oauth.twig index f7c2f8c04f..ee1c182013 100644 --- a/templates/flutter/base/requests/oauth.twig +++ b/templates/flutter/base/requests/oauth.twig @@ -16,10 +16,11 @@ params.forEach((key, value) { if (value is List) { for (var item in value) { - query.add(Uri.encodeComponent(key + '[]') + '=' + Uri.encodeComponent(item)); + query.add( + '${Uri.encodeComponent('$key[]')}=${Uri.encodeComponent(item)}'); } } else if(value != null) { - query.add(Uri.encodeComponent(key) + '=' + Uri.encodeComponent(value)); + query.add('${Uri.encodeComponent(key)}=${Uri.encodeComponent(value)}'); } }); diff --git a/templates/flutter/base/utils.twig b/templates/flutter/base/utils.twig index 3927955752..0ffa596590 100644 --- a/templates/flutter/base/utils.twig +++ b/templates/flutter/base/utils.twig @@ -1,6 +1,10 @@ {%- macro map_parameter(parameters) -%} {%- for parameter in parameters ~%} +{% if not parameter.nullable and not parameter.required %} + if ({{ parameter.name | caseCamel | overrideIdentifier }} != null) '{{ parameter.name }}': {{ parameter.name | caseCamel | overrideIdentifier }}{% if parameter.enumValues | length > 0 %}!.value{% endif %}, +{% else %} '{{ parameter.name }}': {{ parameter.name | caseCamel | overrideIdentifier }}{% if parameter.enumValues | length > 0 %}{% if not parameter.required %}?{% endif %}.value{% endif %}, +{% endif %} {%- endfor ~%} {%- endmacro ~%} diff --git a/templates/flutter/docs/example.md.twig b/templates/flutter/docs/example.md.twig index 3447fa9063..a570328d55 100644 --- a/templates/flutter/docs/example.md.twig +++ b/templates/flutter/docs/example.md.twig @@ -2,6 +2,10 @@ import 'dart:io'; {% endif %} import 'package:{{ language.params.packageName }}/{{ language.params.packageName }}.dart'; +{% if method.parameters.all | hasPermissionParam %} +import 'package:{{ language.params.packageName }}/permission.dart'; +import 'package:{{ language.params.packageName }}/role.dart'; +{% endif %} Client client = Client() {%~ if method.auth|length > 0 %} @@ -17,9 +21,9 @@ Client client = Client() {% if method.type == 'location' %} // Downloading file -UInt8List bytes = await {{ service.name | caseCamel }}.{{ method.name | caseCamel }}( +Uint8List bytes = await {{ service.name | caseCamel }}.{{ method.name | caseCamel }}( {%~ for parameter in method.parameters.all %} - {{ parameter.name | caseCamel | overrideIdentifier}}: {% if parameter.enumValues | length > 0%}{{parameter.enumName}}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample | replace({'': (parameter.name | caseCamel) }) | raw }}{% endif %},{% if not parameter.required %} // optional{% endif %} + {{ parameter.name | caseCamel | overrideIdentifier}}: {% if parameter.enumValues | length > 0%}{{ parameter.enumName | caseUcfirst | overrideIdentifier }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample | replace({'': (parameter.name | caseCamel) }) | raw }}{% endif %},{% if not parameter.required %} // optional{% endif %} {%~ endfor %}{% if method.parameters.all | length > 0 %}{% endif %}) @@ -30,7 +34,7 @@ file.writeAsBytesSync(bytes); FutureBuilder( future: {{ service.name | caseCamel }}.{{ method.name | caseCamel }}( {%~ for parameter in method.parameters.all %} - {{ parameter.name | caseCamel | overrideIdentifier}}:{% if parameter.enumValues | length > 0%} {{parameter.enumName}}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample | replace({'': (parameter.name | caseCamel) }) | raw }} {% endif %},{% if not parameter.required %} // optional{% endif %} + {{ parameter.name | caseCamel | overrideIdentifier}}:{% if parameter.enumValues | length > 0%} {{ parameter.enumName | caseUcfirst | overrideIdentifier }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample | replace({'': (parameter.name | caseCamel) }) | raw }} {% endif %},{% if not parameter.required %} // optional{% endif %} {%~ endfor %} ), // Works for both public file and private file, for private files you need to be logged in @@ -41,10 +45,10 @@ FutureBuilder( } ); {% else %} -{% if method.method != 'delete' and method.type != 'webAuth' %}{% if method.type == 'location' %}UInt8List{% else %}{{ method.responseModel | caseUcfirst | overrideIdentifier }}{% endif %} result = {% endif %}await {{ service.name | caseCamel }}.{{ method.name | caseCamel }}({% if method.parameters.all | length == 0 %});{% endif %} +{% if method.method != 'delete' and method.type != 'webAuth' %}{% if method.type == 'location' %}Uint8List{% else %}{{ method.responseModel | caseUcfirst | overrideIdentifier }}{% endif %} result = {% endif %}await {{ service.name | caseCamel }}.{{ method.name | caseCamel }}({% if method.parameters.all | length == 0 %});{% endif %} {%~ for parameter in method.parameters.all %} - {{ parameter.name | caseCamel | overrideIdentifier}}: {% if parameter.enumValues | length > 0%}{{parameter.enumName}}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample | replace({'': (parameter.name | caseCamel) }) | raw }}{% endif %},{% if not parameter.required %} // optional{% endif %} + {{ parameter.name | caseCamel | overrideIdentifier}}: {% if parameter.enumValues | length > 0%}{{ parameter.enumName | caseUcfirst | overrideIdentifier }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample | replace({'': (parameter.name | caseCamel) }) | raw }}{% endif %},{% if not parameter.required %} // optional{% endif %} {%~ endfor %} {% if method.parameters.all | length > 0 %}); diff --git a/templates/flutter/lib/package.dart.twig b/templates/flutter/lib/package.dart.twig index 87ca947fe6..51965ccb99 100644 --- a/templates/flutter/lib/package.dart.twig +++ b/templates/flutter/lib/package.dart.twig @@ -30,6 +30,7 @@ part 'query.dart'; part 'permission.dart'; part 'role.dart'; part 'id.dart'; +part 'operator.dart'; {% for service in spec.services %} part 'services/{{service.name | caseSnake}}.dart'; {% endfor %} \ No newline at end of file diff --git a/templates/flutter/lib/src/client.dart.twig b/templates/flutter/lib/src/client.dart.twig index 95688741f2..f716447f2e 100644 --- a/templates/flutter/lib/src/client.dart.twig +++ b/templates/flutter/lib/src/client.dart.twig @@ -9,8 +9,8 @@ import 'upload_progress.dart'; /// /// 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; + /// The size for chunked uploads in bytes. + static const int chunkSize = 5 * 1024 * 1024; /// Holds configuration such as project. late Map config; @@ -61,7 +61,7 @@ abstract class Client { /// /// {{header.description}}. {% endif %} - Client set{{header.key | caseUcfirst}}(value); + Client set{{header.key | caseUcfirst}}(String value); {% endfor %} /// Add headers that should be sent with all API calls. diff --git a/templates/flutter/lib/src/client_browser.dart.twig b/templates/flutter/lib/src/client_browser.dart.twig index 2bce9adc5b..9d7b56197a 100644 --- a/templates/flutter/lib/src/client_browser.dart.twig +++ b/templates/flutter/lib/src/client_browser.dart.twig @@ -16,7 +16,7 @@ ClientBase createClient({required String endPoint, required bool selfSigned}) => ClientBrowser(endPoint: endPoint, selfSigned: selfSigned); class ClientBrowser extends ClientBase with ClientMixin { - static const int CHUNK_SIZE = 5 * 1024 * 1024; + static const int chunkSize = 5 * 1024 * 1024; String _endPoint; Map? _headers; @override @@ -130,7 +130,7 @@ class ClientBrowser extends ClientBase with ClientMixin { int size = file.bytes!.length; late Response res; - if (size <= CHUNK_SIZE) { + if (size <= chunkSize) { params[paramName] = http.MultipartFile.fromBytes( paramName, file.bytes!, @@ -150,17 +150,17 @@ class ClientBrowser extends ClientBase with ClientMixin { try { res = await call( HttpMethod.get, - path: path + '/' + params[idParamName], + path: '$path/${params[idParamName]}', headers: headers, ); final int chunksUploaded = res.data['chunksUploaded'] as int; - offset = chunksUploaded * CHUNK_SIZE; + offset = chunksUploaded * chunkSize; } on {{spec.title | caseUcfirst}}Exception catch (_) {} } while (offset < size) { List chunk = []; - final end = min(offset + CHUNK_SIZE, size); + final end = min(offset + chunkSize, size); chunk = file.bytes!.getRange(offset, end).toList(); params[paramName] = http.MultipartFile.fromBytes( paramName, @@ -168,14 +168,14 @@ class ClientBrowser extends ClientBase with ClientMixin { filename: file.filename, ); headers['content-range'] = - 'bytes $offset-${min((offset + CHUNK_SIZE - 1), size - 1)}/$size'; + 'bytes $offset-${min((offset + chunkSize - 1), size - 1)}/$size'; res = await call( HttpMethod.post, path: path, headers: headers, params: params, ); - offset += CHUNK_SIZE; + offset += chunkSize; if (offset < size) { headers['x-{{spec.title | caseLower }}-id'] = res.data['\$id']; } @@ -243,7 +243,7 @@ class ClientBrowser extends ClientBase with ClientMixin { Future webAuth(Uri url, {String? callbackUrlScheme}) { return FlutterWebAuth2.authenticate( url: url.toString(), - callbackUrlScheme: "{{spec.title | caseLower}}-callback-" + config['project']!, + callbackUrlScheme: "{{spec.title | caseLower}}-callback-${config['project']!}", options: const FlutterWebAuth2Options(useWebview: false), ); } diff --git a/templates/flutter/lib/src/client_io.dart.twig b/templates/flutter/lib/src/client_io.dart.twig index f2ee284474..93534e4290 100644 --- a/templates/flutter/lib/src/client_io.dart.twig +++ b/templates/flutter/lib/src/client_io.dart.twig @@ -22,7 +22,7 @@ ClientBase createClient({required String endPoint, required bool selfSigned}) => ClientIO(endPoint: endPoint, selfSigned: selfSigned); class ClientIO extends ClientBase with ClientMixin { - static const int CHUNK_SIZE = 5 * 1024 * 1024; + static const int chunkSize = 5 * 1024 * 1024; String _endPoint; Map? _headers; @override @@ -181,7 +181,7 @@ class ClientIO extends ClientBase with ClientMixin { } catch (e) { debugPrint('Error getting device info: $e'); device = Platform.operatingSystem; - addHeader('user-agent', '$device'); + addHeader('user-agent', device); } _initialized = true; @@ -244,7 +244,7 @@ class ClientIO extends ClientBase with ClientMixin { } late Response res; - if (size <= CHUNK_SIZE) { + if (size <= chunkSize) { if (file.path != null) { params[paramName] = await http.MultipartFile.fromPath( paramName, @@ -272,11 +272,11 @@ class ClientIO extends ClientBase with ClientMixin { try { res = await call( HttpMethod.get, - path: path + '/' + params[idParamName], + path: '$path/${params[idParamName]}', headers: headers, ); final int chunksUploaded = res.data['chunksUploaded'] as int; - offset = chunksUploaded * CHUNK_SIZE; + offset = chunksUploaded * chunkSize; } on {{spec.title | caseUcfirst}}Exception catch (_) {} } @@ -289,11 +289,11 @@ class ClientIO extends ClientBase with ClientMixin { while (offset < size) { List chunk = []; if (file.bytes != null) { - final end = min(offset + CHUNK_SIZE, size); + final end = min(offset + chunkSize, size); chunk = file.bytes!.getRange(offset, end).toList(); } else { raf!.setPositionSync(offset); - chunk = raf.readSync(CHUNK_SIZE); + chunk = raf.readSync(chunkSize); } params[paramName] = http.MultipartFile.fromBytes( paramName, @@ -301,14 +301,14 @@ class ClientIO extends ClientBase with ClientMixin { filename: file.filename, ); headers['content-range'] = - 'bytes $offset-${min((offset + CHUNK_SIZE - 1), size - 1)}/$size'; + 'bytes $offset-${min((offset + chunkSize - 1), size - 1)}/$size'; res = await call( HttpMethod.post, path: path, headers: headers, params: params, ); - offset += CHUNK_SIZE; + offset += chunkSize; if (offset < size) { headers['x-{{spec.title | caseLower }}-id'] = res.data['\$id']; } @@ -333,7 +333,7 @@ class ClientIO extends ClientBase with ClientMixin { url: url.toString(), callbackUrlScheme: callbackUrlScheme != null && _customSchemeAllowed ? callbackUrlScheme - : "{{spec.title | caseLower}}-callback-" + config['project']!, + : "{{spec.title | caseLower}}-callback-${config['project']!}", options: const FlutterWebAuth2Options( intentFlags: ephemeralIntentFlags, useWebview: false, diff --git a/templates/flutter/lib/src/client_mixin.dart.twig b/templates/flutter/lib/src/client_mixin.dart.twig index 03d8d4a3ae..e91cc260a3 100644 --- a/templates/flutter/lib/src/client_mixin.dart.twig +++ b/templates/flutter/lib/src/client_mixin.dart.twig @@ -12,23 +12,25 @@ mixin ClientMixin { required Map headers, required Map params, }) { - if (params.isNotEmpty) { - params.removeWhere((key, value) => value == null); - } http.BaseRequest request = http.Request(method.name(), uri); if (headers['content-type'] == 'multipart/form-data') { request = http.MultipartRequest(method.name(), uri); if (params.isNotEmpty) { params.forEach((key, value) { + if (value == null) { + return; + } if (value is http.MultipartFile) { (request as http.MultipartRequest).files.add(value); } else { if (value is List) { value.asMap().forEach((i, v) { - (request as http.MultipartRequest).fields.addAll({ - "$key[$i]": v.toString(), - }); + if (v != null) { + (request as http.MultipartRequest).fields.addAll({ + "$key[$i]": v.toString(), + }); + } }); } else { (request as http.MultipartRequest).fields.addAll({ @@ -40,15 +42,19 @@ mixin ClientMixin { } } else if (method == HttpMethod.get) { if (params.isNotEmpty) { - params = params.map((key, value){ - if (value is int || value is double) { - return MapEntry(key, value.toString()); - } - if (value is List) { - return MapEntry(key + "[]", value); + Map filteredParams = {}; + params.forEach((key, value) { + if (value != null) { + if (value is int || value is double) { + filteredParams[key] = value.toString(); + } else if (value is List) { + filteredParams["$key[]"] = value; + } else { + filteredParams[key] = value; + } } - return MapEntry(key, value); }); + params = filteredParams; } uri = Uri( fragment: uri.fragment, diff --git a/templates/flutter/lib/src/realtime_io.dart.twig b/templates/flutter/lib/src/realtime_io.dart.twig index 819e938f0a..27539b251c 100644 --- a/templates/flutter/lib/src/realtime_io.dart.twig +++ b/templates/flutter/lib/src/realtime_io.dart.twig @@ -32,10 +32,10 @@ class RealtimeIO extends RealtimeBase with RealtimeMixin { final cookies = await (client as ClientIO).cookieJar.loadForRequest(uri); headers = {HttpHeaders.cookieHeader: CookieManager.getCookies(cookies)}; - final _websok = IOWebSocketChannel((client as ClientIO).selfSigned + final websok = IOWebSocketChannel((client as ClientIO).selfSigned ? await _connectForSelfSignedCert(uri, headers) : await WebSocket.connect(uri.toString(), headers: headers)); - return _websok; + return websok; } /// Subscribe diff --git a/templates/flutter/lib/src/realtime_mixin.dart.twig b/templates/flutter/lib/src/realtime_mixin.dart.twig index d96ba8d748..96e30699b0 100644 --- a/templates/flutter/lib/src/realtime_mixin.dart.twig +++ b/templates/flutter/lib/src/realtime_mixin.dart.twig @@ -21,7 +21,7 @@ mixin RealtimeMixin { late WebSocketFactory getWebSocket; GetFallbackCookie? getFallbackCookie; int? get closeCode => _websok?.closeCode; - Map _subscriptions = {}; + final Map _subscriptions = {}; bool _reconnect = true; int _retries = 0; StreamSubscription? _websocketSubscription; @@ -53,7 +53,7 @@ mixin RealtimeMixin { _heartbeatTimer = null; } - _createSocket() async { + Future _createSocket() async { if(_creatingSocket || _channels.isEmpty) return; _creatingSocket = true; final uri = _prepareUri(); @@ -164,7 +164,7 @@ mixin RealtimeMixin { "project": client.config['project'], "channels[]": _channels.toList(), }, - path: uri.path + "/realtime", + path: "${uri.path}/realtime", ); } diff --git a/templates/flutter/pubspec.yaml.twig b/templates/flutter/pubspec.yaml.twig index b50600ecb7..e1315fe692 100644 --- a/templates/flutter/pubspec.yaml.twig +++ b/templates/flutter/pubspec.yaml.twig @@ -19,10 +19,10 @@ dependencies: flutter: sdk: flutter cookie_jar: ^4.0.8 - device_info_plus: ^11.5.0 - flutter_web_auth_2: ^4.1.0 + device_info_plus: '>=11.5.0 <13.0.0' + flutter_web_auth_2: ^5.0.0-alpha.3 http: '>=0.13.6 <2.0.0' - package_info_plus: ^8.0.2 + package_info_plus: '>=8.0.2 <10.0.0' path_provider: ^2.1.4 web_socket_channel: ^3.0.1 web: ^1.0.0 diff --git a/templates/go/CHANGELOG.md.twig b/templates/go/CHANGELOG.md.twig index 6463f38da8..dfcefd0336 100644 --- a/templates/go/CHANGELOG.md.twig +++ b/templates/go/CHANGELOG.md.twig @@ -1,4 +1 @@ -# {{sdk.changelog | raw}} - -## {{sdk.version}} - \ No newline at end of file +{{sdk.changelog | raw}} \ No newline at end of file diff --git a/templates/go/client.go.twig b/templates/go/client.go.twig index b079bf1992..ae813b505d 100644 --- a/templates/go/client.go.twig +++ b/templates/go/client.go.twig @@ -441,7 +441,6 @@ func toString(arg interface{}) string { } } - // flatten recursively flattens params into a map[string]string and writes it to result func flatten(params interface{}, prefix string, result *map[string]string) error { if result == nil { diff --git a/templates/go/operator.go.twig b/templates/go/operator.go.twig new file mode 100644 index 0000000000..79675dcd59 --- /dev/null +++ b/templates/go/operator.go.twig @@ -0,0 +1,277 @@ +package operator + +import ( + "encoding/json" + "fmt" + "math" +) + +type Condition string + +const ( + ConditionEqual Condition = "equal" + ConditionNotEqual Condition = "notEqual" + ConditionGreaterThan Condition = "greaterThan" + ConditionGreaterThanEqual Condition = "greaterThanEqual" + ConditionLessThan Condition = "lessThan" + ConditionLessThanEqual Condition = "lessThanEqual" + ConditionContains Condition = "contains" + ConditionIsNull Condition = "isNull" + ConditionIsNotNull Condition = "isNotNull" +) + +type operatorOptions struct { + Method string + Values *[]interface{} +} + +func validateNumeric(value interface{}, paramName string) { + switch v := value.(type) { + case float64: + if math.IsNaN(v) || math.IsInf(v, 0) { + panic(fmt.Sprintf("%s cannot be NaN or Infinity", paramName)) + } + case float32: + if math.IsNaN(float64(v)) || math.IsInf(float64(v), 0) { + panic(fmt.Sprintf("%s cannot be NaN or Infinity", paramName)) + } + } +} + +func parseOperator(options operatorOptions) string { + data := struct { + Method string `json:"method"` + Values []interface{} `json:"values"` + }{ + Method: options.Method, + } + + if options.Values != nil { + data.Values = *options.Values + } + + jsonData, err := json.Marshal(data) + if err != nil { + panic(fmt.Errorf("failed to marshal operator data: %w", err)) + } + + return string(jsonData) +} + +func Increment(value ...interface{}) string { + var values []interface{} + if len(value) == 0 { + values = []interface{}{1} + } else if len(value) == 1 { + validateNumeric(value[0], "value") + values = []interface{}{value[0]} + } else { + validateNumeric(value[0], "value") + validateNumeric(value[1], "max") + values = []interface{}{value[0], value[1]} + } + return parseOperator(operatorOptions{ + Method: "increment", + Values: &values, + }) +} + +func Decrement(value ...interface{}) string { + var values []interface{} + if len(value) == 0 { + values = []interface{}{1} + } else if len(value) == 1 { + validateNumeric(value[0], "value") + values = []interface{}{value[0]} + } else { + validateNumeric(value[0], "value") + validateNumeric(value[1], "min") + values = []interface{}{value[0], value[1]} + } + return parseOperator(operatorOptions{ + Method: "decrement", + Values: &values, + }) +} + +func Multiply(factor interface{}, max ...interface{}) string { + validateNumeric(factor, "factor") + values := []interface{}{factor} + if len(max) > 0 && max[0] != nil { + validateNumeric(max[0], "max") + values = append(values, max[0]) + } + return parseOperator(operatorOptions{ + Method: "multiply", + Values: &values, + }) +} + +func Divide(divisor interface{}, min ...interface{}) string { + validateNumeric(divisor, "divisor") + if len(min) > 0 && min[0] != nil { + validateNumeric(min[0], "min") + } + // Check for zero divisor + switch v := divisor.(type) { + case int: + if v == 0 { + panic("divisor cannot be zero") + } + case float64: + if v == 0.0 { + panic("divisor cannot be zero") + } + } + values := []interface{}{divisor} + if len(min) > 0 && min[0] != nil { + values = append(values, min[0]) + } + return parseOperator(operatorOptions{ + Method: "divide", + Values: &values, + }) +} + +func Modulo(divisor interface{}) string { + validateNumeric(divisor, "divisor") + switch v := divisor.(type) { + case int: + if v == 0 { + panic("divisor cannot be zero") + } + case float64: + if v == 0.0 { + panic("divisor cannot be zero") + } + } + values := []interface{}{divisor} + return parseOperator(operatorOptions{ + Method: "modulo", + Values: &values, + }) +} + +func Power(exponent interface{}, max ...interface{}) string { + validateNumeric(exponent, "exponent") + values := []interface{}{exponent} + if len(max) > 0 && max[0] != nil { + validateNumeric(max[0], "max") + values = append(values, max[0]) + } + return parseOperator(operatorOptions{ + Method: "power", + Values: &values, + }) +} + +func ArrayAppend(values []interface{}) string { + return parseOperator(operatorOptions{ + Method: "arrayAppend", + Values: &values, + }) +} + +func ArrayPrepend(values []interface{}) string { + return parseOperator(operatorOptions{ + Method: "arrayPrepend", + Values: &values, + }) +} + +func ArrayInsert(index int, value interface{}) string { + values := []interface{}{index, value} + return parseOperator(operatorOptions{ + Method: "arrayInsert", + Values: &values, + }) +} + +func ArrayRemove(value interface{}) string { + values := []interface{}{value} + return parseOperator(operatorOptions{ + Method: "arrayRemove", + Values: &values, + }) +} + +func ArrayUnique() string { + values := []interface{}{} + return parseOperator(operatorOptions{ + Method: "arrayUnique", + Values: &values, + }) +} + +func ArrayIntersect(values []interface{}) string { + return parseOperator(operatorOptions{ + Method: "arrayIntersect", + Values: &values, + }) +} + +func ArrayDiff(values []interface{}) string { + return parseOperator(operatorOptions{ + Method: "arrayDiff", + Values: &values, + }) +} + +func ArrayFilter(condition Condition, value ...interface{}) string { + values := []interface{}{string(condition), nil} + if len(value) > 0 && value[0] != nil { + values[1] = value[0] + } + return parseOperator(operatorOptions{ + Method: "arrayFilter", + Values: &values, + }) +} + +func StringConcat(value interface{}) string { + values := []interface{}{value} + return parseOperator(operatorOptions{ + Method: "stringConcat", + Values: &values, + }) +} + +func StringReplace(search string, replace string) string { + values := []interface{}{search, replace} + return parseOperator(operatorOptions{ + Method: "stringReplace", + Values: &values, + }) +} + +func Toggle() string { + values := []interface{}{} + return parseOperator(operatorOptions{ + Method: "toggle", + Values: &values, + }) +} + +func DateAddDays(days int) string { + values := []interface{}{days} + return parseOperator(operatorOptions{ + Method: "dateAddDays", + Values: &values, + }) +} + +func DateSubDays(days int) string { + values := []interface{}{days} + return parseOperator(operatorOptions{ + Method: "dateSubDays", + Values: &values, + }) +} + +func DateSetNow() string { + values := []interface{}{} + return parseOperator(operatorOptions{ + Method: "dateSetNow", + Values: &values, + }) +} diff --git a/templates/go/query.go.twig b/templates/go/query.go.twig index bccde654cf..3d6d555208 100644 --- a/templates/go/query.go.twig +++ b/templates/go/query.go.twig @@ -202,51 +202,27 @@ func NotEndsWith(attribute string, value interface{}) string { } func CreatedBefore(value interface{}) string { - values := toArray(value) - return parseQuery(queryOptions{ - Method: "createdBefore", - Values: &values, - }) + return LessThan("$createdAt", value) } func CreatedAfter(value interface{}) string { - values := toArray(value) - return parseQuery(queryOptions{ - Method: "createdAfter", - Values: &values, - }) + return GreaterThan("$createdAt", value) } func CreatedBetween(start, end interface{}) string { - values := []interface{}{start, end} - return parseQuery(queryOptions{ - Method: "createdBetween", - Values: &values, - }) + return Between("$createdAt", start, end) } func UpdatedBefore(value interface{}) string { - values := toArray(value) - return parseQuery(queryOptions{ - Method: "updatedBefore", - Values: &values, - }) + return LessThan("$updatedAt", value) } func UpdatedAfter(value interface{}) string { - values := toArray(value) - return parseQuery(queryOptions{ - Method: "updatedAfter", - Values: &values, - }) + return GreaterThan("$updatedAt", value) } func UpdatedBetween(start, end interface{}) string { - values := []interface{}{start, end} - return parseQuery(queryOptions{ - Method: "updatedBetween", - Values: &values, - }) + return Between("$updatedAt", start, end) } func Select(attributes interface{}) string { @@ -268,7 +244,14 @@ func OrderDesc(attribute string) string { return parseQuery(queryOptions{ Method: "orderDesc", Attribute: &attribute, - })} + }) +} + +func OrderRandom() string { + return parseQuery(queryOptions{ + Method: "orderRandom", + }) +} func CursorBefore(documentId interface{}) string { values := toArray(documentId) diff --git a/templates/kotlin/docs/java/example.md.twig b/templates/kotlin/docs/java/example.md.twig index 5c3c04d757..9ae6a3a830 100644 --- a/templates/kotlin/docs/java/example.md.twig +++ b/templates/kotlin/docs/java/example.md.twig @@ -3,20 +3,17 @@ import {{ sdk.namespace | caseDot }}.coroutines.CoroutineCallback; {% if method.parameters.all | filter((param) => param.type == 'file') | length > 0 %} import {{ sdk.namespace | caseDot }}.models.InputFile; {% endif %} +{% if method.parameters.all | hasPermissionParam %} +import {{ sdk.namespace | caseDot }}.Permission; +import {{ sdk.namespace | caseDot }}.Role; +{% endif %} import {{ sdk.namespace | caseDot }}.services.{{ service.name | caseUcfirst }}; {% set added = [] %} {% for parameter in method.parameters.all %} -{% if method == parameter.required %} {% if parameter.enumValues is not empty %} -{% if parameter.enumName is not empty %} -{% set name = parameter.enumName %} -{% else %} -{% set name = parameter.name %} -{% endif %} -{% if name not in added %} -import {{ sdk.namespace | caseDot }}.enums.{{ name | caseUcfirst }}; -{% set added = added|merge([name]) %} -{% endif %} +{% if parameter.enumName not in added %} +import {{ sdk.namespace | caseDot }}.enums.{{ parameter.enumName | caseUcfirst }}; +{% set added = added|merge([parameter.enumName]) %} {% endif %} {% endif %} {% endfor %} @@ -41,7 +38,7 @@ Client client = new Client() }));{% endif %} {%~ for parameter in method.parameters.all %} - {% if parameter.enumValues | length > 0%}{{ parameter.enumName }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample }}{% endif %}, // {{ parameter.name }}{% if not parameter.required %} (optional){% endif %} + {% if parameter.enumValues | length > 0%}{{ parameter.enumName | caseUcfirst }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | javaParamExample }}{% endif %}, // {{ parameter.name }}{% if not parameter.required %} (optional){% endif %} {%~ if loop.last %} new CoroutineCallback<>((result, error) -> { diff --git a/templates/kotlin/docs/kotlin/example.md.twig b/templates/kotlin/docs/kotlin/example.md.twig index c7472c62ca..faa129321e 100644 --- a/templates/kotlin/docs/kotlin/example.md.twig +++ b/templates/kotlin/docs/kotlin/example.md.twig @@ -6,20 +6,17 @@ import {{ sdk.namespace | caseDot }}.models.InputFile import {{ sdk.namespace | caseDot }}.services.{{ service.name | caseUcfirst }} {% set added = [] %} {% for parameter in method.parameters.all %} -{% if method == parameter.required %} {% if parameter.enumValues is not empty %} -{% if parameter.enumName is not empty %} -{% set name = parameter.enumName %} -{% else %} -{% set name = parameter.name %} -{% endif %} -{% if name not in added %} -import {{ sdk.namespace | caseDot }}.enums.{{ name | caseUcfirst }} -{% set added = added|merge([name]) %} -{% endif %} +{% if parameter.enumName not in added %} +import {{ sdk.namespace | caseDot }}.enums.{{ parameter.enumName | caseUcfirst }} +{% set added = added|merge([parameter.enumName]) %} {% endif %} {% endif %} {% endfor %} +{% if method.parameters.all | hasPermissionParam %} +import {{ sdk.namespace | caseDot }}.Permission +import {{ sdk.namespace | caseDot }}.Role +{% endif %} val client = Client() {% if method.auth|length > 0 %} @@ -35,7 +32,7 @@ val {{ service.name | caseCamel }} = {{ service.name | caseUcfirst }}(client) {% for parameter in method.parameters.all %} {% if parameter.required %} - {{parameter.name}} = {% if parameter.enumValues | length > 0 %} {{ parameter.enumName }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else%}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %} + {{parameter.name}} = {% if parameter.enumValues | length > 0 %} {{ parameter.enumName | caseUcfirst }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else%}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %} {% else %} {{parameter.name}} = {{ parameter | paramExample }}{% if not loop.last %},{% endif %} // optional diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig index 98369715b2..1e1b4d6ace 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig @@ -54,7 +54,6 @@ class Client @JvmOverloads constructor( val config: MutableMap - init { headers = mutableMapOf( "content-type" to "application/json", @@ -552,7 +551,7 @@ class Client @JvmOverloads constructor( val warnings = response.headers["x-{{ spec.title | lower }}-warning"] if (warnings != null) { warnings.split(";").forEach { warning -> - println("Warning: $warning") + System.err.println("Warning: $warning") } } diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Operator.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Operator.kt.twig new file mode 100644 index 0000000000..a58312f433 --- /dev/null +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Operator.kt.twig @@ -0,0 +1,130 @@ +package {{ sdk.namespace | caseDot }} + +import {{ sdk.namespace | caseDot }}.extensions.toJson + +enum class Condition(val value: String) { + EQUAL("equal"), + NOT_EQUAL("notEqual"), + GREATER_THAN("greaterThan"), + GREATER_THAN_EQUAL("greaterThanEqual"), + LESS_THAN("lessThan"), + LESS_THAN_EQUAL("lessThanEqual"), + CONTAINS("contains"), + IS_NULL("isNull"), + IS_NOT_NULL("isNotNull"); + + override fun toString() = value +} + +class Operator( + val method: String, + val values: List? = null, +) { + override fun toString() = this.toJson() + + companion object { + fun increment(value: Number = 1, max: Number? = null): String { + require(!value.toDouble().isNaN() && !value.toDouble().isInfinite()) { "Value cannot be NaN or Infinity" } + max?.let { require(!it.toDouble().isNaN() && !it.toDouble().isInfinite()) { "Max cannot be NaN or Infinity" } } + val values = mutableListOf(value) + max?.let { values.add(it) } + return Operator("increment", values).toJson() + } + + fun decrement(value: Number = 1, min: Number? = null): String { + require(!value.toDouble().isNaN() && !value.toDouble().isInfinite()) { "Value cannot be NaN or Infinity" } + min?.let { require(!it.toDouble().isNaN() && !it.toDouble().isInfinite()) { "Min cannot be NaN or Infinity" } } + val values = mutableListOf(value) + min?.let { values.add(it) } + return Operator("decrement", values).toJson() + } + + fun multiply(factor: Number, max: Number? = null): String { + require(!factor.toDouble().isNaN() && !factor.toDouble().isInfinite()) { "Factor cannot be NaN or Infinity" } + max?.let { require(!it.toDouble().isNaN() && !it.toDouble().isInfinite()) { "Max cannot be NaN or Infinity" } } + val values = mutableListOf(factor) + max?.let { values.add(it) } + return Operator("multiply", values).toJson() + } + + fun divide(divisor: Number, min: Number? = null): String { + require(!divisor.toDouble().isNaN() && !divisor.toDouble().isInfinite()) { "Divisor cannot be NaN or Infinity" } + min?.let { require(!it.toDouble().isNaN() && !it.toDouble().isInfinite()) { "Min cannot be NaN or Infinity" } } + require(divisor.toDouble() != 0.0) { "Divisor cannot be zero" } + val values = mutableListOf(divisor) + min?.let { values.add(it) } + return Operator("divide", values).toJson() + } + + fun modulo(divisor: Number): String { + require(!divisor.toDouble().isNaN() && !divisor.toDouble().isInfinite()) { "Divisor cannot be NaN or Infinity" } + require(divisor.toDouble() != 0.0) { "Divisor cannot be zero" } + return Operator("modulo", listOf(divisor)).toJson() + } + + fun power(exponent: Number, max: Number? = null): String { + require(!exponent.toDouble().isNaN() && !exponent.toDouble().isInfinite()) { "Exponent cannot be NaN or Infinity" } + max?.let { require(!it.toDouble().isNaN() && !it.toDouble().isInfinite()) { "Max cannot be NaN or Infinity" } } + val values = mutableListOf(exponent) + max?.let { values.add(it) } + return Operator("power", values).toJson() + } + + fun arrayAppend(values: List): String { + return Operator("arrayAppend", values).toJson() + } + + fun arrayPrepend(values: List): String { + return Operator("arrayPrepend", values).toJson() + } + + fun arrayInsert(index: Int, value: Any): String { + return Operator("arrayInsert", listOf(index, value)).toJson() + } + + fun arrayRemove(value: Any): String { + return Operator("arrayRemove", listOf(value)).toJson() + } + + fun arrayUnique(): String { + return Operator("arrayUnique", emptyList()).toJson() + } + + fun arrayIntersect(values: List): String { + return Operator("arrayIntersect", values).toJson() + } + + fun arrayDiff(values: List): String { + return Operator("arrayDiff", values).toJson() + } + + fun arrayFilter(condition: Condition, value: Any? = null): String { + val values = listOf(condition.value, value) + return Operator("arrayFilter", values).toJson() + } + + fun stringConcat(value: Any): String { + return Operator("stringConcat", listOf(value)).toJson() + } + + fun stringReplace(search: String, replace: String): String { + return Operator("stringReplace", listOf(search, replace)).toJson() + } + + fun toggle(): String { + return Operator("toggle", emptyList()).toJson() + } + + fun dateAddDays(days: Int): String { + return Operator("dateAddDays", listOf(days)).toJson() + } + + fun dateSubDays(days: Int): String { + return Operator("dateSubDays", listOf(days)).toJson() + } + + fun dateSetNow(): String { + return Operator("dateSetNow", emptyList()).toJson() + } + } +} diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig index 915075943b..b12757b56e 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig @@ -41,6 +41,8 @@ class Query( fun orderDesc(attribute: String) = Query("orderDesc", attribute).toJson() + fun orderRandom() = Query("orderRandom").toJson() + fun cursorBefore(documentId: String) = Query("cursorBefore", null, listOf(documentId)).toJson() fun cursorAfter(documentId: String) = Query("cursorAfter", null, listOf(documentId)).toJson() @@ -61,17 +63,17 @@ class Query( fun notEndsWith(attribute: String, value: String) = Query("notEndsWith", attribute, listOf(value)).toJson() - fun createdBefore(value: String) = Query("createdBefore", null, listOf(value)).toJson() + fun createdBefore(value: String) = lessThan("\$createdAt", value) - fun createdAfter(value: String) = Query("createdAfter", null, listOf(value)).toJson() + fun createdAfter(value: String) = greaterThan("\$createdAt", value) - fun createdBetween(start: String, end: String) = Query("createdBetween", null, listOf(start, end)).toJson() + fun createdBetween(start: String, end: String) = between("\$createdAt", start, end) - fun updatedBefore(value: String) = Query("updatedBefore", null, listOf(value)).toJson() + fun updatedBefore(value: String) = lessThan("\$updatedAt", value) - fun updatedAfter(value: String) = Query("updatedAfter", null, listOf(value)).toJson() + fun updatedAfter(value: String) = greaterThan("\$updatedAt", value) - fun updatedBetween(start: String, end: String) = Query("updatedBetween", null, listOf(start, end)).toJson() + fun updatedBetween(start: String, end: String) = between("\$updatedAt", start, end) fun or(queries: List) = Query("or", null, queries.map { it.fromJson() }).toJson() diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/models/Model.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/models/Model.kt.twig index 27e153ed82..2f71cedc0a 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/models/Model.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/models/Model.kt.twig @@ -2,6 +2,11 @@ package {{ sdk.namespace | caseDot }}.models import com.google.gson.annotations.SerializedName import io.appwrite.extensions.jsonCast +{%~ for property in definition.properties %} +{%~ if property.enum %} +import {{ sdk.namespace | caseDot }}.enums.{{ property.enumName | caseUcfirst }} +{%~ endif %} +{%~ endfor %} /** * {{ definition.description | replace({"\n": "\n * "}) | raw }} @@ -27,7 +32,7 @@ import io.appwrite.extensions.jsonCast ) { fun toMap(): Map = mapOf( {%~ for property in definition.properties %} - "{{ property.name | escapeDollarSign }}" to {% if property.sub_schema %}{% if property.type == 'array' %}{{property.name | escapeKeyword | removeDollarSign}}.map { it.toMap() }{% else %}{{property.name | escapeKeyword | removeDollarSign}}.toMap(){% endif %}{% else %}{{property.name | escapeKeyword | removeDollarSign}}{% endif %} as Any, + "{{ property.name | escapeDollarSign }}" to {% if property.sub_schema %}{% if property.type == 'array' %}{{property.name | escapeKeyword | removeDollarSign}}.map { it.toMap() }{% else %}{{property.name | escapeKeyword | removeDollarSign}}.toMap(){% endif %}{% elseif property.enum %}{{property.name | escapeKeyword | removeDollarSign}}{% if not property.required %}?{% endif %}.value{% else %}{{property.name | escapeKeyword | removeDollarSign}}{% endif %} as Any, {%~ endfor %} {%~ if definition.additionalProperties %} "data" to data!!.jsonCast(to = Map::class.java) @@ -61,10 +66,10 @@ import io.appwrite.extensions.jsonCast {%~ endif %} ) = {{ definition | modelType(spec) | raw }}( {%~ for property in definition.properties %} - {{ property.name | escapeKeyword | removeDollarSign }} = {% if property.sub_schema %}{% if property.type == 'array' %}(map["{{ property.name | escapeDollarSign }}"] as List>).map { {{ property.sub_schema | caseUcfirst }}.from(map = it{% if property.sub_schema | hasGenericType(spec) %}, nestedType{% endif %}) }{% else %}{{ property.sub_schema | caseUcfirst }}.from(map = map["{{property.name | escapeDollarSign }}"] as Map{% if property.sub_schema | hasGenericType(spec) %}, nestedType{% endif %}){% endif %}{% else %}{% if property.type == "integer" or property.type == "number" %}({% endif %}map["{{ property.name | escapeDollarSign }}"]{% if property.type == "integer" or property.type == "number" %} as{% if not property.required %}?{% endif %} Number){% endif %}{% if property.type == "integer" %}{% if not property.required %}?{% endif %}.toLong(){% elseif property.type == "number" %}{% if not property.required %}?{% endif %}.toDouble(){% else %} as{% if not property.required %}?{% endif %} {{ property | propertyType(spec) | raw }}{% endif %}{% endif %}, + {{ property.name | escapeKeyword | removeDollarSign }} = {{ property | propertyAssignment(spec) | raw }}, {%~ endfor %} {%~ if definition.additionalProperties %} - data = map.jsonCast(to = nestedType) + data = map["data"]?.jsonCast(to = nestedType) ?: map.jsonCast(to = nestedType) {%~ endif %} ) } diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig index c68896a7e5..fc4efd20d4 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig @@ -4,7 +4,7 @@ import {{ sdk.namespace | caseDot }}.Client {% if spec.definitions is not empty %} import {{ sdk.namespace | caseDot }}.models.* {% endif %} -{% if spec.enums is not empty %} +{% if spec.requestEnums is not empty %} import {{ sdk.namespace | caseDot }}.enums.* {% endif %} import {{ sdk.namespace | caseDot }}.exceptions.{{ spec.title | caseUcfirst }}Exception diff --git a/templates/node/docs/example.md.twig b/templates/node/docs/example.md.twig index b8c5516180..e2aaad0fea 100644 --- a/templates/node/docs/example.md.twig +++ b/templates/node/docs/example.md.twig @@ -19,10 +19,10 @@ const result = await {{ service.name | caseCamel }}.{{ method.name | caseCamel } {% else %}{ {%~ for parameter in method.parameters.all %} {%~ if parameter.required %} - {{ parameter.name | caseCamel }}: {% if parameter.enumValues | length > 0%}sdk.{{ parameter.enumName }}.{{(parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample}}{% endif %}{% if not loop.last %},{% endif%} + {{ parameter.name | caseCamel }}: {% if parameter.enumValues | length > 0%}sdk.{{ parameter.enumName | caseUcfirst }}.{{(parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample}}{% endif %}{% if not loop.last %},{% endif%} {%~ else %} - {{ parameter.name | caseCamel }}: {% if parameter.enumValues | length > 0%}sdk.{{ parameter.enumName }}.{{(parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample}}{% endif %}{% if not loop.last %},{% endif%} // optional + {{ parameter.name | caseCamel }}: {% if parameter.enumValues | length > 0%}sdk.{{ parameter.enumName | caseUcfirst }}.{{(parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample}}{% endif %}{% if not loop.last %},{% endif%} // optional {%~ endif %} {%~ endfor -%} }); diff --git a/templates/node/src/client.ts.twig b/templates/node/src/client.ts.twig index 7bfbb9b9d2..a092d6c94a 100644 --- a/templates/node/src/client.ts.twig +++ b/templates/node/src/client.ts.twig @@ -96,6 +96,10 @@ class Client { * @returns {this} */ setEndpoint(endpoint: string): this { + if (!endpoint || typeof endpoint !== 'string') { + throw new {{spec.title | caseUcfirst}}Exception('Endpoint must be a valid string'); + } + if (!endpoint.startsWith('http://') && !endpoint.startsWith('https://')) { throw new {{spec.title | caseUcfirst}}Exception('Invalid endpoint URL: ' + endpoint); } diff --git a/templates/node/src/index.ts.twig b/templates/node/src/index.ts.twig index 2742b0d8af..22e42234cd 100644 --- a/templates/node/src/index.ts.twig +++ b/templates/node/src/index.ts.twig @@ -7,6 +7,7 @@ export type { QueryTypes, QueryTypesList } from './query'; export { Permission } from './permission'; export { Role } from './role'; export { ID } from './id'; -{% for enum in spec.enums %} +export { Operator, Condition } from './operator'; +{% for enum in spec.allEnums %} export { {{ enum.name | caseUcfirst }} } from './enums/{{enum.name | caseKebab}}'; {% endfor %} diff --git a/templates/node/src/operator.ts.twig b/templates/node/src/operator.ts.twig new file mode 100644 index 0000000000..2386a6c414 --- /dev/null +++ b/templates/node/src/operator.ts.twig @@ -0,0 +1,308 @@ +type OperatorValuesSingle = string | number | boolean; +export type OperatorValuesList = string[] | number[] | boolean[] | any[]; +export type OperatorValues = OperatorValuesSingle | OperatorValuesList; + +export enum Condition { + Equal = "equal", + NotEqual = "notEqual", + GreaterThan = "greaterThan", + GreaterThanEqual = "greaterThanEqual", + LessThan = "lessThan", + LessThanEqual = "lessThanEqual", + Contains = "contains", + IsNull = "isNull", + IsNotNull = "isNotNull", +} + +/** + * Helper class to generate operator strings for atomic operations. + */ +export class Operator { + method: string; + values: OperatorValuesList | undefined; + + /** + * Constructor for Operator class. + * + * @param {string} method + * @param {OperatorValues} values + */ + constructor( + method: string, + values?: OperatorValues + ) { + this.method = method; + + if (values !== undefined) { + if (Array.isArray(values)) { + this.values = values; + } else { + this.values = [values] as OperatorValuesList; + } + } + } + + /** + * Convert the operator object to a JSON string. + * + * @returns {string} + */ + toString(): string { + return JSON.stringify({ + method: this.method, + values: this.values, + }); + } + + /** + * Increment a numeric attribute by a specified value. + * + * @param {number} value + * @param {number} max + * @returns {string} + */ + static increment = (value: number = 1, max?: number): string => { + if (isNaN(value) || !isFinite(value)) { + throw new Error("Value cannot be NaN or Infinity"); + } + if (max !== undefined && (isNaN(max) || !isFinite(max))) { + throw new Error("Max cannot be NaN or Infinity"); + } + const values: any[] = [value]; + if (max !== undefined) { + values.push(max); + } + return new Operator("increment", values).toString(); + }; + + /** + * Decrement a numeric attribute by a specified value. + * + * @param {number} value + * @param {number} min + * @returns {string} + */ + static decrement = (value: number = 1, min?: number): string => { + if (isNaN(value) || !isFinite(value)) { + throw new Error("Value cannot be NaN or Infinity"); + } + if (min !== undefined && (isNaN(min) || !isFinite(min))) { + throw new Error("Min cannot be NaN or Infinity"); + } + const values: any[] = [value]; + if (min !== undefined) { + values.push(min); + } + return new Operator("decrement", values).toString(); + }; + + /** + * Multiply a numeric attribute by a specified factor. + * + * @param {number} factor + * @param {number} max + * @returns {string} + */ + static multiply = (factor: number, max?: number): string => { + if (isNaN(factor) || !isFinite(factor)) { + throw new Error("Factor cannot be NaN or Infinity"); + } + if (max !== undefined && (isNaN(max) || !isFinite(max))) { + throw new Error("Max cannot be NaN or Infinity"); + } + const values: any[] = [factor]; + if (max !== undefined) { + values.push(max); + } + return new Operator("multiply", values).toString(); + }; + + /** + * Divide a numeric attribute by a specified divisor. + * + * @param {number} divisor + * @param {number} min + * @returns {string} + */ + static divide = (divisor: number, min?: number): string => { + if (isNaN(divisor) || !isFinite(divisor)) { + throw new Error("Divisor cannot be NaN or Infinity"); + } + if (min !== undefined && (isNaN(min) || !isFinite(min))) { + throw new Error("Min cannot be NaN or Infinity"); + } + if (divisor === 0) { + throw new Error("Divisor cannot be zero"); + } + const values: any[] = [divisor]; + if (min !== undefined) { + values.push(min); + } + return new Operator("divide", values).toString(); + }; + + /** + * Apply modulo operation on a numeric attribute. + * + * @param {number} divisor + * @returns {string} + */ + static modulo = (divisor: number): string => { + if (isNaN(divisor) || !isFinite(divisor)) { + throw new Error("Divisor cannot be NaN or Infinity"); + } + if (divisor === 0) { + throw new Error("Divisor cannot be zero"); + } + return new Operator("modulo", [divisor]).toString(); + }; + + /** + * Raise a numeric attribute to a specified power. + * + * @param {number} exponent + * @param {number} max + * @returns {string} + */ + static power = (exponent: number, max?: number): string => { + if (isNaN(exponent) || !isFinite(exponent)) { + throw new Error("Exponent cannot be NaN or Infinity"); + } + if (max !== undefined && (isNaN(max) || !isFinite(max))) { + throw new Error("Max cannot be NaN or Infinity"); + } + const values: any[] = [exponent]; + if (max !== undefined) { + values.push(max); + } + return new Operator("power", values).toString(); + }; + + /** + * Append values to an array attribute. + * + * @param {any[]} values + * @returns {string} + */ + static arrayAppend = (values: any[]): string => + new Operator("arrayAppend", values).toString(); + + /** + * Prepend values to an array attribute. + * + * @param {any[]} values + * @returns {string} + */ + static arrayPrepend = (values: any[]): string => + new Operator("arrayPrepend", values).toString(); + + /** + * Insert a value at a specific index in an array attribute. + * + * @param {number} index + * @param {any} value + * @returns {string} + */ + static arrayInsert = (index: number, value: any): string => + new Operator("arrayInsert", [index, value]).toString(); + + /** + * Remove a value from an array attribute. + * + * @param {any} value + * @returns {string} + */ + static arrayRemove = (value: any): string => + new Operator("arrayRemove", [value]).toString(); + + /** + * Remove duplicate values from an array attribute. + * + * @returns {string} + */ + static arrayUnique = (): string => + new Operator("arrayUnique", []).toString(); + + /** + * Keep only values that exist in both the current array and the provided array. + * + * @param {any[]} values + * @returns {string} + */ + static arrayIntersect = (values: any[]): string => + new Operator("arrayIntersect", values).toString(); + + /** + * Remove values from the array that exist in the provided array. + * + * @param {any[]} values + * @returns {string} + */ + static arrayDiff = (values: any[]): string => + new Operator("arrayDiff", values).toString(); + + /** + * Filter array values based on a condition. + * + * @param {Condition} condition + * @param {any} value + * @returns {string} + */ + static arrayFilter = (condition: Condition, value?: any): string => { + const values: any[] = [condition as string, value === undefined ? null : value]; + return new Operator("arrayFilter", values).toString(); + }; + + /** + * Concatenate a value to a string or array attribute. + * + * @param {any} value + * @returns {string} + */ + static stringConcat = (value: any): string => + new Operator("stringConcat", [value]).toString(); + + /** + * Replace occurrences of a search string with a replacement string. + * + * @param {string} search + * @param {string} replace + * @returns {string} + */ + static stringReplace = (search: string, replace: string): string => + new Operator("stringReplace", [search, replace]).toString(); + + /** + * Toggle a boolean attribute. + * + * @returns {string} + */ + static toggle = (): string => + new Operator("toggle", []).toString(); + + /** + * Add days to a date attribute. + * + * @param {number} days + * @returns {string} + */ + static dateAddDays = (days: number): string => + new Operator("dateAddDays", [days]).toString(); + + /** + * Subtract days from a date attribute. + * + * @param {number} days + * @returns {string} + */ + static dateSubDays = (days: number): string => + new Operator("dateSubDays", [days]).toString(); + + /** + * Set a date attribute to the current date and time. + * + * @returns {string} + */ + static dateSetNow = (): string => + new Operator("dateSetNow", []).toString(); +} diff --git a/templates/node/src/services/template.ts.twig b/templates/node/src/services/template.ts.twig index 9bb7016578..ddefea56e0 100644 --- a/templates/node/src/services/template.ts.twig +++ b/templates/node/src/services/template.ts.twig @@ -5,14 +5,9 @@ import type { Models } from '../models'; {% for method in service.methods %} {% for parameter in method.parameters.all %} {% if parameter.enumValues is not empty %} -{% if parameter.enumName is not empty %} -{% set name = parameter.enumName %} -{% else %} -{% set name = parameter.name %} -{% endif %} -{% if name not in added %} -import { {{ name | caseUcfirst }} } from '../enums/{{ name | caseKebab }}'; -{% set added = added|merge([name]) %} +{% if parameter.enumName not in added %} +import { {{ parameter.enumName | caseUcfirst }} } from '../enums/{{ parameter.enumName | caseKebab }}'; +{% set added = added|merge([parameter.enumName]) %} {% endif %} {% endif %} {% endfor %} diff --git a/templates/php/composer.json.twig b/templates/php/composer.json.twig index d02b22973b..e9248bfb74 100644 --- a/templates/php/composer.json.twig +++ b/templates/php/composer.json.twig @@ -7,6 +7,9 @@ "url": "{{ spec.contactURL }}", "email": "{{ spec.contactEmail }}" }, + "scripts": { + "test": "vendor/bin/phpunit" + }, "autoload": { "psr-4": { "{{spec.title | caseUcfirst}}\\": "src/{{spec.title | caseUcfirst}}" @@ -19,7 +22,7 @@ }, "require-dev": { "phpunit/phpunit": "^10", - "mockery/mockery": "^1.6.6" + "mockery/mockery": "^1.6.12" }, "minimum-stability": "dev" } \ No newline at end of file diff --git a/templates/php/docs/example.md.twig b/templates/php/docs/example.md.twig index 028e08ae44..f92a3c9964 100644 --- a/templates/php/docs/example.md.twig +++ b/templates/php/docs/example.md.twig @@ -7,20 +7,17 @@ use {{ spec.title | caseUcfirst }}\InputFile; use {{ spec.title | caseUcfirst }}\Services\{{ service.name | caseUcfirst }}; {% set added = [] %} {% for parameter in method.parameters.all %} -{% if method == parameter.required %} {% if parameter.enumValues is not empty %} -{% if parameter.enumName is not empty %} -{% set name = parameter.enumName %} -{% else %} -{% set name = parameter.name %} -{% endif %} -{% if name not in added %} -use {{ spec.title | caseUcfirst }}\Enums\{{parameter.enumName | caseUcfirst}}; -{% set added = added|merge([name]) %} -{% endif %} +{% if parameter.enumName not in added %} +use {{ spec.title | caseUcfirst }}\Enums\{{ parameter.enumName | caseUcfirst }}; +{% set added = added|merge([parameter.enumName]) %} {% endif %} {% endif %} {% endfor %} +{% if method.parameters.all | hasPermissionParam %} +use {{ spec.title | caseUcfirst }}\Permission; +use {{ spec.title | caseUcfirst }}\Role; +{% endif %} $client = (new Client()) {%~ if method.auth|length > 0 %} @@ -37,7 +34,7 @@ ${{ service.name | caseCamel }} = new {{ service.name | caseUcfirst }}($client); $result = ${{ service.name | caseCamel }}->{{ method.name | caseCamel }}({% if method.parameters.all | length == 0 %});{% endif %} {%~ for parameter in method.parameters.all %} - {{ parameter.name | caseCamel }}: {% if parameter.enumValues | length > 0%}{{ parameter.enumName }}::{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}(){% else%}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %}{% if not parameter.required %} // optional{% endif %} + {{ parameter.name | caseCamel }}: {% if parameter.enumValues | length > 0%}{{ parameter.enumName | caseUcfirst }}::{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}(){% else%}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %}{% if not parameter.required %} // optional{% endif %} {%~ endfor -%} {% if method.parameters.all | length > 0 %});{% endif %} diff --git a/templates/php/phpunit.xml.twig b/templates/php/phpunit.xml.twig new file mode 100644 index 0000000000..95acd0c1b7 --- /dev/null +++ b/templates/php/phpunit.xml.twig @@ -0,0 +1,32 @@ + + + + + ./tests/ + + + + + + ./src/{{ spec.title | caseUcfirst }} + + + diff --git a/templates/php/src/Operator.php.twig b/templates/php/src/Operator.php.twig new file mode 100644 index 0000000000..d9e7e76108 --- /dev/null +++ b/templates/php/src/Operator.php.twig @@ -0,0 +1,331 @@ +method = $method; + + if (is_null($values) || is_array($values)) { + $this->values = $values; + } else { + $this->values = [$values]; + } + } + + public function __toString(): string + { + return json_encode($this); + } + + public function jsonSerialize(): mixed + { + return [ + 'method' => $this->method, + 'values' => $this->values + ]; + } + + /** + * Increment + * + * @param int|float $value + * @param int|float|null $max + * @return string + */ + public static function increment(int|float $value = 1, int|float|null $max = null): string + { + if (is_nan($value) || is_infinite($value)) { + throw new \InvalidArgumentException('Value cannot be NaN or Infinity'); + } + if ($max !== null && (is_nan($max) || is_infinite($max))) { + throw new \InvalidArgumentException('Max cannot be NaN or Infinity'); + } + $values = [$value]; + if ($max !== null) { + $values[] = $max; + } + return (new Operator('increment', $values))->__toString(); + } + + /** + * Decrement + * + * @param int|float $value + * @param int|float|null $min + * @return string + */ + public static function decrement(int|float $value = 1, int|float|null $min = null): string + { + if (is_nan($value) || is_infinite($value)) { + throw new \InvalidArgumentException('Value cannot be NaN or Infinity'); + } + if ($min !== null && (is_nan($min) || is_infinite($min))) { + throw new \InvalidArgumentException('Min cannot be NaN or Infinity'); + } + $values = [$value]; + if ($min !== null) { + $values[] = $min; + } + return (new Operator('decrement', $values))->__toString(); + } + + /** + * Multiply + * + * @param int|float $factor + * @param int|float|null $max + * @return string + */ + public static function multiply(int|float $factor, int|float|null $max = null): string + { + if (is_nan($factor) || is_infinite($factor)) { + throw new \InvalidArgumentException('Factor cannot be NaN or Infinity'); + } + if ($max !== null && (is_nan($max) || is_infinite($max))) { + throw new \InvalidArgumentException('Max cannot be NaN or Infinity'); + } + $values = [$factor]; + if ($max !== null) { + $values[] = $max; + } + return (new Operator('multiply', $values))->__toString(); + } + + /** + * Divide + * + * @param int|float $divisor + * @param int|float|null $min + * @return string + */ + public static function divide(int|float $divisor, int|float|null $min = null): string + { + if (is_nan($divisor) || is_infinite($divisor)) { + throw new \InvalidArgumentException('Divisor cannot be NaN or Infinity'); + } + if ($min !== null && (is_nan($min) || is_infinite($min))) { + throw new \InvalidArgumentException('Min cannot be NaN or Infinity'); + } + if ($divisor === 0 || $divisor === 0.0) { + throw new \InvalidArgumentException('Divisor cannot be zero'); + } + $values = [$divisor]; + if ($min !== null) { + $values[] = $min; + } + return (new Operator('divide', $values))->__toString(); + } + + /** + * Modulo + * + * @param int|float $divisor + * @return string + */ + public static function modulo(int|float $divisor): string + { + if (is_nan($divisor) || is_infinite($divisor)) { + throw new \InvalidArgumentException('Divisor cannot be NaN or Infinity'); + } + if ($divisor === 0 || $divisor === 0.0) { + throw new \InvalidArgumentException('Divisor cannot be zero'); + } + return (new Operator('modulo', [$divisor]))->__toString(); + } + + /** + * Power + * + * @param int|float $exponent + * @param int|float|null $max + * @return string + */ + public static function power(int|float $exponent, int|float|null $max = null): string + { + if (is_nan($exponent) || is_infinite($exponent)) { + throw new \InvalidArgumentException('Exponent cannot be NaN or Infinity'); + } + if ($max !== null && (is_nan($max) || is_infinite($max))) { + throw new \InvalidArgumentException('Max cannot be NaN or Infinity'); + } + $values = [$exponent]; + if ($max !== null) { + $values[] = $max; + } + return (new Operator('power', $values))->__toString(); + } + + /** + * Array Append + * + * @param array $values + * @return string + */ + public static function arrayAppend(array $values): string + { + return (new Operator('arrayAppend', $values))->__toString(); + } + + /** + * Array Prepend + * + * @param array $values + * @return string + */ + public static function arrayPrepend(array $values): string + { + return (new Operator('arrayPrepend', $values))->__toString(); + } + + /** + * Array Insert + * + * @param int $index + * @param mixed $value + * @return string + */ + public static function arrayInsert(int $index, mixed $value): string + { + return (new Operator('arrayInsert', [$index, $value]))->__toString(); + } + + /** + * Array Remove + * + * @param mixed $value + * @return string + */ + public static function arrayRemove(mixed $value): string + { + return (new Operator('arrayRemove', [$value]))->__toString(); + } + + /** + * Array Unique + * + * @return string + */ + public static function arrayUnique(): string + { + return (new Operator('arrayUnique', []))->__toString(); + } + + /** + * Array Intersect + * + * @param array $values + * @return string + */ + public static function arrayIntersect(array $values): string + { + return (new Operator('arrayIntersect', $values))->__toString(); + } + + /** + * Array Diff + * + * @param array $values + * @return string + */ + public static function arrayDiff(array $values): string + { + return (new Operator('arrayDiff', $values))->__toString(); + } + + /** + * Array Filter + * + * @param string $condition + * @param mixed $value + * @return string + */ + public static function arrayFilter(string $condition, mixed $value = null): string + { + $values = [$condition, $value]; + return (new Operator('arrayFilter', $values))->__toString(); + } + + /** + * Concat + * + * @param mixed $value + * @return string + */ + public static function stringConcat(mixed $value): string + { + return (new Operator('stringConcat', [$value]))->__toString(); + } + + /** + * Replace + * + * @param string $search + * @param string $replace + * @return string + */ + public static function stringReplace(string $search, string $replace): string + { + return (new Operator('stringReplace', [$search, $replace]))->__toString(); + } + + /** + * Toggle + * + * @return string + */ + public static function toggle(): string + { + return (new Operator('toggle', []))->__toString(); + } + + /** + * Date Add Days + * + * @param int $days + * @return string + */ + public static function dateAddDays(int $days): string + { + return (new Operator('dateAddDays', [$days]))->__toString(); + } + + /** + * Date Subtract Days + * + * @param int $days + * @return string + */ + public static function dateSubDays(int $days): string + { + return (new Operator('dateSubDays', [$days]))->__toString(); + } + + /** + * Date Set Now + * + * @return string + */ + public static function dateSetNow(): string + { + return (new Operator('dateSetNow', []))->__toString(); + } +} diff --git a/templates/php/src/Query.php.twig b/templates/php/src/Query.php.twig index 4a3db0e26a..16f64393df 100644 --- a/templates/php/src/Query.php.twig +++ b/templates/php/src/Query.php.twig @@ -233,6 +233,16 @@ class Query implements \JsonSerializable return (new Query('orderDesc', $attribute, null))->__toString(); } + /** + * Order Random + * + * @return string + */ + public static function orderRandom(): string + { + return (new Query('orderRandom', null, null))->__toString(); + } + /** * Limit * @@ -336,7 +346,7 @@ class Query implements \JsonSerializable */ public static function createdBefore(string $value): string { - return (new Query('createdBefore', null, $value))->__toString(); + return self::lessThan('$createdAt', $value); } /** @@ -347,7 +357,7 @@ class Query implements \JsonSerializable */ public static function createdAfter(string $value): string { - return (new Query('createdAfter', null, $value))->__toString(); + return self::greaterThan('$createdAt', $value); } /** @@ -359,7 +369,7 @@ class Query implements \JsonSerializable */ public static function createdBetween(string $start, string $end): string { - return (new Query('createdBetween', null, [$start, $end]))->__toString(); + return self::between('$createdAt', $start, $end); } /** @@ -370,7 +380,7 @@ class Query implements \JsonSerializable */ public static function updatedBefore(string $value): string { - return (new Query('updatedBefore', null, $value))->__toString(); + return self::lessThan('$updatedAt', $value); } /** @@ -381,7 +391,7 @@ class Query implements \JsonSerializable */ public static function updatedAfter(string $value): string { - return (new Query('updatedAfter', null, $value))->__toString(); + return self::greaterThan('$updatedAt', $value); } /** @@ -393,7 +403,7 @@ class Query implements \JsonSerializable */ public static function updatedBetween(string $start, string $end): string { - return (new Query('updatedBetween', null, [$start, $end]))->__toString(); + return self::between('$updatedAt', $start, $end); } /** diff --git a/templates/php/src/Services/Service.php.twig b/templates/php/src/Services/Service.php.twig index 32338d2d54..b9b9c13109 100644 --- a/templates/php/src/Services/Service.php.twig +++ b/templates/php/src/Services/Service.php.twig @@ -10,14 +10,9 @@ use {{ spec.title | caseUcfirst }}\InputFile; {% for method in service.methods %} {% for parameter in method.parameters.all %} {% if parameter.enumValues is not empty %} -{% if parameter.enumName is not empty %} -{% set name = parameter.enumName %} -{% else %} -{% set name = parameter.name %} -{% endif %} -{% if name not in added %} -use {{ spec.title | caseUcfirst }}\Enums\{{ name | caseUcfirst }}; -{% set added = added|merge([name]) %} +{% if parameter.enumName not in added %} +use {{ spec.title | caseUcfirst }}\Enums\{{ parameter.enumName | caseUcfirst }}; +{% set added = added|merge([parameter.enumName]) %} {% endif %} {% endif %} {% endfor %} @@ -30,7 +25,17 @@ class {{ service.name | caseUcfirst }} extends Service parent::__construct($client); } +{% set nonDeprecatedMethodNames = [] %} {% for method in service.methods %} +{% if not method.deprecated %} +{% set nonDeprecatedMethodNames = nonDeprecatedMethodNames|merge([method.name | caseCamel | lower]) %} +{% endif %} +{% endfor %} +{% for method in service.methods %} +{% set methodNameLower = method.name | caseCamel | lower %} +{% if method.deprecated and methodNameLower in nonDeprecatedMethodNames %} +{# Skip deprecated methods that have namespace collisions with non-deprecated methods #} +{% else %} {% set deprecated_message = '' %} /** {% if method.description %} @@ -72,6 +77,7 @@ class {{ service.name | caseUcfirst }} extends Service } {%~ if not loop.last %} + {%~ endif %} {%~ endif %} {%~ endfor %} } \ No newline at end of file diff --git a/templates/php/tests/IDTest.php.twig b/templates/php/tests/IDTest.php.twig index a48a6b5d83..344cc8bbc1 100644 --- a/templates/php/tests/IDTest.php.twig +++ b/templates/php/tests/IDTest.php.twig @@ -6,7 +6,8 @@ use PHPUnit\Framework\TestCase; final class IDTest extends TestCase { public function testUnique(): void { - $this->assertSame('unique()', ID::unique()); + $id = ID::unique(); + $this->assertSame(20, strlen($id)); } public function testCustom(): void { diff --git a/templates/php/tests/OperatorTest.php.twig b/templates/php/tests/OperatorTest.php.twig new file mode 100644 index 0000000000..d15334836d --- /dev/null +++ b/templates/php/tests/OperatorTest.php.twig @@ -0,0 +1,92 @@ +assertSame('{"method":"increment","values":[1]}', Operator::increment()); + $this->assertSame('{"method":"increment","values":[5,100]}', Operator::increment(5, 100)); + } + + public function testDecrement(): void { + $this->assertSame('{"method":"decrement","values":[1]}', Operator::decrement()); + $this->assertSame('{"method":"decrement","values":[3,0]}', Operator::decrement(3, 0)); + } + + public function testMultiply(): void { + $this->assertSame('{"method":"multiply","values":[2]}', Operator::multiply(2)); + $this->assertSame('{"method":"multiply","values":[3,1000]}', Operator::multiply(3, 1000)); + } + + public function testDivide(): void { + $this->assertSame('{"method":"divide","values":[2]}', Operator::divide(2)); + $this->assertSame('{"method":"divide","values":[4,1]}', Operator::divide(4, 1)); + } + + public function testModulo(): void { + $this->assertSame('{"method":"modulo","values":[5]}', Operator::modulo(5)); + } + + public function testPower(): void { + $this->assertSame('{"method":"power","values":[2]}', Operator::power(2)); + $this->assertSame('{"method":"power","values":[3,100]}', Operator::power(3, 100)); + } + + public function testArrayAppend(): void { + $this->assertSame('{"method":"arrayAppend","values":["item1","item2"]}', Operator::arrayAppend(['item1', 'item2'])); + } + + public function testArrayPrepend(): void { + $this->assertSame('{"method":"arrayPrepend","values":["first","second"]}', Operator::arrayPrepend(['first', 'second'])); + } + + public function testArrayInsert(): void { + $this->assertSame('{"method":"arrayInsert","values":[0,"newItem"]}', Operator::arrayInsert(0, 'newItem')); + } + + public function testArrayRemove(): void { + $this->assertSame('{"method":"arrayRemove","values":["oldItem"]}', Operator::arrayRemove('oldItem')); + } + + public function testArrayUnique(): void { + $this->assertSame('{"method":"arrayUnique","values":[]}', Operator::arrayUnique()); + } + + public function testArrayIntersect(): void { + $this->assertSame('{"method":"arrayIntersect","values":["a","b","c"]}', Operator::arrayIntersect(['a', 'b', 'c'])); + } + + public function testArrayDiff(): void { + $this->assertSame('{"method":"arrayDiff","values":["x","y"]}', Operator::arrayDiff(['x', 'y'])); + } + + public function testArrayFilter(): void { + $this->assertSame('{"method":"arrayFilter","values":["equal","test"]}', Operator::arrayFilter(Condition::Equal, 'test')); + } + + public function testStringConcat(): void { + $this->assertSame('{"method":"stringConcat","values":["suffix"]}', Operator::stringConcat('suffix')); + } + + public function testStringReplace(): void { + $this->assertSame('{"method":"stringReplace","values":["old","new"]}', Operator::stringReplace('old', 'new')); + } + + public function testToggle(): void { + $this->assertSame('{"method":"toggle","values":[]}', Operator::toggle()); + } + + public function testDateAddDays(): void { + $this->assertSame('{"method":"dateAddDays","values":[7]}', Operator::dateAddDays(7)); + } + + public function testDateSubDays(): void { + $this->assertSame('{"method":"dateSubDays","values":[3]}', Operator::dateSubDays(3)); + } + + public function testDateSetNow(): void { + $this->assertSame('{"method":"dateSetNow","values":[]}', Operator::dateSetNow()); + } +} diff --git a/templates/php/tests/QueryTest.php.twig b/templates/php/tests/QueryTest.php.twig index 20c84dada6..31e73ea713 100644 --- a/templates/php/tests/QueryTest.php.twig +++ b/templates/php/tests/QueryTest.php.twig @@ -37,165 +37,250 @@ final class QueryTest extends TestCase { public function testBasicFilterEqual(): void { foreach($this->tests as $test) { - $this->assertSame( - "equal(\"attr\", $test->expectedValues)", - Query::equal('attr', $test->value), - $test->description, - ); + $query = json_decode(Query::equal('attr', $test->value), true); + $expected = json_decode($test->expectedValues, true); + $this->assertSame('attr', $query['attribute'], $test->description); + $this->assertSame($expected, $query['values'], $test->description); + $this->assertSame('equal', $query['method'], $test->description); } } public function testBasicFilterNotEqual(): void { foreach($this->tests as $test) { - $this->assertSame( - "notEqual(\"attr\", $test->expectedValues)", - Query::notEqual('attr', $test->value), - $test->description, - ); + $query = json_decode(Query::notEqual('attr', $test->value), true); + $expected = json_decode($test->expectedValues, true); + $this->assertSame('attr', $query['attribute'], $test->description); + $this->assertSame($expected, $query['values'], $test->description); + $this->assertSame('notEqual', $query['method'], $test->description); } } public function testBasicFilterLessThan(): void { foreach($this->tests as $test) { - $this->assertSame( - "lessThan(\"attr\", $test->expectedValues)", - Query::lessThan('attr', $test->value), - $test->description, - ); + $query = json_decode(Query::lessThan('attr', $test->value), true); + $expected = json_decode($test->expectedValues, true); + $this->assertSame('attr', $query['attribute'], $test->description); + $this->assertSame($expected, $query['values'], $test->description); + $this->assertSame('lessThan', $query['method'], $test->description); } } public function testBasicFilterLessThanEqual(): void { foreach($this->tests as $test) { - $this->assertSame( - "lessThanEqual(\"attr\", $test->expectedValues)", - Query::lessThanEqual('attr', $test->value), - $test->description, - ); + $query = json_decode(Query::lessThanEqual('attr', $test->value), true); + $expected = json_decode($test->expectedValues, true); + $this->assertSame('attr', $query['attribute'], $test->description); + $this->assertSame($expected, $query['values'], $test->description); + $this->assertSame('lessThanEqual', $query['method'], $test->description); } } public function testBasicFilterGreaterThan(): void { foreach($this->tests as $test) { - $this->assertSame( - "greaterThan(\"attr\", $test->expectedValues)", - Query::greaterThan('attr', $test->value), - $test->description, - ); + $query = json_decode(Query::greaterThan('attr', $test->value), true); + $expected = json_decode($test->expectedValues, true); + $this->assertSame('attr', $query['attribute'], $test->description); + $this->assertSame($expected, $query['values'], $test->description); + $this->assertSame('greaterThan', $query['method'], $test->description); } } public function testBasicFilterGreaterThanEqual(): void { foreach($this->tests as $test) { - $this->assertSame( - "greaterThanEqual(\"attr\", $test->expectedValues)", - Query::greaterThanEqual('attr', $test->value), - $test->description, - ); + $query = json_decode(Query::greaterThanEqual('attr', $test->value), true); + $expected = json_decode($test->expectedValues, true); + $this->assertSame('attr', $query['attribute'], $test->description); + $this->assertSame($expected, $query['values'], $test->description); + $this->assertSame('greaterThanEqual', $query['method'], $test->description); } } public function testSearch(): void { - $this->assertSame('search("attr", ["keyword1 keyword2"])', Query::search('attr', 'keyword1 keyword2')); + $query = json_decode(Query::search('attr', 'keyword1 keyword2'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame(['keyword1 keyword2'], $query['values']); + $this->assertSame('search', $query['method']); } public function testIsNull(): void { - $this->assertSame('isNull("attr")', Query::isNull('attr')); + $query = json_decode(Query::isNull('attr'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertNull($query['values'] ?? null); + $this->assertSame('isNull', $query['method']); } public function testIsNotNull(): void { - $this->assertSame('isNotNull("attr")', Query::isNotNull('attr')); + $query = json_decode(Query::isNotNull('attr'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertNull($query['values'] ?? null); + $this->assertSame('isNotNull', $query['method']); } public function testBetweenWithIntegers(): void { - $this->assertSame('between("attr", 1, 2)', Query::between('attr', 1, 2)); + $query = json_decode(Query::between('attr', 1, 2), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame([1, 2], $query['values']); + $this->assertSame('between', $query['method']); } public function testBetweenWithDoubles(): void { - $this->assertSame('between("attr", 1, 2)', Query::between('attr', 1.0, 2.0)); + $query = json_decode(Query::between('attr', 1.0, 2.0), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame([1, 2], $query['values']); + $this->assertSame('between', $query['method']); } public function testBetweenWithStrings(): void { - $this->assertSame('between("attr", "a", "z")', Query::between('attr', 'a', 'z')); + $query = json_decode(Query::between('attr', 'a', 'z'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame(['a', 'z'], $query['values']); + $this->assertSame('between', $query['method']); } public function testSelect(): void { - $this->assertSame('select(["attr1","attr2"])', Query::select(['attr1', 'attr2'])); + $query = json_decode(Query::select(['attr1', 'attr2']), true); + $this->assertNull($query['attribute'] ?? null); + $this->assertSame(['attr1', 'attr2'], $query['values']); + $this->assertSame('select', $query['method']); } public function testOrderAsc(): void { - $this->assertSame('orderAsc("attr")', Query::orderAsc('attr')); + $query = json_decode(Query::orderAsc('attr'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertNull($query['values'] ?? null); + $this->assertSame('orderAsc', $query['method']); } public function testOrderDesc(): void { - $this->assertSame('orderDesc("attr")', Query::orderDesc('attr')); + $query = json_decode(Query::orderDesc('attr'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertNull($query['values'] ?? null); + $this->assertSame('orderDesc', $query['method']); + } + + public function testOrderRandom(): void { + $query = json_decode(Query::orderRandom(), true); + $this->assertNull($query['attribute'] ?? null); + $this->assertNull($query['values'] ?? null); + $this->assertSame('orderRandom', $query['method']); } public function testCursorBefore(): void { - $this->assertSame('cursorBefore("attr")', Query::cursorBefore('attr')); + $query = json_decode(Query::cursorBefore('attr'), true); + $this->assertNull($query['attribute'] ?? null); + $this->assertSame(['attr'], $query['values']); + $this->assertSame('cursorBefore', $query['method']); } public function testCursorAfter(): void { - $this->assertSame('cursorAfter("attr")', Query::cursorAfter('attr')); + $query = json_decode(Query::cursorAfter('attr'), true); + $this->assertNull($query['attribute'] ?? null); + $this->assertSame(['attr'], $query['values']); + $this->assertSame('cursorAfter', $query['method']); } public function testLimit(): void { - $this->assertSame('limit(1)', Query::limit(1)); + $query = json_decode(Query::limit(1), true); + $this->assertNull($query['attribute'] ?? null); + $this->assertSame([1], $query['values']); + $this->assertSame('limit', $query['method']); } public function testOffset(): void { - $this->assertSame('offset(1)', Query::offset(1)); + $query = json_decode(Query::offset(1), true); + $this->assertNull($query['attribute'] ?? null); + $this->assertSame([1], $query['values']); + $this->assertSame('offset', $query['method']); } public function testNotContains(): void { - $this->assertSame('notContains("attr", ["value"])', Query::notContains('attr', 'value')); + $query = json_decode(Query::notContains('attr', 'value'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame(['value'], $query['values']); + $this->assertSame('notContains', $query['method']); } public function testNotSearch(): void { - $this->assertSame('notSearch("attr", ["keyword1 keyword2"])', Query::notSearch('attr', 'keyword1 keyword2')); + $query = json_decode(Query::notSearch('attr', 'keyword1 keyword2'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame(['keyword1 keyword2'], $query['values']); + $this->assertSame('notSearch', $query['method']); } public function testNotBetweenWithIntegers(): void { - $this->assertSame('notBetween("attr", 1, 2)', Query::notBetween('attr', 1, 2)); + $query = json_decode(Query::notBetween('attr', 1, 2), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame([1, 2], $query['values']); + $this->assertSame('notBetween', $query['method']); } public function testNotBetweenWithDoubles(): void { - $this->assertSame('notBetween("attr", 1, 2)', Query::notBetween('attr', 1.0, 2.0)); + $query = json_decode(Query::notBetween('attr', 1.0, 2.0), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame([1, 2], $query['values']); + $this->assertSame('notBetween', $query['method']); } public function testNotBetweenWithStrings(): void { - $this->assertSame('notBetween("attr", "a", "z")', Query::notBetween('attr', 'a', 'z')); + $query = json_decode(Query::notBetween('attr', 'a', 'z'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame(['a', 'z'], $query['values']); + $this->assertSame('notBetween', $query['method']); } public function testNotStartsWith(): void { - $this->assertSame('notStartsWith("attr", ["prefix"])', Query::notStartsWith('attr', 'prefix')); + $query = json_decode(Query::notStartsWith('attr', 'prefix'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame(['prefix'], $query['values']); + $this->assertSame('notStartsWith', $query['method']); } public function testNotEndsWith(): void { - $this->assertSame('notEndsWith("attr", ["suffix"])', Query::notEndsWith('attr', 'suffix')); + $query = json_decode(Query::notEndsWith('attr', 'suffix'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame(['suffix'], $query['values']); + $this->assertSame('notEndsWith', $query['method']); } public function testCreatedBefore(): void { - $this->assertSame('createdBefore("2023-01-01")', Query::createdBefore('2023-01-01')); + $query = json_decode(Query::createdBefore('2023-01-01'), true); + $this->assertSame('$createdAt', $query['attribute']); + $this->assertSame(['2023-01-01'], $query['values']); + $this->assertSame('lessThan', $query['method']); } public function testCreatedAfter(): void { - $this->assertSame('createdAfter("2023-01-01")', Query::createdAfter('2023-01-01')); + $query = json_decode(Query::createdAfter('2023-01-01'), true); + $this->assertSame('$createdAt', $query['attribute']); + $this->assertSame(['2023-01-01'], $query['values']); + $this->assertSame('greaterThan', $query['method']); } public function testCreatedBetween(): void { - $this->assertSame('{"method":"createdBetween","values":["2023-01-01","2023-12-31"]}', Query::createdBetween('2023-01-01', '2023-12-31')); + $query = json_decode(Query::createdBetween('2023-01-01', '2023-12-31'), true); + $this->assertSame('$createdAt', $query['attribute']); + $this->assertSame(['2023-01-01', '2023-12-31'], $query['values']); + $this->assertSame('between', $query['method']); } public function testUpdatedBefore(): void { - $this->assertSame('updatedBefore("2023-01-01")', Query::updatedBefore('2023-01-01')); + $query = json_decode(Query::updatedBefore('2023-01-01'), true); + $this->assertSame('$updatedAt', $query['attribute']); + $this->assertSame(['2023-01-01'], $query['values']); + $this->assertSame('lessThan', $query['method']); } public function testUpdatedAfter(): void { - $this->assertSame('updatedAfter("2023-01-01")', Query::updatedAfter('2023-01-01')); + $query = json_decode(Query::updatedAfter('2023-01-01'), true); + $this->assertSame('$updatedAt', $query['attribute']); + $this->assertSame(['2023-01-01'], $query['values']); + $this->assertSame('greaterThan', $query['method']); } public function testUpdatedBetween(): void { - $this->assertSame('{"method":"updatedBetween","values":["2023-01-01","2023-12-31"]}', Query::updatedBetween('2023-01-01', '2023-12-31')); + $query = json_decode(Query::updatedBetween('2023-01-01', '2023-12-31'), true); + $this->assertSame('$updatedAt', $query['attribute']); + $this->assertSame(['2023-01-01', '2023-12-31'], $query['values']); + $this->assertSame('between', $query['method']); } } diff --git a/templates/php/tests/Services/ServiceTest.php.twig b/templates/php/tests/Services/ServiceTest.php.twig index 6e31e9bf3a..781c4aa4c6 100644 --- a/templates/php/tests/Services/ServiceTest.php.twig +++ b/templates/php/tests/Services/ServiceTest.php.twig @@ -6,6 +6,17 @@ use Appwrite\Client; use Appwrite\InputFile; use Mockery; use PHPUnit\Framework\TestCase; +{% set added = [] %} +{% for method in service.methods %} +{% for parameter in method.parameters.all %} +{% if parameter.enumName is not empty %} +{% if parameter.enumName not in added %} +use Appwrite\Enums\{{ parameter.enumName | caseUcfirst }}; +{% set added = added|merge([parameter.enumName]) %} +{% endif %} +{% endif %} +{% endfor %} +{% endfor %} final class {{service.name | caseUcfirst}}Test extends TestCase { private $client; @@ -16,29 +27,40 @@ final class {{service.name | caseUcfirst}}Test extends TestCase { $this->{{ service.name | caseCamel }} = new {{ service.name | caseUcfirst }}($this->client); } +{% set nonDeprecatedMethodNames = [] %} +{% for method in service.methods %} +{% if not method.deprecated %} +{% set nonDeprecatedMethodNames = nonDeprecatedMethodNames|merge([method.name | caseCamel | lower]) %} +{% endif %} +{% endfor %} {% for method in service.methods %} +{% set methodNameLower = method.name | caseCamel | lower %} +{% if method.deprecated and methodNameLower in nonDeprecatedMethodNames %} +{# Skip deprecated methods that have namespace collisions with non-deprecated methods #} +{% else %} public function testMethod{{method.name | caseUcfirst}}(): void { {%~ if method.responseModel and method.responseModel != 'any' ~%} $data = array( {%- 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' %}array(){% elseif property.type == 'array' %}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 -%} ); - {%~ elseif (method.responseModel and method.responseModel == 'any') or method.type == 'webAuth' ~%} + {%~ elseif (method.responseModel and method.responseModel == 'any') or (method | getReturn == 'array') ~%} $data = array(); {%~ else ~%} $data = ''; {%~ endif ~%} $this->client - ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any(){% if method.type == 'webAuth' %}, Mockery::any(){% endif %}) ->andReturn($data); $response = $this->{{service.name | caseCamel}}->{{method.name | caseCamel}}({%~ for parameter in method.parameters.all | filter((param) => param.required) ~%} - {% if parameter.type == 'object' %}array(){% elseif parameter.type == 'array' %}array(){% elseif parameter.type == 'file' %}InputFile::withData('', "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 ~%}{% if not loop.last %},{% endif %}{%~ endfor ~%} + {% if parameter.enumName %}{{ parameter.enumName | caseUcfirst }}::{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}(){% elseif parameter.type == 'object' %}array(){% elseif parameter.type == 'array' %}array(){% elseif parameter.type == 'file' %}InputFile::withData('', "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 ~%}{% if not loop.last %},{% endif %}{%~ endfor ~%} ); $this->assertSame($data, $response); } +{% endif %} {% endfor %} } diff --git a/templates/python/base/params.twig b/templates/python/base/params.twig index 8a574ec969..824d61116f 100644 --- a/templates/python/base/params.twig +++ b/templates/python/base/params.twig @@ -12,20 +12,22 @@ {% endfor %} {% for parameter in method.parameters.query %} - api_params['{{ parameter.name }}'] = {{ parameter.name | escapeKeyword | caseSnake }} -{% endfor %} -{% for parameter in method.parameters.body %} -{% if method.consumes[0] == "multipart/form-data" and ( parameter.type != "string" and parameter.type != "array" ) %} - api_params['{{ parameter.name }}'] = str({{ parameter.name | escapeKeyword | caseSnake }}).lower() if type({{ parameter.name | escapeKeyword | caseSnake }}) is bool else {{ parameter.name | escapeKeyword | caseSnake }} +{% if not parameter.nullable and not parameter.required %} + if {{ parameter.name | escapeKeyword | caseSnake }} is not None: + api_params['{{ parameter.name }}'] = {{ parameter.name | escapeKeyword | caseSnake }} {% else %} api_params['{{ parameter.name }}'] = {{ parameter.name | escapeKeyword | caseSnake }} {% endif %} {% endfor %} -{% for parameter in method.parameters.formData %} -{% if method.consumes[0] == "multipart/form-data" and ( parameter.type != "string" and parameter.type != "array" ) %} - api_params['{{ parameter.name }}'] = str({{ parameter.name | escapeKeyword | caseSnake }}).lower() if type({{ parameter.name | escapeKeyword | caseSnake }}) is bool else {{ parameter.name | escapeKeyword | caseSnake }} +{% for parameter in method.parameters.body|merge(method.parameters.formData|default([])) %} +{% set paramName = parameter.name | escapeKeyword | caseSnake %} +{% set isMultipart = method.consumes|length > 0 and method.consumes[0] == "multipart/form-data" %} +{% set formattedValue = paramName | formatParamValue(parameter.type, isMultipart) %} +{% if not parameter.nullable and not parameter.required %} + if {{ paramName }} is not None: + api_params['{{ parameter.name }}'] = {{ formattedValue }} {% else %} - api_params['{{ parameter.name }}'] = {{ parameter.name | escapeKeyword | caseSnake }} + api_params['{{ parameter.name }}'] = {{ formattedValue }} {% endif %} {% endfor %} {% endif %} \ No newline at end of file diff --git a/templates/python/docs/example.md.twig b/templates/python/docs/example.md.twig index e61cc6d12c..c60e0141e5 100644 --- a/templates/python/docs/example.md.twig +++ b/templates/python/docs/example.md.twig @@ -5,20 +5,17 @@ from {{ spec.title | caseSnake }}.input_file import InputFile {% endif %} {% set added = [] %} {% for parameter in method.parameters.all %} -{% if method == parameter.required %} {% if parameter.enumValues is not empty %} -{% if parameter.enumName is not empty %} -{% set name = parameter.enumName %} -{% else %} -{% set name = parameter.name %} -{% endif %} -{% if name not in added %} +{% if parameter.enumName not in added %} from {{ spec.title | caseSnake }}.enums import {{parameter.enumName | caseUcfirst}} -{% set added = added|merge([name]) %} -{% endif %} +{% set added = added|merge([parameter.enumName]) %} {% endif %} {% endif %} {% endfor %} +{% if method.parameters.all | hasPermissionParam %} +from {{ spec.title | caseSnake }}.permission import Permission +from {{ spec.title | caseSnake }}.role import Role +{% endif %} client = Client() {% if method.auth|length > 0 %} @@ -35,7 +32,7 @@ client.set_{{header | caseSnake}}('{{node[header]['x-appwrite']['demo'] | raw }} result = {{ service.name | caseSnake }}.{{ method.name | caseSnake }}({% if method.parameters.all | length == 0 %}){% endif %} {%~ for parameter in method.parameters.all %} - {{ parameter.name | caseSnake }} = {% if parameter.enumValues | length > 0 %}{{ parameter.enumName }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %}{% if not parameter.required %} # optional{% endif %} + {{ parameter.name | caseSnake }} = {% if parameter.enumValues | length > 0 %}{{ parameter.enumName | caseUcfirst }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %}{% if not parameter.required %} # optional{% endif %} {%~ endfor %} {% if method.parameters.all | length > 0 %}) diff --git a/templates/python/package/client.py.twig b/templates/python/package/client.py.twig index f9d4b90f13..936f6974e1 100644 --- a/templates/python/package/client.py.twig +++ b/templates/python/package/client.py.twig @@ -2,6 +2,7 @@ import io import json import os import platform +import sys import requests from .input_file import InputFile from .exception import {{spec.title | caseUcfirst}}Exception @@ -56,8 +57,6 @@ 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 = {} files = {} stringify = False @@ -98,7 +97,7 @@ class Client: warnings = response.headers.get('x-{{ spec.title | lower }}-warning') if warnings: for warning in warnings.split(';'): - print(f'Warning: {warning}') + print(f'Warning: {warning}', file=sys.stderr) content_type = response.headers['Content-Type'] diff --git a/templates/python/package/encoders/value_class_encoder.py.twig b/templates/python/package/encoders/value_class_encoder.py.twig index ee0bb49c60..ecd999eb24 100644 --- a/templates/python/package/encoders/value_class_encoder.py.twig +++ b/templates/python/package/encoders/value_class_encoder.py.twig @@ -1,11 +1,11 @@ import json -{%~ for enum in spec.enums %} +{%~ for enum in spec.allEnums %} from ..enums.{{ enum.name | caseSnake }} import {{ enum.name | caseUcfirst | overrideIdentifier }} {%~ endfor %} class ValueClassEncoder(json.JSONEncoder): def default(self, o): - {%~ for enum in spec.enums %} + {%~ for enum in spec.allEnums %} if isinstance(o, {{ enum.name | caseUcfirst | overrideIdentifier }}): return o.value diff --git a/templates/python/package/operator.py.twig b/templates/python/package/operator.py.twig new file mode 100644 index 0000000000..ddec91ec7c --- /dev/null +++ b/templates/python/package/operator.py.twig @@ -0,0 +1,150 @@ +import json +import math +from enum import Enum + +class Condition(Enum): + EQUAL = "equal" + NOT_EQUAL = "notEqual" + GREATER_THAN = "greaterThan" + GREATER_THAN_EQUAL = "greaterThanEqual" + LESS_THAN = "lessThan" + LESS_THAN_EQUAL = "lessThanEqual" + CONTAINS = "contains" + IS_NULL = "isNull" + IS_NOT_NULL = "isNotNull" + +class Operator(): + def __init__(self, method, values=None): + self.method = method + + if values is not None: + self.values = values if isinstance(values, list) else [values] + + def __str__(self): + return json.dumps( + self.__dict__, + separators=(",", ":"), + default=lambda obj: obj.__dict__ + ) + + @staticmethod + def increment(value=1, max=None): + if isinstance(value, float) and (math.isnan(value) or math.isinf(value)): + raise ValueError("Value cannot be NaN or Infinity") + if max is not None and isinstance(max, float) and (math.isnan(max) or math.isinf(max)): + raise ValueError("Max cannot be NaN or Infinity") + values = [value] + if max is not None: + values.append(max) + return str(Operator("increment", values)) + + @staticmethod + def decrement(value=1, min=None): + if isinstance(value, float) and (math.isnan(value) or math.isinf(value)): + raise ValueError("Value cannot be NaN or Infinity") + if min is not None and isinstance(min, float) and (math.isnan(min) or math.isinf(min)): + raise ValueError("Min cannot be NaN or Infinity") + values = [value] + if min is not None: + values.append(min) + return str(Operator("decrement", values)) + + @staticmethod + def multiply(factor, max=None): + if isinstance(factor, float) and (math.isnan(factor) or math.isinf(factor)): + raise ValueError("Factor cannot be NaN or Infinity") + if max is not None and isinstance(max, float) and (math.isnan(max) or math.isinf(max)): + raise ValueError("Max cannot be NaN or Infinity") + values = [factor] + if max is not None: + values.append(max) + return str(Operator("multiply", values)) + + @staticmethod + def divide(divisor, min=None): + if isinstance(divisor, float) and (math.isnan(divisor) or math.isinf(divisor)): + raise ValueError("Divisor cannot be NaN or Infinity") + if min is not None and isinstance(min, float) and (math.isnan(min) or math.isinf(min)): + raise ValueError("Min cannot be NaN or Infinity") + if divisor == 0: + raise ValueError("Divisor cannot be zero") + values = [divisor] + if min is not None: + values.append(min) + return str(Operator("divide", values)) + + @staticmethod + def modulo(divisor): + if isinstance(divisor, float) and (math.isnan(divisor) or math.isinf(divisor)): + raise ValueError("Divisor cannot be NaN or Infinity") + if divisor == 0: + raise ValueError("Divisor cannot be zero") + return str(Operator("modulo", [divisor])) + + @staticmethod + def power(exponent, max=None): + if isinstance(exponent, float) and (math.isnan(exponent) or math.isinf(exponent)): + raise ValueError("Exponent cannot be NaN or Infinity") + if max is not None and isinstance(max, float) and (math.isnan(max) or math.isinf(max)): + raise ValueError("Max cannot be NaN or Infinity") + values = [exponent] + if max is not None: + values.append(max) + return str(Operator("power", values)) + + @staticmethod + def array_append(values): + return str(Operator("arrayAppend", values)) + + @staticmethod + def array_prepend(values): + return str(Operator("arrayPrepend", values)) + + @staticmethod + def array_insert(index, value): + return str(Operator("arrayInsert", [index, value])) + + @staticmethod + def array_remove(value): + return str(Operator("arrayRemove", [value])) + + @staticmethod + def array_unique(): + return str(Operator("arrayUnique", [])) + + @staticmethod + def array_intersect(values): + return str(Operator("arrayIntersect", values)) + + @staticmethod + def array_diff(values): + return str(Operator("arrayDiff", values)) + + @staticmethod + def array_filter(condition, value=None): + values = [condition.value if isinstance(condition, Condition) else condition, value] + return str(Operator("arrayFilter", values)) + + @staticmethod + def string_concat(value): + return str(Operator("stringConcat", [value])) + + @staticmethod + def string_replace(search, replace): + return str(Operator("stringReplace", [search, replace])) + + @staticmethod + def toggle(): + return str(Operator("toggle", [])) + + @staticmethod + def date_add_days(days): + return str(Operator("dateAddDays", [days])) + + @staticmethod + def date_sub_days(days): + return str(Operator("dateSubDays", [days])) + + @staticmethod + def date_set_now(): + return str(Operator("dateSetNow", [])) diff --git a/templates/python/package/query.py.twig b/templates/python/package/query.py.twig index a0127e5793..9d3e69c9b8 100644 --- a/templates/python/package/query.py.twig +++ b/templates/python/package/query.py.twig @@ -1,6 +1,5 @@ import json - # Inherit from dict to allow for easy serialization class Query(): def __init__(self, method, attribute=None, values=None): @@ -79,6 +78,10 @@ class Query(): def order_desc(attribute): return str(Query("orderDesc", attribute, None)) + @staticmethod + def order_random(): + return str(Query("orderRandom", None, None)) + @staticmethod def cursor_before(id): return str(Query("cursorBefore", None, id)) @@ -121,27 +124,27 @@ class Query(): @staticmethod def created_before(value): - return str(Query("createdBefore", None, value)) + return Query.less_than("$createdAt", value) @staticmethod def created_after(value): - return str(Query("createdAfter", None, value)) + return Query.greater_than("$createdAt", value) @staticmethod def created_between(start, end): - return str(Query("createdBetween", None, [start, end])) + return Query.between("$createdAt", start, end) @staticmethod def updated_before(value): - return str(Query("updatedBefore", None, value)) + return Query.less_than("$updatedAt", value) @staticmethod def updated_after(value): - return str(Query("updatedAfter", None, value)) + return Query.greater_than("$updatedAt", value) @staticmethod def updated_between(start, end): - return str(Query("updatedBetween", None, [start, end])) + return Query.between("$updatedAt", start, end) @staticmethod def or_queries(queries): diff --git a/templates/python/package/service.py.twig b/templates/python/package/service.py.twig index b5b60e6c22..f5e2adb5f6 100644 --- a/templates/python/package/service.py.twig +++ b/templates/python/package/service.py.twig @@ -1,6 +1,5 @@ from .client import Client - class Service: def __init__(self, client: Client): self.client = client diff --git a/templates/python/package/services/service.py.twig b/templates/python/package/services/service.py.twig index 250f77f375..0f5bb03812 100644 --- a/templates/python/package/services/service.py.twig +++ b/templates/python/package/services/service.py.twig @@ -1,6 +1,7 @@ from ..service import Service -from typing import List, Dict, Any +from typing import List, Dict, Any, Optional from ..exception import AppwriteException +from appwrite.utils.deprecated import deprecated {% set added = [] %} {% for method in service.methods %} {% for parameter in method.parameters.all %} @@ -9,14 +10,9 @@ from ..input_file import InputFile {% set added = added|merge(['InputFile']) %} {% endif %} {% if parameter.enumValues is not empty%} -{% if parameter.enumName is not empty %} -{% set name = parameter.enumName %} -{% else %} -{% set name = parameter.name %} -{% endif %} -{% if name not in added %} -from ..enums.{{ name | caseSnake }} import {{ name | caseUcfirst }}; -{% set added = added|merge([name]) %} +{% if parameter.enumName not in added %} +from ..enums.{{ parameter.enumName | caseSnake }} import {{ parameter.enumName | caseUcfirst }}; +{% set added = added|merge([parameter.enumName]) %} {% endif %} {% endif %} {% endfor %} @@ -39,6 +35,13 @@ class {{ service.name | caseUcfirst }}(Service): {% endif %} {% if not shouldSkip %} +{% if method.deprecated %} +{% if method.since and method.replaceWith %} + @deprecated("This API has been deprecated since {{ method.since }}. Please use `{{ method.replaceWith | caseSnakeExceptFirstDot }}` instead.") +{% else %} + @deprecated("This API has been deprecated.") +{% endif %} +{% endif %} def {{ method.name | caseSnake }}(self{% if method.parameters.all|length > 0 %}, {% endif %}{% for parameter in method.parameters.all %}{{ parameter.name | escapeKeyword | caseSnake }}: {{ parameter | getPropertyType(method) | raw }}{% if not parameter.required %} = None{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, on_progress = None{% endif %}) -> {% if method.type == 'webAuth' %}str{% elseif method.type == 'location' %}bytes{% else %}Dict[str, Any]{% endif %}: """ {% autoescape false %}{{ method.description | replace({"\n": "\n "}) }}{% endautoescape %} diff --git a/templates/python/package/utils/__init__.py.twig b/templates/python/package/utils/__init__.py.twig new file mode 100644 index 0000000000..5bc7dd0e42 --- /dev/null +++ b/templates/python/package/utils/__init__.py.twig @@ -0,0 +1 @@ +# This file makes the 'utils' directory a Python package. diff --git a/templates/python/package/utils/deprecated.py.twig b/templates/python/package/utils/deprecated.py.twig new file mode 100644 index 0000000000..0e0e6e6e42 --- /dev/null +++ b/templates/python/package/utils/deprecated.py.twig @@ -0,0 +1,51 @@ +""" +A decorator to mark functions as deprecated. + +When the function is called, a DeprecationWarning is emitted. +Compatible with Python 2.7+ and Python 3.x. +""" + +import functools +import warnings + +def deprecated(reason=None): + """ + Decorator to mark functions as deprecated. + Emits a DeprecationWarning when the function is called. + + Args: + reason (str, optional): Reason for deprecation. Defaults to None. + + Usage: + @deprecated("Use another_function instead.") + def old_function(...): + ... + """ + def decorator(func): + message = "Call to deprecated function '{}'.{}".format( + func.__name__, + " " + reason if reason else "" + ) + + @functools.wraps(func) + def wrapped(*args, **kwargs): + warnings.simplefilter('always', DeprecationWarning) # show warning every time + try: + warnings.warn( + message, + category=DeprecationWarning, + stacklevel=2 + ) + return func(*args, **kwargs) + finally: + warnings.simplefilter('default', DeprecationWarning) # reset filter + return wrapped + + # Support both @deprecated and @deprecated("reason") + if callable(reason): + # Used as @deprecated without arguments + func = reason + reason = None + return decorator(func) + else: + return decorator diff --git a/templates/python/setup.py.twig b/templates/python/setup.py.twig index 02f0a444cd..d233ba2ec2 100644 --- a/templates/python/setup.py.twig +++ b/templates/python/setup.py.twig @@ -4,15 +4,10 @@ long_description: str with open("README.md", "r", encoding="utf-8") as readme_file_desc: long_description = readme_file_desc.read() - + setuptools.setup( name = '{{spec.title | caseSnake}}', - packages = [ - '{{spec.title | caseSnake}}', - '{{spec.title | caseSnake}}/services', - '{{spec.title | caseSnake}}/encoders', - '{{spec.title | caseSnake}}/enums', - ], + packages = setuptools.find_packages(), version = '{{sdk.version}}', license='{{spec.licenseName}}', description = '{{sdk.shortDescription}}', diff --git a/templates/react-native/docs/example.md.twig b/templates/react-native/docs/example.md.twig index 24266e9377..58ca374c5c 100644 --- a/templates/react-native/docs/example.md.twig +++ b/templates/react-native/docs/example.md.twig @@ -1,4 +1,4 @@ -import { Client, {{ service.name | caseUcfirst }}{% for parameter in method.parameters.all %}{% if parameter.enumValues | length > 0%}, {{ parameter.enumName | caseUcfirst}}{% endif %}{% endfor %} } from "{{ language.params.npmPackage }}"; +import { Client, {{ service.name | caseUcfirst }}{% for parameter in method.parameters.all %}{% if parameter.enumValues | length > 0%}, {{ parameter.enumName | caseUcfirst}}{% endif %}{% endfor %}{% if method.parameters.all | hasPermissionParam %}, Permission, Role{% endif %} } from "{{ language.params.npmPackage }}"; const client = new Client() {%~ if method.auth|length > 0 %} @@ -16,10 +16,10 @@ const {{ service.name | caseCamel }} = new {{service.name | caseUcfirst}}(client {% else %}{ {%~ for parameter in method.parameters.all %} {%~ if parameter.required %} - {{ parameter.name | caseCamel }}: {% if parameter.enumValues | length > 0 %}{{ parameter.enumName }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else%}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %} + {{ parameter.name | caseCamel }}: {% if parameter.enumValues | length > 0 %}{{ parameter.enumName | caseUcfirst }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else%}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %} {%~ else %} - {{ parameter.name | caseCamel }}: {% if parameter.enumValues | length > 0 %}{{ parameter.enumName }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else%}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %} // optional + {{ parameter.name | caseCamel }}: {% if parameter.enumValues | length > 0 %}{{ parameter.enumName | caseUcfirst }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else%}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %} // optional {%~ endif %} {%~ endfor -%} }); diff --git a/templates/react-native/package.json.twig b/templates/react-native/package.json.twig index 28a7e31ad5..73e3acb7c9 100644 --- a/templates/react-native/package.json.twig +++ b/templates/react-native/package.json.twig @@ -26,7 +26,7 @@ }, "devDependencies": { "@rollup/plugin-typescript": "8.3.2", - "playwright": "1.15.0", + "playwright": "1.56.1", "rollup": "2.75.4", "serve-handler": "6.1.0", "tslib": "2.4.0", @@ -37,7 +37,6 @@ "react-native": ">=0.76.7 <1.0.0" }, "peerDependencies": { - "expo": "*", - "react-native": "*" + "expo": "*" } } diff --git a/templates/react-native/src/client.ts.twig b/templates/react-native/src/client.ts.twig index dfb5d0d117..25becb0bed 100644 --- a/templates/react-native/src/client.ts.twig +++ b/templates/react-native/src/client.ts.twig @@ -129,6 +129,10 @@ class Client { * @returns {this} */ setEndpoint(endpoint: string): this { + if (!endpoint || typeof endpoint !== 'string') { + throw new {{spec.title | caseUcfirst}}Exception('Endpoint must be a valid string'); + } + if (!endpoint.startsWith('http://') && !endpoint.startsWith('https://')) { throw new {{spec.title | caseUcfirst}}Exception('Invalid endpoint URL: ' + endpoint); } @@ -147,6 +151,10 @@ class Client { * @returns {this} */ setEndpointRealtime(endpointRealtime: string): this { + if (!endpointRealtime || typeof endpointRealtime !== 'string') { + throw new {{spec.title | caseUcfirst}}Exception('Endpoint must be a valid string'); + } + if (!endpointRealtime.startsWith('ws://') && !endpointRealtime.startsWith('wss://')) { throw new {{spec.title | caseUcfirst}}Exception('Invalid realtime endpoint URL: ' + endpointRealtime); } diff --git a/templates/react-native/src/index.ts.twig b/templates/react-native/src/index.ts.twig index 2e0a975453..8a9b5aa947 100644 --- a/templates/react-native/src/index.ts.twig +++ b/templates/react-native/src/index.ts.twig @@ -8,6 +8,7 @@ export { Query } from './query'; export { Permission } from './permission'; export { Role } from './role'; export { ID } from './id'; -{% for enum in spec.enums %} +export { Operator, Condition } from './operator'; +{% for enum in spec.allEnums %} export { {{ enum.name | caseUcfirst }} } from './enums/{{enum.name | caseKebab}}'; {% endfor %} \ No newline at end of file diff --git a/templates/react-native/src/models.ts.twig b/templates/react-native/src/models.ts.twig index b9d8596e27..6b6ae32181 100644 --- a/templates/react-native/src/models.ts.twig +++ b/templates/react-native/src/models.ts.twig @@ -1,3 +1,9 @@ +{% if spec.responseEnums|length > 0 %} +{% for responseEnum in spec.responseEnums %} +import { {{ responseEnum.name }} } from "./enums/{{ responseEnum.name | caseKebab }}" +{% endfor %} +{% endif %} + export namespace Models { declare const __default: unique symbol; @@ -11,7 +17,7 @@ export namespace Models { /** * {{ property.description }} */ - {{ property.name }}{% if not property.required %}?{% endif %}: {{ property | getSubSchema(spec) | raw }}; + {{ property.name }}{% if not property.required %}?{% endif %}: {{ property | getSubSchema(spec, definition.name) | raw }}; {% endfor %} } {% if definition.additionalProperties %} diff --git a/templates/react-native/src/operator.ts.twig b/templates/react-native/src/operator.ts.twig new file mode 100644 index 0000000000..2386a6c414 --- /dev/null +++ b/templates/react-native/src/operator.ts.twig @@ -0,0 +1,308 @@ +type OperatorValuesSingle = string | number | boolean; +export type OperatorValuesList = string[] | number[] | boolean[] | any[]; +export type OperatorValues = OperatorValuesSingle | OperatorValuesList; + +export enum Condition { + Equal = "equal", + NotEqual = "notEqual", + GreaterThan = "greaterThan", + GreaterThanEqual = "greaterThanEqual", + LessThan = "lessThan", + LessThanEqual = "lessThanEqual", + Contains = "contains", + IsNull = "isNull", + IsNotNull = "isNotNull", +} + +/** + * Helper class to generate operator strings for atomic operations. + */ +export class Operator { + method: string; + values: OperatorValuesList | undefined; + + /** + * Constructor for Operator class. + * + * @param {string} method + * @param {OperatorValues} values + */ + constructor( + method: string, + values?: OperatorValues + ) { + this.method = method; + + if (values !== undefined) { + if (Array.isArray(values)) { + this.values = values; + } else { + this.values = [values] as OperatorValuesList; + } + } + } + + /** + * Convert the operator object to a JSON string. + * + * @returns {string} + */ + toString(): string { + return JSON.stringify({ + method: this.method, + values: this.values, + }); + } + + /** + * Increment a numeric attribute by a specified value. + * + * @param {number} value + * @param {number} max + * @returns {string} + */ + static increment = (value: number = 1, max?: number): string => { + if (isNaN(value) || !isFinite(value)) { + throw new Error("Value cannot be NaN or Infinity"); + } + if (max !== undefined && (isNaN(max) || !isFinite(max))) { + throw new Error("Max cannot be NaN or Infinity"); + } + const values: any[] = [value]; + if (max !== undefined) { + values.push(max); + } + return new Operator("increment", values).toString(); + }; + + /** + * Decrement a numeric attribute by a specified value. + * + * @param {number} value + * @param {number} min + * @returns {string} + */ + static decrement = (value: number = 1, min?: number): string => { + if (isNaN(value) || !isFinite(value)) { + throw new Error("Value cannot be NaN or Infinity"); + } + if (min !== undefined && (isNaN(min) || !isFinite(min))) { + throw new Error("Min cannot be NaN or Infinity"); + } + const values: any[] = [value]; + if (min !== undefined) { + values.push(min); + } + return new Operator("decrement", values).toString(); + }; + + /** + * Multiply a numeric attribute by a specified factor. + * + * @param {number} factor + * @param {number} max + * @returns {string} + */ + static multiply = (factor: number, max?: number): string => { + if (isNaN(factor) || !isFinite(factor)) { + throw new Error("Factor cannot be NaN or Infinity"); + } + if (max !== undefined && (isNaN(max) || !isFinite(max))) { + throw new Error("Max cannot be NaN or Infinity"); + } + const values: any[] = [factor]; + if (max !== undefined) { + values.push(max); + } + return new Operator("multiply", values).toString(); + }; + + /** + * Divide a numeric attribute by a specified divisor. + * + * @param {number} divisor + * @param {number} min + * @returns {string} + */ + static divide = (divisor: number, min?: number): string => { + if (isNaN(divisor) || !isFinite(divisor)) { + throw new Error("Divisor cannot be NaN or Infinity"); + } + if (min !== undefined && (isNaN(min) || !isFinite(min))) { + throw new Error("Min cannot be NaN or Infinity"); + } + if (divisor === 0) { + throw new Error("Divisor cannot be zero"); + } + const values: any[] = [divisor]; + if (min !== undefined) { + values.push(min); + } + return new Operator("divide", values).toString(); + }; + + /** + * Apply modulo operation on a numeric attribute. + * + * @param {number} divisor + * @returns {string} + */ + static modulo = (divisor: number): string => { + if (isNaN(divisor) || !isFinite(divisor)) { + throw new Error("Divisor cannot be NaN or Infinity"); + } + if (divisor === 0) { + throw new Error("Divisor cannot be zero"); + } + return new Operator("modulo", [divisor]).toString(); + }; + + /** + * Raise a numeric attribute to a specified power. + * + * @param {number} exponent + * @param {number} max + * @returns {string} + */ + static power = (exponent: number, max?: number): string => { + if (isNaN(exponent) || !isFinite(exponent)) { + throw new Error("Exponent cannot be NaN or Infinity"); + } + if (max !== undefined && (isNaN(max) || !isFinite(max))) { + throw new Error("Max cannot be NaN or Infinity"); + } + const values: any[] = [exponent]; + if (max !== undefined) { + values.push(max); + } + return new Operator("power", values).toString(); + }; + + /** + * Append values to an array attribute. + * + * @param {any[]} values + * @returns {string} + */ + static arrayAppend = (values: any[]): string => + new Operator("arrayAppend", values).toString(); + + /** + * Prepend values to an array attribute. + * + * @param {any[]} values + * @returns {string} + */ + static arrayPrepend = (values: any[]): string => + new Operator("arrayPrepend", values).toString(); + + /** + * Insert a value at a specific index in an array attribute. + * + * @param {number} index + * @param {any} value + * @returns {string} + */ + static arrayInsert = (index: number, value: any): string => + new Operator("arrayInsert", [index, value]).toString(); + + /** + * Remove a value from an array attribute. + * + * @param {any} value + * @returns {string} + */ + static arrayRemove = (value: any): string => + new Operator("arrayRemove", [value]).toString(); + + /** + * Remove duplicate values from an array attribute. + * + * @returns {string} + */ + static arrayUnique = (): string => + new Operator("arrayUnique", []).toString(); + + /** + * Keep only values that exist in both the current array and the provided array. + * + * @param {any[]} values + * @returns {string} + */ + static arrayIntersect = (values: any[]): string => + new Operator("arrayIntersect", values).toString(); + + /** + * Remove values from the array that exist in the provided array. + * + * @param {any[]} values + * @returns {string} + */ + static arrayDiff = (values: any[]): string => + new Operator("arrayDiff", values).toString(); + + /** + * Filter array values based on a condition. + * + * @param {Condition} condition + * @param {any} value + * @returns {string} + */ + static arrayFilter = (condition: Condition, value?: any): string => { + const values: any[] = [condition as string, value === undefined ? null : value]; + return new Operator("arrayFilter", values).toString(); + }; + + /** + * Concatenate a value to a string or array attribute. + * + * @param {any} value + * @returns {string} + */ + static stringConcat = (value: any): string => + new Operator("stringConcat", [value]).toString(); + + /** + * Replace occurrences of a search string with a replacement string. + * + * @param {string} search + * @param {string} replace + * @returns {string} + */ + static stringReplace = (search: string, replace: string): string => + new Operator("stringReplace", [search, replace]).toString(); + + /** + * Toggle a boolean attribute. + * + * @returns {string} + */ + static toggle = (): string => + new Operator("toggle", []).toString(); + + /** + * Add days to a date attribute. + * + * @param {number} days + * @returns {string} + */ + static dateAddDays = (days: number): string => + new Operator("dateAddDays", [days]).toString(); + + /** + * Subtract days from a date attribute. + * + * @param {number} days + * @returns {string} + */ + static dateSubDays = (days: number): string => + new Operator("dateSubDays", [days]).toString(); + + /** + * Set a date attribute to the current date and time. + * + * @returns {string} + */ + static dateSetNow = (): string => + new Operator("dateSetNow", []).toString(); +} diff --git a/templates/react-native/src/query.ts.twig b/templates/react-native/src/query.ts.twig index 60673481fb..89a88ef3d9 100644 --- a/templates/react-native/src/query.ts.twig +++ b/templates/react-native/src/query.ts.twig @@ -78,6 +78,9 @@ export class Query { static orderAsc = (attribute: string): string => new Query("orderAsc", attribute).toString(); + static orderRandom = (): string => + new Query("orderRandom").toString(); + static cursorAfter = (documentId: string): string => new Query("cursorAfter", undefined, documentId).toString(); @@ -159,7 +162,7 @@ export class Query { * @returns {string} */ static createdBefore = (value: string): string => - new Query("createdBefore", undefined, value).toString(); + Query.lessThan("$createdAt", value); /** * Filter resources where document was created after date. @@ -168,7 +171,7 @@ export class Query { * @returns {string} */ static createdAfter = (value: string): string => - new Query("createdAfter", undefined, value).toString(); + Query.greaterThan("$createdAt", value); /** * Filter resources where document was created between dates. @@ -178,7 +181,7 @@ export class Query { * @returns {string} */ static createdBetween = (start: string, end: string): string => - new Query("createdBetween", undefined, [start, end] as QueryTypesList).toString(); + Query.between("$createdAt", start, end); /** * Filter resources where document was updated before date. @@ -187,7 +190,7 @@ export class Query { * @returns {string} */ static updatedBefore = (value: string): string => - new Query("updatedBefore", undefined, value).toString(); + Query.lessThan("$updatedAt", value); /** * Filter resources where document was updated after date. @@ -196,7 +199,7 @@ export class Query { * @returns {string} */ static updatedAfter = (value: string): string => - new Query("updatedAfter", undefined, value).toString(); + Query.greaterThan("$updatedAt", value); /** * Filter resources where document was updated between dates. @@ -206,7 +209,7 @@ export class Query { * @returns {string} */ static updatedBetween = (start: string, end: string): string => - new Query("updatedBetween", undefined, [start, end] as QueryTypesList).toString(); + Query.between("$updatedAt", start, end); static or = (queries: string[]) => new Query("or", undefined, queries.map((query) => JSON.parse(query))).toString(); diff --git a/templates/react-native/src/services/template.ts.twig b/templates/react-native/src/services/template.ts.twig index ab03da3ddc..3ceedcfcf0 100644 --- a/templates/react-native/src/services/template.ts.twig +++ b/templates/react-native/src/services/template.ts.twig @@ -9,14 +9,9 @@ import { Platform } from 'react-native'; {% for method in service.methods %} {% for parameter in method.parameters.all %} {% if parameter.enumValues is not empty %} -{% if parameter.enumName is not empty %} -{% set name = parameter.enumName %} -{% else %} -{% set name = parameter.name %} -{% endif %} -{% if name not in added %} -import { {{ name | caseUcfirst }} } from '../enums/{{ name | caseKebab }}'; -{% set added = added|merge([name]) %} +{% if parameter.enumName not in added %} +import { {{ parameter.enumName | caseUcfirst }} } from '../enums/{{ parameter.enumName | caseKebab }}'; +{% set added = added|merge([parameter.enumName]) %} {% endif %} {% endif %} {% endfor %} diff --git a/templates/ruby/docs/example.md.twig b/templates/ruby/docs/example.md.twig index b353a9b5ed..f4d080de74 100644 --- a/templates/ruby/docs/example.md.twig +++ b/templates/ruby/docs/example.md.twig @@ -4,14 +4,16 @@ include {{ spec.title | caseUcfirst }} {% set break = false %} {% for parameter in method.parameters.all %} {% if not break %} -{% if method == parameter.required %} {% if parameter.enumValues is not empty %} include {{ spec.title | caseUcfirst }}::Enums {% set break = true %} {% endif %} {% endif %} -{% endif %} {% endfor %} +{% if method.parameters.all | hasPermissionParam %} +include {{ spec.title | caseUcfirst }}::Permission +include {{ spec.title | caseUcfirst }}::Role +{% endif %} client = Client.new .set_endpoint('{{ spec.endpointDocs | raw }}') # Your API Endpoint @@ -26,7 +28,7 @@ client = Client.new result = {{ service.name | caseSnake }}.{{ method.name | caseSnake }}({% if method.parameters.all | length == 0 %}){% endif %} {%~ for parameter in method.parameters.all %} - {{ parameter.name | caseSnake }}: {% if parameter.enumValues | length > 0%}{{ parameter.enumName }}::{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %}{% if not parameter.required %} # optional{% endif %} + {{ parameter.name | caseSnake }}: {% if parameter.enumValues | length > 0%}{{ parameter.enumName | caseUcfirst }}::{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %}{% if not parameter.required %} # optional{% endif %} {%~ endfor -%} {% if method.parameters.all | length > 0 %}) diff --git a/templates/ruby/lib/container.rb.twig b/templates/ruby/lib/container.rb.twig index fd83f4b57f..cce4a9df69 100644 --- a/templates/ruby/lib/container.rb.twig +++ b/templates/ruby/lib/container.rb.twig @@ -11,12 +11,13 @@ require_relative '{{ spec.title | caseSnake }}/query' require_relative '{{ spec.title | caseSnake }}/permission' require_relative '{{ spec.title | caseSnake }}/role' require_relative '{{ spec.title | caseSnake }}/id' +require_relative '{{ spec.title | caseSnake }}/operator' {% for defintion in spec.definitions %} require_relative '{{ spec.title | caseSnake }}/models/{{ defintion.name | caseSnake }}' {% endfor %} -{% for enum in spec.enums %} +{% for enum in spec.allEnums %} require_relative '{{ spec.title | caseSnake }}/enums/{{ enum.name | caseSnake }}' {% endfor %} diff --git a/templates/ruby/lib/container/client.rb.twig b/templates/ruby/lib/container/client.rb.twig index 75715f1e90..d8b91c0ed9 100644 --- a/templates/ruby/lib/container/client.rb.twig +++ b/templates/ruby/lib/container/client.rb.twig @@ -68,7 +68,6 @@ module {{ spec.title | caseUcfirst }} self end - # Add Header # # @param [String] key The key for the header to add diff --git a/templates/ruby/lib/container/models/model.rb.twig b/templates/ruby/lib/container/models/model.rb.twig index 83d5a22d45..0163b0614c 100644 --- a/templates/ruby/lib/container/models/model.rb.twig +++ b/templates/ruby/lib/container/models/model.rb.twig @@ -21,7 +21,15 @@ module {{ spec.title | caseUcfirst }} {% endif %} ) {% for property in definition.properties %} +{% if property.enum %} +{% if property.required %} + @{{ property.name | caseSnake | escapeKeyword }} = validate_{{ property.name | caseSnake | escapeKeyword }}({{ property.name | caseSnake | escapeKeyword }}) +{% else %} + @{{ property.name | caseSnake | escapeKeyword }} = {{ property.name | caseSnake | escapeKeyword }}.nil? ? {{ property.name | caseSnake | escapeKeyword }} : validate_{{ property.name | caseSnake | escapeKeyword }}({{ property.name | caseSnake | escapeKeyword }}) +{% endif %} +{% else %} @{{ property.name | caseSnake | escapeKeyword }} = {{ property.name | caseSnake | escapeKeyword }} +{% endif %} {% endfor %} {% if definition.additionalProperties %} @data = data @@ -35,7 +43,7 @@ module {{ spec.title | caseUcfirst }} {% endfor %} {% if definition.additionalProperties %} - data: map + data: map["data"] || map {% endif %} ) end @@ -67,6 +75,29 @@ module {{ spec.title | caseUcfirst }} end {% endif %} {% endfor %} +{% endif %} +{% endfor %} +{% if definition.properties | filter(p => p.enum) | length > 0 %} + + private + +{% endif %} +{% for property in definition.properties %} +{% if property.enum %} + def validate_{{ property.name | caseSnake | escapeKeyword }}({{ property.name | caseSnake | escapeKeyword }}) + valid_{{ property.name | caseSnake }} = [ +{% for value in property.enum %} + {{ spec.title | caseUcfirst }}::Enums::{{ property.enumName | caseUcfirst }}::{{ value | caseUpper }}, +{% endfor %} + ] + + unless valid_{{ property.name | caseSnake }}.include?({{ property.name | caseSnake | escapeKeyword }}) + raise ArgumentError, "Invalid " + {{ property.name | caseSnake | escapeKeyword }} + ". Must be one of: " + valid_{{ property.name | caseSnake }}.join(', ') + end + + {{ property.name | caseSnake | escapeKeyword }} + end + {% endif %} {% endfor %} end diff --git a/templates/ruby/lib/container/operator.rb.twig b/templates/ruby/lib/container/operator.rb.twig new file mode 100644 index 0000000000..635f3c37bd --- /dev/null +++ b/templates/ruby/lib/container/operator.rb.twig @@ -0,0 +1,145 @@ +require 'json' + +module {{spec.title | caseUcfirst}} + module Condition + EQUAL = "equal" + NOT_EQUAL = "notEqual" + GREATER_THAN = "greaterThan" + GREATER_THAN_EQUAL = "greaterThanEqual" + LESS_THAN = "lessThan" + LESS_THAN_EQUAL = "lessThanEqual" + CONTAINS = "contains" + IS_NULL = "isNull" + IS_NOT_NULL = "isNotNull" + end + + class Operator + def initialize(method, values = nil) + @method = method + + if values != nil + if values.is_a?(Array) + @values = values + else + @values = [values] + end + end + end + + def to_json(*args) + result = { method: @method } + result[:values] = @values unless @values.nil? + result.to_json(*args) + end + + def to_s + return self.to_json + end + + class << Operator + def increment(value = 1, max = nil) + raise ArgumentError, "Value cannot be NaN or Infinity" if value.respond_to?(:nan?) && (value.nan? || value.infinite?) + raise ArgumentError, "Max cannot be NaN or Infinity" if max != nil && max.respond_to?(:nan?) && (max.nan? || max.infinite?) + values = [value] + values << max if max != nil + return Operator.new("increment", values).to_s + end + + def decrement(value = 1, min = nil) + raise ArgumentError, "Value cannot be NaN or Infinity" if value.respond_to?(:nan?) && (value.nan? || value.infinite?) + raise ArgumentError, "Min cannot be NaN or Infinity" if min != nil && min.respond_to?(:nan?) && (min.nan? || min.infinite?) + values = [value] + values << min if min != nil + return Operator.new("decrement", values).to_s + end + + def multiply(factor, max = nil) + raise ArgumentError, "Factor cannot be NaN or Infinity" if factor.respond_to?(:nan?) && (factor.nan? || factor.infinite?) + raise ArgumentError, "Max cannot be NaN or Infinity" if max != nil && max.respond_to?(:nan?) && (max.nan? || max.infinite?) + values = [factor] + values << max if max != nil + return Operator.new("multiply", values).to_s + end + + def divide(divisor, min = nil) + raise ArgumentError, "Divisor cannot be NaN or Infinity" if divisor.respond_to?(:nan?) && (divisor.nan? || divisor.infinite?) + raise ArgumentError, "Min cannot be NaN or Infinity" if min != nil && min.respond_to?(:nan?) && (min.nan? || min.infinite?) + raise ArgumentError, "Divisor cannot be zero" if divisor == 0 || divisor == 0.0 + values = [divisor] + values << min if min != nil + return Operator.new("divide", values).to_s + end + + def modulo(divisor) + raise ArgumentError, "Divisor cannot be NaN or Infinity" if divisor.respond_to?(:nan?) && (divisor.nan? || divisor.infinite?) + raise ArgumentError, "Divisor cannot be zero" if divisor == 0 || divisor == 0.0 + return Operator.new("modulo", [divisor]).to_s + end + + def power(exponent, max = nil) + raise ArgumentError, "Exponent cannot be NaN or Infinity" if exponent.respond_to?(:nan?) && (exponent.nan? || exponent.infinite?) + raise ArgumentError, "Max cannot be NaN or Infinity" if max != nil && max.respond_to?(:nan?) && (max.nan? || max.infinite?) + values = [exponent] + values << max if max != nil + return Operator.new("power", values).to_s + end + + def array_append(values) + return Operator.new("arrayAppend", values).to_s + end + + def array_prepend(values) + return Operator.new("arrayPrepend", values).to_s + end + + def array_insert(index, value) + return Operator.new("arrayInsert", [index, value]).to_s + end + + def array_remove(value) + return Operator.new("arrayRemove", [value]).to_s + end + + def array_unique() + return Operator.new("arrayUnique", []).to_s + end + + def array_intersect(values) + return Operator.new("arrayIntersect", values).to_s + end + + def array_diff(values) + return Operator.new("arrayDiff", values).to_s + end + + def array_filter(condition, value = nil) + values = [condition, value] + return Operator.new("arrayFilter", values).to_s + end + + def string_concat(value) + return Operator.new("stringConcat", [value]).to_s + end + + def string_replace(search, replace) + return Operator.new("stringReplace", [search, replace]).to_s + end + + def toggle() + return Operator.new("toggle", []).to_s + end + + def date_add_days(days) + return Operator.new("dateAddDays", [days]).to_s + end + + def date_sub_days(days) + return Operator.new("dateSubDays", [days]).to_s + end + + def date_set_now() + return Operator.new("dateSetNow", []).to_s + end + end + end +end diff --git a/templates/ruby/lib/container/query.rb.twig b/templates/ruby/lib/container/query.rb.twig index 1b18022602..fb132d9d62 100644 --- a/templates/ruby/lib/container/query.rb.twig +++ b/templates/ruby/lib/container/query.rb.twig @@ -88,6 +88,10 @@ module {{spec.title | caseUcfirst}} return Query.new("orderDesc", attribute, nil).to_s end + def order_random() + return Query.new("orderRandom", nil, nil).to_s + end + def cursor_before(id) return Query.new("cursorBefore", nil, id).to_s end @@ -129,27 +133,27 @@ module {{spec.title | caseUcfirst}} end def created_before(value) - return Query.new("createdBefore", nil, value).to_s + return less_than("$createdAt", value) end def created_after(value) - return Query.new("createdAfter", nil, value).to_s + return greater_than("$createdAt", value) end def created_between(start, ending) - return Query.new("createdBetween", nil, [start, ending]).to_s + return between("$createdAt", start, ending) end def updated_before(value) - return Query.new("updatedBefore", nil, value).to_s + return less_than("$updatedAt", value) end def updated_after(value) - return Query.new("updatedAfter", nil, value).to_s + return greater_than("$updatedAt", value) end def updated_between(start, ending) - return Query.new("updatedBetween", nil, [start, ending]).to_s + return between("$updatedAt", start, ending) end def or(queries) diff --git a/templates/swift/Package.swift.twig b/templates/swift/Package.swift.twig index 12b50297f4..b8d3e4008b 100644 --- a/templates/swift/Package.swift.twig +++ b/templates/swift/Package.swift.twig @@ -34,7 +34,7 @@ let package = Package( {%~ if spec.definitions is not empty %} "{{spec.title | caseUcfirst}}Models", {%~ endif %} - {%~ if spec.enums is not empty %} + {%~ if spec.allEnums is not empty %} "{{spec.title | caseUcfirst}}Enums", {%~ endif %} "JSONCodable" @@ -44,11 +44,14 @@ let package = Package( .target( name: "{{spec.title | caseUcfirst}}Models", dependencies: [ + {%~ if spec.allEnums is not empty %} + "{{spec.title | caseUcfirst}}Enums", + {%~ endif %} "JSONCodable" ] ), {%~ endif %} - {%~ if spec.enums is not empty %} + {%~ if spec.allEnums is not empty %} .target( name: "{{spec.title | caseUcfirst}}Enums" ), diff --git a/templates/swift/Sources/Client.swift.twig b/templates/swift/Sources/Client.swift.twig index 58dfa90b56..85800aca23 100644 --- a/templates/swift/Sources/Client.swift.twig +++ b/templates/swift/Sources/Client.swift.twig @@ -5,6 +5,7 @@ import NIOSSL import Foundation import AsyncHTTPClient @_exported import {{spec.title | caseUcfirst}}Models +@_exported import JSONCodable let DASHDASH = "--" let CRLF = "\r\n" @@ -314,7 +315,6 @@ open class Client { var request = HTTPClientRequest(url: endPoint + path + queryParameters) request.method = .RAW(value: method) - for (key, value) in self.headers.merging(headers, uniquingKeysWith: { $1 }) { request.headers.add(name: key, value: value) } @@ -350,7 +350,7 @@ open class Client { if let warning = response.headers["x-{{ spec.title | lower }}-warning"].first { warning.split(separator: ";").forEach { warning in - print("Warning: \(warning)") + fputs("Warning: \(warning)\n", stderr) } } diff --git a/templates/swift/Sources/JSONCodable/Codable+JSON.swift.twig b/templates/swift/Sources/JSONCodable/Codable+JSON.swift.twig index c9b87119ba..e08de620fe 100644 --- a/templates/swift/Sources/JSONCodable/Codable+JSON.swift.twig +++ b/templates/swift/Sources/JSONCodable/Codable+JSON.swift.twig @@ -121,7 +121,6 @@ extension AnyCodable: ExpressibleByStringLiteral {} extension AnyCodable: ExpressibleByArrayLiteral {} extension AnyCodable: ExpressibleByDictionaryLiteral {} - extension AnyCodable: Hashable { public func hash(into hasher: inout Hasher) { switch value { diff --git a/templates/swift/Sources/Models/Model.swift.twig b/templates/swift/Sources/Models/Model.swift.twig index c6a5bab508..76aa83f143 100644 --- a/templates/swift/Sources/Models/Model.swift.twig +++ b/templates/swift/Sources/Models/Model.swift.twig @@ -1,5 +1,8 @@ import Foundation import JSONCodable +{% if definition.properties | filter(p => p.enum) | length > 0 %} +import {{spec.title | caseUcfirst}}Enums +{% endif %} /// {{ definition.description }} {% if definition.properties | length == 0 and not definition.additionalProperties %} @@ -20,7 +23,6 @@ open class {{ definition | modelType(spec) | raw }}: Codable { /// {{ property.description }} public let {{ property.name | escapeSwiftKeyword | removeDollarSign }}: {{ property | propertyType(spec) | raw }}{% if not property.required %}?{% endif %} - {%~ endfor %} {%~ if definition.additionalProperties %} /// Additional properties @@ -48,7 +50,19 @@ open class {{ definition | modelType(spec) | raw }}: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) {%~ for property in definition.properties %} + {%~ if property.enum %} + {%~ if property.required %} + self.{{ property.name | escapeSwiftKeyword | removeDollarSign }} = {{ property | propertyType(spec) | raw }}(rawValue: try container.decode(String.self, forKey: .{{ property.name | escapeSwiftKeyword | removeDollarSign }}))! + {%~ else %} + if let {{ property.name | escapeSwiftKeyword | removeDollarSign }}String = try container.decodeIfPresent(String.self, forKey: .{{ property.name | escapeSwiftKeyword | removeDollarSign }}) { + self.{{ property.name | escapeSwiftKeyword | removeDollarSign }} = {{ property | propertyType(spec) | raw }}(rawValue: {{ property.name | escapeSwiftKeyword | removeDollarSign }}String) + } else { + self.{{ property.name | escapeSwiftKeyword | removeDollarSign }} = nil + } + {%~ endif %} + {%~ else %} self.{{ property.name | escapeSwiftKeyword | removeDollarSign }} = try container.decode{% if not property.required %}IfPresent{% endif %}({{ property | propertyType(spec) | raw }}.self, forKey: .{{ property.name | escapeSwiftKeyword | removeDollarSign }}) + {%~ endif %} {%~ endfor %} {%~ if definition.additionalProperties %} self.data = try container.decode(T.self, forKey: .data) @@ -59,7 +73,7 @@ open class {{ definition | modelType(spec) | raw }}: Codable { var container = encoder.container(keyedBy: CodingKeys.self) {%~ for property in definition.properties %} - try container.encode{% if not property.required %}IfPresent{% endif %}({{ property.name | escapeSwiftKeyword | removeDollarSign }}, forKey: .{{ property.name | escapeSwiftKeyword | removeDollarSign }}) + try container.encode{% if not property.required %}IfPresent{% endif %}({{ property.name | escapeSwiftKeyword | removeDollarSign }}{% if property.enum %}{% if property.required %}.rawValue{% else %}?.rawValue{% endif %}{% endif %}, forKey: .{{ property.name | escapeSwiftKeyword | removeDollarSign }}) {%~ endfor %} {%~ if definition.additionalProperties %} try container.encode(data, forKey: .data) @@ -69,7 +83,7 @@ open class {{ definition | modelType(spec) | raw }}: Codable { public func toMap() -> [String: Any] { return [ {%~ for property in definition.properties %} - "{{ property.name }}": {% if property.sub_schema %}{% if property.type == 'array' %}{{property.name | escapeSwiftKeyword | removeDollarSign}}.map { $0.toMap() }{% else %}{{property.name | escapeSwiftKeyword | removeDollarSign}}.toMap(){% endif %}{% else %}{{property.name | escapeSwiftKeyword | removeDollarSign}}{% endif %} as Any{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + "{{ property.name }}": {% if property.sub_schema %}{% if property.type == 'array' %}{{property.name | escapeSwiftKeyword | removeDollarSign}}.map { $0.toMap() }{% else %}{{property.name | escapeSwiftKeyword | removeDollarSign}}.toMap(){% endif %}{% elseif property.enum %}{{property.name | escapeSwiftKeyword | removeDollarSign}}{% if not property.required %}?{% endif %}.rawValue{% else %}{{property.name | escapeSwiftKeyword | removeDollarSign}}{% endif %} as Any{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} {%~ endfor %} {%~ if definition.additionalProperties %} @@ -91,6 +105,9 @@ open class {{ definition | modelType(spec) | raw }}: Codable { {%- else -%} {%- if property | isAnyCodableArray(spec) -%} (map["{{property.name }}"] as{% if isDocument %}?{% else %}{% if property.required %}!{% else %}?{% endif %}{% endif %} [Any]{% if isDocument or not property.required %} ?? []{% endif %}).map { AnyCodable($0) } + {%- elseif property.enum -%} + {%- set enumName = property['enumName'] ?? property.name -%} + {% if property.required %}{{ enumName | caseUcfirst }}(rawValue: map["{{property.name }}"] as{% if isDocument %}?{% else %}!{% endif %} String{% if isDocument %} ?? ""{% endif %})!{% else %}map["{{property.name }}"] as? String != nil ? {{ enumName | caseUcfirst }}(rawValue: map["{{property.name }}"] as! String) : nil{% endif %} {%- else -%} map["{{property.name }}"] as{% if isDocument %}?{% else %}{% if property.required %}!{% else %}?{% endif %}{% endif %} {{ property | propertyType(spec) | raw }}{% if isDocument and property.required %}{% if property.type == 'string' %} ?? ""{% elseif property.type == 'integer' %} ?? 0{% elseif property.type == 'number' %} ?? 0.0{% elseif property.type == 'boolean' %} ?? false{% elseif property.type == 'array' %} ?? []{% endif %}{% endif %} {%- endif -%} @@ -98,7 +115,7 @@ open class {{ definition | modelType(spec) | raw }}: Codable { {%~ endfor %} {%~ if definition.additionalProperties %} - data: try! JSONDecoder().decode(T.self, from: JSONSerialization.data(withJSONObject: map, options: [])) + data: try! JSONDecoder().decode(T.self, from: JSONSerialization.data(withJSONObject: map["data"] as? [String: Any] ?? map, options: [])) {%~ endif %} ) } diff --git a/templates/swift/Sources/Operator.swift.twig b/templates/swift/Sources/Operator.swift.twig new file mode 100644 index 0000000000..6af290f0c6 --- /dev/null +++ b/templates/swift/Sources/Operator.swift.twig @@ -0,0 +1,305 @@ +import Foundation + +public enum Condition: String, Codable { + case equal = "equal" + case notEqual = "notEqual" + case greaterThan = "greaterThan" + case greaterThanEqual = "greaterThanEqual" + case lessThan = "lessThan" + case lessThanEqual = "lessThanEqual" + case contains = "contains" + case isNull = "isNull" + case isNotNull = "isNotNull" +} + +enum OperatorValue: Codable { + case string(String) + case int(Int) + case double(Double) + case bool(Bool) + case array([OperatorValue]) + case null + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if container.decodeNil() { + self = .null + } else if let stringValue = try? container.decode(String.self) { + self = .string(stringValue) + } else if let intValue = try? container.decode(Int.self) { + self = .int(intValue) + } else if let doubleValue = try? container.decode(Double.self) { + self = .double(doubleValue) + } else if let boolValue = try? container.decode(Bool.self) { + self = .bool(boolValue) + } else if let arrayValue = try? container.decode([OperatorValue].self) { + self = .array(arrayValue) + } else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "OperatorValue cannot be decoded" + ) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .string(let value): + try container.encode(value) + case .int(let value): + try container.encode(value) + case .double(let value): + try container.encode(value) + case .bool(let value): + try container.encode(value) + case .array(let value): + try container.encode(value) + case .null: + try container.encodeNil() + } + } +} + +public struct Operator : Codable, CustomStringConvertible { + var method: String + var values: [OperatorValue]? + + init(method: String, values: Any? = nil) { + self.method = method + self.values = Operator.convertToOperatorValueArray(values) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.method = try container.decode(String.self, forKey: .method) + self.values = try container.decodeIfPresent([OperatorValue].self, forKey: .values) + } + + private static func convertToOperatorValueArray(_ values: Any?) -> [OperatorValue]? { + // Handle nil + if values == nil { + return nil + } + + // Handle NSNull as [.null] + if values is NSNull { + return [.null] + } + + switch values { + case let valueArray as [OperatorValue]: + return valueArray + case let stringArray as [String]: + return stringArray.map { .string($0) } + case let intArray as [Int]: + return intArray.map { .int($0) } + case let doubleArray as [Double]: + return doubleArray.map { .double($0) } + case let boolArray as [Bool]: + return boolArray.map { .bool($0) } + case let stringValue as String: + return [.string(stringValue)] + case let intValue as Int: + return [.int(intValue)] + case let doubleValue as Double: + return [.double(doubleValue)] + case let boolValue as Bool: + return [.bool(boolValue)] + case let anyArray as [Any]: + // Preserve empty arrays as empty OperatorValue arrays + if anyArray.isEmpty { + return [] + } + + // Map all items, converting nil/unknown to .null + let nestedValues = anyArray.map { item -> OperatorValue in + if item is NSNull { + return .null + } else if let stringValue = item as? String { + return .string(stringValue) + } else if let intValue = item as? Int { + return .int(intValue) + } else if let doubleValue = item as? Double { + return .double(doubleValue) + } else if let boolValue = item as? Bool { + return .bool(boolValue) + } else if let nestedArray = item as? [Any] { + let converted = convertToOperatorValueArray(nestedArray) ?? [] + return .array(converted) + } else { + // Unknown/unsupported types become .null + return .null + } + } + return nestedValues + default: + // Unknown types become [.null] + return [.null] + } + } + + enum CodingKeys: String, CodingKey { + case method + case values + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(method, forKey: .method) + + if (values != nil) { + try container.encode(values, forKey: .values) + } + } + + public var description: String { + guard let data = try? JSONEncoder().encode(self) else { + return "" + } + + return String(data: data, encoding: .utf8) ?? "" + } + + public static func increment(_ value: Double = 1, max: Double? = nil) -> String { + if value.isNaN || value.isInfinite { + fatalError("Value cannot be NaN or Infinity") + } + if let max = max, max.isNaN || max.isInfinite { + fatalError("Max cannot be NaN or Infinity") + } + var values: [Any] = [value] + if let max = max { + values.append(max) + } + return Operator(method: "increment", values: values).description + } + + public static func decrement(_ value: Double = 1, min: Double? = nil) -> String { + if value.isNaN || value.isInfinite { + fatalError("Value cannot be NaN or Infinity") + } + if let min = min, min.isNaN || min.isInfinite { + fatalError("Min cannot be NaN or Infinity") + } + var values: [Any] = [value] + if let min = min { + values.append(min) + } + return Operator(method: "decrement", values: values).description + } + + public static func multiply(_ factor: Double, max: Double? = nil) -> String { + if factor.isNaN || factor.isInfinite { + fatalError("Factor cannot be NaN or Infinity") + } + if let max = max, max.isNaN || max.isInfinite { + fatalError("Max cannot be NaN or Infinity") + } + var values: [Any] = [factor] + if let max = max { + values.append(max) + } + return Operator(method: "multiply", values: values).description + } + + public static func divide(_ divisor: Double, min: Double? = nil) -> String { + if divisor.isNaN || divisor.isInfinite { + fatalError("Divisor cannot be NaN or Infinity") + } + if let min = min, min.isNaN || min.isInfinite { + fatalError("Min cannot be NaN or Infinity") + } + if divisor == 0 { + fatalError("Divisor cannot be zero") + } + var values: [Any] = [divisor] + if let min = min { + values.append(min) + } + return Operator(method: "divide", values: values).description + } + + public static func modulo(_ divisor: Double) -> String { + if divisor.isNaN || divisor.isInfinite { + fatalError("Divisor cannot be NaN or Infinity") + } + if divisor == 0 { + fatalError("Divisor cannot be zero") + } + return Operator(method: "modulo", values: [divisor]).description + } + + public static func power(_ exponent: Double, max: Double? = nil) -> String { + if exponent.isNaN || exponent.isInfinite { + fatalError("Exponent cannot be NaN or Infinity") + } + if let max = max, max.isNaN || max.isInfinite { + fatalError("Max cannot be NaN or Infinity") + } + var values: [Any] = [exponent] + if let max = max { + values.append(max) + } + return Operator(method: "power", values: values).description + } + + public static func arrayAppend(_ values: [Any]) -> String { + return Operator(method: "arrayAppend", values: values).description + } + + public static func arrayPrepend(_ values: [Any]) -> String { + return Operator(method: "arrayPrepend", values: values).description + } + + public static func arrayInsert(_ index: Int, value: Any) -> String { + return Operator(method: "arrayInsert", values: [index, value]).description + } + + public static func arrayRemove(_ value: Any) -> String { + return Operator(method: "arrayRemove", values: [value]).description + } + + public static func arrayUnique() -> String { + return Operator(method: "arrayUnique", values: []).description + } + + public static func arrayIntersect(_ values: [Any]) -> String { + return Operator(method: "arrayIntersect", values: values).description + } + + public static func arrayDiff(_ values: [Any]) -> String { + return Operator(method: "arrayDiff", values: values).description + } + + public static func arrayFilter(_ condition: Condition, value: Any? = nil) -> String { + let values: [Any] = [condition.rawValue, value ?? NSNull()] + return Operator(method: "arrayFilter", values: values).description + } + + public static func stringConcat(_ value: Any) -> String { + return Operator(method: "stringConcat", values: [value]).description + } + + public static func stringReplace(_ search: String, _ replace: String) -> String { + return Operator(method: "stringReplace", values: [search, replace]).description + } + + public static func toggle() -> String { + return Operator(method: "toggle", values: []).description + } + + public static func dateAddDays(_ days: Int) -> String { + return Operator(method: "dateAddDays", values: [days]).description + } + + public static func dateSubDays(_ days: Int) -> String { + return Operator(method: "dateSubDays", values: [days]).description + } + + public static func dateSetNow() -> String { + return Operator(method: "dateSetNow", values: []).description + } +} diff --git a/templates/swift/Sources/Query.swift.twig b/templates/swift/Sources/Query.swift.twig index 5af7f4aa56..7610dc3835 100644 --- a/templates/swift/Sources/Query.swift.twig +++ b/templates/swift/Sources/Query.swift.twig @@ -280,6 +280,12 @@ public struct Query : Codable, CustomStringConvertible { ).description } + public static func orderRandom() -> String { + return Query( + method: "orderRandom" + ).description + } + public static func cursorBefore(_ id: String) -> String { return Query( method: "cursorBefore", @@ -373,45 +379,27 @@ public struct Query : Codable, CustomStringConvertible { } public static func createdBefore(_ value: String) -> String { - return Query( - method: "createdBefore", - values: [value] - ).description + return lessThan("$createdAt", value: value) } public static func createdAfter(_ value: String) -> String { - return Query( - method: "createdAfter", - values: [value] - ).description + return greaterThan("$createdAt", value: value) } public static func createdBetween(_ start: String, _ end: String) -> String { - return Query( - method: "createdBetween", - values: [start, end] - ).description + return between("$createdAt", start: start, end: end) } public static func updatedBefore(_ value: String) -> String { - return Query( - method: "updatedBefore", - values: [value] - ).description + return lessThan("$updatedAt", value: value) } public static func updatedAfter(_ value: String) -> String { - return Query( - method: "updatedAfter", - values: [value] - ).description + return greaterThan("$updatedAt", value: value) } public static func updatedBetween(_ start: String, _ end: String) -> String { - return Query( - method: "updatedBetween", - values: [start, end] - ).description + return between("$updatedAt", start: start, end: end) } public static func or(_ queries: [String]) -> String { diff --git a/templates/swift/Sources/WebSockets/WebSocketClient.swift.twig b/templates/swift/Sources/WebSockets/WebSocketClient.swift.twig index 72322b784c..0e6c3e7ee6 100644 --- a/templates/swift/Sources/WebSockets/WebSocketClient.swift.twig +++ b/templates/swift/Sources/WebSockets/WebSocketClient.swift.twig @@ -297,7 +297,13 @@ public class WebSocketClient { self.channel = channel } - return channel.pipeline.addHandler(handler) + return channel.pipeline.addHandler(handler).map { + if let delegate = self.delegate { + delegate.onOpen(channel: channel) + } else { + self.onOpen(channel) + } + } } // MARK: - Close connection @@ -381,7 +387,6 @@ public class WebSocketClient { ) } - /// Sends the JSON representation of the given model to the connected server in multiple frames. /// /// - parameters: diff --git a/templates/unity/Assets/Editor/Appwrite.Editor.asmdef.twig b/templates/unity/Assets/Editor/Appwrite.Editor.asmdef.twig new file mode 100644 index 0000000000..46bb7794fc --- /dev/null +++ b/templates/unity/Assets/Editor/Appwrite.Editor.asmdef.twig @@ -0,0 +1,16 @@ +{ + "name": "{{ spec.title | caseUcfirst }}.Editor", + "rootNamespace": "{{ spec.title | caseUcfirst }}", + "references": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/templates/unity/Assets/Editor/AppwriteSetupAssistant.cs.twig b/templates/unity/Assets/Editor/AppwriteSetupAssistant.cs.twig new file mode 100644 index 0000000000..f9d34ce4a4 --- /dev/null +++ b/templates/unity/Assets/Editor/AppwriteSetupAssistant.cs.twig @@ -0,0 +1,178 @@ +using UnityEngine; +using UnityEditor; +using UnityEditor.PackageManager; +using System.Linq; +using System; +using System.Collections.Generic; + +namespace {{ spec.title | caseUcfirst }}.Editor +{ + [InitializeOnLoad] + public static class {{ spec.title | caseUcfirst }}SetupAssistant + { + private const string UNITASK_PACKAGE_URL = "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask"; + private const string UNITASK_PACKAGE_NAME = "com.cysharp.unitask"; + private const string WEBSOCKET_PACKAGE_URL = "https://github.com/endel/NativeWebSocket.git#upm"; + private const string WEBSOCKET_PACKAGE_NAME = "com.endel.nativewebsocket"; + private const string SETUP_COMPLETED_KEY = "{{ spec.title | caseUcfirst }}_Setup_Completed"; + private const string SHOW_SETUP_DIALOG_KEY = "{{ spec.title | caseUcfirst }}_Show_Setup_Dialog"; + private static bool _isBusy; // General flag to prevent running two operations at once + + public static bool HasUniTask { get; private set; } + public static bool HasWebSocket { get; private set; } + + static {{ spec.title | caseUcfirst }}SetupAssistant() + { + // Use delayCall so the Unity Editor has time to initialize + EditorApplication.delayCall += InitialCheck; + } + + private static void InitialCheck() + { + if (EditorApplication.isCompiling || EditorApplication.isUpdating || EditorPrefs.GetBool(SETUP_COMPLETED_KEY, false)) return; + + RefreshPackageStatus(() => { + if (!HasUniTask || !HasWebSocket) + { + if (!EditorPrefs.GetBool(SHOW_SETUP_DIALOG_KEY, false)) + { + EditorPrefs.SetBool(SHOW_SETUP_DIALOG_KEY, true); + ShowSetupWindow(); + } + } + else + { + CompleteSetup(); + } + }); + } + + /// + /// Asynchronously checks installed packages and invokes the callback when finished. + /// + public static void RefreshPackageStatus(Action onRefreshed = null) + { + if (_isBusy) return; + _isBusy = true; + + var request = Client.List(); + // Subscribe to the editor update event to poll the request status each frame + EditorApplication.update += CheckProgress; + + void CheckProgress() + { + if (!request.IsCompleted) return; + + EditorApplication.update -= CheckProgress; // Unsubscribe so we don't call it again + if (request.Status == StatusCode.Success) + { + HasUniTask = request.Result.Any(p => p.name == UNITASK_PACKAGE_NAME); + HasWebSocket = request.Result.Any(p => p.name == WEBSOCKET_PACKAGE_NAME); + } + else + { + Debug.LogWarning($"{{ spec.title | caseUcfirst }} Setup: Could not refresh package status - {request.Error?.message ?? "Unknown"}"); + } + _isBusy = false; + onRefreshed?.Invoke(); // Invoke the callback + } + } + + public static void InstallUniTask(Action onCompleted) => InstallPackage(UNITASK_PACKAGE_URL, onCompleted); + public static void InstallWebSocket(Action onCompleted) => InstallPackage(WEBSOCKET_PACKAGE_URL, onCompleted); + + /// + /// New reliable method to install all missing packages. + /// + public static void InstallAllPackages(Action onSuccess, Action onError) + { + if (_isBusy) { onError?.Invoke("Another operation is already in progress."); return; } + + var packagesToInstall = new Queue(); + if (!HasUniTask) packagesToInstall.Enqueue(UNITASK_PACKAGE_URL); + if (!HasWebSocket) packagesToInstall.Enqueue(WEBSOCKET_PACKAGE_URL); + + if (packagesToInstall.Count == 0) + { + onSuccess?.Invoke(); + return; + } + + _isBusy = true; + AssetDatabase.StartAssetEditing(); // Pause asset importing to speed up operations + InstallNextPackage(packagesToInstall, onSuccess, onError); + } + + /// + /// Recursively installs packages from the queue one by one. + /// + private static void InstallNextPackage(Queue packageQueue, Action onSuccess, Action onError) + { + if (packageQueue.Count == 0) + { + AssetDatabase.StopAssetEditing(); + _isBusy = false; + onSuccess?.Invoke(); // All packages installed, invoke the final callback + return; + } + + string packageUrl = packageQueue.Dequeue(); + var request = Client.Add(packageUrl); + EditorApplication.update += CheckInstallProgress; + + void CheckInstallProgress() + { + if (!request.IsCompleted) return; + + EditorApplication.update -= CheckInstallProgress; + if (request.Status == StatusCode.Success) + { + Debug.Log($"{{ spec.title | caseUcfirst }} Setup: Successfully installed {request.Result.displayName}."); + InstallNextPackage(packageQueue, onSuccess, onError); // Install the next package + } + else + { + string error = request.Error?.message ?? "Unknown error"; + Debug.LogError($"{{ spec.title | caseUcfirst }} Setup: Failed to install {packageUrl} - {error}"); + AssetDatabase.StopAssetEditing(); + _isBusy = false; + onError?.Invoke(error); + } + } + } + + private static void InstallPackage(string packageUrl, Action onCompleted) + { + if (_isBusy) return; + + var queue = new Queue(); + queue.Enqueue(packageUrl); + + InstallNextPackage(queue,() => onCompleted?.Invoke(), Debug.LogError); + } + + private static void ShowSetupWindow() + { + var window = EditorWindow.GetWindow<{{ spec.title | caseUcfirst }}SetupWindow>(true, "{{ spec.title | caseUcfirst }} Setup Assistant"); + window.Show(); + window.Focus(); + } + private static void CompleteSetup() + { + EditorPrefs.SetBool(SETUP_COMPLETED_KEY, true); + EditorPrefs.SetBool(SHOW_SETUP_DIALOG_KEY, true); + Debug.Log("{{ spec.title | caseUcfirst }} Setup: Setup completed successfully!"); + } + [MenuItem("{{ spec.title | caseUcfirst }}/Setup Assistant", priority = 1)] + public static void ShowSetupAssistant() => ShowSetupWindow(); + [MenuItem("{{ spec.title | caseUcfirst }}/Reset Setup", priority = 100)] + public static void ResetSetup() + { + EditorPrefs.DeleteKey(SETUP_COMPLETED_KEY); + EditorPrefs.DeleteKey(SHOW_SETUP_DIALOG_KEY); + HasUniTask = false; + HasWebSocket = false; + Debug.Log("{{ spec.title | caseUcfirst }} Setup: Setup state reset. Reopening the window will trigger the check."); + } + } +} \ No newline at end of file diff --git a/templates/unity/Assets/Editor/AppwriteSetupWindow.cs.twig b/templates/unity/Assets/Editor/AppwriteSetupWindow.cs.twig new file mode 100644 index 0000000000..a27b4816ed --- /dev/null +++ b/templates/unity/Assets/Editor/AppwriteSetupWindow.cs.twig @@ -0,0 +1,256 @@ +using UnityEngine; +using UnityEditor; +using System; + +namespace {{ spec.title | caseUcfirst }}.Editor +{ + public class {{ spec.title | caseUcfirst }}SetupWindow : EditorWindow + { + private Vector2 _scrollPosition; + private string _statusMessage = ""; + private MessageType _statusMessageType = MessageType.Info; + private bool _isBusy; // Flag to block the UI during asynchronous operations + + private void OnEnable() + { + titleContent = new GUIContent("{{ spec.title | caseUcfirst }} Setup", "{{ spec.title | caseUcfirst }} SDK Setup"); + minSize = new Vector2(520, 520); + maxSize = new Vector2(520, 520); + RefreshStatus(); + } + + private void OnFocus() + { + RefreshStatus(); + } + + // Requests a status refresh and provides a callback to repaint the window + private void RefreshStatus() + { + _isBusy = true; + Repaint(); // Repaint immediately to show the "Working..." message + {{ spec.title | caseUcfirst }}SetupAssistant.RefreshPackageStatus(() => { + _isBusy = false; + Repaint(); + }); + } + + private void OnGUI() + { + EditorGUILayout.Space(20); + DrawHeader(); + EditorGUILayout.Space(15); + + _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); + + if (!string.IsNullOrEmpty(_statusMessage)) + { + EditorGUILayout.HelpBox(_statusMessage, _statusMessageType); + EditorGUILayout.Space(10); + } + + // Disable the UI while _isBusy = true + using (new EditorGUI.DisabledScope(_isBusy)) + { + DrawDependenciesSection(); + EditorGUILayout.Space(15); + + DrawQuickStartSection(); + EditorGUILayout.Space(15); + + DrawActionButtons(); + } + + if (_isBusy) + { + EditorGUILayout.Space(10); + EditorGUILayout.HelpBox("Working, please wait...", MessageType.Info); + } + + EditorGUILayout.EndScrollView(); + + EditorGUILayout.Space(10); + DrawFooter(); + } + + private void DrawDependenciesSection() + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("📦 Dependencies", EditorStyles.boldLabel); + + var missingPackages = !{{ spec.title | caseUcfirst }}SetupAssistant.HasUniTask || !{{ spec.title | caseUcfirst }}SetupAssistant.HasWebSocket; + if (GUILayout.Button("Install All", GUILayout.Width(100)) && missingPackages) + { + _isBusy = true; + ShowMessage("Installing all required packages...", MessageType.Info); + // Call the new, reliable method to install packages + {{ spec.title | caseUcfirst }}SetupAssistant.InstallAllPackages( + onSuccess: () => { + ShowMessage("All packages installed successfully!", MessageType.Info); + RefreshStatus(); // Refresh package statuses and UI after completion + }, + onError: (error) => { + ShowMessage($"Failed to install packages: {error}", MessageType.Error); + _isBusy = false; // Clear busy flag in case of error + Repaint(); + } + ); + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.Space(10); + + // Pass installation methods to the UI + DrawPackageStatus("UniTask", {{ spec.title | caseUcfirst }}SetupAssistant.HasUniTask, + "Required for async operations", + {{ spec.title | caseUcfirst }}SetupAssistant.InstallUniTask); + + EditorGUILayout.Space(5); + + DrawPackageStatus("NativeWebSocket", {{ spec.title | caseUcfirst }}SetupAssistant.HasWebSocket, + "Required for realtime features", + {{ spec.title | caseUcfirst }}SetupAssistant.InstallWebSocket); + + EditorGUILayout.Space(5); + + if (!missingPackages && !_isBusy) + { + EditorGUILayout.HelpBox("✨ All required packages are installed!", MessageType.Info); + } + + EditorGUILayout.EndVertical(); + } + + private void DrawPackageStatus(string packageName, bool isInstalled, string description, Action installAction) + { + var boxStyle = new GUIStyle(EditorStyles.helpBox) + { + padding = new RectOffset(10, 10, 10, 10), + margin = new RectOffset(5, 5, 0, 0) + }; + + EditorGUILayout.BeginVertical(boxStyle); + EditorGUILayout.BeginHorizontal(); + + var statusIcon = isInstalled ? "✅" : "⚠️"; + var nameStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 12 }; + EditorGUILayout.LabelField($"{statusIcon} {packageName}", nameStyle); + + if (!isInstalled && GUILayout.Button("Install", GUILayout.Width(100))) + { + _isBusy = true; + ShowMessage($"Installing {packageName}...", MessageType.Info); + installAction?.Invoke(() => { // Invoke installation + ShowMessage($"{packageName} installed successfully!", MessageType.Info); + RefreshStatus(); // Refresh package statuses and UI after completion + }); + } + + EditorGUILayout.EndHorizontal(); + + if (!isInstalled) + { + EditorGUILayout.Space(2); + var descStyle = new GUIStyle(EditorStyles.miniLabel) { wordWrap = true }; + EditorGUILayout.LabelField(description, descStyle); + } + + EditorGUILayout.EndVertical(); + } + + private void DrawHeader() + { + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + var headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 16, alignment = TextAnchor.MiddleCenter }; + EditorGUILayout.LabelField("🚀 {{ spec.title | caseUcfirst }} SDK Setup", headerStyle, GUILayout.ExpandWidth(false)); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.Space(4); + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + EditorGUILayout.LabelField("Configure your {{ spec.title | caseUcfirst }} SDK for Unity", EditorStyles.centeredGreyMiniLabel, GUILayout.ExpandWidth(false)); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + } + private void DrawQuickStartSection() + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.LabelField("⚡ Quick Start", EditorStyles.boldLabel); + EditorGUILayout.Space(10); + var allPackagesInstalled = {{ spec.title | caseUcfirst }}SetupAssistant.HasUniTask && {{ spec.title | caseUcfirst }}SetupAssistant.HasWebSocket; + GUI.enabled = allPackagesInstalled; + var buttonStyle = new GUIStyle(GUI.skin.button) { padding = new RectOffset(12, 12, 8, 8), margin = new RectOffset(5, 5, 5, 5), fontSize = 12 }; + if (GUILayout.Button("🎮 Setup Current Scene", buttonStyle)) { SetupCurrentScene(); } + GUI.enabled = true; + EditorGUILayout.Space(10); + var headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 11 }; + EditorGUILayout.LabelField("This will create in the current scene:", headerStyle); + var itemStyle = new GUIStyle(EditorStyles.label) { richText = true, padding = new RectOffset(15, 0, 2, 2), fontSize = 11 }; + EditorGUILayout.LabelField("• {{ spec.title | caseUcfirst }}Manager - Main SDK manager component", itemStyle); + EditorGUILayout.LabelField("• {{ spec.title | caseUcfirst }}Config - Configuration asset for your project", itemStyle); + EditorGUILayout.LabelField("• Realtime - WebSocket connection handler", itemStyle); + if (!allPackagesInstalled) + { + EditorGUILayout.Space(10); + EditorGUILayout.HelpBox("Please install all required packages first", MessageType.Warning); + } + EditorGUILayout.EndVertical(); + } + private void DrawActionButtons() + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.BeginHorizontal(); + var buttonStyle = new GUIStyle(GUI.skin.button) { padding = new RectOffset(15, 15, 8, 8), margin = new RectOffset(5, 5, 5, 5), fontSize = 11 }; + if (GUILayout.Button(new GUIContent(" 📖 Documentation", "Open {{ spec.title | caseUcfirst }} documentation"), buttonStyle)) { Application.OpenURL("https://appwrite.io/docs"); } + if (GUILayout.Button(new GUIContent(" 💬 Discord Community", "Join our Discord community"), buttonStyle)) { Application.OpenURL("https://discord.gg/dJ9xrMr9hq"); } + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + } + private void DrawFooter() + { + EditorGUI.DrawRect(GUILayoutUtility.GetRect(0, 1), Color.gray); + EditorGUILayout.Space(5); + EditorGUILayout.LabelField("{{ spec.title | caseUcfirst }} SDK for Unity", EditorStyles.centeredGreyMiniLabel); + } + private async void SetupCurrentScene() + { + try + { + ShowMessage("Setting up the scene...", MessageType.Info); + var type = Type.GetType("{{ spec.title | caseUcfirst }}.Utilities.{{ spec.title | caseUcfirst }}Utilities, {{ spec.title | caseUcfirst }}"); + if (type == null) { ShowMessage("{{ spec.title | caseUcfirst }}Utilities not found. Ensure the Runtime assembly is compiled.", MessageType.Warning); return; } + var method = type.GetMethod("QuickSetup", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public); + if (method == null) { ShowMessage("QuickSetup method not found in {{ spec.title | caseUcfirst }}Utilities.", MessageType.Warning); return; } + var task = method.Invoke(null, null); + if (task == null) { ShowMessage("QuickSetup returned null.", MessageType.Warning); return; } + dynamic dynamicTask = task; + var result = await dynamicTask; + if (result != null) + { + var goProp = result.GetType().GetProperty("gameObject"); + var go = goProp?.GetValue(result) as GameObject; + if (go != null) { Selection.activeGameObject = go; ShowMessage("Scene setup completed successfully!", MessageType.Info); } + } + } + catch (Exception ex) { ShowMessage($"Setup failed: {ex.Message}", MessageType.Error); } + } + private void ShowMessage(string message, MessageType type) + { + _statusMessage = message; + _statusMessageType = type; + Repaint(); + if (type != MessageType.Error) + { + EditorApplication.delayCall += () => { + if (_statusMessage == message) + { + System.Threading.Tasks.Task.Delay(5000).ContinueWith(_ => { if (_statusMessage == message) { _statusMessage = ""; Repaint(); } }, System.Threading.Tasks.TaskScheduler.FromCurrentSynchronizationContext()); + } + }; + } + } + } +} \ No newline at end of file diff --git a/templates/unity/Assets/Runtime/Appwrite.asmdef.twig b/templates/unity/Assets/Runtime/Appwrite.asmdef.twig new file mode 100644 index 0000000000..9acbb2c37a --- /dev/null +++ b/templates/unity/Assets/Runtime/Appwrite.asmdef.twig @@ -0,0 +1,24 @@ +{ + "name": "{{ spec.title | caseUcfirst }}", + "rootNamespace": "{{ spec.title | caseUcfirst }}", + "references": [ + "{{ spec.title | caseUcfirst }}.Core", + "endel.nativewebsocket", + "UniTask" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [ + { + "name": "com.cysharp.unitask", + "expression": "", + "define": "UNI_TASK" + } + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/templates/unity/Assets/Runtime/AppwriteConfig.cs.twig b/templates/unity/Assets/Runtime/AppwriteConfig.cs.twig new file mode 100644 index 0000000000..d004db16fc --- /dev/null +++ b/templates/unity/Assets/Runtime/AppwriteConfig.cs.twig @@ -0,0 +1,131 @@ +using System; +using UnityEngine; + +namespace {{ spec.title | caseUcfirst }} +{ + // Define the service enum with Flags attribute for multi-selection in the inspector + [Flags] + public enum {{ spec.title | caseUcfirst }}Service + { + None = 0, + [Tooltip("Selects all main services: Account, Databases, Functions, Storage")] + Main = (1 << 4) - 1, // 0-3 + [Tooltip("Selects all other services: Avatars, GraphQL, Locale, Messaging, Teams")] + Others = (1 << 9) - 1 ^ (1 << 4) - 1, // 4-8 + Account = 1 << 0, + Databases = 1 << 1, + Functions = 1 << 2, + Storage = 1 << 3, + Avatars = 1 << 4, + Graphql = 1 << 5, + Locale = 1 << 6, + Messaging = 1 << 7, + Teams = 1 << 8, + + [Tooltip("Selects all available services.")] + All = ~0 + + } + + /// + /// ScriptableObject configuration for {{ spec.title | caseUcfirst }} client settings + /// + [CreateAssetMenu(fileName = "{{ spec.title | caseUcfirst }}Config", menuName = "{{ spec.title | caseUcfirst }}/Configuration")] + public class {{ spec.title | caseUcfirst }}Config : ScriptableObject + { + [Header("Connection Settings")] + [Tooltip("Endpoint URL for {{ spec.title | caseUcfirst }} API (e.g., https://cloud.{{ spec.title | lower }}.io/v1)")] + [SerializeField] private string endpoint = "https://cloud.{{ spec.title | lower }}.io/v1"; + + [Tooltip("WebSocket endpoint for realtime updates (optional)")] + [SerializeField] private string realtimeEndpoint = "wss://cloud.{{ spec.title | lower }}.io/v1"; + + [Tooltip("Enable if using a self-signed SSL certificate")] + [SerializeField] private bool selfSigned; + + [Header("Project Settings")] + [Tooltip("Your {{ spec.title | caseUcfirst }} project ID")] + [SerializeField] private string projectId = ""; + + [Header("Service Initialization")] + [Tooltip("Select which {{ spec.title | caseUcfirst }} services to initialize.")] + [SerializeField] private {{ spec.title | caseUcfirst }}Service servicesToInitialize = {{ spec.title | caseUcfirst }}Service.All; + + [Header("Advanced Settings")] + [Tooltip("Dev key (optional). Dev keys allow bypassing rate limits and CORS errors in your development environment. WARNING: Storing dev keys in ScriptableObjects is a security risk. Do not expose this in public repositories. Consider loading from a secure location at runtime for production builds.")] + [SerializeField] private string devKey = ""; + + [Tooltip("Automatically connect to {{ spec.title | caseUcfirst }} on start")] + [SerializeField] private bool autoConnect; + + public string Endpoint => endpoint; + public string RealtimeEndpoint => realtimeEndpoint; + public bool SelfSigned => selfSigned; + public string ProjectId => projectId; + public string DevKey => devKey; + public bool AutoConnect => autoConnect; + public {{ spec.title | caseUcfirst }}Service ServicesToInitialize => servicesToInitialize; + + /// + /// Validate configuration settings + /// + private void OnValidate() + { + if (string.IsNullOrEmpty(endpoint)) + Debug.LogWarning("{{ spec.title | caseUcfirst }}Config: Endpoint is required"); + + if (string.IsNullOrEmpty(projectId)) + Debug.LogWarning("{{ spec.title | caseUcfirst }}Config: Project ID is required"); + + if (!string.IsNullOrEmpty(devKey)) + Debug.LogWarning("{{ spec.title | caseUcfirst }}Config: Dev Key is set. For security, avoid storing keys directly in assets for production builds."); + } + + + /// + /// Apply this configuration to a client + /// + public void ApplyTo(Client client) + { + client.SetEndpoint(endpoint); + client.SetProject(projectId); + + if (!string.IsNullOrEmpty(realtimeEndpoint)) + client.SetEndPointRealtime(realtimeEndpoint); + + client.SetSelfSigned(selfSigned); + + if (!string.IsNullOrEmpty(devKey)) + client.SetDevKey(devKey); + } + + #if UNITY_EDITOR + [UnityEditor.MenuItem("{{ spec.title | caseUcfirst }}/Create Configuration")] + public static {{ spec.title | caseUcfirst }}Config CreateConfiguration() + { + var config = CreateInstance<{{ spec.title | caseUcfirst }}Config>(); + + if (!System.IO.Directory.Exists("Assets/{{ spec.title | caseUcfirst }}")) + { + UnityEditor.AssetDatabase.CreateFolder("Assets", "{{ spec.title | caseUcfirst }}"); + } + if (!System.IO.Directory.Exists("Assets/{{ spec.title | caseUcfirst }}/Resources")) + { + UnityEditor.AssetDatabase.CreateFolder("Assets/{{ spec.title | caseUcfirst }}", "Resources"); + } + + string path = "Assets/{{ spec.title | caseUcfirst }}/Resources/{{ spec.title | caseUcfirst }}Config.asset"; + path = UnityEditor.AssetDatabase.GenerateUniqueAssetPath(path); + + UnityEditor.AssetDatabase.CreateAsset(config, path); + UnityEditor.AssetDatabase.SaveAssets(); + UnityEditor.EditorUtility.FocusProjectWindow(); + UnityEditor.Selection.activeObject = config; + + Debug.Log($"{{ spec.title | caseUcfirst }} configuration created at: {path}"); + + return config; + } + #endif + } +} \ No newline at end of file diff --git a/templates/unity/Assets/Runtime/AppwriteManager.cs.twig b/templates/unity/Assets/Runtime/AppwriteManager.cs.twig new file mode 100644 index 0000000000..44f4b833e7 --- /dev/null +++ b/templates/unity/Assets/Runtime/AppwriteManager.cs.twig @@ -0,0 +1,281 @@ +#if UNI_TASK +using System; +using System.Collections.Generic; +using {{ spec.title | caseUcfirst }}.Services; +using Cysharp.Threading.Tasks; +using UnityEngine; + +namespace {{ spec.title | caseUcfirst }} +{ + /// + /// Unity MonoBehaviour wrapper for {{ spec.title | caseUcfirst }} Client with DI support + /// + public class {{ spec.title | caseUcfirst }}Manager : MonoBehaviour + { + [Header("Configuration")] + [SerializeField] private {{ spec.title | caseUcfirst }}Config config; + [SerializeField] private bool initializeOnStart = true; + [SerializeField] private bool dontDestroyOnLoad = true; + + private Client _client; + private Realtime _realtime; + private bool _isInitialized; + + private readonly Dictionary _services = new(); + + // Events + public static event Action OnClientInitialized; + public static event Action OnClientDestroyed; + + // Singleton instance for easy access + public static {{ spec.title | caseUcfirst }}Manager Instance { get; private set; } + + // Properties + public Client Client + { + get + { + if (_client == null) + throw new InvalidOperationException("{{ spec.title | caseUcfirst }} client is not initialized. Call Initialize() first."); + return _client; + } + } + + public Realtime Realtime + { + get + { + if (ReferenceEquals(_realtime, null)) + Debug.LogWarning("Realtime was not initialized. Call Initialize(true) to enable it."); + return _realtime; + } + } + + public bool IsInitialized => _isInitialized; + public {{ spec.title | caseUcfirst }}Config Config => config; + + private void Awake() + { + if (ReferenceEquals(Instance, null)) + { + Instance = this; + if (dontDestroyOnLoad) + DontDestroyOnLoad(gameObject); + } + else if (!ReferenceEquals(Instance, this)) + { + Debug.LogWarning("Multiple {{ spec.title | caseUcfirst }}Manager instances detected. Destroying duplicate."); + Destroy(gameObject); + } + } + + private void Start() + { + if (initializeOnStart) + { + Initialize().Forget(); + } + } + + /// + /// Initialize the {{ spec.title | caseUcfirst }} client and selected services + /// + public async UniTask Initialize(bool needRealtime = false) + { + if (_isInitialized) + { + Debug.LogWarning("{{ spec.title | caseUcfirst }} client is already initialized"); + return true; + } + + if (!config) + { + Debug.LogError("{{ spec.title | caseUcfirst }}Config is not assigned!"); + return false; + } + + try + { + _client = new Client(); + config.ApplyTo(_client); + + InitializeSelectedServices(); + + if (config.AutoConnect) + { + var pingResult = await _client.Ping(); + Debug.Log($"{{ spec.title | caseUcfirst }} connected successfully: {pingResult}"); + } + + if (needRealtime) + { + InitializeRealtime(); + } + + _isInitialized = true; + OnClientInitialized?.Invoke(_client); + + Debug.Log("{{ spec.title | caseUcfirst }} client initialized successfully"); + return true; + } + catch (Exception ex) + { + Debug.LogError($"Failed to initialize {{ spec.title | caseUcfirst }} client: {ex.Message}"); + return false; + } + } + + /// + /// Initialize selected {{ spec.title | caseUcfirst }} services based on the configuration. + /// + private void InitializeSelectedServices() + { + _services.Clear(); + var servicesToInit = config.ServicesToInitialize; + + // Direct service instantiation - no reflection needed for known service types + // This is more performant and AOT-friendly than generic reflection + + if (servicesToInit.HasFlag({{ spec.title | caseUcfirst }}Service.Account)) + TryCreateService(); + + if (servicesToInit.HasFlag({{ spec.title | caseUcfirst }}Service.Databases)) + TryCreateService(); + + if (servicesToInit.HasFlag({{ spec.title | caseUcfirst }}Service.Functions)) + TryCreateService(); + + if (servicesToInit.HasFlag({{ spec.title | caseUcfirst }}Service.Storage)) + TryCreateService(); + + if (servicesToInit.HasFlag({{ spec.title | caseUcfirst }}Service.Avatars)) + TryCreateService(); + + if (servicesToInit.HasFlag({{ spec.title | caseUcfirst }}Service.Graphql)) + TryCreateService(); + + if (servicesToInit.HasFlag({{ spec.title | caseUcfirst }}Service.Locale)) + TryCreateService(); + + if (servicesToInit.HasFlag({{ spec.title | caseUcfirst }}Service.Messaging)) + TryCreateService(); + + if (servicesToInit.HasFlag({{ spec.title | caseUcfirst }}Service.Teams)) + TryCreateService(); + } + + /// + /// Try to create and register a service instance. + /// + private void TryCreateService() where T : Service + { + try + { + var service = (T)Activator.CreateInstance(typeof(T), _client); + _services[typeof(T)] = service; + } + catch (Exception ex) + { + Debug.LogError($"Failed to create service {typeof(T).Name}: {ex.Message}"); + } + } + + private void InitializeRealtime() + { + if (_client == null) + throw new InvalidOperationException("Client must be initialized before realtime"); + if (ReferenceEquals(_realtime, null)) + { + var realtimeGo = new GameObject("{{ spec.title | caseUcfirst }}Realtime"); + realtimeGo.transform.SetParent(transform); + _realtime = realtimeGo.AddComponent(); + _realtime.Initialize(_client); + } + else + { + // Update existing realtime with new client reference + _realtime.UpdateClient(_client); + } + } + + /// + /// Get an initialized service instance + /// + public T GetService() where T : class + { + if (!_isInitialized) + throw new InvalidOperationException("Client is not initialized. Call Initialize() first."); + + var type = typeof(T); + if (_services.TryGetValue(type, out var service)) + { + return (T)service; + } + + throw new InvalidOperationException($"Service of type {type.Name} was not initialized. Ensure it is selected in the {{ spec.title | caseUcfirst }}Config asset."); + } + + /// + /// Try to get an initialized service instance without throwing an exception. + /// + /// True if the service was found and initialized, otherwise false. + public bool TryGetService(out T service) where T : class + { + if (!_isInitialized) + { + service = null; + Debug.LogWarning("{{ spec.title | caseUcfirst }}Manager: Cannot get service, client is not initialized."); + return false; + } + + var type = typeof(T); + if (_services.TryGetValue(type, out var serviceObj)) + { + service = (T)serviceObj; + return true; + } + + service = null; + return false; + } + + public void SetConfig({{ spec.title | caseUcfirst }}Config newConfig) + { + config = newConfig; + } + + public async UniTask Reinitialize({{ spec.title | caseUcfirst }}Config newConfig = null, bool needRealtime = false) + { + config = newConfig ?? config; + Shutdown(); + return await Initialize(needRealtime); + } + + private void Shutdown() + { + if (!ReferenceEquals(_realtime, null)) + { + _realtime.Disconnect().Forget(); + if (_realtime.gameObject != null) + Destroy(_realtime.gameObject); + } + _realtime = null; + _client = null; + _isInitialized = false; + _services.Clear(); + + OnClientDestroyed?.Invoke(); + Debug.Log("{{ spec.title | caseUcfirst }} client shutdown"); + } + + private void OnDestroy() + { + if (Instance == this) + { + Shutdown(); + Instance = null; + } + } + } +} +#endif diff --git a/templates/unity/Assets/Runtime/Core/Appwrite.Core.asmdef.twig b/templates/unity/Assets/Runtime/Core/Appwrite.Core.asmdef.twig new file mode 100644 index 0000000000..f28030d1b4 --- /dev/null +++ b/templates/unity/Assets/Runtime/Core/Appwrite.Core.asmdef.twig @@ -0,0 +1,22 @@ +{ + "name": "{{ spec.title | caseUcfirst }}.Core", + "rootNamespace": "{{ spec.title | caseUcfirst }}", + "references": [ + "UniTask" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [ + { + "name": "com.cysharp.unitask", + "expression": "", + "define": "UNI_TASK" + } + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/templates/unity/Assets/Runtime/Core/Client.cs.twig b/templates/unity/Assets/Runtime/Core/Client.cs.twig new file mode 100644 index 0000000000..eb052e873a --- /dev/null +++ b/templates/unity/Assets/Runtime/Core/Client.cs.twig @@ -0,0 +1,729 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +#if UNI_TASK +using Cysharp.Threading.Tasks; +#endif +using UnityEngine; +using UnityEngine.Networking; +using {{ spec.title | caseUcfirst }}.Converters; +using {{ spec.title | caseUcfirst }}.Extensions; +using {{ spec.title | caseUcfirst }}.Models; + +namespace {{ spec.title | caseUcfirst }} +{ + public class Client + { + private const string SESSION_PREF = "{{ spec.title | caseUcfirst }}_Session"; + private const string JWT_PREF = "{{ spec.title | caseUcfirst }}_JWT"; + + public string Endpoint => _endpoint; + public Dictionary Config => _config; + public CookieContainer CookieContainer => _cookieContainer; + + private readonly Dictionary _headers; + private readonly Dictionary _config; + private string _endpoint; + private bool _selfSigned; + private readonly CookieContainer _cookieContainer; + + private static readonly int ChunkSize = 5 * 1024 * 1024; + + public static JsonSerializerOptions DeserializerOptions { get; set; } = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNameCaseInsensitive = true, + Converters = + { + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), + new ValueClassConverter(), + new ObjectToInferredTypesConverter() + } + }; + + public static JsonSerializerOptions SerializerOptions { get; set; } = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = + { + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), + new ValueClassConverter(), + new ObjectToInferredTypesConverter() + } + }; + + public Client( + string endpoint = "{{spec.endpoint}}", + bool selfSigned = false) + { + _endpoint = endpoint; + _selfSigned = selfSigned; + _cookieContainer = new CookieContainer(); + + _headers = new Dictionary() + { + { "content-type", "application/json" }, + { "user-agent" , $"{{spec.title | caseUcfirst}}{{ language.name | caseUcfirst }}SDK/{{ sdk.version }} ({Environment.OSVersion.Platform}; {Environment.OSVersion.VersionString})"}, + { "x-sdk-name", "{{ sdk.name }}" }, + { "x-sdk-platform", "{{ sdk.platform }}" }, + { "x-sdk-language", "{{ language.name | caseLower }}" }, + { "x-sdk-version", "{{ sdk.version }}"}{% if spec.global.defaultHeaders | length > 0 %}, + {%~ for key,header in spec.global.defaultHeaders %} + { "{{key}}", "{{header}}" }{% if not loop.last %},{% endif %} + {%~ endfor %}{% endif %} + + }; + + _config = new Dictionary(); + // Load persistent data (session and JWT) + LoadSession(); + // Note: CookieContainer handles its own loading in constructor based on platform + } + + public Client SetSelfSigned(bool selfSigned) + { + _selfSigned = selfSigned; + return this; + } + + public Client SetEndpoint(string endpoint) + { + if (!endpoint.StartsWith("http://") && !endpoint.StartsWith("https://")) { + throw new {{ spec.title | caseUcfirst }}Exception("Invalid endpoint URL: " + endpoint); + } + + _endpoint = endpoint; + return this; + } +#if UNI_TASK + /// + /// Sends a "ping" request to {{ spec.title | caseUcfirst }} to verify connectivity. + /// + /// Ping response as string + public async UniTask Ping() + { + var headers = new Dictionary + { + ["content-type"] = "application/json" + }; + + var parameters = new Dictionary(); + + return await Call("GET", "/ping", headers, parameters, + response => (response.TryGetValue("result", out var result) ? result?.ToString() : null) ?? string.Empty); + } +#endif + /// + /// Set realtime endpoint for WebSocket connections + /// + /// Realtime endpoint URL + /// Client instance for method chaining + public Client SetEndPointRealtime(string endpointRealtime) + { + if (!endpointRealtime.StartsWith("ws://") && !endpointRealtime.StartsWith("wss://")) { + throw new {{ spec.title | caseUcfirst }}Exception("Invalid realtime endpoint URL: " + endpointRealtime); + } + + _config["endpointRealtime"] = endpointRealtime; + return this; + } + + {%~ for header in spec.global.headers %} + {%~ if header.description %} + /// {{header.description}} + {%~ endif %} + public Client Set{{header.key | caseUcfirst}}(string value) { + _config["{{ header.key | caseCamel }}"] = value; + AddHeader("{{header.name}}", value); + {%~ if header.key | caseCamel == "session" or header.key | caseCamel == "jWT" %} + SaveSession(); + {%~ endif %} + + return this; + } + + {%~ endfor %} + /// + /// Get the current session from config + /// + /// Current session token or null + public string GetSession() + { + return _config.GetValueOrDefault("session"); + } + + /// + /// Get the current JWT from config + /// + /// Current JWT token or null + public string GetJWT() + { + return _config.GetValueOrDefault("jWT"); + } + + /// + /// Clear session and JWT from client + /// + /// Client instance for method chaining + public Client ClearSession() + { + _config.Remove("session"); + _config.Remove("jWT"); + _headers.Remove("X-{{ spec.title | caseUcfirst }}-Session"); + _headers.Remove("X-{{ spec.title | caseUcfirst }}-JWT"); + SaveSession(); // Auto-save when session is cleared + return this; + } + + public Client AddHeader(string key, string value) + { + _headers[key] = value; + return this; + } + + /// + /// Load session data from persistent storage + /// + private void LoadSession() + { + try { + LoadPref(SESSION_PREF, "session", "X-{{ spec.title | caseUcfirst }}-Session"); + LoadPref(JWT_PREF, "jWT", "X-{{ spec.title | caseUcfirst }}-JWT"); + } catch (Exception ex) { + Debug.LogWarning($"Failed to load session: {ex.Message}"); + } + } + + private void LoadPref(string prefKey, string configKey, string headerKey) + { + if (!PlayerPrefs.HasKey(prefKey)) return; + var value = PlayerPrefs.GetString(prefKey); + if (string.IsNullOrEmpty(value)) return; + _config[configKey] = value; + _headers[headerKey] = value; + } + + /// + /// Save session data to persistent storage + /// + private void SaveSession() + { + try { + SavePref("session", SESSION_PREF); + SavePref("jWT", JWT_PREF); + PlayerPrefs.Save(); + } catch (Exception ex) { + Debug.LogWarning($"Failed to save session: {ex.Message}"); + } + } + + private void SavePref(string configKey, string prefKey) + { + if (_config.ContainsKey(configKey)) { + PlayerPrefs.SetString(prefKey, _config[configKey]); + } + else { + PlayerPrefs.DeleteKey(prefKey); + } + } + + /// + /// Delete persistent session storage + /// + public void DeleteSessionStorage() + { + try { + PlayerPrefs.DeleteKey(SESSION_PREF); + PlayerPrefs.DeleteKey(JWT_PREF); + PlayerPrefs.Save(); + _cookieContainer.DeleteCookieStorage(); + } catch (Exception ex) { + Debug.LogWarning($"Failed to delete session storage: {ex.Message}"); + } + } + + private UnityWebRequest PrepareRequest( + string method, + string path, + Dictionary headers, + Dictionary parameters) + { + var methodGet = "GET".Equals(method, StringComparison.OrdinalIgnoreCase); + var queryString = methodGet ? "?" + parameters.ToQueryString() : string.Empty; + var url = _endpoint + path + queryString; + + var isMultipart = headers.TryGetValue("Content-Type", out var contentType) && + "multipart/form-data".Equals(contentType, StringComparison.OrdinalIgnoreCase); + + UnityWebRequest request; + + if (isMultipart) + { + var form = new List(); + + foreach (var parameter in parameters) + { + if (parameter.Key == "file" && parameter.Value is InputFile inputFile) + { + byte[] fileData = {}; + switch (inputFile.SourceType) + { + case "path": + if (System.IO.File.Exists(inputFile.Path)) + { + fileData = System.IO.File.ReadAllBytes(inputFile.Path); + } + break; + case "stream": + if (inputFile.Data is Stream stream) + { + using (var memoryStream = new MemoryStream()) + { + stream.CopyTo(memoryStream); + fileData = memoryStream.ToArray(); + } + } + break; + case "bytes": + fileData = inputFile.Data as byte[] ?? Array.Empty(); + break; + } + + form.Add(new MultipartFormFileSection(parameter.Key, fileData, inputFile.Filename, inputFile.MimeType)); + } + else if (parameter.Value is IEnumerable enumerable) + { + if (parameter.Value == null) continue; + var list = new List(enumerable); + for (int index = 0; index < list.Count; index++) + { + form.Add(new MultipartFormDataSection($"{parameter.Key}[{index}]", list[index]?.ToString() ?? string.Empty)); + } + } + else + { + if (parameter.Value == null) continue; + form.Add(new MultipartFormDataSection(parameter.Key, parameter.Value?.ToString() ?? string.Empty)); + } + } + request = UnityWebRequest.Post(url, form); + } + else if (methodGet) + { + request = UnityWebRequest.Get(url); + } + else + { + request = CreateJsonRequest(url, method, parameters); + } + + // Add default headers + foreach (var header in _headers) + { + if (!header.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) || !isMultipart) + { + request.SetRequestHeader(header.Key, header.Value); + } + } + + // Add specific headers + foreach (var header in headers) + { + if (!header.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) || !isMultipart) + { + request.SetRequestHeader(header.Key, header.Value); + } + } + + // Add cookies + var uri = new Uri(url); + var cookieHeader = _cookieContainer.GetCookieHeader(uri.Host, uri.AbsolutePath); +#if !(UNITY_WEBGL && !UNITY_EDITOR) + if (!string.IsNullOrEmpty(cookieHeader)) + { + Debug.Log($"[Client] Setting cookie header: {cookieHeader}"); + request.SetRequestHeader("Cookie", cookieHeader); + } +#endif + + // Handle self-signed certificates + if (_selfSigned) + { + request.certificateHandler = new AcceptAllCertificatesSignedWithASpecificKeyPublicKey(); + } + + return request; + } + + private UnityWebRequest CreateJsonRequest(string url, string method, Dictionary parameters) + { + string body = parameters.ToJson(); + byte[] bodyData = Encoding.UTF8.GetBytes(body); + + var request = new UnityWebRequest(url, method.ToUpperInvariant()); + request.uploadHandler = new UploadHandlerRaw(bodyData); + request.downloadHandler = new DownloadHandlerBuffer(); + request.SetRequestHeader("Content-Type", "application/json"); + + return request; + } + +#if UNI_TASK + public async UniTask Redirect( + string method, + string path, + Dictionary headers, + Dictionary parameters) + { + var request = PrepareRequest(method, path, headers, parameters); + request.redirectLimit = 0; // Disable auto-redirect + + var operation = request.SendWebRequest(); + + while (!operation.isDone) + { + await UniTask.Yield(); + } + + var code = (int)request.responseCode; + + if (code >= 400) + { + var text = request.downloadHandler?.text ?? string.Empty; + var message = ""; + var type = ""; + + var contentType = request.GetResponseHeader("Content-Type") ?? string.Empty; + + if (contentType.Contains("application/json")) + { + try + { + using var errorDoc = JsonDocument.Parse(text); + message = errorDoc.RootElement.GetProperty("message").GetString() ?? ""; + if (errorDoc.RootElement.TryGetProperty("type", out var typeElement)) + { + type = typeElement.GetString() ?? ""; + } + } + catch + { + message = text; + } + } + else + { + message = text; + } + + request.Dispose(); + throw new {{ spec.title | caseUcfirst }}Exception(message, code, type, text); + } + + var location = request.GetResponseHeader("Location") ?? string.Empty; + request.Dispose(); + return location; + } + + public UniTask> Call( + string method, + string path, + Dictionary headers, + Dictionary parameters) + { + return Call>(method, path, headers, parameters); + } + + public async UniTask Call( + string method, + string path, + Dictionary headers, + Dictionary parameters, + Func, T>? convert = null) where T : class + { + var request = PrepareRequest(method, path, headers, parameters); + + var operation = request.SendWebRequest(); + while (!operation.isDone) + { + await UniTask.Yield(); + } + + var code = (int)request.responseCode; + + // Handle cookies after response +#if !(UNITY_WEBGL && !UNITY_EDITOR) + // Handle Set-Cookie headers (non-WebGL) + var setCookieHeader = request.GetResponseHeader("Set-Cookie"); + if (!string.IsNullOrEmpty(setCookieHeader)) + { + var uri = new Uri(request.url); + _cookieContainer.ParseSetCookieHeader(setCookieHeader, uri.Host); + } +#endif + + // Check for warnings + var warning = request.GetResponseHeader("x-{{ spec.title | lower }}-warning"); + if (!string.IsNullOrEmpty(warning)) + { + Debug.LogWarning("Warning: " + warning); + } + + var contentType = request.GetResponseHeader("Content-Type") ?? string.Empty; + var isJson = contentType.Contains("application/json"); + + if (code >= 400) + { + var text = request.downloadHandler?.text ?? string.Empty; + var message = ""; + var type = ""; + + if (isJson) + { + try + { + using var errorDoc = JsonDocument.Parse(text); + message = errorDoc.RootElement.GetProperty("message").GetString() ?? ""; + if (errorDoc.RootElement.TryGetProperty("type", out var typeElement)) + { + type = typeElement.GetString() ?? ""; + } + } + catch + { + message = text; + } + } + else + { + message = text; + } + + request.Dispose(); + throw new {{ spec.title | caseUcfirst }}Exception(message, code, type, text); + } + + if (isJson) + { + var responseString = request.downloadHandler.text; + + var dict = JsonSerializer.Deserialize>( + responseString, + DeserializerOptions); + + request.Dispose(); + + if (convert != null && dict != null) + { + return convert(dict); + } + + return (dict as T)!; + } + else + { + var result = request.downloadHandler.data as T; + request.Dispose(); + return result!; + } + } + + public async UniTask ChunkedUpload( + string path, + Dictionary headers, + Dictionary parameters, + Func, T> converter, + string paramName, + string? idParamName = null, + Action? onProgress = null) where T : class + { + if (string.IsNullOrEmpty(paramName)) + throw new ArgumentException("Parameter name cannot be null or empty", nameof(paramName)); + + if (!parameters.ContainsKey(paramName)) + throw new ArgumentException($"Parameter {paramName} not found", nameof(paramName)); + + var input = parameters[paramName] as InputFile; + if (input == null) + throw new ArgumentException($"Parameter {paramName} must be an InputFile", nameof(paramName)); + + var size = 0L; + switch(input.SourceType) + { + case "path": + var info = new FileInfo(input.Path); + input.Data = info.OpenRead(); + size = info.Length; + break; + case "stream": + var stream = input.Data as Stream; + if (stream == null) + throw new InvalidOperationException("Stream data is null"); + size = stream.Length; + break; + case "bytes": + var bytes = input.Data as byte[]; + if (bytes == null) + throw new InvalidOperationException("Byte array data is null"); + size = bytes.Length; + break; + }; + + var offset = 0L; + var buffer = new byte[Math.Min(size, ChunkSize)]; + var result = new Dictionary(); + + if (size < ChunkSize) + { + switch(input.SourceType) + { + case "path": + case "stream": + var dataStream = input.Data as Stream; + if (dataStream == null) + throw new InvalidOperationException("Stream data is null"); + await dataStream.ReadAsync(buffer, 0, (int)size); + break; + case "bytes": + var dataBytes = input.Data as byte[]; + if (dataBytes == null) + throw new InvalidOperationException("Byte array data is null"); + buffer = dataBytes; + break; + } + + var multipartHeaders = new Dictionary(headers) + { + ["Content-Type"] = "multipart/form-data" + }; + + var multipartParameters = new Dictionary(parameters); + multipartParameters[paramName] = new InputFile + { + Data = buffer, + Filename = input.Filename, + MimeType = input.MimeType, + SourceType = "bytes" + }; + + return await Call( + method: "POST", + path, + multipartHeaders, + multipartParameters, + converter + ); + } + + if (!string.IsNullOrEmpty(idParamName)) + { + try + { + // Make a request to check if a file already exists + var current = await Call>( + method: "GET", + path: $"{path}/{parameters[idParamName!]}", + new Dictionary { { "Content-Type", "application/json" } }, + parameters: new Dictionary() + ); + if (current.TryGetValue("chunksUploaded", out var chunksUploadedValue) && chunksUploadedValue != null) + { + offset = Convert.ToInt64(chunksUploadedValue) * ChunkSize; + } + } + catch + { + // ignored as it mostly means file not found + } + } + + while (offset < size) + { + switch(input.SourceType) + { + case "path": + case "stream": + var stream = input.Data as Stream; + if (stream == null) + throw new InvalidOperationException("Stream data is null"); + stream.Seek(offset, SeekOrigin.Begin); + await stream.ReadAsync(buffer, 0, ChunkSize); + break; + case "bytes": + buffer = ((byte[])input.Data) + .Skip((int)offset) + .Take((int)Math.Min(size - offset, ChunkSize - 1)) + .ToArray(); + break; + } + + var chunkHeaders = new Dictionary(headers) + { + ["Content-Type"] = "multipart/form-data", + ["Content-Range"] = $"bytes {offset}-{Math.Min(offset + ChunkSize - 1, size - 1)}/{size}" + }; + + var chunkParameters = new Dictionary(parameters); + chunkParameters[paramName] = new InputFile + { + Data = buffer, + Filename = input.Filename, + MimeType = input.MimeType, + SourceType = "bytes" + }; + + result = await Call>( + method: "POST", + path, + chunkHeaders, + chunkParameters + ); + + offset += ChunkSize; + + var id = result.ContainsKey("$id") + ? result["$id"]?.ToString() ?? string.Empty + : string.Empty; + var chunksTotal = result.TryGetValue("chunksTotal", out var chunksTotalValue) && chunksTotalValue != null + ? Convert.ToInt64(chunksTotalValue) + : 0L; + var chunksUploaded = result.TryGetValue("chunksUploaded", out var chunksUploadedValue) && chunksUploadedValue != null + ? Convert.ToInt64(chunksUploadedValue) + : 0L; + + headers["x-{{ spec.title | lower }}-id"] = id; + + onProgress?.Invoke( + new UploadProgress( + id: id, + progress: Math.Min(offset, size) / size * 100, + sizeUploaded: Math.Min(offset, size), + chunksTotal: chunksTotal, + chunksUploaded: chunksUploaded)); + } + + // Convert to non-nullable dictionary for converter + var nonNullableResult = result.Where(kvp => kvp.Value != null) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value!); + + return converter(nonNullableResult); + } +#endif + + } + + // Custom certificate handler for self-signed certificates + public class AcceptAllCertificatesSignedWithASpecificKeyPublicKey : CertificateHandler + { + protected override bool ValidateCertificate(byte[] certificateData) + { + return true; // Accept all certificates + } + } +} diff --git a/templates/unity/Assets/Runtime/Core/CookieContainer.cs.twig b/templates/unity/Assets/Runtime/Core/CookieContainer.cs.twig new file mode 100644 index 0000000000..110c365e98 --- /dev/null +++ b/templates/unity/Assets/Runtime/Core/CookieContainer.cs.twig @@ -0,0 +1,291 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using UnityEngine; +#if UNITY_WEBGL && !UNITY_EDITOR +using System.Runtime.InteropServices; +#endif + +namespace {{ spec.title | caseUcfirst }} +{ + /// + /// Simple cookie container for Unity WebRequest + /// + [Serializable] + public class Cookie + { + public string Name { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public string Domain { get; set; } = string.Empty; + public string Path { get; set; } = "/"; + public DateTime Expires { get; set; } = DateTime.MaxValue; + public int? MaxAge { get; set; } // null means didn't set, 0+ means seconds from creation + public DateTime CreatedAt { get; set; } = DateTime.Now; + public bool HttpOnly { get; set; } + public bool Secure { get; set; } + public string SameSite { get; set; } = string.Empty; + + // Max-Age priority over Expires + public bool IsExpired => + MaxAge.HasValue + ? MaxAge <= 0 || DateTime.Now > CreatedAt.AddSeconds(MaxAge.Value) + : DateTime.Now > Expires; + + public bool MatchesDomain(string requestDomain) + { + if (string.IsNullOrEmpty(Domain)) return true; + var d = Domain.ToLowerInvariant(); + var r = requestDomain.ToLowerInvariant(); + return r == d || r.EndsWith("." + d) || (d.StartsWith(".") && r.EndsWith(d)); + } + + public bool MatchesPath(string requestPath) => + string.IsNullOrEmpty(Path) || requestPath.StartsWith(Path, StringComparison.OrdinalIgnoreCase); + + public override string ToString() + { + return $"{Name}={Value} " + + $"{(string.IsNullOrEmpty(Domain) ? "" : $"; Domain={Domain}")}" + + $"{(string.IsNullOrEmpty(Path) ? "" : $"; Path={Path}")}" + + $"{(Expires == DateTime.MaxValue ? "" : $"; Expires={Expires:R}")}" + + $"{(MaxAge.HasValue ? $"; Max-Age={MaxAge.Value}" : "")}" + + $"{(HttpOnly ? "; HttpOnly" : "")}" + + $"{(Secure ? "; Secure" : "")}" + + $"{(string.IsNullOrEmpty(SameSite) ? "" : $"; SameSite={SameSite}")}"; + } + } + + /// + /// Simple cookie container implementation for Unity + /// + public class CookieContainer + { + private readonly List _cookies = new List(); + private readonly object _lock = new object(); + + private const string CookiePrefsKey = "{{ spec.title | caseUcfirst }}_Cookies"; +#if UNITY_WEBGL && !UNITY_EDITOR + [DllImport("__Internal")] + private static extern void EnableWebGLHttpCredentials(int enable); +#endif + + public CookieContainer() + { +#if UNITY_WEBGL && !UNITY_EDITOR + try + { + EnableWebGLHttpCredentials(1); + Debug.Log("[CookieContainer] WebGL credentials enabled - browser will handle cookies."); + } + catch + { + // Ignore errors - jslib may not be loaded + } + // In WebGL, the browser handles cookies automatically, so we don't load from PlayerPrefs +#else + // In non-WebGL builds, load cookies from PlayerPrefs + LoadCookies(); +#endif + } + + /// + /// Add a cookie to the container + /// + private void AddCookie(Cookie cookie) + { + if (cookie?.Name == null) return; + + lock (_lock) + { + // Remove existing cookie with the same name, domain, and path + Debug.Log($"[CookieContainer] Removing duplicates for {cookie.Name}"); + _cookies.RemoveAll(c => c.Name == cookie.Name && c.Domain == cookie.Domain && c.Path == cookie.Path); + if (!cookie.IsExpired) + { + _cookies.Add(cookie); + Debug.Log($"[CookieContainer] Cookie added to container: {cookie}"); + SaveCookies(); // Auto-save when cookie is added + } + else + { + Debug.Log($"[CookieContainer] Cookie is expired, not added: {cookie.Name}"); + } + } + } + + /// + /// Get cookies for a specific domain and path + /// + public List GetCookies(string domain, string path = "/") + { + lock (_lock) + { + CleanExpiredCookies(); + var list = _cookies.Where(c => + c.MatchesDomain(domain) && + c.MatchesPath(path) && + !c.IsExpired).ToList(); + Debug.Log($"[CookieContainer] GetCookies for domain={domain} path={path} => {list.Count}"); + return list; + } + } + + /// + /// Get cookie header string for request + /// + public string GetCookieHeader(string domain, string path = "/") => + string.Join("; ", GetCookies(domain, path) + .Select(c => $"{c.Name}={c.Value}")); + + /// + /// Parse Set-Cookie header and add to container + /// + public void ParseSetCookieHeader(string setCookieHeader, string domain) + { + if (string.IsNullOrWhiteSpace(setCookieHeader) || string.IsNullOrWhiteSpace(domain)) return; + foreach (var c in setCookieHeader.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) + ParseCookie(c.Trim(), domain); + } + + /// + /// Parse a single cookie string + /// + private void ParseCookie(string cookieString, string domain) + { + var parts = cookieString.Split(';'); + var kv = parts[0].Split('=', 2); + if (kv.Length != 2) return; + + var c = new Cookie { Name = kv[0].Trim(), Value = kv[1].Trim(), Domain = domain.ToLowerInvariant() }; + foreach (var p in parts.Skip(1)) + { + var seg = p.Split('=', 2); + var key = seg[0].Trim().ToLowerInvariant(); + var val = seg.Length > 1 ? seg[1].Trim() : null; + switch (key) + { + case "domain": c.Domain = val?.ToLowerInvariant() ?? string.Empty; break; + case "path": c.Path = val ?? string.Empty; break; + case "expires": + if (DateTime.TryParse(val, out var e)) c.Expires = e; + break; + case "max-age": + if (int.TryParse(val, out var m)) c.MaxAge = m; + break; + case "httponly": c.HttpOnly = true; break; + case "secure": c.Secure = true; break; + case "samesite": c.SameSite = val?.ToLowerInvariant() ?? string.Empty; break; + } + } + Debug.Log($"[CookieContainer] Parsed cookie => {c}"); + AddCookie(c); + } + + /// + /// Clear all cookies + /// + public void Clear() + { + lock (_lock) + { + _cookies.Clear(); + SaveCookies(); // Auto-save when cookies are cleared + } + } + + /// + /// Get the total number of cookies in the container + /// + public int Count + { + get + { + lock (_lock) + { + CleanExpiredCookies(); + return _cookies.Count; + } + } + } + + public string GetContents() + { + lock (_lock) + { + CleanExpiredCookies(); + return string.Join("\n", _cookies); + } + } + + /// + /// Remove expired cookies (must be called within lock) + /// + private void CleanExpiredCookies() => + _cookies.RemoveAll(c => c == null || c.IsExpired); + + /// + /// Load cookies from persistent storage + /// + public void LoadCookies() + { + lock (_lock) + { + try + { + if (PlayerPrefs.HasKey(CookiePrefsKey)) + { + var loaded = JsonSerializer.Deserialize>(PlayerPrefs.GetString(CookiePrefsKey), Client.DeserializerOptions); + _cookies.Clear(); + if (loaded != null) + { + _cookies.AddRange(loaded); + } + } + Debug.Log($"[CookieContainer] Loaded cookies from prefs: {_cookies.Count}"); + CleanExpiredCookies(); + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to load cookies: {ex.Message}"); + _cookies.Clear(); + } + } + } + + /// + /// Save cookies to persistent storage (must be called within lock) + /// + private void SaveCookies() + { + // Note: This method should only be called from within a lock block + try + { + CleanExpiredCookies(); + var json = JsonSerializer.Serialize(_cookies, Client.SerializerOptions); + PlayerPrefs.SetString(CookiePrefsKey, json); + PlayerPrefs.Save(); + Debug.Log($"[CookieContainer] Saved cookies to prefs: {_cookies.Count}"); + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to save cookies: {ex.Message}"); + } + } + + /// + /// Delete persistent cookie storage + /// + public void DeleteCookieStorage() + { + lock (_lock) + { + _cookies.Clear(); + if (PlayerPrefs.HasKey(CookiePrefsKey)) + PlayerPrefs.DeleteKey(CookiePrefsKey); + PlayerPrefs.Save(); + Debug.Log("[CookieContainer] Deleted cookie storage"); + } + } + } +} diff --git a/templates/unity/Assets/Runtime/Core/Plugins/Android/AndroidManifest.xml b/templates/unity/Assets/Runtime/Core/Plugins/Android/AndroidManifest.xml new file mode 100644 index 0000000000..3d7dc9f313 --- /dev/null +++ b/templates/unity/Assets/Runtime/Core/Plugins/Android/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + diff --git a/templates/unity/Assets/Runtime/Core/Plugins/Android/AndroidManifest.xml.twig b/templates/unity/Assets/Runtime/Core/Plugins/Android/AndroidManifest.xml.twig new file mode 100644 index 0000000000..e69de29bb2 diff --git a/templates/unity/Assets/Runtime/Core/Plugins/Microsoft.Bcl.AsyncInterfaces.dll b/templates/unity/Assets/Runtime/Core/Plugins/Microsoft.Bcl.AsyncInterfaces.dll new file mode 100644 index 0000000000..29fb9b9370 Binary files /dev/null and b/templates/unity/Assets/Runtime/Core/Plugins/Microsoft.Bcl.AsyncInterfaces.dll differ diff --git a/templates/unity/Assets/Runtime/Core/Plugins/System.IO.Pipelines.dll b/templates/unity/Assets/Runtime/Core/Plugins/System.IO.Pipelines.dll new file mode 100644 index 0000000000..796ec8395c Binary files /dev/null and b/templates/unity/Assets/Runtime/Core/Plugins/System.IO.Pipelines.dll differ diff --git a/templates/unity/Assets/Runtime/Core/Plugins/System.Runtime.CompilerServices.Unsafe.dll b/templates/unity/Assets/Runtime/Core/Plugins/System.Runtime.CompilerServices.Unsafe.dll new file mode 100644 index 0000000000..491a80a978 Binary files /dev/null and b/templates/unity/Assets/Runtime/Core/Plugins/System.Runtime.CompilerServices.Unsafe.dll differ diff --git a/templates/unity/Assets/Runtime/Core/Plugins/System.Text.Encodings.Web.dll b/templates/unity/Assets/Runtime/Core/Plugins/System.Text.Encodings.Web.dll new file mode 100644 index 0000000000..6042c008fa Binary files /dev/null and b/templates/unity/Assets/Runtime/Core/Plugins/System.Text.Encodings.Web.dll differ diff --git a/templates/unity/Assets/Runtime/Core/Plugins/System.Text.Json.dll b/templates/unity/Assets/Runtime/Core/Plugins/System.Text.Json.dll new file mode 100644 index 0000000000..9f85348581 Binary files /dev/null and b/templates/unity/Assets/Runtime/Core/Plugins/System.Text.Json.dll differ diff --git a/templates/unity/Assets/Runtime/Core/Plugins/WebGLCookies.jslib b/templates/unity/Assets/Runtime/Core/Plugins/WebGLCookies.jslib new file mode 100644 index 0000000000..e93b35e3ca --- /dev/null +++ b/templates/unity/Assets/Runtime/Core/Plugins/WebGLCookies.jslib @@ -0,0 +1,38 @@ +mergeInto(LibraryManager.library, { + OpenUrlSamePage: function (urlPtr) { + var url = UTF8ToString(urlPtr); + window.location.href = url; + }, + // Enable credentials on fetch/XHR so cookies are sent/received on cross-origin requests + EnableWebGLHttpCredentials: function(enable) { + try { + if (enable) { + // Patch fetch to default credentials: 'include' + if (typeof window !== 'undefined' && window.fetch && !window.__aw_fetchPatched) { + var origFetch = window.fetch.bind(window); + window.fetch = function(input, init) { + init = init || {}; + if (!init.credentials) init.credentials = 'include'; + return origFetch(input, init); + }; + window.__aw_fetchPatched = true; + } + // Patch XHR to set withCredentials=true + if (typeof window !== 'undefined' && window.XMLHttpRequest && !window.__aw_xhrPatched) { + var p = window.XMLHttpRequest.prototype; + var origOpen = p.open; + var origSend = p.send; + p.open = function() { + try { this.withCredentials = true; } catch (e) {} + return origOpen.apply(this, arguments); + }; + p.send = function() { + try { this.withCredentials = true; } catch (e) {} + return origSend.apply(this, arguments); + }; + window.__aw_xhrPatched = true; + } + } + } catch (e) { /* noop */ } + } +}); diff --git a/templates/unity/Assets/Runtime/Core/Services/ServiceTemplate.cs.twig b/templates/unity/Assets/Runtime/Core/Services/ServiceTemplate.cs.twig new file mode 100644 index 0000000000..ee5cbd9086 --- /dev/null +++ b/templates/unity/Assets/Runtime/Core/Services/ServiceTemplate.cs.twig @@ -0,0 +1,81 @@ +{% import 'dotnet/base/utils.twig' as utils %} +#if UNI_TASK +using System; +using System.Collections.Generic; +using System.Linq; +using Cysharp.Threading.Tasks; +{% if spec.definitions is not empty %} +using {{ spec.title | caseUcfirst }}.Models; +{% endif %} +{% if spec.enums is not empty %} +using {{ spec.title | caseUcfirst }}.Enums; +{% endif %} +{% if service.name|lower == 'account' or service.name|lower == 'general' %} +using {{ spec.title | caseUcfirst }}.Extensions; +using System.Web; +using UnityEngine; +{% endif %} + +namespace {{ spec.title | caseUcfirst }}.Services +{ + public class {{ service.name | caseUcfirst }} : Service + { + public {{ service.name | caseUcfirst }}(Client client) : base(client) + { + } + + {%~ for method in service.methods %} + {%~ if method.description %} + /// + {{~ method.description | dotnetComment }} + /// + {%~ endif %} + /// + {%~ if method.deprecated %} + {%~ if method.since and method.replaceWith %} + [Obsolete("This API has been deprecated since {{ method.since }}. Please use `{{ method.replaceWith | capitalizeFirst }}` instead.")] + {%~ else %} + [Obsolete("This API has been deprecated.")] + {%~ endif %} + {%~ endif %} +{% if method.type == "webAuth" %} +#if UNITY_EDITOR || UNITY_IOS || UNITY_ANDROID || UNITY_WEBGL +{% endif %} + public {% if method.type == "webAuth" %}async {% endif ~%} UniTask{% if method.type == "webAuth" %}{% else %}<{{ utils.resultType(spec.title, method) }}>{% endif %} {{ method.name | caseUcfirst }}({{ utils.method_parameters(method.parameters, method.consumes) }}) + { + var apiPath = "{{ method.path }}"{% if method.parameters.path | length == 0 %};{% endif %} + + {{~ include('dotnet/base/params.twig') }} + + {%~ if method.responseModel %} + static {{ utils.resultType(spec.title, method) }} Convert(Dictionary it) => + {%~ if method.responseModel == 'any' %} + it; + {%~ else %} + {{ utils.resultType(spec.title, method) }}.From(map: it); + {%~ endif %} + {%~ endif %} + + {%~ if method.type == 'location' %} + {{~ include('dotnet/base/requests/location.twig') }} + {%~ elseif method.type == 'webAuth' %} + {{~ include('unity/base/requests/oauth.twig') }} + {%~ elseif 'multipart/form-data' in method.consumes %} + {{~ include('dotnet/base/requests/file.twig') }} + {%~ else %} + {{~ include('dotnet/base/requests/api.twig')}} + {%~ endif %} + } +{% if method.type == "webAuth" %} +#else + public UniTask {{ method.name | caseUcfirst }}({{ utils.method_parameters(method.parameters, method.consumes) }}) + { + Debug.LogWarning("[{{ spec.title | caseUcfirst }}] OAuth2 authorization is not supported on this platform. Available only in Editor, WebGL, iOS or Android."); + return UniTask.CompletedTask; + } +#endif{% endif %} + + {%~ endfor %} + } +} +#endif \ No newline at end of file diff --git a/templates/unity/Assets/Runtime/Core/WebAuthComponent.cs.twig b/templates/unity/Assets/Runtime/Core/WebAuthComponent.cs.twig new file mode 100644 index 0000000000..adb28480e7 --- /dev/null +++ b/templates/unity/Assets/Runtime/Core/WebAuthComponent.cs.twig @@ -0,0 +1,100 @@ +#if (UNITY_EDITOR || UNITY_IOS || UNITY_ANDROID || UNITY_WEBGL) && UNI_TASK +using System; +using System.Collections.Concurrent; +using System.Web; +using Cysharp.Threading.Tasks; +using UnityEngine; + +namespace {{ spec.title | caseUcfirst }} +{ + public static class WebAuthComponent + { + private static readonly ConcurrentDictionary> PendingAuth = new(); + + public static event Action OnDeepLink; + + [RuntimeInitializeOnLoadMethod] + private static void Initialize() + { + Application.deepLinkActivated -= OnDeepLinkActivated; + Application.deepLinkActivated += OnDeepLinkActivated; + Debug.Log("[{{ spec.title | caseUcfirst }}DeepLinkHandler] Initialized for OAuth callbacks."); + } + + private static void OnDeepLinkActivated(string url) + { + Debug.Log($"[{{ spec.title | caseUcfirst }}DeepLinkHandler] Received deep link: {url}"); + OnDeepLink?.Invoke(url); + } + static WebAuthComponent() + { + OnDeepLink += HandleCallback; + } + + public static async UniTask Authenticate(string authUrl) + { + var authUri = new Uri(authUrl); + var projectId = HttpUtility.ParseQueryString(authUri.Query).Get("project"); + if (string.IsNullOrEmpty(projectId)) + { + throw new {{ spec.title | caseUcfirst }}Exception("Project ID not found in authentication URL."); + } + + var callbackScheme = $"{{ spec.title | caseLower }}-callback-{projectId}"; + var tcs = new UniTaskCompletionSource(); + + if (!PendingAuth.TryAdd(callbackScheme, tcs)) + { + throw new {{ spec.title | caseUcfirst }}Exception("Authentication process already in progress."); + } + + Debug.Log($"[WebAuthenticator] Opening authentication URL: {authUrl}"); +#if UNITY_WEBGL && !UNITY_EDITOR + OpenUrlSamePage(authUrl); +#else + Application.OpenURL(authUrl); +#endif + Debug.Log($"[WebAuthenticator] Waiting for callback with scheme: {callbackScheme}"); + + try + { + return await tcs.Task; + } + finally + { + PendingAuth.TryRemove(callbackScheme, out _); + } + } + + private static void HandleCallback(string url) + { + try + { + var uri = new Uri(url); + var scheme = uri.Scheme; + + Debug.Log($"[WebAuthenticator] Received callback with scheme: {scheme}"); + + if (PendingAuth.TryGetValue(scheme, out var tcs)) + { + Debug.Log($"[WebAuthenticator] Found matching pending authentication for scheme: {scheme}"); + tcs.TrySetResult(uri); + } + else + { + Debug.LogWarning($"[WebAuthenticator] No pending authentication found for scheme: {scheme}"); + } + } + catch (Exception ex) + { + Debug.LogError($"[WebAuthenticator] Error handling callback: {ex.Message}"); + } + } + +#if UNITY_WEBGL && !UNITY_EDITOR + [System.Runtime.InteropServices.DllImport("__Internal")] + private static extern void OpenUrlSamePage(string url); +#endif + } +} +#endif \ No newline at end of file diff --git a/templates/unity/Assets/Runtime/Core/csc.rsp b/templates/unity/Assets/Runtime/Core/csc.rsp new file mode 100644 index 0000000000..dcc377f897 --- /dev/null +++ b/templates/unity/Assets/Runtime/Core/csc.rsp @@ -0,0 +1 @@ +-nullable:enable \ No newline at end of file diff --git a/templates/unity/Assets/Runtime/Realtime.cs.twig b/templates/unity/Assets/Runtime/Realtime.cs.twig new file mode 100644 index 0000000000..ce8580020f --- /dev/null +++ b/templates/unity/Assets/Runtime/Realtime.cs.twig @@ -0,0 +1,516 @@ +#if UNI_TASK +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Cysharp.Threading.Tasks; +using UnityEngine; +using NativeWebSocket; + +namespace {{ spec.title | caseUcfirst }} +{ + #region Realtime Data Models + + // Base class to identify a message type + internal class RealtimeMessageBase + { + [JsonPropertyName("type")] + public string Type { get; set; } + } + + // Generic message structure + internal class RealtimeMessage : RealtimeMessageBase + { + [JsonPropertyName("data")] + public T Data { get; set; } + } + + // Specific data models for different message types + internal class RealtimeErrorData + { + [JsonPropertyName("code")] + public int Code { get; set; } + [JsonPropertyName("message")] + public string Message { get; set; } + } + + internal class RealtimeConnectedData + { + [JsonPropertyName("user")] + public Dictionary User { get; set; } + } + + internal class RealtimeAuthData + { + [JsonPropertyName("session")] + public string Session { get; set; } + } + + /// + /// Realtime response event structure + /// + [Serializable] + public class RealtimeResponseEvent + { + [JsonPropertyName("events")] + public string[] Events { get; set; } + [JsonPropertyName("channels")] + public string[] Channels { get; set; } + [JsonPropertyName("timestamp")] + public string Timestamp { get; set; } + [JsonPropertyName("payload")] + public T Payload { get; set; } + } + + #endregion + + /// + /// Realtime subscription for Unity + /// + public class RealtimeSubscription + { + public string[] Channels { get; internal set; } + public Action>> OnMessage { get; internal set; } + internal Action OnClose { get; set; } + + /// + /// Close this subscription + /// + public void Close() + { + OnClose?.Invoke(); + } + } + + /// + /// Realtime connection interface for Unity WebSocket communication + /// + public class Realtime : MonoBehaviour + { + private Client _client; + private WebSocket _webSocket; + private readonly HashSet _channels = new(); + private readonly Dictionary _subscriptions = new(); + private int _subscriptionCounter; + private bool _reconnect = true; + private int _reconnectAttempts; + private CancellationTokenSource _cancellationTokenSource; + private bool _creatingSocket; + private string _lastUrl; + private CancellationTokenSource _heartbeatTokenSource; + private string _lastSession; + public HashSet Channels => _channels; + + public bool IsConnected => _webSocket?.State == WebSocketState.Open; + public event Action OnConnected; + public event Action OnDisconnected; + public event Action OnError; + + public void Initialize(Client client) + { + _client = client; + _lastSession = _client.GetSession(); + } + + /// + /// Update the client reference (used when client is reinitialized) + /// + public void UpdateClient(Client client) + { + _client = client; + var newSession = _client.GetSession(); + + // If session changed and we're connected, re-authenticate + if (_lastSession != newSession && IsConnected) + { + _lastSession = newSession; + SendFallbackAuthentication(); + } + } + + /// + /// Notify realtime that session has changed and re-authentication may be needed + /// + public void OnSessionChanged() + { + var newSession = _client?.GetSession(); + if (_lastSession != newSession && IsConnected) + { + _lastSession = newSession; + SendFallbackAuthentication(); + } + } + + private void Update() + { + // DispatchMessageQueue ensures that WebSocket messages are processed on the main thread. + // This is crucial for Unity API calls (e.g., modifying GameObjects, UI) from within WebSocket events. + // Note: This ties message processing to the game's frame rate and Time.timeScale. If the game is paused (Time.timeScale = 0), message processing will also pause. + #if !UNITY_WEBGL || UNITY_EDITOR + _webSocket?.DispatchMessageQueue(); + #endif + } + + public RealtimeSubscription Subscribe(string[] channels, Action>> callback) + { + Debug.Log($"[Realtime] Subscribe called for channels: [{string.Join(", ", channels)}]"); + + var subscriptionId = ++_subscriptionCounter; + var subscription = new RealtimeSubscription + { + Channels = channels, + OnMessage = callback, + OnClose = () => CloseSubscription(subscriptionId, channels) + }; + + _subscriptions[subscriptionId] = subscription; + + // Add channels to the set + foreach (var channel in channels) + { + _channels.Add(channel); + } + + // Ensure reconnect is enabled when subscribing + _reconnect = true; + CreateSocket().Forget(); + + return subscription; + } + + private void CloseSubscription(int subscriptionId, string[] channels) + { + _subscriptions.Remove(subscriptionId); + + // Remove channels that are no longer in use + foreach (var channel in channels) + { + bool stillInUse = _subscriptions.Values.Any(s => s.Channels.Contains(channel)); + if (!stillInUse) + { + _channels.Remove(channel); + } + } + + // Recreate socket with new channels or close if none + if (_channels.Count > 0) + { + CreateSocket().Forget(); + } + else + { + // No more subscriptions, close and disable reconnect + CloseConnection(allowReconnect: false).Forget(); + } + } + + private async UniTask CreateSocket() + { + if (_creatingSocket || _channels.Count == 0) return; + _creatingSocket = true; + + Debug.Log($"[Realtime] Creating socket for {_channels.Count} channels"); + + try + { + var uri = PrepareUri(); + Debug.Log($"[Realtime] Connecting to URI: {uri}"); + + if (_webSocket == null || _webSocket.State == WebSocketState.Closed) + { + _webSocket = new WebSocket(uri); + _lastUrl = uri; + SetupWebSocketEvents(); + } + else if (_lastUrl != uri && _webSocket.State != WebSocketState.Closed) + { + await CloseConnection(); + _webSocket = new WebSocket(uri); + _lastUrl = uri; + SetupWebSocketEvents(); + } + + if (_webSocket.State == WebSocketState.Connecting || _webSocket.State == WebSocketState.Open) + { + Debug.Log($"[Realtime] Socket already connecting/connected: {_webSocket.State}"); + _creatingSocket = false; + return; + } + + Debug.Log("[Realtime] Attempting to connect..."); + await _webSocket.Connect(); + Debug.Log("[Realtime] Connect call completed"); + _reconnectAttempts = 0; + } + catch (Exception ex) + { + Debug.LogError($"[Realtime] Connection failed: {ex.Message}"); + OnError?.Invoke(ex); + Retry(); + } + finally + { + _creatingSocket = false; + } + } + + private void SetupWebSocketEvents() + { + _webSocket.OnOpen += OnWebSocketOpen; + _webSocket.OnMessage += OnWebSocketMessage; + _webSocket.OnError += OnWebSocketError; + _webSocket.OnClose += OnWebSocketClose; + } + + private void OnWebSocketOpen() + { + _reconnectAttempts = 0; + OnConnected?.Invoke(); + StartHeartbeat(); + Debug.Log("[Realtime] WebSocket opened successfully."); + } + + private void OnWebSocketMessage(byte[] data) + { + try + { + var message = Encoding.UTF8.GetString(data); + var baseMessage = JsonSerializer.Deserialize(message, Client.DeserializerOptions); + + switch (baseMessage.Type) + { + case "connected": + var connectedMsg = JsonSerializer.Deserialize>(message, Client.DeserializerOptions); + HandleConnectedMessage(connectedMsg.Data); + break; + case "event": + var eventMsg = JsonSerializer.Deserialize>>>(message, Client.DeserializerOptions); + HandleRealtimeEvent(eventMsg.Data); + break; + case "error": + var errorMsg = JsonSerializer.Deserialize>(message, Client.DeserializerOptions); + HandleErrorMessage(errorMsg.Data); + break; + case "pong": + Debug.Log("[Realtime] Received pong"); + break; + default: + Debug.Log($"[Realtime] Unknown message type: {baseMessage.Type}"); + break; + } + } + catch (Exception ex) + { + Debug.LogError($"[Realtime] Message processing failed: {ex.Message}"); + OnError?.Invoke(ex); + } + } + + private void HandleConnectedMessage(RealtimeConnectedData data) + { + Debug.Log("[Realtime] Received 'connected' message"); + + if (data.User == null || data.User.Count == 0) + { + Debug.Log("[Realtime] No user found, sending fallback authentication"); + SendFallbackAuthentication(); + } + } + + private void SendFallbackAuthentication() + { + var session = _client.Config.GetValueOrDefault("session"); + + if (!string.IsNullOrEmpty(session)) + { + var authMessage = new RealtimeMessage + { + Type = "authentication", + Data = new RealtimeAuthData { Session = session } + }; + + var json = JsonSerializer.Serialize(authMessage, Client.SerializerOptions); + _webSocket.SendText(json); + } + } + + private void HandleErrorMessage(RealtimeErrorData data) + { + OnError?.Invoke(new {{ spec.title | caseUcfirst }}Exception(data.Message, data.Code)); + } + + private void HandleRealtimeEvent(RealtimeResponseEvent> eventData) + { + try + { + var subscriptionsCopy = _subscriptions.Values.ToArray(); + foreach (var subscription in subscriptionsCopy) + { + if (subscription.Channels.Any(subChannel => eventData.Channels.Contains(subChannel))) + { + subscription.OnMessage?.Invoke(eventData); + } + } + } + catch (Exception ex) + { + Debug.LogError($"[Realtime] HandleRealtimeEvent error: {ex.Message}"); + OnError?.Invoke(ex); + } + } + + private void OnWebSocketError(string error) + { + Debug.LogError($"[Realtime] WebSocket error: {error}"); + OnError?.Invoke(new {{ spec.title | caseUcfirst }}Exception($"WebSocket error: {error}")); + Retry(); + + } + + private void OnWebSocketClose(WebSocketCloseCode closeCode) + { + Debug.Log($"[Realtime] WebSocket closed with code: {closeCode}"); + StopHeartbeat(); + OnDisconnected?.Invoke(); + if (_reconnect && closeCode != WebSocketCloseCode.PolicyViolation) + { + Retry(); + } + } + + private void StartHeartbeat() + { + StopHeartbeat(); + _heartbeatTokenSource = new CancellationTokenSource(); + + UniTask.Create(async () => + { + try + { + while (!_heartbeatTokenSource.Token.IsCancellationRequested && _webSocket?.State == WebSocketState.Open) + { + await UniTask.Delay(TimeSpan.FromSeconds(20), cancellationToken: _heartbeatTokenSource.Token); + + if (_webSocket?.State == WebSocketState.Open && !_heartbeatTokenSource.Token.IsCancellationRequested) + { + var pingMessage = new { type = "ping" }; + var json = JsonSerializer.Serialize(pingMessage, Client.SerializerOptions); + await _webSocket.SendText(json); + } + } + } + catch (OperationCanceledException) + { + // Expected when cancellation is requested + } + catch (Exception ex) + { + OnError?.Invoke(ex); + } + }); + } + + private void StopHeartbeat() + { + _heartbeatTokenSource?.Cancel(); + _heartbeatTokenSource?.Dispose(); + _heartbeatTokenSource = null; + } + + private void Retry() + { + if (!_reconnect) return; + + _reconnectAttempts++; + var timeout = GetTimeout(); + + Debug.Log($"Reconnecting in {timeout} seconds."); + + UniTask.Create(async () => + { + await UniTask.Delay(TimeSpan.FromSeconds(timeout)); + await CreateSocket(); + }); + } + + private int GetTimeout() + { + return _reconnectAttempts < 5 ? 1 : + _reconnectAttempts < 15 ? 5 : + _reconnectAttempts < 100 ? 10 : 60; + } + + private string PrepareUri() + { + var realtimeEndpoint = _client.Config.GetValueOrDefault("endpointRealtime"); + if (string.IsNullOrEmpty(realtimeEndpoint)) + { + throw new {{ spec.title | caseUcfirst }}Exception("Please set endPointRealtime to connect to the realtime server."); + } + + var project = _client.Config.GetValueOrDefault("project", ""); + if (string.IsNullOrEmpty(project)) + { + throw new {{ spec.title | caseUcfirst }}Exception("Project ID is required to connect to the realtime server."); + } + + var channelParams = string.Join("&", _channels.Select(c => $"channels[]={Uri.EscapeDataString(c)}")); + + var uri = new Uri(realtimeEndpoint); + + var realtimePath = uri.AbsolutePath.TrimEnd('/') + "/realtime"; + + var baseUrl = $"{uri.Scheme}://{uri.Host}"; + if ((uri.Scheme == "wss" && uri.Port != 443) || (uri.Scheme == "ws" && uri.Port != 80)) + { + baseUrl += $":{uri.Port}"; + } + + return $"{baseUrl}{realtimePath}?project={Uri.EscapeDataString(project)}&{channelParams}"; + } + + private async UniTask CloseConnection(bool allowReconnect = true) + { + var previousReconnect = _reconnect; + _reconnect = false; + StopHeartbeat(); + _cancellationTokenSource?.Cancel(); + + if (_webSocket != null) + { + await _webSocket.Close(); + } + + _reconnectAttempts = 0; + + // Restore reconnect flag if we want to allow future reconnects + if (allowReconnect) + { + _reconnect = previousReconnect; + } + } + + public async UniTask Disconnect() + { + // Disconnect permanently - don't allow auto-reconnect + await CloseConnection(allowReconnect: false); + } + + /// + /// Reconnect after a manual disconnect + /// + public void EnableReconnect() + { + _reconnect = true; + } + + private void OnDestroy() + { + Disconnect().Forget(); + } + } +} +#endif \ No newline at end of file diff --git a/templates/unity/Assets/Runtime/Utilities/AppwriteUtilities.cs.twig b/templates/unity/Assets/Runtime/Utilities/AppwriteUtilities.cs.twig new file mode 100644 index 0000000000..ac0d5b43eb --- /dev/null +++ b/templates/unity/Assets/Runtime/Utilities/AppwriteUtilities.cs.twig @@ -0,0 +1,80 @@ +#if UNI_TASK +using System; +using UnityEngine; +using Cysharp.Threading.Tasks; + +namespace {{ spec.title | caseUcfirst }}.Utilities +{ + /// + /// Utility class for {{ spec.title | caseUcfirst }} Unity integration + /// + public static class {{ spec.title | caseUcfirst }}Utilities + { + #if UNITY_EDITOR + /// + /// Quick setup for {{ spec.title | caseUcfirst }} in Unity (Editor Only) + /// + public static async UniTask<{{ spec.title | caseUcfirst }}Manager> QuickSetup() + { + // Create configuration + var config = {{ spec.title | caseUcfirst }}Config.CreateConfiguration(); + + + // Create manager + var managerGO = new GameObject("{{ spec.title | caseUcfirst }}Manager"); + var manager = managerGO.AddComponent<{{ spec.title | caseUcfirst }}Manager>(); + manager.SetConfig(config); + + // Initialize + var success = await manager.Initialize(true); + if (!success) + { + UnityEngine.Object.Destroy(managerGO); + throw new InvalidOperationException("Failed to initialize {{ spec.title | caseUcfirst }}Manager"); + } + //Create Realtime instance + var a =manager.Realtime; + return manager; + } + #endif + + /// + /// Run async operation with Unity-safe error handling + /// + public static async UniTask SafeExecute( + Func> operation, + T defaultValue = default, + bool logErrors = true) + { + try + { + return await operation(); + } + catch (Exception ex) + { + if (logErrors) + Debug.LogError($"{{ spec.title | caseUcfirst }} operation failed: {ex.Message}"); + return defaultValue; + } + } + + /// + /// Run async operation with Unity-safe error handling (no return value) + /// + public static async UniTask SafeExecute( + Func operation, + bool logErrors = true) + { + try + { + await operation(); + } + catch (Exception ex) + { + if (logErrors) + Debug.LogError($"{{ spec.title | caseUcfirst }} operation failed: {ex.Message}"); + } + } + } +} +#endif diff --git a/templates/unity/Assets/Samples~/AppwriteExample/AppwriteExample.cs.twig b/templates/unity/Assets/Samples~/AppwriteExample/AppwriteExample.cs.twig new file mode 100644 index 0000000000..2d976dbc90 --- /dev/null +++ b/templates/unity/Assets/Samples~/AppwriteExample/AppwriteExample.cs.twig @@ -0,0 +1,128 @@ +using {{ spec.title | caseUcfirst }}; +using Cysharp.Threading.Tasks; +using UnityEngine; + +namespace Samples.{{ spec.title | caseUcfirst }}Example +{ + /// + /// Example of how to use {{ spec.title | caseUcfirst }} with Unity integration + /// + public class {{ spec.title | caseUcfirst }}Example : MonoBehaviour + { + [Header("Configuration")] + [SerializeField] private {{ spec.title | caseUcfirst }}Config config; + + private {{ spec.title | caseUcfirst }}Manager _manager; + + private async void Start() + + { + // Method 1: Using {{ spec.title | caseUcfirst }}Manager (Recommended) + await ExampleWithManager(); + + // Method 2: Using Client directly + await ExampleWithDirectClient(); + } + + /// + /// Example using {{ spec.title | caseUcfirst }}Manager for easy setup + /// + private async UniTask ExampleWithManager() + { + Debug.Log("=== Example with {{ spec.title | caseUcfirst }}Manager ==="); + + // Get or create manager + _manager = {{ spec.title | caseUcfirst }}Manager.Instance; + if (_manager == null) + { + var managerGo = new GameObject("{{ spec.title | caseUcfirst }}Manager"); + _manager = managerGo.AddComponent<{{ spec.title | caseUcfirst }}Manager>(); + _manager.SetConfig(config); + } + + // Initialize + var success = await _manager.Initialize(true); + if (!success) + { + Debug.LogError("Failed to initialize {{ spec.title | caseUcfirst }}Manager"); + return; + } + + // Use services through manager + try + { + // Direct client access + var client = _manager.Client; + var pingResult = await client.Ping(); + Debug.Log($"Ping result: {pingResult}"); + + // Service creation through DI container + // var account = _manager.GetService(); + // var databases = _manager.GetService(); + + // Realtime example + var realtime = _manager.Realtime; + var subscription = realtime.Subscribe( + new[] { "databases.*.collections.*.documents" }, + response => + { + Debug.Log($"Realtime event: {response.Events[0]}"); + } + ); + + Debug.Log("{{ spec.title | caseUcfirst }}Manager example completed successfully"); + } + catch (System.Exception ex) + { + Debug.LogError($"{{ spec.title | caseUcfirst }}Manager example failed: {ex.Message}"); + } + } + + /// + /// Example using Client directly + /// + private async UniTask ExampleWithDirectClient() + { + Debug.Log("=== Example with Direct Client ==="); + + try + { + // Create and configure client + var client = new Client() + .SetEndpoint(config.Endpoint) + .SetProject(config.ProjectId); + + if (!string.IsNullOrEmpty(config.DevKey)) + client.SetDevKey(config.DevKey); + + if (!string.IsNullOrEmpty(config.RealtimeEndpoint)) + client.SetEndPointRealtime(config.RealtimeEndpoint); + + // Test connection + var pingResult = await client.Ping(); + Debug.Log($"Direct client ping: {pingResult}"); + + // Create services manually + // var account = new Account(client); + // var databases = new Databases(client); + + // Realtime example + // You need to create a Realtime instance manually or attach dependently + // realtime.Initialize(client); + // var subscription = realtime.Subscribe( + // new[] { "databases.*.collections.*.documents" }, + // response => + // { + // Debug.Log($"Realtime event: {response.Events[0]}"); + // } + // ); + + Debug.Log("Direct client example completed successfully"); + } + catch (System.Exception ex) + { + Debug.LogError($"Direct client example failed: {ex.Message}"); + } + } + } +} diff --git a/templates/unity/CHANGELOG.md.twig b/templates/unity/CHANGELOG.md.twig new file mode 100644 index 0000000000..dfcefd0336 --- /dev/null +++ b/templates/unity/CHANGELOG.md.twig @@ -0,0 +1 @@ +{{sdk.changelog | raw}} \ No newline at end of file diff --git a/templates/unity/LICENSE.twig b/templates/unity/LICENSE.twig new file mode 100644 index 0000000000..21f5bc7f0a --- /dev/null +++ b/templates/unity/LICENSE.twig @@ -0,0 +1 @@ +{{sdk.license}} diff --git a/templates/unity/Packages/manifest.json b/templates/unity/Packages/manifest.json new file mode 100644 index 0000000000..98ba8ea842 --- /dev/null +++ b/templates/unity/Packages/manifest.json @@ -0,0 +1,47 @@ +{ + "dependencies": { + "com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask", + "com.endel.nativewebsocket": "https://github.com/endel/NativeWebSocket.git#upm", + "com.unity.collab-proxy": "2.1.0", + "com.unity.feature.2d": "2.0.0", + "com.unity.ide.rider": "3.0.25", + "com.unity.ide.visualstudio": "2.0.21", + "com.unity.ide.vscode": "1.2.5", + "com.unity.test-framework": "1.1.33", + "com.unity.textmeshpro": "3.0.6", + "com.unity.timeline": "1.6.5", + "com.unity.ugui": "1.0.0", + "com.unity.visualscripting": "1.9.1", + "com.unity.modules.ai": "1.0.0", + "com.unity.modules.androidjni": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.cloth": "1.0.0", + "com.unity.modules.director": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.particlesystem": "1.0.0", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.physics2d": "1.0.0", + "com.unity.modules.screencapture": "1.0.0", + "com.unity.modules.terrain": "1.0.0", + "com.unity.modules.terrainphysics": "1.0.0", + "com.unity.modules.tilemap": "1.0.0", + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.uielements": "1.0.0", + "com.unity.modules.umbra": "1.0.0", + "com.unity.modules.unityanalytics": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.unitywebrequestassetbundle": "1.0.0", + "com.unity.modules.unitywebrequestaudio": "1.0.0", + "com.unity.modules.unitywebrequesttexture": "1.0.0", + "com.unity.modules.unitywebrequestwww": "1.0.0", + "com.unity.modules.vehicles": "1.0.0", + "com.unity.modules.video": "1.0.0", + "com.unity.modules.vr": "1.0.0", + "com.unity.modules.wind": "1.0.0", + "com.unity.modules.xr": "1.0.0" + } +} diff --git a/templates/unity/Packages/packages-lock.json b/templates/unity/Packages/packages-lock.json new file mode 100644 index 0000000000..552d14b8c5 --- /dev/null +++ b/templates/unity/Packages/packages-lock.json @@ -0,0 +1,490 @@ +{ + "dependencies": { + "com.cysharp.unitask": { + "version": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask", + "depth": 0, + "source": "git", + "dependencies": {}, + "hash": "f213ff497e4ff462a77319cf677cf20cc0860ca9" + }, + "com.endel.nativewebsocket": { + "version": "https://github.com/endel/NativeWebSocket.git#upm", + "depth": 0, + "source": "git", + "dependencies": {}, + "hash": "1d8b49b3fee41c09a98141f1f1a5e4db47e14229" + }, + "com.unity.2d.animation": { + "version": "7.0.11", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.2d.common": "6.0.6", + "com.unity.2d.sprite": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.uielements": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.2d.aseprite": { + "version": "1.0.1", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.2d.common": "6.0.6", + "com.unity.2d.sprite": "1.0.0", + "com.unity.mathematics": "1.2.6", + "com.unity.modules.animation": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.2d.common": { + "version": "6.0.6", + "depth": 2, + "source": "registry", + "dependencies": { + "com.unity.burst": "1.5.1", + "com.unity.2d.sprite": "1.0.0", + "com.unity.mathematics": "1.1.0", + "com.unity.modules.uielements": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.2d.path": { + "version": "5.0.2", + "depth": 2, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.2d.pixel-perfect": { + "version": "5.0.3", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.2d.psdimporter": { + "version": "6.0.7", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.2d.common": "6.0.6", + "com.unity.2d.sprite": "1.0.0", + "com.unity.2d.animation": "7.0.9" + }, + "url": "https://packages.unity.com" + }, + "com.unity.2d.sprite": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": {} + }, + "com.unity.2d.spriteshape": { + "version": "7.0.7", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.2d.path": "5.0.2", + "com.unity.2d.common": "6.0.6", + "com.unity.mathematics": "1.1.0", + "com.unity.modules.physics2d": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.2d.tilemap": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": {} + }, + "com.unity.2d.tilemap.extras": { + "version": "2.2.6", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.ugui": "1.0.0", + "com.unity.2d.tilemap": "1.0.0", + "com.unity.modules.tilemap": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.burst": { + "version": "1.6.6", + "depth": 3, + "source": "registry", + "dependencies": { + "com.unity.mathematics": "1.2.1" + }, + "url": "https://packages.unity.com" + }, + "com.unity.collab-proxy": { + "version": "2.1.0", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.ext.nunit": { + "version": "1.0.6", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.feature.2d": { + "version": "2.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.2d.animation": "7.0.11", + "com.unity.2d.pixel-perfect": "5.0.3", + "com.unity.2d.psdimporter": "6.0.7", + "com.unity.2d.sprite": "1.0.0", + "com.unity.2d.spriteshape": "7.0.7", + "com.unity.2d.tilemap": "1.0.0", + "com.unity.2d.tilemap.extras": "2.2.6", + "com.unity.2d.aseprite": "1.0.1" + } + }, + "com.unity.ide.rider": { + "version": "3.0.25", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ext.nunit": "1.0.6" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ide.visualstudio": { + "version": "2.0.21", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.test-framework": "1.1.9" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ide.vscode": { + "version": "1.2.5", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.mathematics": { + "version": "1.2.6", + "depth": 2, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.test-framework": { + "version": "1.1.33", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ext.nunit": "1.0.6", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.textmeshpro": { + "version": "3.0.6", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ugui": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.timeline": { + "version": "1.6.5", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.director": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.particlesystem": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ugui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0" + } + }, + "com.unity.visualscripting": { + "version": "1.9.1", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ugui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.modules.ai": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.androidjni": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.animation": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.assetbundle": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.audio": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.cloth": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0" + } + }, + "com.unity.modules.director": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.animation": "1.0.0" + } + }, + "com.unity.modules.imageconversion": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.imgui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.jsonserialize": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.particlesystem": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.physics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.physics2d": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.screencapture": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.subsystems": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.terrain": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.terrainphysics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.terrain": "1.0.0" + } + }, + "com.unity.modules.tilemap": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics2d": "1.0.0" + } + }, + "com.unity.modules.ui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.uielements": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.uielementsnative": "1.0.0" + } + }, + "com.unity.modules.uielementsnative": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.umbra": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.unityanalytics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.unitywebrequest": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.unitywebrequestassetbundle": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0" + } + }, + "com.unity.modules.unitywebrequestaudio": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.audio": "1.0.0" + } + }, + "com.unity.modules.unitywebrequesttexture": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.unitywebrequestwww": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.unitywebrequestassetbundle": "1.0.0", + "com.unity.modules.unitywebrequestaudio": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.vehicles": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0" + } + }, + "com.unity.modules.video": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0" + } + }, + "com.unity.modules.vr": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.xr": "1.0.0" + } + }, + "com.unity.modules.wind": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.xr": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.subsystems": "1.0.0" + } + } + } +} diff --git a/templates/unity/ProjectSettings/AudioManager.asset b/templates/unity/ProjectSettings/AudioManager.asset new file mode 100644 index 0000000000..27287fec5f --- /dev/null +++ b/templates/unity/ProjectSettings/AudioManager.asset @@ -0,0 +1,19 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!11 &1 +AudioManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Volume: 1 + Rolloff Scale: 1 + Doppler Factor: 1 + Default Speaker Mode: 2 + m_SampleRate: 0 + m_DSPBufferSize: 1024 + m_VirtualVoiceCount: 512 + m_RealVoiceCount: 32 + m_SpatializerPlugin: + m_AmbisonicDecoderPlugin: + m_DisableAudio: 0 + m_VirtualizeEffects: 1 + m_RequestedDSPBufferSize: 0 diff --git a/templates/unity/ProjectSettings/ClusterInputManager.asset b/templates/unity/ProjectSettings/ClusterInputManager.asset new file mode 100644 index 0000000000..e7886b266a --- /dev/null +++ b/templates/unity/ProjectSettings/ClusterInputManager.asset @@ -0,0 +1,6 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!236 &1 +ClusterInputManager: + m_ObjectHideFlags: 0 + m_Inputs: [] diff --git a/templates/unity/ProjectSettings/DynamicsManager.asset b/templates/unity/ProjectSettings/DynamicsManager.asset new file mode 100644 index 0000000000..72d14303c9 --- /dev/null +++ b/templates/unity/ProjectSettings/DynamicsManager.asset @@ -0,0 +1,37 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!55 &1 +PhysicsManager: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_Gravity: {x: 0, y: -9.81, z: 0} + m_DefaultMaterial: {fileID: 0} + m_BounceThreshold: 2 + m_DefaultMaxDepenetrationVelocity: 10 + m_SleepThreshold: 0.005 + m_DefaultContactOffset: 0.01 + m_DefaultSolverIterations: 6 + m_DefaultSolverVelocityIterations: 1 + m_QueriesHitBackfaces: 0 + m_QueriesHitTriggers: 1 + m_EnableAdaptiveForce: 0 + m_ClothInterCollisionDistance: 0.1 + m_ClothInterCollisionStiffness: 0.2 + m_ContactsGeneration: 1 + m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + m_AutoSimulation: 1 + m_AutoSyncTransforms: 0 + m_ReuseCollisionCallbacks: 1 + m_ClothInterCollisionSettingsToggle: 0 + m_ClothGravity: {x: 0, y: -9.81, z: 0} + m_ContactPairsMode: 0 + m_BroadphaseType: 0 + m_WorldBounds: + m_Center: {x: 0, y: 0, z: 0} + m_Extent: {x: 250, y: 250, z: 250} + m_WorldSubdivisions: 8 + m_FrictionType: 0 + m_EnableEnhancedDeterminism: 0 + m_EnableUnifiedHeightmaps: 1 + m_SolverType: 0 + m_DefaultMaxAngularSpeed: 50 diff --git a/templates/unity/ProjectSettings/EditorBuildSettings.asset b/templates/unity/ProjectSettings/EditorBuildSettings.asset new file mode 100644 index 0000000000..82ab0f5910 --- /dev/null +++ b/templates/unity/ProjectSettings/EditorBuildSettings.asset @@ -0,0 +1,11 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1045 &1 +EditorBuildSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Scenes: + - enabled: 1 + path: Assets/Scenes/SampleScene.unity + guid: 2cda990e2423bbf4892e6590ba056729 + m_configObjects: {} diff --git a/templates/unity/ProjectSettings/EditorSettings.asset b/templates/unity/ProjectSettings/EditorSettings.asset new file mode 100644 index 0000000000..fa3ed49435 --- /dev/null +++ b/templates/unity/ProjectSettings/EditorSettings.asset @@ -0,0 +1,40 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!159 &1 +EditorSettings: + m_ObjectHideFlags: 0 + serializedVersion: 11 + m_SerializationMode: 2 + m_LineEndingsForNewScripts: 0 + m_DefaultBehaviorMode: 1 + m_PrefabRegularEnvironment: {fileID: 0} + m_PrefabUIEnvironment: {fileID: 0} + m_SpritePackerMode: 4 + m_SpritePackerPaddingPower: 1 + m_EtcTextureCompressorBehavior: 1 + m_EtcTextureFastCompressor: 1 + m_EtcTextureNormalCompressor: 2 + m_EtcTextureBestCompressor: 4 + m_ProjectGenerationIncludedExtensions: txt;xml;fnt;cd;asmdef;asmref;rsp + m_ProjectGenerationRootNamespace: + m_EnableTextureStreamingInEditMode: 1 + m_EnableTextureStreamingInPlayMode: 1 + m_AsyncShaderCompilation: 1 + m_CachingShaderPreprocessor: 1 + m_PrefabModeAllowAutoSave: 1 + m_EnterPlayModeOptionsEnabled: 0 + m_EnterPlayModeOptions: 3 + m_GameObjectNamingDigits: 1 + m_GameObjectNamingScheme: 0 + m_AssetNamingUsesSpace: 1 + m_UseLegacyProbeSampleCount: 0 + m_SerializeInlineMappingsOnOneLine: 1 + m_DisableCookiesInLightmapper: 1 + m_AssetPipelineMode: 1 + m_CacheServerMode: 0 + m_CacheServerEndpoint: + m_CacheServerNamespacePrefix: default + m_CacheServerEnableDownload: 1 + m_CacheServerEnableUpload: 1 + m_CacheServerEnableAuth: 0 + m_CacheServerEnableTls: 0 diff --git a/templates/unity/ProjectSettings/GraphicsSettings.asset b/templates/unity/ProjectSettings/GraphicsSettings.asset new file mode 100644 index 0000000000..c165afb2af --- /dev/null +++ b/templates/unity/ProjectSettings/GraphicsSettings.asset @@ -0,0 +1,64 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!30 &1 +GraphicsSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_Deferred: + m_Mode: 1 + m_Shader: {fileID: 69, guid: 0000000000000000f000000000000000, type: 0} + m_DeferredReflections: + m_Mode: 1 + m_Shader: {fileID: 74, guid: 0000000000000000f000000000000000, type: 0} + m_ScreenSpaceShadows: + m_Mode: 1 + m_Shader: {fileID: 64, guid: 0000000000000000f000000000000000, type: 0} + m_LegacyDeferred: + m_Mode: 1 + m_Shader: {fileID: 63, guid: 0000000000000000f000000000000000, type: 0} + m_DepthNormals: + m_Mode: 1 + m_Shader: {fileID: 62, guid: 0000000000000000f000000000000000, type: 0} + m_MotionVectors: + m_Mode: 1 + m_Shader: {fileID: 75, guid: 0000000000000000f000000000000000, type: 0} + m_LightHalo: + m_Mode: 1 + m_Shader: {fileID: 105, guid: 0000000000000000f000000000000000, type: 0} + m_LensFlare: + m_Mode: 1 + m_Shader: {fileID: 102, guid: 0000000000000000f000000000000000, type: 0} + m_VideoShadersIncludeMode: 2 + m_AlwaysIncludedShaders: + - {fileID: 7, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 15104, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 15105, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 15106, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 10753, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 10770, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 10783, guid: 0000000000000000f000000000000000, type: 0} + m_PreloadedShaders: [] + m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0} + m_CustomRenderPipeline: {fileID: 0} + m_TransparencySortMode: 0 + m_TransparencySortAxis: {x: 0, y: 0, z: 1} + m_DefaultRenderingPath: 1 + m_DefaultMobileRenderingPath: 1 + m_TierSettings: [] + m_LightmapStripping: 0 + m_FogStripping: 0 + m_InstancingStripping: 0 + m_LightmapKeepPlain: 1 + m_LightmapKeepDirCombined: 1 + m_LightmapKeepDynamicPlain: 1 + m_LightmapKeepDynamicDirCombined: 1 + m_LightmapKeepShadowMask: 1 + m_LightmapKeepSubtractive: 1 + m_FogKeepLinear: 1 + m_FogKeepExp: 1 + m_FogKeepExp2: 1 + m_AlbedoSwatchInfos: [] + m_LightsUseLinearIntensity: 0 + m_LightsUseColorTemperature: 0 + m_DefaultRenderingLayerMask: 1 + m_LogWhenShaderIsCompiled: 0 diff --git a/templates/unity/ProjectSettings/InputManager.asset b/templates/unity/ProjectSettings/InputManager.asset new file mode 100644 index 0000000000..b16147e954 --- /dev/null +++ b/templates/unity/ProjectSettings/InputManager.asset @@ -0,0 +1,487 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!13 &1 +InputManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Axes: + - serializedVersion: 3 + m_Name: Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: left + positiveButton: right + altNegativeButton: a + altPositiveButton: d + gravity: 3 + dead: 0.001 + sensitivity: 3 + snap: 1 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: down + positiveButton: up + altNegativeButton: s + altPositiveButton: w + gravity: 3 + dead: 0.001 + sensitivity: 3 + snap: 1 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left ctrl + altNegativeButton: + altPositiveButton: mouse 0 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left alt + altNegativeButton: + altPositiveButton: mouse 1 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire3 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left shift + altNegativeButton: + altPositiveButton: mouse 2 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Jump + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: space + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse X + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse Y + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 1 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse ScrollWheel + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 2 + joyNum: 0 + - serializedVersion: 3 + m_Name: Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0.19 + sensitivity: 1 + snap: 0 + invert: 0 + type: 2 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0.19 + sensitivity: 1 + snap: 0 + invert: 1 + type: 2 + axis: 1 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 0 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 1 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire3 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 2 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Jump + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 3 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Submit + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: return + altNegativeButton: + altPositiveButton: joystick button 0 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Submit + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: enter + altNegativeButton: + altPositiveButton: space + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Cancel + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: escape + altNegativeButton: + altPositiveButton: joystick button 1 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Enable Debug Button 1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left ctrl + altNegativeButton: + altPositiveButton: joystick button 8 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Enable Debug Button 2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: backspace + altNegativeButton: + altPositiveButton: joystick button 9 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Reset + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left alt + altNegativeButton: + altPositiveButton: joystick button 1 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Next + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: page down + altNegativeButton: + altPositiveButton: joystick button 5 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Previous + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: page up + altNegativeButton: + altPositiveButton: joystick button 4 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Validate + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: return + altNegativeButton: + altPositiveButton: joystick button 0 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Persistent + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: right shift + altNegativeButton: + altPositiveButton: joystick button 2 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Multiplier + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left shift + altNegativeButton: + altPositiveButton: joystick button 3 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: left + positiveButton: right + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: down + positiveButton: up + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: down + positiveButton: up + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 2 + axis: 6 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: left + positiveButton: right + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 2 + axis: 5 + joyNum: 0 diff --git a/templates/unity/ProjectSettings/MemorySettings.asset b/templates/unity/ProjectSettings/MemorySettings.asset new file mode 100644 index 0000000000..5b5facecac --- /dev/null +++ b/templates/unity/ProjectSettings/MemorySettings.asset @@ -0,0 +1,35 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!387306366 &1 +MemorySettings: + m_ObjectHideFlags: 0 + m_EditorMemorySettings: + m_MainAllocatorBlockSize: -1 + m_ThreadAllocatorBlockSize: -1 + m_MainGfxBlockSize: -1 + m_ThreadGfxBlockSize: -1 + m_CacheBlockSize: -1 + m_TypetreeBlockSize: -1 + m_ProfilerBlockSize: -1 + m_ProfilerEditorBlockSize: -1 + m_BucketAllocatorGranularity: -1 + m_BucketAllocatorBucketsCount: -1 + m_BucketAllocatorBlockSize: -1 + m_BucketAllocatorBlockCount: -1 + m_ProfilerBucketAllocatorGranularity: -1 + m_ProfilerBucketAllocatorBucketsCount: -1 + m_ProfilerBucketAllocatorBlockSize: -1 + m_ProfilerBucketAllocatorBlockCount: -1 + m_TempAllocatorSizeMain: -1 + m_JobTempAllocatorBlockSize: -1 + m_BackgroundJobTempAllocatorBlockSize: -1 + m_JobTempAllocatorReducedBlockSize: -1 + m_TempAllocatorSizeGIBakingWorker: -1 + m_TempAllocatorSizeNavMeshWorker: -1 + m_TempAllocatorSizeAudioWorker: -1 + m_TempAllocatorSizeCloudWorker: -1 + m_TempAllocatorSizeGfx: -1 + m_TempAllocatorSizeJobWorker: -1 + m_TempAllocatorSizeBackgroundWorker: -1 + m_TempAllocatorSizePreloadManager: -1 + m_PlatformMemorySettings: {} diff --git a/templates/unity/ProjectSettings/NavMeshAreas.asset b/templates/unity/ProjectSettings/NavMeshAreas.asset new file mode 100644 index 0000000000..ad2654e02e --- /dev/null +++ b/templates/unity/ProjectSettings/NavMeshAreas.asset @@ -0,0 +1,93 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!126 &1 +NavMeshProjectSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + areas: + - name: Walkable + cost: 1 + - name: Not Walkable + cost: 1 + - name: Jump + cost: 2 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + m_LastAgentTypeID: -887442657 + m_Settings: + - serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.75 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_SettingNames: + - Humanoid diff --git a/templates/unity/ProjectSettings/NetworkManager.asset b/templates/unity/ProjectSettings/NetworkManager.asset new file mode 100644 index 0000000000..5dc6a831d9 --- /dev/null +++ b/templates/unity/ProjectSettings/NetworkManager.asset @@ -0,0 +1,8 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!149 &1 +NetworkManager: + m_ObjectHideFlags: 0 + m_DebugLevel: 0 + m_Sendrate: 15 + m_AssetToPrefab: {} diff --git a/templates/unity/ProjectSettings/PackageManagerSettings.asset b/templates/unity/ProjectSettings/PackageManagerSettings.asset new file mode 100644 index 0000000000..b3a65dda68 --- /dev/null +++ b/templates/unity/ProjectSettings/PackageManagerSettings.asset @@ -0,0 +1,44 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &1 +MonoBehaviour: + m_ObjectHideFlags: 61 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 13964, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_EnablePreReleasePackages: 0 + m_EnablePackageDependencies: 0 + m_AdvancedSettingsExpanded: 1 + m_ScopedRegistriesSettingsExpanded: 1 + m_SeeAllPackageVersions: 0 + oneTimeWarningShown: 0 + m_Registries: + - m_Id: main + m_Name: + m_Url: https://packages.unity.com + m_Scopes: [] + m_IsDefault: 1 + m_Capabilities: 7 + m_UserSelectedRegistryName: + m_UserAddingNewScopedRegistry: 0 + m_RegistryInfoDraft: + m_ErrorMessage: + m_Original: + m_Id: + m_Name: + m_Url: + m_Scopes: [] + m_IsDefault: 0 + m_Capabilities: 0 + m_Modified: 0 + m_Name: + m_Url: + m_Scopes: + - + m_SelectedScopeIndex: 0 diff --git a/templates/unity/ProjectSettings/Physics2DSettings.asset b/templates/unity/ProjectSettings/Physics2DSettings.asset new file mode 100644 index 0000000000..6cfcddaacd --- /dev/null +++ b/templates/unity/ProjectSettings/Physics2DSettings.asset @@ -0,0 +1,56 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!19 &1 +Physics2DSettings: + m_ObjectHideFlags: 0 + serializedVersion: 5 + m_Gravity: {x: 0, y: -9.81} + m_DefaultMaterial: {fileID: 0} + m_VelocityIterations: 8 + m_PositionIterations: 3 + m_VelocityThreshold: 1 + m_MaxLinearCorrection: 0.2 + m_MaxAngularCorrection: 8 + m_MaxTranslationSpeed: 100 + m_MaxRotationSpeed: 360 + m_BaumgarteScale: 0.2 + m_BaumgarteTimeOfImpactScale: 0.75 + m_TimeToSleep: 0.5 + m_LinearSleepTolerance: 0.01 + m_AngularSleepTolerance: 2 + m_DefaultContactOffset: 0.01 + m_JobOptions: + serializedVersion: 2 + useMultithreading: 0 + useConsistencySorting: 0 + m_InterpolationPosesPerJob: 100 + m_NewContactsPerJob: 30 + m_CollideContactsPerJob: 100 + m_ClearFlagsPerJob: 200 + m_ClearBodyForcesPerJob: 200 + m_SyncDiscreteFixturesPerJob: 50 + m_SyncContinuousFixturesPerJob: 50 + m_FindNearestContactsPerJob: 100 + m_UpdateTriggerContactsPerJob: 100 + m_IslandSolverCostThreshold: 100 + m_IslandSolverBodyCostScale: 1 + m_IslandSolverContactCostScale: 10 + m_IslandSolverJointCostScale: 10 + m_IslandSolverBodiesPerJob: 50 + m_IslandSolverContactsPerJob: 50 + m_SimulationMode: 0 + m_QueriesHitTriggers: 1 + m_QueriesStartInColliders: 1 + m_CallbacksOnDisable: 1 + m_ReuseCollisionCallbacks: 1 + m_AutoSyncTransforms: 0 + m_AlwaysShowColliders: 0 + m_ShowColliderSleep: 1 + m_ShowColliderContacts: 0 + m_ShowColliderAABB: 0 + m_ContactArrowScale: 0.2 + m_ColliderAwakeColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.7529412} + m_ColliderAsleepColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.36078432} + m_ColliderContactColor: {r: 1, g: 0, b: 1, a: 0.6862745} + m_ColliderAABBColor: {r: 1, g: 1, b: 0, a: 0.2509804} + m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff diff --git a/templates/unity/ProjectSettings/PresetManager.asset b/templates/unity/ProjectSettings/PresetManager.asset new file mode 100644 index 0000000000..67a94daefe --- /dev/null +++ b/templates/unity/ProjectSettings/PresetManager.asset @@ -0,0 +1,7 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1386491679 &1 +PresetManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_DefaultPresets: {} diff --git a/templates/unity/ProjectSettings/ProjectSettings.asset b/templates/unity/ProjectSettings/ProjectSettings.asset new file mode 100644 index 0000000000..d367bab888 --- /dev/null +++ b/templates/unity/ProjectSettings/ProjectSettings.asset @@ -0,0 +1,782 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!129 &1 +PlayerSettings: + m_ObjectHideFlags: 0 + serializedVersion: 24 + productGUID: 4ab987bef3577704db7ede380ba94997 + AndroidProfiler: 0 + AndroidFilterTouchesWhenObscured: 0 + AndroidEnableSustainedPerformanceMode: 0 + defaultScreenOrientation: 4 + targetDevice: 2 + useOnDemandResources: 0 + accelerometerFrequency: 60 + companyName: DefaultCompany + productName: AppwriteTemplateSDK + defaultCursor: {fileID: 0} + cursorHotspot: {x: 0, y: 0} + m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1} + m_ShowUnitySplashScreen: 1 + m_ShowUnitySplashLogo: 1 + m_SplashScreenOverlayOpacity: 1 + m_SplashScreenAnimation: 1 + m_SplashScreenLogoStyle: 1 + m_SplashScreenDrawMode: 0 + m_SplashScreenBackgroundAnimationZoom: 1 + m_SplashScreenLogoAnimationZoom: 1 + m_SplashScreenBackgroundLandscapeAspect: 1 + m_SplashScreenBackgroundPortraitAspect: 1 + m_SplashScreenBackgroundLandscapeUvs: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + m_SplashScreenBackgroundPortraitUvs: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + m_SplashScreenLogos: [] + m_VirtualRealitySplashScreen: {fileID: 0} + m_HolographicTrackingLossScreen: {fileID: 0} + defaultScreenWidth: 1920 + defaultScreenHeight: 1080 + defaultScreenWidthWeb: 960 + defaultScreenHeightWeb: 600 + m_StereoRenderingPath: 0 + m_ActiveColorSpace: 0 + m_MTRendering: 1 + mipStripping: 0 + numberOfMipsStripped: 0 + m_StackTraceTypes: 010000000100000001000000010000000100000001000000 + iosShowActivityIndicatorOnLoading: -1 + androidShowActivityIndicatorOnLoading: -1 + iosUseCustomAppBackgroundBehavior: 0 + iosAllowHTTPDownload: 1 + allowedAutorotateToPortrait: 1 + allowedAutorotateToPortraitUpsideDown: 1 + allowedAutorotateToLandscapeRight: 1 + allowedAutorotateToLandscapeLeft: 1 + useOSAutorotation: 1 + use32BitDisplayBuffer: 1 + preserveFramebufferAlpha: 0 + disableDepthAndStencilBuffers: 0 + androidStartInFullscreen: 1 + androidRenderOutsideSafeArea: 1 + androidUseSwappy: 1 + androidBlitType: 0 + androidResizableWindow: 0 + androidDefaultWindowWidth: 1920 + androidDefaultWindowHeight: 1080 + androidMinimumWindowWidth: 400 + androidMinimumWindowHeight: 300 + androidFullscreenMode: 1 + defaultIsNativeResolution: 1 + macRetinaSupport: 1 + runInBackground: 0 + captureSingleScreen: 0 + muteOtherAudioSources: 0 + Prepare IOS For Recording: 0 + Force IOS Speakers When Recording: 0 + deferSystemGesturesMode: 0 + hideHomeButton: 0 + submitAnalytics: 1 + usePlayerLog: 1 + bakeCollisionMeshes: 0 + forceSingleInstance: 0 + useFlipModelSwapchain: 1 + resizableWindow: 0 + useMacAppStoreValidation: 0 + macAppStoreCategory: public.app-category.games + gpuSkinning: 0 + xboxPIXTextureCapture: 0 + xboxEnableAvatar: 0 + xboxEnableKinect: 0 + xboxEnableKinectAutoTracking: 0 + xboxEnableFitness: 0 + visibleInBackground: 1 + allowFullscreenSwitch: 1 + fullscreenMode: 1 + xboxSpeechDB: 0 + xboxEnableHeadOrientation: 0 + xboxEnableGuest: 0 + xboxEnablePIXSampling: 0 + metalFramebufferOnly: 0 + xboxOneResolution: 0 + xboxOneSResolution: 0 + xboxOneXResolution: 3 + xboxOneMonoLoggingLevel: 0 + xboxOneLoggingLevel: 1 + xboxOneDisableEsram: 0 + xboxOneEnableTypeOptimization: 0 + xboxOnePresentImmediateThreshold: 0 + switchQueueCommandMemory: 1048576 + switchQueueControlMemory: 16384 + switchQueueComputeMemory: 262144 + switchNVNShaderPoolsGranularity: 33554432 + switchNVNDefaultPoolsGranularity: 16777216 + switchNVNOtherPoolsGranularity: 16777216 + switchNVNMaxPublicTextureIDCount: 0 + switchNVNMaxPublicSamplerIDCount: 0 + switchMaxWorkerMultiple: 8 + stadiaPresentMode: 0 + stadiaTargetFramerate: 0 + vulkanNumSwapchainBuffers: 3 + vulkanEnableSetSRGBWrite: 0 + vulkanEnablePreTransform: 0 + vulkanEnableLateAcquireNextImage: 0 + vulkanEnableCommandBufferRecycling: 1 + m_SupportedAspectRatios: + 4:3: 1 + 5:4: 1 + 16:10: 1 + 16:9: 1 + Others: 1 + bundleVersion: 1.0 + preloadedAssets: [] + metroInputSource: 0 + wsaTransparentSwapchain: 0 + m_HolographicPauseOnTrackingLoss: 1 + xboxOneDisableKinectGpuReservation: 1 + xboxOneEnable7thCore: 1 + vrSettings: + enable360StereoCapture: 0 + isWsaHolographicRemotingEnabled: 0 + enableFrameTimingStats: 0 + enableOpenGLProfilerGPURecorders: 1 + useHDRDisplay: 0 + D3DHDRBitDepth: 0 + m_ColorGamuts: 00000000 + targetPixelDensity: 30 + resolutionScalingMode: 0 + resetResolutionOnWindowResize: 0 + androidSupportedAspectRatio: 1 + androidMaxAspectRatio: 2.1 + applicationIdentifier: + Standalone: com.DefaultCompany.2DProject + buildNumber: + Standalone: 0 + iPhone: 0 + tvOS: 0 + overrideDefaultApplicationIdentifier: 1 + AndroidBundleVersionCode: 1 + AndroidMinSdkVersion: 22 + AndroidTargetSdkVersion: 0 + AndroidPreferredInstallLocation: 1 + aotOptions: + stripEngineCode: 1 + iPhoneStrippingLevel: 0 + iPhoneScriptCallOptimization: 0 + ForceInternetPermission: 0 + ForceSDCardPermission: 0 + CreateWallpaper: 0 + APKExpansionFiles: 0 + keepLoadedShadersAlive: 0 + StripUnusedMeshComponents: 0 + VertexChannelCompressionMask: 4054 + iPhoneSdkVersion: 988 + iOSTargetOSVersionString: 12.0 + tvOSSdkVersion: 0 + tvOSRequireExtendedGameController: 0 + tvOSTargetOSVersionString: 12.0 + uIPrerenderedIcon: 0 + uIRequiresPersistentWiFi: 0 + uIRequiresFullScreen: 1 + uIStatusBarHidden: 1 + uIExitOnSuspend: 0 + uIStatusBarStyle: 0 + appleTVSplashScreen: {fileID: 0} + appleTVSplashScreen2x: {fileID: 0} + tvOSSmallIconLayers: [] + tvOSSmallIconLayers2x: [] + tvOSLargeIconLayers: [] + tvOSLargeIconLayers2x: [] + tvOSTopShelfImageLayers: [] + tvOSTopShelfImageLayers2x: [] + tvOSTopShelfImageWideLayers: [] + tvOSTopShelfImageWideLayers2x: [] + iOSLaunchScreenType: 0 + iOSLaunchScreenPortrait: {fileID: 0} + iOSLaunchScreenLandscape: {fileID: 0} + iOSLaunchScreenBackgroundColor: + serializedVersion: 2 + rgba: 0 + iOSLaunchScreenFillPct: 100 + iOSLaunchScreenSize: 100 + iOSLaunchScreenCustomXibPath: + iOSLaunchScreeniPadType: 0 + iOSLaunchScreeniPadImage: {fileID: 0} + iOSLaunchScreeniPadBackgroundColor: + serializedVersion: 2 + rgba: 0 + iOSLaunchScreeniPadFillPct: 100 + iOSLaunchScreeniPadSize: 100 + iOSLaunchScreeniPadCustomXibPath: + iOSLaunchScreenCustomStoryboardPath: + iOSLaunchScreeniPadCustomStoryboardPath: + iOSDeviceRequirements: [] + iOSURLSchemes: [] + macOSURLSchemes: [] + iOSBackgroundModes: 0 + iOSMetalForceHardShadows: 0 + metalEditorSupport: 1 + metalAPIValidation: 1 + iOSRenderExtraFrameOnPause: 0 + iosCopyPluginsCodeInsteadOfSymlink: 0 + appleDeveloperTeamID: + iOSManualSigningProvisioningProfileID: + tvOSManualSigningProvisioningProfileID: + iOSManualSigningProvisioningProfileType: 0 + tvOSManualSigningProvisioningProfileType: 0 + appleEnableAutomaticSigning: 0 + iOSRequireARKit: 0 + iOSAutomaticallyDetectAndAddCapabilities: 1 + appleEnableProMotion: 0 + shaderPrecisionModel: 0 + clonedFromGUID: 10ad67313f4034357812315f3c407484 + templatePackageId: com.unity.template.2d@6.1.2 + templateDefaultScene: Assets/Scenes/SampleScene.unity + useCustomMainManifest: 0 + useCustomLauncherManifest: 0 + useCustomMainGradleTemplate: 0 + useCustomLauncherGradleManifest: 0 + useCustomBaseGradleTemplate: 0 + useCustomGradlePropertiesTemplate: 0 + useCustomProguardFile: 0 + AndroidTargetArchitectures: 1 + AndroidTargetDevices: 0 + AndroidSplashScreenScale: 0 + androidSplashScreen: {fileID: 0} + AndroidKeystoreName: + AndroidKeyaliasName: + AndroidBuildApkPerCpuArchitecture: 0 + AndroidTVCompatibility: 0 + AndroidIsGame: 1 + AndroidEnableTango: 0 + androidEnableBanner: 1 + androidUseLowAccuracyLocation: 0 + androidUseCustomKeystore: 0 + m_AndroidBanners: + - width: 320 + height: 180 + banner: {fileID: 0} + androidGamepadSupportLevel: 0 + chromeosInputEmulation: 1 + AndroidMinifyWithR8: 0 + AndroidMinifyRelease: 0 + AndroidMinifyDebug: 0 + AndroidValidateAppBundleSize: 1 + AndroidAppBundleSizeToValidate: 150 + m_BuildTargetIcons: [] + m_BuildTargetPlatformIcons: + - m_BuildTarget: Android + m_Icons: + - m_Textures: [] + m_Width: 432 + m_Height: 432 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 324 + m_Height: 324 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 216 + m_Height: 216 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 162 + m_Height: 162 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 108 + m_Height: 108 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 81 + m_Height: 81 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 192 + m_Height: 192 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 144 + m_Height: 144 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 96 + m_Height: 96 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 72 + m_Height: 72 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 48 + m_Height: 48 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 36 + m_Height: 36 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 192 + m_Height: 192 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 144 + m_Height: 144 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 96 + m_Height: 96 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 72 + m_Height: 72 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 48 + m_Height: 48 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 36 + m_Height: 36 + m_Kind: 0 + m_SubKind: + m_BuildTargetBatching: [] + m_BuildTargetShaderSettings: [] + m_BuildTargetGraphicsJobs: + - m_BuildTarget: MacStandaloneSupport + m_GraphicsJobs: 0 + - m_BuildTarget: Switch + m_GraphicsJobs: 0 + - m_BuildTarget: MetroSupport + m_GraphicsJobs: 0 + - m_BuildTarget: AppleTVSupport + m_GraphicsJobs: 0 + - m_BuildTarget: BJMSupport + m_GraphicsJobs: 0 + - m_BuildTarget: LinuxStandaloneSupport + m_GraphicsJobs: 0 + - m_BuildTarget: PS4Player + m_GraphicsJobs: 0 + - m_BuildTarget: iOSSupport + m_GraphicsJobs: 0 + - m_BuildTarget: WindowsStandaloneSupport + m_GraphicsJobs: 0 + - m_BuildTarget: XboxOnePlayer + m_GraphicsJobs: 0 + - m_BuildTarget: LuminSupport + m_GraphicsJobs: 0 + - m_BuildTarget: AndroidPlayer + m_GraphicsJobs: 0 + - m_BuildTarget: WebGLSupport + m_GraphicsJobs: 0 + m_BuildTargetGraphicsJobMode: [] + m_BuildTargetGraphicsAPIs: + - m_BuildTarget: AndroidPlayer + m_APIs: 150000000b000000 + m_Automatic: 1 + - m_BuildTarget: iOSSupport + m_APIs: 10000000 + m_Automatic: 1 + m_BuildTargetVRSettings: [] + m_DefaultShaderChunkSizeInMB: 16 + m_DefaultShaderChunkCount: 0 + openGLRequireES31: 0 + openGLRequireES31AEP: 0 + openGLRequireES32: 0 + m_TemplateCustomTags: {} + mobileMTRendering: + Android: 1 + iPhone: 1 + tvOS: 1 + m_BuildTargetGroupLightmapEncodingQuality: [] + m_BuildTargetGroupLightmapSettings: [] + m_BuildTargetNormalMapEncoding: [] + m_BuildTargetDefaultTextureCompressionFormat: + - m_BuildTarget: Android + m_Format: 3 + playModeTestRunnerEnabled: 0 + runPlayModeTestAsEditModeTest: 0 + actionOnDotNetUnhandledException: 1 + enableInternalProfiler: 0 + logObjCUncaughtExceptions: 1 + enableCrashReportAPI: 0 + cameraUsageDescription: + locationUsageDescription: + microphoneUsageDescription: + bluetoothUsageDescription: + switchNMETAOverride: + switchNetLibKey: + switchSocketMemoryPoolSize: 6144 + switchSocketAllocatorPoolSize: 128 + switchSocketConcurrencyLimit: 14 + switchScreenResolutionBehavior: 2 + switchUseCPUProfiler: 0 + switchEnableFileSystemTrace: 0 + switchUseGOLDLinker: 0 + switchLTOSetting: 0 + switchApplicationID: 0x01004b9000490000 + switchNSODependencies: + switchTitleNames_0: + switchTitleNames_1: + switchTitleNames_2: + switchTitleNames_3: + switchTitleNames_4: + switchTitleNames_5: + switchTitleNames_6: + switchTitleNames_7: + switchTitleNames_8: + switchTitleNames_9: + switchTitleNames_10: + switchTitleNames_11: + switchTitleNames_12: + switchTitleNames_13: + switchTitleNames_14: + switchTitleNames_15: + switchPublisherNames_0: + switchPublisherNames_1: + switchPublisherNames_2: + switchPublisherNames_3: + switchPublisherNames_4: + switchPublisherNames_5: + switchPublisherNames_6: + switchPublisherNames_7: + switchPublisherNames_8: + switchPublisherNames_9: + switchPublisherNames_10: + switchPublisherNames_11: + switchPublisherNames_12: + switchPublisherNames_13: + switchPublisherNames_14: + switchPublisherNames_15: + switchIcons_0: {fileID: 0} + switchIcons_1: {fileID: 0} + switchIcons_2: {fileID: 0} + switchIcons_3: {fileID: 0} + switchIcons_4: {fileID: 0} + switchIcons_5: {fileID: 0} + switchIcons_6: {fileID: 0} + switchIcons_7: {fileID: 0} + switchIcons_8: {fileID: 0} + switchIcons_9: {fileID: 0} + switchIcons_10: {fileID: 0} + switchIcons_11: {fileID: 0} + switchIcons_12: {fileID: 0} + switchIcons_13: {fileID: 0} + switchIcons_14: {fileID: 0} + switchIcons_15: {fileID: 0} + switchSmallIcons_0: {fileID: 0} + switchSmallIcons_1: {fileID: 0} + switchSmallIcons_2: {fileID: 0} + switchSmallIcons_3: {fileID: 0} + switchSmallIcons_4: {fileID: 0} + switchSmallIcons_5: {fileID: 0} + switchSmallIcons_6: {fileID: 0} + switchSmallIcons_7: {fileID: 0} + switchSmallIcons_8: {fileID: 0} + switchSmallIcons_9: {fileID: 0} + switchSmallIcons_10: {fileID: 0} + switchSmallIcons_11: {fileID: 0} + switchSmallIcons_12: {fileID: 0} + switchSmallIcons_13: {fileID: 0} + switchSmallIcons_14: {fileID: 0} + switchSmallIcons_15: {fileID: 0} + switchManualHTML: + switchAccessibleURLs: + switchLegalInformation: + switchMainThreadStackSize: 1048576 + switchPresenceGroupId: + switchLogoHandling: 0 + switchReleaseVersion: 0 + switchDisplayVersion: 1.0.0 + switchStartupUserAccount: 0 + switchSupportedLanguagesMask: 0 + switchLogoType: 0 + switchApplicationErrorCodeCategory: + switchUserAccountSaveDataSize: 0 + switchUserAccountSaveDataJournalSize: 0 + switchApplicationAttribute: 0 + switchCardSpecSize: -1 + switchCardSpecClock: -1 + switchRatingsMask: 0 + switchRatingsInt_0: 0 + switchRatingsInt_1: 0 + switchRatingsInt_2: 0 + switchRatingsInt_3: 0 + switchRatingsInt_4: 0 + switchRatingsInt_5: 0 + switchRatingsInt_6: 0 + switchRatingsInt_7: 0 + switchRatingsInt_8: 0 + switchRatingsInt_9: 0 + switchRatingsInt_10: 0 + switchRatingsInt_11: 0 + switchRatingsInt_12: 0 + switchLocalCommunicationIds_0: + switchLocalCommunicationIds_1: + switchLocalCommunicationIds_2: + switchLocalCommunicationIds_3: + switchLocalCommunicationIds_4: + switchLocalCommunicationIds_5: + switchLocalCommunicationIds_6: + switchLocalCommunicationIds_7: + switchParentalControl: 0 + switchAllowsScreenshot: 1 + switchAllowsVideoCapturing: 1 + switchAllowsRuntimeAddOnContentInstall: 0 + switchDataLossConfirmation: 0 + switchUserAccountLockEnabled: 0 + switchSystemResourceMemory: 16777216 + switchSupportedNpadStyles: 22 + switchNativeFsCacheSize: 32 + switchIsHoldTypeHorizontal: 0 + switchSupportedNpadCount: 8 + switchEnableTouchScreen: 1 + switchSocketConfigEnabled: 0 + switchTcpInitialSendBufferSize: 32 + switchTcpInitialReceiveBufferSize: 64 + switchTcpAutoSendBufferSizeMax: 256 + switchTcpAutoReceiveBufferSizeMax: 256 + switchUdpSendBufferSize: 9 + switchUdpReceiveBufferSize: 42 + switchSocketBufferEfficiency: 4 + switchSocketInitializeEnabled: 1 + switchNetworkInterfaceManagerInitializeEnabled: 1 + switchPlayerConnectionEnabled: 1 + switchUseNewStyleFilepaths: 0 + switchUseLegacyFmodPriorities: 1 + switchUseMicroSleepForYield: 1 + switchEnableRamDiskSupport: 0 + switchMicroSleepForYieldTime: 25 + switchRamDiskSpaceSize: 12 + ps4NPAgeRating: 12 + ps4NPTitleSecret: + ps4NPTrophyPackPath: + ps4ParentalLevel: 11 + ps4ContentID: ED1633-NPXX51362_00-0000000000000000 + ps4Category: 0 + ps4MasterVersion: 01.00 + ps4AppVersion: 01.00 + ps4AppType: 0 + ps4ParamSfxPath: + ps4VideoOutPixelFormat: 0 + ps4VideoOutInitialWidth: 1920 + ps4VideoOutBaseModeInitialWidth: 1920 + ps4VideoOutReprojectionRate: 60 + ps4PronunciationXMLPath: + ps4PronunciationSIGPath: + ps4BackgroundImagePath: + ps4StartupImagePath: + ps4StartupImagesFolder: + ps4IconImagesFolder: + ps4SaveDataImagePath: + ps4SdkOverride: + ps4BGMPath: + ps4ShareFilePath: + ps4ShareOverlayImagePath: + ps4PrivacyGuardImagePath: + ps4ExtraSceSysFile: + ps4NPtitleDatPath: + ps4RemotePlayKeyAssignment: -1 + ps4RemotePlayKeyMappingDir: + ps4PlayTogetherPlayerCount: 0 + ps4EnterButtonAssignment: 2 + ps4ApplicationParam1: 0 + ps4ApplicationParam2: 0 + ps4ApplicationParam3: 0 + ps4ApplicationParam4: 0 + ps4DownloadDataSize: 0 + ps4GarlicHeapSize: 2048 + ps4ProGarlicHeapSize: 2560 + playerPrefsMaxSize: 32768 + ps4Passcode: bi9UOuSpM2Tlh01vOzwvSikHFswuzleh + ps4pnSessions: 1 + ps4pnPresence: 1 + ps4pnFriends: 1 + ps4pnGameCustomData: 1 + playerPrefsSupport: 0 + enableApplicationExit: 0 + resetTempFolder: 1 + restrictedAudioUsageRights: 0 + ps4UseResolutionFallback: 0 + ps4ReprojectionSupport: 0 + ps4UseAudio3dBackend: 0 + ps4UseLowGarlicFragmentationMode: 1 + ps4SocialScreenEnabled: 0 + ps4ScriptOptimizationLevel: 2 + ps4Audio3dVirtualSpeakerCount: 14 + ps4attribCpuUsage: 0 + ps4PatchPkgPath: + ps4PatchLatestPkgPath: + ps4PatchChangeinfoPath: + ps4PatchDayOne: 0 + ps4attribUserManagement: 0 + ps4attribMoveSupport: 0 + ps4attrib3DSupport: 0 + ps4attribShareSupport: 0 + ps4attribExclusiveVR: 0 + ps4disableAutoHideSplash: 0 + ps4videoRecordingFeaturesUsed: 0 + ps4contentSearchFeaturesUsed: 0 + ps4CompatibilityPS5: 0 + ps4AllowPS5Detection: 0 + ps4GPU800MHz: 1 + ps4attribEyeToEyeDistanceSettingVR: 0 + ps4IncludedModules: [] + ps4attribVROutputEnabled: 0 + monoEnv: + splashScreenBackgroundSourceLandscape: {fileID: 0} + splashScreenBackgroundSourcePortrait: {fileID: 0} + blurSplashScreenBackground: 1 + spritePackerPolicy: + webGLMemorySize: 32 + webGLExceptionSupport: 1 + webGLNameFilesAsHashes: 0 + webGLDataCaching: 1 + webGLDebugSymbols: 0 + webGLEmscriptenArgs: + webGLModulesDirectory: + webGLTemplate: APPLICATION:Default + webGLAnalyzeBuildSize: 0 + webGLUseEmbeddedResources: 0 + webGLCompressionFormat: 0 + webGLWasmArithmeticExceptions: 0 + webGLLinkerTarget: 1 + webGLThreadsSupport: 0 + webGLDecompressionFallback: 0 + webGLPowerPreference: 2 + scriptingDefineSymbols: {} + additionalCompilerArguments: {} + platformArchitecture: {} + scriptingBackend: {} + il2cppCompilerConfiguration: {} + managedStrippingLevel: + EmbeddedLinux: 1 + GameCoreScarlett: 1 + GameCoreXboxOne: 1 + Lumin: 1 + Nintendo Switch: 1 + PS4: 1 + PS5: 1 + Stadia: 1 + WebGL: 1 + Windows Store Apps: 1 + XboxOne: 1 + iPhone: 1 + tvOS: 1 + incrementalIl2cppBuild: {} + suppressCommonWarnings: 1 + allowUnsafeCode: 0 + useDeterministicCompilation: 1 + enableRoslynAnalyzers: 1 + additionalIl2CppArgs: + scriptingRuntimeVersion: 1 + gcIncremental: 1 + assemblyVersionValidation: 1 + gcWBarrierValidation: 0 + apiCompatibilityLevelPerPlatform: {} + m_RenderingPath: 1 + m_MobileRenderingPath: 1 + metroPackageName: 2D_BuiltInRenderer + metroPackageVersion: + metroCertificatePath: + metroCertificatePassword: + metroCertificateSubject: + metroCertificateIssuer: + metroCertificateNotAfter: 0000000000000000 + metroApplicationDescription: 2D_BuiltInRenderer + wsaImages: {} + metroTileShortName: + metroTileShowName: 0 + metroMediumTileShowName: 0 + metroLargeTileShowName: 0 + metroWideTileShowName: 0 + metroSupportStreamingInstall: 0 + metroLastRequiredScene: 0 + metroDefaultTileSize: 1 + metroTileForegroundText: 2 + metroTileBackgroundColor: {r: 0.13333334, g: 0.17254902, b: 0.21568628, a: 0} + metroSplashScreenBackgroundColor: {r: 0.12941177, g: 0.17254902, b: 0.21568628, a: 1} + metroSplashScreenUseBackgroundColor: 0 + platformCapabilities: {} + metroTargetDeviceFamilies: {} + metroFTAName: + metroFTAFileTypes: [] + metroProtocolName: + vcxProjDefaultLanguage: + XboxOneProductId: + XboxOneUpdateKey: + XboxOneSandboxId: + XboxOneContentId: + XboxOneTitleId: + XboxOneSCId: + XboxOneGameOsOverridePath: + XboxOnePackagingOverridePath: + XboxOneAppManifestOverridePath: + XboxOneVersion: 1.0.0.0 + XboxOnePackageEncryption: 0 + XboxOnePackageUpdateGranularity: 2 + XboxOneDescription: + XboxOneLanguage: + - enus + XboxOneCapability: [] + XboxOneGameRating: {} + XboxOneIsContentPackage: 0 + XboxOneEnhancedXboxCompatibilityMode: 0 + XboxOneEnableGPUVariability: 1 + XboxOneSockets: {} + XboxOneSplashScreen: {fileID: 0} + XboxOneAllowedProductIds: [] + XboxOnePersistentLocalStorageSize: 0 + XboxOneXTitleMemory: 8 + XboxOneOverrideIdentityName: + XboxOneOverrideIdentityPublisher: + vrEditorSettings: {} + cloudServicesEnabled: {} + luminIcon: + m_Name: + m_ModelFolderPath: + m_PortalFolderPath: + luminCert: + m_CertPath: + m_SignPackage: 1 + luminIsChannelApp: 0 + luminVersion: + m_VersionCode: 1 + m_VersionName: + apiCompatibilityLevel: 6 + activeInputHandler: 0 + windowsGamepadBackendHint: 0 + cloudProjectId: a0b72f85-7dbc-4748-aad1-c91100eebf4c + framebufferDepthMemorylessMode: 0 + qualitySettingsNames: [] + projectName: AppwriteTemplateSDK + organizationId: comanda-a + cloudEnabled: 0 + legacyClampBlendShapeWeights: 0 + playerDataPath: + forceSRGBBlit: 1 + virtualTexturingSupportEnabled: 0 diff --git a/templates/unity/ProjectSettings/ProjectVersion.txt b/templates/unity/ProjectSettings/ProjectVersion.txt new file mode 100644 index 0000000000..16ee581cfe --- /dev/null +++ b/templates/unity/ProjectSettings/ProjectVersion.txt @@ -0,0 +1,2 @@ +m_EditorVersion: 2021.3.45f1 +m_EditorVersionWithRevision: 2021.3.45f1 (3409e2af086f) diff --git a/templates/unity/ProjectSettings/QualitySettings.asset b/templates/unity/ProjectSettings/QualitySettings.asset new file mode 100644 index 0000000000..bcd6706535 --- /dev/null +++ b/templates/unity/ProjectSettings/QualitySettings.asset @@ -0,0 +1,239 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!47 &1 +QualitySettings: + m_ObjectHideFlags: 0 + serializedVersion: 5 + m_CurrentQuality: 5 + m_QualitySettings: + - serializedVersion: 2 + name: Very Low + pixelLightCount: 0 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 15 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + skinWeights: 1 + textureQuality: 1 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 0 + lodBias: 0.3 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 4 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Low + pixelLightCount: 0 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 20 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + skinWeights: 2 + textureQuality: 0 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 0 + lodBias: 0.4 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 16 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Medium + pixelLightCount: 1 + shadows: 1 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 20 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + skinWeights: 2 + textureQuality: 0 + anisotropicTextures: 1 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 1 + lodBias: 0.7 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 64 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: High + pixelLightCount: 2 + shadows: 2 + shadowResolution: 1 + shadowProjection: 1 + shadowCascades: 2 + shadowDistance: 40 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + skinWeights: 2 + textureQuality: 0 + anisotropicTextures: 1 + antiAliasing: 0 + softParticles: 0 + softVegetation: 1 + realtimeReflectionProbes: 1 + billboardsFaceCameraPosition: 1 + vSyncCount: 1 + lodBias: 1 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 256 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Very High + pixelLightCount: 3 + shadows: 2 + shadowResolution: 2 + shadowProjection: 1 + shadowCascades: 2 + shadowDistance: 70 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + skinWeights: 4 + textureQuality: 0 + anisotropicTextures: 2 + antiAliasing: 2 + softParticles: 1 + softVegetation: 1 + realtimeReflectionProbes: 1 + billboardsFaceCameraPosition: 1 + vSyncCount: 1 + lodBias: 1.5 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 1024 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Ultra + pixelLightCount: 4 + shadows: 2 + shadowResolution: 2 + shadowProjection: 1 + shadowCascades: 4 + shadowDistance: 150 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + skinWeights: 255 + textureQuality: 0 + anisotropicTextures: 2 + antiAliasing: 2 + softParticles: 1 + softVegetation: 1 + realtimeReflectionProbes: 1 + billboardsFaceCameraPosition: 1 + vSyncCount: 1 + lodBias: 2 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 4096 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + excludedTargetPlatforms: [] + m_PerPlatformDefaultQuality: + Android: 2 + Lumin: 5 + GameCoreScarlett: 5 + GameCoreXboxOne: 5 + Nintendo Switch: 5 + PS4: 5 + PS5: 5 + Stadia: 5 + Standalone: 5 + WebGL: 3 + Windows Store Apps: 5 + XboxOne: 5 + iPhone: 2 + tvOS: 2 diff --git a/templates/unity/ProjectSettings/TagManager.asset b/templates/unity/ProjectSettings/TagManager.asset new file mode 100644 index 0000000000..1c92a7840e --- /dev/null +++ b/templates/unity/ProjectSettings/TagManager.asset @@ -0,0 +1,43 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!78 &1 +TagManager: + serializedVersion: 2 + tags: [] + layers: + - Default + - TransparentFX + - Ignore Raycast + - + - Water + - UI + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + m_SortingLayers: + - name: Default + uniqueID: 0 + locked: 0 diff --git a/templates/unity/ProjectSettings/TimeManager.asset b/templates/unity/ProjectSettings/TimeManager.asset new file mode 100644 index 0000000000..558a017e1f --- /dev/null +++ b/templates/unity/ProjectSettings/TimeManager.asset @@ -0,0 +1,9 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!5 &1 +TimeManager: + m_ObjectHideFlags: 0 + Fixed Timestep: 0.02 + Maximum Allowed Timestep: 0.33333334 + m_TimeScale: 1 + Maximum Particle Timestep: 0.03 diff --git a/templates/unity/ProjectSettings/UnityConnectSettings.asset b/templates/unity/ProjectSettings/UnityConnectSettings.asset new file mode 100644 index 0000000000..a88bee0f15 --- /dev/null +++ b/templates/unity/ProjectSettings/UnityConnectSettings.asset @@ -0,0 +1,36 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!310 &1 +UnityConnectSettings: + m_ObjectHideFlags: 0 + serializedVersion: 1 + m_Enabled: 0 + m_TestMode: 0 + m_EventOldUrl: https://api.uca.cloud.unity3d.com/v1/events + m_EventUrl: https://cdp.cloud.unity3d.com/v1/events + m_ConfigUrl: https://config.uca.cloud.unity3d.com + m_DashboardUrl: https://dashboard.unity3d.com + m_TestInitMode: 0 + CrashReportingSettings: + m_EventUrl: https://perf-events.cloud.unity3d.com + m_Enabled: 0 + m_LogBufferSize: 10 + m_CaptureEditorExceptions: 1 + UnityPurchasingSettings: + m_Enabled: 0 + m_TestMode: 0 + UnityAnalyticsSettings: + m_Enabled: 0 + m_TestMode: 0 + m_InitializeOnStartup: 1 + m_PackageRequiringCoreStatsPresent: 0 + UnityAdsSettings: + m_Enabled: 0 + m_InitializeOnStartup: 1 + m_TestMode: 0 + m_IosGameId: + m_AndroidGameId: + m_GameIds: {} + m_GameId: + PerformanceReportingSettings: + m_Enabled: 0 diff --git a/templates/unity/ProjectSettings/VFXManager.asset b/templates/unity/ProjectSettings/VFXManager.asset new file mode 100644 index 0000000000..46f38e16ee --- /dev/null +++ b/templates/unity/ProjectSettings/VFXManager.asset @@ -0,0 +1,14 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!937362698 &1 +VFXManager: + m_ObjectHideFlags: 0 + m_IndirectShader: {fileID: 0} + m_CopyBufferShader: {fileID: 0} + m_SortShader: {fileID: 0} + m_StripUpdateShader: {fileID: 0} + m_RenderPipeSettingsPath: + m_FixedTimeStep: 0.016666668 + m_MaxDeltaTime: 0.05 + m_CompiledVersion: 0 + m_RuntimeVersion: 0 diff --git a/templates/unity/ProjectSettings/VersionControlSettings.asset b/templates/unity/ProjectSettings/VersionControlSettings.asset new file mode 100644 index 0000000000..dca288142f --- /dev/null +++ b/templates/unity/ProjectSettings/VersionControlSettings.asset @@ -0,0 +1,8 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!890905787 &1 +VersionControlSettings: + m_ObjectHideFlags: 0 + m_Mode: Visible Meta Files + m_CollabEditorSettings: + inProgressEnabled: 1 diff --git a/templates/unity/ProjectSettings/XRSettings.asset b/templates/unity/ProjectSettings/XRSettings.asset new file mode 100644 index 0000000000..482590c196 --- /dev/null +++ b/templates/unity/ProjectSettings/XRSettings.asset @@ -0,0 +1,10 @@ +{ + "m_SettingKeys": [ + "VR Device Disabled", + "VR Device User Alert" + ], + "m_SettingValues": [ + "False", + "False" + ] +} \ No newline at end of file diff --git a/templates/unity/ProjectSettings/boot.config b/templates/unity/ProjectSettings/boot.config new file mode 100644 index 0000000000..e69de29bb2 diff --git a/templates/unity/README.md.twig b/templates/unity/README.md.twig new file mode 100644 index 0000000000..41a3d86537 --- /dev/null +++ b/templates/unity/README.md.twig @@ -0,0 +1,175 @@ +# {{ 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) +![Unity](https://img.shields.io/badge/Unity-2021.3%2B-blue.svg) +[![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 + +### Unity Package Manager (UPM) + +1. Open Unity and go to **Window > Package Manager** +2. Click the **+** button and select **Add package from git URL** +3. Enter the following URL: +``` +https://github.com/{{ sdk.gitUserName|url_encode }}/{{ sdk.gitRepoName|url_encode }}.git?path=Assets +``` +4. Click **Add** +5. In Unity, open the **Appwrite → Setup Assistant** menu and install the required dependencies +![](./.media/setup-assistant.png) +### Manual Installation + +1. Download the latest release from [GitHub](/releases) or zip +2. Import the Unity package into your project +3. In Unity, open the **Appwrite → Setup Assistant** menu and install the required dependencies + +## Dependencies + +This SDK requires the following Unity packages and libraries: + +- [**UniTask**](https://github.com/Cysharp/UniTask): For async/await support in Unity +- [**NativeWebSocket**](https://github.com/endel/NativeWebSocket): For WebSocket real-time subscriptions +- **System.Text.Json**: For JSON serialization (provided as a DLL in the project) + +You can also install UniTask and other required dependencies automatically via **Appwrite → Setup Assistant** in Unity. + +## Quick Start +> **Before you begin** +> First, create an Appwrite configuration: +> — via the **QuickStart** window in the **Appwrite Setup Assistant** +> — or through the menu **Appwrite → Create Configuration** +![](./.media/config.png) + +### Example: Unity Integration - Using AppwriteManager + +```csharp + [SerializeField] private AppwriteConfig config; + private AppwriteManager _manager; + + private async UniTask ExampleWithManager() + { + // Get or create manager + _manager = AppwriteManager.Instance ?? new GameObject("AppwriteManager").AddComponent(); + _manager.SetConfig(config); + + // Initialize + var success = await _manager.Initialize(); + if (!success) { Debug.LogError("Failed to initialize AppwriteManager"); return; } + + // Direct client access + var client = _manager.Client; + var pingResult = await client.Ping(); + Debug.Log($"Ping result: {pingResult}"); + + // Service creation through DI container + var account = _manager.GetService(); + var databases = _manager.GetService(); + + // Realtime example + var realtime = _manager.Realtime; + var subscription = realtime.Subscribe( + new[] { "databases.*.collections.*.documents" }, + response => Debug.Log($"Realtime event: {response.Events[0]}") + ); + } +``` + +### Example: Unity Integration - Using Client directly + +```csharp + [SerializeField] private AppwriteConfig config; + + private async UniTask ExampleWithDirectClient() + { + // Create and configure client + var client = new Client() + .SetEndpoint(config.Endpoint) + .SetProject(config.ProjectId); + + if (!string.IsNullOrEmpty(config.ApiKey)) + client.SetKey(config.ApiKey); + + if (!string.IsNullOrEmpty(config.RealtimeEndpoint)) + client.SetEndPointRealtime(config.RealtimeEndpoint); + + // Test connection + var pingResult = await client.Ping(); + Debug.Log($"Direct client ping: {pingResult}"); + + // Create services manually + var account = new Account(client); + var databases = new Databases(client); + + // Realtime example + // You need to create a Realtime instance manually or attach dependently + realtime.Initialize(client); + var subscription = realtime.Subscribe( + new[] { "databases.*.collections.*.documents" }, + response => Debug.Log($"Realtime event: {response.Events[0]}") + ); + } +``` +### Error Handling +```csharp +try +{ + var result = await client..Async(); +} +catch (AppwriteException ex) +{ + Debug.LogError($"Appwrite Error: {ex.Message}"); + Debug.LogError($"Status Code: {ex.Code}"); + Debug.LogError($"Response: {ex.Response}"); +} +``` + +## Preparing Models for Databases API + +When working with the Databases API in Unity, models should be prepared for serialization using the System.Text.Json library. By default, System.Text.Json converts property names from PascalCase to camelCase when serializing to JSON. If your Appwrite collection attributes are not in camelCase, this can cause errors due to mismatches between serialized property names and actual attribute names in your collection. + +To avoid this, add the `JsonPropertyName` attribute to each property in your model class to match the attribute name in Appwrite: + +```csharp +using System.Text.Json.Serialization; + +public class TestModel +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("release_date")] + public System.DateTime ReleaseDate { get; set; } +} +``` + +The `JsonPropertyName` attribute ensures your data object is serialized with the correct attribute names for Appwrite databases. This approach works seamlessly in Unity with the included System.Text.Json DLL. + +## Contribution + +This library is auto-generated by the Appwrite [SDK Generator](https://github.com/appwrite/sdk-generator). To learn how you can help 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. + +## Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information about recent changes. + diff --git a/templates/unity/base/requests/oauth.twig b/templates/unity/base/requests/oauth.twig new file mode 100644 index 0000000000..e1cbe37788 --- /dev/null +++ b/templates/unity/base/requests/oauth.twig @@ -0,0 +1,31 @@ + var project = _client.Config.GetValueOrDefault("project"); + apiParameters["project"] = project; + + var queryString = apiParameters.ToQueryString(); + var authUrl = $"{_client.Endpoint}{apiPath}?{queryString}"; + + var callbackUri = await WebAuthComponent.Authenticate(authUrl); + + var query = HttpUtility.ParseQueryString(callbackUri.Query); + var secret = query.Get("secret"); + var key = query.Get("key"); + var callbackDomain = query.Get("domain"); // Get domain from callback + + if (string.IsNullOrEmpty(secret) || string.IsNullOrEmpty(key)) + { + var error = query.Get("error") ?? "Unknown error"; + throw new {{ spec.title | caseUcfirst }}Exception($"Failed to get authentication credentials from callback. Error: {error}"); + } + + // Use domain from callback if available, otherwise fallback to endpoint host + var domain = !string.IsNullOrEmpty(callbackDomain) ? callbackDomain : new Uri(_client.Endpoint).Host; + var parsedDomain = domain.StartsWith(".") ? domain.Substring(1) : domain; + // Create a Set-Cookie header format and parse it + // This ensures consistent cookie processing with server responses + var setCookieHeader = $"{key}={secret}; Path=/; Domain={parsedDomain}; Secure; HttpOnly; Max-Age={30 * 24 * 60 * 60}"; + Debug.Log($"Setting session cookie for domain: {parsedDomain}"); + _client.CookieContainer.ParseSetCookieHeader(setCookieHeader, parsedDomain); + +#if UNITY_EDITOR + Debug.LogWarning("[{{ spec.title | caseUcfirst }}] OAuth authorization in Editor: you can open and authorize, but cookies cannot be obtained. The session will not be set."); +#endif diff --git a/templates/unity/docs/example.md.twig b/templates/unity/docs/example.md.twig new file mode 100644 index 0000000000..f03bc0a60e --- /dev/null +++ b/templates/unity/docs/example.md.twig @@ -0,0 +1,84 @@ +# {{method.name | caseUcfirst}} + +## Example + +```csharp +using {{ spec.title | caseUcfirst }}; +{% set addedEnum = false %} +{% for parameter in method.parameters.all %} +{% if parameter.enumValues | length > 0 and not addedEnum %} +using {{ spec.title | caseUcfirst }}.Enums; +{% set addedEnum = true %} +{% endif %} +{% endfor %} +using {{ spec.title | caseUcfirst }}.Models; +using {{ spec.title | caseUcfirst }}.Services; +using Cysharp.Threading.Tasks; +using UnityEngine; + +public class {{method.name | caseUcfirst}}Example : MonoBehaviour +{ + private Client client; + private {{service.name | caseUcfirst}} {{service.name | caseCamel}}; + + async void Start() + { + client = new Client() +{% if method.auth|length > 0 %} + .SetEndpoint("{{ spec.endpointDocs | raw }}") // Your API Endpoint +{% for node in method.auth %} +{% for key,header in node|keys %} + .Set{{header | caseUcfirst}}("{{node[header]['x-appwrite']['demo'] | raw }}"){% if loop.last %};{% endif %} // {{node[header].description}} +{% endfor %}{% endfor %}{% endif %} + + {{ service.name | caseCamel }} = new {{ service.name | caseUcfirst }}(client); + + await Example{{method.name | caseUcfirst}}(); + } + + async UniTask Example{{method.name | caseUcfirst}}() + { + try + { +{% if method.method != 'delete' and method.type != 'webAuth' %}{% if method.type == 'location' %} byte[] result = {% else %} {{ method.responseModel | caseUcfirst | overrideIdentifier }} result = {% endif %}{% endif %}await {{ service.name | caseCamel }}.{{ method.name | caseUcfirst }}({% if method.parameters.all | length == 0 %});{% endif %} +{%~ for parameter in method.parameters.all %} + + {{ parameter.name }}: {% if parameter.enumValues | length > 0%}{{ parameter.enumName }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else %}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %}{% if not parameter.required %} // optional{% endif %} +{%~ endfor %} + +{% if method.parameters.all | length > 0 %} );{% endif %} + +{% if method.method != 'delete' and method.type != 'webAuth' %} Debug.Log("Success: " + result); +{% else %} Debug.Log("Success"); +{% endif %} + } + catch ({{spec.title | caseUcfirst}}Exception ex) + { + Debug.LogError($"Error: {ex.Message} (Code: {ex.Code})"); + } + } +} +``` + +## Parameters + +{%~ for parameter in method.parameters.all %} +- **{{parameter.name | caseCamel}}** *{{parameter.type}}* - {{parameter.description}}{% if parameter.required %} *(required)* {% else %} *(optional)*{% endif %} + +{%~ endfor %} + +## Response + +{% if method.responseModel and method.responseModel != 'any' -%} +Returns `{{method.responseModel | caseUcfirst}}` object. +{%- else -%} +{% if method.type == "webAuth" -%} +None Returns +{%- else -%} +Returns response object. +{%- endif -%} +{%- endif %} + +## More Info + +{{method.description}} diff --git a/templates/unity/icon.png b/templates/unity/icon.png new file mode 100644 index 0000000000..dadbae8bab Binary files /dev/null and b/templates/unity/icon.png differ diff --git a/templates/unity/package.json.twig b/templates/unity/package.json.twig new file mode 100644 index 0000000000..0e585b9a48 --- /dev/null +++ b/templates/unity/package.json.twig @@ -0,0 +1,24 @@ +{ + "name": "com.fellmonkey.{{spec.title | caseLower}}-sdk", + "version": "{{sdk.version}}", + "displayName": "{{spec.title}} SDK", + "description": "{{spec.description}}", + "unity": "2021.3", + "documentationUrl": "https://appwrite.io/docs", + "keywords": [ + "{{spec.title | caseLower}}", + "backend", + "baas", + "api", + "database", + "authentication", + "storage", + "functions" + ], + "samples": [ + { + "displayName": "Example", + "description": "Appwrite Example", + "path": "Samples~/AppwriteExample" + } ] +} diff --git a/templates/web/docs/example.md.twig b/templates/web/docs/example.md.twig index 24266e9377..58ca374c5c 100644 --- a/templates/web/docs/example.md.twig +++ b/templates/web/docs/example.md.twig @@ -1,4 +1,4 @@ -import { Client, {{ service.name | caseUcfirst }}{% for parameter in method.parameters.all %}{% if parameter.enumValues | length > 0%}, {{ parameter.enumName | caseUcfirst}}{% endif %}{% endfor %} } from "{{ language.params.npmPackage }}"; +import { Client, {{ service.name | caseUcfirst }}{% for parameter in method.parameters.all %}{% if parameter.enumValues | length > 0%}, {{ parameter.enumName | caseUcfirst}}{% endif %}{% endfor %}{% if method.parameters.all | hasPermissionParam %}, Permission, Role{% endif %} } from "{{ language.params.npmPackage }}"; const client = new Client() {%~ if method.auth|length > 0 %} @@ -16,10 +16,10 @@ const {{ service.name | caseCamel }} = new {{service.name | caseUcfirst}}(client {% else %}{ {%~ for parameter in method.parameters.all %} {%~ if parameter.required %} - {{ parameter.name | caseCamel }}: {% if parameter.enumValues | length > 0 %}{{ parameter.enumName }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else%}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %} + {{ parameter.name | caseCamel }}: {% if parameter.enumValues | length > 0 %}{{ parameter.enumName | caseUcfirst }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else%}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %} {%~ else %} - {{ parameter.name | caseCamel }}: {% if parameter.enumValues | length > 0 %}{{ parameter.enumName }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else%}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %} // optional + {{ parameter.name | caseCamel }}: {% if parameter.enumValues | length > 0 %}{{ parameter.enumName | caseUcfirst }}.{{ (parameter.enumKeys[0] ?? parameter.enumValues[0]) | caseEnumKey }}{% else%}{{ parameter | paramExample }}{% endif %}{% if not loop.last %},{% endif %} // optional {%~ endif %} {%~ endfor -%} }); diff --git a/templates/web/package.json.twig b/templates/web/package.json.twig index 7e8771b328..fefcbbf003 100644 --- a/templates/web/package.json.twig +++ b/templates/web/package.json.twig @@ -26,7 +26,7 @@ }, "devDependencies": { "@rollup/plugin-typescript": "8.3.2", - "playwright": "1.15.0", + "playwright": "1.56.1", "rollup": "2.79.2", "serve-handler": "6.1.0", "tslib": "2.4.0", diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 92b1e7b4ba..358e30bfb2 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -300,7 +300,11 @@ class Client { /** * Holds configuration such as project. */ - config = { + config: { + endpoint: string; + endpointRealtime: string; + [key: string]: string | undefined; + } = { endpoint: '{{ spec.endpoint }}', endpointRealtime: '', {%~ for header in spec.global.headers %} @@ -330,6 +334,10 @@ class Client { * @returns {this} */ setEndpoint(endpoint: string): this { + if (!endpoint || typeof endpoint !== 'string') { + throw new {{spec.title | caseUcfirst}}Exception('Endpoint must be a valid string'); + } + if (!endpoint.startsWith('http://') && !endpoint.startsWith('https://')) { throw new {{spec.title | caseUcfirst}}Exception('Invalid endpoint URL: ' + endpoint); } @@ -348,6 +356,10 @@ class Client { * @returns {this} */ setEndpointRealtime(endpointRealtime: string): this { + if (!endpointRealtime || typeof endpointRealtime !== 'string') { + throw new {{spec.title | caseUcfirst}}Exception('Endpoint must be a valid string'); + } + if (!endpointRealtime.startsWith('ws://') && !endpointRealtime.startsWith('wss://')) { throw new {{spec.title | caseUcfirst}}Exception('Invalid realtime endpoint URL: ' + endpointRealtime); } @@ -423,7 +435,9 @@ class Client { } const channels = new URLSearchParams(); - channels.set('project', this.config.project); + if (this.config.project) { + channels.set('project', this.config.project); + } this.realtime.channels.forEach(channel => { channels.append('channels[]', channel); }); @@ -478,10 +492,13 @@ class Client { this.realtime.lastMessage = message; switch (message.type) { case 'connected': - const cookie = JSON.parse(window.localStorage.getItem('cookieFallback') ?? '{}'); - const session = cookie?.[`a_session_${this.config.project}`]; - const messageData = message.data; + let session = this.config.session; + if (!session) { + const cookie = JSON.parse(window.localStorage.getItem('cookieFallback') ?? '{}'); + session = cookie?.[`a_session_${this.config.project}`]; + } + const messageData = message.data; if (session && !messageData.user) { this.realtime.socket?.send(JSON.stringify({ type: 'authentication', @@ -532,6 +549,9 @@ class Client { /** * Subscribes to {{spec.title | caseUcfirst}} events and passes you the payload in realtime. * + * @deprecated Use the Realtime service instead. + * @see Realtime + * * @param {string|string[]} channels * Channel to subscribe - pass a single channel as a string or multiple with an array of strings. * diff --git a/templates/web/src/index.ts.twig b/templates/web/src/index.ts.twig index b5456a7107..c9aba90fc7 100644 --- a/templates/web/src/index.ts.twig +++ b/templates/web/src/index.ts.twig @@ -9,11 +9,13 @@ export { Client, Query, {{spec.title | caseUcfirst}}Exception } from './client'; {% for service in spec.services %} export { {{service.name | caseUcfirst}} } from './services/{{service.name | caseKebab}}'; {% endfor %} +export { Realtime } from './services/realtime'; export type { Models, Payload, RealtimeResponseEvent, UploadProgress } from './client'; export type { QueryTypes, QueryTypesList } from './query'; export { Permission } from './permission'; export { Role } from './role'; export { ID } from './id'; -{% for enum in spec.enums %} +export { Operator, Condition } from './operator'; +{% for enum in spec.allEnums %} export { {{ enum.name | caseUcfirst }} } from './enums/{{enum.name | caseKebab}}'; {% endfor %} \ No newline at end of file diff --git a/templates/web/src/models.ts.twig b/templates/web/src/models.ts.twig index 55c152eccf..d52b15b3a0 100644 --- a/templates/web/src/models.ts.twig +++ b/templates/web/src/models.ts.twig @@ -1,3 +1,9 @@ +{% if spec.responseEnums|length > 0 %} +{% for responseEnum in spec.responseEnums %} +import { {{ responseEnum.name }} } from "./enums/{{ responseEnum.name | caseKebab }}" +{% endfor %} +{% endif %} + /** * {{spec.title | caseUcfirst}} Models */ @@ -14,7 +20,7 @@ export namespace Models { /** * {{ property.description | raw }} */ - {{ property.name }}{% if not property.required %}?{% endif %}: {{ property | getSubSchema(spec) | raw }}; + {{ property.name }}{% if not property.required %}?{% endif %}: {{ property | getSubSchema(spec, definition.name) | raw }}; {% endfor %} } {% if definition.additionalProperties %} diff --git a/templates/web/src/operator.ts.twig b/templates/web/src/operator.ts.twig new file mode 100644 index 0000000000..2386a6c414 --- /dev/null +++ b/templates/web/src/operator.ts.twig @@ -0,0 +1,308 @@ +type OperatorValuesSingle = string | number | boolean; +export type OperatorValuesList = string[] | number[] | boolean[] | any[]; +export type OperatorValues = OperatorValuesSingle | OperatorValuesList; + +export enum Condition { + Equal = "equal", + NotEqual = "notEqual", + GreaterThan = "greaterThan", + GreaterThanEqual = "greaterThanEqual", + LessThan = "lessThan", + LessThanEqual = "lessThanEqual", + Contains = "contains", + IsNull = "isNull", + IsNotNull = "isNotNull", +} + +/** + * Helper class to generate operator strings for atomic operations. + */ +export class Operator { + method: string; + values: OperatorValuesList | undefined; + + /** + * Constructor for Operator class. + * + * @param {string} method + * @param {OperatorValues} values + */ + constructor( + method: string, + values?: OperatorValues + ) { + this.method = method; + + if (values !== undefined) { + if (Array.isArray(values)) { + this.values = values; + } else { + this.values = [values] as OperatorValuesList; + } + } + } + + /** + * Convert the operator object to a JSON string. + * + * @returns {string} + */ + toString(): string { + return JSON.stringify({ + method: this.method, + values: this.values, + }); + } + + /** + * Increment a numeric attribute by a specified value. + * + * @param {number} value + * @param {number} max + * @returns {string} + */ + static increment = (value: number = 1, max?: number): string => { + if (isNaN(value) || !isFinite(value)) { + throw new Error("Value cannot be NaN or Infinity"); + } + if (max !== undefined && (isNaN(max) || !isFinite(max))) { + throw new Error("Max cannot be NaN or Infinity"); + } + const values: any[] = [value]; + if (max !== undefined) { + values.push(max); + } + return new Operator("increment", values).toString(); + }; + + /** + * Decrement a numeric attribute by a specified value. + * + * @param {number} value + * @param {number} min + * @returns {string} + */ + static decrement = (value: number = 1, min?: number): string => { + if (isNaN(value) || !isFinite(value)) { + throw new Error("Value cannot be NaN or Infinity"); + } + if (min !== undefined && (isNaN(min) || !isFinite(min))) { + throw new Error("Min cannot be NaN or Infinity"); + } + const values: any[] = [value]; + if (min !== undefined) { + values.push(min); + } + return new Operator("decrement", values).toString(); + }; + + /** + * Multiply a numeric attribute by a specified factor. + * + * @param {number} factor + * @param {number} max + * @returns {string} + */ + static multiply = (factor: number, max?: number): string => { + if (isNaN(factor) || !isFinite(factor)) { + throw new Error("Factor cannot be NaN or Infinity"); + } + if (max !== undefined && (isNaN(max) || !isFinite(max))) { + throw new Error("Max cannot be NaN or Infinity"); + } + const values: any[] = [factor]; + if (max !== undefined) { + values.push(max); + } + return new Operator("multiply", values).toString(); + }; + + /** + * Divide a numeric attribute by a specified divisor. + * + * @param {number} divisor + * @param {number} min + * @returns {string} + */ + static divide = (divisor: number, min?: number): string => { + if (isNaN(divisor) || !isFinite(divisor)) { + throw new Error("Divisor cannot be NaN or Infinity"); + } + if (min !== undefined && (isNaN(min) || !isFinite(min))) { + throw new Error("Min cannot be NaN or Infinity"); + } + if (divisor === 0) { + throw new Error("Divisor cannot be zero"); + } + const values: any[] = [divisor]; + if (min !== undefined) { + values.push(min); + } + return new Operator("divide", values).toString(); + }; + + /** + * Apply modulo operation on a numeric attribute. + * + * @param {number} divisor + * @returns {string} + */ + static modulo = (divisor: number): string => { + if (isNaN(divisor) || !isFinite(divisor)) { + throw new Error("Divisor cannot be NaN or Infinity"); + } + if (divisor === 0) { + throw new Error("Divisor cannot be zero"); + } + return new Operator("modulo", [divisor]).toString(); + }; + + /** + * Raise a numeric attribute to a specified power. + * + * @param {number} exponent + * @param {number} max + * @returns {string} + */ + static power = (exponent: number, max?: number): string => { + if (isNaN(exponent) || !isFinite(exponent)) { + throw new Error("Exponent cannot be NaN or Infinity"); + } + if (max !== undefined && (isNaN(max) || !isFinite(max))) { + throw new Error("Max cannot be NaN or Infinity"); + } + const values: any[] = [exponent]; + if (max !== undefined) { + values.push(max); + } + return new Operator("power", values).toString(); + }; + + /** + * Append values to an array attribute. + * + * @param {any[]} values + * @returns {string} + */ + static arrayAppend = (values: any[]): string => + new Operator("arrayAppend", values).toString(); + + /** + * Prepend values to an array attribute. + * + * @param {any[]} values + * @returns {string} + */ + static arrayPrepend = (values: any[]): string => + new Operator("arrayPrepend", values).toString(); + + /** + * Insert a value at a specific index in an array attribute. + * + * @param {number} index + * @param {any} value + * @returns {string} + */ + static arrayInsert = (index: number, value: any): string => + new Operator("arrayInsert", [index, value]).toString(); + + /** + * Remove a value from an array attribute. + * + * @param {any} value + * @returns {string} + */ + static arrayRemove = (value: any): string => + new Operator("arrayRemove", [value]).toString(); + + /** + * Remove duplicate values from an array attribute. + * + * @returns {string} + */ + static arrayUnique = (): string => + new Operator("arrayUnique", []).toString(); + + /** + * Keep only values that exist in both the current array and the provided array. + * + * @param {any[]} values + * @returns {string} + */ + static arrayIntersect = (values: any[]): string => + new Operator("arrayIntersect", values).toString(); + + /** + * Remove values from the array that exist in the provided array. + * + * @param {any[]} values + * @returns {string} + */ + static arrayDiff = (values: any[]): string => + new Operator("arrayDiff", values).toString(); + + /** + * Filter array values based on a condition. + * + * @param {Condition} condition + * @param {any} value + * @returns {string} + */ + static arrayFilter = (condition: Condition, value?: any): string => { + const values: any[] = [condition as string, value === undefined ? null : value]; + return new Operator("arrayFilter", values).toString(); + }; + + /** + * Concatenate a value to a string or array attribute. + * + * @param {any} value + * @returns {string} + */ + static stringConcat = (value: any): string => + new Operator("stringConcat", [value]).toString(); + + /** + * Replace occurrences of a search string with a replacement string. + * + * @param {string} search + * @param {string} replace + * @returns {string} + */ + static stringReplace = (search: string, replace: string): string => + new Operator("stringReplace", [search, replace]).toString(); + + /** + * Toggle a boolean attribute. + * + * @returns {string} + */ + static toggle = (): string => + new Operator("toggle", []).toString(); + + /** + * Add days to a date attribute. + * + * @param {number} days + * @returns {string} + */ + static dateAddDays = (days: number): string => + new Operator("dateAddDays", [days]).toString(); + + /** + * Subtract days from a date attribute. + * + * @param {number} days + * @returns {string} + */ + static dateSubDays = (days: number): string => + new Operator("dateSubDays", [days]).toString(); + + /** + * Set a date attribute to the current date and time. + * + * @returns {string} + */ + static dateSetNow = (): string => + new Operator("dateSetNow", []).toString(); +} diff --git a/templates/web/src/query.ts.twig b/templates/web/src/query.ts.twig index 60cfa96648..4ebd532c5f 100644 --- a/templates/web/src/query.ts.twig +++ b/templates/web/src/query.ts.twig @@ -195,6 +195,14 @@ export class Query { static orderAsc = (attribute: string): string => new Query("orderAsc", attribute).toString(); + /** + * Sort results randomly. + * + * @returns {string} + */ + static orderRandom = (): string => + new Query("orderRandom").toString(); + /** * Return results after documentId. * @@ -300,7 +308,7 @@ export class Query { * @returns {string} */ static createdBefore = (value: string): string => - new Query("createdBefore", undefined, value).toString(); + Query.lessThan("$createdAt", value); /** * Filter resources where document was created after date. @@ -309,7 +317,7 @@ export class Query { * @returns {string} */ static createdAfter = (value: string): string => - new Query("createdAfter", undefined, value).toString(); + Query.greaterThan("$createdAt", value); /** * Filter resources where document was created between dates. @@ -319,7 +327,7 @@ export class Query { * @returns {string} */ static createdBetween = (start: string, end: string): string => - new Query("createdBetween", undefined, [start, end] as QueryTypesList).toString(); + Query.between("$createdAt", start, end); /** * Filter resources where document was updated before date. @@ -328,7 +336,7 @@ export class Query { * @returns {string} */ static updatedBefore = (value: string): string => - new Query("updatedBefore", undefined, value).toString(); + Query.lessThan("$updatedAt", value); /** * Filter resources where document was updated after date. @@ -337,7 +345,7 @@ export class Query { * @returns {string} */ static updatedAfter = (value: string): string => - new Query("updatedAfter", undefined, value).toString(); + Query.greaterThan("$updatedAt", value); /** * Filter resources where document was updated between dates. @@ -347,7 +355,7 @@ export class Query { * @returns {string} */ static updatedBetween = (start: string, end: string): string => - new Query("updatedBetween", undefined, [start, end] as QueryTypesList).toString(); + Query.between("$updatedAt", start, end); /** * Combine multiple queries using logical OR operator. diff --git a/templates/web/src/services/realtime.ts.twig b/templates/web/src/services/realtime.ts.twig new file mode 100644 index 0000000000..d30e716ea1 --- /dev/null +++ b/templates/web/src/services/realtime.ts.twig @@ -0,0 +1,437 @@ +import { {{ spec.title | caseUcfirst}}Exception, Client } from '../client'; + +export type RealtimeSubscription = { + close: () => Promise; +} + +export type RealtimeCallback = { + channels: Set; + callback: (event: RealtimeResponseEvent) => void; +} + +export type RealtimeResponse = { + type: string; + data?: any; +} + +export type RealtimeResponseEvent = { + events: string[]; + channels: string[]; + timestamp: string; + payload: T; +} + +export type RealtimeResponseConnected = { + channels: string[]; + user?: object; +} + +export type RealtimeRequest = { + type: 'authentication'; + data: { + session: string; + }; +} + +export enum RealtimeCode { + NORMAL_CLOSURE = 1000, + POLICY_VIOLATION = 1008, + UNKNOWN_ERROR = -1 +} + +export class Realtime { + private readonly TYPE_ERROR = 'error'; + private readonly TYPE_EVENT = 'event'; + private readonly TYPE_PONG = 'pong'; + private readonly TYPE_CONNECTED = 'connected'; + private readonly DEBOUNCE_MS = 1; + private readonly HEARTBEAT_INTERVAL = 20000; // 20 seconds in milliseconds + + private client: Client; + private socket?: WebSocket; + private activeChannels = new Set(); + private activeSubscriptions = new Map>(); + private heartbeatTimer?: number; + + private subCallDepth = 0; + private reconnectAttempts = 0; + private subscriptionsCounter = 0; + private reconnect = true; + + private onErrorCallbacks: Array<(error?: Error, statusCode?: number) => void> = []; + private onCloseCallbacks: Array<() => void> = []; + private onOpenCallbacks: Array<() => void> = []; + + constructor(client: Client) { + this.client = client; + } + + /** + * Register a callback function to be called when an error occurs + * + * @param {Function} callback - Callback function to handle errors + * @returns {void} + */ + public onError(callback: (error?: Error, statusCode?: number) => void): void { + this.onErrorCallbacks.push(callback); + } + + /** + * Register a callback function to be called when the connection closes + * + * @param {Function} callback - Callback function to handle connection close + * @returns {void} + */ + public onClose(callback: () => void): void { + this.onCloseCallbacks.push(callback); + } + + /** + * Register a callback function to be called when the connection opens + * + * @param {Function} callback - Callback function to handle connection open + * @returns {void} + */ + public onOpen(callback: () => void): void { + this.onOpenCallbacks.push(callback); + } + + private startHeartbeat(): void { + this.stopHeartbeat(); + this.heartbeatTimer = window.setInterval(() => { + if (this.socket && this.socket.readyState === WebSocket.OPEN) { + this.socket.send(JSON.stringify({ type: 'ping' })); + } + }, this.HEARTBEAT_INTERVAL); + } + + private stopHeartbeat(): void { + if (this.heartbeatTimer) { + window.clearInterval(this.heartbeatTimer); + this.heartbeatTimer = undefined; + } + } + + private async createSocket(): Promise { + if (this.activeChannels.size === 0) { + this.reconnect = false; + await this.closeSocket(); + return; + } + + const projectId = this.client.config.project; + if (!projectId) { + throw new {{spec.title | caseUcfirst}}Exception('Missing project ID'); + } + + let queryParams = `project=${projectId}`; + for (const channel of this.activeChannels) { + queryParams += `&channels[]=${encodeURIComponent(channel)}`; + } + + const endpoint = + this.client.config.endpointRealtime !== '' + ? this.client.config.endpointRealtime + : this.client.config.endpoint || ''; + const realtimeEndpoint = endpoint + .replace('https://', 'wss://') + .replace('http://', 'ws://'); + const url = `${realtimeEndpoint}/realtime?${queryParams}`; + + if (this.socket) { + this.reconnect = false; + await this.closeSocket(); + } + + return new Promise((resolve, reject) => { + try { + this.socket = new WebSocket(url); + + this.socket.addEventListener('open', () => { + this.reconnectAttempts = 0; + this.onOpenCallbacks.forEach(callback => callback()); + this.startHeartbeat(); + resolve(); + }); + + this.socket.addEventListener('message', (event: MessageEvent) => { + try { + const message = JSON.parse(event.data) as RealtimeResponse; + this.handleMessage(message); + } catch (error) { + console.error('Failed to parse message:', error); + } + }); + + this.socket.addEventListener('close', async (event: CloseEvent) => { + this.stopHeartbeat(); + this.onCloseCallbacks.forEach(callback => callback()); + + if (!this.reconnect || event.code === RealtimeCode.POLICY_VIOLATION) { + this.reconnect = true; + return; + } + + const timeout = this.getTimeout(); + console.log(`Realtime disconnected. Re-connecting in ${timeout / 1000} seconds.`); + + await this.sleep(timeout); + this.reconnectAttempts++; + + try { + await this.createSocket(); + } catch (error) { + console.error('Failed to reconnect:', error); + } + }); + + this.socket.addEventListener('error', (event: Event) => { + this.stopHeartbeat(); + const error = new Error('WebSocket error'); + console.error('WebSocket error:', error.message); + this.onErrorCallbacks.forEach(callback => callback(error)); + reject(error); + }); + } catch (error) { + reject(error); + } + }); + } + + private async closeSocket(): Promise { + this.stopHeartbeat(); + + if (this.socket) { + return new Promise((resolve) => { + if (!this.socket) { + resolve(); + return; + } + + if (this.socket.readyState === WebSocket.OPEN || + this.socket.readyState === WebSocket.CONNECTING) { + this.socket.addEventListener('close', () => { + resolve(); + }, { once: true }); + this.socket.close(RealtimeCode.NORMAL_CLOSURE); + } else { + resolve(); + } + }); + } + } + + private getTimeout(): number { + if (this.reconnectAttempts < 5) { + return 1000; + } else if (this.reconnectAttempts < 15) { + return 5000; + } else if (this.reconnectAttempts < 100) { + return 10000; + } else { + return 60000; + } + } + + private sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + /** + * Subscribe to a single channel + * + * @param {string} channel - Channel name to subscribe to + * @param {Function} callback - Callback function to handle events + * @returns {Promise} Subscription object with close method + */ + public async subscribe( + channel: string, + callback: (event: RealtimeResponseEvent) => void + ): Promise; + + /** + * Subscribe to multiple channels + * + * @param {string[]} channels - Array of channel names to subscribe to + * @param {Function} callback - Callback function to handle events + * @returns {Promise} Subscription object with close method + */ + public async subscribe( + channels: string[], + callback: (event: RealtimeResponseEvent) => void + ): Promise; + + /** + * Subscribe to a single channel with typed payload + * + * @param {string} channel - Channel name to subscribe to + * @param {Function} callback - Callback function to handle events with typed payload + * @returns {Promise} Subscription object with close method + */ + public async subscribe( + channel: string, + callback: (event: RealtimeResponseEvent) => void + ): Promise; + + /** + * Subscribe to multiple channels with typed payload + * + * @param {string[]} channels - Array of channel names to subscribe to + * @param {Function} callback - Callback function to handle events with typed payload + * @returns {Promise} Subscription object with close method + */ + public async subscribe( + channels: string[], + callback: (event: RealtimeResponseEvent) => void + ): Promise; + + public async subscribe( + channelsOrChannel: string | string[], + callback: (event: RealtimeResponseEvent) => void + ): Promise { + const channels = Array.isArray(channelsOrChannel) + ? new Set(channelsOrChannel) + : new Set([channelsOrChannel]); + + this.subscriptionsCounter++; + const count = this.subscriptionsCounter; + + for (const channel of channels) { + this.activeChannels.add(channel); + } + + this.activeSubscriptions.set(count, { + channels, + callback + }); + + this.subCallDepth++; + + await this.sleep(this.DEBOUNCE_MS); + + if (this.subCallDepth === 1) { + await this.createSocket(); + } + + this.subCallDepth--; + + return { + close: async () => { + this.activeSubscriptions.delete(count); + this.cleanUp(channels); + await this.createSocket(); + } + }; + } + + private cleanUp(channels: Set): void { + this.activeChannels = new Set( + Array.from(this.activeChannels).filter(channel => { + if (!channels.has(channel)) { + return true; + } + + const subsWithChannel = Array.from(this.activeSubscriptions.values()) + .filter(sub => sub.channels.has(channel)); + + return subsWithChannel.length > 0; + }) + ); + } + + private handleMessage(message: RealtimeResponse): void { + if (!message.type) { + return; + } + + switch (message.type) { + case this.TYPE_CONNECTED: + this.handleResponseConnected(message); + break; + case this.TYPE_ERROR: + this.handleResponseError(message); + break; + case this.TYPE_EVENT: + this.handleResponseEvent(message); + break; + case this.TYPE_PONG: + // Handle pong response if needed + break; + } + } + + private handleResponseConnected(message: RealtimeResponse): void { + if (!message.data) { + return; + } + + const messageData = message.data as RealtimeResponseConnected; + + let session = this.client.config.session; + if (!session) { + try { + const cookie = JSON.parse(window.localStorage.getItem('cookieFallback') ?? '{}'); + session = cookie?.[`a_session_${this.client.config.project}`]; + } catch (error) { + console.error('Failed to parse cookie fallback:', error); + } + } + + if (session && !messageData.user) { + this.socket?.send(JSON.stringify({ + type: 'authentication', + data: { + session + } + })); + } + } + + private handleResponseError(message: RealtimeResponse): void { + const error = new {{spec.title | caseUcfirst}}Exception( + message.data?.message || 'Unknown error' + ); + const statusCode = message.data?.code; + this.onErrorCallbacks.forEach(callback => callback(error, statusCode)); + } + + private handleResponseEvent(message: RealtimeResponse): void { + const data = message.data; + if (!data) { + return; + } + + const channels = data.channels as string[]; + const events = data.events as string[]; + const payload = data.payload; + const timestamp = data.timestamp as string; + + if (!channels || !events || !payload) { + return; + } + + const hasActiveChannel = channels.some(channel => + this.activeChannels.has(channel) + ); + + if (!hasActiveChannel) { + return; + } + + for (const [_, subscription] of this.activeSubscriptions) { + const hasSubscribedChannel = channels.some(channel => + subscription.channels.has(channel) + ); + + if (hasSubscribedChannel) { + const response: RealtimeResponseEvent = { + events, + channels, + timestamp, + payload + }; + subscription.callback(response); + } + } + } +} diff --git a/templates/web/src/services/template.ts.twig b/templates/web/src/services/template.ts.twig index 236a836c6b..008868f437 100644 --- a/templates/web/src/services/template.ts.twig +++ b/templates/web/src/services/template.ts.twig @@ -6,14 +6,9 @@ import type { Models } from '../models'; {% for method in service.methods %} {% for parameter in method.parameters.all %} {% if parameter.enumValues is not empty %} -{% if parameter.enumName is not empty %} -{% set name = parameter.enumName %} -{% else %} -{% set name = parameter.name %} -{% endif %} -{% if name not in added %} -import { {{ name | caseUcfirst }} } from '../enums/{{ name | caseKebab }}'; -{% set added = added|merge([name]) %} +{% if parameter.enumName not in added %} +import { {{ parameter.enumName | caseUcfirst }} } from '../enums/{{ parameter.enumName | caseKebab }}'; +{% set added = added|merge([parameter.enumName]) %} {% endif %} {% endif %} {% endfor %} diff --git a/tests/Android14Java11Test.php b/tests/Android14Java11Test.php index 23ca00f73f..b6e114f404 100644 --- a/tests/Android14Java11Test.php +++ b/tests/Android14Java11Test.php @@ -32,6 +32,7 @@ class Android14Java11Test extends Base // ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Android14Java17Test.php b/tests/Android14Java17Test.php index 78e17f3cec..aede9c7af1 100644 --- a/tests/Android14Java17Test.php +++ b/tests/Android14Java17Test.php @@ -31,6 +31,7 @@ class Android14Java17Test extends Base // ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Android14Java8Test.php b/tests/Android14Java8Test.php index 280fabf7ac..0355e77b04 100644 --- a/tests/Android14Java8Test.php +++ b/tests/Android14Java8Test.php @@ -32,6 +32,7 @@ class Android14Java8Test extends Base // ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Android5Java17Test.php b/tests/Android5Java17Test.php index 1bf4ad99e5..9677e0f00d 100644 --- a/tests/Android5Java17Test.php +++ b/tests/Android5Java17Test.php @@ -31,6 +31,7 @@ class Android5Java17Test extends Base // ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/AppleSwift56Test.php b/tests/AppleSwift56Test.php index ccf79f6cc0..b4a6709f14 100644 --- a/tests/AppleSwift56Test.php +++ b/tests/AppleSwift56Test.php @@ -30,6 +30,7 @@ class AppleSwift56Test extends Base ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Base.php b/tests/Base.php index b36a59442a..4917f2d9ad 100644 --- a/tests/Base.php +++ b/tests/Base.php @@ -103,6 +103,7 @@ abstract class Base extends TestCase '{"method":"select","values":["name","age"]}', '{"method":"orderAsc","attribute":"title"}', '{"method":"orderDesc","attribute":"title"}', + '{"method":"orderRandom"}', '{"method":"cursorAfter","values":["my_movie_id"]}', '{"method":"cursorBefore","values":["my_movie_id"]}', '{"method":"limit","values":[50]}', @@ -114,12 +115,12 @@ abstract class Base extends TestCase '{"method":"notBetween","attribute":"age","values":[50,100]}', '{"method":"notStartsWith","attribute":"name","values":["Ann"]}', '{"method":"notEndsWith","attribute":"name","values":["nne"]}', - '{"method":"createdBefore","values":["2023-01-01"]}', - '{"method":"createdAfter","values":["2023-01-01"]}', - '{"method":"createdBetween","values":["2023-01-01","2023-12-31"]}', - '{"method":"updatedBefore","values":["2023-01-01"]}', - '{"method":"updatedAfter","values":["2023-01-01"]}', - '{"method":"updatedBetween","values":["2023-01-01","2023-12-31"]}', + '{"method":"lessThan","attribute":"$createdAt","values":["2023-01-01"]}', + '{"method":"greaterThan","attribute":"$createdAt","values":["2023-01-01"]}', + '{"method":"between","attribute":"$createdAt","values":["2023-01-01","2023-12-31"]}', + '{"method":"lessThan","attribute":"$updatedAt","values":["2023-01-01"]}', + '{"method":"greaterThan","attribute":"$updatedAt","values":["2023-01-01"]}', + '{"method":"between","attribute":"$updatedAt","values":["2023-01-01","2023-12-31"]}', '{"method":"distanceEqual","attribute":"location","values":[[[[40.7128,-74],[40.7128,-74]],1000,true]]}', '{"method":"distanceEqual","attribute":"location","values":[[[40.7128,-74],1000,true]]}', '{"method":"distanceNotEqual","attribute":"location","values":[[[40.7128,-74],1000,true]]}', @@ -162,6 +163,34 @@ abstract class Base extends TestCase 'custom_id' ]; + protected const OPERATOR_HELPER_RESPONSES = [ + '{"method":"increment","values":[1]}', + '{"method":"increment","values":[5,100]}', + '{"method":"decrement","values":[1]}', + '{"method":"decrement","values":[3,0]}', + '{"method":"multiply","values":[2]}', + '{"method":"multiply","values":[3,1000]}', + '{"method":"divide","values":[2]}', + '{"method":"divide","values":[4,1]}', + '{"method":"modulo","values":[5]}', + '{"method":"power","values":[2]}', + '{"method":"power","values":[3,100]}', + '{"method":"arrayAppend","values":["item1","item2"]}', + '{"method":"arrayPrepend","values":["first","second"]}', + '{"method":"arrayInsert","values":[0,"newItem"]}', + '{"method":"arrayRemove","values":["oldItem"]}', + '{"method":"arrayUnique","values":[]}', + '{"method":"arrayIntersect","values":["a","b","c"]}', + '{"method":"arrayDiff","values":["x","y"]}', + '{"method":"arrayFilter","values":["equal","test"]}', + '{"method":"stringConcat","values":["suffix"]}', + '{"method":"stringReplace","values":["old","new"]}', + '{"method":"toggle","values":[]}', + '{"method":"dateAddDays","values":[7]}', + '{"method":"dateSubDays","values":[3]}', + '{"method":"dateSetNow","values":[]}', + ]; + protected string $class = ''; protected string $language = ''; protected array $build = []; diff --git a/tests/DartBetaTest.php b/tests/DartBetaTest.php index af0165ab92..b9571f32ed 100644 --- a/tests/DartBetaTest.php +++ b/tests/DartBetaTest.php @@ -29,6 +29,7 @@ class DartBetaTest extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/DartStableTest.php b/tests/DartStableTest.php index 1aaafd33a5..68cf9bef06 100644 --- a/tests/DartStableTest.php +++ b/tests/DartStableTest.php @@ -29,6 +29,7 @@ class DartStableTest extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Deno1193Test.php b/tests/Deno1193Test.php index 0dfc163b36..cab7e3090b 100644 --- a/tests/Deno1193Test.php +++ b/tests/Deno1193Test.php @@ -25,6 +25,7 @@ class Deno1193Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Deno1303Test.php b/tests/Deno1303Test.php index 31f392bffa..05cd38a5e1 100644 --- a/tests/Deno1303Test.php +++ b/tests/Deno1303Test.php @@ -25,6 +25,7 @@ class Deno1303Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/DotNet60Test.php b/tests/DotNet60Test.php index c8833f802e..7abecf6b98 100644 --- a/tests/DotNet60Test.php +++ b/tests/DotNet60Test.php @@ -29,6 +29,7 @@ class DotNet60Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/DotNet80Test.php b/tests/DotNet80Test.php index 52a01d4cca..bee14eaec7 100644 --- a/tests/DotNet80Test.php +++ b/tests/DotNet80Test.php @@ -29,6 +29,7 @@ class DotNet80Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/DotNet90Test.php b/tests/DotNet90Test.php index e8ea867108..b7ddf06420 100644 --- a/tests/DotNet90Test.php +++ b/tests/DotNet90Test.php @@ -29,6 +29,7 @@ class DotNet90Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/FlutterBetaTest.php b/tests/FlutterBetaTest.php index 6e49db807c..366f3d1320 100644 --- a/tests/FlutterBetaTest.php +++ b/tests/FlutterBetaTest.php @@ -30,6 +30,7 @@ class FlutterBetaTest extends Base ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/FlutterStableTest.php b/tests/FlutterStableTest.php index 66a1c5c3f0..62521f7800 100644 --- a/tests/FlutterStableTest.php +++ b/tests/FlutterStableTest.php @@ -30,6 +30,7 @@ class FlutterStableTest extends Base ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Go112Test.php b/tests/Go112Test.php index 6cdd04fb0a..d58b64a903 100644 --- a/tests/Go112Test.php +++ b/tests/Go112Test.php @@ -27,5 +27,6 @@ class Go112Test extends Base ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES, ]; } diff --git a/tests/Go118Test.php b/tests/Go118Test.php index 7138dd5ca6..786150c50f 100644 --- a/tests/Go118Test.php +++ b/tests/Go118Test.php @@ -27,5 +27,6 @@ class Go118Test extends Base ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES, ]; } diff --git a/tests/KotlinJava11Test.php b/tests/KotlinJava11Test.php index f97a930838..fab40bb4a7 100644 --- a/tests/KotlinJava11Test.php +++ b/tests/KotlinJava11Test.php @@ -17,7 +17,7 @@ class KotlinJava11Test extends Base 'chmod +x tests/sdks/kotlin/gradlew', ]; protected string $command = - 'docker run --network="mockapi" -v $(pwd):/app -w /app/tests/sdks/kotlin openjdk:11-jdk-slim sh -c "./gradlew test -q && cat result.txt"'; + 'docker run --network="mockapi" -v $(pwd):/app -w /app/tests/sdks/kotlin eclipse-temurin:11-jdk-jammy sh -c "./gradlew test -q && cat result.txt"'; protected array $expectedOutput = [ ...Base::PING_RESPONSE, @@ -30,6 +30,7 @@ class KotlinJava11Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/KotlinJava17Test.php b/tests/KotlinJava17Test.php index 41dfac724d..ac117f47d4 100644 --- a/tests/KotlinJava17Test.php +++ b/tests/KotlinJava17Test.php @@ -17,7 +17,7 @@ class KotlinJava17Test extends Base 'chmod +x tests/sdks/kotlin/gradlew', ]; protected string $command = - 'docker run --network="mockapi" -v $(pwd):/app -w /app/tests/sdks/kotlin openjdk:17-jdk-slim sh -c "./gradlew test -q && cat result.txt"'; + 'docker run --network="mockapi" -v $(pwd):/app -w /app/tests/sdks/kotlin eclipse-temurin:17-jdk-jammy sh -c "./gradlew test -q && cat result.txt"'; protected array $expectedOutput = [ ...Base::PING_RESPONSE, @@ -30,6 +30,7 @@ class KotlinJava17Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/KotlinJava8Test.php b/tests/KotlinJava8Test.php index bb030a4b4f..41fe724e1d 100644 --- a/tests/KotlinJava8Test.php +++ b/tests/KotlinJava8Test.php @@ -17,7 +17,7 @@ class KotlinJava8Test extends Base 'chmod +x tests/sdks/kotlin/gradlew', ]; protected string $command = - 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/kotlin openjdk:8-jdk-slim sh -c "./gradlew test -q && cat result.txt"'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/kotlin eclipse-temurin:8-jdk-jammy sh -c "./gradlew test -q && cat result.txt"'; protected array $expectedOutput = [ ...Base::PING_RESPONSE, @@ -30,6 +30,7 @@ class KotlinJava8Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Node16Test.php b/tests/Node16Test.php index b780896ca8..aded19b033 100644 --- a/tests/Node16Test.php +++ b/tests/Node16Test.php @@ -33,6 +33,7 @@ class Node16Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Node18Test.php b/tests/Node18Test.php index 3a84bd3bec..33a7b1a73f 100644 --- a/tests/Node18Test.php +++ b/tests/Node18Test.php @@ -33,6 +33,7 @@ class Node18Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Node20Test.php b/tests/Node20Test.php index 67e8a08c05..b2395b0135 100644 --- a/tests/Node20Test.php +++ b/tests/Node20Test.php @@ -33,6 +33,7 @@ class Node20Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/PHP80Test.php b/tests/PHP80Test.php index 74ed3d0473..bf9cefccd3 100644 --- a/tests/PHP80Test.php +++ b/tests/PHP80Test.php @@ -25,6 +25,7 @@ class PHP80Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/PHP83Test.php b/tests/PHP83Test.php index d453463d14..5cba40ca1d 100644 --- a/tests/PHP83Test.php +++ b/tests/PHP83Test.php @@ -25,6 +25,7 @@ class PHP83Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Python310Test.php b/tests/Python310Test.php index 320bd7a201..486627d633 100644 --- a/tests/Python310Test.php +++ b/tests/Python310Test.php @@ -29,6 +29,7 @@ class Python310Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Python311Test.php b/tests/Python311Test.php index dcc0e9c367..68e0849603 100644 --- a/tests/Python311Test.php +++ b/tests/Python311Test.php @@ -29,6 +29,7 @@ class Python311Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Python312Test.php b/tests/Python312Test.php index caf34fa2bd..a42d090b83 100644 --- a/tests/Python312Test.php +++ b/tests/Python312Test.php @@ -29,6 +29,7 @@ class Python312Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Python313Test.php b/tests/Python313Test.php index 293fad1809..e752278bbe 100644 --- a/tests/Python313Test.php +++ b/tests/Python313Test.php @@ -29,6 +29,7 @@ class Python313Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Python39Test.php b/tests/Python39Test.php index 9c3d081552..36f9dfcaef 100644 --- a/tests/Python39Test.php +++ b/tests/Python39Test.php @@ -29,6 +29,7 @@ class Python39Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Ruby27Test.php b/tests/Ruby27Test.php index ec368dcdaa..7565fe1418 100644 --- a/tests/Ruby27Test.php +++ b/tests/Ruby27Test.php @@ -27,6 +27,7 @@ class Ruby27Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Ruby30Test.php b/tests/Ruby30Test.php index 0fef364ea7..d1b8a94313 100644 --- a/tests/Ruby30Test.php +++ b/tests/Ruby30Test.php @@ -27,6 +27,7 @@ class Ruby30Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Ruby31Test.php b/tests/Ruby31Test.php index c7a4873f07..a8de8c44b2 100644 --- a/tests/Ruby31Test.php +++ b/tests/Ruby31Test.php @@ -27,6 +27,7 @@ class Ruby31Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Swift56Test.php b/tests/Swift56Test.php index 15fa954e26..356c1a0543 100644 --- a/tests/Swift56Test.php +++ b/tests/Swift56Test.php @@ -29,6 +29,7 @@ class Swift56Test extends Base ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Unity2021Test.php b/tests/Unity2021Test.php new file mode 100644 index 0000000000..9d37ad6f48 --- /dev/null +++ b/tests/Unity2021Test.php @@ -0,0 +1,46 @@ + Unity_lic.ulf && /opt/unity/Editor/Unity -nographics -batchmode -manualLicenseFile Unity_lic.ulf -quit || true && /opt/unity/Editor/Unity -projectPath . -batchmode -nographics -runTests -testPlatform PlayMode -stackTraceLogType None -logFile - 2>/dev/null | sed -n \'/Test Started/,\$p\' | grep -v -E \'^(UnityEngine\\.|System\\.|Cysharp\\.|\\(Filename:|\\[.*\\]|##utp:|^\\s*\$|The header Origin is managed automatically|Connected to realtime:)\' | grep -v \'StackTraceUtility\'"'; + + public function testHTTPSuccess(): void + { + // Set Unity test mode to exclude problematic files + $GLOBALS['UNITY_TEST_MODE'] = true; + + parent::testHTTPSuccess(); + } + + protected array $expectedOutput = [ + ...Base::PING_RESPONSE, + ...Base::FOO_RESPONSES, + ...Base::BAR_RESPONSES, + ...Base::GENERAL_RESPONSES, + ...Base::UPLOAD_RESPONSES, + ...Base::DOWNLOAD_RESPONSES, + ...Base::ENUM_RESPONSES, + ...Base::EXCEPTION_RESPONSES, + ...Base::REALTIME_RESPONSES, + ...Base::COOKIE_RESPONSES, + ...Base::QUERY_HELPER_RESPONSES, + ...Base::PERMISSION_HELPER_RESPONSES, + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES + ]; +} diff --git a/tests/WebChromiumTest.php b/tests/WebChromiumTest.php index d4460964e2..8e609e383e 100644 --- a/tests/WebChromiumTest.php +++ b/tests/WebChromiumTest.php @@ -15,10 +15,10 @@ class WebChromiumTest extends Base 'cp tests/languages/web/tests.js tests/sdks/web/tests.js', 'cp tests/languages/web/node.js tests/sdks/web/node.js', 'cp tests/languages/web/index.html tests/sdks/web/index.html', - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/web mcr.microsoft.com/playwright:v1.15.0-focal sh -c "npm install && npm run build"', + 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/web mcr.microsoft.com/playwright:v1.56.1-jammy sh -c "npm install && npm run build"', ]; protected string $command = - 'docker run --network="mockapi" --rm -v $(pwd):/app -e BROWSER=chromium -w /app/tests/sdks/web mcr.microsoft.com/playwright:v1.15.0-focal node tests.js'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -e BROWSER=chromium -w /app/tests/sdks/web mcr.microsoft.com/playwright:v1.56.1-jammy node tests.js'; protected array $expectedOutput = [ ...Base::PING_RESPONSE, @@ -34,6 +34,7 @@ class WebChromiumTest extends Base ...Base::REALTIME_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/WebNodeTest.php b/tests/WebNodeTest.php index 57ed5d6ea4..dc8e48275e 100644 --- a/tests/WebNodeTest.php +++ b/tests/WebNodeTest.php @@ -34,6 +34,7 @@ class WebNodeTest extends Base ...Base::REALTIME_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, - ...Base::ID_HELPER_RESPONSES + ...Base::ID_HELPER_RESPONSES, + ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/languages/android/Tests.kt b/tests/languages/android/Tests.kt index 72947d0b81..aa8a083ef3 100644 --- a/tests/languages/android/Tests.kt +++ b/tests/languages/android/Tests.kt @@ -8,6 +8,8 @@ import io.appwrite.Permission import io.appwrite.Role import io.appwrite.ID import io.appwrite.Query +import io.appwrite.Operator +import io.appwrite.Condition import io.appwrite.enums.MockType import io.appwrite.extensions.fromJson import io.appwrite.extensions.toJson @@ -203,6 +205,7 @@ class ServiceTest { writeToFile(Query.select(listOf("name", "age"))) writeToFile(Query.orderAsc("title")) writeToFile(Query.orderDesc("title")) + writeToFile(Query.orderRandom()) writeToFile(Query.cursorAfter("my_movie_id")) writeToFile(Query.cursorBefore("my_movie_id")) writeToFile(Query.limit(50)) @@ -266,6 +269,33 @@ class ServiceTest { writeToFile(ID.unique()) writeToFile(ID.custom("custom_id")) + // Operator helper tests + writeToFile(Operator.increment(1)) + writeToFile(Operator.increment(5, 100)) + writeToFile(Operator.decrement(1)) + writeToFile(Operator.decrement(3, 0)) + writeToFile(Operator.multiply(2)) + writeToFile(Operator.multiply(3, 1000)) + writeToFile(Operator.divide(2)) + writeToFile(Operator.divide(4, 1)) + writeToFile(Operator.modulo(5)) + writeToFile(Operator.power(2)) + writeToFile(Operator.power(3, 100)) + writeToFile(Operator.arrayAppend(listOf("item1", "item2"))) + writeToFile(Operator.arrayPrepend(listOf("first", "second"))) + writeToFile(Operator.arrayInsert(0, "newItem")) + writeToFile(Operator.arrayRemove("oldItem")) + writeToFile(Operator.arrayUnique()) + writeToFile(Operator.arrayIntersect(listOf("a", "b", "c"))) + writeToFile(Operator.arrayDiff(listOf("x", "y"))) + writeToFile(Operator.arrayFilter(Condition.EQUAL, "test")) + writeToFile(Operator.stringConcat("suffix")) + writeToFile(Operator.stringReplace("old", "new")) + writeToFile(Operator.toggle()) + writeToFile(Operator.dateAddDays(7)) + writeToFile(Operator.dateSubDays(3)) + writeToFile(Operator.dateSetNow()) + mock = general.headers() writeToFile(mock.result) } diff --git a/tests/languages/apple/Tests.swift b/tests/languages/apple/Tests.swift index 5b896281d1..ae8b55b921 100644 --- a/tests/languages/apple/Tests.swift +++ b/tests/languages/apple/Tests.swift @@ -32,7 +32,8 @@ class Tests: XCTestCase { // reset configs client.setProject("console") - client.setEndpointRealtime("ws://cloud.appwrite.io/v1") + client.setEndpointRealtime("wss://cloud.appwrite.io/v1") + client.setSelfSigned(false) let foo = Foo(client) let bar = Bar(client) @@ -177,6 +178,7 @@ class Tests: XCTestCase { print(Query.select(["name", "age"])) print(Query.orderAsc("title")) print(Query.orderDesc("title")) + print(Query.orderRandom()) print(Query.cursorAfter("my_movie_id")) print(Query.cursorBefore("my_movie_id")) print(Query.limit(50)) @@ -246,6 +248,33 @@ class Tests: XCTestCase { print(ID.unique()) print(ID.custom("custom_id")) + // Operator helper tests + print(Operator.increment(1)) + print(Operator.increment(5, max: 100)) + print(Operator.decrement(1)) + print(Operator.decrement(3, min: 0)) + print(Operator.multiply(2)) + print(Operator.multiply(3, max: 1000)) + print(Operator.divide(2)) + print(Operator.divide(4, min: 1)) + print(Operator.modulo(5)) + print(Operator.power(2)) + print(Operator.power(3, max: 100)) + print(Operator.arrayAppend(["item1", "item2"])) + print(Operator.arrayPrepend(["first", "second"])) + print(Operator.arrayInsert(0, value: "newItem")) + print(Operator.arrayRemove("oldItem")) + print(Operator.arrayUnique()) + print(Operator.arrayIntersect(["a", "b", "c"])) + print(Operator.arrayDiff(["x", "y"])) + print(Operator.arrayFilter(Condition.equal, value: "test")) + print(Operator.stringConcat("suffix")) + print(Operator.stringReplace("old", "new")) + print(Operator.toggle()) + print(Operator.dateAddDays(7)) + print(Operator.dateSubDays(3)) + print(Operator.dateSetNow()) + mock = try await general.headers() print(mock.result) } diff --git a/tests/languages/dart/tests.dart b/tests/languages/dart/tests.dart index 52a1badd7b..4bdc5018cf 100644 --- a/tests/languages/dart/tests.dart +++ b/tests/languages/dart/tests.dart @@ -143,6 +143,7 @@ void main() async { print(Query.select(["name", "age"])); print(Query.orderAsc("title")); print(Query.orderDesc("title")); + print(Query.orderRandom()); print(Query.cursorAfter("my_movie_id")); print(Query.cursorBefore("my_movie_id")); print(Query.limit(50)); @@ -212,6 +213,33 @@ void main() async { print(ID.unique()); print(ID.custom('custom_id')); + // Operator helper tests + print(Operator.increment(1)); + print(Operator.increment(5, 100)); + print(Operator.decrement(1)); + 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.arrayAppend(["item1", "item2"])); + print(Operator.arrayPrepend(["first", "second"])); + print(Operator.arrayInsert(0, "newItem")); + print(Operator.arrayRemove("oldItem")); + print(Operator.arrayUnique()); + print(Operator.arrayIntersect(["a", "b", "c"])); + print(Operator.arrayDiff(["x", "y"])); + print(Operator.arrayFilter(Condition.equal, "test")); + print(Operator.stringConcat("suffix")); + print(Operator.stringReplace("old", "new")); + print(Operator.toggle()); + print(Operator.dateAddDays(7)); + print(Operator.dateSubDays(3)); + print(Operator.dateSetNow()); + response = await general.headers(); print(response.result); } diff --git a/tests/languages/deno/tests.ts b/tests/languages/deno/tests.ts index ba662ffa94..7ef662d9de 100644 --- a/tests/languages/deno/tests.ts +++ b/tests/languages/deno/tests.ts @@ -9,6 +9,8 @@ async function start() { let Role = appwrite.Role; let ID = appwrite.ID; let Query = appwrite.Query; + let Operator = appwrite.Operator; + let Condition = appwrite.Condition; // Init SDK let client = new appwrite.Client().addHeader("Origin", "http://localhost"); @@ -169,6 +171,7 @@ async function start() { console.log(Query.select(["name", "age"])); console.log(Query.orderAsc("title")); console.log(Query.orderDesc("title")); + console.log(Query.orderRandom()); console.log(Query.cursorAfter("my_movie_id")); console.log(Query.cursorBefore("my_movie_id")); console.log(Query.limit(50)); @@ -238,6 +241,33 @@ async function start() { console.log(ID.unique()); console.log(ID.custom("custom_id")); + // Operator helper tests + console.log(Operator.increment(1)); + console.log(Operator.increment(5, 100)); + console.log(Operator.decrement(1)); + console.log(Operator.decrement(3, 0)); + console.log(Operator.multiply(2)); + console.log(Operator.multiply(3, 1000)); + console.log(Operator.divide(2)); + console.log(Operator.divide(4, 1)); + console.log(Operator.modulo(5)); + console.log(Operator.power(2)); + console.log(Operator.power(3, 100)); + console.log(Operator.arrayAppend(["item1", "item2"])); + console.log(Operator.arrayPrepend(["first", "second"])); + console.log(Operator.arrayInsert(0, "newItem")); + console.log(Operator.arrayRemove("oldItem")); + console.log(Operator.arrayUnique()); + console.log(Operator.arrayIntersect(["a", "b", "c"])); + console.log(Operator.arrayDiff(["x", "y"])); + console.log(Operator.arrayFilter(Condition.Equal, "test")); + console.log(Operator.stringConcat("suffix")); + console.log(Operator.stringReplace("old", "new")); + console.log(Operator.toggle()); + console.log(Operator.dateAddDays(7)); + console.log(Operator.dateSubDays(3)); + console.log(Operator.dateSetNow()); + response = await general.headers(); // @ts-ignore console.log(response.result); diff --git a/tests/languages/dotnet/Tests.cs b/tests/languages/dotnet/Tests.cs index b479d2039f..167eb538d7 100644 --- a/tests/languages/dotnet/Tests.cs +++ b/tests/languages/dotnet/Tests.cs @@ -151,6 +151,7 @@ public async Task Test1() TestContext.WriteLine(Query.Select(new List { "name", "age" })); TestContext.WriteLine(Query.OrderAsc("title")); TestContext.WriteLine(Query.OrderDesc("title")); + TestContext.WriteLine(Query.OrderRandom()); TestContext.WriteLine(Query.CursorAfter("my_movie_id")); TestContext.WriteLine(Query.CursorBefore("my_movie_id")); TestContext.WriteLine(Query.Limit(50)); @@ -224,6 +225,33 @@ public async Task Test1() TestContext.WriteLine(ID.Unique()); TestContext.WriteLine(ID.Custom("custom_id")); + // Operator helper tests + TestContext.WriteLine(Operator.Increment(1)); + TestContext.WriteLine(Operator.Increment(5, 100)); + TestContext.WriteLine(Operator.Decrement(1)); + TestContext.WriteLine(Operator.Decrement(3, 0)); + TestContext.WriteLine(Operator.Multiply(2)); + TestContext.WriteLine(Operator.Multiply(3, 1000)); + TestContext.WriteLine(Operator.Divide(2)); + TestContext.WriteLine(Operator.Divide(4, 1)); + TestContext.WriteLine(Operator.Modulo(5)); + TestContext.WriteLine(Operator.Power(2)); + TestContext.WriteLine(Operator.Power(3, 100)); + TestContext.WriteLine(Operator.ArrayAppend(new List { "item1", "item2" })); + TestContext.WriteLine(Operator.ArrayPrepend(new List { "first", "second" })); + TestContext.WriteLine(Operator.ArrayInsert(0, "newItem")); + TestContext.WriteLine(Operator.ArrayRemove("oldItem")); + TestContext.WriteLine(Operator.ArrayUnique()); + TestContext.WriteLine(Operator.ArrayIntersect(new List { "a", "b", "c" })); + TestContext.WriteLine(Operator.ArrayDiff(new List { "x", "y" })); + TestContext.WriteLine(Operator.ArrayFilter(Condition.Equal, "test")); + TestContext.WriteLine(Operator.StringConcat("suffix")); + TestContext.WriteLine(Operator.StringReplace("old", "new")); + TestContext.WriteLine(Operator.Toggle()); + TestContext.WriteLine(Operator.DateAddDays(7)); + TestContext.WriteLine(Operator.DateSubDays(3)); + TestContext.WriteLine(Operator.DateSetNow()); + mock = await general.Headers(); TestContext.WriteLine(mock.Result); } diff --git a/tests/languages/flutter/tests.dart b/tests/languages/flutter/tests.dart index 9c000010b2..939c0ff148 100644 --- a/tests/languages/flutter/tests.dart +++ b/tests/languages/flutter/tests.dart @@ -177,6 +177,7 @@ void main() async { print(Query.select(["name", "age"])); print(Query.orderAsc("title")); print(Query.orderDesc("title")); + print(Query.orderRandom()); print(Query.cursorAfter("my_movie_id")); print(Query.cursorBefore("my_movie_id")); print(Query.limit(50)); @@ -246,6 +247,33 @@ void main() async { print(ID.unique()); print(ID.custom('custom_id')); + // Operator helper tests + print(Operator.increment(1)); + print(Operator.increment(5, 100)); + print(Operator.decrement(1)); + 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.arrayAppend(["item1", "item2"])); + print(Operator.arrayPrepend(["first", "second"])); + print(Operator.arrayInsert(0, "newItem")); + print(Operator.arrayRemove("oldItem")); + print(Operator.arrayUnique()); + print(Operator.arrayIntersect(["a", "b", "c"])); + print(Operator.arrayDiff(["x", "y"])); + print(Operator.arrayFilter(Condition.equal, "test")); + print(Operator.stringConcat("suffix")); + print(Operator.stringReplace("old", "new")); + print(Operator.toggle()); + print(Operator.dateAddDays(7)); + print(Operator.dateSubDays(3)); + print(Operator.dateSetNow()); + response = await general.headers(); print(response.result); } diff --git a/tests/languages/go/tests.go b/tests/languages/go/tests.go index cd619df1d3..1b4dc13644 100644 --- a/tests/languages/go/tests.go +++ b/tests/languages/go/tests.go @@ -9,6 +9,7 @@ import ( "github.com/repoowner/sdk-for-go/client" "github.com/repoowner/sdk-for-go/file" "github.com/repoowner/sdk-for-go/id" + "github.com/repoowner/sdk-for-go/operator" "github.com/repoowner/sdk-for-go/permission" "github.com/repoowner/sdk-for-go/query" "github.com/repoowner/sdk-for-go/role" @@ -144,6 +145,9 @@ func testGeneralService(client client.Client, stringInArray []string) { // Test Id Helpers testIdHelpers() + // Test Operator Helpers + testOperatorHelpers() + // Final test headersResponse, err := general.Headers() if err != nil { @@ -202,6 +206,7 @@ func testQueries() { fmt.Println(query.Select([]interface{}{"name", "age"})) fmt.Println(query.OrderAsc("title")) fmt.Println(query.OrderDesc("title")) + fmt.Println(query.OrderRandom()) fmt.Println(query.CursorAfter("my_movie_id")) fmt.Println(query.CursorBefore("my_movie_id")) fmt.Println(query.Limit(50)) @@ -273,3 +278,31 @@ func testIdHelpers() { fmt.Println(id.Unique()) fmt.Println(id.Custom("custom_id")) } + +func testOperatorHelpers() { + fmt.Println(operator.Increment(1)) + fmt.Println(operator.Increment(5, 100)) + fmt.Println(operator.Decrement(1)) + fmt.Println(operator.Decrement(3, 0)) + fmt.Println(operator.Multiply(2)) + fmt.Println(operator.Multiply(3, 1000)) + fmt.Println(operator.Divide(2)) + fmt.Println(operator.Divide(4, 1)) + fmt.Println(operator.Modulo(5)) + fmt.Println(operator.Power(2)) + fmt.Println(operator.Power(3, 100)) + fmt.Println(operator.ArrayAppend([]interface{}{"item1", "item2"})) + fmt.Println(operator.ArrayPrepend([]interface{}{"first", "second"})) + fmt.Println(operator.ArrayInsert(0, "newItem")) + fmt.Println(operator.ArrayRemove("oldItem")) + fmt.Println(operator.ArrayUnique()) + fmt.Println(operator.ArrayIntersect([]interface{}{"a", "b", "c"})) + fmt.Println(operator.ArrayDiff([]interface{}{"x", "y"})) + fmt.Println(operator.ArrayFilter(operator.ConditionEqual, "test")) + fmt.Println(operator.StringConcat("suffix")) + fmt.Println(operator.StringReplace("old", "new")) + fmt.Println(operator.Toggle()) + fmt.Println(operator.DateAddDays(7)) + fmt.Println(operator.DateSubDays(3)) + fmt.Println(operator.DateSetNow()) +} diff --git a/tests/languages/kotlin/Tests.kt b/tests/languages/kotlin/Tests.kt index e5bd10bf50..f8b13d735b 100644 --- a/tests/languages/kotlin/Tests.kt +++ b/tests/languages/kotlin/Tests.kt @@ -5,6 +5,8 @@ import io.appwrite.Permission import io.appwrite.Role import io.appwrite.ID import io.appwrite.Query +import io.appwrite.Operator +import io.appwrite.Condition import io.appwrite.enums.MockType import io.appwrite.exceptions.AppwriteException import io.appwrite.extensions.fromJson @@ -170,6 +172,7 @@ class ServiceTest { writeToFile(Query.select(listOf("name", "age"))) writeToFile(Query.orderAsc("title")) writeToFile(Query.orderDesc("title")) + writeToFile(Query.orderRandom()) writeToFile(Query.cursorAfter("my_movie_id")) writeToFile(Query.cursorBefore("my_movie_id")) writeToFile(Query.limit(50)) @@ -233,6 +236,33 @@ class ServiceTest { writeToFile(ID.unique()) writeToFile(ID.custom("custom_id")) + // Operator helper tests + writeToFile(Operator.increment(1)) + writeToFile(Operator.increment(5, 100)) + writeToFile(Operator.decrement(1)) + writeToFile(Operator.decrement(3, 0)) + writeToFile(Operator.multiply(2)) + writeToFile(Operator.multiply(3, 1000)) + writeToFile(Operator.divide(2)) + writeToFile(Operator.divide(4, 1)) + writeToFile(Operator.modulo(5)) + writeToFile(Operator.power(2)) + writeToFile(Operator.power(3, 100)) + writeToFile(Operator.arrayAppend(listOf("item1", "item2"))) + writeToFile(Operator.arrayPrepend(listOf("first", "second"))) + writeToFile(Operator.arrayInsert(0, "newItem")) + writeToFile(Operator.arrayRemove("oldItem")) + writeToFile(Operator.arrayUnique()) + writeToFile(Operator.arrayIntersect(listOf("a", "b", "c"))) + writeToFile(Operator.arrayDiff(listOf("x", "y"))) + writeToFile(Operator.arrayFilter(Condition.EQUAL, "test")) + writeToFile(Operator.stringConcat("suffix")) + writeToFile(Operator.stringReplace("old", "new")) + writeToFile(Operator.toggle()) + writeToFile(Operator.dateAddDays(7)) + writeToFile(Operator.dateSubDays(3)) + writeToFile(Operator.dateSetNow()) + mock = general.headers() writeToFile(mock.result) } diff --git a/tests/languages/node/test.js b/tests/languages/node/test.js index 58adcc6f97..ee0fd703f5 100644 --- a/tests/languages/node/test.js +++ b/tests/languages/node/test.js @@ -1,9 +1,11 @@ -const { - Client, +const { + Client, Permission, Query, Role, ID, + Operator, + Condition, MockType, Foo, Bar, @@ -254,6 +256,7 @@ async function start() { console.log(Query.select(["name", "age"])); console.log(Query.orderAsc("title")); console.log(Query.orderDesc("title")); + console.log(Query.orderRandom()); console.log(Query.cursorAfter("my_movie_id")); console.log(Query.cursorBefore("my_movie_id")); console.log(Query.limit(50)); @@ -323,6 +326,33 @@ async function start() { console.log(ID.unique()); console.log(ID.custom('custom_id')); + // Operator helper tests + console.log(Operator.increment(1)); + console.log(Operator.increment(5, 100)); + console.log(Operator.decrement(1)); + console.log(Operator.decrement(3, 0)); + console.log(Operator.multiply(2)); + console.log(Operator.multiply(3, 1000)); + console.log(Operator.divide(2)); + console.log(Operator.divide(4, 1)); + console.log(Operator.modulo(5)); + console.log(Operator.power(2)); + console.log(Operator.power(3, 100)); + console.log(Operator.arrayAppend(["item1", "item2"])); + console.log(Operator.arrayPrepend(["first", "second"])); + console.log(Operator.arrayInsert(0, "newItem")); + console.log(Operator.arrayRemove("oldItem")); + console.log(Operator.arrayUnique()); + console.log(Operator.arrayIntersect(["a", "b", "c"])); + console.log(Operator.arrayDiff(["x", "y"])); + console.log(Operator.arrayFilter(Condition.Equal, "test")); + console.log(Operator.stringConcat("suffix")); + console.log(Operator.stringReplace("old", "new")); + console.log(Operator.toggle()); + console.log(Operator.dateAddDays(7)); + console.log(Operator.dateSubDays(3)); + console.log(Operator.dateSetNow()); + response = await general.headers(); console.log(response.result); } diff --git a/tests/languages/php/test.php b/tests/languages/php/test.php index ecbcbdcd4f..ba0d4ffe38 100644 --- a/tests/languages/php/test.php +++ b/tests/languages/php/test.php @@ -7,6 +7,7 @@ include __DIR__ . '/../../sdks/php/src/Appwrite/Permission.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/Role.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/ID.php'; +include __DIR__ . '/../../sdks/php/src/Appwrite/Operator.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/AppwriteException.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/Enums/MockType.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/Services/Foo.php'; @@ -20,6 +21,8 @@ use Appwrite\Permission; use Appwrite\Role; use Appwrite\ID; +use Appwrite\Operator; +use Appwrite\Condition; use Appwrite\Enums\MockType; use Appwrite\Services\Bar; use Appwrite\Services\Foo; @@ -144,6 +147,7 @@ echo Query::select(['name', 'age']) . "\n"; echo Query::orderAsc('title') . "\n"; echo Query::orderDesc('title') . "\n"; +echo Query::orderRandom() . "\n"; echo Query::cursorAfter('my_movie_id') . "\n"; echo Query::cursorBefore('my_movie_id') . "\n"; echo Query::limit(50) . "\n"; @@ -213,5 +217,32 @@ echo ID::unique() . "\n"; echo ID::custom('custom_id') . "\n"; +// Operator helper tests +echo Operator::increment() . "\n"; +echo Operator::increment(5, 100) . "\n"; +echo Operator::decrement() . "\n"; +echo Operator::decrement(3, 0) . "\n"; +echo Operator::multiply(2) . "\n"; +echo Operator::multiply(3, 1000) . "\n"; +echo Operator::divide(2) . "\n"; +echo Operator::divide(4, 1) . "\n"; +echo Operator::modulo(5) . "\n"; +echo Operator::power(2) . "\n"; +echo Operator::power(3, 100) . "\n"; +echo Operator::arrayAppend(['item1', 'item2']) . "\n"; +echo Operator::arrayPrepend(['first', 'second']) . "\n"; +echo Operator::arrayInsert(0, 'newItem') . "\n"; +echo Operator::arrayRemove('oldItem') . "\n"; +echo Operator::arrayUnique() . "\n"; +echo Operator::arrayIntersect(['a', 'b', 'c']) . "\n"; +echo Operator::arrayDiff(['x', 'y']) . "\n"; +echo Operator::arrayFilter(Condition::Equal, 'test') . "\n"; +echo Operator::stringConcat('suffix') . "\n"; +echo Operator::stringReplace('old', 'new') . "\n"; +echo Operator::toggle() . "\n"; +echo Operator::dateAddDays(7) . "\n"; +echo Operator::dateSubDays(3) . "\n"; +echo Operator::dateSetNow() . "\n"; + $response = $general->headers(); echo "{$response['result']}\n"; diff --git a/tests/languages/python/tests.py b/tests/languages/python/tests.py index cb94c7bc81..f43a2b7a1e 100644 --- a/tests/languages/python/tests.py +++ b/tests/languages/python/tests.py @@ -8,6 +8,7 @@ from appwrite.permission import Permission from appwrite.role import Role from appwrite.id import ID +from appwrite.operator import Operator, Condition from appwrite.enums.mock_type import MockType import os.path @@ -129,6 +130,7 @@ print(Query.select(["name", "age"])) print(Query.order_asc("title")) print(Query.order_desc("title")) +print(Query.order_random()) print(Query.cursor_after("my_movie_id")) print(Query.cursor_before("my_movie_id")) print(Query.limit(50)) @@ -196,5 +198,32 @@ print(ID.unique()) print(ID.custom('custom_id')) +# Operator helper tests +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']) \ No newline at end of file diff --git a/tests/languages/ruby/tests.rb b/tests/languages/ruby/tests.rb index 8188269f7b..c932edbca7 100644 --- a/tests/languages/ruby/tests.rb +++ b/tests/languages/ruby/tests.rb @@ -141,6 +141,7 @@ puts Query.select(["name", "age"]) puts Query.order_asc("title") puts Query.order_desc("title") +puts Query.order_random() puts Query.cursor_after("my_movie_id") puts Query.cursor_before("my_movie_id") puts Query.limit(50) @@ -204,5 +205,32 @@ puts ID.unique() puts ID.custom('custom_id') +# Operator helper tests +puts Operator.increment(1) +puts Operator.increment(5, 100) +puts Operator.decrement(1) +puts Operator.decrement(3, 0) +puts Operator.multiply(2) +puts Operator.multiply(3, 1000) +puts Operator.divide(2) +puts Operator.divide(4, 1) +puts Operator.modulo(5) +puts Operator.power(2) +puts Operator.power(3, 100) +puts Operator.array_append(["item1", "item2"]) +puts Operator.array_prepend(["first", "second"]) +puts Operator.array_insert(0, "newItem") +puts Operator.array_remove("oldItem") +puts Operator.array_unique() +puts Operator.array_intersect(["a", "b", "c"]) +puts Operator.array_diff(["x", "y"]) +puts Operator.array_filter(Condition::EQUAL, "test") +puts Operator.string_concat("suffix") +puts Operator.string_replace("old", "new") +puts Operator.toggle() +puts Operator.date_add_days(7) +puts Operator.date_sub_days(3) +puts Operator.date_set_now() + response = general.headers() puts response.result \ No newline at end of file diff --git a/tests/languages/swift/Tests.swift b/tests/languages/swift/Tests.swift index 317ba7d73d..8719dc6466 100644 --- a/tests/languages/swift/Tests.swift +++ b/tests/languages/swift/Tests.swift @@ -167,6 +167,7 @@ class Tests: XCTestCase { print(Query.select(["name", "age"])) print(Query.orderAsc("title")) print(Query.orderDesc("title")) + print(Query.orderRandom()) print(Query.cursorAfter("my_movie_id")) print(Query.cursorBefore("my_movie_id")) print(Query.limit(50)) @@ -234,6 +235,33 @@ class Tests: XCTestCase { print(ID.unique()) print(ID.custom("custom_id")) + // Operator helper tests + print(Operator.increment(1)) + print(Operator.increment(5, max: 100)) + print(Operator.decrement(1)) + print(Operator.decrement(3, min: 0)) + print(Operator.multiply(2)) + print(Operator.multiply(3, max: 1000)) + print(Operator.divide(2)) + print(Operator.divide(4, min: 1)) + print(Operator.modulo(5)) + print(Operator.power(2)) + print(Operator.power(3, max: 100)) + print(Operator.arrayAppend(["item1", "item2"])) + print(Operator.arrayPrepend(["first", "second"])) + print(Operator.arrayInsert(0, value: "newItem")) + print(Operator.arrayRemove("oldItem")) + print(Operator.arrayUnique()) + print(Operator.arrayIntersect(["a", "b", "c"])) + print(Operator.arrayDiff(["x", "y"])) + print(Operator.arrayFilter(Condition.equal, value: "test")) + print(Operator.stringConcat("suffix")) + print(Operator.stringReplace("old", "new")) + print(Operator.toggle()) + print(Operator.dateAddDays(7)) + print(Operator.dateSubDays(3)) + print(Operator.dateSetNow()) + mock = try await general.headers() print(mock.result) } catch { diff --git a/tests/languages/unity/Tests.asmdef b/tests/languages/unity/Tests.asmdef new file mode 100644 index 0000000000..4df3d5cb60 --- /dev/null +++ b/tests/languages/unity/Tests.asmdef @@ -0,0 +1,23 @@ +{ + "name": "Tests", + "rootNamespace": "AppwriteTests", + "references": [ + "UnityEngine.TestRunner", + "UnityEditor.TestRunner", + "Appwrite", + "Appwrite.Core", + "UniTask", + "endel.nativewebsocket" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/tests/languages/unity/Tests.cs b/tests/languages/unity/Tests.cs new file mode 100644 index 0000000000..ef94beb8f8 --- /dev/null +++ b/tests/languages/unity/Tests.cs @@ -0,0 +1,316 @@ +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.TestTools; + +using Appwrite; +using Appwrite.Models; +using Appwrite.Enums; +using Appwrite.Services; +using NUnit.Framework; + +namespace AppwriteTests +{ + public class Tests + { + [SetUp] + public void Setup() + { + Debug.Log("Test Started"); + } + + [UnityTest] + public IEnumerator Test1() + { + var task = RunAsyncTest(); + yield return new WaitUntil(() => task.IsCompleted); + + if (task.Exception != null) + { + Debug.LogError($"Test failed with exception: {task.Exception}"); + throw task.Exception; + } + } + + private async Task RunAsyncTest() + { + var client = new Client() + .SetProject("123456") + .AddHeader("Origin", "http://localhost") + .SetSelfSigned(true); + + var foo = new Foo(client); + var bar = new Bar(client); + var general = new General(client); + + client.SetProject("console"); + client.SetEndPointRealtime("wss://cloud.appwrite.io/v1"); + + // Create GameObject for Realtime MonoBehaviour + var realtimeObject = new GameObject("RealtimeTest"); + var realtime = realtimeObject.AddComponent(); + realtime.Initialize(client); + + string realtimeResponse = "No realtime message received within timeout"; + RealtimeSubscription subscription = null; + subscription = realtime.Subscribe(new [] { "tests" }, (eventData) => + { + Debug.Log($"[Test] Realtime callback invoked! Payload count: {eventData.Payload?.Count}"); + if (eventData.Payload != null && eventData.Payload.TryGetValue("response", out var value)) + { + Debug.Log($"[Test] Found response value: {value}"); + realtimeResponse = value.ToString(); + Debug.Log($"[Test] Updated realtimeResponse to: {realtimeResponse}"); + } + else + { + Debug.Log("[Test] No 'response' key found in payload"); + } + subscription?.Close(); + }); + + await Task.Delay(5000); + + // Ping test + client.SetProject("123456"); + var ping = await client.Ping(); + Debug.Log(ping); + + // Reset a project for other tests + client.SetProject("console"); + + Mock mock; + // Foo Tests + mock = await foo.Get("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + mock = await foo.Post("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + mock = await foo.Put("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + mock = await foo.Patch("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + mock = await foo.Delete("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + // Bar Tests + mock = await bar.Get("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + mock = await bar.Post("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + mock = await bar.Put("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + mock = await bar.Patch("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + mock = await bar.Delete("string", 123, new List() { "string in array" }); + Debug.Log(mock.Result); + + // General Tests + var result = await general.Redirect(); + Debug.Log((result as Dictionary)["result"]); + + mock = await general.Upload("string", 123, new List() { "string in array" }, InputFile.FromPath("../../resources/file.png")); + Debug.Log(mock.Result); + + mock = await general.Upload("string", 123, new List() { "string in array" }, InputFile.FromPath("../../resources/large_file.mp4")); + Debug.Log(mock.Result); + + var info = new FileInfo("../../resources/file.png"); + mock = await general.Upload("string", 123, new List() { "string in array" }, InputFile.FromStream(info.OpenRead(), "file.png", "image/png")); + Debug.Log(mock.Result); + + info = new FileInfo("../../resources/large_file.mp4"); + mock = await general.Upload("string", 123, new List() { "string in array" }, InputFile.FromStream(info.OpenRead(), "large_file.mp4", "video/mp4")); + Debug.Log(mock.Result); + + // Download test + var downloadResult = await general.Download(); + if (downloadResult != null) + { + var downloadString = System.Text.Encoding.UTF8.GetString(downloadResult); + Debug.Log(downloadString); + } + + mock = await general.Enum(MockType.First); + Debug.Log(mock.Result); + + try + { + await general.Error400(); + } + catch (AppwriteException e) + { + Debug.Log(e.Message); + Debug.Log(e.Response); + } + + try + { + await general.Error500(); + } + catch (AppwriteException e) + { + Debug.Log(e.Message); + Debug.Log(e.Response); + } + + try + { + await general.Error502(); + } + catch (AppwriteException e) + { + Debug.Log(e.Message); + Debug.Log(e.Response); + } + + try + { + client.SetEndpoint("htp://cloud.appwrite.io/v1"); + } + catch (AppwriteException e) + { + Debug.Log(e.Message); + } + + await general.Empty(); + + await Task.Delay(5000); + Debug.Log(realtimeResponse); + + // Cookie tests + mock = await general.SetCookie(); + Debug.Log(mock.Result); + + mock = await general.GetCookie(); + Debug.Log(mock.Result); + + // Query helper tests + Debug.Log(Query.Equal("released", new List { true })); + Debug.Log(Query.Equal("title", new List { "Spiderman", "Dr. Strange" })); + Debug.Log(Query.NotEqual("title", "Spiderman")); + Debug.Log(Query.LessThan("releasedYear", 1990)); + Debug.Log(Query.GreaterThan("releasedYear", 1990)); + Debug.Log(Query.Search("name", "john")); + Debug.Log(Query.IsNull("name")); + Debug.Log(Query.IsNotNull("name")); + Debug.Log(Query.Between("age", 50, 100)); + Debug.Log(Query.Between("age", 50.5, 100.5)); + Debug.Log(Query.Between("name", "Anna", "Brad")); + Debug.Log(Query.StartsWith("name", "Ann")); + Debug.Log(Query.EndsWith("name", "nne")); + Debug.Log(Query.Select(new List { "name", "age" })); + Debug.Log(Query.OrderAsc("title")); + Debug.Log(Query.OrderDesc("title")); + Debug.Log(Query.OrderRandom()); + Debug.Log(Query.CursorAfter("my_movie_id")); + Debug.Log(Query.CursorBefore("my_movie_id")); + Debug.Log(Query.Limit(50)); + Debug.Log(Query.Offset(20)); + Debug.Log(Query.Contains("title", "Spider")); + Debug.Log(Query.Contains("labels", "first")); + + // New query methods + Debug.Log(Query.NotContains("title", "Spider")); + Debug.Log(Query.NotSearch("name", "john")); + Debug.Log(Query.NotBetween("age", 50, 100)); + Debug.Log(Query.NotStartsWith("name", "Ann")); + Debug.Log(Query.NotEndsWith("name", "nne")); + Debug.Log(Query.CreatedBefore("2023-01-01")); + Debug.Log(Query.CreatedAfter("2023-01-01")); + Debug.Log(Query.CreatedBetween("2023-01-01", "2023-12-31")); + Debug.Log(Query.UpdatedBefore("2023-01-01")); + Debug.Log(Query.UpdatedAfter("2023-01-01")); + Debug.Log(Query.UpdatedBetween("2023-01-01", "2023-12-31")); + + // Spatial Distance query tests + Debug.Log(Query.DistanceEqual("location", new List { new List { 40.7128, -74 }, new List { 40.7128, -74 } }, 1000)); + Debug.Log(Query.DistanceEqual("location", new List { 40.7128, -74 }, 1000, true)); + Debug.Log(Query.DistanceNotEqual("location", new List { 40.7128, -74 }, 1000)); + Debug.Log(Query.DistanceNotEqual("location", new List { 40.7128, -74 }, 1000, true)); + Debug.Log(Query.DistanceGreaterThan("location", new List { 40.7128, -74 }, 1000)); + Debug.Log(Query.DistanceGreaterThan("location", new List { 40.7128, -74 }, 1000, true)); + Debug.Log(Query.DistanceLessThan("location", new List { 40.7128, -74 }, 1000)); + Debug.Log(Query.DistanceLessThan("location", new List { 40.7128, -74 }, 1000, true)); + + // Spatial query tests + Debug.Log(Query.Intersects("location", new List { 40.7128, -74 })); + Debug.Log(Query.NotIntersects("location", new List { 40.7128, -74 })); + Debug.Log(Query.Crosses("location", new List { 40.7128, -74 })); + Debug.Log(Query.NotCrosses("location", new List { 40.7128, -74 })); + Debug.Log(Query.Overlaps("location", new List { 40.7128, -74 })); + Debug.Log(Query.NotOverlaps("location", new List { 40.7128, -74 })); + Debug.Log(Query.Touches("location", new List { 40.7128, -74 })); + Debug.Log(Query.NotTouches("location", new List { 40.7128, -74 })); + Debug.Log(Query.Contains("location", new List { new List { 40.7128, -74 }, new List { 40.7128, -74 } })); + Debug.Log(Query.NotContains("location", new List { new List { 40.7128, -74 }, new List { 40.7128, -74 } })); + Debug.Log(Query.Equal("location", new List { new List { 40.7128, -74 }, new List { 40.7128, -74 } })); + Debug.Log(Query.NotEqual("location", new List { new List { 40.7128, -74 }, new List { 40.7128, -74 } })); + + Debug.Log(Query.Or(new List { Query.Equal("released", true), Query.LessThan("releasedYear", 1990) })); + Debug.Log(Query.And(new List { Query.Equal("released", false), Query.GreaterThan("releasedYear", 2015) })); + + // Permission & Roles helper tests + Debug.Log(Permission.Read(Role.Any())); + Debug.Log(Permission.Write(Role.User(ID.Custom("userid")))); + Debug.Log(Permission.Create(Role.Users())); + Debug.Log(Permission.Update(Role.Guests())); + Debug.Log(Permission.Delete(Role.Team("teamId", "owner"))); + Debug.Log(Permission.Delete(Role.Team("teamId"))); + Debug.Log(Permission.Create(Role.Member("memberId"))); + Debug.Log(Permission.Update(Role.Users("verified"))); + Debug.Log(Permission.Update(Role.User(ID.Custom("userid"), "unverified"))); + Debug.Log(Permission.Create(Role.Label("admin"))); + + // ID helper tests + Debug.Log(ID.Unique()); + Debug.Log(ID.Custom("custom_id")); + + // Operator helper tests + Debug.Log(Operator.Increment(1)); + Debug.Log(Operator.Increment(5, 100)); + Debug.Log(Operator.Decrement(1)); + Debug.Log(Operator.Decrement(3, 0)); + Debug.Log(Operator.Multiply(2)); + Debug.Log(Operator.Multiply(3, 1000)); + Debug.Log(Operator.Divide(2)); + Debug.Log(Operator.Divide(4, 1)); + Debug.Log(Operator.Modulo(5)); + Debug.Log(Operator.Power(2)); + Debug.Log(Operator.Power(3, 100)); + Debug.Log(Operator.ArrayAppend(new List { "item1", "item2" })); + Debug.Log(Operator.ArrayPrepend(new List { "first", "second" })); + Debug.Log(Operator.ArrayInsert(0, "newItem")); + Debug.Log(Operator.ArrayRemove("oldItem")); + Debug.Log(Operator.ArrayUnique()); + Debug.Log(Operator.ArrayIntersect(new List { "a", "b", "c" })); + Debug.Log(Operator.ArrayDiff(new List { "x", "y" })); + Debug.Log(Operator.ArrayFilter(Condition.Equal, "test")); + Debug.Log(Operator.StringConcat("suffix")); + Debug.Log(Operator.StringReplace("old", "new")); + Debug.Log(Operator.Toggle()); + Debug.Log(Operator.DateAddDays(7)); + Debug.Log(Operator.DateSubDays(3)); + Debug.Log(Operator.DateSetNow()); + + mock = await general.Headers(); + Debug.Log(mock.Result); + + // Cleanup Realtime GameObject + if (realtimeObject) + { + Object.DestroyImmediate(realtimeObject); + } + + } + } +} diff --git a/tests/languages/web/index.html b/tests/languages/web/index.html index 34cbdbc1f0..7e81de9c5e 100644 --- a/tests/languages/web/index.html +++ b/tests/languages/web/index.html @@ -21,7 +21,7 @@ let responseRealtime = 'Realtime failed!'; // Init SDK - const { Client, Foo, Bar, General, Query, Permission, Role, ID, MockType } = Appwrite; + const { Client, Foo, Bar, General, Realtime, Query, Permission, Role, ID, Operator, Condition, MockType } = Appwrite; const client = new Client(); const foo = new Foo(client); @@ -35,10 +35,12 @@ // Realtime setup client.setProject('console'); - client.setEndpointRealtime('ws://cloud.appwrite.io/v1'); + client.setEndpointRealtime('wss://cloud.appwrite.io/v1'); - client.subscribe('tests', event => { - responseRealtime = event.payload.response; + const realtime = new Realtime(client); + + await realtime.subscribe(['tests'], message => { + responseRealtime = message.payload.response; }); // Foo @@ -246,6 +248,7 @@ console.log(Query.select(["name", "age"])); console.log(Query.orderAsc("title")); console.log(Query.orderDesc("title")); + console.log(Query.orderRandom()); console.log(Query.cursorAfter("my_movie_id")); console.log(Query.cursorBefore("my_movie_id")); console.log(Query.limit(50)); @@ -316,6 +319,33 @@ console.log(ID.unique()); console.log(ID.custom('custom_id')); + // Operator helper tests + console.log(Operator.increment(1)); + console.log(Operator.increment(5, 100)); + console.log(Operator.decrement(1)); + console.log(Operator.decrement(3, 0)); + console.log(Operator.multiply(2)); + console.log(Operator.multiply(3, 1000)); + console.log(Operator.divide(2)); + console.log(Operator.divide(4, 1)); + console.log(Operator.modulo(5)); + console.log(Operator.power(2)); + console.log(Operator.power(3, 100)); + console.log(Operator.arrayAppend(["item1", "item2"])); + console.log(Operator.arrayPrepend(["first", "second"])); + console.log(Operator.arrayInsert(0, "newItem")); + console.log(Operator.arrayRemove("oldItem")); + console.log(Operator.arrayUnique()); + console.log(Operator.arrayIntersect(["a", "b", "c"])); + console.log(Operator.arrayDiff(["x", "y"])); + console.log(Operator.arrayFilter(Condition.Equal, "test")); + console.log(Operator.stringConcat("suffix")); + console.log(Operator.stringReplace("old", "new")); + console.log(Operator.toggle()); + console.log(Operator.dateAddDays(7)); + console.log(Operator.dateSubDays(3)); + console.log(Operator.dateSetNow()); + response = await general.headers(); console.log(response.result); diff --git a/tests/languages/web/node.js b/tests/languages/web/node.js index c2ef1582aa..f4c7bed43a 100644 --- a/tests/languages/web/node.js +++ b/tests/languages/web/node.js @@ -1,4 +1,4 @@ -const { Client, Foo, Bar, General, Query, Permission, Role, ID, MockType } = require('./dist/cjs/sdk.js'); +const { Client, Foo, Bar, General, Query, Permission, Role, ID, Operator, Condition, MockType } = require('./dist/cjs/sdk.js'); async function start() { let response; @@ -179,6 +179,7 @@ async function start() { console.log(Query.select(["name", "age"])); console.log(Query.orderAsc("title")); console.log(Query.orderDesc("title")); + console.log(Query.orderRandom()); console.log(Query.cursorAfter("my_movie_id")); console.log(Query.cursorBefore("my_movie_id")); console.log(Query.limit(50)); @@ -249,6 +250,33 @@ async function start() { console.log(ID.unique()); console.log(ID.custom('custom_id')); + // Operator helper tests + console.log(Operator.increment(1)); + console.log(Operator.increment(5, 100)); + console.log(Operator.decrement(1)); + console.log(Operator.decrement(3, 0)); + console.log(Operator.multiply(2)); + console.log(Operator.multiply(3, 1000)); + console.log(Operator.divide(2)); + console.log(Operator.divide(4, 1)); + console.log(Operator.modulo(5)); + console.log(Operator.power(2)); + console.log(Operator.power(3, 100)); + console.log(Operator.arrayAppend(["item1", "item2"])); + console.log(Operator.arrayPrepend(["first", "second"])); + console.log(Operator.arrayInsert(0, "newItem")); + console.log(Operator.arrayRemove("oldItem")); + console.log(Operator.arrayUnique()); + console.log(Operator.arrayIntersect(["a", "b", "c"])); + console.log(Operator.arrayDiff(["x", "y"])); + console.log(Operator.arrayFilter(Condition.Equal, "test")); + console.log(Operator.stringConcat("suffix")); + console.log(Operator.stringReplace("old", "new")); + console.log(Operator.toggle()); + console.log(Operator.dateAddDays(7)); + console.log(Operator.dateSubDays(3)); + console.log(Operator.dateSetNow()); + response = await general.headers(); console.log(response.result); } diff --git a/tests/resources/spec.json b/tests/resources/spec.json index 86b1670747..f1f0b500d2 100644 --- a/tests/resources/spec.json +++ b/tests/resources/spec.json @@ -53,6 +53,12 @@ "demo": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..." } }, + "Session": { + "type": "apiKey", + "name": "X-Appwrite-Session", + "description": "The user session to authenticate with", + "in": "header" + }, "Locale": { "type": "apiKey", "name": "X-Appwrite-Locale", @@ -1961,6 +1967,19 @@ "type": "string", "description": "Result message.", "x-example": "Success" + }, + "status": { + "type": "string", + "description": "Mock status. Possible values: `active`, `inactive`, `pending`, `completed`, or `cancelled`", + "x-example": "active", + "enum": [ + "active", + "inactive", + "pending", + "completed", + "cancelled" + ], + "x-enum-name": "MockStatus" } }, "required": [ diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000..9431a635b1 --- /dev/null +++ b/uv.lock @@ -0,0 +1,3 @@ +version = 1 +revision = 3 +requires-python = ">=3.11"