From 2c1ba7926e96ca124cdf7c2f90932a2f4f4d6f32 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 14 Sep 2020 23:25:26 -0400 Subject: [PATCH] Support JSON request bodies --- Pachyderm/Client.swift | 36 ++++++---- Pachyderm/Model/Account.swift | 2 +- Pachyderm/Model/Attachment.swift | 2 +- Pachyderm/Model/Filter.swift | 2 +- Pachyderm/Model/List.swift | 6 +- Pachyderm/Model/Notification.swift | 2 +- Pachyderm/Request/Body.swift | 112 ++++++++++++++++++----------- Pachyderm/Request/Request.swift | 2 +- 8 files changed, 100 insertions(+), 64 deletions(-) diff --git a/Pachyderm/Client.swift b/Pachyderm/Client.swift index abc163d5..6fd43c60 100644 --- a/Pachyderm/Client.swift +++ b/Pachyderm/Client.swift @@ -26,7 +26,7 @@ public class Client { public var timeoutInterval: TimeInterval = 60 - lazy var decoder: JSONDecoder = { + static let decoder: JSONDecoder = { let decoder = JSONDecoder() let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ" @@ -36,6 +36,16 @@ public class Client { return decoder }() + static let encoder: JSONEncoder = { + let encoder = JSONEncoder() + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ" + formatter.timeZone = TimeZone(abbreviation: "UTC") + formatter.locale = Locale(identifier: "en_US_POSIX") + encoder.dateEncodingStrategy = .formatted(formatter) + return encoder + }() + public init(baseURL: URL, accessToken: String? = nil, session: URLSession = .shared) { self.baseURL = baseURL self.accessToken = accessToken @@ -59,12 +69,12 @@ public class Client { return } guard response.statusCode == 200 else { - let mastodonError = try? self.decoder.decode(MastodonError.self, from: data) + let mastodonError = try? Client.decoder.decode(MastodonError.self, from: data) let error: Error = mastodonError.flatMap { .mastodonError($0.description) } ?? .unexpectedStatus(response.statusCode) completion(.failure(error)) return } - guard let result = try? self.decoder.decode(Result.self, from: data) else { + guard let result = try? Client.decoder.decode(Result.self, from: data) else { completion(.failure(.invalidModel)) return } @@ -92,7 +102,7 @@ public class Client { // MARK: - Authorization public func registerApp(name: String, redirectURI: String, scopes: [Scope], website: URL? = nil, completion: @escaping Callback) { - let request = Request(method: .post, path: "/api/v1/apps", body: .parameters([ + let request = Request(method: .post, path: "/api/v1/apps", body: ParametersBody([ "client_name" => name, "redirect_uris" => redirectURI, "scopes" => scopes.scopeString, @@ -109,7 +119,7 @@ public class Client { } public func getAccessToken(authorizationCode: String, redirectURI: String, completion: @escaping Callback) { - let request = Request(method: .post, path: "/oauth/token", body: .parameters([ + let request = Request(method: .post, path: "/oauth/token", body: ParametersBody([ "client_id" => clientID, "client_secret" => clientSecret, "grant_type" => "authorization_code", @@ -168,13 +178,13 @@ public class Client { } public static func block(domain: String) -> Request { - return Request(method: .post, path: "/api/v1/domain_blocks", body: .parameters([ + return Request(method: .post, path: "/api/v1/domain_blocks", body: ParametersBody([ "domain" => domain ])) } public static func unblock(domain: String) -> Request { - return Request(method: .delete, path: "/api/v1/domain_blocks", body: .parameters([ + return Request(method: .delete, path: "/api/v1/domain_blocks", body: ParametersBody([ "domain" => domain ])) } @@ -185,7 +195,7 @@ public class Client { } public static func createFilter(phrase: String, context: [Filter.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request { - return Request(method: .post, path: "/api/v1/filters", body: .parameters([ + return Request(method: .post, path: "/api/v1/filters", body: ParametersBody([ "phrase" => phrase, "irreversible" => irreversible, "whole_word" => wholeWord, @@ -209,7 +219,7 @@ public class Client { } public static func followRemote(acct: String) -> Request { - return Request(method: .post, path: "/api/v1/follows", body: .parameters(["uri" => acct])) + return Request(method: .post, path: "/api/v1/follows", body: ParametersBody(["uri" => acct])) } // MARK: - Lists @@ -222,12 +232,12 @@ public class Client { } public static func createList(title: String) -> Request { - return Request(method: .post, path: "/api/v1/lists", body: .parameters(["title" => title])) + return Request(method: .post, path: "/api/v1/lists", body: ParametersBody(["title" => title])) } // MARK: - Media public static func upload(attachment: FormAttachment, description: String? = nil, focus: (Float, Float)? = nil) -> Request { - return Request(method: .post, path: "/api/v1/media", body: .formData([ + return Request(method: .post, path: "/api/v1/media", body: FormDataBody([ "description" => description, "focus" => focus ], attachment)) @@ -259,7 +269,7 @@ public class Client { } public static func report(account: Account, statuses: [Status], comment: String) -> Request { - return Request(method: .post, path: "/api/v1/reports", body: .parameters([ + return Request(method: .post, path: "/api/v1/reports", body: ParametersBody([ "account_id" => account.id, "comment" => comment ] + "status_ids" => statuses.map { $0.id })) @@ -287,7 +297,7 @@ public class Client { spoilerText: String? = nil, visibility: Status.Visibility? = nil, language: String? = nil) -> Request { - return Request(method: .post, path: "/api/v1/statuses", body: .parameters([ + return Request(method: .post, path: "/api/v1/statuses", body: ParametersBody([ "status" => text, "content_type" => contentType.mimeType, "in_reply_to_id" => inReplyTo, diff --git a/Pachyderm/Model/Account.swift b/Pachyderm/Model/Account.swift index 5c0f3bdd..5a7e8499 100644 --- a/Pachyderm/Model/Account.swift +++ b/Pachyderm/Model/Account.swift @@ -115,7 +115,7 @@ public final class Account: AccountProtocol, Decodable { } public static func mute(_ account: Account, notifications: Bool? = nil) -> Request { - return Request(method: .post, path: "/api/v1/accounts/\(account.id)/mute", body: .parameters([ + return Request(method: .post, path: "/api/v1/accounts/\(account.id)/mute", body: ParametersBody([ "notifications" => notifications ])) } diff --git a/Pachyderm/Model/Attachment.swift b/Pachyderm/Model/Attachment.swift index 61964575..216a5df7 100644 --- a/Pachyderm/Model/Attachment.swift +++ b/Pachyderm/Model/Attachment.swift @@ -20,7 +20,7 @@ public class Attachment: Codable { public let blurHash: String? public static func update(_ attachment: Attachment, focus: (Float, Float)?, description: String?) -> Request { - return Request(method: .put, path: "/api/v1/media/\(attachment.id)", body: .formData([ + return Request(method: .put, path: "/api/v1/media/\(attachment.id)", body: FormDataBody([ "description" => (description ?? attachment.description), "focus" => focus ], nil)) diff --git a/Pachyderm/Model/Filter.swift b/Pachyderm/Model/Filter.swift index 7035b474..d67d0011 100644 --- a/Pachyderm/Model/Filter.swift +++ b/Pachyderm/Model/Filter.swift @@ -23,7 +23,7 @@ public class Filter: Decodable { } public static func update(_ filter: Filter, phrase: String? = nil, context: [Context]? = nil, irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request { - return Request(method: .put, path: "/api/v1/filters/\(filter.id)", body: .parameters([ + return Request(method: .put, path: "/api/v1/filters/\(filter.id)", body: ParametersBody([ "phrase" => (phrase ?? filter.phrase), "irreversible" => (irreversible ?? filter.irreversible), "whole_word" => (wholeWord ?? filter.wholeWord), diff --git a/Pachyderm/Model/List.swift b/Pachyderm/Model/List.swift index 4cc33bc9..69c1da52 100644 --- a/Pachyderm/Model/List.swift +++ b/Pachyderm/Model/List.swift @@ -31,7 +31,7 @@ public class List: Decodable, Equatable, Hashable { } public static func update(_ list: List, title: String) -> Request { - return Request(method: .put, path: "/api/v1/lists/\(list.id)", body: .parameters(["title" => title])) + return Request(method: .put, path: "/api/v1/lists/\(list.id)", body: ParametersBody(["title" => title])) } public static func delete(_ list: List) -> Request { @@ -39,13 +39,13 @@ public class List: Decodable, Equatable, Hashable { } public static func add(_ list: List, accounts accountIDs: [String]) -> Request { - return Request(method: .post, path: "/api/v1/lists/\(list.id)/accounts", body: .parameters( + return Request(method: .post, path: "/api/v1/lists/\(list.id)/accounts", body: ParametersBody( "account_ids" => accountIDs )) } public static func remove(_ list: List, accounts accountIDs: [String]) -> Request { - return Request(method: .delete, path: "/api/v1/lists/\(list.id)/accounts", body: .parameters( + return Request(method: .delete, path: "/api/v1/lists/\(list.id)/accounts", body: ParametersBody( "account_ids" => accountIDs )) } diff --git a/Pachyderm/Model/Notification.swift b/Pachyderm/Model/Notification.swift index 080b17f0..fadb3758 100644 --- a/Pachyderm/Model/Notification.swift +++ b/Pachyderm/Model/Notification.swift @@ -34,7 +34,7 @@ public class Notification: Decodable { } public static func dismiss(id notificationID: String) -> Request { - return Request(method: .post, path: "/api/v1/notifications/dismiss", body: .parameters([ + return Request(method: .post, path: "/api/v1/notifications/dismiss", body: ParametersBody([ "id" => notificationID ])) } diff --git a/Pachyderm/Request/Body.swift b/Pachyderm/Request/Body.swift index bb1b74dd..5d0165e1 100644 --- a/Pachyderm/Request/Body.swift +++ b/Pachyderm/Request/Body.swift @@ -8,56 +8,82 @@ import Foundation -enum Body { - case parameters([Parameter]?) - case formData([Parameter]?, FormAttachment?) - case empty +protocol Body { + var mimeType: String? { get } + var data: Data? { get } } -extension Body { - private static let boundary: String = "PachydermBoundary" +struct EmptyBody: Body { + var mimeType: String? { nil } + var data: Data? { nil } +} + +struct ParametersBody: Body { + let parameters: [Parameter]? - var data: Data? { - switch self { - case let .parameters(parameters): - return parameters?.urlEncoded.data(using: .utf8) - case let .formData(parameters, attachment): - var data = Data() - parameters?.forEach { param in - guard let value = param.value else { return } - data.append("--\(Body.boundary)\r\n") - data.append("Content-Disposition: form-data; name=\"\(param.name)\"\r\n\r\n") - data.append("\(value)\r\n") - } - if let attachment = attachment { - data.append("--\(Body.boundary)\r\n") - data.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(attachment.fileName)\"\r\n") - data.append("Content-Type: \(attachment.mimeType)\r\n\r\n") - data.append(attachment.data) - data.append("\r\n") - } - - data.append("--\(Body.boundary)--\r\n") - return data - case .empty: - return nil - } + init(_ parmaeters: [Parameter]?) { + self.parameters = parmaeters } var mimeType: String? { - switch self { - case let .parameters(parameters): - if parameters == nil { - return nil - } - return "application/x-www-form-urlencoded; charset=utf-8" - case let .formData(parameters, attachment): - if parameters == nil && attachment == nil { - return nil - } - return "multipart/form-data; boundary=\(Body.boundary)" - case .empty: + if parameters == nil || parameters!.isEmpty { return nil } + return "application/x-www-form-urlencoded; charset=utf-8" + } + + var data: Data? { + return parameters?.urlEncoded.data(using: .utf8) } } + +struct FormDataBody: Body { + private static let boundary = "PachydermBoundary" + + let parameters: [Parameter]? + let attachment: FormAttachment? + + init(_ parameters: [Parameter]?, _ attachment: FormAttachment?) { + self.parameters = parameters + self.attachment = attachment + } + + var mimeType: String? { + if parameters == nil && attachment == nil { + return nil + } + return "multipart/form-data; boundary=\(FormDataBody.boundary)" + } + + var data: Data? { + var data = Data() + parameters?.forEach { param in + guard let value = param.value else { return } + data.append("--\(FormDataBody.boundary)\r\n") + data.append("Content-Disposition: form-data; name=\"\(param.name)\"\r\n\r\n") + data.append("\(value)\r\n") + } + if let attachment = attachment { + data.append("--\(FormDataBody.boundary)\r\n") + data.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(attachment.fileName)\"\r\n") + data.append("Content-Type: \(attachment.mimeType)\r\n\r\n") + data.append(attachment.data) + data.append("\r\n") + } + + data.append("--\(FormDataBody.boundary)--\r\n") + return data + } +} + +struct JsonBody: Body { + let value: T + + init(_ value: T) { + self.value = value + } + + var mimeType: String? { "application/json" } + + var data: Data? { try? Client.encoder.encode(value) } +} diff --git a/Pachyderm/Request/Request.swift b/Pachyderm/Request/Request.swift index c21aaf67..fe2141e7 100644 --- a/Pachyderm/Request/Request.swift +++ b/Pachyderm/Request/Request.swift @@ -14,7 +14,7 @@ public struct Request { let body: Body var queryParameters: [Parameter] - init(method: Method, path: String, body: Body = .empty, queryParameters: [Parameter] = []) { + init(method: Method, path: String, body: Body = EmptyBody(), queryParameters: [Parameter] = []) { self.method = method self.path = path self.body = body