diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 06f10a51a..2446a8f3e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,7 +42,9 @@ jobs: Ruby30, Ruby31, AppleSwift56, + AppleSwift510, Swift56, + Swift510, WebChromium, WebNode ] diff --git a/composer.lock b/composer.lock index 912f91304..d8f259414 100644 --- a/composer.lock +++ b/composer.lock @@ -2444,16 +2444,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 +2517,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 +2533,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 +2696,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 +2737,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 +2753,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 +2840,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 +2907,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 +2923,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/Dockerfile b/mock-server/Dockerfile index f048d39a4..9dc887cd5 100644 --- a/mock-server/Dockerfile +++ b/mock-server/Dockerfile @@ -1,4 +1,4 @@ -FROM composer:2.0 as composer +FROM composer:2.0 AS composer LABEL maintainer="team@appwrite.io" @@ -14,7 +14,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \ --no-plugins --no-scripts --prefer-dist \ `if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi` -FROM phpswoole/swoole:5.1.2-php8.3-alpine as final +FROM phpswoole/swoole:5.1.2-php8.3-alpine AS final RUN apk add docker diff --git a/mock-server/app/http.php b/mock-server/app/http.php index 0b779d23c..9279393a0 100644 --- a/mock-server/app/http.php +++ b/mock-server/app/http.php @@ -306,8 +306,9 @@ ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'general') ->label('sdk.method', 'upload') + ->label('sdk.methodType', 'upload') ->label('sdk.description', 'Mock a file upload request.') - ->label('sdk.request.type', 'multipart/form-data') + ->label('sdk.request.type', Response::CONTENT_TYPE_MULTIPART) ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_MOCK) @@ -319,6 +320,7 @@ ->inject('request') ->inject('response') ->action(function (string $x, int $y, array $z, mixed $file, Request $request, Response $response) { + $file = $request->getFiles('file'); $contentRange = $request->getHeader('content-range'); diff --git a/src/SDK/Language/Apple.php b/src/SDK/Language/Apple.php index 69e3498c4..5656584d8 100644 --- a/src/SDK/Language/Apple.php +++ b/src/SDK/Language/Apple.php @@ -47,13 +47,13 @@ public function getFiles(): array ], [ 'scope' => 'default', - 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Models/{{ spec.title | caseUcfirst}}Error.swift', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}Models/{{ spec.title | caseUcfirst}}Error.swift', 'template' => '/swift/Sources/Models/Error.swift.twig', ], [ 'scope' => 'default', - 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Models/InputFile.swift', - 'template' => 'swift/Sources/Models/InputFile.swift.twig', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}Models/Payload.swift', + 'template' => 'swift/Sources/Models/Payload.swift.twig', ], [ 'scope' => 'default', @@ -77,29 +77,24 @@ public function getFiles(): array ], [ 'scope' => 'default', - 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Models/UploadProgress.swift', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}Models/UploadProgress.swift', 'template' => 'swift/Sources/Models/UploadProgress.swift.twig', ], [ 'scope' => 'default', - 'destination' => '/Sources/JSONCodable/Codable+JSON.swift', - 'template' => 'swift/Sources/JSONCodable/Codable+JSON.swift.twig', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}Extensions/Codable+JSON.swift', + 'template' => 'swift/Sources/Extensions/Codable+JSON.swift.twig', ], [ 'scope' => 'default', - 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Extensions/Cookie+Codable.swift', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}Extensions/Cookie+Codable.swift', 'template' => 'swift/Sources/Extensions/Cookie+Codable.swift.twig', ], [ 'scope' => 'default', - 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Extensions/HTTPClientRequest+Cookies.swift', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}Extensions/HTTPClientRequest+Cookies.swift', 'template' => 'swift/Sources/Extensions/HTTPClientRequest+Cookies.swift.twig', ], - [ - 'scope' => 'default', - 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Extensions/String+MimeTypes.swift', - 'template' => 'swift/Sources/Extensions/String+MimeTypes.swift.twig', - ], [ 'scope' => 'default', 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/StreamingDelegate.swift', @@ -213,7 +208,7 @@ public function getFiles(): array ], [ 'scope' => 'default', - 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Models/RealtimeModels.swift', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}Models/RealtimeModels.swift', 'template' => '/swift/Sources/Models/RealtimeModels.swift.twig', ], [ @@ -486,11 +481,11 @@ public function getFiles(): array ]; } - protected function getReturnType(array $method, array $spec, string $namespace, string $generic = 'T'): string + protected function getReturnType(array $method, array $spec, string $generic = 'T'): string { if ($method['type'] === 'webAuth') { return 'Bool'; } - return parent::getReturnType($method, $spec, $namespace, $generic); + return parent::getReturnType($method, $spec, $generic); } } diff --git a/src/SDK/Language/Swift.php b/src/SDK/Language/Swift.php index 39b8dd85e..9f0836a58 100644 --- a/src/SDK/Language/Swift.php +++ b/src/SDK/Language/Swift.php @@ -144,13 +144,13 @@ public function getFiles(): array ], [ 'scope' => 'default', - 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Models/{{ spec.title | caseUcfirst}}Error.swift', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}Models/{{ spec.title | caseUcfirst}}Error.swift', 'template' => '/swift/Sources/Models/Error.swift.twig', ], [ 'scope' => 'default', - 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Models/InputFile.swift', - 'template' => 'swift/Sources/Models/InputFile.swift.twig', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}Models/Payload.swift', + 'template' => 'swift/Sources/Models/Payload.swift.twig', ], [ 'scope' => 'default', @@ -174,29 +174,24 @@ public function getFiles(): array ], [ 'scope' => 'default', - 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Models/UploadProgress.swift', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}Models/UploadProgress.swift', 'template' => 'swift/Sources/Models/UploadProgress.swift.twig', ], [ 'scope' => 'default', - 'destination' => '/Sources/JSONCodable/Codable+JSON.swift', - 'template' => 'swift/Sources/JSONCodable/Codable+JSON.swift.twig', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}Extensions/Codable+JSON.swift', + 'template' => 'swift/Sources/Extensions/Codable+JSON.swift.twig', ], [ 'scope' => 'default', - 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Extensions/Cookie+Codable.swift', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}Extensions/Cookie+Codable.swift', 'template' => 'swift/Sources/Extensions/Cookie+Codable.swift.twig', ], [ 'scope' => 'default', - 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Extensions/HTTPClientRequest+Cookies.swift', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}Extensions/HTTPClientRequest+Cookies.swift', 'template' => 'swift/Sources/Extensions/HTTPClientRequest+Cookies.swift.twig', ], - [ - 'scope' => 'default', - 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Extensions/String+MimeTypes.swift', - 'template' => 'swift/Sources/Extensions/String+MimeTypes.swift.twig', - ], [ 'scope' => 'default', 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/StreamingDelegate.swift', @@ -311,7 +306,8 @@ public function getTypeName(array $parameter, array $spec = []): string self::TYPE_INTEGER => 'Int', self::TYPE_NUMBER => 'Double', self::TYPE_STRING => 'String', - self::TYPE_FILE => 'InputFile', + self::TYPE_FILE, + self::TYPE_PAYLOAD => 'Payload', self::TYPE_BOOLEAN => 'Bool', self::TYPE_ARRAY => (!empty(($parameter['array'] ?? [])['type']) && !\is_array($parameter['array']['type'])) ? '[' . $this->getTypeName($parameter['array']) . ']' @@ -397,7 +393,10 @@ public function getParamExample(array $param): string if (empty($example) && $example !== 0 && $example !== false) { switch ($type) { case self::TYPE_FILE: - $output .= 'InputFile.fromPath("file.png")'; + $output .= 'Payload.fromFile("/path/to/file.png")'; + break; + case self::TYPE_PAYLOAD: + $output .= 'Payload.fromString("")'; // TODO: Update to fromJson() break; case self::TYPE_NUMBER: case self::TYPE_INTEGER: 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..dc7deeaef 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 @@ -342,7 +342,7 @@ class Client @JvmOverloads constructor( if (size < CHUNK_SIZE) { val data = when(input.sourceType) { "file", "path" -> File(input.path).asRequestBody() - "bytes" -> input.toBinary().toRequestBody(input.mimeType?.toMediaType()) + "bytes" -> input.toBinary().toRequestBody() else -> throw UnsupportedOperationException() } params[paramName] = MultipartBody.Part.createFormData( 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..679b1a983 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 @@ -12,7 +12,6 @@ class Payload private constructor() { lateinit var filename: String lateinit var sourceType: String lateinit var data: Any - var mimeType: String? = null override fun toString(): String { if (sourceType != "bytes") { @@ -62,9 +61,6 @@ class Payload private constructor() { fun fromFileObject(file: File, name: String = "") = Payload().apply { path = file.canonicalPath filename = if (name != "") name else file.name - mimeType = Files.probeContentType(Paths.get(file.canonicalPath)) - ?: URLConnection.guessContentTypeFromName(filename) - ?: "" sourceType = "file" } } diff --git a/templates/apple/Package.swift.twig b/templates/apple/Package.swift.twig index 12b50297f..a3bcac6c6 100644 --- a/templates/apple/Package.swift.twig +++ b/templates/apple/Package.swift.twig @@ -16,14 +16,15 @@ let package = Package( targets: [ "{{spec.title | caseUcfirst}}", "{{spec.title | caseUcfirst}}Enums", - "{{spec.title | caseUcfirst}}Models", - "JSONCodable" + "{{spec.title | caseUcfirst}}Extensions", + "{{spec.title | caseUcfirst}}Models" ] ), ], dependencies: [ .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.19.0"), .package(url: "https://github.com/apple/swift-nio.git", from: "2.58.0"), + .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "4.0.0"), ], targets: [ .target( @@ -31,20 +32,21 @@ let package = Package( dependencies: [ .product(name: "AsyncHTTPClient", package: "async-http-client"), .product(name: "NIOWebSocket", package: "swift-nio"), + .product(name: "Crypto", package: "swift-crypto"), {%~ if spec.definitions is not empty %} "{{spec.title | caseUcfirst}}Models", {%~ endif %} {%~ if spec.enums is not empty %} "{{spec.title | caseUcfirst}}Enums", {%~ endif %} - "JSONCodable" + "{{spec.title | caseUcfirst}}Extensions" ] ), {%~ if spec.definitions is not empty %} .target( name: "{{spec.title | caseUcfirst}}Models", dependencies: [ - "JSONCodable" + "{{spec.title | caseUcfirst}}Extensions", ] ), {%~ endif %} @@ -54,7 +56,10 @@ let package = Package( ), {%~ endif %} .target( - name: "JSONCodable" + name: "{{spec.title | caseUcfirst}}Extensions", + dependencies: [ + .product(name: "AsyncHTTPClient", package: "async-http-client"), + ] ), .testTarget( name: "{{spec.title | caseUcfirst}}Tests", diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig index 2a93f526c..29be950ce 100644 --- a/templates/apple/Sources/Client.swift.twig +++ b/templates/apple/Sources/Client.swift.twig @@ -262,7 +262,7 @@ open class Client { with params: [String: Any?] ) throws { if request.headers["content-type"][0] == "multipart/form-data" { - buildMultipart(&request, with: params, chunked: !request.headers["content-range"].isEmpty) + try buildMultipart(&request, with: params, chunked: !request.headers["content-range"].isEmpty) } else { try buildJSON(&request, with: params) } @@ -288,7 +288,6 @@ open class Client { case 0..<400: if response.headers["Set-Cookie"].count > 0 { let domain = URL(string: request.url)!.host! - let existing = UserDefaults.standard.stringArray(forKey: domain) let new = response.headers["Set-Cookie"] UserDefaults.standard.set(new, forKey: domain) @@ -338,21 +337,12 @@ open class Client { converter: ((Any) -> T)? = nil, onProgress: ((UploadProgress) -> Void)? = nil ) async throws -> T { - let input = params[paramName] as! InputFile - - switch(input.sourceType) { - case "path": - input.data = ByteBuffer(data: try! Data(contentsOf: URL(fileURLWithPath: input.path))) - case "data": - input.data = ByteBuffer(data: input.data as! Data) - default: - break - } - - let size = (input.data as! ByteBuffer).readableBytes + let payload = params[paramName] as! Payload + let buffer = try payload.toBinary() + let size = buffer.readableBytes if size < Client.chunkSize { - params[paramName] = input + params[paramName] = payload return try await call( method: "POST", path: path, @@ -383,10 +373,14 @@ open class Client { } while offset < size { - let slice = (input.data as! ByteBuffer).getSlice(at: offset, length: Client.chunkSize) - ?? (input.data as! ByteBuffer).getSlice(at: offset, length: Int(size - offset)) + let slice = buffer.getSlice(at: offset, length: Client.chunkSize) + ?? buffer.getSlice(at: offset, length: Int(size - offset)) + + params[paramName] = Payload.fromBinary( + slice!, + filename: payload.filename ?? "" + ) - params[paramName] = InputFile.fromBuffer(slice!, filename: input.filename, mimeType: input.mimeType) headers["content-range"] = "bytes \(offset)-\(min((offset + Client.chunkSize) - 1, size - 1))/\(size)" result = try await call( @@ -398,7 +392,9 @@ open class Client { ) offset += Client.chunkSize - headers["x-{{ spec.title | caseLower }}-id"] = result["$id"] as? String + + headers["x-appwrite-id"] = result["$id"] as? String + onProgress?(UploadProgress( id: result["$id"] as? String ?? "", progress: Double(min(offset, size))/Double(size) * 100.0, @@ -458,62 +454,122 @@ open class Client { _ request: inout HTTPClientRequest, with params: [String: Any?] = [:], chunked: Bool = false - ) { - func addPart(name: String, value: Any) { - bodyBuffer.writeString(DASHDASH) - bodyBuffer.writeString(Client.boundary) - bodyBuffer.writeString(CRLF) - bodyBuffer.writeString("Content-Disposition: form-data; name=\"\(name)\"") - - if let file = value as? InputFile { - bodyBuffer.writeString("; filename=\"\(file.filename)\"") - bodyBuffer.writeString(CRLF) - bodyBuffer.writeString("Content-Length: \(bodyBuffer.readableBytes)") - bodyBuffer.writeString(CRLF+CRLF) - - var buffer = file.data! as! ByteBuffer - - bodyBuffer.writeBuffer(&buffer) - bodyBuffer.writeString(CRLF) - return + ) throws { + func addPart(name: String, value: Any, body: inout ByteBuffer) throws { + body.writeString(DASHDASH) + body.writeString(Client.boundary) + body.writeString(CRLF) + body.writeString("Content-Disposition: form-data; name=\"\(name)\"") + + switch value { + case is Payload: + let payload = value as! Payload + var buffer = try payload.toBinary() + body.writeString("; filename=\"\(payload.filename!)\"") + body.writeString(CRLF) + body.writeString("Content-Length: \(buffer.readableBytes)") + body.writeString(CRLF+CRLF) + body.writeBuffer(&buffer) + body.writeString(CRLF) + case is ByteBuffer: + var buffer = value as! ByteBuffer + body.writeString(CRLF) + body.writeString("Content-Length: \(buffer.readableBytes)") + body.writeString(CRLF+CRLF) + body.writeBuffer(&buffer) + body.writeString(CRLF) + default: + let string = String(describing: value) + body.writeString(CRLF) + body.writeString("Content-Length: \(string.count)") + body.writeString(CRLF+CRLF) + body.writeString(string) + body.writeString(CRLF) } - - let string = String(describing: value) - bodyBuffer.writeString(CRLF) - bodyBuffer.writeString("Content-Length: \(string.count)") - bodyBuffer.writeString(CRLF+CRLF) - bodyBuffer.writeString(string) - bodyBuffer.writeString(CRLF) } - var bodyBuffer = ByteBuffer() + var body = ByteBuffer() for (key, value) in params { - switch key { - case "file": - addPart(name: key, value: value!) - default: - if let list = value as? [Any] { - for listValue in list { - addPart(name: "\(key)[]", value: listValue) - } - continue + if let list = value as? [Any] { + for listValue in list { + try addPart(name: "\(key)[]", value: listValue, body: &body) } - addPart(name: key, value: value!) + continue } + try addPart(name: key, value: value!, body: &body) } - bodyBuffer.writeString(DASHDASH) - bodyBuffer.writeString(Client.boundary) - bodyBuffer.writeString(DASHDASH) - bodyBuffer.writeString(CRLF) + body.writeString(DASHDASH) + body.writeString(Client.boundary) + body.writeString(DASHDASH) + body.writeString(CRLF) request.headers.remove(name: "content-type") + if !chunked { - request.headers.add(name: "Content-Length", value: bodyBuffer.readableBytes.description) + request.headers.add(name: "Content-Length", value: body.readableBytes.description) } - request.headers.add(name: "Content-Type", value: "multipart/form-data;boundary=\"\(Client.boundary)\"") - request.body = .bytes(bodyBuffer) + + request.headers.add(name: "Content-Type", value: "multipart/form-data; boundary=\"\(Client.boundary)\"") + request.body = .bytes(body) + } + + private func parseFormData(with boundary: String, for body: String) -> [String: Any] { + var data: [String: Any] = [:] + + for part in body.components(separatedBy: boundary) { + var lines = part.components(separatedBy: CRLF).filter { !$0.isEmpty } + + guard let disposition = lines.first(where: { $0.contains("Content-Disposition: form-data;") }) else { + continue; + } + + // Find part name + if let nameStart = disposition.range(of: "name=\""), + let nameEnd = disposition.range(of: "\"", range: nameStart.upperBound..{% endif %}( {%~ for parameter in method.parameters.all %} - {{ parameter.name | caseCamel | escapeSwiftKeyword }}: {{ parameter | typeName(spec) | raw }}{% if not parameter.required or parameter.nullable %}? = nil{% endif %}{% if not loop.last or 'multipart/form-data' in method.consumes or method.responseModel | hasGenericType(spec) %},{% endif %} + {{ parameter.name | caseCamel | escapeSwiftKeyword }}: {{ parameter | typeName(spec) | raw }}{% if not parameter.required or parameter.nullable %}? = nil{% endif %}{% if not loop.last or ('multipart/form-data' in method.consumes and method.type == 'upload') or method.responseModel | hasGenericType(spec) %},{% endif %} {%~ endfor %} {%~ if method.responseModel | hasGenericType(spec) %} nestedType: T.Type {%~ endif %} - {%~ if 'multipart/form-data' in method.consumes %} + {%~ if 'multipart/form-data' in method.consumes and method.type == 'upload' %} onProgress: ((UploadProgress) -> Void)? = nil {%~ endif %} ) async throws -> {{ method | returnType(spec) | raw }} { @@ -46,9 +46,12 @@ open class {{ service.name | caseUcfirst | overrideIdentifier }}: Service { {%~ if method.headers | length <= 0 %} let apiHeaders: [String: String] = [:] {%~ else %} - {% if 'multipart/form-data' in method.consumes -%} var + {% if 'multipart/form-data' in method.consumes and method.type == 'upload' -%} var {%- else -%} let {%- endif %} apiHeaders: [String: String] = [ + {%~ if 'multipart/form-data' in method.produces %} + "accept": "multipart/form-data", + {%~ endif %} {%~ for key, header in method.headers %} "{{ key }}": "{{ header }}"{% if not loop.last %},{% endif %} @@ -66,7 +69,7 @@ open class {{ service.name | caseUcfirst | overrideIdentifier }}: Service { } {%~ endif %} - {%~ if 'multipart/form-data' in method.consumes %} + {%~ if 'multipart/form-data' in method.consumes and method.type == 'upload' %} {{~ include('swift/base/requests/file.twig') }} {%~ else %} {{~ include('swift/base/requests/api.twig') }} @@ -96,7 +99,7 @@ open class {{ service.name | caseUcfirst | overrideIdentifier }}: Service { {{ parameter.name | caseCamel | escapeSwiftKeyword }}: {{ parameter | typeName(spec) | raw }}{% if not parameter.required or parameter.nullable %}? = nil{% endif %}{% if not loop.last or 'multipart/form-data' in method.consumes %},{% endif %} {%~ endfor %} - {%~ if 'multipart/form-data' in method.consumes %} + {%~ if 'multipart/form-data' in method.consumes and method.type == 'upload' %} onProgress: ((UploadProgress) -> Void)? = nil {%~ endif %} ) async throws -> {{ method | returnType(spec, '[String: AnyCodable]') | raw }} { @@ -105,7 +108,7 @@ open class {{ service.name | caseUcfirst | overrideIdentifier }}: Service { {{ parameter.name | caseCamel | escapeSwiftKeyword }}: {{ parameter.name | caseCamel | escapeSwiftKeyword }}, {%~ endfor %} nestedType: [String: AnyCodable].self - {%~ if 'multipart/form-data' in method.consumes %} + {%~ if 'multipart/form-data' in method.consumes and method.type == 'upload' %} onProgress: onProgress {%~ endif %} ) diff --git a/templates/dart/test/services/service_test.dart.twig b/templates/dart/test/services/service_test.dart.twig index fce004cbf..de4f8b7f4 100644 --- a/templates/dart/test/services/service_test.dart.twig +++ b/templates/dart/test/services/service_test.dart.twig @@ -96,7 +96,7 @@ void main() { {%~ endif ~%} final response = await {{service.name | caseCamel}}.{{method.name | caseCamel}}({%~ for parameter in method.parameters.all | filter((param) => param.required) ~%} - {{parameter.name | escapeKeyword | caseCamel}}: {% if parameter.type == 'object' %}{}{% elseif parameter.type == 'array' %}[]{% elseif parameter.type == 'file' %}Payload.fromPath(path: './image.png'){% elseif parameter.type == 'boolean' %}true{% elseif parameter.type == 'string' %}'{% if parameter.example is not empty %}{{parameter.example | escapeDollarSign}}{% endif %}'{% elseif parameter.type == 'integer' and parameter['x-example'] is empty %}1{% elseif parameter.type == 'number' and parameter['x-example'] is empty %}1.0{% else %}{{parameter.example}}{%~ endif ~%},{%~ endfor ~%} + {{parameter.name | escapeKeyword | caseCamel}}: {% if parameter.type == 'object' %}{}{% elseif parameter.type == 'array' %}[]{% elseif parameter.type == 'file' %}Payload.fromFile(path: './image.png'){% elseif parameter.type == 'boolean' %}true{% elseif parameter.type == 'string' %}'{% if parameter.example is not empty %}{{parameter.example | escapeDollarSign}}{% endif %}'{% elseif parameter.type == 'integer' and parameter['x-example'] is empty %}1{% elseif parameter.type == 'number' and parameter['x-example'] is empty %}1.0{% else %}{{parameter.example}}{%~ endif ~%},{%~ endfor ~%} ); {%- if method.type == 'location' ~%} 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/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig index b8f393fa3..d1faf7b06 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig @@ -341,7 +341,7 @@ class Client @JvmOverloads constructor( if (size < CHUNK_SIZE) { val data = when(input.sourceType) { "file", "path" -> File(input.path).asRequestBody() - "bytes" -> input.toBinary().toRequestBody(input.mimeType?.toMediaType()) + "bytes" -> input.toBinary().toRequestBody() else -> throw UnsupportedOperationException() } params[paramName] = MultipartBody.Part.createFormData( 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..0e7b4ae1e 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 @@ -12,7 +12,6 @@ class Payload private constructor() { lateinit var filename: String lateinit var sourceType: String lateinit var data: Any - var mimeType: String? = null override fun toString(): String { if (sourceType != "bytes") { @@ -62,9 +61,6 @@ class Payload private constructor() { fun fromFileObject(file: File, name: String = "") = Payload().apply { path = file.canonicalPath filename = if (name != "") name else file.name - mimeType = Files.probeContentType(Paths.get(file.canonicalPath)) - ?: URLConnection.guessContentTypeFromName(filename) - ?: "" sourceType = "file" } } diff --git a/templates/swift/Package.swift.twig b/templates/swift/Package.swift.twig index 12b50297f..b2c1d6e22 100644 --- a/templates/swift/Package.swift.twig +++ b/templates/swift/Package.swift.twig @@ -16,14 +16,15 @@ let package = Package( targets: [ "{{spec.title | caseUcfirst}}", "{{spec.title | caseUcfirst}}Enums", - "{{spec.title | caseUcfirst}}Models", - "JSONCodable" + "{{spec.title | caseUcfirst}}Extensions", + "{{spec.title | caseUcfirst}}Models" ] ), ], dependencies: [ .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.19.0"), .package(url: "https://github.com/apple/swift-nio.git", from: "2.58.0"), + .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "4.0.0"), ], targets: [ .target( @@ -31,20 +32,21 @@ let package = Package( dependencies: [ .product(name: "AsyncHTTPClient", package: "async-http-client"), .product(name: "NIOWebSocket", package: "swift-nio"), + .product(name: "Crypto", package: "swift-crypto"), {%~ if spec.definitions is not empty %} "{{spec.title | caseUcfirst}}Models", {%~ endif %} {%~ if spec.enums is not empty %} "{{spec.title | caseUcfirst}}Enums", {%~ endif %} - "JSONCodable" + "{{spec.title | caseUcfirst}}Extensions" ] ), {%~ if spec.definitions is not empty %} .target( name: "{{spec.title | caseUcfirst}}Models", dependencies: [ - "JSONCodable" + "{{spec.title | caseUcfirst}}Extensions" ] ), {%~ endif %} @@ -54,7 +56,10 @@ let package = Package( ), {%~ endif %} .target( - name: "JSONCodable" + name: "{{spec.title | caseUcfirst}}Extensions", + dependencies: [ + .product(name: "AsyncHTTPClient", package: "async-http-client"), + ] ), .testTarget( name: "{{spec.title | caseUcfirst}}Tests", diff --git a/templates/swift/Sources/Client.swift.twig b/templates/swift/Sources/Client.swift.twig index 9a354fd16..24ab19f3a 100644 --- a/templates/swift/Sources/Client.swift.twig +++ b/templates/swift/Sources/Client.swift.twig @@ -287,7 +287,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) } @@ -327,6 +326,19 @@ open class Client { } } + if let contentType = response.headers["content-type"].first, contentType.contains("multipart/form-data") { + let data = try await response.body.collect(upTo: Int.max) + let body = String(buffer: data) + let lines = body.components(separatedBy: .newlines) + + if let boundaryLine = lines.first(where: { $0.hasPrefix("--") }) { + let boundary = boundaryLine.trimmingCharacters(in: .whitespacesAndNewlines) + let dict = parseFormData(with: boundary, for: body) + + return converter?(dict) ?? dict as! T + } + } + switch response.status.code { case 0..<400: switch T.self { @@ -350,7 +362,6 @@ open class Client { do { let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] - message = dict?["message"] as? String ?? response.status.reasonPhrase type = dict?["type"] as? String ?? "" } catch { @@ -374,21 +385,12 @@ open class Client { converter: ((Any) -> T)? = nil, onProgress: ((UploadProgress) -> Void)? = nil ) async throws -> T { - let input = params[paramName] as! InputFile - - switch(input.sourceType) { - case "path": - input.data = ByteBuffer(data: try! Data(contentsOf: URL(fileURLWithPath: input.path))) - case "data": - input.data = ByteBuffer(data: input.data as! Data) - default: - break - } - - let size = (input.data as! ByteBuffer).readableBytes + let payload = params[paramName] as! Payload + let buffer = try payload.toBinary() + let size = buffer.readableBytes if size < Client.chunkSize { - params[paramName] = input + params[paramName] = payload return try await call( method: "POST", path: path, @@ -419,10 +421,14 @@ open class Client { } while offset < size { - let slice = (input.data as! ByteBuffer).getSlice(at: offset, length: Client.chunkSize) - ?? (input.data as! ByteBuffer).getSlice(at: offset, length: Int(size - offset)) + let slice = buffer.getSlice(at: offset, length: Client.chunkSize) + ?? buffer.getSlice(at: offset, length: Int(size - offset)) + + params[paramName] = Payload.fromBinary( + slice!, + filename: payload.filename ?? "" + ) - params[paramName] = InputFile.fromBuffer(slice!, filename: input.filename, mimeType: input.mimeType) headers["content-range"] = "bytes \(offset)-\(min((offset + Client.chunkSize) - 1, size - 1))/\(size)" result = try await call( @@ -495,61 +501,121 @@ open class Client { with params: [String: Any?] = [:], chunked: Bool = false ) { - func addPart(name: String, value: Any) { - bodyBuffer.writeString(DASHDASH) - bodyBuffer.writeString(Client.boundary) - bodyBuffer.writeString(CRLF) - bodyBuffer.writeString("Content-Disposition: form-data; name=\"\(name)\"") - - if let file = value as? InputFile { - bodyBuffer.writeString("; filename=\"\(file.filename)\"") - bodyBuffer.writeString(CRLF) - bodyBuffer.writeString("Content-Length: \(bodyBuffer.readableBytes)") - bodyBuffer.writeString(CRLF+CRLF) - - var buffer = file.data! as! ByteBuffer - - bodyBuffer.writeBuffer(&buffer) - bodyBuffer.writeString(CRLF) - return + func addPart(name: String, value: Any, body: inout ByteBuffer) { + body.writeString(DASHDASH) + body.writeString(Client.boundary) + body.writeString(CRLF) + body.writeString("Content-Disposition: form-data; name=\"\(name)\"") + + switch value { + case is Payload: + let payload = value as! Payload + var buffer = payload.data! as! ByteBuffer + body.writeString("; filename=\"\(payload.filename!)\"") + body.writeString(CRLF) + body.writeString("Content-Length: \(buffer.readableBytes)") + body.writeString(CRLF+CRLF) + body.writeBuffer(&buffer) + body.writeString(CRLF) + case is ByteBuffer: + var buffer = value as! ByteBuffer + body.writeString(CRLF) + body.writeString("Content-Length: \(buffer.readableBytes)") + body.writeString(CRLF+CRLF) + body.writeBuffer(&buffer) + body.writeString(CRLF) + default: + let string = String(describing: value) + body.writeString(CRLF) + body.writeString("Content-Length: \(string.count)") + body.writeString(CRLF+CRLF) + body.writeString(string) + body.writeString(CRLF) } - - let string = String(describing: value) - bodyBuffer.writeString(CRLF) - bodyBuffer.writeString("Content-Length: \(string.count)") - bodyBuffer.writeString(CRLF+CRLF) - bodyBuffer.writeString(string) - bodyBuffer.writeString(CRLF) } - var bodyBuffer = ByteBuffer() + var body = ByteBuffer() for (key, value) in params { - switch key { - case "file": - addPart(name: key, value: value!) - default: - if let list = value as? [Any] { - for listValue in list { - addPart(name: "\(key)[]", value: listValue) - } - continue + if let list = value as? [Any] { + for listValue in list { + addPart(name: "\(key)[]", value: listValue, body: &body) } - addPart(name: key, value: value!) + continue } + addPart(name: key, value: value!, body: &body) } - bodyBuffer.writeString(DASHDASH) - bodyBuffer.writeString(Client.boundary) - bodyBuffer.writeString(DASHDASH) - bodyBuffer.writeString(CRLF) + body.writeString(DASHDASH) + body.writeString(Client.boundary) + body.writeString(DASHDASH) + body.writeString(CRLF) request.headers.remove(name: "content-type") + if !chunked { - request.headers.add(name: "Content-Length", value: bodyBuffer.readableBytes.description) + request.headers.add(name: "Content-Length", value: body.readableBytes.description) } - request.headers.add(name: "Content-Type", value: "multipart/form-data;boundary=\"\(Client.boundary)\"") - request.body = .bytes(bodyBuffer) + + request.headers.add(name: "Content-Type", value: "multipart/form-data; boundary=\"\(Client.boundary)\"") + request.body = .bytes(body) + } + + private func parseFormData(with boundary: String, for body: String) -> [String: Any] { + var data: [String: Any] = [:] + + for part in body.components(separatedBy: boundary) { + var lines = part.components(separatedBy: CRLF).filter { !$0.isEmpty } + + guard let disposition = lines.first(where: { $0.contains("Content-Disposition: form-data;") }) else { + continue; + } + + // Find part name + if let nameStart = disposition.range(of: "name=\""), + let nameEnd = disposition.range(of: "\"", range: nameStart.upperBound.. String { - let lower: String = ext!.lowercased() - if ext != nil && mimeTypes.contains(where: { $0.0 == lower }) { - return mimeTypes[lower]! - } - return DEFAULT_MIME_TYPE -} - -extension NSString { - public func mimeType() -> String { - return mimeFromExt(ext: self.pathExtension) - } -} - -extension String { - public func mimeType() -> String { - return (self as NSString).mimeType() - } -} \ No newline at end of file diff --git a/templates/swift/Sources/Models/Error.swift.twig b/templates/swift/Sources/Models/Error.swift.twig index 046ee2289..2fee0c573 100644 --- a/templates/swift/Sources/Models/Error.swift.twig +++ b/templates/swift/Sources/Models/Error.swift.twig @@ -6,7 +6,7 @@ open class {{ spec.title | caseUcfirst}}Error : Swift.Error, Decodable { public let code: Int? public let type: String? - init(message: String, code: Int? = nil, type: String? = nil) { + public init(message: String, code: Int? = nil, type: String? = nil) { self.message = message self.code = code self.type = type diff --git a/templates/swift/Sources/Models/InputFile.swift.twig b/templates/swift/Sources/Models/InputFile.swift.twig deleted file mode 100644 index cb9a491c9..000000000 --- a/templates/swift/Sources/Models/InputFile.swift.twig +++ /dev/null @@ -1,41 +0,0 @@ -import Foundation -import NIO - -open class InputFile { - - public var path: String = "" - public var filename: String = "" - public var mimeType: String = "" - public var sourceType: String = "" - public var data: Any? = nil - - internal init() { - } - - public static func fromPath(_ path: String) -> InputFile { - let instance = InputFile() - instance.path = path - instance.filename = URL(fileURLWithPath: path, isDirectory: false).lastPathComponent - instance.mimeType = path.mimeType() - instance.sourceType = "path" - return instance - } - - public static func fromData(_ data: Data, filename: String, mimeType: String) -> InputFile { - let instance = InputFile() - instance.filename = filename - instance.mimeType = mimeType - instance.sourceType = "data" - instance.data = data - return instance - } - - public static func fromBuffer(_ buffer: ByteBuffer, filename: String, mimeType: String) -> InputFile { - let instance = InputFile() - instance.filename = filename - instance.mimeType = mimeType - instance.sourceType = "buffer" - instance.data = buffer - return instance - } -} diff --git a/templates/swift/Sources/Models/Model.swift.twig b/templates/swift/Sources/Models/Model.swift.twig index cc3fbe6be..7cdfce1a4 100644 --- a/templates/swift/Sources/Models/Model.swift.twig +++ b/templates/swift/Sources/Models/Model.swift.twig @@ -1,5 +1,5 @@ import Foundation -import JSONCodable +import {{ spec.title }}Extensions /// {{ definition.description }} {% if definition.properties | length == 0 and not definition.additionalProperties %} @@ -20,7 +20,7 @@ public class {{ definition | modelType(spec) | raw }} { init( {%~ for property in definition.properties %} - {{ property.name | escapeSwiftKeyword | removeDollarSign }}: {{ property | propertyType(spec) | raw }}{% if not property.required %}?{% endif %}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {{ property.name | removeDollarSign }}: {{ property | propertyType(spec) | raw }}{% if not property.required %}?{% endif %}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} {%~ endfor %} {%~ if definition.additionalProperties %} @@ -28,7 +28,7 @@ public class {{ definition | modelType(spec) | raw }} { {%~ endif %} ) { {%~ for property in definition.properties %} - self.{{ property.name | escapeSwiftKeyword | removeDollarSign }} = {{ property.name | escapeSwiftKeyword | removeDollarSign }} + self.{{ property.name | removeDollarSign }} = {{ property.name | escapeSwiftKeyword | removeDollarSign }} {%~ endfor %} {%~ if definition.additionalProperties %} self.data = data @@ -38,7 +38,7 @@ public class {{ definition | modelType(spec) | raw }} { public func toMap() -> [String: Any] { return [ {%~ for property in definition.properties %} - "{{ property.name | escapeSwiftKeyword }}": {% 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 }}": self.{% 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 %} {%~ endfor %} {%~ if definition.additionalProperties %} @@ -50,7 +50,7 @@ public class {{ definition | modelType(spec) | raw }} { public static func from(map: [String: Any] ) -> {{ definition.name | caseUcfirst }} { return {{ definition.name | caseUcfirst }}( {%~ for property in definition.properties %} - {{ property.name | escapeSwiftKeyword | removeDollarSign }}: {% if property.sub_schema %}{% if property.type == 'array' %}(map["{{property.name }}"] as! [[String: Any]]).map { {{property.sub_schema | caseUcfirst}}.from(map: $0) }{% else %}{{property.sub_schema | caseUcfirst}}.from(map: map["{{property.name }}"] as! [String: Any]){% endif %}{% else %}map["{{property.name }}"] as{% if property.required %}!{% else %}?{% endif %} {{ property | propertyType(spec) | raw }}{% endif %}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {{ property.name | removeDollarSign }}: {% if property.sub_schema %}{% if property.type == 'array' %}(map["{{property.name }}"] as! [[String: Any]]).map { {{property.sub_schema | caseUcfirst}}.from(map: $0) }{% else %}{{property.sub_schema | caseUcfirst}}.from(map: map["{{property.name }}"] as! [String: Any]){% endif %}{% else %}map["{{property.name }}"] as{% if property.required %}!{% else %}?{% endif %} {{ property | propertyType(spec) | raw }}{% endif %}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} {%~ endfor %} {%~ if definition.additionalProperties %} diff --git a/templates/swift/Sources/Models/Payload.swift.twig b/templates/swift/Sources/Models/Payload.swift.twig new file mode 100644 index 000000000..85edc7797 --- /dev/null +++ b/templates/swift/Sources/Models/Payload.swift.twig @@ -0,0 +1,105 @@ +import Foundation +import NIO +import NIOFoundationCompat +import {{ spec.title | caseUcfirst }}Extensions + +open class Payload { + + enum Error: Swift.Error { + case invalidType(String) + } + + public var path: String? = nil + public var filename: String? = nil + public var data: Any? = nil + + internal init() {} + + public static func fromFile(_ path: String, filename: String? = "") throws -> Payload { + let instance = Payload() + instance.path = path + instance.filename = filename + if instance.filename == "" { + instance.filename = URL(fileURLWithPath: path, isDirectory: false).lastPathComponent + } + instance.data = ByteBuffer(data: try Data(contentsOf: URL(fileURLWithPath: path))) + return instance + } + + public static func fromData(_ data: Data, filename: String? = "") -> Payload { + let instance = Payload() + instance.filename = filename + instance.data = data + return instance + } + + public static func fromBinary(_ buffer: ByteBuffer, filename: String? = "") -> Payload { + let instance = Payload() + instance.filename = filename + instance.data = buffer + return instance + } + + public static func fromString(_ data: String, filename: String? = "") -> Payload { + let instance = Payload() + instance.filename = filename + instance.data = data + return instance + } + + public static func fromJson(_ data: [String: Any?], filename: String? = "") throws -> Payload { + let instance = Payload() + let string = try JSONSerialization.data(withJSONObject: data) + let json = String(data: string, encoding: .utf8) + instance.data = json + return instance + } + + public func toString() throws -> String { + return try convertDataTo(String.self) + } + + public func toData() throws -> Data { + return try convertDataTo(Data.self) + } + + public func toBinary() throws -> ByteBuffer { + return try convertDataTo(ByteBuffer.self) + } + + public func toJson() throws -> [String: Any?] { + guard let stringData = try? toData() else { + throw Error.invalidType("Payload can not be converted to a map") + } + return try JSONSerialization.jsonObject(with: stringData) as! [String: Any?] + } + + private func convertDataTo(_ type: T.Type) throws -> T { + guard let data = self.data else { + throw Error.invalidType("Payload can not be converted to \(T.self)") + } + + switch (T.self, data) { + case (is String.Type, let data as String): + return data as! T + case (is String.Type, let data as Data): + return String(data: data, encoding: .utf8) as! T + case (is String.Type, let data as ByteBuffer): + return String(buffer: data) as! T + case (is Data.Type, let data as String): + return data.data(using: .utf8)! as! T + case (is Data.Type, let data as Data): + return data as! T + case (is Data.Type, let data as ByteBuffer): + return Data(buffer: data) as! T + case (is ByteBuffer.Type, let data as String): + return ByteBuffer(string: data) as! T + case (is ByteBuffer.Type, let data as Data): + return ByteBuffer(data: data) as! T + case (is ByteBuffer.Type, let data as ByteBuffer): + return data as! T + default: + throw Error.invalidType("Payload can not be converted to \(T.self)") + } + } +} diff --git a/templates/swift/Sources/Models/RealtimeModels.swift.twig b/templates/swift/Sources/Models/RealtimeModels.swift.twig index 5129b4cd8..bef7d67ca 100644 --- a/templates/swift/Sources/Models/RealtimeModels.swift.twig +++ b/templates/swift/Sources/Models/RealtimeModels.swift.twig @@ -3,7 +3,7 @@ import Foundation public class RealtimeSubscription { private var close: () async throws -> Void - init(close: @escaping () async throws-> Void) { + public init(close: @escaping () async throws-> Void) { self.close = close } @@ -16,7 +16,7 @@ public class RealtimeCallback { public let channels: Set public let callback: (RealtimeResponseEvent) -> Void - init( + public init( for channels: Set, with callback: @escaping (RealtimeResponseEvent) -> Void ) { @@ -31,7 +31,7 @@ public class RealtimeResponseEvent { public let timestamp: String? public var payload: [String: Any]? - init( + public init( events: [String], channels: [String], timestamp: String, diff --git a/templates/swift/Sources/OAuth/WebAuthComponent.swift.twig b/templates/swift/Sources/OAuth/WebAuthComponent.swift.twig index a146b28be..b1bf46b8c 100644 --- a/templates/swift/Sources/OAuth/WebAuthComponent.swift.twig +++ b/templates/swift/Sources/OAuth/WebAuthComponent.swift.twig @@ -1,10 +1,10 @@ import AsyncHTTPClient import Foundation import NIO - #if canImport(SwiftUI) import SwiftUI #endif +import {{ spec.title | caseUcfirst }}Models /// /// Used to authenticate with external OAuth2 providers. Launches browser windows and handles diff --git a/templates/swift/Sources/Services/Realtime.swift.twig b/templates/swift/Sources/Services/Realtime.swift.twig index 7838fc9fa..3918ac093 100644 --- a/templates/swift/Sources/Services/Realtime.swift.twig +++ b/templates/swift/Sources/Services/Realtime.swift.twig @@ -2,6 +2,7 @@ import Foundation import AsyncHTTPClient import NIO import NIOHTTP1 +import {{ spec.title | caseUcfirst }}Models open class Realtime : Service { diff --git a/templates/swift/Sources/Services/Service.swift.twig b/templates/swift/Sources/Services/Service.swift.twig index c14e9b975..bff3d5156 100644 --- a/templates/swift/Sources/Services/Service.swift.twig +++ b/templates/swift/Sources/Services/Service.swift.twig @@ -1,8 +1,8 @@ import AsyncHTTPClient import Foundation import NIO -import JSONCodable import {{spec.title | caseUcfirst}}Enums +import {{spec.title | caseUcfirst}}Extensions import {{spec.title | caseUcfirst}}Models /// {{ service.description }} @@ -30,7 +30,7 @@ open class {{ service.name | caseUcfirst | overrideIdentifier }}: Service { {%~ if method.responseModel | hasGenericType(spec) %} nestedType: T.Type {%~ endif %} - {%~ if 'multipart/form-data' in method.consumes %} + {%~ if 'multipart/form-data' in method.consumes and method.type == 'upload' %} onProgress: ((UploadProgress) -> Void)? = nil {%~ endif %} ) async throws -> {{ method | returnType(spec) | raw }} { @@ -38,10 +38,13 @@ open class {{ service.name | caseUcfirst | overrideIdentifier }}: Service { {%~ if method.headers | length <= 0 %} let apiHeaders: [String: String] = [:] {%~ else %} - {% if 'multipart/form-data' in method.consumes -%} var + {% if 'multipart/form-data' in method.consumes and method.type == 'upload' -%} var {%- else -%} let {%- endif %} apiHeaders: [String: String] = [ {%~ for key, header in method.headers %} + {%~ if 'multipart/form-data' in method.produces %} + "accept": "multipart/form-data", + {%~ endif %} "{{ key }}": "{{ header }}"{% if not loop.last %},{% endif %} {%~ endfor %} @@ -63,7 +66,7 @@ open class {{ service.name | caseUcfirst | overrideIdentifier }}: Service { } {%~ endif %} - {%~ if 'multipart/form-data' in method.consumes %} + {%~ if 'multipart/form-data' in method.consumes and method.type == 'upload' %} {{~ include('swift/base/requests/file.twig') }} {%~ else %} {{~ include('swift/base/requests/api.twig') }} @@ -90,7 +93,7 @@ open class {{ service.name | caseUcfirst | overrideIdentifier }}: Service { {{ parameter.name | caseCamel | escapeSwiftKeyword }}: {{ parameter | typeName(spec) | raw }}{% if not parameter.required or parameter.nullable %}? = nil{% endif %}{% if not loop.last or 'multipart/form-data' in method.consumes %},{% endif %} {%~ endfor %} - {%~ if 'multipart/form-data' in method.consumes %} + {%~ if 'multipart/form-data' in method.consumes and method.type == 'upload' %} onProgress: ((UploadProgress) -> Void)? = nil {%~ endif %} ) async throws -> {{ method | returnType(spec, '[String: AnyCodable]') | raw }} { @@ -99,7 +102,7 @@ open class {{ service.name | caseUcfirst | overrideIdentifier }}: Service { {{ parameter.name | caseCamel | escapeSwiftKeyword }}: {{ parameter.name | caseCamel | escapeSwiftKeyword }}, {%~ endfor %} nestedType: [String: AnyCodable].self - {%~ if 'multipart/form-data' in method.consumes %} + {%~ if 'multipart/form-data' in method.consumes and method.type == 'upload' %} onProgress: onProgress {%~ endif %} ) diff --git a/templates/swift/base/params.twig b/templates/swift/base/params.twig index 1ca566d79..fd00682aa 100644 --- a/templates/swift/base/params.twig +++ b/templates/swift/base/params.twig @@ -6,11 +6,15 @@ {%~ if method.parameters.query | merge(method.parameters.body) | length <= 0 %} let apiParams: [String: Any] = [:] {%~ else %} - {% if 'multipart/form-data' in method.consumes -%} var + {% if 'multipart/form-data' in method.consumes and method.type == 'upload' -%} var {%- else -%} let {%- endif %} apiParams: [String: Any?] = [ {%~ for parameter in method.parameters.query | merge(method.parameters.body) %} + {%~ if parameter.type == 'payload' %} + "{{ parameter.name }}": try? {{ parameter.name | caseCamel | escapeSwiftKeyword }}?.toBinary(){% if not loop.last or (method.type == 'location' or method.type == 'webAuth' and method.auth | length > 0) %},{% endif %} + {%~ else %} "{{ parameter.name }}": {{ parameter.name | caseCamel | escapeSwiftKeyword }}{% if not loop.last or (method.type == 'location' or method.type == 'webAuth' and method.auth | length > 0) %},{% endif %} + {%~ endif %} {%~ endfor %} {%~ if method.type == 'location' or method.type == 'webAuth' %} diff --git a/templates/swift/example-swiftui/Shared/ExampleViewModel.swift b/templates/swift/example-swiftui/Shared/ExampleViewModel.swift index 3c6de090c..08cb75932 100644 --- a/templates/swift/example-swiftui/Shared/ExampleViewModel.swift +++ b/templates/swift/example-swiftui/Shared/ExampleViewModel.swift @@ -108,10 +108,9 @@ extension ExampleView { let mime = "image/png" #endif - let file = InputFile.fromData( + let file = Payload.fromData( image.data, - filename: fileName, - mimeType: mime + filename: fileName ) do { diff --git a/tests/AppleSwift510Test.php b/tests/AppleSwift510Test.php new file mode 100644 index 000000000..d5a52f129 --- /dev/null +++ b/tests/AppleSwift510Test.php @@ -0,0 +1,35 @@ +