feat: add Toot & User entity
This commit is contained in:
parent
00173e5c30
commit
7ecbcec077
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// Mastodon+API+Error+MastodonAPIError.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.API.Error {
|
||||
public enum MastodonAPIError: Swift.Error {
|
||||
case generic(errorResponse: Mastodon.Response.ErrorResponse)
|
||||
|
||||
init(errorResponse: Mastodon.Response.ErrorResponse) {
|
||||
self = .generic(errorResponse: errorResponse)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// Mastodon+API+Error.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import enum NIOHTTP1.HTTPResponseStatus
|
||||
|
||||
extension Mastodon.API {
|
||||
public struct Error: Swift.Error {
|
||||
|
||||
public var httpResponseStatus: HTTPResponseStatus
|
||||
public var mastodonAPIError: MastodonAPIError?
|
||||
|
||||
init(
|
||||
httpResponseStatus: HTTPResponseStatus,
|
||||
mastodonAPIError: Mastodon.API.Error.MastodonAPIError?
|
||||
) {
|
||||
self.httpResponseStatus = httpResponseStatus
|
||||
self.mastodonAPIError = mastodonAPIError
|
||||
}
|
||||
|
||||
init(
|
||||
httpResponseStatus: HTTPResponseStatus,
|
||||
errorResponse: Mastodon.Response.ErrorResponse
|
||||
) {
|
||||
self.init(
|
||||
httpResponseStatus: httpResponseStatus,
|
||||
mastodonAPIError: MastodonAPIError(errorResponse: errorResponse)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -16,17 +16,27 @@ extension Mastodon.API.App {
|
|||
|
||||
public static func create(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
query: CreateQuery
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.API.App.Application>, Error> {
|
||||
fatalError()
|
||||
let request = Mastodon.API.request(
|
||||
url: appEndpointURL(domain: domain),
|
||||
query: query,
|
||||
authorization: nil
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Application.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API.App {
|
||||
|
||||
struct Application: Codable {
|
||||
public struct Application: Codable {
|
||||
|
||||
public let id: String
|
||||
|
||||
|
@ -49,12 +59,19 @@ extension Mastodon.API.App {
|
|||
}
|
||||
}
|
||||
|
||||
struct CreateQuery {
|
||||
public struct CreateQuery: Codable, PostQuery {
|
||||
public let clientName: String
|
||||
public let redirectURIs: String
|
||||
public let scopes: String?
|
||||
public let website: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case clientName = "client_name"
|
||||
case redirectURIs = "redirect_uris"
|
||||
case scopes
|
||||
case website
|
||||
}
|
||||
|
||||
public init(
|
||||
clientName: String,
|
||||
redirectURIs: String = "urn:ietf:wg:oauth:2.0:oob",
|
||||
|
@ -67,19 +84,8 @@ extension Mastodon.API.App {
|
|||
self.website = website
|
||||
}
|
||||
|
||||
var queryItems: [URLQueryItem]? {
|
||||
var items: [URLQueryItem] = []
|
||||
items.append(URLQueryItem(name: "client_name", value: clientName))
|
||||
items.append(URLQueryItem(name: "redirect_uris", value: redirectURIs))
|
||||
scopes.flatMap {
|
||||
items.append(URLQueryItem(name: "scopes", value: $0))
|
||||
}
|
||||
website.flatMap {
|
||||
items.append(URLQueryItem(name: "website", value: $0))
|
||||
}
|
||||
|
||||
guard !items.isEmpty else { return nil }
|
||||
return items
|
||||
var body: Data? {
|
||||
return try? Mastodon.API.encoder.encode(self)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
//
|
||||
// Mastodon+API+Error.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.API.Error {
|
||||
|
||||
struct ErrorResponse: Codable {
|
||||
let error: String
|
||||
let errorDescription: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case error
|
||||
case errorDescription = "error_description"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// Mastodon+API+OAuth.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.API.OAuth {
|
||||
|
||||
public static let authorizationField = "Authorization"
|
||||
|
||||
public struct Authorization {
|
||||
public let accessToken: String
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
//
|
||||
// Mastodon+API+Timeline.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
extension Mastodon.API.Timeline {
|
||||
|
||||
static func publicTimelineEndpointURL(domain: String) -> URL {
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("timelines/public")
|
||||
}
|
||||
|
||||
public static func create(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
query: PublicTimelineQuery
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
|
||||
let request = Mastodon.API.request(
|
||||
url: publicTimelineEndpointURL(domain: domain),
|
||||
query: query,
|
||||
authorization: nil
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Toot].self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API.Timeline {
|
||||
public struct PublicTimelineQuery: Codable, GetQuery {
|
||||
|
||||
public let local: Bool?
|
||||
public let remote: Bool?
|
||||
public let onlyMedia: Bool?
|
||||
public let maxID: Mastodon.Entity.Toot.ID?
|
||||
public let sinceID: Mastodon.Entity.Toot.ID?
|
||||
public let minID: Mastodon.Entity.Toot.ID?
|
||||
public let limit: Int?
|
||||
|
||||
public init(local: Bool?, remote: Bool?, onlyMedia: Bool?, maxID: Mastodon.Entity.Toot.ID?, sinceID: Mastodon.Entity.Toot.ID?, minID: Mastodon.Entity.Toot.ID?, limit: Int?) {
|
||||
self.local = local
|
||||
self.remote = remote
|
||||
self.onlyMedia = onlyMedia
|
||||
self.maxID = maxID
|
||||
self.sinceID = sinceID
|
||||
self.minID = minID
|
||||
self.limit = limit
|
||||
}
|
||||
|
||||
var queryItems: [URLQueryItem]? {
|
||||
var items: [URLQueryItem] = []
|
||||
local.flatMap {
|
||||
items.append(URLQueryItem(name: "local", value: $0.queryItemValue))
|
||||
}
|
||||
remote.flatMap {
|
||||
items.append(URLQueryItem(name: "remote", value: $0.queryItemValue))
|
||||
}
|
||||
onlyMedia.flatMap {
|
||||
items.append(URLQueryItem(name: "only_media", value: $0.queryItemValue))
|
||||
}
|
||||
maxID.flatMap {
|
||||
items.append(URLQueryItem(name: "max_id", value: $0))
|
||||
}
|
||||
sinceID.flatMap {
|
||||
items.append(URLQueryItem(name: "since_id", value: $0))
|
||||
}
|
||||
minID.flatMap {
|
||||
items.append(URLQueryItem(name: "min_id", value: $0))
|
||||
}
|
||||
limit.flatMap {
|
||||
items.append(URLQueryItem(name: "limit", value: String($0)))
|
||||
}
|
||||
guard !items.isEmpty else { return nil }
|
||||
return items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Bool {
|
||||
var queryItemValue: String {
|
||||
return self ? "true" : "false"
|
||||
}
|
||||
}
|
|
@ -6,36 +6,107 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import NIOHTTP1
|
||||
import enum NIOHTTP1.HTTPResponseStatus
|
||||
|
||||
public extension Mastodon.API {
|
||||
extension Mastodon.API {
|
||||
|
||||
static let timeoutInterval: TimeInterval = 10
|
||||
static let httpHeaderDateFormatter = ISO8601DateFormatter()
|
||||
static let encoder: JSONEncoder = {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.dateEncodingStrategy = .iso8601
|
||||
return encoder
|
||||
}()
|
||||
static let decoder: JSONDecoder = {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
|
||||
return decoder
|
||||
}()
|
||||
static let httpHeaderDateFormatter = ISO8601DateFormatter()
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API {
|
||||
enum Error { }
|
||||
enum App { }
|
||||
}
|
||||
|
||||
extension Mastodon.API {
|
||||
|
||||
static func endpointURL(domain: String) -> URL {
|
||||
return URL(string: "https://" + domain + "/api/v1/")!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API {
|
||||
public enum App { }
|
||||
public enum OAuth { }
|
||||
public enum Timeline { }
|
||||
}
|
||||
|
||||
extension Mastodon.API {
|
||||
|
||||
static func request(
|
||||
url: URL
|
||||
url: URL,
|
||||
query: GetQuery,
|
||||
authorization: OAuth.Authorization?
|
||||
) -> URLRequest {
|
||||
fatalError()
|
||||
var components = URLComponents(string: url.absoluteString)!
|
||||
components.queryItems = query.queryItems
|
||||
|
||||
let requestURL = components.url!
|
||||
var request = URLRequest(
|
||||
url: requestURL,
|
||||
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
|
||||
timeoutInterval: Mastodon.API.timeoutInterval
|
||||
)
|
||||
if let authorization = authorization {
|
||||
request.setValue(
|
||||
"Bearer \(authorization.accessToken)",
|
||||
forHTTPHeaderField: Mastodon.API.OAuth.authorizationField
|
||||
)
|
||||
}
|
||||
request.httpMethod = "GET"
|
||||
return request
|
||||
}
|
||||
|
||||
static func request(
|
||||
url: URL,
|
||||
query: PostQuery,
|
||||
authorization: OAuth.Authorization?
|
||||
) -> URLRequest {
|
||||
let components = URLComponents(string: url.absoluteString)!
|
||||
let requestURL = components.url!
|
||||
var request = URLRequest(
|
||||
url: requestURL,
|
||||
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
|
||||
timeoutInterval: Mastodon.API.timeoutInterval
|
||||
)
|
||||
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = query.body
|
||||
if let authorization = authorization {
|
||||
request.setValue(
|
||||
"Bearer \(authorization.accessToken)",
|
||||
forHTTPHeaderField: Mastodon.API.OAuth.authorizationField
|
||||
)
|
||||
}
|
||||
request.httpMethod = "POST"
|
||||
return request
|
||||
}
|
||||
|
||||
static func decode<T>(type: T.Type, from data: Data, response: URLResponse) throws -> T where T : Decodable {
|
||||
// decode data then decode error if could
|
||||
do {
|
||||
return try Mastodon.API.decoder.decode(type, from: data)
|
||||
} catch let decodeError {
|
||||
#if DEBUG
|
||||
debugPrint(decodeError)
|
||||
#endif
|
||||
|
||||
guard let httpURLResponse = response as? HTTPURLResponse else {
|
||||
assertionFailure()
|
||||
throw decodeError
|
||||
}
|
||||
|
||||
let httpResponseStatus = HTTPResponseStatus(statusCode: httpURLResponse.statusCode)
|
||||
if let errorResponse = try? Mastodon.API.decoder.decode(Mastodon.Response.ErrorResponse.self, from: data) {
|
||||
throw Mastodon.API.Error(httpResponseStatus: httpResponseStatus, errorResponse: errorResponse)
|
||||
}
|
||||
|
||||
throw Mastodon.API.Error(httpResponseStatus: httpResponseStatus, mastodonAPIError: nil)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// Mastodon+Entity+Toot.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
public struct Toot: Codable {
|
||||
|
||||
public typealias ID = String
|
||||
|
||||
public let id: ID
|
||||
|
||||
public let createdAt: Date
|
||||
public let content: String
|
||||
public let account: User
|
||||
|
||||
public let language: String
|
||||
public let visibility: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case createdAt = "created_at"
|
||||
case content
|
||||
case account
|
||||
case language
|
||||
case visibility
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// Mastodon+Entity+User.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
public struct User: Codable {
|
||||
|
||||
public typealias ID = String
|
||||
|
||||
public let id: ID
|
||||
|
||||
public let username: Date
|
||||
public let acct: String
|
||||
public let displayName: String?
|
||||
public let avatar: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case username
|
||||
case acct
|
||||
case displayName = "display_name"
|
||||
case avatar
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// Query.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol GetQuery {
|
||||
var queryItems: [URLQueryItem]? { get }
|
||||
}
|
||||
|
||||
protocol PostQuery {
|
||||
var body: Data? { get }
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// Mastodon+Response+ErrorResponse.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Response {
|
||||
public struct ErrorResponse: Codable {
|
||||
public let error: String
|
||||
public let errorDescription: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case error
|
||||
case errorDescription = "error_description"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,11 +16,19 @@ final class MastodonSDKTests: XCTestCase {
|
|||
clientName: "XCTest",
|
||||
website: nil
|
||||
)
|
||||
Mastodon.API.App.create(session: session, query: query)
|
||||
Mastodon.API.App.create(session: session, domain: domain, query: query)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { completion in
|
||||
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
XCTFail(error.localizedDescription)
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { response in
|
||||
XCTAssertEqual(response.value.name, "XCTest")
|
||||
XCTAssertEqual(response.value.website, nil)
|
||||
XCTAssertEqual(response.value.redirectURI, "urn:ietf:wg:oauth:2.0:oob")
|
||||
theExpectation.fulfill()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
@ -28,6 +36,4 @@ final class MastodonSDKTests: XCTestCase {
|
|||
wait(for: [theExpectation], timeout: 10.0)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue