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
|
public var timeoutInterval: TimeInterval = 60
|
||||||
|
|
||||||
lazy var decoder: JSONDecoder = {
|
static let decoder: JSONDecoder = {
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
|
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
|
||||||
|
@ -36,6 +36,16 @@ public class Client {
|
||||||
return decoder
|
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) {
|
public init(baseURL: URL, accessToken: String? = nil, session: URLSession = .shared) {
|
||||||
self.baseURL = baseURL
|
self.baseURL = baseURL
|
||||||
self.accessToken = accessToken
|
self.accessToken = accessToken
|
||||||
|
@ -59,12 +69,12 @@ public class Client {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard response.statusCode == 200 else {
|
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)
|
let error: Error = mastodonError.flatMap { .mastodonError($0.description) } ?? .unexpectedStatus(response.statusCode)
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
return
|
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))
|
completion(.failure(.invalidModel))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -92,7 +102,7 @@ public class Client {
|
||||||
|
|
||||||
// MARK: - Authorization
|
// MARK: - Authorization
|
||||||
public func registerApp(name: String, redirectURI: String, scopes: [Scope], website: URL? = nil, completion: @escaping Callback<RegisteredApplication>) {
|
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,
|
"client_name" => name,
|
||||||
"redirect_uris" => redirectURI,
|
"redirect_uris" => redirectURI,
|
||||||
"scopes" => scopes.scopeString,
|
"scopes" => scopes.scopeString,
|
||||||
|
@ -109,7 +119,7 @@ public class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getAccessToken(authorizationCode: String, redirectURI: String, completion: @escaping Callback<LoginSettings>) {
|
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_id" => clientID,
|
||||||
"client_secret" => clientSecret,
|
"client_secret" => clientSecret,
|
||||||
"grant_type" => "authorization_code",
|
"grant_type" => "authorization_code",
|
||||||
|
@ -168,13 +178,13 @@ public class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func block(domain: String) -> Request<Empty> {
|
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
|
"domain" => domain
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func unblock(domain: String) -> Request<Empty> {
|
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
|
"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> {
|
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,
|
"phrase" => phrase,
|
||||||
"irreversible" => irreversible,
|
"irreversible" => irreversible,
|
||||||
"whole_word" => wholeWord,
|
"whole_word" => wholeWord,
|
||||||
|
@ -209,7 +219,7 @@ public class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func followRemote(acct: String) -> Request<Account> {
|
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
|
// MARK: - Lists
|
||||||
|
@ -222,12 +232,12 @@ public class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func createList(title: String) -> Request<List> {
|
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
|
// MARK: - Media
|
||||||
public static func upload(attachment: FormAttachment, description: String? = nil, focus: (Float, Float)? = nil) -> Request<Attachment> {
|
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,
|
"description" => description,
|
||||||
"focus" => focus
|
"focus" => focus
|
||||||
], attachment))
|
], attachment))
|
||||||
|
@ -259,7 +269,7 @@ public class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func report(account: Account, statuses: [Status], comment: String) -> Request<Report> {
|
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,
|
"account_id" => account.id,
|
||||||
"comment" => comment
|
"comment" => comment
|
||||||
] + "status_ids" => statuses.map { $0.id }))
|
] + "status_ids" => statuses.map { $0.id }))
|
||||||
|
@ -287,7 +297,7 @@ public class Client {
|
||||||
spoilerText: String? = nil,
|
spoilerText: String? = nil,
|
||||||
visibility: Status.Visibility? = nil,
|
visibility: Status.Visibility? = nil,
|
||||||
language: String? = nil) -> Request<Status> {
|
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,
|
"status" => text,
|
||||||
"content_type" => contentType.mimeType,
|
"content_type" => contentType.mimeType,
|
||||||
"in_reply_to_id" => inReplyTo,
|
"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> {
|
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
|
"notifications" => notifications
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ public class Attachment: Codable {
|
||||||
public let blurHash: String?
|
public let blurHash: String?
|
||||||
|
|
||||||
public static func update(_ attachment: Attachment, focus: (Float, Float)?, description: String?) -> Request<Attachment> {
|
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),
|
"description" => (description ?? attachment.description),
|
||||||
"focus" => focus
|
"focus" => focus
|
||||||
], nil))
|
], 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> {
|
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),
|
"phrase" => (phrase ?? filter.phrase),
|
||||||
"irreversible" => (irreversible ?? filter.irreversible),
|
"irreversible" => (irreversible ?? filter.irreversible),
|
||||||
"whole_word" => (wholeWord ?? filter.wholeWord),
|
"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> {
|
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> {
|
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> {
|
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
|
"account_ids" => accountIDs
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func remove(_ list: List, accounts accountIDs: [String]) -> Request<Empty> {
|
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
|
"account_ids" => accountIDs
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ public class Notification: Decodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func dismiss(id notificationID: String) -> Request<Empty> {
|
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
|
"id" => notificationID
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,56 +8,82 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum Body {
|
protocol Body {
|
||||||
case parameters([Parameter]?)
|
var mimeType: String? { get }
|
||||||
case formData([Parameter]?, FormAttachment?)
|
var data: Data? { get }
|
||||||
case empty
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Body {
|
struct EmptyBody: Body {
|
||||||
private static let boundary: String = "PachydermBoundary"
|
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? {
|
var data: Data? {
|
||||||
switch self {
|
|
||||||
case let .parameters(parameters):
|
|
||||||
return parameters?.urlEncoded.data(using: .utf8)
|
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()
|
var data = Data()
|
||||||
parameters?.forEach { param in
|
parameters?.forEach { param in
|
||||||
guard let value = param.value else { return }
|
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("Content-Disposition: form-data; name=\"\(param.name)\"\r\n\r\n")
|
||||||
data.append("\(value)\r\n")
|
data.append("\(value)\r\n")
|
||||||
}
|
}
|
||||||
if let attachment = attachment {
|
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-Disposition: form-data; name=\"file\"; filename=\"\(attachment.fileName)\"\r\n")
|
||||||
data.append("Content-Type: \(attachment.mimeType)\r\n\r\n")
|
data.append("Content-Type: \(attachment.mimeType)\r\n\r\n")
|
||||||
data.append(attachment.data)
|
data.append(attachment.data)
|
||||||
data.append("\r\n")
|
data.append("\r\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
data.append("--\(Body.boundary)--\r\n")
|
data.append("--\(FormDataBody.boundary)--\r\n")
|
||||||
return data
|
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
|
let body: Body
|
||||||
var queryParameters: [Parameter]
|
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.method = method
|
||||||
self.path = path
|
self.path = path
|
||||||
self.body = body
|
self.body = body
|
||||||
|
|
Loading…
Reference in New Issue