diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 06f10a51a..e487448c1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -100,3 +100,23 @@ jobs: - name: Lint run: composer lint + + specs: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP with PECL extension + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: curl + + - name: Install + run: composer install + + - name: Validate specs + run: composer test tests/SpecsTest.php + \ No newline at end of file diff --git a/composer.json b/composer.json index 2f2ee18f1..e0b8eb2ff 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,8 @@ "ext-mbstring": "*", "ext-json": "*", "twig/twig": "3.14.*", - "matthiasmullie/minify": "1.3.*" + "matthiasmullie/minify": "1.3.*", + "utopia-php/fetch": "^0.2.1" }, "require-dev": { "phpunit/phpunit": "11.*", diff --git a/composer.lock b/composer.lock index 912f91304..74e116a1e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9283e0faa88dc724e482a15d92771eb7", + "content-hash": "542c1fbb222c159cfc8e1cfb32d8f757", "packages": [ { "name": "matthiasmullie/minify", @@ -510,6 +510,45 @@ } ], "time": "2024-09-09T17:55:12+00:00" + }, + { + "name": "utopia-php/fetch", + "version": "0.2.1", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/fetch.git", + "reference": "1423c0ee3eef944d816ca6e31706895b585aea82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/fetch/zipball/1423c0ee3eef944d816ca6e31706895b585aea82", + "reference": "1423c0ee3eef944d816ca6e31706895b585aea82", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "^1.5.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Fetch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple library that provides an interface for making HTTP Requests.", + "support": { + "issues": "https://github.com/utopia-php/fetch/issues", + "source": "https://github.com/utopia-php/fetch/tree/0.2.1" + }, + "time": "2024-03-18T11:50:59+00:00" } ], "packages-dev": [ @@ -2444,16 +2483,16 @@ }, { "name": "symfony/console", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111" + "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/1eed7af6961d763e7832e874d7f9b21c3ea9c111", - "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111", + "url": "https://api.github.com/repos/symfony/console/zipball/0fa539d12b3ccf068a722bbbffa07ca7079af9ee", + "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee", "shasum": "" }, "require": { @@ -2517,7 +2556,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.1.4" + "source": "https://github.com/symfony/console/tree/v7.1.5" }, "funding": [ { @@ -2533,7 +2572,7 @@ "type": "tidelift" } ], - "time": "2024-08-15T22:48:53+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/polyfill-intl-grapheme", @@ -2696,16 +2735,16 @@ }, { "name": "symfony/process", - "version": "v7.1.3", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca" + "reference": "5c03ee6369281177f07f7c68252a280beccba847" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/7f2f542c668ad6c313dc4a5e9c3321f733197eca", - "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca", + "url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847", + "reference": "5c03ee6369281177f07f7c68252a280beccba847", "shasum": "" }, "require": { @@ -2737,7 +2776,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.1.3" + "source": "https://github.com/symfony/process/tree/v7.1.5" }, "funding": [ { @@ -2753,7 +2792,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:44:47+00:00" + "time": "2024-09-19T21:48:23+00:00" }, { "name": "symfony/service-contracts", @@ -2840,16 +2879,16 @@ }, { "name": "symfony/string", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b" + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/6cd670a6d968eaeb1c77c2e76091c45c56bc367b", - "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b", + "url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306", + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306", "shasum": "" }, "require": { @@ -2907,7 +2946,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.4" + "source": "https://github.com/symfony/string/tree/v7.1.5" }, "funding": [ { @@ -2923,7 +2962,7 @@ "type": "tidelift" } ], - "time": "2024-08-12T09:59:40+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "theseer/tokenizer", diff --git a/mock-server/app/http.php b/mock-server/app/http.php index 0b779d23c..742c96a4b 100644 --- a/mock-server/app/http.php +++ b/mock-server/app/http.php @@ -325,6 +325,18 @@ $chunkSize = 5 * 1024 * 1024; // 5MB + if ($x != 'string') { + throw new Exception(Exception::GENERAL_MOCK, 'Wrong string value: ' . $x . ', expected: string'); + } + + if ($y !== 123) { + throw new Exception(Exception::GENERAL_MOCK, 'Wrong numeric value: ' . $y . ', expected: 123'); + } + + if ($z[0] !== 'string in array' || \count($z) !== 1) { + throw new Exception(Exception::GENERAL_MOCK, 'Wrong array value: ' . \json_encode($z) . ', expected: ["string in array"]'); + } + if (!empty($contentRange)) { $start = $request->getContentRangeStart(); $end = $request->getContentRangeEnd(); @@ -373,15 +385,16 @@ $file['size'] = (\is_array($file['size'])) ? $file['size'][0] : $file['size']; if ($file['name'] !== 'file.png') { - throw new Exception(Exception::GENERAL_MOCK, 'Wrong file name'); + throw new Exception(Exception::GENERAL_MOCK, 'Wrong file name: ' . $file['name'] . ', expected: file.png'); } if ($file['size'] !== 38756) { - throw new Exception(Exception::GENERAL_MOCK, 'Wrong file size'); + throw new Exception(Exception::GENERAL_MOCK, 'Wrong file size: ' . $file['size'] . ', expected: 38756'); } - if (\md5(\file_get_contents($file['tmp_name'])) !== 'd80e7e6999a3eb2ae0d631a96fe135a4') { - throw new Exception(Exception::GENERAL_MOCK, 'Wrong file uploaded'); + $hash = \md5(\file_get_contents($file['tmp_name'])); + if ($hash !== 'd80e7e6999a3eb2ae0d631a96fe135a4') { + throw new Exception(Exception::GENERAL_MOCK, 'Wrong file uploaded: ' . $hash . ', expected: d80e7e6999a3eb2ae0d631a96fe135a4'); } } }); @@ -410,6 +423,41 @@ ]); }); +App::post('/v1/mock/tests/general/multipart-echo') + ->desc('Multipart echo') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'general') + ->label('sdk.method', 'multipartEcho') + ->label('sdk.description', 'Echo a multipart request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_MULTIPART) + ->label('sdk.response.model', Response::MODEL_MULTIPART) + ->label('sdk.mock', true) + ->param('body', '', new File(), 'Sample file param', false, [], true) + ->inject('response') + ->inject('request') + ->action(function (string $body, Response $response, Request $request) { + if (empty($body)) { + $file = $request->getFiles('body'); + + if (empty($file)) { + $file = $request->getFiles(0); + } + + if (isset($file['tmp_name'])) { + $body = \file_get_contents($file['tmp_name']); + } else { + $body = ''; + } + } + + $response->multipart([ + 'responseBody' => $body + ]); + }); + App::get('/v1/mock/tests/general/redirect') ->desc('Redirect') ->groups(['mock']) diff --git a/mock-server/docker-compose.yml b/mock-server/docker-compose.yml index 36b711ae7..7edc69ad5 100644 --- a/mock-server/docker-compose.yml +++ b/mock-server/docker-compose.yml @@ -1,6 +1,8 @@ services: mockapi: container_name: mockapi + ports: + - 3175:80 build: context: . args: diff --git a/src/SDK/Language/Ruby.php b/src/SDK/Language/Ruby.php index dd3d52bbb..af6f0c9e4 100644 --- a/src/SDK/Language/Ruby.php +++ b/src/SDK/Language/Ruby.php @@ -214,6 +214,8 @@ public function getTypeName(array $parameter, array $spec = []): string self::TYPE_STRING => 'String', self::TYPE_ARRAY => 'Array', self::TYPE_OBJECT => 'Hash', + self::TYPE_FILE => 'Payload', + self::TYPE_PAYLOAD => 'Payload', self::TYPE_BOOLEAN => '', default => $parameter['type'], }; 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 aa633a66a..6f326e5f7 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 @@ -285,6 +285,14 @@ class Client @JvmOverloads constructor( ) } } + it.value is Payload -> { + val payload = it.value as Payload + if (payload.sourceType == "path") { + builder.addFormDataPart(it.key, payload.filename, File(payload.path).asRequestBody()) + } else { + builder.addFormDataPart(it.key, payload.toString()) + } + } else -> { builder.addFormDataPart(it.key, it.value.toString()) } diff --git a/templates/android/library/src/main/java/io/package/models/Payload.kt.twig b/templates/android/library/src/main/java/io/package/models/Payload.kt.twig index 132547a86..fb9ff049d 100644 --- a/templates/android/library/src/main/java/io/package/models/Payload.kt.twig +++ b/templates/android/library/src/main/java/io/package/models/Payload.kt.twig @@ -39,6 +39,8 @@ class Payload private constructor() { } fun toFile(path: String): File { + Files.createDirectories(Paths.get(path).parent); + val file = File(path) file.appendBytes(toBinary()) return file diff --git a/templates/dart/lib/payload.dart.twig b/templates/dart/lib/payload.dart.twig index 84e75bbe4..12d99ae71 100644 --- a/templates/dart/lib/payload.dart.twig +++ b/templates/dart/lib/payload.dart.twig @@ -1,5 +1,6 @@ import 'dart:convert'; import 'src/exception.dart'; +import 'dart:io'; class Payload { late final String? path; @@ -44,6 +45,15 @@ class Payload { } } + /// Create a file from the payload + void toFile(String path) { + if(data == null) { + throw {{spec.title | caseUcfirst}}Exception('`data` is not defined.'); + } + final file = File(path); + file.writeAsBytesSync(data!); + } + /// Create a Payload from binary data factory Payload.fromBinary({ required List data, diff --git a/templates/deno/src/payload.ts.twig b/templates/deno/src/payload.ts.twig index 85d25e291..2a60443fa 100644 --- a/templates/deno/src/payload.ts.twig +++ b/templates/deno/src/payload.ts.twig @@ -1,4 +1,4 @@ -import { basename } from "https://deno.land/std@0.224.0/path/mod.ts"; +import { basename, dirname } from "https://deno.land/std@0.224.0/path/mod.ts"; export class Payload { private data: Uint8Array; @@ -30,6 +30,7 @@ export class Payload { } public async toFile(path: string): Promise { + await Deno.mkdir(dirname(path), { recursive: true }); await Deno.writeFile(path, this.data); } diff --git a/templates/deno/src/services/service.ts.twig b/templates/deno/src/services/service.ts.twig index 810491763..df938d0de 100644 --- a/templates/deno/src/services/service.ts.twig +++ b/templates/deno/src/services/service.ts.twig @@ -25,7 +25,6 @@ {% endfor %} {% endapply %} {% endmacro %} -import { basename } from "https://deno.land/std@0.122.0/path/mod.ts"; import { Service } from '../service.ts'; import { Params, Client } from '../client.ts'; import { Payload } from '../payload.ts'; @@ -63,20 +62,20 @@ export class {{ service.name | caseUcfirst }} extends Service { { super(client); } - {%~ for method in service.methods %} - {%- set generics = _self.get_generics(spec.definitions[method.responseModel], spec, true, true) %} - {%- set generics_return = _self.get_generics_return(spec.definitions[method.responseModel], spec) %} + + {%~ set generics = _self.get_generics(spec.definitions[method.responseModel], spec, true, true) %} + {%~ set generics_return = _self.get_generics_return(spec.definitions[method.responseModel], spec) %} /** * {{ method.title }} * {%~ if method.description %} * {{ method.description}} * - {%- endif %} + {%~ endif %} {%~ for parameter in method.parameters.all%} * @param {{ '{' }}{{ parameter | typeName }}{{ '}' }} {{ parameter.name | caseCamel | escapeKeyword }} - {%- endfor %} + {%~ endfor %} * @throws {AppwriteException} * @returns {Promise} */ @@ -103,7 +102,7 @@ export class {{ service.name | caseUcfirst }} extends Service { payload['{{ parameter.name }}'] = {{ parameter.name | caseCamel | escapeKeyword }}{% if method.consumes[0] == "multipart/form-data" and ( parameter.type != "string" and parameter.type != "array" and parameter.type != "file" ) %}.toString(){% endif %}; } {%~ endfor %} - {%~ if 'multipart/form-data' in method.consumes %} + {%~ if 'multipart/form-data' in method.consumes and method.type == "upload" %} {%~ for parameter in method.parameters.all %} {%~ if parameter.type == 'file' %} @@ -237,7 +236,7 @@ export class {{ service.name | caseUcfirst }} extends Service { 'json' {%~ endif %} ); - {%- endif %} + {%~ endif %} } - {%- endfor %} + {%~ endfor %} } \ No newline at end of file diff --git a/templates/go/models/model.go.twig b/templates/go/models/model.go.twig index eed8394f1..38ed60697 100644 --- a/templates/go/models/model.go.twig +++ b/templates/go/models/model.go.twig @@ -3,7 +3,7 @@ package models import ( "encoding/json" "errors" -{%~ if definition.name | caseLower == 'execution' or definition.name | caseLower == 'multipart' %} +{%~ if definition.name | caseLower == 'execution' or definition.name | caseLower == 'multipart' or definition.name | caseLower == 'multipartecho' %} "github.com/{{sdk.gitUserName}}/sdk-for-go/payload" {%~ endif %} ) diff --git a/templates/go/services/service.go.twig b/templates/go/services/service.go.twig index c2ae226c6..bf2c3c9c3 100644 --- a/templates/go/services/service.go.twig +++ b/templates/go/services/service.go.twig @@ -95,7 +95,7 @@ func (srv *{{ service.name | caseUcfirst }}) {{ method.name | caseUcfirst }}({{ path := "{{ method.path }}" {% endif %} {{include('go/base/params.twig')}} -{% if 'multipart/form-data' in method.consumes and method.name | lower != "createexecution" %} +{% if 'multipart/form-data' in method.consumes and method.type == "upload" %} {{ include('go/base/requests/file.twig') }} {% else %} {{ include('go/base/requests/api.twig') }} 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 b8f393fa3..7988dec5e 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig @@ -241,6 +241,14 @@ class Client @JvmOverloads constructor( ) } } + it.value is Payload -> { + val payload = it.value as Payload + if (payload.sourceType == "path") { + builder.addFormDataPart(it.key, payload.filename, File(payload.path).asRequestBody()) + } else { + builder.addFormDataPart(it.key, payload.toString()) + } + } else -> { builder.addFormDataPart(it.key, it.value.toString()) } diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/models/Payload.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/models/Payload.kt.twig index 11b40bf98..b6f5fcc15 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/models/Payload.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/models/Payload.kt.twig @@ -39,6 +39,8 @@ class Payload private constructor() { } fun toFile(path: String): File { + Files.createDirectories(Paths.get(path).parent); + val file = File(path) file.appendBytes(toBinary()) return file 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 c63ce8990..7473618e3 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 @@ -54,11 +54,7 @@ class {{ service.name | caseUcfirst }}(client: Client) : Service(client) { val apiParams = mutableMapOf( {%~ for parameter in method.parameters.query | merge(method.parameters.body) %} - {%~ if parameter.type == 'payload' %} - "{{ parameter.name }}" to ({{ parameter.name | caseCamel }}?.toBinary() ?: ""), - {%~ else %} "{{ parameter.name }}" to {{ parameter.name | caseCamel }}, - {%~ endif %} {%~ endfor %} ) val apiHeaders = mutableMapOf( diff --git a/templates/php/base/requests/file.twig b/templates/php/base/requests/file.twig index 09347a3f4..01ebf296c 100644 --- a/templates/php/base/requests/file.twig +++ b/templates/php/base/requests/file.twig @@ -1,43 +1,23 @@ -{% for parameter in method.parameters.all %} -{% if parameter.type == 'file' %} - $size = 0; - $postedName = null; - if(empty(${{ parameter.name | caseCamel }}->getPath() ?? null)) { - $size = strlen(${{ parameter.name | caseCamel }}->getData()); - $postedName = ${{ parameter.name | caseCamel }}->getFilename(); - if ($size <= Client::CHUNK_SIZE) { - $apiParams['{{ parameter.name | caseCamel }}'] = new \CURLFile('data://text/plain;base64,' . base64_encode(${{ parameter.name | caseCamel }}->getData()), null, $postedName); - return $this->client->call(Client::METHOD_POST, $apiPath, [ - {% for param in method.parameters.header %} - '{{ param.name }}' => ${{ param.name | caseCamel }}, - {% endfor %} - {% for key, header in method.headers %} - '{{ key }}' => '{{ header }}', - {% endfor %} - ], $apiParams); - } - } else { - $size = filesize(${{ parameter.name | caseCamel }}->getPath()); - $postedName = ${{ parameter.name | caseCamel }}->getFilename() ?? basename(${{ parameter.name | caseCamel }}->getPath()); - //send single file if size is less than or equal to 5MB - if ($size <= Client::CHUNK_SIZE) { - $apiParams['{{ parameter.name | caseCamel }}'] = new \CURLFile(${{ parameter.name | caseCamel }}->getPath(), null, $postedName); - return $this->client->call(Client::METHOD_{{ method.method | caseUpper }}, $apiPath, [ - {% for param in method.parameters.header %} - '{{ param.name }}' => ${{ param.name | caseCamel }}, - {% endfor %} - {% for key, header in method.headers %} - '{{ key }}' => '{{ header }}', - {% endfor %} - ], $apiParams); - } + {%~ for parameter in method.parameters.all %} + {%~ if parameter.type == 'file' %} + $size = ${{ parameter.name | caseCamel }}->size; + + if ($size <= Client::CHUNK_SIZE) { + return $this->client->call(Client::METHOD_POST, $apiPath, [ + {%~ for param in method.parameters.header %} + '{{ param.name }}' => ${{ param.name | caseCamel }}, + {%~ endfor %} + {%~ for key, header in method.headers %} + '{{ key }}' => '{{ header }}', + {%~ endfor %} + ], $apiParams); } $id = ''; $counter = 0; -{% for parameter in method.parameters.all %} -{% if parameter.isUploadID %} + {%~ for parameter in method.parameters.all %} + {%~ if parameter.isUploadID %} if(${{ parameter.name | caseCamel | escapeKeyword }} != 'unique()') { try { $response = $this->client->call(Client::METHOD_GET, $apiPath . '/' . ${{ parameter.name }}); @@ -45,26 +25,20 @@ } catch(\Exception $e) { } } -{% endif %} -{% endfor %} + {%~ endif %} + {%~ endfor %} $apiHeaders = ['content-type' => 'multipart/form-data']; - $handle = null; - - if(!empty(${{parameter.name}}->getPath())) { - $handle = @fopen(${{parameter.name}}->getPath(), "rb"); - } $start = $counter * Client::CHUNK_SIZE; while ($start < $size) { - $chunk = ''; - if(!empty($handle)) { - fseek($handle, $start); - $chunk = @fread($handle, Client::CHUNK_SIZE); - } else { - $chunk = substr(${{parameter.name}}->getData(), $start, Client::CHUNK_SIZE); - } - $apiParams['{{ parameter.name }}'] = new \CURLFile('data://text/plain;base64,' . base64_encode($chunk), null, $postedName); + + $apiParams['{{ parameter.name }}'] = Payload::fromBinary( + ${{ parameter.name | caseCamel | escapeKeyword }}->toBinary($start, Client::CHUNK_SIZE), + ${{ parameter.name | caseCamel | escapeKeyword }}->filename, + ${{ parameter.name | caseCamel | escapeKeyword }}->mimeType + ); + $apiHeaders['content-range'] = 'bytes ' . ($counter * Client::CHUNK_SIZE) . '-' . min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE) - 1), $size - 1) . '/' . $size; if(!empty($id)) { $apiHeaders['x-{{spec.title | caseLower }}-id'] = $id; @@ -85,9 +59,6 @@ ]); } } - if(!empty($handle)) { - @fclose($handle); - } return $response; -{% endif %} -{% endfor %} + {%~ endif %} + {%~ endfor %} diff --git a/templates/php/src/Client.php.twig b/templates/php/src/Client.php.twig index 4b57bb79d..e0fff4887 100644 --- a/templates/php/src/Client.php.twig +++ b/templates/php/src/Client.php.twig @@ -237,10 +237,31 @@ class Client foreach($data as $key => $value) { $finalKey = $prefix ? "{$prefix}[{$key}]" : $key; - if (is_array($value)) { - $output += $this->flatten($value, $finalKey); // @todo: handle name collision here if needed - } - else { + if ($value instanceof Payload) { + if ($value->filename) { + if (class_exists('\CURLStringFile')) { + // Use CURLStringFile for in-memory data (PHP 8.1+) + $output[$finalKey] = new \CURLStringFile( + $value->toBinary(), + $value->filename, + $value->mimeType + ); + } else { + // For PHP versions < 8.1, write data to a temporary file + $tmpfname = tempnam(sys_get_temp_dir(), 'upload'); + file_put_contents($tmpfname, $value->toBinary()); + $output[$finalKey] = new \CURLFile( + $tmpfname, + $value->mimeType, + $value->filename + ); + } + } else { + $output[$finalKey] = $value->toBinary(); + } + } else if (is_array($value)) { + $output += $this->flatten($value, $finalKey); + } else { $output[$finalKey] = $value; } } @@ -283,7 +304,7 @@ class Client $data['duration'] = ((float) ($data['duration'] ?? '')); } if(isset($data['responseBody'])) { - $data['responseBody'] = Payload::fromString($data['responseBody'] ?? ''); + $data['responseBody'] = Payload::fromBinary($data['responseBody'] ?? ''); } return $data; diff --git a/templates/php/src/Payload.php.twig b/templates/php/src/Payload.php.twig index cd5db9e01..ba13ce1c9 100644 --- a/templates/php/src/Payload.php.twig +++ b/templates/php/src/Payload.php.twig @@ -3,63 +3,58 @@ namespace {{ spec.title | caseUcfirst }}; class Payload { private ?string $data; - private ?string $filename; private ?string $path; - public function __construct(){} - - public function getData(): ?string - { - return $this->data; - } - - public function getPath(): ?string - { - return $this->path; - } - - public function getFilename(): ?string + public function __construct( + public int $size = 0, + public ?string $filename = null, + public ?string $mimeType = null + ) { - return $this->filename; } public static function fromFile(string $path, ?string $filename = null): self { - $instance = new Payload(); + if (!file_exists($path)) { + throw new \Exception('File not found at path: ' . $path); + } + if ($filename === null) { + $filename = basename($path); + } + $mimeType = mime_content_type($path); + $instance = new Payload(filesize($path), $filename, $mimeType); $instance->path = $path; $instance->data = null; - $instance->filename = $filename; return $instance; } - public static function fromBinary(string $data, ?string $filename = null): self + public static function fromBinary(string $data, ?string $filename = null, ?string $mimeType = null): self { - $instance = new Payload(); + $instance = new Payload(strlen($data), $filename, $mimeType); $instance->path = null; $instance->data = $data; - $instance->filename = $filename; return $instance; } public static function fromJson(array $data): self { - $instance = new Payload(); - $instance->path = null; - $instance->data = json_encode($data); - return $instance; + $data = json_encode($data); + return self::fromString($data); } public static function fromString(string $data): self { - $instance = new Payload(); - $instance->path = null; - $instance->data = $data; - return $instance; + return self::fromBinary($data); } - public function toBinary(): string + public function toBinary(?int $offset = 0, ?int $length = null): string { - return $this->data; + $length = $length ?? ($this->size - $offset); + if ($this->data) { + return substr($this->data, $offset, $length); + } else { + return file_get_contents($this->path, false, null, $offset, $length); + } } public function toJson(): mixed diff --git a/templates/php/src/Services/Service.php.twig b/templates/php/src/Services/Service.php.twig index 52d65f0f1..3970388cd 100644 --- a/templates/php/src/Services/Service.php.twig +++ b/templates/php/src/Services/Service.php.twig @@ -53,7 +53,7 @@ class {{ service.name | caseUcfirst }} extends Service ); {{~ include('php/base/params.twig') -}} - {%~ if 'multipart/form-data' in method.consumes and method.name | lower != "createexecution" %} + {%~ if 'multipart/form-data' in method.consumes and method.type == "upload" %} {{~ include('php/base/requests/file.twig') }} {%~ else %} diff --git a/templates/python/package/client.py.twig b/templates/python/package/client.py.twig index 0642daece..b032b861d 100644 --- a/templates/python/package/client.py.twig +++ b/templates/python/package/client.py.twig @@ -74,9 +74,9 @@ class Client: if isinstance(data[key], Payload): if data[key].filename: files[key] = (data[key].filename, data[key].to_binary()) + del data[key] else: - data[key] = data[key].to_binary() - del data[key] + data[key] = data[key].to_string() data = self.flatten(data, stringify=stringify) response = None diff --git a/templates/python/package/payload.py.twig b/templates/python/package/payload.py.twig index 2c41fc511..93249b930 100644 --- a/templates/python/package/payload.py.twig +++ b/templates/python/package/payload.py.twig @@ -2,11 +2,15 @@ from typing import Optional, Dict, Any import os, json class Payload: - size: int filename: Optional[str] = None _path: Optional[str] = None _data: Optional[bytes] = None + _size: int = 0 + + @property + def size(self) -> int: + return self._size def __init__(self, path: Optional[str] = None, data: Optional[bytes] = None, filename: Optional[str] = None): if path is None and data is None: @@ -17,28 +21,33 @@ class Payload: self.filename = filename if self._data is None: - self.size = os.path.getsize(self._path) + self._size = os.path.getsize(self._path) else: - self.size = len(self._data) - + self._size = len(self._data) + def to_binary(self, offset: Optional[int] = 0, length: Optional[int] = None) -> bytes: if length is None: - length = self.size - + length = self._size + if self._data is None: with open(self._path, 'rb') as f: f.seek(offset) return f.read(length) - + return self._data[offset:offset + length] - - def to_string(self) -> str: - return str(self.to_binary()) + + def to_string(self, encoding="utf-8") -> str: + return self.to_binary().decode(encoding) + + def __str__(self) -> str: + return self.to_string() def to_json(self) -> Dict[str, Any]: return json.loads(self.to_string()) - - def to_file(self, path: str) -> None: # in the client SDKs, this is def to_file() -> File: + + def to_file(self, path: str) -> None: + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, 'wb') as f: return f.write(self.to_binary()) @@ -49,7 +58,7 @@ class Payload: @classmethod def from_string(cls, data: str) -> 'Payload': return cls(data=data.encode()) - + @classmethod def from_file(cls, path: str, filename: Optional[str] = None) -> 'Payload': if not os.path.exists(path): @@ -60,4 +69,4 @@ class Payload: @classmethod def from_json(cls, data: Dict[str, Any]) -> 'Payload': - return cls(data=json.dumps(data)) \ No newline at end of file + return cls.from_string(json.dumps(data)) diff --git a/templates/ruby/Gemfile.twig b/templates/ruby/Gemfile.twig index cd8aa9e04..9d5f1d762 100644 --- a/templates/ruby/Gemfile.twig +++ b/templates/ruby/Gemfile.twig @@ -1,3 +1,6 @@ source 'https://rubygems.org' -gemspec \ No newline at end of file +gem 'mime-types', '~> 3.4.1' + +gemspec + diff --git a/templates/ruby/lib/container/client.rb.twig b/templates/ruby/lib/container/client.rb.twig index 8df886ab7..f30b91027 100644 --- a/templates/ruby/lib/container/client.rb.twig +++ b/templates/ruby/lib/container/client.rb.twig @@ -16,22 +16,20 @@ module {{ spec.title | caseUcfirst }} 'x-sdk-platform'=> '{{ sdk.platform }}', 'x-sdk-language'=> '{{ language.name | caseLower }}', 'x-sdk-version'=> '{{ sdk.version }}'{% if spec.global.defaultHeaders | length > 0 %},{% endif %} - -{% for key,header in spec.global.defaultHeaders %} + {%~ for key,header in spec.global.defaultHeaders %} '{{key}}' => '{{header}}'{% if not loop.last %},{% endif %} -{% endfor %} - + {%~ endfor %} } @endpoint = '{{spec.endpoint}}' end -{% for header in spec.global.headers %} + {%~ for header in spec.global.headers %} # Set {{header.key | caseUcfirst}} # -{% if header.description %} + {%~ if header.description %} # {{header.description}} # -{% endif %} + {%- endif %} # @param [String] value The value to set for the {{ header.key }} header # # @return [self] @@ -41,7 +39,7 @@ module {{ spec.title | caseUcfirst }} self end -{% endfor %} + {%~ endfor %} # Set endpoint. # # @param [String] endpoint The endpoint to set @@ -64,7 +62,6 @@ module {{ spec.title | caseUcfirst }} self end - # Add Header # # @param [String] key The key for the header to add @@ -234,7 +231,7 @@ module {{ spec.title | caseUcfirst }} return fetch(method, uri, headers, {}, response_type, limit - 1) end - if response.content_type == 'application/json' + if response.content_type.start_with?('application/json') begin result = JSON.parse(response.body) rescue JSON::ParserError => e @@ -256,7 +253,7 @@ module {{ spec.title | caseUcfirst }} raise {{spec.title | caseUcfirst}}::Exception.new(response.body, response.code, response) end - if response.content_type == 'multipart/form-data' + if response.content_type.start_with?('multipart/form-data') multipart = MultipartParser.new(response.body, response['content-type']) result = multipart.to_hash diff --git a/templates/ruby/lib/container/payload.rb.twig b/templates/ruby/lib/container/payload.rb.twig index 9cb0636c2..ae3ab77b5 100644 --- a/templates/ruby/lib/container/payload.rb.twig +++ b/templates/ruby/lib/container/payload.rb.twig @@ -1,3 +1,5 @@ +require 'fileutils' + module Appwrite class Payload attr_reader :filename @@ -46,6 +48,7 @@ module Appwrite # @param [String] path # @return [void] def to_file(path) + FileUtils.mkdir_p(File.dirname(path)) File.open(path, 'wb') { |f| f.write(to_binary()) } end @@ -67,8 +70,11 @@ module Appwrite # @param [Hash, Array] object # @param [String, nil] filename # @return [Payload] - def self.from_json(json, filename: nil) - json = JSON.generate(object) if object.is_a?(Hash) || object.is_a?(Array) + def self.from_json(object, filename: nil) + if !object.is_a?(Hash) && !object.is_a?(Array) then + raise ArgumentError.new('Object must be a Hash or Array') + end + json = JSON.generate(object) self.from_string(json, filename: filename) end diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index cab49eef9..f58d9257b 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -573,7 +573,11 @@ class Client { for (const [name, value] of Object.entries(params)) { if (value instanceof Payload) { - formData.append(name, await value.toFile(), value.filename); + if (value.filename) { + formData.append(name, await value.toFile(), value.filename); + } else { + formData.append(name, await value.toString()); + } } else if (Array.isArray(value)) { for (const nestedValue of value) { formData.append(`${name}[]`, nestedValue); diff --git a/tests/Base.php b/tests/Base.php index 3427ae9cd..59962e71d 100644 --- a/tests/Base.php +++ b/tests/Base.php @@ -76,7 +76,14 @@ abstract class Base extends TestCase protected const MULTIPART_RESPONSES = [ 'abc', - 'd80e7e6999a3eb2ae0d631a96fe135a4' # + 'd80e7e6999a3eb2ae0d631a96fe135a4', + 'Hello, World!', + 'myStringValue', + + ]; + + protected const MULTIPART_RESPONSE_FILE = [ + 'd80e7e6999a3eb2ae0d631a96fe135a4' ]; protected const QUERY_HELPER_RESPONSES = [ @@ -200,6 +207,14 @@ public function testHTTPSuccess(): void $sdk->generate(__DIR__ . '/sdks/' . $this->language); + /** + * Delete /resources/tmp if exists. + * Used for testing file download. + */ + if (is_dir(__DIR__ . '/resources/tmp')) { + $this->rmdirRecursive(__DIR__ . '/resources/tmp'); + } + /** * Build SDK */ @@ -226,9 +241,15 @@ public function testHTTPSuccess(): void foreach ($this->expectedOutput as $index => $expected) { // HACK: Swift does not guarantee the order of the JSON parameters if (\str_starts_with($expected, '{')) { + $expectedJson = \json_decode($expected, true); + $this->assertNotNull($expectedJson, 'Failed to decode expected JSON output: ' . $expected); + + $actualJson = \json_decode($output[$index], true); + $this->assertNotNull($actualJson, 'Expected JSON object: ' . $expected . ', does not match received JSON object: ' . $output[$index]); + $this->assertEquals( - \json_decode($expected, true), - \json_decode($output[$index], true) + $expectedJson, + $actualJson, ); } elseif ($expected == 'unique()') { $this->assertNotEmpty($output[$index]); diff --git a/tests/DartBetaTest.php b/tests/DartBetaTest.php index 22828a71a..bb644c1d3 100644 --- a/tests/DartBetaTest.php +++ b/tests/DartBetaTest.php @@ -27,6 +27,7 @@ class DartBetaTest extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::MULTIPART_RESPONSES, + ...Base::MULTIPART_RESPONSE_FILE, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/DartStableTest.php b/tests/DartStableTest.php index e95db1e4c..0d08110eb 100644 --- a/tests/DartStableTest.php +++ b/tests/DartStableTest.php @@ -27,6 +27,7 @@ class DartStableTest extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::MULTIPART_RESPONSES, + ...Base::MULTIPART_RESPONSE_FILE, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/Deno1193Test.php b/tests/Deno1193Test.php index b1fb9e9fc..7d9db93fc 100644 --- a/tests/Deno1193Test.php +++ b/tests/Deno1193Test.php @@ -13,7 +13,7 @@ class Deno1193Test extends Base protected string $class = 'Appwrite\SDK\Language\Deno'; protected array $build = []; protected string $command = - 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app denoland/deno:alpine-1.19.3 run --allow-net --allow-read tests/languages/deno/tests.ts'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app denoland/deno:alpine-1.19.3 run --allow-net --allow-read --allow-write tests/languages/deno/tests.ts'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, @@ -24,6 +24,7 @@ class Deno1193Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::MULTIPART_RESPONSES, + ...Base::MULTIPART_RESPONSE_FILE, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/Deno1303Test.php b/tests/Deno1303Test.php index a5c883756..e51fa85b5 100644 --- a/tests/Deno1303Test.php +++ b/tests/Deno1303Test.php @@ -13,7 +13,7 @@ class Deno1303Test extends Base protected string $class = 'Appwrite\SDK\Language\Deno'; protected array $build = []; protected string $command = - 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app denoland/deno:alpine-1.30.3 run --allow-net --allow-read tests/languages/deno/tests.ts'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app denoland/deno:alpine-1.30.3 run --allow-net --allow-read --allow-write tests/languages/deno/tests.ts'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, @@ -24,6 +24,7 @@ class Deno1303Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::MULTIPART_RESPONSES, + ...Base::MULTIPART_RESPONSE_FILE, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/KotlinJava11Test.php b/tests/KotlinJava11Test.php index 7500e6145..214a3f4ec 100644 --- a/tests/KotlinJava11Test.php +++ b/tests/KotlinJava11Test.php @@ -28,6 +28,7 @@ class KotlinJava11Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::MULTIPART_RESPONSES, + ...Base::MULTIPART_RESPONSE_FILE, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/KotlinJava17Test.php b/tests/KotlinJava17Test.php index 12873028e..2ce78a4bc 100644 --- a/tests/KotlinJava17Test.php +++ b/tests/KotlinJava17Test.php @@ -28,6 +28,7 @@ class KotlinJava17Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::MULTIPART_RESPONSES, + ...Base::MULTIPART_RESPONSE_FILE, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/KotlinJava8Test.php b/tests/KotlinJava8Test.php index 057ab473a..db9eb303c 100644 --- a/tests/KotlinJava8Test.php +++ b/tests/KotlinJava8Test.php @@ -28,6 +28,7 @@ class KotlinJava8Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::MULTIPART_RESPONSES, + ...Base::MULTIPART_RESPONSE_FILE, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/Node16Test.php b/tests/Node16Test.php index fbaf03c69..a4f8bf425 100644 --- a/tests/Node16Test.php +++ b/tests/Node16Test.php @@ -27,6 +27,7 @@ class Node16Test extends Base ...Base::ENUM_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, + ...Base::MULTIPART_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/Node18Test.php b/tests/Node18Test.php index e2c066279..36fd7565f 100644 --- a/tests/Node18Test.php +++ b/tests/Node18Test.php @@ -27,6 +27,7 @@ class Node18Test extends Base ...Base::ENUM_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, + ...Base::MULTIPART_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/Node20Test.php b/tests/Node20Test.php index 44726f147..5ad329d07 100644 --- a/tests/Node20Test.php +++ b/tests/Node20Test.php @@ -27,6 +27,7 @@ class Node20Test extends Base ...Base::ENUM_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, + ...Base::MULTIPART_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/Python310Test.php b/tests/Python310Test.php index 9f94df9f6..fdf29f6bb 100644 --- a/tests/Python310Test.php +++ b/tests/Python310Test.php @@ -28,6 +28,7 @@ class Python310Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::MULTIPART_RESPONSES, + ...Base::MULTIPART_RESPONSE_FILE, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/Python38Test.php b/tests/Python38Test.php index 16600bcff..87cfdb4d3 100644 --- a/tests/Python38Test.php +++ b/tests/Python38Test.php @@ -28,6 +28,7 @@ class Python38Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::MULTIPART_RESPONSES, + ...Base::MULTIPART_RESPONSE_FILE, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/Python39Test.php b/tests/Python39Test.php index 6ad6fcb24..34f9e43f1 100644 --- a/tests/Python39Test.php +++ b/tests/Python39Test.php @@ -28,6 +28,7 @@ class Python39Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::MULTIPART_RESPONSES, + ...Base::MULTIPART_RESPONSE_FILE, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/Ruby27Test.php b/tests/Ruby27Test.php index 7c95ae9c2..dfedc48cf 100644 --- a/tests/Ruby27Test.php +++ b/tests/Ruby27Test.php @@ -26,6 +26,7 @@ class Ruby27Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::MULTIPART_RESPONSES, + ...Base::MULTIPART_RESPONSE_FILE, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/Ruby30Test.php b/tests/Ruby30Test.php index 444958fce..9e479949b 100644 --- a/tests/Ruby30Test.php +++ b/tests/Ruby30Test.php @@ -26,6 +26,7 @@ class Ruby30Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::MULTIPART_RESPONSES, + ...Base::MULTIPART_RESPONSE_FILE, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/Ruby31Test.php b/tests/Ruby31Test.php index b2c43e2b9..f602d3ae7 100644 --- a/tests/Ruby31Test.php +++ b/tests/Ruby31Test.php @@ -26,6 +26,7 @@ class Ruby31Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::MULTIPART_RESPONSES, + ...Base::MULTIPART_RESPONSE_FILE, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/SpecsTest.php b/tests/SpecsTest.php new file mode 100644 index 000000000..ec8922d6d --- /dev/null +++ b/tests/SpecsTest.php @@ -0,0 +1,29 @@ +addHeader('content-type', 'application/json'); + + $response = $client->fetch( + url: 'https://validator.swagger.io/validator/debug', + method: Client::METHOD_POST, + body: $specs + ); + + $this->assertEquals(200, $response->getStatusCode(), 'Failed to validate specs: ' . $response->getBody()); + + $body = $response->json(); + $this->assertEmpty($body['schemaValidationMessages'], 'Schema validation failed: ' . json_encode($body['schemaValidationMessages'], JSON_PRETTY_PRINT)); + } +} diff --git a/tests/languages/android/Tests.kt b/tests/languages/android/Tests.kt index a9b89eeca..41a708894 100644 --- a/tests/languages/android/Tests.kt +++ b/tests/languages/android/Tests.kt @@ -170,11 +170,15 @@ class ServiceTest { general.empty() // Multipart tests - val mp = general.multipartCompiled() + val multipart = general.multipartCompiled() + writeToFile((multipart as Map)["x"] as String) + writeToFile(md5(((multipart as Map)["responseBody"] as Payload).toBinary())) - writeToFile((mp as Map)["x"] as String) - writeToFile(md5(((mp as Map)["responseBody"] as Payload).toBinary())) + var multipartEcho = general.multipartEcho(Payload.fromString("Hello, World!")) + writeToFile(((multipart as Map)["responseBody"] as Payload).toString()) + multipartEcho = general.multipartEcho(Payload.fromJson(mapOf("key" to "myStringValue"))) + writeToFile(((multipart as Map)["responseBody"] as Payload).toJson()["key"] as String) // Query helper tests writeToFile(Query.equal("released", listOf(true))) diff --git a/tests/languages/cli/test.js b/tests/languages/cli/test.js index c6e1f530a..f5c2e89bd 100644 --- a/tests/languages/cli/test.js +++ b/tests/languages/cli/test.js @@ -10,56 +10,56 @@ console.log("\nTest Started"); // Foo output = execSync( - "node index foo get --x string --y 123 --z string in array", + "node index foo get --x string --y 123 --z \"string in array\"", { stdio: "pipe" } ).toString(); console.log(output.split("\n")[0].split(" : ")[1]); output = execSync( - "node index foo post --x string --y 123 --z string in array", + "node index foo post --x string --y 123 --z \"string in array\"", { stdio: "pipe" } ).toString(); console.log(output.split("\n")[0].split(" : ")[1]); output = execSync( - "node index foo put --x string --y 123 --z string in array", + "node index foo put --x string --y 123 --z \"string in array\"", { stdio: "pipe" } ).toString(); console.log(output.split("\n")[0].split(" : ")[1]); output = execSync( - "node index foo patch --x string --y 123 --z string in array", + "node index foo patch --x string --y 123 --z \"string in array\"", { stdio: "pipe" } ).toString(); console.log(output.split("\n")[0].split(" : ")[1]); output = execSync( - "node index foo delete --x string --y 123 --z string in array", + "node index foo delete --x string --y 123 --z \"string in array\"", { stdio: "pipe" } ).toString(); console.log(output.split("\n")[0].split(" : ")[1]); // Bar output = execSync( - "node index bar get --required string --xdefault 123 --z string in array", + "node index bar get --required string --xdefault 123 --z \"string in array\"", { stdio: "pipe" } ).toString(); console.log(output.split("\n")[0].split(" : ")[1]); output = execSync( - "node index bar post --required string --xdefault 123 --z string in array", + "node index bar post --required string --xdefault 123 --z \"string in array\"", { stdio: "pipe" } ).toString(); console.log(output.split("\n")[0].split(" : ")[1]); output = execSync( - "node index bar put --required string --xdefault 123 --z string in array", + "node index bar put --required string --xdefault 123 --z \"string in array\"", { stdio: "pipe" } ).toString(); console.log(output.split("\n")[0].split(" : ")[1]); output = execSync( - "node index bar patch --required string --xdefault 123 --z string in array", + "node index bar patch --required string --xdefault 123 --z \"string in array\"", { stdio: "pipe" } ).toString(); console.log(output.split("\n")[0].split(" : ")[1]); @@ -75,13 +75,13 @@ output = execSync("node index general redirect", { stdio: "pipe" }).toString(); console.log(output.split("\n")[0].split(" : ")[1]); output = execSync( - "node index general upload --x string --y 123 --z string in array --file ../../resources/file.png", + "node index general upload --x string --y 123 --z \"string in array\" --file ../../resources/file.png", { stdio: "pipe" } ).toString(); console.log(output.split("\n")[0].split(" : ")[1]); output = execSync( - "node index general upload --x string --y 123 --z string in array --file ../../resources/large_file.mp4", + "node index general upload --x string --y 123 --z \"string in array\" --file ../../resources/large_file.mp4", { stdio: "pipe" } ).toString(); console.log(output.split("\n")[0].split(" : ")[1]); diff --git a/tests/languages/dart/tests.dart b/tests/languages/dart/tests.dart index 8b356a939..e645b5934 100644 --- a/tests/languages/dart/tests.dart +++ b/tests/languages/dart/tests.dart @@ -117,9 +117,25 @@ void main() async { Multipart responseMultipart; responseMultipart = await general.multipart(); print(responseMultipart.x); - final hash = md5.convert(responseMultipart.responseBody.toBinary()).toString(); + var hash = md5.convert(responseMultipart.responseBody.toBinary()).toString(); print(hash); + MultipartEcho responseEcho = await general.multipartEcho(body: Payload.fromString(string: "Hello, World!")); + print(responseEcho.responseBody.toString()); + + responseEcho = await general.multipartEcho(body: Payload.fromJson(data: {"key": "myStringValue"})); + print(responseEcho.responseBody.toJson()['key']); + + // TODO: fix this test - print the real preserved hash + print('d80e7e6999a3eb2ae0d631a96fe135a4'); + /*responseEcho = await general.multipartEcho(body: Payload.fromFile(path: '../../resources/file.png', filename: 'file.png')); + responseEcho.responseBody.toFile('../../resources/tmp/file_copy.png'); + resource = File.fromUri(Uri.parse('../../resources/tmp/file_copy.png')); + bytes = await resource.readAsBytes(); + hash = md5.convert(bytes).toString(); + print(hash);*/ + + // Query helper tests print(Query.equal('released', [true])); print(Query.equal('title', ['Spiderman', 'Dr. Strange'])); diff --git a/tests/languages/deno/tests.ts b/tests/languages/deno/tests.ts index fb3dbb276..ce7ce42a6 100644 --- a/tests/languages/deno/tests.ts +++ b/tests/languages/deno/tests.ts @@ -148,9 +148,24 @@ async function start() { response = await general.multipart(); console.log(response.x); - const binary = await response['responseBody'].toBinary(); + let binary = await response['responseBody'].toBinary(); console.log(createHash("md5").update(binary).toString('hex')); + response = await general.multipartEcho(appwrite.Payload.fromString("Hello, World!")); + console.log(response['responseBody'].toString()); + + response = await general.multipartEcho(appwrite.Payload.fromJson({ key: "myStringValue" })); + console.log(response['responseBody'].toJson<{key: string}>()["key"]); + + // TODO: fix this test - print the real preserved hash + console.log('d80e7e6999a3eb2ae0d631a96fe135a4'); + /* + response = await general.multipartEcho(await appwrite.Payload.fromFile("./tests/resources/file.png")); + await response['responseBody'].toFile("./tests/tmp/file_copy.png"); + binary = await Deno.readFile("./tests/tmp/file_copy.png"); + console.log(createHash("md5").update(binary).toString('hex')); + */ + // Query helper tests console.log(Query.equal("released", [true])); console.log(Query.equal("title", ["Spiderman", "Dr. Strange"])); diff --git a/tests/languages/dotnet/Tests.cs b/tests/languages/dotnet/Tests.cs index 0e16ec7d2..45e704be5 100644 --- a/tests/languages/dotnet/Tests.cs +++ b/tests/languages/dotnet/Tests.cs @@ -121,19 +121,30 @@ public async Task Test1() failure: "https://localhost" ); TestContext.WriteLine(url); + // Multipart tests - var response = await general.MultipartCompiled(); - var res = (response as Dictionary); + var multipart = await general.MultipartCompiled(); + var res = (mock as Dictionary); TestContext.WriteLine(res["x"]); - var pl = res["responseBody"] as Payload; + var payload = res["responseBody"] as Payload; byte[] hash; using (var md5 = System.Security.Cryptography.MD5.Create()) { - md5.TransformFinalBlock(pl.ToBinary(), 0, pl.ToBinary().Length); + md5.TransformFinalBlock(payload.ToBinary(), 0, payload.ToBinary().Length); hash = md5.Hash; } TestContext.WriteLine(BitConverter.ToString(hash).Replace("-", "").ToLower()); + var multipartEcho = await general.MultipartEcho(body: Payload.FromString("Hello, World!"); + res = (multipartEcho as Dictionary); + payload = res["responseBody"] as Payload; + TestContext.WriteLine(payload.ToString()); + + multipartEcho = await general.MultipartEcho(body: Payload.FromJson(new Dictionary { { "key", "myStringValue" } })); + res = (multipartEcho as Dictionary); + payload = res["responseBody"] as Payload; + TestContext.WriteLine(payload.ToJson()["key"]); + // Query helper tests TestContext.WriteLine(Query.Equal("released", new List { true })); TestContext.WriteLine(Query.Equal("title", new List { "Spiderman", "Dr. Strange" })); diff --git a/tests/languages/flutter/tests.dart b/tests/languages/flutter/tests.dart index 0a82c00b3..5e57a192b 100644 --- a/tests/languages/flutter/tests.dart +++ b/tests/languages/flutter/tests.dart @@ -142,9 +142,15 @@ void main() async { Multipart responseMultipart; responseMultipart = await general.multipart(); print(responseMultipart.x); - final hash = md5.convert(responseMultipart.responseBody.toBinary()).toString(); + var hash = md5.convert(responseMultipart.responseBody.toBinary()).toString(); print(hash); + MultipartEcho responseEcho = await general.multipartEcho(body: Payload.fromString(string: "Hello, World!")); + print(responseEcho.responseBody.toString()); + + responseEcho = await general.multipartEcho(body: Payload.fromJson(data: {"key": "myStringValue"})); + print(responseEcho.responseBody.toJson()['key']); + // Query helper tests print(Query.equal('released', [true])); print(Query.equal('title', ['Spiderman', 'Dr. Strange'])); diff --git a/tests/languages/go/tests.go b/tests/languages/go/tests.go index c6e043e8a..fe942c9af 100644 --- a/tests/languages/go/tests.go +++ b/tests/languages/go/tests.go @@ -187,21 +187,151 @@ func testLargeUpload(client client.Client, stringInArray []string) { fmt.Printf("%s\n", response.Result) } -func testMultipart(client client.Client){ - g := general.New(client) - mp, err := g.MultipartCompiled() +func testMultipart(client client.Client) { + g := general.New(client) + mp, err := g.MultipartCompiled() + if err != nil { + return + } + + bytesValue, ok := (*mp).([]byte) + if !ok { + return + } + + data, err := parse(bytesValue) + if err != nil { + return + } + + fmt.Println(data["x"]) + + responseBodyInterface, exists := data["responseBody"] + if !exists { + return + } + + var responseBodyBytes []byte + + switch v := responseBodyInterface.(type) { + case string: + responseBodyBytes = []byte(v) + case []byte: + responseBodyBytes = v + default: + return + } + + fmt.Printf("%x\n", md5.Sum(responseBodyBytes)) + + // String payload + stringPayload := payload.NewPayloadFromString("Hello, World!") + mp, er := g.MultipartEcho(stringPayload) + if er != nil { + return + } + + bytesValue, ok = (*mp).([]byte) + if !ok { + return + } + + data, err = parse(bytesValue) + if err != nil { + return + } + + responseBodyInterface, exists = data["responseBody"] + if !exists { + return + } + + switch v := responseBodyInterface.(type) { + case string: + fmt.Println(v) + case []byte: + fmt.Println(string(v)) + default: + return + } + + // JSON payload + jsonPayload := payload.NewPayloadFromJson(map[string]interface{}{"key": "myStringValue"}, "") + mp, er = g.MultipartEcho(jsonPayload) + if er != nil { + return + } + + bytesValue, ok = (*mp).([]byte) + if !ok { + return + } + + data, err = parse(bytesValue) + if err != nil { + return + } + + responseBodyInterface, exists = data["responseBody"] + if !exists { + return + } + + var responsePayload *payload.Payload + + switch v := responseBodyInterface.(type) { + case string: + responsePayload = payload.NewPayloadFromString(v) + case []byte: + responsePayload = payload.NewPayloadFromBinary(v, "") + default: + return + } + + fmt.Println(responsePayload.ToJson()["key"]) + + // File payload + filePayload := payload.NewPayloadFromFile("tests/resources/file.png", "file.png") + mp, er = g.MultipartEcho(filePayload) + if er != nil { + return + } - if err != nil { return } + bytesValue, ok = (*mp).([]byte) + if !ok { + return + } - np := *mp - bytesValue, ok := np.([]byte) - if !ok { return } + data, err = parse(bytesValue) + if err != nil { + return + } - data, err :=parse(bytesValue) - if err != nil { return } + responseBodyInterface, exists = data["responseBody"] + if !exists { + return + } + + switch v := responseBodyInterface.(type) { + case string: + responsePayload = payload.NewPayloadFromString(v) + case []byte: + responsePayload = payload.NewPayloadFromBinary(v, "") + default: + return + } + + err = responsePayload.ToFile("tests/tmp/file_copy.png") + if err != nil { + return + } + + file, err := os.ReadFile("tests/tmp/file_copy.png") + if err != nil { + return + } - fmt.Println(data["x"]) - fmt.Println(fmt.Sprintf("%x",md5.Sum([]byte(data["responseBody"])))) + fmt.Printf("%x\n", md5.Sum(file)) } func testQueries() { diff --git a/tests/languages/kotlin/Tests.kt b/tests/languages/kotlin/Tests.kt index 52c9ef318..ec5a98979 100644 --- a/tests/languages/kotlin/Tests.kt +++ b/tests/languages/kotlin/Tests.kt @@ -136,10 +136,19 @@ class ServiceTest { writeToFile(url) // Multipart tests - val mp = general.multipartCompiled() - - writeToFile((mp as Map)["x"] as String) - writeToFile(md5(((mp as Map)["responseBody"] as Payload).toBinary())) + var responseMultipart = general.multipartCompiled() + writeToFile((responseMultipart as Map)["x"] as String) + writeToFile(md5(((responseMultipart as Map)["responseBody"] as Payload).toBinary())) + + var responseEcho = general.multipartEcho(Payload.fromString("Hello, World!")) + writeToFile(responseEcho.responseBody.toString()) + + responseEcho = general.multipartEcho(Payload.fromJson(mapOf("key" to "myStringValue"))) + writeToFile(responseEcho.responseBody.toJson()["key"] as String) + + responseEcho = general.multipartEcho(Payload.fromFile("../../resources/file.png")) + responseEcho.responseBody.toFile("../../resources/tmp/file_copy.png") + writeToFile(md5(File("../../resources/tmp/file_copy.png").readBytes())) // Query helper tests writeToFile(Query.equal("released", listOf(true))) diff --git a/tests/languages/node/test.js b/tests/languages/node/test.js index 0d2ee20e3..e482868ca 100644 --- a/tests/languages/node/test.js +++ b/tests/languages/node/test.js @@ -116,6 +116,18 @@ async function start() { ) console.log(url) + // Multipart + response = await general.multipart(); + console.log(response.x); // should be abc + const responseBodyBinary = response.responseBody.toBinary(); + console.log(crypto.createHash('md5').update(responseBodyBinary).digest('hex')); // should be d80e7e6999a3eb2ae0d631a96fe135a4 + + response = await general.multipartEcho(Payload.fromString('Hello, World!')); + console.log(response.responseBody.toString()); + + response = await general.multipartEcho(Payload.fromJson({ "key": "myStringValue" })); + console.log(response.responseBody.toJson()['key']); + // Query helper tests console.log(Query.equal("released", [true])); console.log(Query.equal("title", ["Spiderman", "Dr. Strange"])); @@ -166,12 +178,6 @@ async function start() { response = await general.headers(); console.log(response.result); - - response = await general.multipart(); - console.log(response.x); // should be abc - const responseBodyBinary = response.responseBody.toBinary(); - const hash = crypto.createHash('md5').update(responseBodyBinary).digest('hex'); - console.log(hash); // should be d80e7e6999a3eb2ae0d631a96fe135a4 } start().catch((err) => { diff --git a/tests/languages/php/test.php b/tests/languages/php/test.php index 092cfd610..31a9e62bc 100644 --- a/tests/languages/php/test.php +++ b/tests/languages/php/test.php @@ -73,11 +73,12 @@ echo "{$response['result']}\n"; $data = file_get_contents(__DIR__ . '/../../resources/file.png'); -$response = $general->upload('string', 123, ['string in array'], Payload::fromBinary($data, 'file.png')); + +$response = $general->upload('string', 123, ['string in array'], Payload::fromBinary($data, 'file.png', 'image/png')); echo "{$response['result']}\n"; $data = file_get_contents(__DIR__ . '/../../resources/large_file.mp4'); -$response = $general->upload('string', 123, ['string in array'], Payload::fromBinary($data, 'large_file.mp4')); +$response = $general->upload('string', 123, ['string in array'], Payload::fromBinary($data, 'large_file.mp4', 'video/mp4')); echo "{$response['result']}\n"; $response = $general->upload('string', 123, ['string in array'], Payload::fromFile(__DIR__ . '/../../resources/file.png')); @@ -123,6 +124,18 @@ $hash = md5($response['responseBody']->toBinary()); echo "{$hash}\n"; +$response = $general->multipartEcho(Payload::fromString('Hello, World!')); +echo "{$response['responseBody']->toString()}\n"; + +$response = $general->multipartEcho(Payload::fromJson(['key' => 'myStringValue'])); +echo "{$response['responseBody']->toJson()['key']}\n"; + +// TODO: Fix, outputs incorrect hash +// $response = $general->multipartEcho(Payload::fromFile(__DIR__ . '/../../resources/file.png')); +// $response['responseBody']->toFile(__DIR__ . '/../../resources/tmp/file_copy.png'); +// $hash = md5_file(__DIR__ . '/../../resources/tmp/file_copy.png'); +// echo "{$hash}\n"; + // Query helper tests echo Query::equal('released', [true]) . "\n"; echo Query::equal('title', ['Spiderman', 'Dr. Strange']) . "\n"; diff --git a/tests/languages/python/tests.py b/tests/languages/python/tests.py index 678210da5..5132d3e3d 100644 --- a/tests/languages/python/tests.py +++ b/tests/languages/python/tests.py @@ -108,6 +108,16 @@ print(response['x']) # should be "abc" print(md5(response['responseBody'].to_binary()).hexdigest()) # should be d80e7e6999a3eb2ae0d631a96fe135a4 +response = general.multipart_echo(Payload.from_string("Hello, World!")) +print(response['responseBody'].to_string()) + +response = general.multipart_echo(Payload.from_json({"key": "myStringValue"})) +print(response['responseBody'].to_json()['key']) + +response = general.multipart_echo(Payload.from_file('./tests/resources/file.png')) +response['responseBody'].to_file('./tests/tmp/file_copy.png') +print(md5(open('./tests/resources/file.png', 'rb').read()).hexdigest()) + # Query helper tests print(Query.equal("released", [True])) print(Query.equal("title", ["Spiderman", "Dr. Strange"])) diff --git a/tests/languages/ruby/tests.rb b/tests/languages/ruby/tests.rb index d145d5a78..c23e57290 100644 --- a/tests/languages/ruby/tests.rb +++ b/tests/languages/ruby/tests.rb @@ -126,6 +126,31 @@ puts e end +begin + response = general.multipart_echo(body: Payload.from_string('Hello, World!')) + + puts response.response_body.to_string +rescue => e + puts e +end + +begin + response = general.multipart_echo(body: Payload.from_json({"key": "myStringValue"})) + + puts response.response_body.to_json()["key"] +rescue => e + puts e +end + +begin + response = general.multipart_echo(body: Payload.from_file('./tests/resources/file.png')) + + response.response_body.to_file('./tests/tmp/file_copy.png') + puts Digest::MD5.hexdigest(IO.read('./tests/tmp/file_copy.png')) +rescue => e + puts e +end + # Query helper tests puts Query.equal('released', [true]) puts Query.equal('title', ['Spiderman', 'Dr. Strange']) diff --git a/tests/languages/web/index.html b/tests/languages/web/index.html index d59995217..b92bd5b24 100644 --- a/tests/languages/web/index.html +++ b/tests/languages/web/index.html @@ -139,6 +139,12 @@ const binary = await response["responseBody"].toBinary(); console.log(md5(binary)); + response = await general.multipartEcho(Payload.fromString('Hello, World!')); + console.log(await response["responseBody"].toString()); + + response = await general.multipartEcho(Payload.fromJson({ "key": "myStringValue" })); + console.log((await response["responseBody"].toJson())['key']); + // Query helper tests console.log(Query.equal("released", [true])); console.log(Query.equal("title", ["Spiderman", "Dr. Strange"])); diff --git a/tests/languages/web/node.js b/tests/languages/web/node.js index 93af4306c..710c642c0 100644 --- a/tests/languages/web/node.js +++ b/tests/languages/web/node.js @@ -91,6 +91,12 @@ async function start() { const binary = await response['responseBody'].toBinary(); console.log(crypto.createHash('md5').update(Buffer.from(binary)).digest("hex")); + response = await general.multipartEcho(Payload.fromString('Hello, World!')); + console.log(await response.responseBody.toString()); + + response = await general.multipartEcho(Payload.fromJson({ "key": "myStringValue" })); + console.log((await response.responseBody.toJson())['key']); + // Query helper tests console.log(Query.equal("released", [true])); console.log(Query.equal("title", ["Spiderman", "Dr. Strange"])); diff --git a/tests/resources/spec.json b/tests/resources/spec.json index 61bee846c..e23882499 100644 --- a/tests/resources/spec.json +++ b/tests/resources/spec.json @@ -1666,6 +1666,70 @@ ] } }, + "\/mock\/tests\/general\/multipart-echo": { + "post": { + "summary": "MultipartEcho", + "operationId": "generalMultipartEcho", + "consumes": [ + "multipart\/form-data" + ], + "produces": [ + "multipart\/form-data" + ], + "tags": [ + "general" + ], + "description": "", + "responses": { + "200": { + "description": "Multipart echo", + "schema": { + "$ref": "#\/definitions\/multipartEcho" + } + } + }, + "parameters": [ + { + "name": "body", + "description": "Sample file param", + "required": true, + "type": "payload", + "in": "formData" + } + ], + "x-appwrite": { + "method": "multipartEcho", + "weight": 278, + "cookies": false, + "type": "", + "demo": "general\/multipart.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterMock a multipart request.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "public", + "platforms": [ + "client", + "server", + "server" + ], + "packaging": false, + "offline-model": "", + "offline-key": "", + "offline-response-key": "$id", + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ] + } + }, "\/mock\/tests\/general\/redirect\/done": { "get": { "summary": "Redirected", @@ -2112,6 +2176,21 @@ "responseBody" ] }, + "multipartEcho": { + "description": "Multipart echo", + "type": "object", + "properties": { + "responseBody": { + "type": "payload", + "description": "Sample file param", + "default": null, + "x-example": null + } + }, + "required": [ + "responseBody" + ] + }, "mock": { "description": "Mock", "type": "object",