Sources/Tea/Tea.swift (332 lines of code) (raw):
import Alamofire
import Foundation
import Swift
#if swift(<5.5)
#error("Tea doesn't support Swift versions below 5.5.")
#endif
open class TeaError: Error {
public var message: String?
public init() {
}
public init(_ msg: String?) {
message = msg
}
public func getMessage() -> String? {
return message
}
}
open class ValidateError: TeaError {
public var data: Any?
public init(_ any: Any?) {
super.init()
data = any
}
public override init(_ msg: String?) {
super.init(msg)
}
}
open class ReuqestError: TeaError {
public var code: String?
public var statusCode: Int?
public var data: [String: Any]?
public var description: String?
public var accessDeniedDetail: [String: Any]?
public init(_ map: [String: Any]?) {
super.init()
message = map?["message"] as? String
code = map?["code"] as? String
description = map?["description"] as? String
accessDeniedDetail = map?["accessDeniedDetail"] as? [String: Any]
if map?["data"] != nil {
data = map?["data"] as? [String: Any]
if data?["statusCode"] != nil {
statusCode = data?["statusCode"] as? Int
}
}
}
public func getCode() -> String? {
return code
}
public func getStatusCode() -> Int? {
return statusCode
}
}
open class RetryableError: TeaError {
public var couse: Error?
public init(_ err: Error?) {
super.init(err?.localizedDescription)
couse = err
}
}
open class UnretryableError: TeaError {
public var request: TeaRequest?
public var couse: Error?
public init(_ req: TeaRequest?, _ err: Error?) {
super.init(err?.localizedDescription)
request = req
couse = err
}
}
open class TeaCore {
private static let bufferLength: Int = 1024
private static let defaultConnectTimeout: Int = 5 * 1000
private static let defaultReadTimeout: Int = 10 * 1000
private static let defaultMaxIdleConnsPerHost : Int = 128
public static func composeUrl(_ request: TeaRequest) -> String {
var url: String = ""
let host: String = request.headers["host"] ?? ""
url = url + request.protocol_.lowercased() + "://" + host
if request.port != 80 {
url = url + ":" + String(request.port)
}
url = url + request.pathname
if request.query.count > 0 {
let query: String = httpQueryString(request.query)
if query.lengthOfBytes(using: .utf8) > 0 {
if url.contains("?") {
if url.last != "&" {
url += "&"
}
url += query
} else {
url += "?" + query
}
}
}
return url
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public static func doAction(_ request: TeaRequest, _ config: URLSessionConfiguration = URLSessionConfiguration.default) async throws -> TeaResponse {
if request.body != nil {
let task = AF.upload(
request.body!,
to: TeaCore.composeUrl(request),
method: HTTPMethod(rawValue: request.method),
headers: HTTPHeaders(request.headers))
.serializingData()
let response = await task.response
return try TeaResponse(response)
} else {
let task = AF.request(
TeaCore.composeUrl(request),
method: HTTPMethod(rawValue: request.method),
headers: HTTPHeaders(request.headers))
.serializingData()
let response = await task.response
return try TeaResponse(response)
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public static func doAction(_ request: TeaRequest, _ runtime: [String: Any]) async throws -> TeaResponse {
let config: URLSessionConfiguration = URLSessionConfiguration.default
var connectTimeout: Int? = runtime["connectTimeout"] as? Int
if connectTimeout == 0 {
connectTimeout = defaultConnectTimeout
}
var readTimeout: Int? = runtime["readTimeout"] as? Int
if readTimeout == 0 {
readTimeout = defaultReadTimeout
}
var maxIdleConns: Int? = runtime["maxIdleConns"] as? Int
if maxIdleConns == 0 {
maxIdleConns = defaultMaxIdleConnsPerHost
}
config.timeoutIntervalForRequest = TimeInterval(connectTimeout ?? defaultConnectTimeout)
config.timeoutIntervalForResource = TimeInterval(readTimeout ?? defaultReadTimeout)
config.httpMaximumConnectionsPerHost = maxIdleConns ?? defaultMaxIdleConnsPerHost
return try await TeaCore.doAction(request, config)
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public static func doAction(_ request: TeaRequest) async throws -> TeaResponse {
let runtime: [String: Any] = [:]
return try await TeaCore.doAction(request, runtime)
}
public static func allowRetry(_ dict: Any?, _ retryTimes: Int32, _ now: Int32) -> Bool {
if(retryTimes < 1){
return true
}
let dic = dict as? [String: Any]
let retryable = dic?["retryable"]
let isNotExists = dic?["maxAttempts"] == nil
if dict == nil || retryable == nil || !(retryable as! Bool) || isNotExists {
return false
}
let maxAttempts: Int = dic?["maxAttempts"] as! Int
return maxAttempts >= retryTimes
}
public static func getBackoffTime(_ dict: Any?, _ retryTimes: Int32) -> Int32 {
var backOffTime: Int32 = 0
let dic = dict as? [String: Any]
let policy: String = dic?["policy"] as! String
if policy == "" || policy.isEmpty || policy == "no" {
return backOffTime
}
let period: String = dic?["period"] as! String
if period != "" {
backOffTime = Int32(period)!
if backOffTime <= 0 {
return retryTimes
}
}
return backOffTime
}
public static func isRetryable(_ e: Error) -> Bool {
return e is RetryableError
}
public static func timeNow() -> Int32 {
return Int32(Date().timeIntervalSince1970)
}
public static func sleep(_ time: Int32) -> Void {
Thread.sleep(forTimeInterval: Double(time))
}
public static func toReadable(_ string: String) -> InputStream {
return toReadable(string.toBytes())
}
public static func toReadable(_ bytes: [UInt8]) -> InputStream {
let data = Data.init(bytes: bytes, count: bytes.count)
return InputStream.init(data: data)
}
}
open class TeaConverter {
public static func merge<T: Any>(_ dict: [String: T]?...) -> [String: T] {
var mergeDict: [String: T] = [:]
for dic in dict {
if (dic != nil) {
for (k, v) in dic! {
mergeDict[k] = v
}
}
}
return mergeDict
}
public static func fromMap<T: TeaModel>(_ model: T, _ dict: [String: Any]) -> T {
model.fromMap(dict)
return model
}
}
open class TeaModel {
public init() { }
open func toMap() -> [String: Any] {
return [:]
}
open func fromMap(_ dict: [String: Any?]?) -> Void { }
open func validate() throws -> Void { }
public func validateRequired(_ prop: Any?, _ name: String) throws -> Void {
if prop == nil {
throw ValidateError("\(name) is required")
}
}
public func validateMaxLength(_ prop: Any?, _ name: String, _ maxLen: Int) throws -> Void {
if prop == nil {
return
}
if prop is String && (prop as! String).count > maxLen{
throw ValidateError("\(name) is exceed max-length: \(maxLen)")
}
if prop is Dictionary<String, Any> && (prop as! Dictionary<String, Any>).count > maxLen{
throw ValidateError("\(name) is exceed max-length: \(maxLen)")
}
if prop is Array<Any> && (prop as! Array<Any>).count > maxLen{
throw ValidateError("\(name) is exceed max-length: \(maxLen)")
}
}
public func validateMinLength(_ prop: Any?, _ name: String, _ minLen: Int) throws -> Void {
if prop == nil {
return
}
if prop is String && (prop as! String).count < minLen{
throw ValidateError("\(name) is less than min-length: \(minLen)")
}
if prop is Dictionary<String, Any> && (prop as! Dictionary<String, Any>).count < minLen{
throw ValidateError("\(name) is less than min-length: \(minLen)")
}
if prop is Array<Any> && (prop as! Array<Any>).count < minLen{
throw ValidateError("\(name) is less than min-length: \(minLen)")
}
}
public func validateMaximum(_ prop: Int?, _ name: String, _ maximum: Int) throws -> Void {
if prop == nil {
return
}
if prop! > maximum {
throw ValidateError("\(name) is greater than the maximum: \(maximum)")
}
}
public func validateMinimum(_ prop: Int?, _ name: String, _ minimum: Int) throws -> Void {
if prop == nil {
return
}
if prop! < minimum {
throw ValidateError("\(name) is less than the minimum: \(minimum)")
}
}
public func validateMaximum(_ prop: Int32?, _ name: String, _ maximum: Int) throws -> Void {
if prop == nil {
return
}
if prop! > maximum {
throw ValidateError("\(name) is greater than the maximum: \(maximum)")
}
}
public func validateMinimum(_ prop: Int32?, _ name: String, _ minimum: Int) throws -> Void {
if prop == nil {
return
}
if prop! < minimum {
throw ValidateError("\(name) is less than the minimum: \(minimum)")
}
}
public func validatePattern(_ prop: String?, _ name: String, _ pattern: String) throws -> Void {
if prop?.range(of: pattern, options: .regularExpression) != nil {
throw ValidateError("\(name) is not match: \(pattern)")
}
}
}
open class TeaRequest {
public var requestType: String = "default"
public var protocol_: String = "http"
public var method: String = "GET"
public var pathname: String = ""
public var headers: [String: String] = [String: String]()
public var query: [String: String] = [String: String]()
public var body: InputStream?
public var port: Int = 80
public init() { }
}
open class TeaResponse {
public var headers: [String: String] = [String: String]()
/// The status code of the response.
public let statusCode: Int32
/// The status message of the response
public let statusMessage: String
/// The response data.
public let body: Data?
/// The original URLRequest for the response.
public let request: URLRequest?
/// The HTTPURLResponse object.
public let response: HTTPURLResponse?
public init(_ res: DataResponse<Data, AFError>?) throws {
if res?.error != nil {
throw RetryableError(res?.error)
}
statusCode = Int32(res?.response?.statusCode ?? 0)
body = res?.data
request = res?.request
response = res?.response
headers = response?.headers.dictionary ?? [:]
statusMessage = res?.debugDescription ?? ""
}
}
func httpQueryString(_ query: [String: Any]) -> String {
var url: String = ""
if query.count > 0 {
let keys = Array(query.keys).sorted()
var arr: [String] = [String]()
for key in keys {
let value = query[key]
if value == nil {
continue
}
arr.append(key.urlEncode() + "=" + "\(value ?? "")".urlEncode())
}
if arr.count > 0 {
url = arr.joined(separator: "&")
}
}
return url
}