forked from shadowfacts/Tusker
Support JSON request bodies
This commit is contained in:
parent
911e66a159
commit
2c1ba7926e
|
@ -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<RegisteredApplication>) {
|
||||
let request = Request<RegisteredApplication>(method: .post, path: "/api/v1/apps", body: .parameters([
|
||||
let request = Request<RegisteredApplication>(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<LoginSettings>) {
|
||||
let request = Request<LoginSettings>(method: .post, path: "/oauth/token", body: .parameters([
|
||||
let request = Request<LoginSettings>(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<Empty> {
|
||||
return Request<Empty>(method: .post, path: "/api/v1/domain_blocks", body: .parameters([
|
||||
return Request<Empty>(method: .post, path: "/api/v1/domain_blocks", body: ParametersBody([
|
||||
"domain" => domain
|
||||
]))
|
||||
}
|
||||
|
||||
public static func unblock(domain: String) -> Request<Empty> {
|
||||
return Request<Empty>(method: .delete, path: "/api/v1/domain_blocks", body: .parameters([
|
||||
return Request<Empty>(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<Filter> {
|
||||
return Request<Filter>(method: .post, path: "/api/v1/filters", body: .parameters([
|
||||
return Request<Filter>(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<Account> {
|
||||
return Request<Account>(method: .post, path: "/api/v1/follows", body: .parameters(["uri" => acct]))
|
||||
return Request<Account>(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<List> {
|
||||
return Request<List>(method: .post, path: "/api/v1/lists", body: .parameters(["title" => title]))
|
||||
return Request<List>(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<Attachment> {
|
||||
return Request<Attachment>(method: .post, path: "/api/v1/media", body: .formData([
|
||||
return Request<Attachment>(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<Report> {
|
||||
return Request<Report>(method: .post, path: "/api/v1/reports", body: .parameters([
|
||||
return Request<Report>(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<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses", body: .parameters([
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses", body: ParametersBody([
|
||||
"status" => text,
|
||||
"content_type" => contentType.mimeType,
|
||||
"in_reply_to_id" => inReplyTo,
|
||||
|
|
|
@ -115,7 +115,7 @@ public final class Account: AccountProtocol, Decodable {
|
|||
}
|
||||
|
||||
public static func mute(_ account: Account, notifications: Bool? = nil) -> Request<Relationship> {
|
||||
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(account.id)/mute", body: .parameters([
|
||||
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(account.id)/mute", body: ParametersBody([
|
||||
"notifications" => notifications
|
||||
]))
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ public class Attachment: Codable {
|
|||
public let blurHash: String?
|
||||
|
||||
public static func update(_ attachment: Attachment, focus: (Float, Float)?, description: String?) -> Request<Attachment> {
|
||||
return Request<Attachment>(method: .put, path: "/api/v1/media/\(attachment.id)", body: .formData([
|
||||
return Request<Attachment>(method: .put, path: "/api/v1/media/\(attachment.id)", body: FormDataBody([
|
||||
"description" => (description ?? attachment.description),
|
||||
"focus" => focus
|
||||
], nil))
|
||||
|
|
|
@ -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<Filter> {
|
||||
return Request<Filter>(method: .put, path: "/api/v1/filters/\(filter.id)", body: .parameters([
|
||||
return Request<Filter>(method: .put, path: "/api/v1/filters/\(filter.id)", body: ParametersBody([
|
||||
"phrase" => (phrase ?? filter.phrase),
|
||||
"irreversible" => (irreversible ?? filter.irreversible),
|
||||
"whole_word" => (wholeWord ?? filter.wholeWord),
|
||||
|
|
|
@ -31,7 +31,7 @@ public class List: Decodable, Equatable, Hashable {
|
|||
}
|
||||
|
||||
public static func update(_ list: List, title: String) -> Request<List> {
|
||||
return Request<List>(method: .put, path: "/api/v1/lists/\(list.id)", body: .parameters(["title" => title]))
|
||||
return Request<List>(method: .put, path: "/api/v1/lists/\(list.id)", body: ParametersBody(["title" => title]))
|
||||
}
|
||||
|
||||
public static func delete(_ list: List) -> Request<Empty> {
|
||||
|
@ -39,13 +39,13 @@ public class List: Decodable, Equatable, Hashable {
|
|||
}
|
||||
|
||||
public static func add(_ list: List, accounts accountIDs: [String]) -> Request<Empty> {
|
||||
return Request<Empty>(method: .post, path: "/api/v1/lists/\(list.id)/accounts", body: .parameters(
|
||||
return Request<Empty>(method: .post, path: "/api/v1/lists/\(list.id)/accounts", body: ParametersBody(
|
||||
"account_ids" => accountIDs
|
||||
))
|
||||
}
|
||||
|
||||
public static func remove(_ list: List, accounts accountIDs: [String]) -> Request<Empty> {
|
||||
return Request<Empty>(method: .delete, path: "/api/v1/lists/\(list.id)/accounts", body: .parameters(
|
||||
return Request<Empty>(method: .delete, path: "/api/v1/lists/\(list.id)/accounts", body: ParametersBody(
|
||||
"account_ids" => accountIDs
|
||||
))
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ public class Notification: Decodable {
|
|||
}
|
||||
|
||||
public static func dismiss(id notificationID: String) -> Request<Empty> {
|
||||
return Request<Empty>(method: .post, path: "/api/v1/notifications/dismiss", body: .parameters([
|
||||
return Request<Empty>(method: .post, path: "/api/v1/notifications/dismiss", body: ParametersBody([
|
||||
"id" => notificationID
|
||||
]))
|
||||
}
|
||||
|
|
|
@ -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]?
|
||||
|
||||
init(_ parmaeters: [Parameter]?) {
|
||||
self.parameters = parmaeters
|
||||
}
|
||||
|
||||
var mimeType: String? {
|
||||
if parameters == nil || parameters!.isEmpty {
|
||||
return nil
|
||||
}
|
||||
return "application/x-www-form-urlencoded; charset=utf-8"
|
||||
}
|
||||
|
||||
var data: Data? {
|
||||
switch self {
|
||||
case let .parameters(parameters):
|
||||
return parameters?.urlEncoded.data(using: .utf8)
|
||||
case let .formData(parameters, attachment):
|
||||
}
|
||||
}
|
||||
|
||||
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("--\(Body.boundary)\r\n")
|
||||
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("--\(Body.boundary)\r\n")
|
||||
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("--\(Body.boundary)--\r\n")
|
||||
data.append("--\(FormDataBody.boundary)--\r\n")
|
||||
return data
|
||||
case .empty:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct JsonBody<T: Encodable>: Body {
|
||||
let value: T
|
||||
|
||||
init(_ value: T) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
var mimeType: String? { "application/json" }
|
||||
|
||||
var data: Data? { try? Client.encoder.encode(value) }
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ public struct Request<ResultType: Decodable> {
|
|||
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
|
||||
|
|
Loading…
Reference in New Issue