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 @@
+