diff --git a/Sources/APIInterface/include/WordPressComRESTAPIInterfacing.h b/Sources/APIInterface/include/WordPressComRESTAPIInterfacing.h index 1f652fd6..5590ba14 100644 --- a/Sources/APIInterface/include/WordPressComRESTAPIInterfacing.h +++ b/Sources/APIInterface/include/WordPressComRESTAPIInterfacing.h @@ -6,6 +6,22 @@ @property (strong, nonatomic, readonly) NSURL * _Nonnull baseURL; +/// Whether the user's preferred language locale should be appended to the request. +/// Should default to `true`. +/// +/// - SeeAlso: `localeKey` and `localeValue` to configure the locale appendend to the request. +@property (nonatomic, readonly) BOOL appendsPreferredLanguageLocale; + +/// The key with which to specify locale in the parameters of a request. +@property (strong, nonatomic, readonly) NSString * _Nonnull localeKey; + +/// The value with which to specify locale in the parameters of a request. +@property (strong, nonatomic, readonly) NSString * _Nonnull localeValue; + +@property (strong, nonatomic, readonly) NSURLSession * _Nonnull urlSession; + +@property (strong, nonatomic, readonly) void (^ _Nullable invalidTokenHandler)(void); + /// - Note: `parameters` has `id` instead of the more common `NSObject *` as its value type so it will convert to `AnyObject` in Swift. /// In Swift, it's simpler to work with `AnyObject` than with `NSObject`. For example `"abc" as AnyObject` over `"abc" as NSObject`. - (NSProgress * _Nullable)get:(NSString * _Nonnull)URLString diff --git a/Sources/WordPressKit/Models/RemotePostParameters.swift b/Sources/WordPressKit/Models/RemotePostParameters.swift index ba22b6cd..90cda7ba 100644 --- a/Sources/WordPressKit/Models/RemotePostParameters.swift +++ b/Sources/WordPressKit/Models/RemotePostParameters.swift @@ -198,10 +198,14 @@ private enum RemotePostWordPressComCodingKeys: String, CodingKey { static let postTags = "post_tag" } -struct RemotePostCreateParametersWordPressComEncoder: Encodable { +public struct RemotePostCreateParametersWordPressComEncoder: Encodable { let parameters: RemotePostCreateParameters - func encode(to encoder: Encoder) throws { + public init(parameters: RemotePostCreateParameters) { + self.parameters = parameters + } + + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: RemotePostWordPressComCodingKeys.self) try container.encodeIfPresent(parameters.type, forKey: .type) try container.encodeIfPresent(parameters.status, forKey: .status) @@ -281,10 +285,14 @@ struct RemotePostUpdateParametersWordPressComMetadata: Encodable { } } -struct RemotePostUpdateParametersWordPressComEncoder: Encodable { +public struct RemotePostUpdateParametersWordPressComEncoder: Encodable { let parameters: RemotePostUpdateParameters - func encode(to encoder: Encoder) throws { + public init(parameters: RemotePostUpdateParameters) { + self.parameters = parameters + } + + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: RemotePostWordPressComCodingKeys.self) try container.encodeIfPresent(parameters.ifNotModifiedSince, forKey: .ifNotModifiedSince) @@ -348,10 +356,14 @@ private enum RemotePostXMLRPCCodingKeys: String, CodingKey { static let postTags = "post_tag" } -struct RemotePostCreateParametersXMLRPCEncoder: Encodable { +public struct RemotePostCreateParametersXMLRPCEncoder: Encodable { let parameters: RemotePostCreateParameters - func encode(to encoder: Encoder) throws { + public init(parameters: RemotePostCreateParameters) { + self.parameters = parameters + } + + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: RemotePostXMLRPCCodingKeys.self) try container.encode(parameters.type, forKey: .type) try container.encodeIfPresent(parameters.status, forKey: .postStatus) @@ -387,10 +399,14 @@ struct RemotePostCreateParametersXMLRPCEncoder: Encodable { } } -struct RemotePostUpdateParametersXMLRPCEncoder: Encodable { +public struct RemotePostUpdateParametersXMLRPCEncoder: Encodable { let parameters: RemotePostUpdateParameters - func encode(to encoder: Encoder) throws { + public init(parameters: RemotePostUpdateParameters) { + self.parameters = parameters + } + + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: RemotePostXMLRPCCodingKeys.self) try container.encodeIfPresent(parameters.ifNotModifiedSince, forKey: .ifNotModifiedSince) try container.encodeIfPresent(parameters.status, forKey: .postStatus) diff --git a/Sources/WordPressKit/WordPressAPI/DateFormatter+WordPressCom.swift b/Sources/WordPressKit/WordPressAPI/DateFormatter+WordPressCom.swift index 1bb03baf..a20a1775 100644 --- a/Sources/WordPressKit/WordPressAPI/DateFormatter+WordPressCom.swift +++ b/Sources/WordPressKit/WordPressAPI/DateFormatter+WordPressCom.swift @@ -3,7 +3,7 @@ extension DateFormatter { /// A `DateFormatter` configured to manage dates compatible with the WordPress.com API. /// /// - SeeAlso: [https://developer.wordpress.com/docs/api/](https://developer.wordpress.com/docs/api/) - static let wordPressCom: DateFormatter = { + public static let wordPressCom: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ" formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0) as TimeZone diff --git a/Sources/WordPressKit/WordPressAPI/HTTPRequestBuilder.swift b/Sources/WordPressKit/WordPressAPI/HTTPRequestBuilder.swift index c53cf9ca..bb26a907 100644 --- a/Sources/WordPressKit/WordPressAPI/HTTPRequestBuilder.swift +++ b/Sources/WordPressKit/WordPressAPI/HTTPRequestBuilder.swift @@ -5,8 +5,8 @@ import wpxmlrpc /// /// Calling this class's url related functions (the ones that changes path, query, etc) does not modify the /// original URL string. The URL will be perserved in the final result that's returned by the `build` function. -final class HTTPRequestBuilder { - enum Method: String, CaseIterable { +public final class HTTPRequestBuilder { + public enum Method: String, CaseIterable { case get = "GET" case post = "POST" case put = "PUT" diff --git a/Sources/WordPressKit/WordPressAPI/WordPressAPIError.swift b/Sources/WordPressKit/WordPressAPI/WordPressAPIError.swift index a4680b20..508710f1 100644 --- a/Sources/WordPressKit/WordPressAPI/WordPressAPIError.swift +++ b/Sources/WordPressKit/WordPressAPI/WordPressAPIError.swift @@ -18,14 +18,10 @@ public enum WordPressAPIError: Error where EndpointError: Localiz /// The API call returned an status code that's unacceptable to the endpoint. case unacceptableStatusCode(response: HTTPURLResponse, body: Data) /// The API call returned an HTTP response that WordPressKit can't parse. Receiving this error could be an indicator that there is an error response that's not handled properly by WordPressKit. - case unparsableResponse(response: HTTPURLResponse?, body: Data?, underlyingError: Error) + case unparsableResponse(response: HTTPURLResponse?, body: Data?, underlyingError: Error = URLError(.cannotParseResponse)) /// Other error occured. case unknown(underlyingError: Error) - static func unparsableResponse(response: HTTPURLResponse?, body: Data?) -> Self { - return WordPressAPIError.unparsableResponse(response: response, body: body, underlyingError: URLError(.cannotParseResponse)) - } - var response: HTTPURLResponse? { switch self { case .requestEncodingFailure, .connection, .unknown: diff --git a/Sources/WordPressKit/WordPressAPI/WordPressComRestApi.swift b/Sources/WordPressKit/WordPressAPI/WordPressComRestApi.swift index a0b9d605..677834a1 100644 --- a/Sources/WordPressKit/WordPressAPI/WordPressComRestApi.swift +++ b/Sources/WordPressKit/WordPressAPI/WordPressComRestApi.swift @@ -95,11 +95,15 @@ open class WordPressComRestApi: NSObject { private let backgroundUploads: Bool - private let localeKey: String + public let localeKey: String + + public var localeValue: String { + WordPressComLanguageDatabase().deviceLanguage.slug + } @objc public let baseURL: URL - private var invalidTokenHandler: (() -> Void)? + public var invalidTokenHandler: (() -> Void)? /** Configure whether or not the user's preferred language locale should be appended. Defaults to true. @@ -173,10 +177,6 @@ open class WordPressComRestApi: NSObject { } } - @objc func setInvalidTokenHandler(_ handler: @escaping () -> Void) { - invalidTokenHandler = handler - } - // MARK: Network requests /** @@ -306,21 +306,6 @@ open class WordPressComRestApi: NSObject { return "\(String(describing: oAuthToken)),\(String(describing: userAgent))".hashValue } - private func requestBuilder(URLString: String) throws -> HTTPRequestBuilder { - guard let url = URL(string: URLString, relativeTo: baseURL) else { - throw URLError(.badURL) - } - - var builder = HTTPRequestBuilder(url: url) - - if appendsPreferredLanguageLocale { - let preferredLanguageIdentifier = WordPressComLanguageDatabase().deviceLanguage.slug - builder = builder.query(defaults: [URLQueryItem(name: localeKey, value: preferredLanguageIdentifier)]) - } - - return builder - } - @objc public func temporaryFileURL(withExtension fileExtension: String) -> URL { assert(!fileExtension.isEmpty, "file Extension cannot be empty") let fileName = "\(ProcessInfo.processInfo.globallyUniqueString)_file.\(fileExtension)" @@ -330,7 +315,7 @@ open class WordPressComRestApi: NSObject { // MARK: - Async - private lazy var urlSession: URLSession = { + public lazy var urlSession: URLSession = { URLSession(configuration: sessionConfiguration(background: false)) }() @@ -360,15 +345,37 @@ open class WordPressComRestApi: NSObject { return configuration } - func perform( - _ method: HTTPRequestBuilder.Method, + public func upload( URLString: String, - parameters: [String: AnyObject]? = nil, + parameters: [String: AnyObject]?, + fileParts: [FilePart], + requestEnqueued: RequestEnqueuedBlock? = nil, fulfilling progress: Progress? = nil ) async -> APIResult { - await perform(method, URLString: URLString, parameters: parameters, fulfilling: progress) { - try (JSONSerialization.jsonObject(with: $0) as AnyObject) + let builder: HTTPRequestBuilder + do { + let form = try fileParts.map { + try MultipartFormField(fileAtPath: $0.url.path, name: $0.parameterName, filename: $0.fileName, mimeType: $0.mimeType) + } + builder = try requestBuilder(URLString: URLString) + .method(.post) + .body(form: form) + } catch { + return .failure(.requestEncodingFailure(underlyingError: error)) } + + return await perform( + request: builder.query(parameters ?? [:]), + fulfilling: progress, + decoder: { try JSONSerialization.jsonObject(with: $0) as AnyObject }, + session: uploadURLSession, + invalidTokenHandler: invalidTokenHandler, + taskCreated: { taskID in + DispatchQueue.main.async { + requestEnqueued?(NSNumber(value: taskID)) + } + } + ) } func perform( @@ -384,8 +391,25 @@ open class WordPressComRestApi: NSObject { return try decoder.decode(type, from: $0) } } +} + +extension WordPressComRESTAPIInterfacing { + + public typealias APIResult = WordPressAPIResult, WordPressComRestApiEndpointError> + + public func perform( + _ method: HTTPRequestBuilder.Method, + URLString: String, + parameters: [String: AnyObject]? = nil, + fulfilling progress: Progress? = nil + ) async -> APIResult { + await perform(method, URLString: URLString, parameters: parameters, fulfilling: progress) { + try (JSONSerialization.jsonObject(with: $0) as AnyObject) + } + } - private func perform( + // FIXME: This was private. It became public during the extraction. Consider whether to make it privated once done. + public func perform( _ method: HTTPRequestBuilder.Method, URLString: String, parameters: [String: AnyObject]?, @@ -394,8 +418,7 @@ open class WordPressComRestApi: NSObject { ) async -> APIResult { var builder: HTTPRequestBuilder do { - builder = try requestBuilder(URLString: URLString) - .method(method) + builder = try requestBuilder(URLString: URLString).method(method) } catch { return .failure(.requestEncodingFailure(underlyingError: error)) } @@ -408,17 +431,25 @@ open class WordPressComRestApi: NSObject { } } - return await perform(request: builder, fulfilling: progress, decoder: decoder) + return await perform( + request: builder, + fulfilling: progress, + decoder: decoder, + session: urlSession, + invalidTokenHandler: invalidTokenHandler + ) } - private func perform( + // FIXME: This was private. It became public during the extraction. Consider whether to make it privated once done. + public func perform( request: HTTPRequestBuilder, fulfilling progress: Progress?, decoder: @escaping (Data) throws -> T, - taskCreated: ((Int) -> Void)? = nil, - session: URLSession? = nil + session: URLSession, + invalidTokenHandler: (() -> Void)?, + taskCreated: ((Int) -> Void)? = nil ) async -> APIResult { - await (session ?? self.urlSession) + await session .perform(request: request, taskCreated: taskCreated, fulfilling: progress, errorType: WordPressComRestApiEndpointError.self) .mapSuccess { response -> HTTPAPIResponse in let object = try decoder(response.body) @@ -426,7 +457,7 @@ open class WordPressComRestApi: NSObject { return HTTPAPIResponse(response: response.response, body: object) } .mapUnacceptableStatusCodeError { response, body in - if let error = self.processError(response: response, body: body, additionalUserInfo: nil) { + if let error = self.processError(response: response, body: body, additionalUserInfo: nil, invalidTokenHandler: invalidTokenHandler) { return error } @@ -444,45 +475,28 @@ open class WordPressComRestApi: NSObject { } } - public func upload( - URLString: String, - parameters: [String: AnyObject]?, - fileParts: [FilePart], - requestEnqueued: RequestEnqueuedBlock? = nil, - fulfilling progress: Progress? = nil - ) async -> APIResult { - let builder: HTTPRequestBuilder - do { - let form = try fileParts.map { - try MultipartFormField(fileAtPath: $0.url.path, name: $0.parameterName, filename: $0.fileName, mimeType: $0.mimeType) - } - builder = try requestBuilder(URLString: URLString) - .method(.post) - .body(form: form) - } catch { - return .failure(.requestEncodingFailure(underlyingError: error)) + func requestBuilder(URLString: String) throws -> HTTPRequestBuilder { + let locale: (String, String)? + if appendsPreferredLanguageLocale { + locale = (localeKey, localeValue) + } else { + locale = nil } - return await perform( - request: builder.query(parameters ?? [:]), - fulfilling: progress, - decoder: { try JSONSerialization.jsonObject(with: $0) as AnyObject }, - taskCreated: { taskID in - DispatchQueue.main.async { - requestEnqueued?(NSNumber(value: taskID)) - } - }, - session: uploadURLSession - ) + return try HTTPRequestBuilder.with(URLString: URLString, relativeTo: baseURL, appendingLocale: locale) } - } // MARK: - Error processing -extension WordPressComRestApi { +extension WordPressComRESTAPIInterfacing { - func processError(response httpResponse: HTTPURLResponse, body data: Data, additionalUserInfo: [String: Any]?) -> WordPressComRestApiEndpointError? { + func processError( + response httpResponse: HTTPURLResponse, + body data: Data, + additionalUserInfo: [String: Any]?, + invalidTokenHandler: (() -> Void)? + ) -> WordPressComRestApiEndpointError? { // Not sure if it's intentional to include 500 status code, but the code seems to be there from the very beginning. // https://github.com/wordpress-mobile/WordPressKit-iOS/blob/1.0.1/WordPressKit/WordPressComRestApi.swift#L374 guard (400...500).contains(httpResponse.statusCode) else { @@ -528,7 +542,7 @@ extension WordPressComRestApi { if mappedError == .invalidToken { // Call `invalidTokenHandler in the main thread since it's typically used by the apps to present an authentication UI. DispatchQueue.main.async { - self.invalidTokenHandler?() + invalidTokenHandler?() } } @@ -564,6 +578,28 @@ extension WordPressComRestApi { ) } } + +extension HTTPRequestBuilder { + + static func with( + URLString: String, + relativeTo baseURL: URL, + appendingLocale locale: (key: String, value: String)? + ) throws -> HTTPRequestBuilder { + guard let url = URL(string: URLString, relativeTo: baseURL) else { + throw URLError(.badURL) + } + + let builder = Self.init(url: url) + + guard let locale else { + return builder + } + + return builder.query(defaults: [URLQueryItem(name: locale.key, value: locale.value)]) + } + +} // MARK: - Anonymous API support extension WordPressComRestApi { diff --git a/Sources/WordPressKit/WordPressAPI/WordPressOrgXMLRPCApi.swift b/Sources/WordPressKit/WordPressAPI/WordPressOrgXMLRPCApi.swift index 2698c89f..4bb825eb 100644 --- a/Sources/WordPressKit/WordPressAPI/WordPressOrgXMLRPCApi.swift +++ b/Sources/WordPressKit/WordPressAPI/WordPressOrgXMLRPCApi.swift @@ -180,7 +180,7 @@ open class WordPressOrgXMLRPCApi: NSObject { /// - Parameters: /// - streaming: set to `true` if there are large data (i.e. uploading files) in given `parameters`. `false` by default. /// - Returns: A `Result` type that contains the XMLRPC success or failure result. - func call(method: String, parameters: [AnyObject]?, fulfilling progress: Progress? = nil, streaming: Bool = false) async -> WordPressAPIResult, WordPressOrgXMLRPCApiFault> { + public func call(method: String, parameters: [AnyObject]?, fulfilling progress: Progress? = nil, streaming: Bool = false) async -> WordPressAPIResult, WordPressOrgXMLRPCApiFault> { let session = streaming ? uploadURLSession : urlSession let builder = HTTPRequestBuilder(url: endpoint) .method(.post)